From 32cfdf7fc8d37fb4fc26be27442f771b693a5f98 Mon Sep 17 00:00:00 2001 From: zy860 Date: Tue, 18 Aug 2026 14:29:19 +0800 Subject: [PATCH] docs: add embedded vpn implementation plan --- .../2026-08-18-embedded-loopback-vpn-plan.md | 447 ++++++++++++++++++ ...2026-08-18-embedded-loopback-vpn-design.md | 6 +- 2 files changed, 452 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/plans/2026-08-18-embedded-loopback-vpn-plan.md diff --git a/docs/superpowers/plans/2026-08-18-embedded-loopback-vpn-plan.md b/docs/superpowers/plans/2026-08-18-embedded-loopback-vpn-plan.md new file mode 100644 index 0000000..2efc5d0 --- /dev/null +++ b/docs/superpowers/plans/2026-08-18-embedded-loopback-vpn-plan.md @@ -0,0 +1,447 @@ +# Embedded Loopback VPN Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Embed a LocalDevVPN-compatible loopback Packet Tunnel in StikDebug, route RPPairing/RSD traffic through it, and remove WiFi as a hard prerequisite without changing the higher-level JIT/debug/location behavior. + +**Architecture:** Add a `StikDebugTunnel` Packet Tunnel Extension with a point-to-point IPv4 route for the configured peer (`10.7.0.1` by default), and manage it from the main app through `NETunnelProviderManager`. Move RPPairing tunnel creation behind an `EmbeddedDeviceTransport` so JIT, heartbeat, syslog, and location all share the same readiness gate. Keep ordinary Internet traffic outside the tunnel and report VPN/RSD failures separately from Internet or DDI-download failures. + +**Tech Stack:** Swift 5, SwiftUI, NetworkExtension, XCTest/Swift Testing, existing `idevice` FFI, Xcode project format. + +## Global Constraints + +- Minimum supported iOS target remains 17.4. +- The embedded tunnel includes only the configured peer route and excludes the default route. +- The default peer remains `10.7.0.1:49152`; the existing custom peer IP setting remains supported. +- No RSD/JIT feature may be gated on `NWPathMonitor(requiredInterfaceType: .wifi)`. +- Existing `tunnel_create_rppairing`/RPPairing/RSD behavior remains the protocol implementation. +- The Packet Tunnel Extension does not implement WLocApp's HTTPS interception or location proxy. +- USB/usbmuxd transport is out of scope. +- Every new pure function receives a failing unit test before its implementation; Xcode project, plist, entitlement, and extension wiring are configuration exceptions. +- Real-device acceptance is required before claiming WiFi-free operation; Windows-side checks cannot prove Network Extension runtime behavior. + +--- + +### Task 1: Add testable tunnel configuration and IPv4 packet primitives + +**Files:** +- Create: `StikJIT/Tunnel/StikDebugTunnelConfiguration.swift` +- Create: `StikJIT/Tunnel/IPv4PacketRewriter.swift` +- Modify: `StikJITTests/StikJITTests.swift` +- Modify: `StikDebug.xcodeproj/project.pbxproj` + +**Interfaces:** +- Produces `StikDebugTunnelConfiguration.default`, `StikDebugTunnelConfiguration(interfaceIP:peerIP:)`, and `providerConfiguration` values for the app and extension. +- Produces `IPv4PacketRewriter.swapEndpoints(in:)` for the Packet Tunnel and unit tests. + +- [ ] **Step 1: Write failing tests for the defaults and packet swap.** + +Add tests to `StikJITTests/StikJITTests.swift`: + +```swift +@Test func embeddedTunnelDefaultsMatchLocalDevVPNEndpoint() { + let configuration = StikDebugTunnelConfiguration.default + + #expect(configuration.interfaceIP == "10.7.0.0") + #expect(configuration.peerIP == "10.7.0.1") + #expect(configuration.peerPrefixLength == 32) + #expect(configuration.providerConfiguration["interfaceIP"] == "10.7.0.0") + #expect(configuration.providerConfiguration["peerIP"] == "10.7.0.1") +} + +@Test func ipv4PacketRewriterSwapsPacketEndpoints() { + var packet = [UInt8](repeating: 0, count: 20) + packet.replaceSubrange(12..<16, with: [10, 7, 0, 2]) + packet.replaceSubrange(16..<20, with: [10, 7, 0, 1]) + + IPv4PacketRewriter.swapEndpoints(in: &packet) + + #expect(Array(packet[12..<16]) == [10, 7, 0, 1]) + #expect(Array(packet[16..<20]) == [10, 7, 0, 2]) +} + +@Test func ipv4PacketRewriterLeavesShortPacketsUnchanged() { + var packet = [UInt8](repeating: 0, count: 19) + let original = packet + + IPv4PacketRewriter.swapEndpoints(in: &packet) + + #expect(packet == original) +} +``` + +- [ ] **Step 2: Run the focused test target and verify it fails because the new types do not exist.** + +Run on macOS/Xcode: + +```bash +xcodebuild -project StikDebug.xcodeproj -scheme StikDebug -destination 'platform=iOS Simulator,name=iPhone 16' -only-testing:StikDebugTests/StikJITTests test +``` + +Expected: compilation failure identifying the missing `StikDebugTunnelConfiguration` and `IPv4PacketRewriter` symbols. + +- [ ] **Step 3: Implement the minimal pure types.** + +`StikDebugTunnelConfiguration` must store `interfaceIP`, `peerIP`, and `peerPrefixLength`, expose the constants `interfaceIPKey = "interfaceIP"` and `peerIPKey = "peerIP"`, and build a `[String: String]` provider dictionary. Its default must be interface `10.7.0.0`, peer `10.7.0.1`, prefix `32`. + +`IPv4PacketRewriter.swapEndpoints` must return without modifying packets shorter than 20 bytes and must swap bytes 12–15 with bytes 16–19 for every IPv4 packet delivered by the peer-only route. It must not parse or rewrite IPv6 packets. This intentionally matches LocalDevVPN's packet loop; the route restriction ensures only loopback peer traffic enters the loop. + +- [ ] **Step 4: Add the files to the main app target and rerun the focused tests.** + +Add both Swift files to the `StikDebug` target in `StikDebug.xcodeproj/project.pbxproj`, then rerun the exact command from Step 2. + +Expected: the three new tests pass and the existing tests remain green. + +- [ ] **Step 5: Commit the tested primitives.** + +```bash +git add StikJIT/Tunnel StikJITTests/StikJITTests.swift StikDebug.xcodeproj/project.pbxproj +git commit -m "feat: add loopback tunnel configuration primitives" +``` + +--- + +### Task 2: Add the `StikDebugTunnel` Packet Tunnel Extension + +**Files:** +- Create: `StikDebugTunnel/PacketTunnelProvider.swift` +- Create: `StikDebugTunnel/Info.plist` +- Create: `StikDebugTunnel/StikDebugTunnel.entitlements` +- Modify: `StikDebug.xcodeproj/project.pbxproj` +- Modify: `StikJIT/Tunnel/StikDebugTunnelConfiguration.swift` + +**Interfaces:** +- Consumes: `StikDebugTunnelConfiguration` and `IPv4PacketRewriter` from Task 1. +- Produces: an extension target with bundle identifier `com.stik.stikdebug.tunnel` and a `PacketTunnelProvider` that configures the peer-only route and packet loop. + +- [ ] **Step 1: Add the extension resource/configuration entries.** + +Create `StikDebugTunnel/Info.plist` with an `NSExtension` dictionary containing: + +```xml +NSExtensionPointIdentifier +com.apple.networkextension.packet-tunnel +NSExtensionPrincipalClass +$(PRODUCT_MODULE_NAME).PacketTunnelProvider +``` + +Create `StikDebugTunnel/StikDebugTunnel.entitlements` with: + +```xml +com.apple.developer.networking.networkextension + + packet-tunnel-provider + +com.apple.security.application-groups + + group.com.stik.stikdebug + +``` + +Add a `PBXNativeTarget` named `StikDebugTunnel` with product type `com.apple.product-type.app-extension`, product name `StikDebugTunnel`, bundle identifier `com.stik.stikdebug.tunnel`, deployment target 17.4, `APPLICATION_EXTENSION_API_ONLY = YES`, the new plist/entitlement paths, and the NetworkExtension framework. Add the target as a dependency of the main `StikDebug` target and add the provider/configuration source files to its source phase. + +- [ ] **Step 2: Implement the provider using the tested packet primitive.** + +`PacketTunnelProvider.startTunnel` must: + +1. Read `interfaceIP` and `peerIP` from `startTunnel(options:)`, falling back to `NETunnelProviderProtocol.providerConfiguration` and then `StikDebugTunnelConfiguration.default`. +2. Configure `NEIPv4Settings(addresses:subnetMasks:)` using the interface IP and `/24` subnet mask. +3. Set `includedRoutes` to one `NEIPv4Route` for the peer IP with subnet mask `255.255.255.255`. +4. Set `excludedRoutes` to `[.default()]`. +5. Call `setTunnelNetworkSettings`; report its error through the completion handler before starting packet reads. +6. Start a recursive `packetFlow.readPackets` loop that calls `IPv4PacketRewriter.swapEndpoints` on each IPv4 packet, writes the packets back, and continues reading. + +`stopTunnel` must cancel the packet loop state and call its completion handler. The provider must not configure DNS, HTTP proxy settings, or a default route. + +- [ ] **Step 3: Run static target checks.** + +Run: + +```powershell +rg -n -S "StikDebugTunnel|packet-tunnel-provider|com.stik.stikdebug.tunnel|StikDebugTunnel.entitlements|StikDebugTunnel/Info.plist" StikDebug.xcodeproj/project.pbxproj StikDebugTunnel +``` + +Expected: one extension target, one packet-tunnel extension point, the correct bundle identifier, both resources, and the Network Extension entitlement are present. + +- [ ] **Step 4: Commit the extension scaffold.** + +```bash +git add StikDebugTunnel StikJIT/Tunnel/StikDebugTunnelConfiguration.swift StikDebug.xcodeproj/project.pbxproj +git commit -m "feat: embed loopback packet tunnel extension" +``` + +--- + +### Task 3: Add the main-app VPN manager and status/error model + +**Files:** +- Create: `StikJIT/Utilities/StikDebugVPNManager.swift` +- Create: `StikJIT/Utilities/StikDebugVPNStatus.swift` +- Modify: `StikJITTests/StikJITTests.swift` +- Modify: `StikDebug.xcodeproj/project.pbxproj` + +**Interfaces:** +- Consumes: `StikDebugTunnelConfiguration` and the `StikDebugTunnel` bundle identifier from Task 1/2. +- Produces: `StikDebugVPNManager.shared.ensureReady() throws`, `.start() throws`, `.stop()`, `currentStatus`, and `StikDebugVPNError` values that distinguish configuration, competing VPN, timeout, and start failures. + +- [ ] **Step 1: Write failing tests for status mapping and provider configuration.** + +Add pure tests: + +```swift +@Test func vpnStatusMappingRecognizesConnectedAndReasserting() { + #expect(StikDebugVPNStatus(neStatus: .connected) == .connected) + #expect(StikDebugVPNStatus(neStatus: .reasserting) == .connecting) + #expect(StikDebugVPNStatus(neStatus: .disconnected) == .disconnected) +} + +@Test func vpnManagerConfigurationUsesEmbeddedExtension() { + let configuration = StikDebugVPNManager.makeProviderConfiguration( + for: .default + ) + + #expect(configuration[StikDebugTunnelConfiguration.interfaceIPKey] == "10.7.0.0") + #expect(configuration[StikDebugTunnelConfiguration.peerIPKey] == "10.7.0.1") + #expect(StikDebugVPNManager.providerBundleIdentifier == "com.stik.stikdebug.tunnel") +} +``` + +- [ ] **Step 2: Run the focused tests and verify they fail because the VPN types do not exist.** + +Run the same focused `xcodebuild` command from Task 1. + +Expected: compilation failure for the missing VPN status and manager symbols. + +- [ ] **Step 3: Implement the pure status/error model.** + +`StikDebugVPNStatus` must map `.invalid` and `.disconnected` to `.disconnected`, `.connecting` and `.reasserting` to `.connecting`, `.connected` to `.connected`, `.disconnecting` to `.disconnecting`, and unknown values to `.failed`. + +`StikDebugVPNError` must provide localized messages for profile creation failure, VPN permission/start failure, a conflicting active VPN, timeout waiting for `.connected`, and a disconnected peer route. It must not mention WiFi. + +- [ ] **Step 4: Implement `StikDebugVPNManager`.** + +Use `NETunnelProviderManager.loadAllFromPreferences` to find only managers whose `NETunnelProviderProtocol.providerBundleIdentifier` equals `com.stik.stikdebug.tunnel`. Create one if absent, set `localizedDescription` to `StikDebug Local Tunnel`, assign the provider configuration, enable it, and save it back to preferences. + +Configure an `NEOnDemandRuleEvaluateConnection` with `interfaceTypeMatch = .any` and a single `NEEvaluateConnectionRule` matching the peer IP. Do not remove or stop managers belonging to other VPNs; throw `StikDebugVPNError.competingVPN` if another manager is connected or connecting. + +`ensureReady()` must load/create the manager, return immediately when its status is `.connected`, start it with the interface/peer options otherwise, and wait up to 10 seconds for `.connected` or a terminal failure using `NEVPNStatusDidChange`. It must be safe to call from the background queue used by `startTunnelInBackground` and must coalesce concurrent starts with a lock/semaphore. + +- [ ] **Step 5: Add the manager to the main app target and rerun tests.** + +Add both files to the `StikDebug` target, run the focused test command, and confirm the status/configuration tests pass. + +- [ ] **Step 6: Commit the VPN manager.** + +```bash +git add StikJIT/Utilities/StikDebugVPNManager.swift StikJIT/Utilities/StikDebugVPNStatus.swift StikJITTests/StikJITTests.swift StikDebug.xcodeproj/project.pbxproj +git commit -m "feat: manage embedded loopback vpn" +``` + +--- + +### Task 4: Move RPPairing creation behind the embedded transport + +**Files:** +- Create: `StikJIT/Utilities/DeviceTransport.swift` +- Modify: `StikJIT/Utilities/JITEnableContext.swift` +- Modify: `StikJIT/Utilities/IdeviceFFIBridge.swift` +- Modify: `StikJITTests/StikJITTests.swift` +- Modify: `StikDebug.xcodeproj/project.pbxproj` + +**Interfaces:** +- Consumes: `StikDebugVPNManager.ensureReady()` and existing `idevice` FFI symbols. +- Produces: `RPPairingTunnel`, `DeviceTransport`, and `EmbeddedDeviceTransport.shared.makeRPPairingTunnel(hostname:targetIPAddress:pairingFileURL:)`. + +- [ ] **Step 1: Write failing tests for transport endpoint validation and error classification.** + +Add pure tests for the new endpoint/error helpers: + +```swift +@Test func transportEndpointUsesRPPairingPort() throws { + let endpoint = try RPPairingEndpoint(ip: "10.7.0.1") + #expect(endpoint.port == 49152) + #expect(endpoint.ip == "10.7.0.1") +} + +@Test func transportEndpointRejectsMalformedIPv4() { + #expect(throws: DeviceTransportError.invalidIPAddress) { + try RPPairingEndpoint(ip: "not-an-ip") + } +} + +@Test func transportErrorsDoNotReportWiFiAsTheRootCause() { + let message = DeviceTransportError.vpnUnavailable("permission denied").errorDescription ?? "" + #expect(!message.localizedCaseInsensitiveContains("wifi")) + #expect(message.localizedCaseInsensitiveContains("vpn")) +} +``` + +- [ ] **Step 2: Run the focused tests and verify they fail because the transport types do not exist.** + +Run the focused `xcodebuild` test command from Task 1. + +Expected: compilation failure for the missing transport types. + +- [ ] **Step 3: Implement endpoint validation and owned tunnel handles.** + +`RPPairingEndpoint` must validate an IPv4 address using `inet_pton`, store the address and port `49152`, and expose a `sockaddr_in` conversion used by the FFI call. + +`RPPairingTunnel` must own optional adapter/handshake handles and free the handshake before the adapter. Its `free()` method must be idempotent. + +`DeviceTransportError` must include invalid IP, pairing file missing/read failure, VPN unavailable, FFI tunnel creation failure, and incomplete handle errors. Its descriptions must identify VPN/RPPairing/RSD layers and must not claim WiFi is required. + +- [ ] **Step 4: Implement `EmbeddedDeviceTransport`.** + +`makeRPPairingTunnel` must: + +1. Call `StikDebugVPNManager.shared.ensureReady()` before touching the RPPairing FFI. +2. Verify the pairing file path exists. +3. Read the pairing file with `rp_pairing_file_read` and always free it with `rp_pairing_file_free`. +4. Build `sockaddr_in` for the validated target IP and port `49152`. +5. Call `tunnel_create_rppairing` with the caller's hostname. +6. Convert FFI errors to `DeviceTransportError.ffiFailure` while freeing the FFI error. +7. Reject and free incomplete adapter/handshake results. + +Keep the FFI implementation in this transport so callers do not duplicate VPN readiness, IP parsing, pairing-file lifetime, or error conversion. + +- [ ] **Step 5: Migrate `JITEnableContext`.** + +Remove its private `TunnelHandles`, `getPairingFile`, and direct `createTunnel` implementation. Use `RPPairingTunnel` and `EmbeddedDeviceTransport.shared.makeRPPairingTunnel` for: + +- the persistent `startTunnel()` handle; +- `withFreshDebugTunnel` used by debug sessions; +- heartbeat creation in `DebugHeartbeatKeepAlive`; +- syslog connection paths that currently call `createTunnel`. + +Preserve the existing adapter/handshake freeing order and concurrency lock. `pubTunnelConnected` remains true only after RPPairing/RSD setup succeeds. + +- [ ] **Step 6: Migrate location simulation.** + +In `simulate_location`, remove the duplicate pairing-file read, port setup, and direct `tunnel_create_rppairing` call. Call the transport with hostname `StikDebugLocation`, `deviceIP`, and the supplied pairing file path. Transfer the returned adapter/handshake into `LocationSimulationState` and leave the existing remote-server/location-simulation error status mapping unchanged. + +- [ ] **Step 7: Rerun tests and commit the transport migration.** + +Run the focused tests and then: + +```bash +git add StikJIT/Utilities/DeviceTransport.swift StikJIT/Utilities/JITEnableContext.swift StikJIT/Utilities/IdeviceFFIBridge.swift StikJITTests/StikJITTests.swift StikDebug.xcodeproj/project.pbxproj +git commit -m "refactor: route RSD connections through embedded vpn" +``` + +--- + +### Task 5: Remove WiFi gating and external LocalDevVPN UI/documentation + +**Files:** +- Modify: `StikJIT/StikJITApp.swift` +- Modify: `StikJIT/Views/SettingsView.swift` +- Modify: `StikJIT/Resources/Localizable.xcstrings` +- Modify: `README.md` +- Modify: `StikDebug.xcodeproj/project.pbxproj` only if new manager files are added to the target here + +**Interfaces:** +- Consumes: `StikDebugVPNManager` and transport errors from Tasks 3–4. +- Produces: user flows that start the embedded VPN automatically and no longer instruct users to install or enable external LocalDevVPN/WiFi. + +- [ ] **Step 1: Write a static regression check before editing the UI/diagnostic code.** + +Use a PowerShell assertion script in the task worktree, without adding it to production sources: + +```powershell +$app = Get-Content -Raw StikJIT\StikJITApp.swift +if ($app -match 'requiredInterfaceType:\s*\.wifi') { throw 'WiFi-only path monitor still blocks the app' } +if ($app -match 'Make sure Wi.?Fi and LocalDevVPN') { throw 'External WiFi/LocalDevVPN error copy still exists' } +``` + +Run it before the production edit and record the expected failure. The failure proves the regression check detects the old behavior. + +- [ ] **Step 2: Remove unused WiFi-only diagnostics and stale connection checks.** + +Confirm with `rg` that `DNSChecker` and `checkDeviceConnection` have no callers. Remove the unused `DNSChecker` class and the unused WiFi/same-network connection helper from `StikJITApp.swift`. Keep `Network` imported only if another helper in that file still uses it; otherwise remove the import. + +Change `startTunnelInBackground` error handling to display the localized transport error. Do not mention WiFi or external LocalDevVPN. Keep DDI download failures separate from tunnel failures. + +- [ ] **Step 3: Replace the Settings external link with embedded VPN status/control.** + +Remove `SettingsLinks.localDevVPN` and the “Download LocalDevVPN” link. Add a section bound to `StikDebugVPNManager.shared` that displays disconnected/connecting/connected/error state and provides Start/Stop actions. Start calls the manager and surfaces its error; Stop calls `stop()`. The existing pairing-file import continues to call `startTunnelInBackground`, which now starts the embedded VPN through the transport. + +- [ ] **Step 4: Update localized strings and README.** + +Remove the `Download LocalDevVPN` localization entry. Add localized strings for “Embedded VPN”, “Start VPN”, “Stop VPN”, “VPN connected”, “VPN disconnected”, “VPN permission required”, and the transport error categories used by the manager. + +Update `README.md` so Requirements list the embedded VPN capability rather than a separately installed LocalDevVPN app, and update the setup/troubleshooting sections to say that WiFi is not a prerequisite. State that Internet is still required for uncached DDI downloads and network-backed content, while cached RSD/JIT traffic is local to the Packet Tunnel. + +- [ ] **Step 5: Rerun the static regression check and commit.** + +Run: + +```powershell +$app = Get-Content -Raw StikJIT\StikJITApp.swift +if ($app -match 'requiredInterfaceType:\s*\.wifi') { throw 'WiFi-only path monitor still blocks the app' } +if ($app -match 'Make sure Wi.?Fi and LocalDevVPN') { throw 'External WiFi/LocalDevVPN error copy still exists' } +rg -n -S "LocalDevVPN|Download LocalDevVPN|\.wifi|requiredInterfaceType" StikJIT README.md +``` + +Expected: no RSD/JIT gating or external setup instruction remains; any remaining `LocalDevVPN` match must be a historical attribution or test/documentation reference explicitly marked as such. + +```bash +git add StikJIT/StikJITApp.swift StikJIT/Views/SettingsView.swift StikJIT/Resources/Localizable.xcstrings README.md +git commit -m "feat: remove wifi and external vpn requirements" +``` + +--- + +### Task 6: Build, test, and validate the project wiring + +**Files:** +- Modify: `StikDebug.xcodeproj/project.pbxproj` only if verification identifies a missing source/build phase entry +- Modify: `StikJITTests/StikJITTests.swift` only for a verified regression test discovered during validation + +**Interfaces:** +- Consumes: all previous tasks. +- Produces: verified project wiring, test results, and a documented real-device acceptance checklist. + +- [ ] **Step 1: Run formatting and repository checks.** + +```powershell +git diff --check +git status --short +rg -n -S "tunnel_create_rppairing|10\.7\.0\.1|49152|NWPathMonitor|requiredInterfaceType|LocalDevVPN|packet-tunnel-provider|com.stik.stikdebug.tunnel" StikJIT StikDebugTunnel StikDebug.xcodeproj README.md +``` + +Confirm that the `tunnel_create_rppairing` implementation call occurs only in `DeviceTransport.swift` (the FFI declaration in `idevice.h` remains), the default peer appears in configuration/tests/UI placeholder as intended, WiFi-only monitoring is absent, and the extension target has the expected Network Extension entries. + +- [ ] **Step 2: Run all unit tests on macOS/Xcode.** + +```bash +xcodebuild -project StikDebug.xcodeproj -scheme StikDebug -destination 'platform=iOS Simulator,name=iPhone 16' test +``` + +Expected: existing tests plus the new configuration, packet, status, endpoint, and error tests pass with exit code 0. + +- [ ] **Step 3: Build the app and extension for a generic iOS device.** + +```bash +xcodebuild -project StikDebug.xcodeproj -scheme StikDebug -destination 'generic/platform=iOS' build CODE_SIGNING_ALLOWED=NO +``` + +If unsigned build is not supported for the extension, run with the configured development team and record the exact entitlement/signing error rather than treating a simulator build as sufficient. + +- [ ] **Step 4: Inspect the built product wiring.** + +Verify the archive/build log contains both `StikDebug.app` and `StikDebugTunnel.appex`, the extension Info.plist packet-tunnel point, the app-group entitlement in both products, and the packet-tunnel entitlement in the extension. Verify the main app declares the embedded extension target dependency. + +- [ ] **Step 5: Execute the physical-device matrix.** + +With a valid pairing file and DDI available, test WiFi-only, cellular-only, both interfaces, and both interfaces disabled while the embedded VPN is connected. Then test VPN denied/stopped, DDI cached offline, DDI uncached with cellular Internet, background/foreground, VPN restart, app discovery, JIT enablement, device info, debug/heartbeat, syslog, and location simulation. + +Record for each case whether the failure is VPN status, peer route/RPPairing, RSD, pairing, or ordinary Internet. Do not claim WiFi-free support unless the cellular-only or offline cases pass on a real device. + +- [ ] **Step 6: Commit only after verification evidence is captured.** + +```bash +git status --short +git log -6 --oneline +``` + +Create the final implementation commit only after the focused tests, full tests, build, and available device checks have been reviewed. If signing or hardware blocks a check, report that blocker explicitly instead of marking the requirement complete. 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 index a745517..353f3aa 100644 --- a/docs/superpowers/specs/2026-08-18-embedded-loopback-vpn-design.md +++ b/docs/superpowers/specs/2026-08-18-embedded-loopback-vpn-design.md @@ -57,7 +57,11 @@ enum DeviceTransportState { protocol DeviceTransport { func ensureReady() throws - func makeRPPairingTunnel(hostname: String) throws -> RPPairingTunnel + func makeRPPairingTunnel( + hostname: String, + targetIPAddress: String, + pairingFileURL: URL + ) throws -> RPPairingTunnel } ```