From db4b808d43c4d586ac05ba3e576a2de3230c121e Mon Sep 17 00:00:00 2001 From: zy860 Date: Tue, 18 Aug 2026 14:24:01 +0800 Subject: [PATCH] docs: design embedded loopback vpn --- ...2026-08-18-embedded-loopback-vpn-design.md | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-18-embedded-loopback-vpn-design.md diff --git a/docs/superpowers/specs/2026-08-18-embedded-loopback-vpn-design.md b/docs/superpowers/specs/2026-08-18-embedded-loopback-vpn-design.md new file mode 100644 index 0000000..a745517 --- /dev/null +++ b/docs/superpowers/specs/2026-08-18-embedded-loopback-vpn-design.md @@ -0,0 +1,169 @@ +# Embedded Loopback VPN Design + +## Goal + +Remove StikDebug's dependency on the separately installed LocalDevVPN app by embedding an equivalent local Network Extension Packet Tunnel, while removing WiFi as a hard prerequisite and preserving the existing RPPairing/RSD feature set. + +## Context and constraints + +StikDebug currently creates RPPairing tunnels to `DeviceConnectionContext.targetIPAddress` on port `49152`. The default address is `10.7.0.1`, which is supplied by the external LocalDevVPN virtual interface. JIT, app discovery, device information, debugging, syslog, and location simulation all ultimately depend on this path. + +The existing app contains WiFi-only DNS diagnostics and user-facing WiFi error text, but the actual RPPairing FFI calls do not inspect WiFi. The replacement must therefore validate tunnel readiness and RSD connectivity rather than interface type. Internet-dependent DDI downloads remain separate from the local RSD transport. + +The reference implementation is the LocalDevVPN Packet Tunnel design: configure a point-to-point IPv4 interface, include only the peer route (`10.7.0.1/32` by default), exclude the default route, and exchange packets through `NEPacketTunnelProvider.packetFlow`. WLocApp is used only as a reference for target layout, VPN manager lifecycle, App Group sharing, and entitlements; its location proxy is out of scope. + +## Approved approach + +### 1. Add an embedded Packet Tunnel target + +Add a `StikDebugTunnel` app extension target to `StikDebug.xcodeproj` with: + +- `PacketTunnelProvider` subclassing `NEPacketTunnelProvider`. +- A small constants/configuration module shared with the main app. +- Extension `Info.plist` declaring `com.apple.networkextension.packet-tunnel`. +- Packet Tunnel Network Extension entitlement. +- The existing app group entitlement shared by the main app and extension. + +The provider accepts interface and peer IP values from `NETunnelProviderProtocol.providerConfiguration` and/or `startVPNTunnel(options:)`. It configures an IPv4 interface, includes only the peer IP route, excludes the default route, then swaps the source and destination IPv4 addresses for packets read from `packetFlow` before writing them back. This reproduces the LocalDevVPN loopback behavior without carrying ordinary Internet traffic through StikDebug. + +The implementation will be an equivalent implementation rather than copying unrelated WLocApp proxy code. If LocalDevVPN source code is reused verbatim, its license and attribution must be preserved; StikDebug is already AGPL-3.0. + +### 2. Add main-app VPN lifecycle management + +Add `StikDebugVPNManager` in the main app. It owns one `NETunnelProviderManager` identified by the embedded extension bundle identifier and provides: + +- `prepare()` to load or create the saved VPN profile. +- `start()` to save/reload the profile and call `startVPNTunnel(options:)`. +- `stop()` to stop the active tunnel. +- `status` derived from `NEVPNStatus`. +- `isTunnelReady()` for non-UI callers. + +The manager uses `providerConfiguration` for the interface IP and peer IP, defaults to interface `10.7.0.0` and peer `10.7.0.1`, and configures an on-demand rule only for the peer address. It must not require WiFi or enable a full-device default route. Existing VPNs are not silently destroyed; if another VPN is active, the manager reports a clear conflict and waits for the user to stop it. + +The app group is reserved for shared tunnel configuration and diagnostics. Pairing files remain in the existing app-owned store unless the extension later needs them; the Packet Tunnel itself does not read pairing material. + +### 3. Gate the RSD layer on transport readiness + +Keep `tunnel_create_rppairing` as the RSD transport primitive. Before calling it, `JITEnableContext` ensures the embedded VPN profile is prepared and active. The FFI call still targets the configured peer IP and port `49152`. + +The transport readiness abstraction will expose: + +```swift +enum DeviceTransportState { + case unavailable(String) + case starting + case ready +} + +protocol DeviceTransport { + func ensureReady() throws + func makeRPPairingTunnel(hostname: String) throws -> RPPairingTunnel +} +``` + +The concrete implementation wraps `StikDebugVPNManager` plus the existing FFI tunnel creation. `JITEnableContext` remains responsible for lifecycle and FFI handle ownership, while callers no longer need to know whether the external or embedded VPN is present. + +All direct RPPairing entry points must use this path, including: + +- the main RSD/debug session; +- debug heartbeat tunnels; +- syslog relay tunnels; +- location simulation in `IdeviceFFIBridge.swift`. + +### 4. Replace WiFi diagnostics with transport diagnostics + +Remove WiFi from the connection prerequisite and error copy. The old `DNSChecker` will either be removed if unused or reduced to an explicit Internet diagnostics utility that reports DNS/Internet status without blocking RSD. + +Connection errors will distinguish: + +- VPN profile missing or permission denied; +- VPN starting or disconnected; +- peer route/RPPairing connection failed; +- invalid or expired pairing file; +- RSD service handshake failure; +- ordinary Internet unavailable for DDI/news/map requests. + +No code path will call `NWPathMonitor(requiredInterfaceType: .wifi)` to decide whether JIT, debugging, syslog, or location simulation may proceed. + +### 5. Preserve existing user flows + +The existing start-tunnel call sites continue to call `startTunnelInBackground`, but that function delegates to the embedded VPN manager before establishing RSD. Settings may expose the embedded VPN status and a stop control, while the existing custom peer IP setting remains supported. + +The README and localized strings will describe the built-in VPN, remove the external LocalDevVPN installation step, and explain that Internet is only needed when downloading uncached DDI files or using network-backed content. + +## Data flow + +```text +User action / background reconnect + | + v +StikDebugVPNManager.prepare/start + | + v +StikDebugTunnel PacketTunnelProvider + 10.7.0.0 interface, route 10.7.0.1/32 + | + v +JITEnableContext.ensureReady + | + v +tunnel_create_rppairing(10.7.0.1:49152) + | + v +RSD services: apps, device info, debug proxy, syslog, location +``` + +The tunnel includes only the peer route, so ordinary network requests continue to use WiFi or cellular according to iOS routing. A cached-DDI JIT session should not need an Internet connection; a first-time DDI download still does. + +## Error handling and lifecycle + +- VPN profile creation and permission errors are surfaced before FFI tunnel creation. +- Concurrent start requests are coalesced by the VPN manager and `JITEnableContext`. +- A disconnected/reasserting VPN invalidates cached RSD handles; the next operation recreates the tunnel. +- Stopping or losing the VPN frees RSD/adapter handles and sets the published connection state to disconnected. +- Background reconnect attempts are allowed to start the embedded profile, but UI permission prompts remain user-driven. +- No automatic default-route VPN behavior is added. + +## Testing strategy + +### Unit tests + +Test pure configuration and state behavior without Network Extension runtime: + +- default interface/peer values; +- custom peer IP propagation; +- peer route is `/32` and default route is excluded; +- WiFi interface status cannot block transport readiness; +- error mapping preserves invalid pairing versus tunnel unavailable; +- repeated `ensureReady()` does not create duplicate starts. + +### Build/static checks + +- Verify the main app and extension targets are present in the Xcode project. +- Verify both entitlements contain the required Network Extension and App Group entries. +- Verify the extension Info.plist has the packet-tunnel extension point. +- Verify no RSD/JIT call site requires `NWPathMonitor(.wifi)` or references external LocalDevVPN installation. +- Run the available Swift test/build commands on the development machine. + +### Real-device acceptance matrix + +Test on a physical supported iOS device with a valid pairing file: + +1. WiFi on, cellular off, embedded VPN on. +2. WiFi off, cellular on, embedded VPN on. +3. WiFi and cellular both off, embedded VPN on. +4. WiFi and cellular both on. +5. VPN stopped or permission denied. +6. DDI cached with no Internet. +7. DDI missing with cellular-only Internet. +8. Background/foreground transition and VPN restart. + +Acceptance requires app discovery, JIT enablement, device info, debug session/heartbeat, syslog, and location simulation to continue using the same pairing and RSD behavior. + +## Risks and non-goals + +- Network Extension entitlement and distribution/signing support may be unavailable for some sideload profiles. The project will expose signing failures clearly, but cannot solve Apple entitlement policy in code. +- The packet swap loop is intentionally limited to the LocalDevVPN-compatible peer route; this is not a general-purpose VPN or Internet proxy. +- USB/usbmuxd transport is not part of this change. +- WLocApp's HTTPS interception, certificate installation, DNS proxy, and location response mutation are not part of this change. +- The final no-WiFi result cannot be claimed until the real-device matrix is executed.