Portless
Reading Portless route matching: exact names, authorities and first suffix wins
Reproduce Portless routing priorities with fixed source and distinguish an exact Tailscale authority from hostname fallback and optional wildcard matching.
What you will learn
- Read the priority chain before drawing the diagram
- Do not replace first match with longest suffix in your explanation
- Preserve source identity and test the real handler
Before you start
- Basic HTTP origins, ports and command-line concepts
- Ability to distinguish loopback, LAN and public exposure
Trace a named request, choose explicit setup boundaries and separate observed behavior from untested integrations.
Key takeaways
- Exact local names precede Tailscale and wildcard tiers.
- Wildcard fallback selects the first matching route, not the longest suffix.
- The experiment exercises the upstream handler rather than a teaching reimplementation.
Read the priority chain before drawing the diagram
findRoute in proxy.ts normalizes the incoming authority to lowercase and removes an explicit :443. It then tries local hostname equality, Tailscale URL authority equality, Tailscale hostname equality ignoring the port, and finally wildcard suffix matching if strict mode is disabled. Each tier uses Array.find, so order within a tier is meaningful.
The authority tier prevents two routes sharing a tailnet hostname on different ports from being confused when an exact authority is available. In our fixtures, :8443 reached backend A and an explicit :443 reached backend B. These names were only Host-header inputs sent to 127.0.0.1; no Tailscale client, account, DNS resolution or tunnel was used.
Do not replace first match with longest suffix in your explanation
With routes for app.localhost before nested.app.localhost, a wildcard request for child.nested.app.localhost reaches the first base route. The implementation does not sort candidate suffixes by specificity. A request for nested.app.localhost itself still reaches the nested route because exact local matching is evaluated before the wildcard tier. Strict mode rejects the unregistered child instead.
The hostname-only Tailscale fallback also has an ordering consequence: a request using an unmatched port can reach the first route with that hostname. This is observable behavior, not a recommendation to depend on ambiguous shared names. When preparing a route table, avoid assuming a nonmatching port necessarily means rejection, and document intended ambiguity handling in tests.
Preserve source identity and test the real handler
Our probe verified Git blob hashes for six source modules, transpiled their TypeScript without changing behavior, and started ephemeral HTTP servers on loopback. It asserted responses from two identifiable backends, not a separately rewritten matching function. The 24 cases also covered live route changes, forwarding headers, the hop guard and internal authorization behavior.
The report records source commit, runtime and compiler versions and explicitly marks TLS, HTTP/2, WebSockets and operating-system changes as untested or not performed. Those limits are part of the result, not footnotes to discard. A future version may change matching rules; rerun against a newly reviewed pin rather than presenting this snapshot as a timeless API contract.
Implementation steps
- 1
Verify the source commit and module hashes.
- 2
Create two identifiable loopback fixture backends.
- 3
Assert exact, strict, authority and ambiguous-suffix cases.
- 4
Save runtime details and the list of untested integration paths.
Copy-ready example
{
"fixtureRoutesInOrder": ["app.localhost -> A", "nested.app.localhost -> B"],
"strictChildRequest": {"host":"child.nested.app.localhost","status":404},
"wildcardChildRequest": {"host":"child.nested.app.localhost","backend":"A"},
"exactNestedRequest": {"host":"nested.app.localhost","backend":"B"},
"observedHttpCases": 24
}Frequently asked questions
Does wildcard mode choose the most specific suffix?
No, not in this inspected revision. It uses the first matching route in the supplied array after earlier exact-match tiers fail.
Was Tailscale actually started?
No. The experiment tested authority matching with fixture Host headers over loopback; sharing integration remains unverified.
Sources
- packages/portless/src/proxy.tsSource checked 2026-09-08
- packages/portless/src/utils.tsSource checked 2026-09-08
- packages/portless/src/types.tsSource checked 2026-09-08