Files
ProxyIOS/CLAUDE.md
Trey t dc072ad8f5 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.
2026-04-11 12:55:08 -05:00

7.7 KiB

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.

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:<port>. 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:

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:

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).