Skip to content

fix(gateway): preserve external Tailscale Funnel routes in serve mode#73755

Open
RenzoMXD wants to merge 2 commits intoopenclaw:mainfrom
RenzoMXD:fix/openclaw-57241-tailscale-preserve-funnel
Open

fix(gateway): preserve external Tailscale Funnel routes in serve mode#73755
RenzoMXD wants to merge 2 commits intoopenclaw:mainfrom
RenzoMXD:fix/openclaw-57241-tailscale-preserve-funnel

Conversation

@RenzoMXD
Copy link
Copy Markdown
Contributor

Summary

  • Problem: With gateway.tailscale.mode = "serve", every gateway restart unconditionally runs tailscale serve --bg --yes <port>, which overwrites any externally configured tailscale funnel route for that port. Public Funnel reverts to tailnet-only on every restart.
  • Why it matters: Token-auth users have no working alternative — mode = "funnel" is intentionally password-only (kept per fix(gateway): block mode=none auth with tailscale serve remote exposure #51339).
  • What changed: New opt-in gateway.tailscale.preserveFunnel: boolean. When true and mode = "serve", check tailscale funnel status --json first; skip re-applying Serve if a Funnel route already covers the gateway port.
  • What did NOT change: Funnel auth-mode policy stays password-only. resetOnExit semantics unchanged. No changes to server-runtime-config.ts (fix: Tailscale serve + auth.mode=none exposes gateway to full... #50631's territory).

Change Type

  • Bug fix

Scope

  • Gateway / orchestration

Linked Issue/PR

Root Cause

enableTailscaleServe(port) runs tailscale serve --bg --yes <port>, which by Tailscale CLI semantics replaces whatever Serve/Funnel configuration controls that port. No precondition check, no opt-out flag — externally configured Funnel routes are clobbered on every gateway restart.

Regression Test Plan

  • Unit test
  • File: src/gateway/server-tailscale.test.ts (new)
  • Cases: (a) serve mode w/o preserveFunnel → calls enableTailscaleServe; (b) serve + preserveFunnel + Funnel route present → skips; (c) serve + preserveFunnel + no route → falls back; (d) funnel mode → never consults the helper.

User-visible Changes

New optional field gateway.tailscale.preserveFunnel (default false). Existing deployments behave identically. When true, an externally managed Funnel route survives gateway restarts.

Diagram

Before:           [restart] -> tailscale serve --bg --yes <port>
                              -> public Funnel reverts to tailnet-only

After (opt-in):   [restart] -> tailscale funnel status --json
                              -> if covers <port> -> SKIP (log "preserving Funnel")
                              -> else             -> tailscale serve --bg --yes <port>

Security Impact

Repro + Verification

Environment

  • OS: Linux / macOS · Runtime: Node 22+ · Channel: any host service using Tailscale Funnel
  • Config:
    { gateway: { tailscale: { mode: "serve", preserveFunnel: true } } }

Steps

  1. gateway.tailscale.mode: "serve".
  2. Outside OpenClaw: tailscale funnel --bg --set-path / <gateway-port>.
  3. Confirm tailscale funnel status shows the route.
  4. Restart the gateway.

Expected (with fix + preserveFunnel: true)

Funnel route still public after restart; log records serve skipped: preserving externally configured Tailscale Funnel for port <port>.

Actual (on main today)

Funnel route gone after restart — only gateway's Serve binding remains.

Evidence

  • Failing test/log before + passing after — cases (b) and (c) in server-tailscale.test.ts fail on main, pass with this PR.

Human Verification

  • Verified: 4 unit-test cases red→green; pnpm tsgo:core, tsgo:core:test, lint:core, check:import-cycles all clean; pnpm config:schema:gen + config:docs:gen regenerated cleanly with gateway.tailscale.preserveFunnel present.
  • Edge cases: missing tailscale binary, malformed JSON, Funnel enabled but for a different port, mode="funnel" with preserveFunnel=true — all confirmed via tests.
  • NOT verified: live Tailscale restart on a real tailnet (would mutate host state — clawsweeper made the same call).

Review Conversations

  • I will reply to or resolve every bot review conversation I address.

Compatibility / Migration

  • Backward compatible? Yes — new optional field, defaults to false.
  • Config/env changes? No required changes.
  • Migration needed? No.

Risks and Mitigations

  • Risk: Tailscale's funnel status --json shape isn't a pinned contract.
    Mitigation: helper returns false on any parse error / non-zero exit; falls back to today's Serve behavior. Zero regression for opt-out users.
  • Risk: Partial Funnel route on a different port.
    Mitigation: exact port match — only a handler whose Proxy resolves to 127.0.0.1:<gatewayPort> triggers the skip.

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation gateway Gateway runtime size: S labels Apr 28, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 28, 2026

Greptile Summary

This PR introduces an opt-in gateway.tailscale.preserveFunnel config flag that, when mode="serve", checks tailscale funnel status --json before applying tailscale serve on gateway startup. If an externally managed Funnel route already covers the port, the Serve step is skipped, preserving public exposure across restarts.

The three issues flagged in the prior review round — the missing empty-stdout guard before parsePossiblyNoisyJsonObject, the silent resetOnExit no-op, and the bare-string proxy comparison that would never match full URL strings — are all resolved in this revision. The proxy-matching logic now correctly handles schemes, trailing slashes, paths, bare host:port, bare port, and IPv6 loopback forms, with corresponding unit tests covering each variant.

Confidence Score: 5/5

Safe to merge — all previously flagged issues are addressed and the new code is well-guarded and tested.

No P0 or P1 issues remain. Previously reported bugs (empty-stdout crash path, silent resetOnExit no-op, always-false proxy comparison) are all fixed. The implementation is backward-compatible, falls back gracefully on any error, and is covered by unit tests for the key edge cases.

No files require special attention.

Reviews (3): Last reviewed commit: "Merge branch 'main' into fix/openclaw-57..." | Re-trigger Greptile

Comment thread src/infra/tailscale.ts Outdated
Comment thread src/gateway/server-tailscale.ts
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 28, 2026

Codex review: needs maintainer review before merge.

Summary
The PR adds optional gateway.tailscale.preserveFunnel config, docs/schema/changelog updates, and gateway/Tailscale tests so serve mode can skip reapplying Serve when an external Funnel route already covers the gateway port.

Reproducibility: yes. Current main statically reproduces the overwrite path through startGatewayTailscaleExposure -> enableTailscaleServe -> tailscale serve --bg --yes <port> on every serve-mode startup, and upstream Tailscale docs state the most recent serve/funnel command controls public versus private state for a port.

Next step before merge
No repair lane needed; the current diff is scoped and this pass found no discrete actionable blocker for automation to fix.

Security
Cleared: The current combined diff adds an opt-in read-only Tailscale status probe plus config/docs/tests, with no dependency, workflow, secret-handling, or supply-chain changes.

Review details

Best possible solution:

Land a scoped preserveFunnel fix that preserves externally owned Funnel routes without changing OpenClaw-managed Funnel auth policy, after required exact-head CI finishes green.

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

Yes. Current main statically reproduces the overwrite path through startGatewayTailscaleExposure -> enableTailscaleServe -> tailscale serve --bg --yes <port> on every serve-mode startup, and upstream Tailscale docs state the most recent serve/funnel command controls public versus private state for a port.

Is this the best way to solve the issue?

Yes for the current PR shape. The opt-in status precheck is a narrow maintainable fix because it only preserves an already external Funnel route and leaves OpenClaw-managed Funnel password-only behavior unchanged.

What I checked:

  • Current main reapplies Serve unconditionally: startGatewayTailscaleExposure calls enableTailscaleServe(params.port) for every tailscaleMode === "serve" startup and has no existing-Funnel precheck on main. (src/gateway/server-tailscale.ts:22, 695960975a60)
  • Serve helper uses the reported overwrite command: enableTailscaleServe runs tailscale serve --bg --yes <port>, matching the PR's described restart path. (src/infra/tailscale.ts:397, 695960975a60)
  • Upstream behavior supports the bug model: Official Tailscale Funnel docs state the same port cannot be Serve and Funnel simultaneously, and the most recent serve/funnel command decides whether the port is private or public.
  • Current combined PR diff is scoped: The current pull files API lists 16 files limited to changelog, generated config metadata, gateway/Tailscale docs, config schema/types, gateway startup wiring, and Tailscale tests/source; the earlier unrelated workflow/security/plugin files are not in the current combined diff. (7176a0d2001a)
  • PR implements the intended opt-in skip: The PR checks hasTailscaleFunnelRouteForPort only when preserveFunnel is true in serve mode, logs the preserved route, and avoids reset cleanup when OpenClaw did not apply Serve. (src/gateway/server-tailscale.ts:22, 7176a0d2001a)
  • Focused tests cover the new behavior: The PR adds gateway tests for default serve behavior, preserveFunnel skip, fallback when no route matches, and funnel-mode isolation, plus parser tests for full URL, trailing path, localhost, IPv6, bare port, wrong port, non-loopback host, disabled Funnel, and empty status payload cases. (src/gateway/server-tailscale.test.ts:1, 7176a0d2001a)

Likely related people:

  • steipete: GitHub path history shows Peter Steinberger split the gateway runtime, owns recent Tailscale auth/docs maintenance, and has frequent recent touches to the gateway and Tailscale helper paths. (role: introduced behavior and recent maintainer; confidence: high; commits: d19bc1562b07, fd9be79be194, d9c5040fc5bc; files: src/gateway/server-tailscale.ts, src/infra/tailscale.ts, docs/gateway/tailscale.md)
  • vincentkoc: Recent history shows gateway restart/runtime work and Tailscale docs maintenance adjacent to this PR's startup and documentation surfaces. (role: recent adjacent maintainer; confidence: medium; commits: 1f41b8b44ba0, 6c49039a23a9, 75ba8398f939; files: src/gateway/server.impl.ts, docs/gateway/tailscale.md)
  • HeimdallStrategy: Earlier path history credits this handle with Tailscale binary detection and related gateway binding/config work in the same infra area. (role: earlier Tailscale infra contributor; confidence: medium; commits: c851bdd47a2f; files: src/infra/tailscale.ts, src/config/types.gateway.ts)

Remaining risk / open question:

  • Required CI was still partially in progress when sampled, so merge should wait for exact-head required checks.
  • No live Tailscale tailnet restart was attempted in this read-only review; verification here is source, docs, diff, upstream contract, and CI/test-coverage based.

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

@RenzoMXD
Copy link
Copy Markdown
Contributor Author

Hi, @greptileai
Please review my PR again.

Comment thread src/infra/tailscale.ts Outdated
@RenzoMXD
Copy link
Copy Markdown
Contributor Author

@greptileai Please review my PR again. I think current CI test failures are not from my change.

@openclaw-barnacle openclaw-barnacle Bot added channel: line Channel integration: line commands Command implementations agents Agent runtime and tooling labels Apr 29, 2026
RenzoMXD added a commit to RenzoMXD/openclaw that referenced this pull request May 1, 2026
Adds the missing public-docs reference flagged in clawsweeper's review on
PR openclaw#73755. Two sibling bullets:

- docs/gateway/tailscale.md (Notes section, alongside resetOnExit)
- docs/gateway/configuration-reference.md (alongside tailscale.mode)

No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@RenzoMXD RenzoMXD force-pushed the fix/openclaw-57241-tailscale-preserve-funnel branch from b86ed6c to 1013e44 Compare May 2, 2026 12:29
@openclaw-barnacle openclaw-barnacle Bot removed channel: line Channel integration: line commands Command implementations agents Agent runtime and tooling labels May 2, 2026
@RenzoMXD RenzoMXD force-pushed the fix/openclaw-57241-tailscale-preserve-funnel branch from 1013e44 to 29d4a0a Compare May 2, 2026 12:32
@RenzoMXD RenzoMXD requested a review from a team as a code owner May 2, 2026 12:32
@openclaw-barnacle openclaw-barnacle Bot added scripts Repository scripts docker Docker and sandbox tooling agents Agent runtime and tooling plugin: google-meet size: L and removed size: M labels May 2, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the scripts Repository scripts label May 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Improvements or additions to documentation gateway Gateway runtime size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: tailscale.mode "serve" overwrites Funnel to tailnet-only on every gateway restart; tailscale.mode "funnel" fails with token auth

1 participant