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 <tom@tailscale.com>
Updates https://github.com/tailscale/corp/issues/11202
irbekrm/kubeipnft
Tom DNetto 1 year ago committed by Tom
parent c26d91d6bd
commit 656a77ab4e

@ -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

@ -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

@ -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+

@ -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()
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()
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

@ -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 {

Loading…
Cancel
Save