From c08cf2a9c6209e4fdef896921af66bbe737b8a24 Mon Sep 17 00:00:00 2001 From: Tom DNetto Date: Mon, 18 Sep 2023 17:03:53 -0700 Subject: [PATCH] all: declare & plumb IPv6 masquerade address for peer This PR plumbs through awareness of an IPv6 SNAT/masquerade address from the wire protocol through to the low-level (tstun / wgengine). This PR is the first in two PRs for implementing IPv6 NAT support to/from peers. A subsequent PR will implement the data-plane changes to implement IPv6 NAT - this is just plumbing. Signed-off-by: Tom DNetto Updates ENG-991 --- control/controlclient/map.go | 8 ++++++++ control/controlclient/map_test.go | 12 ++++++++++++ ipn/ipnlocal/peerapi.go | 3 +++ net/tstun/wrap.go | 8 ++++---- net/tstun/wrap_test.go | 2 +- tailcfg/tailcfg.go | 16 ++++++++++++++++ tailcfg/tailcfg_clone.go | 4 ++++ tailcfg/tailcfg_test.go | 12 +++++++++++- tailcfg/tailcfg_view.go | 9 +++++++++ tstest/integration/testcontrol/testcontrol.go | 10 +++++++--- wgengine/wgcfg/config.go | 1 + wgengine/wgcfg/nmcfg/nmcfg.go | 1 + wgengine/wgcfg/wgcfg_clone.go | 4 ++++ 13 files changed, 81 insertions(+), 9 deletions(-) diff --git a/control/controlclient/map.go b/control/controlclient/map.go index df3fe6429..8623208b7 100644 --- a/control/controlclient/map.go +++ b/control/controlclient/map.go @@ -703,6 +703,14 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang if va == nil || vb == nil || *va != *vb { return nil, false } + case "SelfNodeV6MasqAddrForThisPeer": + va, vb := was.SelfNodeV6MasqAddrForThisPeer(), n.SelfNodeV6MasqAddrForThisPeer + if va == nil && vb == nil { + continue + } + if va == nil || vb == nil || *va != *vb { + return nil, false + } case "ExitNodeDNSResolvers": va, vb := was.ExitNodeDNSResolvers(), views.SliceOfViews(n.ExitNodeDNSResolvers) diff --git a/control/controlclient/map_test.go b/control/controlclient/map_test.go index 2b9b0ac6e..26d01d424 100644 --- a/control/controlclient/map_test.go +++ b/control/controlclient/map_test.go @@ -736,6 +736,18 @@ func TestPeerChangeDiff(t *testing.T) { a: &tailcfg.Node{ID: 1, User: 1}, b: &tailcfg.Node{ID: 1, User: 2}, want: nil, + }, + { + name: "miss-change-masq-v4", + a: &tailcfg.Node{ID: 1, SelfNodeV4MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("100.64.0.1"))}, + b: &tailcfg.Node{ID: 1, SelfNodeV4MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("100.64.0.2"))}, + want: nil, + }, + { + name: "miss-change-masq-v6", + a: &tailcfg.Node{ID: 1, SelfNodeV6MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("2001::3456"))}, + b: &tailcfg.Node{ID: 1, SelfNodeV6MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("2001::3006"))}, + want: nil, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 855c34e1a..bf9708751 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -612,6 +612,9 @@ func (h *peerAPIHandler) isAddressValid(addr netip.Addr) bool { if v := h.peerNode.SelfNodeV4MasqAddrForThisPeer(); v != nil { return *v == addr } + if v := h.peerNode.SelfNodeV6MasqAddrForThisPeer(); v != nil { + return *v == addr + } pfx := netip.PrefixFrom(addr, addr.BitLen()) return views.SliceContains(h.selfNode.Addresses(), pfx) } diff --git a/net/tstun/wrap.go b/net/tstun/wrap.go index d2c5b32f6..a5daa5070 100644 --- a/net/tstun/wrap.go +++ b/net/tstun/wrap.go @@ -98,7 +98,7 @@ type Wrapper struct { // timeNow, if non-nil, will be used to obtain the current time. timeNow func() time.Time - // natV4Config stores the current NAT configuration. + // natV4Config stores the current IPv4 NAT configuration. natV4Config atomic.Pointer[natV4Config] // vectorBuffer stores the oldest unconsumed packet vector from tdev. It is @@ -577,9 +577,9 @@ func (c *natV4Config) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr { return oldSrc } -// natConfigFromWireGuardConfig generates a natV4Config from nm. +// natV4ConfigFromWGConfig generates a natV4Config from nm. // If v4 NAT is not required, it returns nil. -func natConfigFromWGConfig(wcfg *wgcfg.Config) *natV4Config { +func natV4ConfigFromWGConfig(wcfg *wgcfg.Config) *natV4Config { if wcfg == nil { return nil } @@ -632,7 +632,7 @@ func natConfigFromWGConfig(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 := natConfigFromWGConfig(wcfg) + cfg := natV4ConfigFromWGConfig(wcfg) old := t.natV4Config.Swap(cfg) if !reflect.DeepEqual(old, cfg) { t.logf("nat config: %+v", cfg) diff --git a/net/tstun/wrap_test.go b/net/tstun/wrap_test.go index f9e35beec..7560c9aee 100644 --- a/net/tstun/wrap_test.go +++ b/net/tstun/wrap_test.go @@ -780,7 +780,7 @@ func TestNATCfg(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - ncfg := natConfigFromWGConfig(tc.wcfg) + 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) diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index a45a09b94..a1d8befa3 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -374,6 +374,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. @@ -1940,6 +1955,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 1a90838cf..6a2292149 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -71,6 +71,9 @@ func (src *Node) Clone() *Node { if dst.SelfNodeV4MasqAddrForThisPeer != nil { dst.SelfNodeV4MasqAddrForThisPeer = ptr.To(*src.SelfNodeV4MasqAddrForThisPeer) } + if dst.SelfNodeV6MasqAddrForThisPeer != nil { + dst.SelfNodeV6MasqAddrForThisPeer = ptr.To(*src.SelfNodeV6MasqAddrForThisPeer) + } if src.ExitNodeDNSResolvers != nil { dst.ExitNodeDNSResolvers = make([]*dnstype.Resolver, len(src.ExitNodeDNSResolvers)) for i := range dst.ExitNodeDNSResolvers { @@ -113,6 +116,7 @@ var _NodeCloneNeedsRegeneration = Node(struct { DataPlaneAuditLogID string Expired bool SelfNodeV4MasqAddrForThisPeer *netip.Addr + SelfNodeV6MasqAddrForThisPeer *netip.Addr IsWireGuardOnly bool ExitNodeDNSResolvers []*dnstype.Resolver }{}) diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index 6023fa08f..86f8f45ad 100644 --- a/tailcfg/tailcfg_test.go +++ b/tailcfg/tailcfg_test.go @@ -350,7 +350,7 @@ func TestNodeEqual(t *testing.T) { "UnsignedPeerAPIOnly", "ComputedName", "computedHostIfDifferent", "ComputedNameWithHost", "DataPlaneAuditLogID", "Expired", "SelfNodeV4MasqAddrForThisPeer", - "IsWireGuardOnly", "ExitNodeDNSResolvers", + "SelfNodeV6MasqAddrForThisPeer", "IsWireGuardOnly", "ExitNodeDNSResolvers", } 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", @@ -545,6 +545,16 @@ func TestNodeEqual(t *testing.T) { &Node{SelfNodeV4MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("100.64.0.1"))}, true, }, + { + &Node{}, + &Node{SelfNodeV6MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("2001::3456"))}, + false, + }, + { + &Node{SelfNodeV6MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("2001::3456"))}, + &Node{SelfNodeV6MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("2001::3456"))}, + true, + }, { &Node{ CapMap: NodeCapMap{ diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index d99d9b3b5..4a51f03f7 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -186,6 +186,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) ExitNodeDNSResolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] { return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.ExitNodeDNSResolvers) @@ -225,6 +233,7 @@ var _NodeViewNeedsRegeneration = Node(struct { DataPlaneAuditLogID string Expired bool SelfNodeV4MasqAddrForThisPeer *netip.Addr + SelfNodeV6MasqAddrForThisPeer *netip.Addr IsWireGuardOnly bool ExitNodeDNSResolvers []*dnstype.Resolver }{}) diff --git a/tstest/integration/testcontrol/testcontrol.go b/tstest/integration/testcontrol/testcontrol.go index 0d7c6e833..4a2f08921 100644 --- a/tstest/integration/testcontrol/testcontrol.go +++ b/tstest/integration/testcontrol/testcontrol.go @@ -66,7 +66,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 // suppressAutoMapResponses is the set of nodes that should not be sent // automatic map responses from serveMap. (They should only get manually sent ones) @@ -330,7 +330,7 @@ func (s *Server) serveMachine(w http.ResponseWriter, r *http.Request) { // Node masquerades as for the Peer. // // Setting this will have future MapResponses for Node to have -// Peer.SelfNodeV4MasqAddrForThisPeer set to NodeMasqueradesAs. +// Peer.SelfNodeV{4,6}MasqAddrForThisPeer set to NodeMasqueradesAs. // MapResponses for the Peer will now see Node.Addresses as // NodeMasqueradesAs. type MasqueradePair struct { @@ -889,7 +889,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.Is6() { + p.SelfNodeV6MasqAddrForThisPeer = ptr.To(masqIP) + } else { + p.SelfNodeV4MasqAddrForThisPeer = ptr.To(masqIP) + } } s.mu.Lock() 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 a7787e509..885d507fa 100644 --- a/wgengine/wgcfg/nmcfg/nmcfg.go +++ b/wgengine/wgcfg/nmcfg/nmcfg.go @@ -99,6 +99,7 @@ func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags, didExitNodeWarn := false cpeer.V4MasqAddr = peer.SelfNodeV4MasqAddrForThisPeer() + cpeer.V6MasqAddr = peer.SelfNodeV6MasqAddrForThisPeer() for i := range peer.AllowedIPs().LenIter() { allowedIP := peer.AllowedIPs().At(i) if allowedIP.Bits() == 0 && peer.StableID() != exitNode { diff --git a/wgengine/wgcfg/wgcfg_clone.go b/wgengine/wgcfg/wgcfg_clone.go index 939f08e3f..4a2288f1e 100644 --- a/wgengine/wgcfg/wgcfg_clone.go +++ b/wgengine/wgcfg/wgcfg_clone.go @@ -60,6 +60,9 @@ func (src *Peer) Clone() *Peer { if dst.V4MasqAddr != nil { dst.V4MasqAddr = ptr.To(*src.V4MasqAddr) } + if dst.V6MasqAddr != nil { + dst.V6MasqAddr = ptr.To(*src.V6MasqAddr) + } return dst } @@ -69,6 +72,7 @@ var _PeerCloneNeedsRegeneration = Peer(struct { DiscoKey key.DiscoPublic AllowedIPs []netip.Prefix V4MasqAddr *netip.Addr + V6MasqAddr *netip.Addr PersistentKeepalive uint16 WGEndpoint key.NodePublic }{})