net/tstun: add inital support for NAT v4

This adds support in tstun to utitilize the SelfNodeV4MasqAddrForThisPeer and
perform the necessary modifications to the packet as it passes through tstun.

Currently this only handles ICMP, UDP and TCP traffic.
Subnet routers and Exit Nodes are also unsupported.

Updates tailscale/corp#8020

Co-authored-by: Melanie Warrick <warrick@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
Maisem Ali
2022-12-10 01:13:41 +05:00
committed by Maisem Ali
parent 535fad16f8
commit bb31fd7d1c
4 changed files with 384 additions and 7 deletions

View File

@@ -29,7 +29,10 @@ import (
"tailscale.com/types/ipproto"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/types/views"
"tailscale.com/util/clientmetric"
"tailscale.com/util/mak"
"tailscale.com/wgengine/capture"
"tailscale.com/wgengine/filter"
)
@@ -88,6 +91,9 @@ type Wrapper struct {
destMACAtomic syncs.AtomicValue[[6]byte]
discoKey syncs.AtomicValue[key.DiscoPublic]
// natV4Config stores the current NAT configuration.
natV4Config atomic.Pointer[natV4Config]
// vectorBuffer stores the oldest unconsumed packet vector from tdev. It is
// allocated in wrap() and the underlying arrays should never grow.
vectorBuffer [][]byte
@@ -459,6 +465,139 @@ 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()
oldSrc := p.Src.Addr()
newSrc := nc.selectSrcIP(oldSrc, p.Dst.Addr())
if oldSrc != newSrc {
p.UpdateSrcAddr(newSrc)
}
}
// 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()
oldDst := p.Dst.Addr()
newDst := nc.mapDstIP(oldDst)
if newDst != oldDst {
p.UpdateDstAddr(newDst)
}
}
// findV4 returns the first Tailscale IPv4 address in addrs.
func findV4(addrs []netip.Prefix) netip.Addr {
for _, ap := range addrs {
a := ap.Addr()
if a.Is4() && tsaddr.IsTailscaleIP(a) {
return a
}
}
return netip.Addr{}
}
// natV4Config is the configuration for IPv4 NAT.
// 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.
nativeAddr netip.Addr
// listenAddrs is the set of IPv4 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{}
// dstMasqAddrs is map of dst addresses to their respective MasqueradeAsIP
// addresses. The MasqueradeAsIP address is the address that should be used
// as the source address for packets to dst.
dstMasqAddrs views.Map[netip.Addr, netip.Addr] // dst -> masqAddr
// TODO(maisem/nyghtowl): add support for subnets and exit nodes and test them.
// Determine IP routing table algorithm to use - e.g. ART?
}
// 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 {
if c == nil {
return oldDst
}
if _, ok := c.listenAddrs.GetOk(oldDst); ok {
return c.nativeAddr
}
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 *natV4Config) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr {
if c == nil {
return oldSrc
}
if oldSrc != c.nativeAddr {
return oldSrc
}
if eip, ok := c.dstMasqAddrs.GetOk(dst); ok {
return eip
}
return oldSrc
}
// natConfigFromNetMap generates a natV4Config from nm.
// If v4 NAT is not required, it returns nil.
func natConfigFromNetMap(nm *netmap.NetworkMap) *natV4Config {
if nm == nil || nm.SelfNode == nil {
return nil
}
nativeAddr := findV4(nm.SelfNode.Addresses)
if !nativeAddr.IsValid() {
return nil
}
var (
dstMasqAddrs map[netip.Addr]netip.Addr
listenAddrs map[netip.Addr]struct{}
)
for _, p := range nm.Peers {
if !p.SelfNodeV4MasqAddrForThisPeer.IsValid() {
continue
}
peerV4 := findV4(p.Addresses)
if !peerV4.IsValid() {
continue
}
mak.Set(&dstMasqAddrs, peerV4, p.SelfNodeV4MasqAddrForThisPeer)
mak.Set(&listenAddrs, p.SelfNodeV4MasqAddrForThisPeer, struct{}{})
}
if len(listenAddrs) == 0 || len(dstMasqAddrs) == 0 {
return nil
}
return &natV4Config{
nativeAddr: nativeAddr,
listenAddrs: views.MapOf(listenAddrs),
dstMasqAddrs: views.MapOf(dstMasqAddrs),
}
}
// SetNetMap is called when a new NetworkMap is received.
// It currently (2023-03-01) only updates the IPv4 NAT configuration.
func (t *Wrapper) SetNetMap(nm *netmap.NetworkMap) {
cfg := natConfigFromNetMap(nm)
t.natV4Config.Store(cfg)
t.logf("nat config: %+v", cfg)
}
var (
magicDNSIPPort = netip.AddrPortFrom(tsaddr.TailscaleServiceIP(), 0) // 100.100.100.100:0
magicDNSIPPortv6 = netip.AddrPortFrom(tsaddr.TailscaleServiceIPv6(), 0)
@@ -541,8 +680,8 @@ func (t *Wrapper) IdleDuration() time.Duration {
}
func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
// packet from OS read and sent to WG
res, ok := <-t.vectorOutbound
if !ok {
return 0, io.EOF
}
@@ -566,6 +705,8 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
defer parsedPacketPool.Put(p)
for _, data := range res.data {
p.Decode(data[res.dataOffset:])
t.snatV4(p)
if m := t.destIPActivity.Load(); m != nil {
if fn := m[p.Dst.Addr()]; fn != nil {
fn()
@@ -622,6 +763,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)
if m := t.destIPActivity.Load(); m != nil {
if fn := m[p.Dst.Addr()]; fn != nil {
@@ -738,11 +880,12 @@ func (t *Wrapper) filterPacketInboundFromWireGuard(p *packet.Parsed) filter.Resp
func (t *Wrapper) Write(buffs [][]byte, offset int) (int, error) {
metricPacketIn.Add(int64(len(buffs)))
i := 0
if !t.disableFilter {
p := parsedPacketPool.Get().(*packet.Parsed)
defer parsedPacketPool.Put(p)
for _, buff := range buffs {
p.Decode(buff[offset:])
p := parsedPacketPool.Get().(*packet.Parsed)
defer parsedPacketPool.Put(p)
for _, buff := range buffs {
p.Decode(buff[offset:])
t.dnatV4(p)
if !t.disableFilter {
if t.filterPacketInboundFromWireGuard(p) != filter.Accept {
metricPacketInDrop.Add(1)
} else {
@@ -750,7 +893,8 @@ func (t *Wrapper) Write(buffs [][]byte, offset int) (int, error) {
i++
}
}
} else {
}
if t.disableFilter {
i = len(buffs)
}
buffs = buffs[:i]
@@ -801,6 +945,10 @@ func (t *Wrapper) InjectInboundPacketBuffer(pkt stack.PacketBufferPtr) error {
if capt := t.captureHook.Load(); capt != nil {
capt(capture.SynthesizedToLocal, time.Now(), buf[PacketStartOffset:])
}
p := parsedPacketPool.Get().(*packet.Parsed)
defer parsedPacketPool.Put(p)
p.Decode(buf[PacketStartOffset:])
t.dnatV4(p)
return t.InjectInboundDirect(buf, PacketStartOffset)
}

View File

@@ -25,12 +25,14 @@ import (
"tailscale.com/net/connstats"
"tailscale.com/net/netaddr"
"tailscale.com/net/packet"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/tstime/mono"
"tailscale.com/types/ipproto"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netlogtype"
"tailscale.com/types/netmap"
"tailscale.com/util/must"
"tailscale.com/wgengine/filter"
)
@@ -593,3 +595,129 @@ func TestFilterDiscoLoop(t *testing.T) {
t.Errorf("log output mismatch\n got: %q\nwant: %q\n", got, want)
}
}
func TestNATCfg(t *testing.T) {
node := func(ip, eip netip.Addr) *tailcfg.Node {
return &tailcfg.Node{
Addresses: []netip.Prefix{
netip.PrefixFrom(ip, ip.BitLen()),
},
SelfNodeV4MasqAddrForThisPeer: eip,
}
}
var (
noIP netip.Addr
selfNativeIP = netip.MustParseAddr("100.64.0.1")
selfEIP1 = netip.MustParseAddr("100.64.1.1")
selfEIP2 = netip.MustParseAddr("100.64.1.2")
peer1IP = netip.MustParseAddr("100.64.0.2")
peer2IP = netip.MustParseAddr("100.64.0.3")
// subnets should not be impacted.
// TODO(maisem/nyghtowl): add support for subnets and exit nodes and test them.
subnet = netip.MustParseAddr("192.168.0.1")
)
tests := []struct {
name string
nm *netmap.NetworkMap
snatMap map[netip.Addr]netip.Addr // dst -> src
dnatMap map[netip.Addr]netip.Addr
}{
{
name: "no-netmap",
nm: nil,
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfNativeIP,
peer2IP: selfNativeIP,
subnet: selfNativeIP,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfEIP1,
selfEIP2: selfEIP2,
},
},
{
name: "single-peer-requires-nat",
nm: &netmap.NetworkMap{
SelfNode: node(selfNativeIP, noIP),
Peers: []*tailcfg.Node{
node(peer1IP, noIP),
node(peer2IP, selfEIP1),
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfNativeIP,
peer2IP: selfEIP1,
subnet: selfNativeIP,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfNativeIP,
selfEIP2: selfEIP2,
subnet: subnet,
},
},
{
name: "multiple-peers-require-nat",
nm: &netmap.NetworkMap{
SelfNode: node(selfNativeIP, noIP),
Peers: []*tailcfg.Node{
node(peer1IP, selfEIP1),
node(peer2IP, selfEIP2),
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfEIP1,
peer2IP: selfEIP2,
subnet: selfNativeIP,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfNativeIP,
selfEIP2: selfNativeIP,
subnet: subnet,
},
},
{
name: "no-nat",
nm: &netmap.NetworkMap{
SelfNode: node(selfNativeIP, noIP),
Peers: []*tailcfg.Node{
node(peer1IP, noIP),
node(peer2IP, noIP),
},
},
snatMap: map[netip.Addr]netip.Addr{
peer1IP: selfNativeIP,
peer2IP: selfNativeIP,
subnet: selfNativeIP,
},
dnatMap: map[netip.Addr]netip.Addr{
selfNativeIP: selfNativeIP,
selfEIP1: selfEIP1,
selfEIP2: selfEIP2,
subnet: subnet,
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ncfg := natConfigFromNetMap(tc.nm)
for peer, want := range tc.snatMap {
if got := ncfg.selectSrcIP(selfNativeIP, peer); got != want {
t.Errorf("selectSrcIP: got %v; want %v", got, want)
}
}
for dstIP, want := range tc.dnatMap {
if got := ncfg.mapDstIP(dstIP); got != want {
t.Errorf("mapDstIP: got %v; want %v", got, want)
}
}
})
}
}