← Back to PRs

#21256: fix: treat ws:// to Tailscale addresses as secure when bind=tailnet

by jessewunderlich open 2026-02-19 21:04 View on GitHub →
gateway size: S
## Problem The CWE-319 security check added in 2026.2.19 blocks **all** `ws://` connections to non-loopback addresses. This is correct for general network traffic, but breaks users who set `gateway.bind: "tailnet"`. Tailscale connections (100.64.0.0/10 CGNAT range) are encrypted at the network layer via WireGuard (ChaCha20-Poly1305). The plaintext `ws://` is only at the application layer — the actual network traffic is always encrypted. This is materially different from plain LAN or public internet `ws://` which truly are cleartext. ### Impact After upgrading to 2026.2.19, users with `gateway.bind: "tailnet"` cannot use: - `openclaw status` - `openclaw gateway` commands - Any CLI tool that connects to the gateway - Cron tool, gateway config tool, or any internal tool call The **only** workaround is setting up TLS certs via `gateway.tls.enabled: true`, which is heavy for a connection that is already encrypted. ## Solution Add an optional `bindMode` parameter to `isSecureWebSocketUrl()`. When `bindMode === "tailnet"` **AND** the address is in the Tailscale CGNAT range (verified via existing `isTailnetIPv4()`), treat the connection as secure. This is a **targeted exemption**, not a relaxation: - Default behavior (no `bindMode`) remains strict — Tailscale IPs are still blocked - Only `bindMode: "tailnet"` + verified Tailscale CGNAT address = secure - Non-Tailscale IPs with `bindMode: "tailnet"` still throw `SECURITY ERROR` - LAN IPs with any bind mode still throw `SECURITY ERROR` - All `wss://` and loopback behavior is unchanged ### Why Tailscale is special Unlike regular LAN IPs (192.168.x.x, 10.x.x.x), Tailscale 100.x addresses are **only reachable through the WireGuard tunnel**. There is no unencrypted path to a Tailscale CGNAT address — the encryption is a property of the network, not the application. ## Changes | File | Change | |------|--------| | `src/gateway/net.ts` | Add `bindMode` param to `isSecureWebSocketUrl`, import `isTailnetIPv4`, add tailnet exemption logic | | `src/gateway/call.ts` | Pass `bindMode` to security check (1-line change) | | `src/gateway/net.test.ts` | 6 new test cases covering tailnet exemption + edge cases | | `src/gateway/call.test.ts` | Update tailnet test to expect success instead of throw | **4 files changed, 55 insertions(+), 7 deletions(-)** ## Test Coverage - ✅ `ws://100.64.0.1` with `bindMode="tailnet"` → secure - ✅ `ws://100.127.255.254` with `bindMode="tailnet"` → secure - ✅ `ws://100.64.0.1` without bindMode → still blocked (default strict) - ✅ `ws://100.64.0.1` with `bindMode="lan"` → blocked - ✅ `ws://192.168.1.100` with `bindMode="tailnet"` → blocked (not Tailscale) - ✅ `ws://10.0.0.5` with `bindMode="tailnet"` → blocked (not Tailscale) - ✅ Loopback still works with any bindMode - ✅ All existing tests pass unchanged ## Context Discovered after upgrading from 2026.2.17 → 2026.2.19 on a Mac Mini running OpenClaw with `gateway.bind: "tailnet"` for iPhone remote access via Tailscale. All CLI tools and internal tool calls immediately broke with `SECURITY ERROR`. <!-- greptile_comment --> <h3>Greptile Summary</h3> Adds Tailscale CGNAT exemption to the WebSocket security check to allow `ws://` connections to Tailscale addresses when `gateway.bind: "tailnet"`. **Key changes:** - Modified `isSecureWebSocketUrl()` to accept optional `bindMode` parameter and allow `ws://` to Tailscale CGNAT range (100.64.0.0/10) when `bindMode === "tailnet"` - Updated `buildGatewayConnectionDetails()` in `call.ts` to pass `bindMode` to the security check - Added comprehensive test coverage for the Tailscale exemption with 6 new test cases - Updated existing test expectation in `call.test.ts` for Tailscale connections **Rationale:** Tailscale connections use WireGuard encryption at the network layer, making the plaintext `ws://` at the application layer secure. The 100.x CGNAT addresses are only reachable through the encrypted WireGuard tunnel. **Issue found:** The second security check in `GatewayClient.start()` (line 117 of `client.ts`) doesn't receive the `bindMode` parameter, creating an inconsistency where connection details validate successfully but the client still rejects Tailscale connections. <h3>Confidence Score: 2/5</h3> - This PR has a critical logic issue that prevents it from working as intended - The implementation has the right idea and good test coverage, but there's a second security check in `GatewayClient.start()` that wasn't updated to support the Tailscale exemption. This means Tailscale connections will still fail at the client level even though they pass the initial validation. The fix is well-reasoned and the tests are thorough, but the incomplete implementation prevents it from achieving its stated goal. - Pay close attention to `src/gateway/client.ts` - the security check on line 117 needs to be updated to match the changes in `call.ts` <sub>Last reviewed commit: 1b9a88f</sub> <!-- greptile_other_comments_section --> <sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub> <!-- /greptile_comment -->

Most Similar PRs