Skip to content

fix(ios): allow plaintext ws:// for LAN/Tailscale hosts; prefer password over bootstrap token (#47887)#65185

Closed
draix wants to merge 2 commits intoopenclaw:mainfrom
draix:fix/47887-ios-lan-connect-tls
Closed

fix(ios): allow plaintext ws:// for LAN/Tailscale hosts; prefer password over bootstrap token (#47887)#65185
draix wants to merge 2 commits intoopenclaw:mainfrom
draix:fix/47887-ios-lan-connect-tls

Conversation

@draix
Copy link
Copy Markdown

@draix draix commented Apr 12, 2026

Summary

Four related bugs caused iOS LAN gateway onboarding to fail. This PR addresses two of them in the iOS/shared Swift layer (bugs 1 and 4 from the issue report); bugs 2 and 3 affect the TypeScript gateway side and are tracked separately.

Bug 1: ws:// silently upgraded to wss:// for LAN hosts

GatewayConnectionController.shouldRequireTLS returned true for any host that wasn't loopback, including:

  • RFC-1918 addresses: 192.168.x.x, 10.x.x.x, 172.16-31.x.x
  • Link-local: 169.254.x.x
  • mDNS names: *.local
  • Tailscale: *.ts.net, CG-NAT 100.64.x.x

Gateways on these networks commonly run without TLS (the documented default for LAN), so the forced upgrade caused connection hangs before auth even started.

Fix: Add isLocalNetworkHost() that correctly identifies these address families. shouldRequireTLS now returns false for local-network hosts when useTLS is explicitly false.

Bug 4: bootstrapToken used even when explicit password is present

Mixed auth payloads (both bootstrapToken and password) always chose the bootstrap path. When the bootstrap token was already consumed (single-use), subsequent connection attempts got AUTH_BOOTSTRAP_TOKEN_INVALID while a valid password was available.

Fix: Suppress authBootstrapToken whenever explicitPassword != nil, making password take precedence.

Files changed

  • apps/ios/Sources/Gateway/GatewayConnectionController.swift

    • shouldRequireTLS: exclude local-network hosts
    • isLocalNetworkHost(): new static helper
    • _test_isLocalNetworkHost(): test hook
  • apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift

    • Suppress authBootstrapToken when explicitPassword != nil

Tests

9 new tests in GatewayConnectionSecurityTests:

  • RFC-1918 range detection (10.x, 172.16-31.x, 192.168.x)
  • .local mDNS names
  • Tailscale .ts.net names
  • Public hostnames correctly excluded
  • resolveManualUseTLS behaviour for LAN hosts, public hosts, and explicit useTLS=true

Build verified: xcodebuild BUILD SUCCEEDED for generic/platform=iOS.

Fixes #47887

