From b6c6c59dde58194f370be50a39ca24339406dbbd Mon Sep 17 00:00:00 2001 From: Mazdak Nasab Date: Wed, 23 Sep 2026 13:52:21 -0700 Subject: [PATCH] feature/conn25: extend datapath test to verify full packets (#21422) Extend datapath tests to not only assert headers but also the full expected packets. Fixes https://github.com/tailscale/corp/issues/48609 Change-Id: I0e9a1c2f61f2d63eafa430d8e6436908fe927cf4 Signed-off-by: Mazdak Nasab --- feature/conn25/datapath_test.go | 136 +++++++++++++++++++------------- 1 file changed, 82 insertions(+), 54 deletions(-) diff --git a/feature/conn25/datapath_test.go b/feature/conn25/datapath_test.go index 5eb256c97..b8eeb2609 100644 --- a/feature/conn25/datapath_test.go +++ b/feature/conn25/datapath_test.go @@ -100,6 +100,59 @@ func (tn testNet) udpPacket(src, dst netip.AddrPort) *packet.Parsed { return p } +func (tn testNet) checkPacket(t *testing.T, p *packet.Parsed, src, dst netip.AddrPort) { + t.Helper() + + if got, want := p.Src, src; got != want { + t.Errorf("unexpected packet src: got %v, want %v", got, want) + } + if got, want := p.Dst, dst; got != want { + t.Errorf("unexpected packet dst: got %v, want %v", got, want) + } + if got, want := p.Buffer(), tn.udpPacket(src, dst).Buffer(); !bytes.Equal(got, want) { + t.Errorf("unexpected packet bytes:\n got %+x\nwant %+x", got, want) + } +} + +func (tn testNet) checkICMPUnreachable(t *testing.T, got []byte, from, to netip.Addr, invoking *packet.Parsed) { + t.Helper() + + var p packet.Parsed + p.Decode(got) + if !p.IsError() { + t.Errorf("injected packet is not an ICMP error") + } + if got, want := p.Src.Addr(), from; got != want { + t.Errorf("injected packet src: got %v, want %v", got, want) + } + if got, want := p.Dst.Addr(), to; got != want { + t.Errorf("injected packet dst: got %v, want %v", got, want) + } + + var want []byte + if tn.ipVersion == 6 { + // RFC 4443: as much of the invoking packet as fits + // without exceeding the minimum IPv6 MTU. The test packets are + // far below that, so the whole packet is quoted. + want = packet.Generate(packet.ICMP6Header{ + IP6Header: packet.IP6Header{Src: from, Dst: to}, + Type: packet.ICMP6Unreachable, + Code: packet.ICMP6AddressUnreachable, + }, append(make([]byte, 4), invoking.Buffer()...)) + } else { + // RFC 792: the IPv4 header, which is 20 bytes for the option-less + // test packets, plus the first 64 bits of the datagram. + want = packet.Generate(packet.ICMP4Header{ + IP4Header: packet.IP4Header{Src: from, Dst: to}, + Type: packet.ICMP4Unreachable, + Code: packet.ICMP4HostUnreachable, + }, append(make([]byte, 4), invoking.Buffer()[:20+8]...)) + } + if !bytes.Equal(got, want) { + t.Errorf("unexpected injected packet bytes:\n got %+x\nwant %+x", got, want) + } +} + func (tn testNet) newClientDatapath(t *testing.T, throwMappingErr bool) *datapathHandler { t.Helper() @@ -204,12 +257,7 @@ func TestHandlePacketFromTunDevice(t *testing.T) { if want, got := tt.expectedFilterResponse, dph.HandlePacketFromTunDevice(p, tun); want != got { t.Errorf("unexpected filter response: want %v, got %v", want, got) } - if want, got := tt.expectedSrc, p.Src; want != got { - t.Errorf("unexpected packet src: want %v, got %v", want, got) - } - if want, got := tt.expectedDst, p.Dst; want != got { - t.Errorf("unexpected packet dst: want %v, got %v", want, got) - } + tn.checkPacket(t, p, tt.expectedSrc, tt.expectedDst) }) } }) @@ -225,11 +273,6 @@ func TestUnmappedMagicIPICMPUnreachable(t *testing.T) { for _, tn := range testNets { t.Run(tn.name, func(t *testing.T) { - wantProto := ipproto.ICMPv4 - if tn.ipVersion == 6 { - wantProto = ipproto.ICMPv6 - } - dph := tn.newClientDatapath(t, false) chtun, tun := newChannelTUN(t) @@ -238,36 +281,26 @@ func TestUnmappedMagicIPICMPUnreachable(t *testing.T) { gotInboundPacketChan := make(chan []byte, 1) go func() { gotInboundPacketChan <- <-chtun.Inbound }() - p := tn.udpPacket( - netip.AddrPortFrom(tn.clientSrcIP, clientPort), - netip.AddrPortFrom(tn.unusedMagicIP, serverPort), - ) + src := netip.AddrPortFrom(tn.clientSrcIP, clientPort) + dst := netip.AddrPortFrom(tn.unusedMagicIP, serverPort) + p := tn.udpPacket(src, dst) if got, want := dph.HandlePacketFromTunDevice(p, tun), filter.Drop; got != want { t.Fatalf("unexpected filter response: got %v, want %v", got, want) } + // The dropped packet itself must be left untouched, since the + // injected error quotes it. + tn.checkPacket(t, p, src, dst) - var injected packet.Parsed + var injected []byte select { - case b := <-gotInboundPacketChan: - injected.Decode(b) + case injected = <-gotInboundPacketChan: case <-time.After(1 * time.Second): t.Fatal("timed out waiting for injected ICMP packet") } - if !injected.IsError() { - t.Errorf("injected packet is not an ICMP error") - } - if got := injected.IPProto; got != wantProto { - t.Errorf("injected packet proto: got %v, want %v", got, wantProto) - } // The error should appear to come from the unreachable Magic IP, // addressed back to the original sender. - if got, want := injected.Src.Addr(), tn.unusedMagicIP; got != want { - t.Errorf("injected packet src: got %v, want %v", got, want) - } - if got, want := injected.Dst.Addr(), tn.clientSrcIP; got != want { - t.Errorf("injected packet dst: got %v, want %v", got, want) - } + tn.checkICMPUnreachable(t, injected, tn.unusedMagicIP, tn.clientSrcIP, p) }) } } @@ -347,12 +380,7 @@ func TestHandlePacketFromWireGuard(t *testing.T) { if want, got := tt.expectedFilterResponse, dph.HandlePacketFromWireGuard(p, tun); want != got { t.Errorf("unexpected filter response: want %v, got %v", want, got) } - if want, got := tt.expectedSrc, p.Src; want != got { - t.Errorf("unexpected packet src: want %v, got %v", want, got) - } - if want, got := tt.expectedDst, p.Dst; want != got { - t.Errorf("unexpected packet dst: want %v, got %v", want, got) - } + tn.checkPacket(t, p, tt.expectedSrc, tt.expectedDst) if tt.expectedInjectedPkt != nil { slab := make([]byte, (2*wgtun.ReadPacketSpacing)+(2*(1<<16-1))) packets := make([]wgtun.ReadPacket, 1) @@ -402,17 +430,17 @@ func TestClientFlowCache(t *testing.T) { if dph.HandlePacketFromTunDevice(o1, tun) != filter.Accept { t.Errorf("first call to HandlePacketFromTunDevice was not accepted") } - if want, got := netip.AddrPortFrom(tn.transitIP, serverPort), o1.Dst; want != got { - t.Errorf("unexpected packet dst after first call: want %v, got %v", want, got) - } + tn.checkPacket(t, o1, + netip.AddrPortFrom(tn.clientSrcIP, clientPort), + netip.AddrPortFrom(tn.transitIP, serverPort)) // The second call should use the cache. o2 := newOutgoing() if dph.HandlePacketFromTunDevice(o2, tun) != filter.Accept { t.Errorf("second call to HandlePacketFromTunDevice was not accepted") } - if want, got := netip.AddrPortFrom(tn.transitIP, serverPort), o2.Dst; want != got { - t.Errorf("unexpected packet dst after second call: want %v, got %v", want, got) - } + tn.checkPacket(t, o2, + netip.AddrPortFrom(tn.clientSrcIP, clientPort), + netip.AddrPortFrom(tn.transitIP, serverPort)) // Return traffic should have the Transit IP as the source, // and be SNATed to the Magic IP. @@ -424,9 +452,9 @@ func TestClientFlowCache(t *testing.T) { if dph.HandlePacketFromWireGuard(incoming, tun) != filter.Accept { t.Errorf("call to HandlePacketFromWireGuard was not accepted") } - if want, got := netip.AddrPortFrom(tn.magicIP, serverPort), incoming.Src; want != got { - t.Errorf("unexpected packet src after second call: want %v, got %v", want, got) - } + tn.checkPacket(t, incoming, + netip.AddrPortFrom(tn.magicIP, serverPort), + netip.AddrPortFrom(tn.clientSrcIP, clientPort)) }) } } @@ -460,17 +488,17 @@ func TestConnectorFlowCache(t *testing.T) { if dph.HandlePacketFromWireGuard(o1, tun) != filter.Accept { t.Errorf("first call to HandlePacketFromWireGuard was not accepted") } - if want, got := netip.AddrPortFrom(tn.realIP, serverPort), o1.Dst; want != got { - t.Errorf("unexpected packet dst after first call: want %v, got %v", want, got) - } + tn.checkPacket(t, o1, + netip.AddrPortFrom(tn.clientSrcIP, clientPort), + netip.AddrPortFrom(tn.realIP, serverPort)) // The second call should use the cache. o2 := newOutgoing() if dph.HandlePacketFromWireGuard(o2, tun) != filter.Accept { t.Errorf("second call to HandlePacketFromWireGuard was not accepted") } - if want, got := netip.AddrPortFrom(tn.realIP, serverPort), o2.Dst; want != got { - t.Errorf("unexpected packet dst after second call: want %v, got %v", want, got) - } + tn.checkPacket(t, o2, + netip.AddrPortFrom(tn.clientSrcIP, clientPort), + netip.AddrPortFrom(tn.realIP, serverPort)) // Return traffic should have the Real IP as the source, // and be SNATed to the Transit IP. @@ -482,9 +510,9 @@ func TestConnectorFlowCache(t *testing.T) { if dph.HandlePacketFromTunDevice(incoming, tun) != filter.Accept { t.Errorf("call to HandlePacketFromTunDevice was not accepted") } - if want, got := netip.AddrPortFrom(tn.transitIP, serverPort), incoming.Src; want != got { - t.Errorf("unexpected packet src after second call: want %v, got %v", want, got) - } + tn.checkPacket(t, incoming, + netip.AddrPortFrom(tn.transitIP, serverPort), + netip.AddrPortFrom(tn.clientSrcIP, clientPort)) }) } }