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>
pull/9315/merge
Tom DNetto 1 year ago committed by Tom
parent 52e4f24c58
commit da1b917575

@ -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 "<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 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 "<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() {
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)
}

@ -608,11 +608,16 @@ 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
}
test := func(addrFam ipproto.IPProtoVersion) {
var (
noIP netip.Addr
@ -630,6 +635,21 @@ func TestNATCfg(t *testing.T) {
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())}
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
@ -779,8 +799,8 @@ func TestNATCfg(t *testing.T) {
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ncfg := natV4ConfigFromWGConfig(tc.wcfg)
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)
@ -791,9 +811,15 @@ func TestNATCfg(t *testing.T) {
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
// with the correct parameters when various packet operations are performed.

@ -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

@ -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,6 +625,7 @@ func TestOneNodeUpWindowsStyle(t *testing.T) {
// tries to do bi-directional pings between them.
func TestNATPing(t *testing.T) {
t.Parallel()
for _, v6 := range []bool{false, true} {
env := newTestEnv(t)
registerNode := func() (*testNode, key.NodePublic) {
n := newTestNode(t, env)
@ -638,11 +639,21 @@ func TestNATPing(t *testing.T) {
n1, k1 := registerNode()
n2, k2 := registerNode()
n1IP := n1.AwaitIP()
n2IP := n2.AwaitIP()
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
@ -699,18 +710,23 @@ func TestNATPing(t *testing.T) {
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
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[0]; got != tc.n1SeesN2IP {
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[0]; got != tc.n2SeesN1IP {
if got := n1AsN2Peer.TailscaleIPs[ipIdx]; got != tc.n2SeesN1IP {
t.Fatalf("n2 sees n1 as %v; want %v", got, tc.n2SeesN1IP)
}
@ -732,6 +748,7 @@ func TestNATPing(t *testing.T) {
})
}
}
}
func TestLogoutRemovesAllPeers(t *testing.T) {
t.Parallel()
@ -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

@ -900,9 +900,14 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse,
peerAddress := s.masquerades[p.Key][node.Key]
s.mu.Unlock()
if peerAddress.IsValid() {
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)
}

@ -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),

Loading…
Cancel
Save