fix(tailscale): match the Serve rule by proxy value, not port-as-key - #120
Merged
Merged
Conversation
tailscaleServeRuleState looked for the port as an object key anywhere in `tailscale serve status --json`, then compared the strings under it to http://127.0.0.1:<port>. In the real ipn.ServeConfig the only key equal to the port is under TCP, which holds no strings; the proxy lives under Web["<magicdns-name>:<port>"]. So once any rule existed on that port, serveRuleConflict was the only reachable outcome -- including for a rule pi-web created itself. Every start then logged "already configured for another service" (1135 occurrences in the reporter's log), tailscaleUrl stayed empty in pi-web-state.json, and authMiddleware.AllowHost(tsURL) was never called. Parse the config into a typed struct and decide on the proxy value: a Web key ending ":<port>" whose handler proxies to our target is serveRuleSame; a Web hit without that proxy, or a bare TCP entry on the port (a raw TCPForward, or an HTTPS terminator we did not create), is serveRuleConflict; neither is serveRuleMissing. findJSONKey and collectJSONStrings had no other callers and are removed. The three existing fixtures used a fabricated {"HTTPS":{"<port>":...}} shape in which the port *is* an exact key holding the proxy string, which is why this shipped green; they now use the real shape. The same-rule test is the reported case verbatim and fails against the old implementation. Added coverage for a raw TCPForward and for rules on unrelated ports. Closes #118
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
tailscaleServeRuleState(internal/app/tailscale.go) looked for the port as an object key anywhere intailscale serve status --json, then compared the strings under it tohttp://127.0.0.1:<port>. In the realipn.ServeConfigthe only key equal to the port sits underTCPand holds no strings; the proxy lives underWeb["<magicdns-name>:<port>"]:{ "TCP": { "31415": { "HTTPS": true } }, // exact-key match; 0 strings "Web": { "host.example.ts.net:31415": // key contains the port, never matches { "Handlers": { "/": { "Proxy": "http://127.0.0.1:31415" } } } } }So once any rule existed on that port,
serveRuleConflictwas the only reachable outcome — including for a rule pi-web created itself. Every start (token set, no--host) loggedalready configured for another service; the reporter counted 1135 occurrences.tailscaleUrlstayed empty inpi-web-state.jsonandauthMiddleware.AllowHost(tsURL)was never called.The fix parses the config into a typed struct and decides on the proxy value:
Webkey ending:<port>with a handler proxying to our target →serveRuleSameWebhit on that port without that proxy →serveRuleConflictWebhit but a bareTCP[port]entry (rawTCPForward, or an HTTPS terminator we did not create) →serveRuleConflictserveRuleMissingfindJSONKeyandcollectJSONStringshad no other callers and are removed.Behavior note: the old recursive search would incidentally match ports nested inside
Foregroundserve sessions; the typed parse will not. Foreground serve is interactive and ephemeral, and the old outcome there was the same spurious conflict error, so this is not a regression.Related issue
Closes #118
(Split out of #112, secondary finding 2, reported by @laulpogan. The storm itself was fixed in #113.)
Type of change
fix— bug fixLive vs. Export
Testing
make checkpasses (test + build + vet)vitest) cover the changego test ./...) cover the changeThe three existing fixtures in
tailscale_test.goused a fabricated{"HTTPS":{"31415":{...}}}shape in which the port is an exact key holding the proxy string — which is why this bug shipped green. They now use the realServeConfigshape, so the same-rule test is the reported case verbatim and fails against the old implementation. Added two cases: a rawTCPForwardon the port (conflict, no overwrite) and a rule on an unrelated port (correctly ignored, our rule still created).