mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
net/tsaddr, wgengine/netstack: add IPv6 range that forwards to site-relative IPv4
This defines a new magic IPv6 prefix, fd7a:115c:a1e0:b1a::/64, a subset of our existing /48, where the final 32 bits are an IPv4 address, and the middle 32 bits are a user-chosen "site ID". (which must currently be 0000:00xx; the top 3 bytes must be zero for now) e.g., I can say my home LAN's "site ID" is "0000:00bb" and then advertise its 10.2.0.0/16 IPv4 range via IPv6, like: tailscale up --advertise-routes=fd7a:115c:a1e0:b1a::bb:10.2.0.0/112 (112 being /128 minuse the /96 v6 prefix length) Then people in my tailnet can: $ curl '[fd7a:115c:a1e0:b1a::bb:10.2.0.230]' <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" .... Updates #3616, etc RELNOTE=initial support for TS IPv6 addresses to route v4 "via" specific nodes Change-Id: I9b49b6ad10410a24b5866b9fbc69d3cae1f600ef Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
f992749b98
commit
3ae701f0eb
@ -659,6 +659,39 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
NoSNAT: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "via_route_good",
|
||||
goos: "linux",
|
||||
args: upArgsT{
|
||||
advertiseRoutes: "fd7a:115c:a1e0:b1a::bb:10.0.0.0/112",
|
||||
netfilterMode: "off",
|
||||
},
|
||||
want: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
NoSNAT: true,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("fd7a:115c:a1e0:b1a::bb:10.0.0.0/112"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "via_route_short_prefix",
|
||||
goos: "linux",
|
||||
args: upArgsT{
|
||||
advertiseRoutes: "fd7a:115c:a1e0:b1a::/64",
|
||||
netfilterMode: "off",
|
||||
},
|
||||
wantErr: "fd7a:115c:a1e0:b1a::/64 4-in-6 prefix must be at least a /96",
|
||||
},
|
||||
{
|
||||
name: "via_route_short_reserved_siteid",
|
||||
goos: "linux",
|
||||
args: upArgsT{
|
||||
advertiseRoutes: "fd7a:115c:a1e0:b1a:1234:5678::/112",
|
||||
netfilterMode: "off",
|
||||
},
|
||||
wantErr: "route fd7a:115c:a1e0:b1a:1234:5678::/112 contains invalid site ID 12345678; must be 0xff or less",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -7,6 +7,7 @@
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
@ -27,6 +28,7 @@
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
@ -201,6 +203,26 @@ func warnf(format string, args ...any) {
|
||||
ipv6default = netaddr.MustParseIPPrefix("::/0")
|
||||
)
|
||||
|
||||
func validateViaPrefix(ipp netaddr.IPPrefix) error {
|
||||
if !tsaddr.IsViaPrefix(ipp) {
|
||||
return fmt.Errorf("%v is not a 4-in-6 prefix", ipp)
|
||||
}
|
||||
if ipp.Bits() < (128 - 32) {
|
||||
return fmt.Errorf("%v 4-in-6 prefix must be at least a /%v", ipp, 128-32)
|
||||
}
|
||||
a := ipp.IP().As16()
|
||||
// The first 64 bits of a are the via prefix.
|
||||
// The next 32 bits are the "site ID".
|
||||
// The last 32 bits are the IPv4.
|
||||
// For now, we reserve the top 3 bytes of the site ID,
|
||||
// and only allow users to use site IDs 0-255.
|
||||
siteID := binary.BigEndian.Uint32(a[8:12])
|
||||
if siteID > 0xFF {
|
||||
return fmt.Errorf("route %v contains invalid site ID %08x; must be 0xff or less", ipp, siteID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]netaddr.IPPrefix, error) {
|
||||
routeMap := map[netaddr.IPPrefix]bool{}
|
||||
if advertiseRoutes != "" {
|
||||
@ -214,6 +236,11 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]
|
||||
if ipp != ipp.Masked() {
|
||||
return nil, fmt.Errorf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
|
||||
}
|
||||
if tsaddr.IsViaPrefix(ipp) {
|
||||
if err := validateViaPrefix(ipp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if ipp == ipv4default {
|
||||
default4 = true
|
||||
} else if ipp == ipv6default {
|
||||
|
@ -106,7 +106,8 @@ type LocalBackend struct {
|
||||
|
||||
filterHash deephash.Sum
|
||||
|
||||
filterAtomic atomic.Value // of *filter.Filter
|
||||
filterAtomic atomic.Value // of *filter.Filter
|
||||
containsViaIPFuncAtomic atomic.Value // of func(netaddr.IP) bool
|
||||
|
||||
// The mutex protects the following elements.
|
||||
mu sync.Mutex
|
||||
@ -1573,11 +1574,23 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err
|
||||
|
||||
b.logf("using backend prefs for %q: %s", key, b.prefs.Pretty())
|
||||
|
||||
b.sshAtomicBool.Set(b.prefs != nil && b.prefs.RunSSH && canSSH)
|
||||
b.setAtomicValuesFromPrefs(b.prefs)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setAtomicValuesFromPrefs populates sshAtomicBool and containsViaIPFuncAtomic
|
||||
// from the prefs p, which may be nil.
|
||||
func (b *LocalBackend) setAtomicValuesFromPrefs(p *ipn.Prefs) {
|
||||
b.sshAtomicBool.Set(p != nil && p.RunSSH && canSSH)
|
||||
|
||||
if p == nil {
|
||||
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(nil))
|
||||
} else {
|
||||
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(tsaddr.FilterPrefixesCopy(p.AdvertiseRoutes, tsaddr.IsViaPrefix)))
|
||||
}
|
||||
}
|
||||
|
||||
// State returns the backend state machine's current state.
|
||||
func (b *LocalBackend) State() ipn.State {
|
||||
b.mu.Lock()
|
||||
@ -1746,7 +1759,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
netMap := b.netMap
|
||||
stateKey := b.stateKey
|
||||
|
||||
b.sshAtomicBool.Set(newp.RunSSH && canSSH)
|
||||
b.setAtomicValuesFromPrefs(newp)
|
||||
|
||||
oldp := b.prefs
|
||||
newp.Persist = oldp.Persist // caller isn't allowed to override this
|
||||
@ -2690,11 +2703,21 @@ func (b *LocalBackend) ResetForClientDisconnect() {
|
||||
b.authURL = ""
|
||||
b.authURLSticky = ""
|
||||
b.activeLogin = ""
|
||||
b.sshAtomicBool.Set(false)
|
||||
b.setAtomicValuesFromPrefs(nil)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Get() && canSSH }
|
||||
|
||||
// ShouldHandleViaIP reports whether whether ip is an IPv6 address in the
|
||||
// Tailscale ULA's v6 "via" range embedding an IPv4 address to be forwarded to
|
||||
// by Tailscale.
|
||||
func (b *LocalBackend) ShouldHandleViaIP(ip netaddr.IP) bool {
|
||||
if f, ok := b.containsViaIPFuncAtomic.Load().(func(netaddr.IP) bool); ok {
|
||||
return f(ip)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Logout tells the controlclient that we want to log out, and
|
||||
// transitions the local engine to the logged-out state without
|
||||
// waiting for controlclient to be in that state.
|
||||
|
@ -34,6 +34,7 @@ func CGNATRange() netaddr.IPPrefix {
|
||||
cgnatRange oncePrefix
|
||||
ulaRange oncePrefix
|
||||
tsUlaRange oncePrefix
|
||||
tsViaRange oncePrefix
|
||||
ula4To6Range oncePrefix
|
||||
ulaEph6Range oncePrefix
|
||||
serviceIPv6 oncePrefix
|
||||
@ -72,6 +73,14 @@ func TailscaleULARange() netaddr.IPPrefix {
|
||||
return tsUlaRange.v
|
||||
}
|
||||
|
||||
// TailscaleViaRange returns the IPv6 Unique Local Address subset range
|
||||
// TailscaleULARange that's used for IPv4 tunneling via IPv6.
|
||||
func TailscaleViaRange() netaddr.IPPrefix {
|
||||
// Mnemonic: "b1a" sounds like "via".
|
||||
tsViaRange.Do(func() { mustPrefix(&tsViaRange.v, "fd7a:115c:a1e0:b1a::/64") })
|
||||
return tsViaRange.v
|
||||
}
|
||||
|
||||
// Tailscale4To6Range returns the subset of TailscaleULARange used for
|
||||
// auto-translated Tailscale ipv4 addresses.
|
||||
func Tailscale4To6Range() netaddr.IPPrefix {
|
||||
@ -241,3 +250,33 @@ func AllIPv6() netaddr.IPPrefix { return allIPv6 }
|
||||
|
||||
// ExitRoutes returns a slice containing AllIPv4 and AllIPv6.
|
||||
func ExitRoutes() []netaddr.IPPrefix { return []netaddr.IPPrefix{allIPv4, allIPv6} }
|
||||
|
||||
// FilterPrefixes returns a new slice, not aliasing in, containing elements of
|
||||
// in that match f.
|
||||
func FilterPrefixesCopy(in []netaddr.IPPrefix, f func(netaddr.IPPrefix) bool) []netaddr.IPPrefix {
|
||||
var out []netaddr.IPPrefix
|
||||
for _, v := range in {
|
||||
if f(v) {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// IsViaPrefix reports whether p is a CIDR in the Tailscale "via" range.
|
||||
// See TailscaleViaRange.
|
||||
func IsViaPrefix(p netaddr.IPPrefix) bool {
|
||||
return TailscaleViaRange().Contains(p.IP())
|
||||
}
|
||||
|
||||
// UnmapVia returns the IPv4 address that corresponds to the provided Tailscale
|
||||
// "via" IPv4-in-IPv6 address.
|
||||
//
|
||||
// If ip is not a via address, it returns ip unchanged.
|
||||
func UnmapVia(ip netaddr.IP) netaddr.IP {
|
||||
if TailscaleViaRange().Contains(ip) {
|
||||
a := ip.As16()
|
||||
return netaddr.IPFrom4(*(*[4]byte)(a[12:16]))
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
@ -88,3 +88,19 @@ func BenchmarkTailscaleServiceAddr(b *testing.B) {
|
||||
sinkIP = TailscaleServiceIP()
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmapVia(t *testing.T) {
|
||||
tests := []struct {
|
||||
ip string
|
||||
want string
|
||||
}{
|
||||
{"1.2.3.4", "1.2.3.4"}, // unchanged v4
|
||||
{"fd7a:115c:a1e0:b1a::bb:10.2.1.3", "10.2.1.3"},
|
||||
{"fd7a:115c:a1e0:b1b::bb:10.2.1.4", "fd7a:115c:a1e0:b1b:0:bb:a02:104"}, // "b1b",not "bia"
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := UnmapVia(netaddr.MustParseIP(tt.ip)).String(); got != tt.want {
|
||||
t.Errorf("for %q: got %q, want %q", tt.ip, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -431,6 +431,8 @@ func (ns *Impl) peerAPIPortAtomic(ip netaddr.IP) *uint32 {
|
||||
}
|
||||
}
|
||||
|
||||
var viaRange = tsaddr.TailscaleViaRange()
|
||||
|
||||
// shouldProcessInbound reports whether an inbound packet should be
|
||||
// handled by netstack.
|
||||
func (ns *Impl) shouldProcessInbound(p *packet.Parsed, t *tstun.Wrapper) bool {
|
||||
@ -453,6 +455,9 @@ func (ns *Impl) shouldProcessInbound(p *packet.Parsed, t *tstun.Wrapper) bool {
|
||||
if ns.isInboundTSSH(p) && ns.processSSH() {
|
||||
return true
|
||||
}
|
||||
if p.IPVersion == 6 && viaRange.Contains(p.Dst.IP()) {
|
||||
return ns.lb != nil && ns.lb.ShouldHandleViaIP(p.Dst.IP())
|
||||
}
|
||||
if !ns.ProcessLocalIPs && !ns.ProcessSubnets {
|
||||
// Fast path for common case (e.g. Linux server in TUN mode) where
|
||||
// netstack isn't used at all; don't even do an isLocalIP lookup.
|
||||
@ -624,6 +629,12 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
|
||||
dialIP := netaddrIPFromNetstackIP(reqDetails.LocalAddress)
|
||||
isTailscaleIP := tsaddr.IsTailscaleIP(dialIP)
|
||||
|
||||
if viaRange.Contains(dialIP) {
|
||||
isTailscaleIP = false
|
||||
dialIP = tsaddr.UnmapVia(dialIP)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if !isTailscaleIP {
|
||||
// if this is a subnet IP, we added this in before the TCP handshake
|
||||
@ -775,6 +786,9 @@ func (ns *Impl) forwardUDP(client *gonet.UDPConn, wq *waiter.Queue, clientAddr,
|
||||
backendRemoteAddr = &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: int(port)}
|
||||
backendListenAddr = &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: int(srcPort)}
|
||||
} else {
|
||||
if dstIP := dstAddr.IP(); viaRange.Contains(dstIP) {
|
||||
dstAddr = netaddr.IPPortFrom(tsaddr.UnmapVia(dstIP), dstAddr.Port())
|
||||
}
|
||||
backendRemoteAddr = dstAddr.UDPAddr()
|
||||
if dstAddr.IP().Is4() {
|
||||
backendListenAddr = &net.UDPAddr{IP: net.ParseIP("0.0.0.0"), Port: int(srcPort)}
|
||||
|
Loading…
Reference in New Issue
Block a user