Skip to content

fix(security): trust a CGNAT peer only if it arrived over the tailnet - #811

Merged
FerroxLabs merged 2 commits into
mainfrom
fix/529-cgnat-operator-trust
Jul 11, 2026
Merged

FerroxLabs merged 2 commits into
mainfrom
fix/529-cgnat-operator-trust

Conversation

@FerroxLabs

Copy link
Copy Markdown
Owner

100.64.0.0/10 was classified 'operator' unconditionally, reasoning that Tailscale
peers are cryptographically authenticated. But that range is RFC 6598 shared
carrier-grade NAT space used by real ISPs -- it is not Tailscale's. With the app
in remote mode (bound 0.0.0.0) and reached over a carrier-NAT path, the direct
socket peer can be a 100.64.x stranger. This gate guards requireDestructive
(reset / restore / sandbox-disable / password change) and the forwarded-origin
derivation in setup.ts, which has no password step-up at all.

Asking 'is this HOST on a tailnet?' is the wrong question: Tailscale is the
standard workaround FOR a CGNAT ISP, so the hosts behind carrier NAT and the
hosts on a tailnet are largely the SAME hosts -- a host-global check answers yes
for precisely the population at risk. The question is per-connection: did THIS
connection arrive over the tailnet? socket.localAddress answers it. A tailnet
peer lands on one of our own tailnet addresses; a carrier-segment stranger lands
on the physical nic. A missing localAddress fails closed.

Which addresses are ours is decided per INTERFACE, and an interface only counts
as Tailscale's if it is named tailscale* or carries an address in Tailscale's
registered fd7a:115c:a1e0::/48 ULA. That keeps out an unrelated VPN, which may
legitimately be handed RFC 6598 space on a tun/wg device, and it is the only way
to identify the device on macOS, where Tailscale is a bare utun. An
address-less (down / logged-out) tailscale0 confers nothing, since nothing can
arrive on it.

WAYLAND_TAILSCALE_CGNAT_OPERATOR forces the rule on/off for setups reached via a
subnet router, where the local node holds no tailnet address of its own.

Fixes #529

100.64.0.0/10 was classified 'operator' unconditionally, reasoning that Tailscale
peers are cryptographically authenticated. But that range is RFC 6598 shared
carrier-grade NAT space used by real ISPs -- it is not Tailscale's. With the app
in remote mode (bound 0.0.0.0) and reached over a carrier-NAT path, the direct
socket peer can be a 100.64.x stranger. This gate guards requireDestructive
(reset / restore / sandbox-disable / password change) and the forwarded-origin
derivation in setup.ts, which has no password step-up at all.

Asking 'is this HOST on a tailnet?' is the wrong question: Tailscale is the
standard workaround FOR a CGNAT ISP, so the hosts behind carrier NAT and the
hosts on a tailnet are largely the SAME hosts -- a host-global check answers yes
for precisely the population at risk. The question is per-connection: did THIS
connection arrive over the tailnet? socket.localAddress answers it. A tailnet
peer lands on one of our own tailnet addresses; a carrier-segment stranger lands
on the physical nic. A missing localAddress fails closed.

Which addresses are ours is decided per INTERFACE, and an interface only counts
as Tailscale's if it is named tailscale* or carries an address in Tailscale's
registered fd7a:115c:a1e0::/48 ULA. That keeps out an unrelated VPN, which may
legitimately be handed RFC 6598 space on a tun/wg device, and it is the only way
to identify the device on macOS, where Tailscale is a bare utun<N>. An
address-less (down / logged-out) tailscale0 confers nothing, since nothing can
arrive on it.

WAYLAND_TAILSCALE_CGNAT_OPERATOR forces the rule on/off for setups reached via a
subnet router, where the local node holds no tailnet address of its own.

Fixes #529
@FerroxLabs
FerroxLabs enabled auto-merge (squash) July 11, 2026 11:26
@github-actions github-actions Bot added the area:core Wayland Core engine / backends label Jul 11, 2026
@FerroxLabs
FerroxLabs merged commit 22a3308 into main Jul 11, 2026
22 checks passed
@FerroxLabs
FerroxLabs deleted the fix/529-cgnat-operator-trust branch July 11, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core Wayland Core engine / backends

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] networkTrust: 100.64.0.0/10 unconditionally classified 'operator' (RFC6598 CGNAT, not Tailscale-exclusive)

1 participant