diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 6d5f8c0fb..db980a529 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -611,6 +611,9 @@ func (h *peerAPIHandler) isAddressValid(addr netip.Addr) bool { if h.peerNode.SelfNodeV4MasqAddrForThisPeer != nil { return *h.peerNode.SelfNodeV4MasqAddrForThisPeer == addr } + if h.peerNode.SelfNodeV6MasqAddrForThisPeer != nil { + return *h.peerNode.SelfNodeV6MasqAddrForThisPeer == addr + } pfx := netip.PrefixFrom(addr, addr.BitLen()) return slices.Contains(h.selfNode.Addresses, pfx) } diff --git a/net/tstun/wrap.go b/net/tstun/wrap.go index 74bf54134..42fc48f28 100644 --- a/net/tstun/wrap.go +++ b/net/tstun/wrap.go @@ -598,7 +598,7 @@ func natConfigFromWGConfig(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 && (p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() || p.V6MasqAddr != nil && p.V6MasqAddr.IsValid()) { exitNodeRequiresMasq = true break } diff --git a/net/tstun/wrap_test.go b/net/tstun/wrap_test.go index f9e35beec..9d4ae188b 100644 --- a/net/tstun/wrap_test.go +++ b/net/tstun/wrap_test.go @@ -602,6 +602,7 @@ func TestFilterDiscoLoop(t *testing.T) { } func TestNATCfg(t *testing.T) { + t.Error("Missing case for IPv6") node := func(ip, masqIP netip.Addr, otherAllowedIPs ...netip.Prefix) wgcfg.Peer { p := wgcfg.Peer{ PublicKey: key.NewNode().Public(), diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 4c6f9a39f..044e72709 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -294,6 +294,21 @@ type Node struct { // not be masqueraded (e.g. in case of --snat-subnet-routes). SelfNodeV4MasqAddrForThisPeer *netip.Addr `json:",omitempty"` + // SelfNodeV6MasqAddrForThisPeer is the IPv6 that this peer knows the current node as. + // It may be empty if the peer knows the current node by its native + // IPv6 address. + // This field is only populated in a MapResponse for peers and not + // for the current node. + // + // If set, it should be used to masquerade traffic originating from the + // current node to this peer. The masquerade address is only relevant + // for this peer and not for other peers. + // + // This only applies to traffic originating from the current node to the + // peer or any of its subnets. Traffic originating from subnet routes will + // not be masqueraded (e.g. in case of --snat-subnet-routes). + SelfNodeV6MasqAddrForThisPeer *netip.Addr `json:",omitempty"` + // IsWireGuardOnly indicates that this is a non-Tailscale WireGuard peer, it // is not expected to speak Disco or DERP, and it must have Endpoints in // order to be reachable. TODO(#7826): 2023-04-06: only the first parseable @@ -1726,6 +1741,7 @@ func (n *Node) Equal(n2 *Node) bool { eqStrings(n.Tags, n2.Tags) && n.Expired == n2.Expired && eqPtr(n.SelfNodeV4MasqAddrForThisPeer, n2.SelfNodeV4MasqAddrForThisPeer) && + eqPtr(n.SelfNodeV6MasqAddrForThisPeer, n2.SelfNodeV6MasqAddrForThisPeer) && n.IsWireGuardOnly == n2.IsWireGuardOnly } diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index 9d72124b4..92b3b59ba 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -67,6 +67,10 @@ func (src *Node) Clone() *Node { dst.SelfNodeV4MasqAddrForThisPeer = new(netip.Addr) *dst.SelfNodeV4MasqAddrForThisPeer = *src.SelfNodeV4MasqAddrForThisPeer } + if dst.SelfNodeV6MasqAddrForThisPeer != nil { + dst.SelfNodeV6MasqAddrForThisPeer = new(netip.Addr) + *dst.SelfNodeV6MasqAddrForThisPeer = *src.SelfNodeV6MasqAddrForThisPeer + } return dst } @@ -103,6 +107,7 @@ var _NodeCloneNeedsRegeneration = Node(struct { DataPlaneAuditLogID string Expired bool SelfNodeV4MasqAddrForThisPeer *netip.Addr + SelfNodeV6MasqAddrForThisPeer *netip.Addr IsWireGuardOnly bool }{}) diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index b0e3f982e..64209966a 100644 --- a/tailcfg/tailcfg_test.go +++ b/tailcfg/tailcfg_test.go @@ -351,7 +351,7 @@ func TestNodeEqual(t *testing.T) { "UnsignedPeerAPIOnly", "ComputedName", "computedHostIfDifferent", "ComputedNameWithHost", "DataPlaneAuditLogID", "Expired", "SelfNodeV4MasqAddrForThisPeer", - "IsWireGuardOnly", + "SelfNodeV6MasqAddrForThisPeer", "IsWireGuardOnly", } if have := fieldsOf(reflect.TypeOf(Node{})); !reflect.DeepEqual(have, nodeHandles) { t.Errorf("Node.Equal check might be out of sync\nfields: %q\nhandled: %q\n", diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index 9c195da1c..f21f545dd 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -184,6 +184,14 @@ func (v NodeView) SelfNodeV4MasqAddrForThisPeer() *netip.Addr { return &x } +func (v NodeView) SelfNodeV6MasqAddrForThisPeer() *netip.Addr { + if v.ж.SelfNodeV6MasqAddrForThisPeer == nil { + return nil + } + x := *v.ж.SelfNodeV6MasqAddrForThisPeer + return &x +} + func (v NodeView) IsWireGuardOnly() bool { return v.ж.IsWireGuardOnly } func (v NodeView) Equal(v2 NodeView) bool { return v.ж.Equal(v2.ж) } @@ -220,6 +228,7 @@ var _NodeViewNeedsRegeneration = Node(struct { DataPlaneAuditLogID string Expired bool SelfNodeV4MasqAddrForThisPeer *netip.Addr + SelfNodeV6MasqAddrForThisPeer *netip.Addr IsWireGuardOnly bool }{}) diff --git a/tstest/integration/testcontrol/testcontrol.go b/tstest/integration/testcontrol/testcontrol.go index 8f5037380..6e7a8fcdb 100644 --- a/tstest/integration/testcontrol/testcontrol.go +++ b/tstest/integration/testcontrol/testcontrol.go @@ -65,7 +65,7 @@ type Server struct { // MapResponses sent to clients. It is keyed by the requesting nodes // public key, and then the peer node's public key. The value is the // masquerade address to use for that peer. - masquerades map[key.NodePublic]map[key.NodePublic]netip.Addr // node => peer => SelfNodeV4MasqAddrForThisPeer IP + masquerades map[key.NodePublic]map[key.NodePublic]netip.Addr // node => peer => SelfNodeV{4,6}MasqAddrForThisPeer IP noisePubKey key.MachinePublic noisePrivKey key.ControlPrivate // not strictly needed vs. MachinePrivate, but handy to test type interactions. @@ -844,7 +844,11 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse, continue } if masqIP := nodeMasqs[p.Key]; masqIP.IsValid() { - p.SelfNodeV4MasqAddrForThisPeer = ptr.To(masqIP) + if masqIP.Is4() { + p.SelfNodeV4MasqAddrForThisPeer = ptr.To(masqIP) + } else { + p.SelfNodeV6MasqAddrForThisPeer = ptr.To(masqIP) + } } s.mu.Lock() diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go index 78e5bb232..0a6a9be43 100644 --- a/wgengine/magicsock/magicsock_test.go +++ b/wgengine/magicsock/magicsock_test.go @@ -2271,74 +2271,93 @@ func TestIsWireGuardOnlyPeer(t *testing.T) { } func TestIsWireGuardOnlyPeerWithMasquerade(t *testing.T) { - derpMap, cleanup := runDERPAndStun(t, t.Logf, localhostListener{}, netaddr.IPv4(127, 0, 0, 1)) - defer cleanup() + check := func(t *testing.T, tsaip, wgaip, masqip netip.Prefix) { + tskey := key.NewNode() + wgkey := key.NewNode() - tskey := key.NewNode() - tsaip := netip.MustParsePrefix("100.111.222.111/32") + derpMap, cleanup := runDERPAndStun(t, t.Logf, localhostListener{}, netaddr.IPv4(127, 0, 0, 1)) + defer cleanup() - wgkey := key.NewNode() - wgaip := netip.MustParsePrefix("10.64.0.1/32") + uapi := fmt.Sprintf("private_key=%s\npublic_key=%s\nallowed_ip=%s\n\n", + wgkey.UntypedHexString(), tskey.Public().UntypedHexString(), masqip.String()) + wgdev, wgtun, port := newWireguard(t, uapi, []netip.Prefix{wgaip}) + defer wgdev.Close() + wgEp := netip.AddrPortFrom(netip.MustParseAddr("127.0.0.1"), port) - // the ip that the wireguard peer has in allowed ips and expects as a masq source - masqip := netip.MustParsePrefix("10.64.0.2/32") + m := newMagicStackWithKey(t, t.Logf, localhostListener{}, derpMap, tskey) + defer m.Close() - uapi := fmt.Sprintf("private_key=%s\npublic_key=%s\nallowed_ip=%s\n\n", - wgkey.UntypedHexString(), tskey.Public().UntypedHexString(), masqip.String()) - wgdev, wgtun, port := newWireguard(t, uapi, []netip.Prefix{wgaip}) - defer wgdev.Close() - wgEp := netip.AddrPortFrom(netip.MustParseAddr("127.0.0.1"), port) - - m := newMagicStackWithKey(t, t.Logf, localhostListener{}, derpMap, tskey) - defer m.Close() - - nm := &netmap.NetworkMap{ - Name: "ts", - PrivateKey: m.privateKey, - NodeKey: m.privateKey.Public(), - Addresses: []netip.Prefix{tsaip}, - Peers: []*tailcfg.Node{ - { - Key: wgkey.Public(), - Endpoints: []string{wgEp.String()}, - IsWireGuardOnly: true, - Addresses: []netip.Prefix{wgaip}, - AllowedIPs: []netip.Prefix{wgaip}, - SelfNodeV4MasqAddrForThisPeer: ptr.To(masqip.Addr()), + nm := &netmap.NetworkMap{ + Name: "ts", + PrivateKey: m.privateKey, + NodeKey: m.privateKey.Public(), + Addresses: []netip.Prefix{tsaip}, + Peers: []*tailcfg.Node{ + { + Key: wgkey.Public(), + Endpoints: []string{wgEp.String()}, + IsWireGuardOnly: true, + Addresses: []netip.Prefix{wgaip}, + AllowedIPs: []netip.Prefix{wgaip}, + }, }, - }, - } - m.conn.SetNetworkMap(nm) - - cfg, err := nmcfg.WGCfg(nm, t.Logf, netmap.AllowSingleHosts|netmap.AllowSubnetRoutes, "") - if err != nil { - t.Fatal(err) - } - m.Reconfig(cfg) - - pbuf := tuntest.Ping(wgaip.Addr(), tsaip.Addr()) - m.tun.Outbound <- pbuf - - select { - case p := <-wgtun.Inbound: - - // TODO(raggi): move to a bytes.Equal based test later, once - // tuntest.Ping produces correct checksums! - - var pkt packet.Parsed - pkt.Decode(p) - if pkt.ICMP4Header().Type != packet.ICMP4EchoRequest { - t.Fatalf("unexpected packet: %x", p) } - if pkt.Src.Addr() != masqip.Addr() { - t.Fatalf("bad source IP, got %s, want %s", pkt.Src.Addr(), masqip.Addr()) + if masqip.Addr().Is4() { + nm.Peers[0].SelfNodeV4MasqAddrForThisPeer = ptr.To(masqip.Addr()) + } else { + nm.Peers[0].SelfNodeV6MasqAddrForThisPeer = ptr.To(masqip.Addr()) } - if pkt.Dst.Addr() != wgaip.Addr() { - t.Fatalf("bad source IP, got %s, want %s", pkt.Src.Addr(), masqip.Addr()) + m.conn.SetNetworkMap(nm) + + cfg, err := nmcfg.WGCfg(nm, t.Logf, netmap.AllowSingleHosts|netmap.AllowSubnetRoutes, "") + if err != nil { + t.Fatal(err) + } + m.Reconfig(cfg) + + pbuf := tuntest.Ping(wgaip.Addr(), tsaip.Addr()) + m.tun.Outbound <- pbuf + + select { + case p := <-wgtun.Inbound: + // TODO(raggi): move to a bytes.Equal based test later, once + // tuntest.Ping produces correct checksums! + + var pkt packet.Parsed + pkt.Decode(p) + if masqip.Addr().Is4() { + if pkt.ICMP4Header().Type != packet.ICMP4EchoRequest { + t.Fatalf("unexpected packet: %x", p) + } + } else { + if pkt.ICMP6Header().Type != packet.ICMP6EchoRequest { + t.Fatalf("unexpected packet: %x", p) + } + } + if pkt.Src.Addr() != masqip.Addr() { + t.Fatalf("bad source IP, got %s, want %s", pkt.Src.Addr(), masqip.Addr()) + } + if pkt.Dst.Addr() != wgaip.Addr() { + t.Fatalf("bad source IP, got %s, want %s", pkt.Src.Addr(), masqip.Addr()) + } + case <-time.After(time.Second): + t.Fatal("no packet after 1s") } - case <-time.After(time.Second): - t.Fatal("no packet after 1s") } + + t.Run("IPv4", func(t *testing.T) { + tailscaleIP := netip.MustParsePrefix("100.111.222.111/32") + wireguardIP := netip.MustParsePrefix("10.64.0.1/32") + masqueradeIP := netip.MustParsePrefix("10.64.0.2/32") + check(t, tailscaleIP, wireguardIP, masqueradeIP) + }) + + t.Run("IPv6", func(t *testing.T) { + tailscaleIP := netip.MustParsePrefix("100::111/128") + wireguardIP := netip.MustParsePrefix("fd7a:115c:a1e0:ab12:4848:cd2a:3a2c:baa1/128") + masqueradeIP := netip.MustParsePrefix("fd7a:115c:a1e0:ab12:4848:cd2a:3a2c:baa2/128") + check(t, tailscaleIP, wireguardIP, masqueradeIP) + }) } func TestEndpointTracker(t *testing.T) { diff --git a/wgengine/wgcfg/config.go b/wgengine/wgcfg/config.go index 18f019b53..a6a130b6f 100644 --- a/wgengine/wgcfg/config.go +++ b/wgengine/wgcfg/config.go @@ -38,6 +38,7 @@ type Peer struct { DiscoKey key.DiscoPublic // present only so we can handle restarts within wgengine, not passed to WireGuard AllowedIPs []netip.Prefix V4MasqAddr *netip.Addr // if non-nil, masquerade IPv4 traffic to this peer using this address + V6MasqAddr *netip.Addr // if non-nil, masquerade IPv6 traffic to this peer using this address PersistentKeepalive uint16 // wireguard-go's endpoint for this peer. It should always equal Peer.PublicKey. // We represent it explicitly so that we can detect if they diverge and recover. diff --git a/wgengine/wgcfg/nmcfg/nmcfg.go b/wgengine/wgcfg/nmcfg/nmcfg.go index f01b42cb1..2ab18b8dd 100644 --- a/wgengine/wgcfg/nmcfg/nmcfg.go +++ b/wgengine/wgcfg/nmcfg/nmcfg.go @@ -102,6 +102,7 @@ func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags, didExitNodeWarn := false cpeer.V4MasqAddr = peer.SelfNodeV4MasqAddrForThisPeer + cpeer.V6MasqAddr = peer.SelfNodeV6MasqAddrForThisPeer for _, allowedIP := range peer.AllowedIPs { if allowedIP.Bits() == 0 && peer.StableID != exitNode { if didExitNodeWarn { diff --git a/wgengine/wgcfg/wgcfg_clone.go b/wgengine/wgcfg/wgcfg_clone.go index 6887dd6cc..c39777a17 100644 --- a/wgengine/wgcfg/wgcfg_clone.go +++ b/wgengine/wgcfg/wgcfg_clone.go @@ -58,6 +58,10 @@ func (src *Peer) Clone() *Peer { dst.V4MasqAddr = new(netip.Addr) *dst.V4MasqAddr = *src.V4MasqAddr } + if dst.V6MasqAddr != nil { + dst.V6MasqAddr = new(netip.Addr) + *dst.V6MasqAddr = *src.V6MasqAddr + } return dst } @@ -67,6 +71,7 @@ var _PeerCloneNeedsRegeneration = Peer(struct { DiscoKey key.DiscoPublic AllowedIPs []netip.Prefix V4MasqAddr *netip.Addr + V6MasqAddr *netip.Addr PersistentKeepalive uint16 WGEndpoint key.NodePublic }{})