From da1b917575537299c134cad06f81371ddced17df Mon Sep 17 00:00:00 2001 From: Tom DNetto Date: Tue, 3 Oct 2023 11:55:06 -0700 Subject: [PATCH] net/tstun: finish wiring IPv6 NAT support Updates https://github.com/tailscale/corp/issues/11202 Updates ENG-991 Signed-off-by: Tom DNetto --- net/tstun/wrap.go | 158 +++++--- net/tstun/wrap_test.go | 344 ++++++++++-------- tailcfg/tailcfg.go | 3 +- tstest/integration/integration_test.go | 249 +++++++------ tstest/integration/testcontrol/testcontrol.go | 9 +- types/ipproto/ipproto.go | 20 + 6 files changed, 468 insertions(+), 315 deletions(-) diff --git a/net/tstun/wrap.go b/net/tstun/wrap.go index ab0a4a3db..2caa24188 100644 --- a/net/tstun/wrap.go +++ b/net/tstun/wrap.go @@ -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 natConfig struct { + v4, v6 *natFamilyConfig +} + +func (c *natConfig) String() string { + if c == nil { + return "" + } + + 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 natV4Config struct { - // nativeAddr is the IPv4 Tailscale Address of the current node. +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 "" + 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) } diff --git a/net/tstun/wrap_test.go b/net/tstun/wrap_test.go index 865fca2f2..7dd3fd123 100644 --- a/net/tstun/wrap_test.go +++ b/net/tstun/wrap_test.go @@ -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, }, - }, - 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), + dnatMap: map[netip.Addr]netip.Addr{ + selfNativeIP: selfNativeIP, + selfEIP1: selfEIP1, + selfEIP2: 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, - }, - }, - { - name: "multiple-peers-require-nat-with-subnet", - wcfg: &wgcfg.Config{ - Addresses: selfAddrs, - Peers: []wgcfg.Peer{ - node(peer1IP, selfEIP1), - node(peer2IP, selfEIP2, subnet), + { + 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: 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), + snatMap: map[netip.Addr]netip.Addr{ + peer1IP: selfNativeIP, + peer2IP: selfEIP1, + subnetIP: selfNativeIP, }, - }, - 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), + dnatMap: map[netip.Addr]netip.Addr{ + selfNativeIP: selfNativeIP, + selfEIP1: selfNativeIP, + selfEIP2: selfEIP2, + subnetIP: subnetIP, }, }, - snatMap: map[netip.Addr]netip.Addr{ - peer1IP: selfNativeIP, - peer2IP: selfNativeIP, - subnetIP: selfNativeIP, + { + 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, + }, }, - dnatMap: map[netip.Addr]netip.Addr{ - selfNativeIP: selfNativeIP, - selfEIP1: selfEIP1, - selfEIP2: selfEIP2, - subnetIP: subnetIP, + { + 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, + }, }, - }, - { - name: "exit-node-require-nat-peer-doesnt", - wcfg: &wgcfg.Config{ - Addresses: selfAddrs, - Peers: []wgcfg.Peer{ - node(peer1IP, noIP), - node(peer2IP, selfEIP2, exitRoute), + { + 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: selfEIP2, - publicIP: selfEIP2, + { + 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, + }, }, - dnatMap: map[netip.Addr]netip.Addr{ - selfNativeIP: selfNativeIP, - selfEIP2: selfNativeIP, - subnetIP: subnetIP, + { + 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, + }, }, - }, - } + } - 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 diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index b679c7584..65ffb363f 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -117,7 +117,8 @@ type CapabilityVersion int // - 74: 2023-09-18: Client understands NodeCapMap // - 75: 2023-09-12: Client understands NodeAttrDNSForwarderDisableTCPRetries // - 76: 2023-09-20: Client understands ExitNodeDNSResolvers for IsWireGuardOnly nodes -const CurrentCapabilityVersion CapabilityVersion = 76 +// - 77: 2023-10-03: Client understands Peers[].SelfNodeV6MasqAddrForThisPeer +const CurrentCapabilityVersion CapabilityVersion = 77 type StableID string diff --git a/tstest/integration/integration_test.go b/tstest/integration/integration_test.go index 415ee8a94..cec6d469a 100644 --- a/tstest/integration/integration_test.go +++ b/tstest/integration/integration_test.go @@ -76,7 +76,7 @@ func TestOneNodeUpNoAuth(t *testing.T) { n1.AwaitResponding() n1.MustUp() - t.Logf("Got IP: %v", n1.AwaitIP()) + t.Logf("Got IP: %v", n1.AwaitIP4()) n1.AwaitRunning() d1.MustCleanShutdown(t) @@ -130,7 +130,7 @@ func TestControlKnobs(t *testing.T) { n1.AwaitResponding() n1.MustUp() - t.Logf("Got IP: %v", n1.AwaitIP()) + t.Logf("Got IP: %v", n1.AwaitIP4()) n1.AwaitRunning() cmd := n1.Tailscale("debug", "control-knobs") @@ -212,7 +212,7 @@ func TestStateSavedOnStart(t *testing.T) { n1.AwaitResponding() n1.MustUp() - t.Logf("Got IP: %v", n1.AwaitIP()) + t.Logf("Got IP: %v", n1.AwaitIP4()) n1.AwaitRunning() p1 := n1.diskPrefs() @@ -271,7 +271,7 @@ func TestOneNodeUpAuth(t *testing.T) { if err := cmd.Run(); err != nil { t.Fatalf("up: %v", err) } - t.Logf("Got IP: %v", n1.AwaitIP()) + t.Logf("Got IP: %v", n1.AwaitIP4()) n1.AwaitRunning() @@ -574,7 +574,7 @@ func TestNoControlConnWhenDown(t *testing.T) { // Come up the first time. n1.MustUp() - ip1 := n1.AwaitIP() + ip1 := n1.AwaitIP4() n1.AwaitRunning() // Then bring it down and stop the daemon. @@ -590,7 +590,7 @@ func TestNoControlConnWhenDown(t *testing.T) { t.Fatalf("after restart, state = %q; want %q", got, want) } - ip2 := n1.AwaitIP() + ip2 := n1.AwaitIP4() if ip1 != ip2 { t.Errorf("IPs different: %q vs %q", ip1, ip2) } @@ -615,7 +615,7 @@ func TestOneNodeUpWindowsStyle(t *testing.T) { n1.AwaitResponding() n1.MustUp("--unattended") - t.Logf("Got IP: %v", n1.AwaitIP()) + t.Logf("Got IP: %v", n1.AwaitIP4()) n1.AwaitRunning() d1.MustCleanShutdown(t) @@ -625,111 +625,128 @@ func TestOneNodeUpWindowsStyle(t *testing.T) { // tries to do bi-directional pings between them. func TestNATPing(t *testing.T) { t.Parallel() - env := newTestEnv(t) - registerNode := func() (*testNode, key.NodePublic) { - n := newTestNode(t, env) - n.StartDaemon() - n.AwaitListening() - n.MustUp() - n.AwaitRunning() - k := n.MustStatus().Self.PublicKey - return n, k - } - n1, k1 := registerNode() - n2, k2 := registerNode() - - n1IP := n1.AwaitIP() - n2IP := n2.AwaitIP() - - n1ExternalIP := netip.MustParseAddr("100.64.1.1") - n2ExternalIP := netip.MustParseAddr("100.64.2.1") - - tests := []struct { - name string - pairs []testcontrol.MasqueradePair - n1SeesN2IP netip.Addr - n2SeesN1IP netip.Addr - }{ - { - name: "no_nat", - n1SeesN2IP: n2IP, - n2SeesN1IP: n1IP, - }, - { - name: "n1_has_external_ip", - pairs: []testcontrol.MasqueradePair{ - { - Node: k1, - Peer: k2, - NodeMasqueradesAs: n1ExternalIP, - }, + for _, v6 := range []bool{false, true} { + env := newTestEnv(t) + registerNode := func() (*testNode, key.NodePublic) { + n := newTestNode(t, env) + n.StartDaemon() + n.AwaitListening() + n.MustUp() + n.AwaitRunning() + k := n.MustStatus().Self.PublicKey + return n, k + } + n1, k1 := registerNode() + n2, k2 := registerNode() + + var n1IP, n2IP netip.Addr + if v6 { + n1IP = n1.AwaitIP6() + n2IP = n2.AwaitIP6() + } else { + n1IP = n1.AwaitIP4() + n2IP = n2.AwaitIP4() + } + + n1ExternalIP := netip.MustParseAddr("100.64.1.1") + n2ExternalIP := netip.MustParseAddr("100.64.2.1") + if v6 { + n1ExternalIP = netip.MustParseAddr("fd7a:115c:a1e0::1a") + n2ExternalIP = netip.MustParseAddr("fd7a:115c:a1e0::1b") + } + + tests := []struct { + name string + pairs []testcontrol.MasqueradePair + n1SeesN2IP netip.Addr + n2SeesN1IP netip.Addr + }{ + { + name: "no_nat", + n1SeesN2IP: n2IP, + n2SeesN1IP: n1IP, }, - n1SeesN2IP: n2IP, - n2SeesN1IP: n1ExternalIP, - }, - { - name: "n2_has_external_ip", - pairs: []testcontrol.MasqueradePair{ - { - Node: k2, - Peer: k1, - NodeMasqueradesAs: n2ExternalIP, + { + name: "n1_has_external_ip", + pairs: []testcontrol.MasqueradePair{ + { + Node: k1, + Peer: k2, + NodeMasqueradesAs: n1ExternalIP, + }, }, + n1SeesN2IP: n2IP, + n2SeesN1IP: n1ExternalIP, }, - n1SeesN2IP: n2ExternalIP, - n2SeesN1IP: n1IP, - }, - { - name: "both_have_external_ips", - pairs: []testcontrol.MasqueradePair{ - { - Node: k1, - Peer: k2, - NodeMasqueradesAs: n1ExternalIP, + { + name: "n2_has_external_ip", + pairs: []testcontrol.MasqueradePair{ + { + Node: k2, + Peer: k1, + NodeMasqueradesAs: n2ExternalIP, + }, }, - { - Node: k2, - Peer: k1, - NodeMasqueradesAs: n2ExternalIP, + n1SeesN2IP: n2ExternalIP, + n2SeesN1IP: n1IP, + }, + { + name: "both_have_external_ips", + pairs: []testcontrol.MasqueradePair{ + { + Node: k1, + Peer: k2, + NodeMasqueradesAs: n1ExternalIP, + }, + { + Node: k2, + Peer: k1, + NodeMasqueradesAs: n2ExternalIP, + }, }, + n1SeesN2IP: n2ExternalIP, + n2SeesN1IP: n1ExternalIP, }, - n1SeesN2IP: n2ExternalIP, - n2SeesN1IP: n1ExternalIP, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - env.Control.SetMasqueradeAddresses(tc.pairs) - - s1 := n1.MustStatus() - n2AsN1Peer := s1.Peer[k2] - if got := n2AsN1Peer.TailscaleIPs[0]; got != tc.n1SeesN2IP { - t.Fatalf("n1 sees n2 as %v; want %v", got, tc.n1SeesN2IP) - } - - s2 := n2.MustStatus() - n1AsN2Peer := s2.Peer[k1] - if got := n1AsN2Peer.TailscaleIPs[0]; got != tc.n2SeesN1IP { - t.Fatalf("n2 sees n1 as %v; want %v", got, tc.n2SeesN1IP) - } - - if err := n1.Tailscale("ping", tc.n1SeesN2IP.String()).Run(); err != nil { - t.Fatal(err) - } - - if err := n1.Tailscale("ping", "-peerapi", tc.n1SeesN2IP.String()).Run(); err != nil { - t.Fatal(err) - } - - if err := n2.Tailscale("ping", tc.n2SeesN1IP.String()).Run(); err != nil { - t.Fatal(err) - } + } - if err := n2.Tailscale("ping", "-peerapi", tc.n2SeesN1IP.String()).Run(); err != nil { - t.Fatal(err) - } - }) + for _, tc := range tests { + t.Run(fmt.Sprintf("v6=%t/%v", v6, tc.name), func(t *testing.T) { + env.Control.SetMasqueradeAddresses(tc.pairs) + + ipIdx := 0 + if v6 { + ipIdx = 1 + } + + s1 := n1.MustStatus() + n2AsN1Peer := s1.Peer[k2] + if got := n2AsN1Peer.TailscaleIPs[ipIdx]; got != tc.n1SeesN2IP { + t.Fatalf("n1 sees n2 as %v; want %v", got, tc.n1SeesN2IP) + } + + s2 := n2.MustStatus() + n1AsN2Peer := s2.Peer[k1] + if got := n1AsN2Peer.TailscaleIPs[ipIdx]; got != tc.n2SeesN1IP { + t.Fatalf("n2 sees n1 as %v; want %v", got, tc.n2SeesN1IP) + } + + if err := n1.Tailscale("ping", tc.n1SeesN2IP.String()).Run(); err != nil { + t.Fatal(err) + } + + if err := n1.Tailscale("ping", "-peerapi", tc.n1SeesN2IP.String()).Run(); err != nil { + t.Fatal(err) + } + + if err := n2.Tailscale("ping", tc.n2SeesN1IP.String()).Run(); err != nil { + t.Fatal(err) + } + + if err := n2.Tailscale("ping", "-peerapi", tc.n2SeesN1IP.String()).Run(); err != nil { + t.Fatal(err) + } + }) + } } } @@ -743,7 +760,7 @@ func TestLogoutRemovesAllPeers(t *testing.T) { nodes[i].StartDaemon() nodes[i].AwaitResponding() nodes[i].MustUp() - nodes[i].AwaitIP() + nodes[i].AwaitIP4() nodes[i].AwaitRunning() } expectedPeers := len(nodes) - 1 @@ -758,7 +775,7 @@ func TestLogoutRemovesAllPeers(t *testing.T) { if err := tstest.WaitFor(20*time.Second, func() error { return nodes[i].Ping(nodes[j]) }); err != nil { - t.Fatalf("ping %v -> %v: %v", nodes[i].AwaitIP(), nodes[j].AwaitIP(), err) + t.Fatalf("ping %v -> %v: %v", nodes[i].AwaitIP4(), nodes[j].AwaitIP4(), err) } } } @@ -783,7 +800,7 @@ func TestLogoutRemovesAllPeers(t *testing.T) { nodes[0].MustUp() // This will create a new node expectedPeers++ - nodes[0].AwaitIP() + nodes[0].AwaitIP4() wantNode0PeerCount(expectedPeers) // all existing peers and the new node } @@ -1107,8 +1124,8 @@ func (n *testNode) MustLogOut() { func (n *testNode) Ping(otherNode *testNode) error { t := n.env.t - ip := otherNode.AwaitIP().String() - t.Logf("Running ping %v (from %v)...", ip, n.AwaitIP()) + ip := otherNode.AwaitIP4().String() + t.Logf("Running ping %v (from %v)...", ip, n.AwaitIP4()) return n.Tailscale("ping", ip).Run() } @@ -1162,14 +1179,22 @@ func (n *testNode) AwaitIPs() []netip.Addr { return addrs } -// AwaitIP returns the IP address of n. -func (n *testNode) AwaitIP() netip.Addr { +// AwaitIP4 returns the IPv4 address of n. +func (n *testNode) AwaitIP4() netip.Addr { t := n.env.t t.Helper() ips := n.AwaitIPs() return ips[0] } +// AwaitIP6 returns the IPv6 address of n. +func (n *testNode) AwaitIP6() netip.Addr { + t := n.env.t + t.Helper() + ips := n.AwaitIPs() + return ips[1] +} + // AwaitRunning waits for n to reach the IPN state "Running". func (n *testNode) AwaitRunning() { t := n.env.t diff --git a/tstest/integration/testcontrol/testcontrol.go b/tstest/integration/testcontrol/testcontrol.go index 43b092a45..981c5f8c2 100644 --- a/tstest/integration/testcontrol/testcontrol.go +++ b/tstest/integration/testcontrol/testcontrol.go @@ -900,8 +900,13 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse, peerAddress := s.masquerades[p.Key][node.Key] s.mu.Unlock() if peerAddress.IsValid() { - p.Addresses[0] = netip.PrefixFrom(peerAddress, peerAddress.BitLen()) - p.AllowedIPs[0] = netip.PrefixFrom(peerAddress, peerAddress.BitLen()) + if peerAddress.Is6() { + p.Addresses[1] = netip.PrefixFrom(peerAddress, peerAddress.BitLen()) + p.AllowedIPs[1] = netip.PrefixFrom(peerAddress, peerAddress.BitLen()) + } else { + p.Addresses[0] = netip.PrefixFrom(peerAddress, peerAddress.BitLen()) + p.AllowedIPs[0] = netip.PrefixFrom(peerAddress, peerAddress.BitLen()) + } } res.Peers = append(res.Peers, p) } diff --git a/types/ipproto/ipproto.go b/types/ipproto/ipproto.go index a6b1e0c48..8c20d51e4 100644 --- a/types/ipproto/ipproto.go +++ b/types/ipproto/ipproto.go @@ -6,6 +6,26 @@ package ipproto import "fmt" +// IPProtoVersion describes the IP address version. +type IPProtoVersion uint8 + +// Valid IPProtoVersion values. +const ( + IPProtoVersion4 = 4 + IPProtoVersion6 = 6 +) + +func (p IPProtoVersion) String() string { + switch p { + case IPProtoVersion4: + return "IPv4" + case IPProtoVersion6: + return "IPv6" + default: + return fmt.Sprintf("IPProtoVersion-%d", int(p)) + } +} + // Proto is an IP subprotocol as defined by the IANA protocol // numbers list // (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml),