mirror of
https://github.com/tailscale/tailscale.git
synced 2025-06-14 20:18:58 +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,
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -7,6 +7,7 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
@ -27,6 +28,7 @@ import (
|
|||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
"tailscale.com/net/tsaddr"
|
||||||
"tailscale.com/safesocket"
|
"tailscale.com/safesocket"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
@ -201,6 +203,26 @@ var (
|
|||||||
ipv6default = netaddr.MustParseIPPrefix("::/0")
|
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) {
|
func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]netaddr.IPPrefix, error) {
|
||||||
routeMap := map[netaddr.IPPrefix]bool{}
|
routeMap := map[netaddr.IPPrefix]bool{}
|
||||||
if advertiseRoutes != "" {
|
if advertiseRoutes != "" {
|
||||||
@ -214,6 +236,11 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]
|
|||||||
if ipp != ipp.Masked() {
|
if ipp != ipp.Masked() {
|
||||||
return nil, fmt.Errorf("%s has non-address bits set; expected %s", 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 {
|
if ipp == ipv4default {
|
||||||
default4 = true
|
default4 = true
|
||||||
} else if ipp == ipv6default {
|
} else if ipp == ipv6default {
|
||||||
|
@ -107,6 +107,7 @@ type LocalBackend struct {
|
|||||||
filterHash deephash.Sum
|
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.
|
// The mutex protects the following elements.
|
||||||
mu sync.Mutex
|
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.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
|
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.
|
// State returns the backend state machine's current state.
|
||||||
func (b *LocalBackend) State() ipn.State {
|
func (b *LocalBackend) State() ipn.State {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
@ -1746,7 +1759,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
|||||||
netMap := b.netMap
|
netMap := b.netMap
|
||||||
stateKey := b.stateKey
|
stateKey := b.stateKey
|
||||||
|
|
||||||
b.sshAtomicBool.Set(newp.RunSSH && canSSH)
|
b.setAtomicValuesFromPrefs(newp)
|
||||||
|
|
||||||
oldp := b.prefs
|
oldp := b.prefs
|
||||||
newp.Persist = oldp.Persist // caller isn't allowed to override this
|
newp.Persist = oldp.Persist // caller isn't allowed to override this
|
||||||
@ -2690,11 +2703,21 @@ func (b *LocalBackend) ResetForClientDisconnect() {
|
|||||||
b.authURL = ""
|
b.authURL = ""
|
||||||
b.authURLSticky = ""
|
b.authURLSticky = ""
|
||||||
b.activeLogin = ""
|
b.activeLogin = ""
|
||||||
b.sshAtomicBool.Set(false)
|
b.setAtomicValuesFromPrefs(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Get() && canSSH }
|
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
|
// Logout tells the controlclient that we want to log out, and
|
||||||
// transitions the local engine to the logged-out state without
|
// transitions the local engine to the logged-out state without
|
||||||
// waiting for controlclient to be in that state.
|
// waiting for controlclient to be in that state.
|
||||||
|
@ -34,6 +34,7 @@ var (
|
|||||||
cgnatRange oncePrefix
|
cgnatRange oncePrefix
|
||||||
ulaRange oncePrefix
|
ulaRange oncePrefix
|
||||||
tsUlaRange oncePrefix
|
tsUlaRange oncePrefix
|
||||||
|
tsViaRange oncePrefix
|
||||||
ula4To6Range oncePrefix
|
ula4To6Range oncePrefix
|
||||||
ulaEph6Range oncePrefix
|
ulaEph6Range oncePrefix
|
||||||
serviceIPv6 oncePrefix
|
serviceIPv6 oncePrefix
|
||||||
@ -72,6 +73,14 @@ func TailscaleULARange() netaddr.IPPrefix {
|
|||||||
return tsUlaRange.v
|
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
|
// Tailscale4To6Range returns the subset of TailscaleULARange used for
|
||||||
// auto-translated Tailscale ipv4 addresses.
|
// auto-translated Tailscale ipv4 addresses.
|
||||||
func Tailscale4To6Range() netaddr.IPPrefix {
|
func Tailscale4To6Range() netaddr.IPPrefix {
|
||||||
@ -241,3 +250,33 @@ func AllIPv6() netaddr.IPPrefix { return allIPv6 }
|
|||||||
|
|
||||||
// ExitRoutes returns a slice containing AllIPv4 and AllIPv6.
|
// ExitRoutes returns a slice containing AllIPv4 and AllIPv6.
|
||||||
func ExitRoutes() []netaddr.IPPrefix { return []netaddr.IPPrefix{allIPv4, 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()
|
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
|
// shouldProcessInbound reports whether an inbound packet should be
|
||||||
// handled by netstack.
|
// handled by netstack.
|
||||||
func (ns *Impl) shouldProcessInbound(p *packet.Parsed, t *tstun.Wrapper) bool {
|
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() {
|
if ns.isInboundTSSH(p) && ns.processSSH() {
|
||||||
return true
|
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 {
|
if !ns.ProcessLocalIPs && !ns.ProcessSubnets {
|
||||||
// Fast path for common case (e.g. Linux server in TUN mode) where
|
// 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.
|
// 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)
|
dialIP := netaddrIPFromNetstackIP(reqDetails.LocalAddress)
|
||||||
isTailscaleIP := tsaddr.IsTailscaleIP(dialIP)
|
isTailscaleIP := tsaddr.IsTailscaleIP(dialIP)
|
||||||
|
|
||||||
|
if viaRange.Contains(dialIP) {
|
||||||
|
isTailscaleIP = false
|
||||||
|
dialIP = tsaddr.UnmapVia(dialIP)
|
||||||
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if !isTailscaleIP {
|
if !isTailscaleIP {
|
||||||
// if this is a subnet IP, we added this in before the TCP handshake
|
// 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)}
|
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)}
|
backendListenAddr = &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: int(srcPort)}
|
||||||
} else {
|
} else {
|
||||||
|
if dstIP := dstAddr.IP(); viaRange.Contains(dstIP) {
|
||||||
|
dstAddr = netaddr.IPPortFrom(tsaddr.UnmapVia(dstIP), dstAddr.Port())
|
||||||
|
}
|
||||||
backendRemoteAddr = dstAddr.UDPAddr()
|
backendRemoteAddr = dstAddr.UDPAddr()
|
||||||
if dstAddr.IP().Is4() {
|
if dstAddr.IP().Is4() {
|
||||||
backendListenAddr = &net.UDPAddr{IP: net.ParseIP("0.0.0.0"), Port: int(srcPort)}
|
backendListenAddr = &net.UDPAddr{IP: net.ParseIP("0.0.0.0"), Port: int(srcPort)}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user