Procházet zdrojové kódy

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 <[email protected]>
Updates ENG-991
Tom DNetto před 2 roky
rodič
revize
c08cf2a9c6

+ 8 - 0
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)
 

+ 12 - 0
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) {

+ 3 - 0
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)
 }

+ 4 - 4
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)

+ 1 - 1
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)

+ 16 - 0
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
 }
 

+ 4 - 0
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
 }{})

+ 11 - 1
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{

+ 9 - 0
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
 }{})

+ 7 - 3
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()

+ 1 - 0
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.

+ 1 - 0
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 {

+ 4 - 0
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
 }{})