Skip to content

tailscaleServeRuleState matches the port as a JSON key, so a correct Serve rule always reports serveRuleConflict #118

Description

@setkyar

Split out of #112 (secondary finding 2), which was closed by #113 — that PR only addressed the restart storm. Reported by @laulpogan.

Problem

tailscaleServeRuleState (internal/app/tailscale.go:120) looks for the port as an object key anywhere in the JSON, then compares the strings under that key to http://127.0.0.1:<port>:

rule, ok := findJSONKey(status, port)
if !ok { return serveRuleMissing, nil }
strings := collectJSONStrings(rule)
for _, s := range strings { if s == target { return serveRuleSame, nil } }
return serveRuleConflict, nil

In real tailscale serve status --json, the only key equal to the port is under TCP, and it holds no strings. The matching proxy lives under Web, whose key is <host>.ts.net:<port> — never equal to "31415":

{ "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 a rule exists on that port, serveRuleConflict is the only reachable outcome — including for a rule pi-web created itself via configureTailscaleServe.

Consequence

Every start (when a token is set and --host is not, internal/app/app.go:139) prints:

Tailscale Serve unavailable: tailscale HTTPS port 31415 is already configured for another service;
not overwriting it. To replace it, run: tailscale serve --bg --https=31415 http://127.0.0.1:31415

Reporter saw 1135 occurrences in their error log. A rule that is already correct is reported as someone else's, tailscaleUrl stays empty in pi-web-state.json, and authMiddleware.AllowHost(tsURL) (app.go:146) is never called. No user-visible breakage was demonstrated from the missing AllowHost — token auth over the tailnet works today — so that part is worth checking, not a proven failure. The confirmed cost is the noise plus pi-web never adopting its own Serve rule.

Fix

Match on the value rather than the port-as-key: walk the JSON for any string equal to target, or read Web["<dns-name>:<port>"].Handlers[*].Proxy explicitly. Treating TCP[port] == {"HTTPS": true} with a matching Web proxy as serveRuleSame also works.

Verified against current main (1040251): findJSONKey at tailscale.go:144 and collectJSONStrings at :164 behave as described.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions