| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- package nebula
- import (
- "encoding/binary"
- "log/slog"
- "testing"
- "golang.org/x/net/ipv4"
- )
- func TestInnerECN(t *testing.T) {
- cases := []struct {
- name string
- pkt []byte
- want byte
- }{
- {"empty", nil, 0},
- {"v4_NotECT", v4WithToS(0x00), 0x00},
- {"v4_ECT0", v4WithToS(0x02), 0x02},
- {"v4_ECT1", v4WithToS(0x01), 0x01},
- {"v4_CE", v4WithToS(0x03), 0x03},
- {"v4_DSCP_then_NotECT", v4WithToS(0x88 | 0x00), 0x00},
- {"v4_DSCP_then_CE", v4WithToS(0x88 | 0x03), 0x03},
- {"v6_NotECT", v6WithTC(0x00), 0x00},
- {"v6_ECT0", v6WithTC(0x02), 0x02},
- {"v6_CE", v6WithTC(0x03), 0x03},
- {"v6_DSCP_then_CE", v6WithTC(0x88 | 0x03), 0x03},
- {"unknown_version", []byte{0xa5, 0xff}, 0},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- got := innerECN(c.pkt)
- if got != c.want {
- t.Errorf("innerECN=0x%02x want 0x%02x", got, c.want)
- }
- })
- }
- }
- // v4WithToS returns a 2-byte slice tall enough for innerECN: byte 0 carries
- // version=4 in the high nibble, byte 1 is the full ToS so we exercise both
- // the DSCP and ECN portions through the byte 1 mask.
- func v4WithToS(tos byte) []byte {
- return []byte{0x45, tos}
- }
- // v6WithTC builds a 2-byte slice that places a known traffic class value
- // across bytes 0 (high nibble of TC) and 1 (low nibble of TC). innerECN
- // extracts ECN as (b[1]>>4)&0x03, which corresponds to TC[1:0].
- func v6WithTC(tc byte) []byte {
- return []byte{0x60 | (tc>>4)&0x0f, (tc & 0x0f) << 4}
- }
- func TestApplyOuterECN(t *testing.T) {
- silent := slog.New(slog.DiscardHandler)
- hi := &HostInfo{}
- // Build a v4 packet helper with a given inner ECN field.
- v4 := func(innerECN byte) []byte {
- // 20-byte minimal IPv4 header with ToS = innerECN (DSCP zeroed).
- return []byte{
- 0x45, innerECN, 0, 28,
- 0, 0, 0x40, 0,
- 64, 6, 0, 0,
- 10, 0, 0, 1,
- 10, 0, 0, 2,
- }
- }
- // Build a v6 packet helper with a given inner ECN field. ECN occupies
- // TC[1:0] which sit at byte 1 mask 0x30.
- v6 := func(innerECN byte) []byte {
- // 40-byte minimal IPv6 header with TC[1:0] = innerECN.
- pkt := make([]byte, 40)
- pkt[0] = 0x60 // version=6, TC[7:4]=0
- pkt[1] = (innerECN & 0x03) << 4 // TC[3:0]: low 2 bits = ECN, top 2 = DSCP-low (0)
- return pkt
- }
- type cell struct {
- outer byte
- inner byte
- wantECN byte
- wantSame bool // expect inner unchanged (true => verify the byte didn't move)
- }
- // RFC 6040 normal-mode combine table. Only outer==CE causes mutation.
- table := []cell{
- {ecnNotECT, ecnNotECT, ecnNotECT, true},
- {ecnNotECT, ecnECT0, ecnECT0, true},
- {ecnNotECT, ecnECT1, ecnECT1, true},
- {ecnNotECT, ecnCE, ecnCE, true},
- {ecnECT0, ecnNotECT, ecnNotECT, true},
- {ecnECT0, ecnECT0, ecnECT0, true},
- {ecnECT0, ecnECT1, ecnECT1, true},
- {ecnECT0, ecnCE, ecnCE, true},
- {ecnECT1, ecnNotECT, ecnNotECT, true},
- {ecnECT1, ecnECT0, ecnECT0, true},
- {ecnECT1, ecnECT1, ecnECT1, true},
- {ecnECT1, ecnCE, ecnCE, true},
- {ecnCE, ecnNotECT, ecnNotECT, true}, // legacy: log, leave alone
- {ecnCE, ecnECT0, ecnCE, false}, // CE folded in
- {ecnCE, ecnECT1, ecnCE, false},
- {ecnCE, ecnCE, ecnCE, true},
- }
- for _, c := range table {
- t.Run("v4", func(t *testing.T) {
- pkt := v4(c.inner)
- applyOuterECN(pkt, c.outer, hi, silent)
- got := pkt[1] & 0x03
- if got != c.wantECN {
- t.Errorf("v4 outer=0x%02x inner=0x%02x: got 0x%02x want 0x%02x", c.outer, c.inner, got, c.wantECN)
- }
- })
- t.Run("v6", func(t *testing.T) {
- pkt := v6(c.inner)
- applyOuterECN(pkt, c.outer, hi, silent)
- got := (pkt[1] >> 4) & 0x03
- if got != c.wantECN {
- t.Errorf("v6 outer=0x%02x inner=0x%02x: got 0x%02x want 0x%02x", c.outer, c.inner, got, c.wantECN)
- }
- })
- }
- }
- // TestApplyOuterECN_IPv4ChecksumStaysValid guards against H1: folding an outer
- // CE mark into the inner IPv4 ToS byte must keep the IPv4 header checksum valid.
- // The passthrough emit paths write the packet verbatim, so a stale checksum
- // turns an underlay congestion mark into packet loss at the receiver.
- func TestApplyOuterECN_IPv4ChecksumStaysValid(t *testing.T) {
- silent := slog.New(slog.DiscardHandler)
- hi := &HostInfo{}
- // 20-byte IPv4 header with DSCP=0x88 and inner ECN = ECT(0). Folding CE
- // flips only the low two bits of the ToS byte while leaving DSCP intact.
- pkt := []byte{
- 0x45, 0x88 | ecnECT0, 0, 40,
- 0x1c, 0x46, 0x40, 0x00,
- 64, 6, 0, 0,
- 10, 0, 0, 1,
- 10, 0, 0, 2,
- }
- // Stamp a correct header checksum before the fold.
- binary.BigEndian.PutUint16(pkt[10:12], ipv4HeaderChecksum(pkt[:ipv4.HeaderLen]))
- if !ipv4HeaderChecksumValid(pkt[:ipv4.HeaderLen]) {
- t.Fatal("test setup: initial header checksum invalid")
- }
- applyOuterECN(pkt, ecnCE, hi, silent)
- // CE folded in, DSCP preserved.
- if got, want := pkt[1], byte(0x88|ecnCE); got != want {
- t.Fatalf("ToS after fold = 0x%02x, want 0x%02x", got, want)
- }
- // The incremental RFC 1624 update must leave the checksum valid and equal
- // to a full recompute over the mutated header.
- if !ipv4HeaderChecksumValid(pkt[:ipv4.HeaderLen]) {
- t.Fatalf("IPv4 header checksum invalid after CE fold: 0x%04x", binary.BigEndian.Uint16(pkt[10:12]))
- }
- if got, want := binary.BigEndian.Uint16(pkt[10:12]), ipv4HeaderChecksum(pkt[:ipv4.HeaderLen]); got != want {
- t.Fatalf("checksum = 0x%04x, full recompute = 0x%04x", got, want)
- }
- }
- // ipv4HeaderChecksum computes the RFC 1071 IPv4 header checksum over hdr,
- // treating the checksum field (bytes 10:12) as zero.
- func ipv4HeaderChecksum(hdr []byte) uint16 {
- var sum uint32
- for i := 0; i+1 < len(hdr); i += 2 {
- if i == 10 {
- continue // checksum field
- }
- sum += uint32(hdr[i])<<8 | uint32(hdr[i+1])
- }
- for sum > 0xffff {
- sum = (sum >> 16) + (sum & 0xffff)
- }
- return ^uint16(sum)
- }
- // ipv4HeaderChecksumValid reports whether the stored checksum matches a fresh
- // computation over the header.
- func ipv4HeaderChecksumValid(hdr []byte) bool {
- return binary.BigEndian.Uint16(hdr[10:12]) == ipv4HeaderChecksum(hdr)
- }
|