Files
pcap-web-tools/internal/inet/fix_test.go
T
2026-10-05 20:07:31 +02:00

245 lines
8.5 KiB
Go

package inet
import (
"bytes"
"encoding/hex"
"strings"
"testing"
"pcap-web-tools/internal/pcap"
)
func mustHex(t *testing.T, s string) []byte {
t.Helper()
b, err := hex.DecodeString(strings.ReplaceAll(s, " ", ""))
if err != nil {
t.Fatal(err)
}
return b
}
// The want packets were generated by Fix and validated with tshark's checksum checks.
// Each test zeroes the length/checksum fields and checks Fix restores them.
func TestFix(t *testing.T) {
tests := []struct {
name string
want string
hdrLen int // header length passed to Fix (0 = unknown)
zero [][2]int // byte ranges cleared before calling Fix
changes int // expected number of changes, if not len(zero)
notes []string // expected substrings of the notes, in order
}{
{
name: "IPv4 UDP",
want: "45000028000140004011b970c0a80001c0a80002303904d20014117668656c6c6f2c207063617021",
zero: [][2]int{{2, 4}, {10, 12}, {24, 26}, {26, 28}},
},
{
name: "IPv4 with options, TCP, odd length payload",
want: "46000036123440004006118b0a0000010a00000201010100c350005000000001000000005018fffff9700000474554202f0d0a0d0a2e",
hdrLen: 44,
zero: [][2]int{{2, 4}, {10, 12}, {36, 37}, {40, 42}},
},
{
name: "IPv4 ICMP",
want: "45000021000100004001aa22c0a80001080808080800f54babcd00017071727374",
zero: [][2]int{{2, 4}, {10, 12}, {22, 24}},
},
{
name: "IPv4 SCTP",
want: "45000034000040004084b8f2c0a80001c0a800020b5904d200000000a05c7b6a0100001400000001000100000001000100000001",
zero: [][2]int{{2, 4}, {10, 12}, {28, 32}},
notes: []string{"only recalculated when the layer 3 field is used"},
},
{
name: "IPv6 hop-by-hop UDP",
want: "600000000016000020010db800000000000000000000000120010db800000000000000000000000211000000000000001f900035000e1bfcdeadbeefcafe",
zero: [][2]int{{4, 6}, {52, 54}, {54, 56}},
},
{
name: "IPv6 ICMPv6",
want: "60000000000d3a40fe800000000000000000000000000001fe800000000000000000000000000002800046b7123400016162636465",
zero: [][2]int{{4, 6}, {42, 44}},
},
{
name: "IPv6 segment routing header UDP uses final destination",
want: "6000000000242b4020010db800000000000000000000000120010db80000000000000000000000aa110204010000000020010db80000000000000000000000ff1f900035000c7f9901020304",
zero: [][2]int{{4, 6}, {68, 70}, {70, 72}},
},
{
name: "IPv4 options, TCP SYN with options and no payload",
want: "460000401234400040067f7d0a0000010a00000294040000c35000500000000100000000a002ffff705b0000020405b40402080a000000010000000001030307",
hdrLen: 64,
zero: [][2]int{{2, 4}, {10, 12}, {36, 37}, {40, 42}},
},
{
name: "IPv4 options, TCP with options and payload",
want: "4600003d1234400040067f800a0000010a00000294040000c350005000000001000000008018ffffd4a500000101080a0000000100000000474554202f",
hdrLen: 56,
zero: [][2]int{{2, 4}, {10, 12}, {36, 37}, {40, 42}},
},
{
name: "IPv4 options, SCTP DATA without padding",
want: "460000391234400040847f060a0000010a00000294040000c35004d200000001866854b30003001500000001000100000000000068656c6c6f",
hdrLen: 52,
zero: [][2]int{{2, 4}, {10, 12}, {32, 36}, {38, 40}},
notes: []string{"not padded"},
},
{
name: "IPv4 options, SCTP DATA with padding keeps correct chunk length",
want: "4600003c1234400040847f030a0000010a00000294040000c35004d200000001ef89c5b10003001500000001000100000000000068656c6c6f000000",
hdrLen: 52,
zero: [][2]int{{2, 4}, {10, 12}, {32, 36}},
changes: 4,
},
{
name: "SCTP INIT entirely in layer 3",
want: "460000381234400040847f070a0000010a00000294040000c35004d200000001c3d6cafe0100001400000001000100000001000100000001",
hdrLen: 56,
zero: [][2]int{{2, 4}, {10, 12}, {32, 36}, {38, 40}},
},
{
name: "IPv6 SCTP SACK bundled before DATA",
want: "600000000034844020010db800000000000000000000000120010db8000000000000000000000002c35004d200000001634380c903000010000000000000ffff00000000000300180000000100010000000000000102030405060708",
hdrLen: 84,
zero: [][2]int{{4, 6}, {48, 52}, {70, 72}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
want := mustHex(t, tt.want)
pkt := bytes.Clone(want)
for _, z := range tt.zero {
clear(pkt[z[0]:z[1]])
}
res, err := Fix(pkt, tt.hdrLen)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(pkt, want) {
t.Errorf("got %x\nwant %x", pkt, want)
}
changes := tt.changes
if changes == 0 {
changes = len(tt.zero)
}
if len(res.Changes) != changes {
t.Errorf("got %d changes, want %d: %+v", len(res.Changes), changes, res.Changes)
}
checkNotes(t, res.Notes, tt.notes)
})
}
}
func checkNotes(t *testing.T, got, want []string) {
t.Helper()
if len(got) != len(want) {
t.Errorf("notes %q, want %q", got, want)
return
}
for i := range want {
if !strings.Contains(got[i], want[i]) {
t.Errorf("note %q does not contain %q", got[i], want[i])
}
}
}
func TestFixNotes(t *testing.T) {
const (
ip4TCP = "45000000000140004006 0000 c0a80001c0a80002"
tcpHdr = "c35000500000000100000000 5002ffff 0000 0000"
ip4SCTP = "45000000000140004084 0000 c0a80001c0a80002"
sctpHdr = "c35004d2 00000001 00000000"
)
tests := []struct {
name string
pkt string
hdrLen int
notes []string
}{
{"TCP header in payload field", ip4TCP + tcpHdr, 20, []string{"TCP header is in the payload field"}},
{"TCP header length not a multiple of 4", ip4TCP + tcpHdr + "0101", 42, []string{"must be 20-60"}},
{"IP header extends past layer 3", ip4TCP + tcpHdr, 12, []string{"extends past the layer 3 field", "TCP header is in the payload field"}},
{"IPv4 option overruns IHL", "46000000000140004006 0000 c0a80001c0a80002 01 44 08 00" + tcpHdr, 0, []string{"does not fit in the header length"}},
{"SCTP chunk header in payload field", ip4SCTP + sctpHdr + "00030000 00000001 00010000 00000000", 32, []string{"chunk header is not in the layer 3 field"}},
{"SCTP without chunks", ip4SCTP + sctpHdr, 32, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res, err := Fix(mustHex(t, tt.pkt), tt.hdrLen)
if err != nil {
t.Fatal(err)
}
checkNotes(t, res.Notes, tt.notes)
})
}
}
func TestFixIPv4Fragment(t *testing.T) {
// More-fragments flag set: header fixed, UDP left alone.
pkt := mustHex(t, "45000000000120004011 0000 c0a80001c0a80002 303904d200000000 aabb")
res, err := Fix(pkt, 0)
if err != nil {
t.Fatal(err)
}
if len(res.Changes) != 2 || len(res.Notes) != 1 || !strings.Contains(res.Notes[0], "fragment") {
t.Errorf("changes %+v notes %v", res.Changes, res.Notes)
}
if !bytes.Equal(pkt[24:28], []byte{0, 0, 0, 0}) {
t.Errorf("UDP header modified: %x", pkt[20:28])
}
}
func TestFixTruncatedTransport(t *testing.T) {
pkt := mustHex(t, "45000000000140004006 0000 c0a80001c0a80002 c350")
res, err := Fix(pkt, 0)
if err != nil {
t.Fatal(err)
}
if len(res.Notes) != 1 || !strings.Contains(res.Notes[0], "TCP header truncated") {
t.Errorf("notes %v", res.Notes)
}
}
func TestFixErrors(t *testing.T) {
for name, in := range map[string]string{
"empty": "",
"not IP": "0011223344",
"short IPv4": "4500001c",
"IHL too small": "41000000000000004011000000000000000000000000",
"IHL beyond end": "4f000000000000004011000000000000000000000000",
"short IPv6": "6000000000001140",
} {
if _, err := Fix(mustHex(t, in), 0); err == nil {
t.Errorf("%s: expected error", name)
}
}
}
func TestFindIP(t *testing.T) {
tests := []struct {
name string
frame string
linkType uint32
want int
wantErr bool
}{
{"ethernet", "001122334455 66778899aabb 0800 45", pcap.LinkTypeEthernet, 14, false},
{"ethernet IPv6", "001122334455 66778899aabb 86dd 60", pcap.LinkTypeEthernet, 14, false},
{"vlan", "001122334455 66778899aabb 8100 0064 0800 45", pcap.LinkTypeEthernet, 18, false},
{"qinq", "001122334455 66778899aabb 88a8 0001 8100 0064 86dd 60", pcap.LinkTypeEthernet, 22, false},
{"arp", "ffffffffffff 66778899aabb 0806 0001", pcap.LinkTypeEthernet, 0, true},
{"truncated", "001122334455", pcap.LinkTypeEthernet, 0, true},
{"raw", "45", pcap.LinkTypeRaw, 0, false},
{"sll", "0000 0001 0006 66778899aabb 0000 0800 45", pcap.LinkTypeLinuxSLL, 16, false},
{"sll2", "86dd 0000 00000002 0001 00 06 66778899aabb0000 60", pcap.LinkTypeLinuxSLL2, 20, false},
{"unsupported", "00", pcap.LinkTypeIEEE80211, 0, true},
}
for _, tt := range tests {
got, err := FindIP(mustHex(t, tt.frame), tt.linkType)
if (err != nil) != tt.wantErr || got != tt.want {
t.Errorf("%s: FindIP = %d, %v; want %d, err=%v", tt.name, got, err, tt.want, tt.wantErr)
}
}
}