From dc072ad8f5a03a8a6648c186bc9dfa8936051009 Mon Sep 17 00:00:00 2001 From: Trey t Date: Sat, 11 Apr 2026 12:55:08 -0500 Subject: [PATCH] Add CLAUDE.md and README CLAUDE.md documents architecture, build flow, proxy pipeline, IPC/logging conventions, and gotchas for future Claude sessions. README gives a user-facing overview, setup steps, and stack. --- CLAUDE.md | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 73 ++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 CLAUDE.md create mode 100644 README.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bdaa2ec --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,139 @@ +# 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). diff --git a/README.md b/README.md new file mode 100644 index 0000000..d6642f2 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# Proxy + +An on-device iOS HTTP/HTTPS proxy with MITM decryption. Inspect every request your phone makes, from your phone. No computer required. + +Think Proxyman or Charles, but running entirely on-device through a Network Extension. + +## Features + +- **Live traffic capture** — see every request/response grouped by domain as it happens +- **TLS decryption** — per-domain leaf certs signed by an on-device root CA, with LRU caching +- **Auto-pinning detection** — domains that fail TLS (SSL-pinned apps) fall back to passthrough automatically and get listed in the Pinned tab +- **Rules** — block list, SSL-proxying allow list, Map Local, DNS spoofing, breakpoints +- **Compose** — craft arbitrary HTTP requests, import from curl, replay captured requests +- **Rich viewers** — JSON tree, hex view, headers, gzip/brotli decoding +- **App lock** — Face ID / Touch ID gate +- **iPhone + iPad** — adaptive layout with sidebar on iPad, tabs on iPhone + +## How it works + +``` + App traffic + │ + ▼ + NEPacketTunnelProvider ──────► NEProxySettings → 127.0.0.1:PORT + │ + ▼ + SwiftNIO ProxyServer (in the extension) + │ + ┌─────────────────┼─────────────────┐ + ▼ ▼ ▼ + Plain HTTP CONNECT + CONNECT + + (capture) MITM decrypt passthrough + (leaf cert) (pinned domains) +``` + +The packet tunnel doesn't route IP — it just installs a local proxy redirect in the system network settings. All traffic hits the in-process SwiftNIO server, where it's parsed, captured into SQLite (via GRDB), and forwarded upstream. TLS is decrypted by generating a leaf cert per SNI, signed by a root CA the user installs into their device trust store. + +## Setup + +1. Open in Xcode 26.3+ (iOS 17 SDK or later) +2. Run `xcodegen generate` to build the project file +3. Set your `DEVELOPMENT_TEAM` in `project.yml` or Xcode signing +4. Build & run the `ProxyApp` scheme on a device (packet tunnel extensions don't fully work in the simulator) +5. Inside the app, tap **More → Install Certificate**, follow the Safari prompt to install the profile, then go to **Settings → General → About → Certificate Trust Settings** and enable full trust for `ProxyCA` +6. Tap **Start Proxy** on the home screen — iOS will prompt for VPN permission +7. Browse. Watch the traffic roll in. + +## Project structure + +- `App/` — SwiftUI entry point, `AppState`, adaptive `ContentView` +- `PacketTunnel/` — `NEPacketTunnelProvider` extension +- `ProxyCore/` — static framework with the NIO pipeline, GRDB data layer, and shared utilities +- `UI/` — SwiftUI screens (Home, Pin, Compose, More) +- `project.yml` — xcodegen config (edit this, not the `.xcodeproj`) + +See [CLAUDE.md](CLAUDE.md) for architecture details, conventions, and gotchas. + +## Stack + +- SwiftUI (iOS 17+) +- SwiftNIO / SwiftNIOSSL / SwiftNIOExtras +- swift-certificates / swift-crypto (CA + leaf cert generation) +- GRDB (SQLite in the App Group container) +- Network Extension (`NEPacketTunnelProvider`) +- os.Logger for structured logging across app + extension + +## Status + +Personal project. Not App Store–distributed (Network Extensions with `packet-tunnel-provider` require a provisioning profile). + +## License + +Private.