summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJames Tucker <james@tailscale.com>2023-06-20 17:31:49 -0700
committerJames Tucker <james@tailscale.com>2023-06-20 17:31:49 -0700
commit31a5843f79e05d394bb503341c1f870ecc88ddf4 (patch)
tree21185ba07a3226d43bffb782a38c1f9db240111a
parent0f5090c526c2019fae94695b2991cb561e131788 (diff)
downloadtailscale-raggi/v6masq.tar.xz
tailscale-raggi/v6masq.zip
ipn,net/tstun,tailcfg,tstest,wgengine/: add support for IPv6 masqueraderaggi/v6masq
We have existing support for IPv4 masquerade, this adds the IPv6 counterpart. Updates tailscale/corp#11202 Updates tailscale/corp#11409 Signed-off-by: James Tucker <james@tailscale.com>
-rw-r--r--ipn/ipnlocal/peerapi.go3
-rw-r--r--net/tstun/wrap.go2
-rw-r--r--net/tstun/wrap_test.go1
-rw-r--r--tailcfg/tailcfg.go16
-rw-r--r--tailcfg/tailcfg_clone.go5
-rw-r--r--tailcfg/tailcfg_test.go2
-rw-r--r--tailcfg/tailcfg_view.go9
-rw-r--r--tstest/integration/testcontrol/testcontrol.go8
-rw-r--r--wgengine/magicsock/magicsock_test.go133
-rw-r--r--wgengine/wgcfg/config.go1
-rw-r--r--wgengine/wgcfg/nmcfg/nmcfg.go1
-rw-r--r--wgengine/wgcfg/wgcfg_clone.go5
12 files changed, 125 insertions, 61 deletions
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()
-
- tskey := key.NewNode()
- tsaip := netip.MustParsePrefix("100.111.222.111/32")
-
- wgkey := key.NewNode()
- wgaip := netip.MustParsePrefix("10.64.0.1/32")
+ check := func(t *testing.T, tsaip, wgaip, masqip netip.Prefix) {
+ tskey := key.NewNode()
+ wgkey := key.NewNode()
- // the ip that the wireguard peer has in allowed ips and expects as a masq source
- masqip := netip.MustParsePrefix("10.64.0.2/32")
+ derpMap, cleanup := runDERPAndStun(t, t.Logf, localhostListener{}, netaddr.IPv4(127, 0, 0, 1))
+ defer cleanup()
- 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)
+ 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()
+ 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)
+ }
+ if masqip.Addr().Is4() {
+ nm.Peers[0].SelfNodeV4MasqAddrForThisPeer = ptr.To(masqip.Addr())
+ } else {
+ nm.Peers[0].SelfNodeV6MasqAddrForThisPeer = ptr.To(masqip.Addr())
+ }
+ m.conn.SetNetworkMap(nm)
- pbuf := tuntest.Ping(wgaip.Addr(), tsaip.Addr())
- m.tun.Outbound <- pbuf
+ cfg, err := nmcfg.WGCfg(nm, t.Logf, netmap.AllowSingleHosts|netmap.AllowSubnetRoutes, "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ m.Reconfig(cfg)
- select {
- case p := <-wgtun.Inbound:
+ pbuf := tuntest.Ping(wgaip.Addr(), tsaip.Addr())
+ m.tun.Outbound <- pbuf
- // TODO(raggi): move to a bytes.Equal based test later, once
- // tuntest.Ping produces correct checksums!
+ 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 pkt.Dst.Addr() != wgaip.Addr() {
- t.Fatalf("bad source IP, got %s, want %s", pkt.Src.Addr(), masqip.Addr())
+ 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
}{})