net/tstun: finish wiring IPv6 NAT support

Updates https://github.com/tailscale/corp/issues/11202
Updates ENG-991
Signed-off-by: Tom DNetto <tom@tailscale.com>
This commit is contained in:
Tom DNetto
2023-10-03 11:55:06 -07:00
committed by Tom
parent 52e4f24c58
commit da1b917575
6 changed files with 460 additions and 307 deletions
+117 -41
View File
@@ -98,8 +98,8 @@ type Wrapper struct {
// timeNow, if non-nil, will be used to obtain the current time.
timeNow func() time.Time
// natV4Config stores the current IPv4 NAT configuration.
natV4Config atomic.Pointer[natV4Config]
// natConfig stores the current NAT configuration.
natConfig atomic.Pointer[natConfig]
// vectorBuffer stores the oldest unconsumed packet vector from tdev. It is
// allocated in wrap() and the underlying arrays should never grow.
@@ -481,14 +481,9 @@ func (t *Wrapper) sendVectorOutbound(r tunVectorReadResult) {
t.vectorOutbound <- r
}
// snatV4 does SNAT on p if it's an IPv4 packet and the destination
// address requires a different source address.
func (t *Wrapper) snatV4(p *packet.Parsed) {
if p.IPVersion != 4 {
return
}
nc := t.natV4Config.Load()
// snat does SNAT on p if the destination address requires a different source address.
func (t *Wrapper) snat(p *packet.Parsed) {
nc := t.natConfig.Load()
oldSrc := p.Src.Addr()
newSrc := nc.selectSrcIP(oldSrc, p.Dst.Addr())
if oldSrc != newSrc {
@@ -496,13 +491,9 @@ func (t *Wrapper) snatV4(p *packet.Parsed) {
}
}
// dnatV4 does destination NAT on p if it's an IPv4 packet.
func (t *Wrapper) dnatV4(p *packet.Parsed) {
if p.IPVersion != 4 {
return
}
nc := t.natV4Config.Load()
// dnat does destination NAT on p.
func (t *Wrapper) dnat(p *packet.Parsed) {
nc := t.natConfig.Load()
oldDst := p.Dst.Addr()
newDst := nc.mapDstIP(oldDst)
if newDst != oldDst {
@@ -521,15 +512,79 @@ func findV4(addrs []netip.Prefix) netip.Addr {
return netip.Addr{}
}
// natV4Config is the configuration for IPv4 NAT.
// findV6 returns the first Tailscale IPv6 address in addrs.
func findV6(addrs []netip.Prefix) netip.Addr {
for _, ap := range addrs {
a := ap.Addr()
if a.Is6() && tsaddr.IsTailscaleIP(a) {
return a
}
}
return netip.Addr{}
}
// natConfig is the configuration for NAT.
// It should be treated as immutable.
//
// The nil value is a valid configuration.
type natV4Config struct {
// nativeAddr is the IPv4 Tailscale Address of the current node.
type natConfig struct {
v4, v6 *natFamilyConfig
}
func (c *natConfig) String() string {
if c == nil {
return "<nil>"
}
var b strings.Builder
b.WriteString("natConfig{")
fmt.Fprintf(&b, "v4: %v, ", c.v4)
fmt.Fprintf(&b, "v6: %v", c.v6)
b.WriteString("}")
return b.String()
}
// mapDstIP returns the destination IP to use for a packet to dst.
// If dst is not one of the listen addresses, it is returned as-is,
// otherwise the native address is returned.
func (c *natConfig) mapDstIP(oldDst netip.Addr) netip.Addr {
if c == nil {
return oldDst
}
if oldDst.Is4() {
return c.v4.mapDstIP(oldDst)
}
if oldDst.Is6() {
return c.v6.mapDstIP(oldDst)
}
return oldDst
}
// selectSrcIP returns the source IP to use for a packet to dst.
// If the packet is not from the native address, it is returned as-is.
func (c *natConfig) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr {
if c == nil {
return oldSrc
}
if oldSrc.Is4() {
return c.v4.selectSrcIP(oldSrc, dst)
}
if oldSrc.Is6() {
return c.v6.selectSrcIP(oldSrc, dst)
}
return oldSrc
}
// natFamilyConfig is the NAT configuration for a particular
// address family.
// It should be treated as immutable.
//
// The nil value is a valid configuration.
type natFamilyConfig struct {
// nativeAddr is the Tailscale Address of the current node.
nativeAddr netip.Addr
// listenAddrs is the set of IPv4 addresses that should be
// listenAddrs is the set of addresses that should be
// mapped to the native address. These are the addresses that
// peers will use to connect to this node.
listenAddrs views.Map[netip.Addr, struct{}] // masqAddr -> struct{}
@@ -545,12 +600,12 @@ type natV4Config struct {
dstAddrToPeerKeyMapper *table.RoutingTable
}
func (c *natV4Config) String() string {
func (c *natFamilyConfig) String() string {
if c == nil {
return "<nil>"
return "natFamilyConfig(nil)"
}
var b strings.Builder
b.WriteString("natV4Config{")
b.WriteString("natFamilyConfig{")
fmt.Fprintf(&b, "nativeAddr: %v, ", c.nativeAddr)
fmt.Fprint(&b, "listenAddrs: [")
@@ -586,7 +641,7 @@ func (c *natV4Config) String() string {
// mapDstIP returns the destination IP to use for a packet to dst.
// If dst is not one of the listen addresses, it is returned as-is,
// otherwise the native address is returned.
func (c *natV4Config) mapDstIP(oldDst netip.Addr) netip.Addr {
func (c *natFamilyConfig) mapDstIP(oldDst netip.Addr) netip.Addr {
if c == nil {
return oldDst
}
@@ -598,7 +653,7 @@ func (c *natV4Config) mapDstIP(oldDst netip.Addr) netip.Addr {
// selectSrcIP returns the source IP to use for a packet to dst.
// If the packet is not from the native address, it is returned as-is.
func (c *natV4Config) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr {
func (c *natFamilyConfig) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr {
if c == nil {
return oldSrc
}
@@ -615,16 +670,25 @@ func (c *natV4Config) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr {
return oldSrc
}
// natV4ConfigFromWGConfig generates a natV4Config from nm.
// If v4 NAT is not required, it returns nil.
func natV4ConfigFromWGConfig(wcfg *wgcfg.Config) *natV4Config {
// natConfigFromWGConfig generates a natFamilyConfig from nm,
// for the indicated address family.
// If NAT is not required for that address family, it returns nil.
func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.IPProtoVersion) *natFamilyConfig {
if wcfg == nil {
return nil
}
nativeAddr := findV4(wcfg.Addresses)
var nativeAddr netip.Addr
switch addrFam {
case ipproto.IPProtoVersion4:
nativeAddr = findV4(wcfg.Addresses)
case ipproto.IPProtoVersion6:
nativeAddr = findV6(wcfg.Addresses)
}
if !nativeAddr.IsValid() {
return nil
}
var (
rt table.RoutingTableBuilder
dstMasqAddrs map[key.NodePublic]netip.Addr
@@ -637,17 +701,25 @@ func natV4ConfigFromWGConfig(wcfg *wgcfg.Config) *natV4Config {
exitNodeRequiresMasq := false // true if using an exit node and it requires masquerading
for _, p := range wcfg.Peers {
isExitNode := slices.Contains(p.AllowedIPs, tsaddr.AllIPv4()) || slices.Contains(p.AllowedIPs, tsaddr.AllIPv6())
if isExitNode && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
exitNodeRequiresMasq = true
if isExitNode {
hasMasqAddrsForFamily := false ||
(addrFam == ipproto.IPProtoVersion4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid()) ||
(addrFam == ipproto.IPProtoVersion6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid())
if hasMasqAddrsForFamily {
exitNodeRequiresMasq = true
}
break
}
}
for i := range wcfg.Peers {
p := &wcfg.Peers[i]
var addrToUse netip.Addr
if p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
if addrFam == ipproto.IPProtoVersion4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
addrToUse = *p.V4MasqAddr
mak.Set(&listenAddrs, addrToUse, struct{}{})
} else if addrFam == ipproto.IPProtoVersion6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid() {
addrToUse = *p.V6MasqAddr
mak.Set(&listenAddrs, addrToUse, struct{}{})
} else if exitNodeRequiresMasq {
addrToUse = nativeAddr
} else {
@@ -659,7 +731,7 @@ func natV4ConfigFromWGConfig(wcfg *wgcfg.Config) *natV4Config {
if len(listenAddrs) == 0 && len(dstMasqAddrs) == 0 {
return nil
}
return &natV4Config{
return &natFamilyConfig{
nativeAddr: nativeAddr,
listenAddrs: views.MapOf(listenAddrs),
dstMasqAddrs: views.MapOf(dstMasqAddrs),
@@ -668,10 +740,14 @@ func natV4ConfigFromWGConfig(wcfg *wgcfg.Config) *natV4Config {
}
// SetNetMap is called when a new NetworkMap is received.
// It currently (2023-03-01) only updates the IPv4 NAT configuration.
func (t *Wrapper) SetWGConfig(wcfg *wgcfg.Config) {
cfg := natV4ConfigFromWGConfig(wcfg)
old := t.natV4Config.Swap(cfg)
v4, v6 := natConfigFromWGConfig(wcfg, ipproto.IPProtoVersion4), natConfigFromWGConfig(wcfg, ipproto.IPProtoVersion6)
var cfg *natConfig
if v4 != nil || v6 != nil {
cfg = &natConfig{v4: v4, v6: v6}
}
old := t.natConfig.Swap(cfg)
if !reflect.DeepEqual(old, cfg) {
t.logf("nat config: %v", cfg)
}
@@ -786,7 +862,7 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
for _, data := range res.data {
p.Decode(data[res.dataOffset:])
t.snatV4(p)
t.snat(p)
if m := t.destIPActivity.Load(); m != nil {
if fn := m[p.Dst.Addr()]; fn != nil {
fn()
@@ -843,7 +919,7 @@ func (t *Wrapper) injectedRead(res tunInjectedRead, buf []byte, offset int) (int
p := parsedPacketPool.Get().(*packet.Parsed)
defer parsedPacketPool.Put(p)
p.Decode(buf[offset : offset+n])
t.snatV4(p)
t.snat(p)
if m := t.destIPActivity.Load(); m != nil {
if fn := m[p.Dst.Addr()]; fn != nil {
@@ -965,7 +1041,7 @@ func (t *Wrapper) Write(buffs [][]byte, offset int) (int, error) {
captHook := t.captureHook.Load()
for _, buff := range buffs {
p.Decode(buff[offset:])
t.dnatV4(p)
t.dnat(p)
if !t.disableFilter {
if t.filterPacketInboundFromWireGuard(p, captHook) != filter.Accept {
metricPacketInDrop.Add(1)
@@ -1030,7 +1106,7 @@ func (t *Wrapper) InjectInboundPacketBuffer(pkt stack.PacketBufferPtr) error {
if captHook != nil {
captHook(capture.SynthesizedToLocal, t.now(), p.Buffer(), p.CaptureMeta)
}
t.dnatV4(p)
t.dnat(p)
return t.InjectInboundDirect(buf, PacketStartOffset)
}
+185 -159
View File
@@ -608,191 +608,217 @@ func TestNATCfg(t *testing.T) {
AllowedIPs: []netip.Prefix{
netip.PrefixFrom(ip, ip.BitLen()),
},
V4MasqAddr: ptr.To(masqIP),
}
if masqIP.Is4() {
p.V4MasqAddr = ptr.To(masqIP)
} else {
p.V6MasqAddr = ptr.To(masqIP)
}
p.AllowedIPs = append(p.AllowedIPs, otherAllowedIPs...)
return p
}
var (
noIP netip.Addr
test := func(addrFam ipproto.IPProtoVersion) {
var (
noIP netip.Addr
selfNativeIP = netip.MustParseAddr("100.64.0.1")
selfEIP1 = netip.MustParseAddr("100.64.1.1")
selfEIP2 = netip.MustParseAddr("100.64.1.2")
selfAddrs = []netip.Prefix{netip.PrefixFrom(selfNativeIP, selfNativeIP.BitLen())}
selfNativeIP = netip.MustParseAddr("100.64.0.1")
selfEIP1 = netip.MustParseAddr("100.64.1.1")
selfEIP2 = netip.MustParseAddr("100.64.1.2")
selfAddrs = []netip.Prefix{netip.PrefixFrom(selfNativeIP, selfNativeIP.BitLen())}
peer1IP = netip.MustParseAddr("100.64.0.2")
peer2IP = netip.MustParseAddr("100.64.0.3")
peer1IP = netip.MustParseAddr("100.64.0.2")
peer2IP = netip.MustParseAddr("100.64.0.3")
subnet = netip.MustParsePrefix("192.168.0.0/24")
subnetIP = netip.MustParseAddr("192.168.0.1")
subnet = netip.MustParsePrefix("192.168.0.0/24")
subnetIP = netip.MustParseAddr("192.168.0.1")
exitRoute = netip.MustParsePrefix("0.0.0.0/0")
publicIP = netip.MustParseAddr("8.8.8.8")
)
exitRoute = netip.MustParsePrefix("0.0.0.0/0")
publicIP = netip.MustParseAddr("8.8.8.8")
)
if addrFam == ipproto.IPProtoVersion6 {
selfNativeIP = netip.MustParseAddr("fd7a:115c:a1e0::a")
selfEIP1 = netip.MustParseAddr("fd7a:115c:a1e0::1a")
selfEIP2 = netip.MustParseAddr("fd7a:115c:a1e0::1b")
selfAddrs = []netip.Prefix{netip.PrefixFrom(selfNativeIP, selfNativeIP.BitLen())}
tests := []struct {
name string
wcfg *wgcfg.Config
snatMap map[netip.Addr]netip.Addr // dst -> src
dnatMap map[netip.Addr]netip.Addr
}{
{
name: "no-cfg",
wcfg: nil,
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfNativeIP,
peer2IP: selfNativeIP,
subnetIP: selfNativeIP,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfEIP1,
selfEIP2: selfEIP2,
},
},
{
name: "single-peer-requires-nat",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, noIP),
node(peer2IP, selfEIP1),
peer1IP = netip.MustParseAddr("fd7a:115c:a1e0::b")
peer2IP = netip.MustParseAddr("fd7a:115c:a1e0::c")
subnet = netip.MustParsePrefix("2001:db8::/32")
subnetIP = netip.MustParseAddr("2001:db8::FFFF")
exitRoute = netip.MustParsePrefix("::/0")
publicIP = netip.MustParseAddr("2001:4860:4860::8888")
}
tests := []struct {
name string
wcfg *wgcfg.Config
snatMap map[netip.Addr]netip.Addr // dst -> src
dnatMap map[netip.Addr]netip.Addr
}{
{
name: "no-cfg",
wcfg: nil,
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfNativeIP,
peer2IP: selfNativeIP,
subnetIP: selfNativeIP,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfEIP1,
selfEIP2: selfEIP2,
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfNativeIP,
peer2IP: selfEIP1,
subnetIP: selfNativeIP,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfNativeIP,
selfEIP2: selfEIP2,
subnetIP: subnetIP,
},
},
{
name: "multiple-peers-require-nat",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, selfEIP1),
node(peer2IP, selfEIP2),
{
name: "single-peer-requires-nat",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, noIP),
node(peer2IP, selfEIP1),
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfNativeIP,
peer2IP: selfEIP1,
subnetIP: selfNativeIP,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfNativeIP,
selfEIP2: selfEIP2,
subnetIP: subnetIP,
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfEIP1,
peer2IP: selfEIP2,
subnetIP: selfNativeIP,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfNativeIP,
selfEIP2: selfNativeIP,
subnetIP: subnetIP,
},
},
{
name: "multiple-peers-require-nat-with-subnet",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, selfEIP1),
node(peer2IP, selfEIP2, subnet),
{
name: "multiple-peers-require-nat",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, selfEIP1),
node(peer2IP, selfEIP2),
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfEIP1,
peer2IP: selfEIP2,
subnetIP: selfNativeIP,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfNativeIP,
selfEIP2: selfNativeIP,
subnetIP: subnetIP,
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfEIP1,
peer2IP: selfEIP2,
subnetIP: selfEIP2,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfNativeIP,
selfEIP2: selfNativeIP,
subnetIP: subnetIP,
},
},
{
name: "multiple-peers-require-nat-with-default-route",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, selfEIP1),
node(peer2IP, selfEIP2, exitRoute),
{
name: "multiple-peers-require-nat-with-subnet",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, selfEIP1),
node(peer2IP, selfEIP2, subnet),
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfEIP1,
peer2IP: selfEIP2,
subnetIP: selfEIP2,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfNativeIP,
selfEIP2: selfNativeIP,
subnetIP: subnetIP,
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfEIP1,
peer2IP: selfEIP2,
publicIP: selfEIP2,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfNativeIP,
selfEIP2: selfNativeIP,
subnetIP: subnetIP,
},
},
{
name: "no-nat",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, noIP),
node(peer2IP, noIP),
{
name: "multiple-peers-require-nat-with-default-route",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, selfEIP1),
node(peer2IP, selfEIP2, exitRoute),
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfEIP1,
peer2IP: selfEIP2,
publicIP: selfEIP2,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfNativeIP,
selfEIP2: selfNativeIP,
subnetIP: subnetIP,
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfNativeIP,
peer2IP: selfNativeIP,
subnetIP: selfNativeIP,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfEIP1,
selfEIP2: selfEIP2,
subnetIP: subnetIP,
},
},
{
name: "exit-node-require-nat-peer-doesnt",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, noIP),
node(peer2IP, selfEIP2, exitRoute),
{
name: "no-nat",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, noIP),
node(peer2IP, noIP),
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfNativeIP,
peer2IP: selfNativeIP,
subnetIP: selfNativeIP,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfEIP1,
selfEIP2: selfEIP2,
subnetIP: subnetIP,
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfNativeIP,
peer2IP: selfEIP2,
publicIP: selfEIP2,
{
name: "exit-node-require-nat-peer-doesnt",
wcfg: &wgcfg.Config{
Addresses: selfAddrs,
Peers: []wgcfg.Peer{
node(peer1IP, noIP),
node(peer2IP, selfEIP2, exitRoute),
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfNativeIP,
peer2IP: selfEIP2,
publicIP: selfEIP2,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP2: selfNativeIP,
subnetIP: subnetIP,
},
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP2: selfNativeIP,
subnetIP: subnetIP,
},
},
}
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ncfg := natV4ConfigFromWGConfig(tc.wcfg)
for peer, want := range tc.snatMap {
if got := ncfg.selectSrcIP(selfNativeIP, peer); got != want {
t.Errorf("selectSrcIP[%v]: got %v; want %v", peer, got, want)
for _, tc := range tests {
t.Run(fmt.Sprintf("%v/%v", addrFam, tc.name), func(t *testing.T) {
ncfg := natConfigFromWGConfig(tc.wcfg, addrFam)
for peer, want := range tc.snatMap {
if got := ncfg.selectSrcIP(selfNativeIP, peer); got != want {
t.Errorf("selectSrcIP[%v]: got %v; want %v", peer, got, want)
}
}
}
for dstIP, want := range tc.dnatMap {
if got := ncfg.mapDstIP(dstIP); got != want {
t.Errorf("mapDstIP[%v]: got %v; want %v", dstIP, got, want)
for dstIP, want := range tc.dnatMap {
if got := ncfg.mapDstIP(dstIP); got != want {
t.Errorf("mapDstIP[%v]: got %v; want %v", dstIP, got, want)
}
}
}
})
if t.Failed() {
t.Logf("%v", ncfg)
}
})
}
}
test(ipproto.IPProtoVersion4)
test(ipproto.IPProtoVersion6)
}
// TestCaptureHook verifies that the Wrapper.captureHook callback is called