[feature] Listener match-by-interface (e.g. tailscale0), not just match_local_ip #26
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Request (from the homelab setup agent)
Let a listener match on its interface (e.g.
tailscale0) instead of a hardcoded IP (100.89.101.76), so a tailnet re-IP can't silently break the tailscale/admin listener.⚠️ Likely resolved without code — verify first
The stated problem (a re-IP breaking the listener) is caused by pinning a
/32. Every Tailscale address comes from the fixed CGNAT range, so matching the range instead fixes it today with zero code:Why it's safe / equivalent to "match tailscale0":
resolve_listenermatches the connection's OS-reportedlocal_addr()— the host IP the client connected to, which must be an address this host actually holds. This host holds exactly one address in100.64.0.0/10(its own Tailscale IP, which lives only ontailscale0), so "local addr ∈ 100.64.0.0/10" is precisely "arrived on the Tailscale interface." A re-IP swaps one100.xfor another — still in range. No new spoofing surface (still OS-truth).match_local_ipalready accepts any CIDR (not just/32).This ticket stays open until the CGNAT-range config is verified in the homelab. If it suffices, close as won't-fix (superseded by config). Build the feature below only if the general interface-matching capability is still wanted.
Shared assumption (both approaches)
Depends on the edge-router process seeing the real Tailscale address in
local_addr()— i.e. host networking or Tailscale on the host. Under bridged Docker, DNAT rewrites the destination to the container IP and neither approach works (andtailscale0wouldn't exist inside the container). Same caveat already documented formatch_local_ip.If we build it anyway:
match_interfaceLegitimately useful beyond Tailscale — a generic
eth1/VLAN trust zone with no well-known CIDR, an interface carrying multiple/both-family addresses, or config that states intent literally.This is security-sensitive (listeners gate trust zones, incl. the admin dashboard) and needs a design pass before code, not straight-to-implementation. Sketch of the cost:
match_interface: Option<String>on[[listeners]], mutually exclusive withmatch_local_ip(validate).if-addrsor similar) to enumerate interface → {addresses}.interface → {IPs}periodically into a shared map (Arc<RwLock<…>>) thatresolve_listenerconsults. This threads a new dependency into the accept path and adds a new moving part (cf. the config watcher).tailscale0on Linux,utunNon macOS); interface absent at startup (Tailscale not up yet) → empty set, matches nothing until it appears (fail-closed).Priority
Low — blocked on verifying the CGNAT-range config solves the homelab's actual problem. Only escalate to a design + build if the general capability is wanted.