From 656a77ab4e38cf557cdeacc1a8dde23d9ff4d4b7 Mon Sep 17 00:00:00 2001 From: Tom DNetto Date: Fri, 22 Sep 2023 13:15:49 -0700 Subject: [PATCH] net/packet: implement methods for rewriting v6 addresses Implements the ability for the address-rewriting code to support rewriting IPv6 addresses. Specifically, UpdateSrcAddr & UpdateDstAddr. Signed-off-by: Tom DNetto Updates https://github.com/tailscale/corp/issues/11202 --- cmd/derper/depaware.txt | 17 ++++++ cmd/tailscale/depaware.txt | 17 ++++++ cmd/tailscaled/depaware.txt | 8 +-- net/packet/packet.go | 86 +++++++++++++++++++++++------ net/packet/packet_test.go | 107 +++++++++++++++++++++++++++++++++++- 5 files changed, 213 insertions(+), 22 deletions(-) diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index 688f54a78..d2869845e 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -17,6 +17,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa github.com/fxamacker/cbor/v2 from tailscale.com/tka github.com/golang/groupcache/lru from tailscale.com/net/dnscache github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil + github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header L github.com/google/nftables from tailscale.com/util/linuxfw L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+ @@ -78,6 +79,22 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa google.golang.org/protobuf/runtime/protoimpl from github.com/golang/protobuf/proto+ google.golang.org/protobuf/types/descriptorpb from google.golang.org/protobuf/reflect/protodesc google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+ + gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/buffer+ + gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/buffer + 💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip+ + gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs + 💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+ + gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log + gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+ + gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/buffer + 💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+ + gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state + 💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/atomicbitops+ + gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+ + gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/buffer+ + gvisor.dev/gvisor/pkg/tcpip/header from tailscale.com/net/packet + gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header + gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+ nhooyr.io/websocket from tailscale.com/cmd/derper+ nhooyr.io/websocket/internal/errd from nhooyr.io/websocket nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 8b31d0658..86d48fc57 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -17,6 +17,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep github.com/fxamacker/cbor/v2 from tailscale.com/tka L 💣 github.com/godbus/dbus/v5 from github.com/coreos/go-systemd/v22/dbus github.com/golang/groupcache/lru from tailscale.com/net/dnscache + github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header L github.com/google/nftables from tailscale.com/util/linuxfw L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+ @@ -64,6 +65,22 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep go4.org/netipx from tailscale.com/wgengine/filter+ W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+ gopkg.in/yaml.v2 from sigs.k8s.io/yaml + gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/buffer+ + gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/buffer + 💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip+ + gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs + 💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+ + gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log + gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+ + gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/buffer + 💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+ + gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state + 💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/atomicbitops+ + gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+ + gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/buffer+ + gvisor.dev/gvisor/pkg/tcpip/header from tailscale.com/net/packet + gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header + gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+ k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli nhooyr.io/websocket from tailscale.com/derp/derphttp+ nhooyr.io/websocket/internal/errd from nhooyr.io/websocket diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index b5944ebd1..a11e47885 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -238,13 +238,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal tailscale.com/hostinfo from tailscale.com/control/controlclient+ tailscale.com/ipn from tailscale.com/ipn/ipnlocal+ - 💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnserver+ + 💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+ tailscale.com/ipn/ipnlocal from tailscale.com/ssh/tailssh+ tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+ tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal - tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+ + tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+ L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store tailscale.com/ipn/store/mem from tailscale.com/ipn/store+ @@ -347,7 +347,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+ W tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth tailscale.com/util/racebuild from tailscale.com/logpolicy - tailscale.com/util/rands from tailscale.com/ipn/localapi+ + tailscale.com/util/rands from tailscale.com/ipn/ipnlocal+ tailscale.com/util/ringbuffer from tailscale.com/wgengine/magicsock tailscale.com/util/set from tailscale.com/health+ tailscale.com/util/singleflight from tailscale.com/control/controlclient+ @@ -508,7 +508,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de regexp from github.com/coreos/go-iptables/iptables+ regexp/syntax from regexp runtime/debug from github.com/klauspost/compress/zstd+ - runtime/pprof from net/http/pprof+ + runtime/pprof from tailscale.com/ipn/ipnlocal+ runtime/trace from net/http/pprof slices from tailscale.com/wgengine/magicsock+ sort from compress/flate+ diff --git a/net/packet/packet.go b/net/packet/packet.go index e3784a571..9760005ec 100644 --- a/net/packet/packet.go +++ b/net/packet/packet.go @@ -10,6 +10,8 @@ import ( "net/netip" "strings" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" "tailscale.com/net/netaddr" "tailscale.com/types/ipproto" ) @@ -453,11 +455,14 @@ func (q *Parsed) IsEchoResponse() bool { } // UpdateSrcAddr updates the source address in the packet buffer (e.g. during -// SNAT). It also updates the checksum. Currently (2022-12-10) only TCP/UDP/ICMP -// over IPv4 is supported. It panics if called with IPv6 addr. +// SNAT). It also updates the checksum. Currently (2023-09-22) only TCP/UDP/ICMP +// is supported. It panics if provided with an address in a different +// family to the parsed packet. func (q *Parsed) UpdateSrcAddr(src netip.Addr) { - if q.IPVersion != 4 || src.Is6() { - panic("UpdateSrcAddr: only IPv4 is supported") + if src.Is6() && q.IPVersion != 6 { + panic("UpdateSrcAddr: cannot write IPv6 address to v4 packet") + } else if src.Is4() && q.IPVersion != 4 { + panic("UpdateSrcAddr: cannot write IPv4 address to v6 packet") } q.CaptureMeta.DidSNAT = true q.CaptureMeta.OriginalSrc = q.Src @@ -466,19 +471,27 @@ func (q *Parsed) UpdateSrcAddr(src netip.Addr) { q.Src = netip.AddrPortFrom(src, q.Src.Port()) b := q.Buffer() - v4 := src.As4() - copy(b[12:16], v4[:]) - updateV4PacketChecksums(q, old, src) + if src.Is6() { + v6 := src.As16() + copy(b[8:24], v6[:]) + updateV6PacketChecksums(q, old, src) + } else { + v4 := src.As4() + copy(b[12:16], v4[:]) + updateV4PacketChecksums(q, old, src) + } } -// UpdateDstAddr updates the source address in the packet buffer (e.g. during +// UpdateDstAddr updates the destination address in the packet buffer (e.g. during // DNAT). It also updates the checksum. Currently (2022-12-10) only TCP/UDP/ICMP -// over IPv4 is supported. It panics if called with IPv6 addr. +// is supported. It panics if provided with an address in a different +// family to the parsed packet. func (q *Parsed) UpdateDstAddr(dst netip.Addr) { - if q.IPVersion != 4 || dst.Is6() { - panic("UpdateDstAddr: only IPv4 is supported") + if dst.Is6() && q.IPVersion != 6 { + panic("UpdateDstAddr: cannot write IPv6 address to v4 packet") + } else if dst.Is4() && q.IPVersion != 4 { + panic("UpdateDstAddr: cannot write IPv4 address to v6 packet") } - q.CaptureMeta.DidDNAT = true q.CaptureMeta.OriginalDst = q.Dst @@ -486,9 +499,15 @@ func (q *Parsed) UpdateDstAddr(dst netip.Addr) { q.Dst = netip.AddrPortFrom(dst, q.Dst.Port()) b := q.Buffer() - v4 := dst.As4() - copy(b[16:20], v4[:]) - updateV4PacketChecksums(q, old, dst) + if dst.Is6() { + v6 := dst.As16() + copy(b[24:36], v6[:]) + updateV6PacketChecksums(q, old, dst) + } else { + v4 := dst.As4() + copy(b[16:20], v4[:]) + updateV4PacketChecksums(q, old, dst) + } } // EchoIDSeq extracts the identifier/sequence bytes from an ICMP Echo response, @@ -572,13 +591,13 @@ func updateV4PacketChecksums(p *Parsed, old, new netip.Addr) { tr := p.Transport() switch p.IPProto { case ipproto.UDP, ipproto.DCCP: - if len(tr) < 8 { + if len(tr) < header.UDPMinimumSize { // Not enough space for a UDP header. return } updateV4Checksum(tr[6:8], o4[:], n4[:]) case ipproto.TCP: - if len(tr) < 18 { + if len(tr) < header.TCPMinimumSize { // Not enough space for a TCP header. return } @@ -596,6 +615,39 @@ func updateV4PacketChecksums(p *Parsed, old, new netip.Addr) { } } +// updateV6PacketChecksums updates the checksums in the packet buffer. +// p is modified in place. +// If p.IPProto is unknown, no checksums are updated. +func updateV6PacketChecksums(p *Parsed, old, new netip.Addr) { + if len(p.Buffer()) < 40 { + // Not enough space for an IPv6 header. + return + } + o6, n6 := tcpip.AddrFrom16Slice(old.AsSlice()), tcpip.AddrFrom16Slice(new.AsSlice()) + + // Now update the transport layer checksums, where applicable. + tr := p.Transport() + switch p.IPProto { + case ipproto.ICMPv6: + if len(tr) < header.ICMPv6MinimumSize { + return + } + header.ICMPv6(tr).UpdateChecksumPseudoHeaderAddress(o6, n6) + case ipproto.UDP, ipproto.DCCP: + if len(tr) < header.UDPMinimumSize { + return + } + header.UDP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true) + case ipproto.TCP: + if len(tr) < header.TCPMinimumSize { + return + } + header.TCP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true) + case ipproto.SCTP: + // No transport layer update required. + } +} + // updateV4Checksum calculates and updates the checksum in the packet buffer for // a change between old and new. The oldSum must point to the 16-bit checksum // field in the packet buffer that holds the old checksum value, it will be diff --git a/net/packet/packet_test.go b/net/packet/packet_test.go index 4bc2be77a..553fd11f4 100644 --- a/net/packet/packet_test.go +++ b/net/packet/packet_test.go @@ -13,6 +13,9 @@ import ( "testing" "unicode" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/checksum" + "gvisor.dev/gvisor/pkg/tcpip/header" "tailscale.com/tstest" "tailscale.com/types/ipproto" "tailscale.com/util/must" @@ -45,7 +48,7 @@ func fullHeaderChecksumV4(b []byte) uint16 { return ^uint16(s) } -func TestHeaderChecksums(t *testing.T) { +func TestHeaderChecksumsV4(t *testing.T) { // This is not a good enough test, because it doesn't // check the various packet types or the many edge cases // of the checksum algorithm. But it's a start. @@ -109,6 +112,108 @@ func TestHeaderChecksums(t *testing.T) { } } +func TestNatChecksumsV6UDP(t *testing.T) { + a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1") + + // Make a fake UDP packet with 32 bytes of zeros as the datagram payload. + b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.UDPMinimumSize+32)) + b.Encode(&header.IPv6Fields{ + PayloadLength: header.UDPMinimumSize + 32, + TransportProtocol: header.UDPProtocolNumber, + HopLimit: 16, + SrcAddr: tcpip.AddrFrom16Slice(a1.AsSlice()), + DstAddr: tcpip.AddrFrom16Slice(a2.AsSlice()), + }) + udp := header.UDP(b[header.IPv6MinimumSize:]) + udp.Encode(&header.UDPFields{ + SrcPort: 42, + DstPort: 43, + Length: header.UDPMinimumSize + 32, + }) + xsum := header.PseudoHeaderChecksum( + header.UDPProtocolNumber, + tcpip.AddrFrom16Slice(a1.AsSlice()), + tcpip.AddrFrom16Slice(a2.AsSlice()), + uint16(header.UDPMinimumSize+32), + ) + xsum = checksum.Checksum(b.Payload()[header.UDPMinimumSize:], xsum) + udp.SetChecksum(^udp.CalculateChecksum(xsum)) + if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a1.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) { + t.Fatal("test broken; initial packet has incorrect checksum") + } + + // Parse the packet. + var p Parsed + p.Decode(b) + t.Log(p.String()) + + // Update the source address of the packet to be the same as the dest. + p.UpdateSrcAddr(a2) + if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) { + t.Fatal("incorrect checksum after updating source address") + } + + // Update the dest address of the packet to be the original source address. + p.UpdateDstAddr(a1) + if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) { + t.Fatal("incorrect checksum after updating destination address") + } +} + +func TestNatChecksumsV6TCP(t *testing.T) { + a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1") + + // Make a fake TCP packet with no payload. + b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.TCPMinimumSize)) + b.Encode(&header.IPv6Fields{ + PayloadLength: header.TCPMinimumSize, + TransportProtocol: header.TCPProtocolNumber, + HopLimit: 16, + SrcAddr: tcpip.AddrFrom16Slice(a1.AsSlice()), + DstAddr: tcpip.AddrFrom16Slice(a2.AsSlice()), + }) + tcp := header.TCP(b[header.IPv6MinimumSize:]) + tcp.Encode(&header.TCPFields{ + SrcPort: 42, + DstPort: 43, + SeqNum: 1, + AckNum: 2, + DataOffset: header.TCPMinimumSize, + Flags: 3, + WindowSize: 4, + Checksum: 0, + UrgentPointer: 5, + }) + xsum := header.PseudoHeaderChecksum( + header.TCPProtocolNumber, + tcpip.AddrFrom16Slice(a1.AsSlice()), + tcpip.AddrFrom16Slice(a2.AsSlice()), + uint16(header.TCPMinimumSize), + ) + tcp.SetChecksum(^tcp.CalculateChecksum(xsum)) + + if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a1.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), 0, 0) { + t.Fatal("test broken; initial packet has incorrect checksum") + } + + // Parse the packet. + var p Parsed + p.Decode(b) + t.Log(p.String()) + + // Update the source address of the packet to be the same as the dest. + p.UpdateSrcAddr(a2) + if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), 0, 0) { + t.Fatal("incorrect checksum after updating source address") + } + + // Update the dest address of the packet to be the original source address. + p.UpdateDstAddr(a1) + if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), 0, 0) { + t.Fatal("incorrect checksum after updating destination address") + } +} + func mustIPPort(s string) netip.AddrPort { ipp, err := netip.ParseAddrPort(s) if err != nil {