# CLAUDE.md Guidance for Claude Code when working in this repository. ## Project **ProxyApp** — on-device iOS HTTP/HTTPS proxy with MITM decryption, inspired by Proxyman/Charles. Runs a SwiftNIO proxy inside a `NEPacketTunnelProvider` so the app can intercept its own device's traffic without a computer. - Min iOS: 17.0 - Swift: 6.0 (PacketTunnel target is Swift 5 — NEPacketTunnelProvider Sendable issues) - Strict concurrency: complete - Device families: iPhone + iPad ## Build The Xcode project is generated by **xcodegen** from `project.yml`. Do not hand-edit `ProxyApp.xcodeproj/project.pbxproj`. ```sh xcodegen generate # regenerate project after editing project.yml or adding files xcodebuild -scheme ProxyApp -destination 'generic/platform=iOS Simulator' build ``` Scheme: `ProxyApp` (builds ProxyApp + PacketTunnel + ProxyCore). ## Targets | Target | Type | Notes | |--------|------|-------| | `ProxyApp` | iOS app | SwiftUI UI, hosts the PacketTunnel extension | | `PacketTunnel` | App extension | `NEPacketTunnelProvider` — sets up proxy network settings, runs ProxyServer | | `ProxyCore` | Static framework (`MACH_O_TYPE: staticlib`) | Shared code used by app + extension | Both processes share `group.com.treyt.proxyapp` App Group for the SQLite DB, CA material, and IPC. ## Layout ``` App/ ProxyApp target (SwiftUI entry + app state) ContentView.swift Adaptive: TabView on iPhone, NavigationSplitView on iPad AppState.swift VPN control, lock state, top-level observable PacketTunnel/ Packet tunnel extension (just wires up ProxyServer) ProxyCore/Sources/ ProxyEngine/ SwiftNIO pipeline ProxyServer.swift ServerBootstrap on 127.0.0.1, HTTP/CONNECT parser ConnectHandler.swift CONNECT tunnel + plain HTTP forwarding + auto-pin fallback MITMHandler.swift SNI sniff → leaf cert → NIOSSLServerHandler → decrypted pipeline HTTPCaptureHandler.swift Parses decrypted HTTP, writes to TrafficRepository GlueHandler.swift Bidirectional channel glue (local <-> remote) CertificateManager.swift Root CA + leaf cert generation/LRU cache RulesEngine.swift Block / SSL-proxy / map-local / DNS spoof / breakpoint matching DataLayer/ Database/DatabaseManager.swift GRDB pool in App Group container Repositories/*Repository.swift All DB access goes through these Models/*.swift GRDB record structs Shared/ IPCManager.swift Darwin notifications between app & extension NotificationThrottle.swift Rate-limits Darwin notifications (0.5s floor) AppGroupPaths.swift Centralized container URLs ProxyLogger.swift os.Logger per subsystem (tunnel/proxy/mitm/cert/db/ipc/ui…) HTTPBodyDecoder.swift gzip/br body decoding CURLParser.swift, WildcardMatcher.swift, SystemTrafficFilter.swift UI/ Home/ Live traffic list (GRDB ValueObservation + Darwin refresh) Pin/ Pinned domains view Compose/ Request composer (curl import, editor, history) More/ Cert install, rule lists, settings, setup guide SharedComponents/ JSONTreeView, HexView, FilterChipsView, badges, etc. ``` ## How the proxy works 1. User enables VPN → `PacketTunnelProvider.startTunnel` is called. 2. Provider sets `NEProxySettings` to route all traffic to `127.0.0.1:`. **No DNS settings** — adding DNS breaks the tunnel. 3. Provider spins up `ProxyServer` (SwiftNIO `ServerBootstrap`) on that port. 4. Each incoming connection goes through: - HTTP parser → plain HTTP: forward via `ConnectHandler`, capture via `HTTPCaptureHandler` - CONNECT → check rules: passthrough (tunnel bytes) or MITM (decrypt) 5. MITM path: sniff SNI from ClientHello → `CertificateManager.tlsServerContext(for:)` → `NIOSSLServerHandler` → decrypted HTTP parser → `HTTPCaptureHandler` → upstream via a second NIO client with `NIOSSLClientHandler`. 6. **Auto-pinning**: If TLS handshake fails on a domain being MITM'd, `TLSErrorLogger` (inside `MITMHandler`) records the domain in `pinned_domains` and next time it goes to passthrough. This is how SSL-pinned apps stop breaking. ## Certificate install flow `CertificateView` runs a tiny `Network.framework` `NWListener` on localhost serving the root CA as `application/x-x509-ca-cert`, then opens Safari to `http://localhost:PORT/ProxyCA.cer`. This triggers the iOS profile installation flow. The CA lives in the App Group container, not Keychain. ## Data flow / IPC - App and extension both open the same GRDB pool at `AppGroupPaths.databaseURL`. - Extension writes traffic → `IPCManager.shared.post(.newTrafficCaptured)` (throttled) → app's `HomeView` observes via `IPCManager.observe(...)` and refreshes its GRDB `ValueObservation`. - Config changes from the app (rules, SSL list, block list) → post `.configurationChanged` → extension reloads `RulesEngine` snapshot. - **Wrap all Darwin notification callbacks in `DispatchQueue.main.async`** before touching `@State`. Not doing this crashes the app. ## Logging Use `ProxyLogger` (os.Logger wrapper) — never `print`. Pick the right subsystem: ```swift ProxyLogger.tunnel.info("...") // PacketTunnelProvider ProxyLogger.proxy.info("...") // ProxyServer setup ProxyLogger.connect.info("...") // ConnectHandler ProxyLogger.mitm.info("...") // MITMHandler ProxyLogger.capture.info("...") // HTTPCaptureHandler ProxyLogger.cert.info("...") // CertificateManager ProxyLogger.rules.info("...") // RulesEngine ProxyLogger.db.info("...") // DB layer ProxyLogger.ipc.info("...") // IPC ProxyLogger.ui.info("...") // SwiftUI ``` View extension logs: `/axiom:console` (xclog) or Console.app filtering on subsystem `com.treyt.proxyapp`. ## Conventions - **All DB access goes through a repository.** No `dbPool.read` in UI code. - **Never `try!` / `!` force-unwrap** in proxy pipeline code — crashes the extension and kills the whole VPN. - **Use `Color.accentColor` / `Color.primary`**, not `.accentColor` (ShapeStyle ambiguity errors). - **iPad adaptation**: check `horizontalSizeClass == .regular` in `ContentView` — do not create iPad-only views, adapt existing ones. - **No force-adding features without being asked.** This codebase already has half-stubbed features (breakpoints, WebSocket capture, image preview toggle) — leave them alone unless the request is about them. - **No print/NSLog.** Use `ProxyLogger`. - **No new files unless needed.** Prefer extending existing repositories/views. ## When adding a source file Add it under the appropriate target's source directory and regenerate: ```sh xcodegen generate ``` `project.yml` uses directory globs — you don't edit it, you just drop the file in `App/`, `UI/`, `ProxyCore/Sources/...`, or `PacketTunnel/`. ## Gotchas - **Clean builds fail with "No such module NIOCore"** → derived data is stale. Delete `~/Library/Developer/Xcode/DerivedData/ProxyApp-*` and rebuild. - **dyld "Library not loaded: ProxyCore.framework"** → ProxyCore must stay `staticlib`; if you change `MACH_O_TYPE`, update the app/extension embed settings. - **VPN starts but no traffic** → check for DNS settings in `PacketTunnelProvider`. There should be none. - **Swift 6 concurrency errors in PacketTunnel** → that target is pinned to Swift 5 on purpose. Don't "upgrade" it. - **`xcuserstate` modified in every commit** → it's now gitignored; if it shows up again, re-add the pattern. ## Airline research project There is a separate project at `~/Desktop/code/flights` that uses Frida hooks to capture airline API traffic. It is **not** part of this repo. The proxy app is the user-facing version of the same idea (MITM instead of Frida).