Files
tailscale/ipn/ipnlocal/via_test.go
Mike Jensen 90ed0bcf4b wgengine/netstack: block 4via6 forwards to host-scoped targets (#20866)
The packet filter only sees a via address's outer ULA, so netstack unconditionally dialed whatever IPv4 was embedded in it.

This change refuses TCP, UDP, and ping relays to host-scoped destinations after UnmapVia.

Credit to the Anthropic infrastructure security team for finding and reporting.

Fixes https://github.com/tailscale/corp/issues/46646

Change-Id: I93d8e27a2eddbce7eb3f8c5fa4677f8de3a8ed9e

Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-08-19 09:16:08 -06:00

67 lines
2.0 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package ipnlocal
import (
"net/netip"
"testing"
)
func TestViaTargetAllowed(t *testing.T) {
cases := []struct {
ip string
want bool
}{
{"10.0.0.1", true},
{"192.168.1.1", true},
{"8.8.8.8", true},
{"169.254.169.254", false}, // cloud instance metadata
{"169.254.0.1", false}, // other link-local
{"127.0.0.1", false}, // loopback
{"127.255.0.1", false},
{"10.9.4.99", true}, // normal LAN host
{"192.168.50.254", true}, // last routable host on a /24 still allowed
{"0.0.0.0", false}, // Linux connect() treats as localhost
{"255.255.255.255", false},
{"192.168.50.128", true},
{"10.100.200.5", true},
{"10.1.2.255", true}, // last octet 255 is not inherently broadcast
{"172.16.0.9", true},
{"192.168.50.63", true},
{"224.0.0.1", false}, // multicast
{"239.255.252.250", false},
{"10.20.30.40", true},
{"198.51.100.7", true},
{"169.254.169.1", false}, // other link-local
{"255.0.0.5", true},
{"10.9.8.7", true},
{"192.168.1.254", true},
{"10.0.255.200", true}, // host in a /16 whose last octet isn't 0/255
{"224.0.0.251", false},
{"100.64.9.8", false},
{"192.168.5.10", true},
{"172.16.255.254", true}, // last host of a /16 (last octet 254) allowed
{"127.1.2.3", false},
{"198.18.0.10", true},
{"239.255.255.250", false}, // SSDP multicast
{"192.168.50.128", true},
{"169.254.100.200", false}, // link-local
{"10.9.4.255", true}, // last octet 255 is not inherently broadcast
{"224.0.1.129", false},
{"8.20.30.40", true},
{"192.168.50.255", true}, // last octet 255 is not inherently broadcast
{"10.11.12.13", true},
{"239.1.2.3", false},
{"100.64.0.1", false}, // tailnet CGNAT: would proxy as this node
{"100.100.100.100", false},
{"::1", false},
}
for _, tc := range cases {
ip := netip.MustParseAddr(tc.ip)
if got := viaTargetAllowed(ip); got != tc.want {
t.Errorf("viaTargetAllowed(%v) = %v, want %v", ip, got, tc.want)
}
}
}