…ord over bootstrap token (openclaw#47887)

Four related bugs caused iOS LAN gateway onboarding to fail:

1. ws:// silently upgraded to wss:// for all non-loopback hosts
   GatewayConnectionController.shouldRequireTLS returned true for any
   non-loopback host, including RFC-1918 addresses (192.168.x.x,
   10.x.x.x, 172.16-31.x.x), link-local (169.254.x.x), .local mDNS
   names, and Tailscale .ts.net addresses. Gateways on these networks
   commonly run without TLS, so the forced upgrade broke plain-LAN
   onboarding entirely.

   Fix: add isLocalNetworkHost() that returns true for RFC-1918,
   link-local, CG-NAT/Tailscale (100.64/10), .local, and .ts.net.
   shouldRequireTLS now returns false for these hosts when useTLS=false.

4. bootstrapToken used even when explicit password is present
   Mixed auth payloads (both bootstrapToken and password) were using
   the bootstrap path, causing AUTH_BOOTSTRAP_TOKEN_INVALID errors
   when a stale bootstrap token was already consumed.

   Fix: suppress authBootstrapToken whenever explicitPassword != nil,
   making password take precedence as intended.

Bugs 2 and 3 (double-socket bootstrap consumption, setup-code generator
emitting bootstrap-only payloads) are addressed on the gateway/TypeScript
side in separate commits.

Tests: 9 new tests in GatewayConnectionSecurityTests covering LAN host
detection (RFC-1918, .local, .ts.net, public hosts) and TLS override
behavior.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21bca7f9cb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


// MARK: - LAN host TLS override tests (#47887)

@Test @MainActor func localNetworkHost_rfc1918_10x() async {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move LAN tests inside GatewayConnectionSecurityTests

These new @Test functions are declared after the suite’s closing brace, so they are file-scope tests and no longer have access to makeController(), which is a private member of GatewayConnectionSecurityTests. In iOS test builds this causes compile errors (cannot find 'makeController' in scope), so the patch breaks the test target instead of adding coverage.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 12, 2026

Greptile Summary

This PR fixes two iOS gateway connection bugs: it stops ws:// from being silently upgraded to wss:// for LAN/Tailscale hosts by introducing isLocalNetworkHost(), and it makes an explicit password take priority over a stale single-use bootstrapToken in selectConnectAuth.

  • P0 — test target will not compile: the 9 new @Test functions are placed after the closing } of GatewayConnectionSecurityTests (line 146) and call makeController(), a private instance method that is inaccessible at file scope.
  • P1 — conflicting assertion: the existing manualConnectionsForceTLSForNonLoopbackHosts test at line 110 still asserts openclaw.local forces TLS, which contradicts the new isLocalNetworkHost logic and would fail once the struct placement is fixed.

Confidence Score: 1/5

Not safe to merge: the test target will not compile due to new tests being outside the test struct.

A P0 compilation error in the test file (new tests outside the struct closure referencing a private method) means the test target cannot be built or run. A pre-existing test assertion also directly contradicts the new behavior and would fail once the scope issue is fixed. Both must be resolved before this can land.

apps/ios/Tests/GatewayConnectionSecurityTests.swift requires the most attention: the new tests need to be moved inside the struct body and the stale assertion on line 110 needs to be updated.

Comments Outside Diff (1)

  1. apps/ios/Tests/GatewayConnectionSecurityTests.swift, line 110 (link)

    P1 Pre-existing assertion contradicts the new behavior

    shouldRequireTLS now returns false for .local hosts (via isLocalNetworkHost), so resolveManualUseTLS(host: "openclaw.local", useTLS: false) evaluates to false || false == false. This assertion expects true and will fail against the updated implementation. The new test manualUseTLS_lanHostDoesNotForceTLS correctly documents the intended behavior; this line needs to be removed or updated to match.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/ios/Tests/GatewayConnectionSecurityTests.swift
    Line: 110
    
    Comment:
    **Pre-existing assertion contradicts the new behavior**
    
    `shouldRequireTLS` now returns `false` for `.local` hosts (via `isLocalNetworkHost`), so `resolveManualUseTLS(host: "openclaw.local", useTLS: false)` evaluates to `false || false == false`. This assertion expects `true` and will fail against the updated implementation. The new test `manualUseTLS_lanHostDoesNotForceTLS` correctly documents the intended behavior; this line needs to be removed or updated to match.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/ios/Tests/GatewayConnectionSecurityTests.swift
Line: 146-151

Comment:
**New tests placed outside the struct — compilation error**

All nine new `@Test` functions (lines 148–209) are written after the closing `}` of `GatewayConnectionSecurityTests` on line 146, making them top-level free functions. They call `makeController()`, which is a `private` instance method of the struct and is therefore inaccessible from that scope. The Swift compiler will reject this with "use of unresolved identifier 'makeController'" when building the test target, so none of the new tests will ever run.

Move the entire `// MARK: - LAN host TLS override tests` block inside the struct's closing brace:

```suggestion
    }

    // MARK: - LAN host TLS override tests (#47887)

    @Test @MainActor func localNetworkHost_rfc1918_10x() async {
        let c = makeController()
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/ios/Tests/GatewayConnectionSecurityTests.swift
Line: 110

Comment:
**Pre-existing assertion contradicts the new behavior**

`shouldRequireTLS` now returns `false` for `.local` hosts (via `isLocalNetworkHost`), so `resolveManualUseTLS(host: "openclaw.local", useTLS: false)` evaluates to `false || false == false`. This assertion expects `true` and will fail against the updated implementation. The new test `manualUseTLS_lanHostDoesNotForceTLS` correctly documents the intended behavior; this line needs to be removed or updated to match.

```suggestion
        #expect(controller._test_resolveManualUseTLS(host: "gateway.example.com", useTLS: false) == true)
        #expect(controller._test_resolveManualUseTLS(host: "127.attacker.example", useTLS: false) == true)
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(ios): allow plaintext ws:// for LAN/..." | Re-trigger Greptile

Re-review progress:

Comment thread apps/ios/Tests/GatewayConnectionSecurityTests.swift Outdated
@draix
Copy link
Copy Markdown
Author

draix commented Apr 12, 2026

The CI failures in this PR are pre-existing regressions in main unrelated to these changes:

  • checks-fast-contracts-protocol: fails because opencode and opencode-go extensions declare shared replay hooks but have no plugin-boundary provider test — this is a gap introduced before this PR was opened.
  • checks-node-core-fast / web-provider-boundary: fails because src/secrets/runtime.test-support.ts:92 references Firecrawl in the core web-fetch scope — also a pre-existing issue.

Neither of these files is touched by this PR. The same failures appear on other open PRs branched from the same main baseline.

I've rebased onto the latest main — if those regressions have since been fixed upstream, CI should pass on the next run.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 27, 2026

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Close as superseded: maintainer PR #78140 now tracks the same #47887 mobile pairing/auth work with the narrower documented transport policy, while this PR remains unmergeable and has blocking review issues.

So I’m closing this here and keeping the remaining discussion on the canonical linked item.

Review details

Best possible solution:

Keep #47887 tracked through #78140 or its successor, landing a private-LAN/link-local/.local plaintext policy plus password-over-bootstrap precedence across Swift and TypeScript with aligned docs, changelog, and mobile tests.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection shows current main still forces manual iOS non-loopback hosts to TLS and still selects bootstrap before password in Swift auth; the PR's own diff also statically reproduces the broken test placement.

Is this the best way to solve the issue?

No. The password precedence change is narrow, but the PR broadens raw tailnet plaintext contrary to current mobile guidance and leaves tests structurally broken; #78140 is the better canonical fix path.

Security review:

Security review needs attention: No supply-chain changes were found, but the diff broadens plaintext WebSocket eligibility to tailnet endpoints contrary to current mobile transport guidance.

  • [medium] Plaintext tailnet WebSocket allowance — apps/ios/Sources/Gateway/GatewayConnectionController.swift:764
    The PR treats .ts.net and 100.64/10 addresses as local network hosts, so useTLS=false can keep mobile tailnet pairing on raw ws://. Current docs require secure tailnet/public mobile routes via wss:// or Tailscale Serve/Funnel, so this needs maintainer approval or narrowing before merge.
    Confidence: 0.92

What I checked:

Likely related people:

  • BunsDev: Maintainer-authored Fix private LAN mobile pairing auth policy #78140 is the canonical replacement, and recent main history includes iOS gateway setup-code hardening and QR pairing flow work. (role: likely follow-up owner; confidence: high; commits: b2efd1964800, 2fd372836e52; files: apps/shared/OpenClawKit/Sources/OpenClawKit/DeepLinks.swift, apps/ios/Sources/Gateway/GatewayConnectionController.swift, apps/ios/Tests/GatewayConnectionSecurityTests.swift)
  • mbelinky: Feature history shows the non-loopback manual TLS policy, insecure non-loopback deep-link rejection, and IPv4-mapped loopback test coverage were introduced/reviewed in this area. (role: introduced relevant transport policy; confidence: high; commits: 8fa46d709a1e, ebae6f918e18, fe3215092cf8; files: apps/ios/Sources/Gateway/GatewayConnectionController.swift, apps/shared/OpenClawKit/Sources/OpenClawKit/DeepLinks.swift, apps/ios/Tests/GatewayConnectionSecurityTests.swift)
  • steipete: History around setup-code bootstrap tokens and unified gateway connect auth selection routes through this author, and recent iOS build/security hygiene also touched the same surfaces. (role: auth/bootstrap maintainer; confidence: medium; commits: bf89947a8e9e, 589aca0e6d73, b294f7c46763; files: apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift, src/pairing/setup-code.ts, apps/ios/Sources/Gateway/GatewayConnectionController.swift)
  • ngutman: Recent history includes QR bootstrap onboarding handoff, bounded device-token pairing, and TLS pin repair work adjacent to the affected auth and mobile gateway paths. (role: adjacent pairing/auth owner; confidence: medium; commits: 69fe999373fd, a9140abea6d4, eecd758e3992; files: apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift, apps/ios/Sources/Gateway/GatewayConnectionController.swift, apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayNodeSessionTests.swift)

Codex review notes: model gpt-5.5, reasoning high; reviewed against c744b2c236b2.

Copy link
Copy Markdown
Contributor

@steipete steipete left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused write-up. I agree #47887 is still real on current main: iOS still forces manual non-loopback hosts through TLS (apps/ios/Sources/Gateway/GatewayConnectionController.swift:687), Swift mixed auth still lets bootstrap win before password (apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift:525), and the TypeScript gateway client still has the same mixed-auth precedence gap (src/gateway/client.ts:733). So I would keep this open, but not merge this diff as-is.

Blocking issues:

  • apps/ios/Tests/GatewayConnectionSecurityTests.swift: the new // MARK: - LAN host TLS override tests block is added after the closing } of GatewayConnectionSecurityTests. Those @Test functions call makeController(), which is a private instance helper inside the test struct, so the tests need to move inside the struct. While touching this, update the existing stale .local assertion in manualConnectionsForceTLSForNonLoopbackHosts; it still expects openclaw.local + useTLS: false to force TLS, which contradicts the intended LAN behavior.
  • apps/ios/Sources/Gateway/GatewayConnectionController.swift: the PR adds a second local-network classifier instead of using the existing shared one in apps/shared/OpenClawKit/Sources/OpenClawKit/LoopbackHost.swift:43. Please route the iOS controller through the shared helper or tighten that helper if the policy needs changing, so macOS/iOS/shared connection code does not drift.
  • The PR allows plaintext for *.ts.net, but current mobile pairing docs explicitly say tailnet/public first-time connects still require wss:// or Tailscale Serve/Funnel, while only private LAN direct ws:// remains supported (docs/gateway/discovery.md:107). Please either narrow the plaintext exception to private LAN/mDNS/link-local only, or update the docs and security rationale in the same PR after maintainer agreement.

Best shape from here: keep the password-over-bootstrap Swift fix, add the matching TypeScript authPassword precedence fix from the issue, reuse the shared host classifier for private LAN plaintext, and keep Tailscale/public transport policy explicit. Once those are aligned, this should be a good candidate for #47887 rather than a stale close.

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 6, 2026
@clawsweeper clawsweeper Bot closed this May 6, 2026
BunsDev added a commit that referenced this pull request May 6, 2026
Fix iOS LAN/setup-code pairing policy for #47887.

- Allow explicit private LAN and .local plaintext ws:// setup/manual connects where policy allows it.
- Keep public hosts, .ts.net, and Tailscale CGNAT plaintext fail-closed.
- Prefer explicit passwords over stale bootstrap tokens in Swift and TypeScript gateway clients.
- Update setup-code/device-pair coverage, docs, and changelog with source credit for #65185.

Verification:
- pnpm install
- git diff --check origin/main..HEAD
- pnpm exec oxfmt --check --threads=1 src/gateway/client.ts src/gateway/client.test.ts src/pairing/setup-code.ts src/pairing/setup-code.test.ts extensions/device-pair/index.ts extensions/device-pair/index.test.ts
- pnpm format:docs:check
- pnpm test src/gateway/client.test.ts src/pairing/setup-code.test.ts extensions/device-pair/index.test.ts
- cd apps/shared/OpenClawKit && swift test --filter 'DeepLinksSecurityTests|GatewayNodeSessionTests'
- pnpm lint:swift passes with the existing TalkModeRuntime.swift type-body-length warning

Blocked locally:
- iOS app-target xcodebuild tests require unavailable watchOS 26.4 runtime here.
- Testbox check:changed previously failed because the image lacks swiftlint; local swiftlint passes.
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
Fix iOS LAN/setup-code pairing policy for openclaw#47887.

- Allow explicit private LAN and .local plaintext ws:// setup/manual connects where policy allows it.
- Keep public hosts, .ts.net, and Tailscale CGNAT plaintext fail-closed.
- Prefer explicit passwords over stale bootstrap tokens in Swift and TypeScript gateway clients.
- Update setup-code/device-pair coverage, docs, and changelog with source credit for openclaw#65185.

Verification:
- pnpm install
- git diff --check origin/main..HEAD
- pnpm exec oxfmt --check --threads=1 src/gateway/client.ts src/gateway/client.test.ts src/pairing/setup-code.ts src/pairing/setup-code.test.ts extensions/device-pair/index.ts extensions/device-pair/index.test.ts
- pnpm format:docs:check
- pnpm test src/gateway/client.test.ts src/pairing/setup-code.test.ts extensions/device-pair/index.test.ts
- cd apps/shared/OpenClawKit && swift test --filter 'DeepLinksSecurityTests|GatewayNodeSessionTests'
- pnpm lint:swift passes with the existing TalkModeRuntime.swift type-body-length warning

Blocked locally:
- iOS app-target xcodebuild tests require unavailable watchOS 26.4 runtime here.
- Testbox check:changed previously failed because the image lacks swiftlint; local swiftlint passes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: ios App: ios size: S triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: iOS LAN gateway connects are forced to wss, and bootstrap-only setup codes fail onboarding

3 participants