ecn_inner_test.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. package nebula
  2. import (
  3. "encoding/binary"
  4. "log/slog"
  5. "testing"
  6. "golang.org/x/net/ipv4"
  7. )
  8. func TestInnerECN(t *testing.T) {
  9. cases := []struct {
  10. name string
  11. pkt []byte
  12. want byte
  13. }{
  14. {"empty", nil, 0},
  15. {"v4_NotECT", v4WithToS(0x00), 0x00},
  16. {"v4_ECT0", v4WithToS(0x02), 0x02},
  17. {"v4_ECT1", v4WithToS(0x01), 0x01},
  18. {"v4_CE", v4WithToS(0x03), 0x03},
  19. {"v4_DSCP_then_NotECT", v4WithToS(0x88 | 0x00), 0x00},
  20. {"v4_DSCP_then_CE", v4WithToS(0x88 | 0x03), 0x03},
  21. {"v6_NotECT", v6WithTC(0x00), 0x00},
  22. {"v6_ECT0", v6WithTC(0x02), 0x02},
  23. {"v6_CE", v6WithTC(0x03), 0x03},
  24. {"v6_DSCP_then_CE", v6WithTC(0x88 | 0x03), 0x03},
  25. {"unknown_version", []byte{0xa5, 0xff}, 0},
  26. }
  27. for _, c := range cases {
  28. t.Run(c.name, func(t *testing.T) {
  29. got := innerECN(c.pkt)
  30. if got != c.want {
  31. t.Errorf("innerECN=0x%02x want 0x%02x", got, c.want)
  32. }
  33. })
  34. }
  35. }
  36. // v4WithToS returns a 2-byte slice tall enough for innerECN: byte 0 carries
  37. // version=4 in the high nibble, byte 1 is the full ToS so we exercise both
  38. // the DSCP and ECN portions through the byte 1 mask.
  39. func v4WithToS(tos byte) []byte {
  40. return []byte{0x45, tos}
  41. }
  42. // v6WithTC builds a 2-byte slice that places a known traffic class value
  43. // across bytes 0 (high nibble of TC) and 1 (low nibble of TC). innerECN
  44. // extracts ECN as (b[1]>>4)&0x03, which corresponds to TC[1:0].
  45. func v6WithTC(tc byte) []byte {
  46. return []byte{0x60 | (tc>>4)&0x0f, (tc & 0x0f) << 4}
  47. }
  48. func TestApplyOuterECN(t *testing.T) {
  49. silent := slog.New(slog.DiscardHandler)
  50. hi := &HostInfo{}
  51. // Build a v4 packet helper with a given inner ECN field.
  52. v4 := func(innerECN byte) []byte {
  53. // 20-byte minimal IPv4 header with ToS = innerECN (DSCP zeroed).
  54. return []byte{
  55. 0x45, innerECN, 0, 28,
  56. 0, 0, 0x40, 0,
  57. 64, 6, 0, 0,
  58. 10, 0, 0, 1,
  59. 10, 0, 0, 2,
  60. }
  61. }
  62. // Build a v6 packet helper with a given inner ECN field. ECN occupies
  63. // TC[1:0] which sit at byte 1 mask 0x30.
  64. v6 := func(innerECN byte) []byte {
  65. // 40-byte minimal IPv6 header with TC[1:0] = innerECN.
  66. pkt := make([]byte, 40)
  67. pkt[0] = 0x60 // version=6, TC[7:4]=0
  68. pkt[1] = (innerECN & 0x03) << 4 // TC[3:0]: low 2 bits = ECN, top 2 = DSCP-low (0)
  69. return pkt
  70. }
  71. type cell struct {
  72. outer byte
  73. inner byte
  74. wantECN byte
  75. wantSame bool // expect inner unchanged (true => verify the byte didn't move)
  76. }
  77. // RFC 6040 normal-mode combine table. Only outer==CE causes mutation.
  78. table := []cell{
  79. {ecnNotECT, ecnNotECT, ecnNotECT, true},
  80. {ecnNotECT, ecnECT0, ecnECT0, true},
  81. {ecnNotECT, ecnECT1, ecnECT1, true},
  82. {ecnNotECT, ecnCE, ecnCE, true},
  83. {ecnECT0, ecnNotECT, ecnNotECT, true},
  84. {ecnECT0, ecnECT0, ecnECT0, true},
  85. {ecnECT0, ecnECT1, ecnECT1, true},
  86. {ecnECT0, ecnCE, ecnCE, true},
  87. {ecnECT1, ecnNotECT, ecnNotECT, true},
  88. {ecnECT1, ecnECT0, ecnECT0, true},
  89. {ecnECT1, ecnECT1, ecnECT1, true},
  90. {ecnECT1, ecnCE, ecnCE, true},
  91. {ecnCE, ecnNotECT, ecnNotECT, true}, // legacy: log, leave alone
  92. {ecnCE, ecnECT0, ecnCE, false}, // CE folded in
  93. {ecnCE, ecnECT1, ecnCE, false},
  94. {ecnCE, ecnCE, ecnCE, true},
  95. }
  96. for _, c := range table {
  97. t.Run("v4", func(t *testing.T) {
  98. pkt := v4(c.inner)
  99. applyOuterECN(pkt, c.outer, hi, silent)
  100. got := pkt[1] & 0x03
  101. if got != c.wantECN {
  102. t.Errorf("v4 outer=0x%02x inner=0x%02x: got 0x%02x want 0x%02x", c.outer, c.inner, got, c.wantECN)
  103. }
  104. })
  105. t.Run("v6", func(t *testing.T) {
  106. pkt := v6(c.inner)
  107. applyOuterECN(pkt, c.outer, hi, silent)
  108. got := (pkt[1] >> 4) & 0x03
  109. if got != c.wantECN {
  110. t.Errorf("v6 outer=0x%02x inner=0x%02x: got 0x%02x want 0x%02x", c.outer, c.inner, got, c.wantECN)
  111. }
  112. })
  113. }
  114. }
  115. // TestApplyOuterECN_IPv4ChecksumStaysValid guards against H1: folding an outer
  116. // CE mark into the inner IPv4 ToS byte must keep the IPv4 header checksum valid.
  117. // The passthrough emit paths write the packet verbatim, so a stale checksum
  118. // turns an underlay congestion mark into packet loss at the receiver.
  119. func TestApplyOuterECN_IPv4ChecksumStaysValid(t *testing.T) {
  120. silent := slog.New(slog.DiscardHandler)
  121. hi := &HostInfo{}
  122. // 20-byte IPv4 header with DSCP=0x88 and inner ECN = ECT(0). Folding CE
  123. // flips only the low two bits of the ToS byte while leaving DSCP intact.
  124. pkt := []byte{
  125. 0x45, 0x88 | ecnECT0, 0, 40,
  126. 0x1c, 0x46, 0x40, 0x00,
  127. 64, 6, 0, 0,
  128. 10, 0, 0, 1,
  129. 10, 0, 0, 2,
  130. }
  131. // Stamp a correct header checksum before the fold.
  132. binary.BigEndian.PutUint16(pkt[10:12], ipv4HeaderChecksum(pkt[:ipv4.HeaderLen]))
  133. if !ipv4HeaderChecksumValid(pkt[:ipv4.HeaderLen]) {
  134. t.Fatal("test setup: initial header checksum invalid")
  135. }
  136. applyOuterECN(pkt, ecnCE, hi, silent)
  137. // CE folded in, DSCP preserved.
  138. if got, want := pkt[1], byte(0x88|ecnCE); got != want {
  139. t.Fatalf("ToS after fold = 0x%02x, want 0x%02x", got, want)
  140. }
  141. // The incremental RFC 1624 update must leave the checksum valid and equal
  142. // to a full recompute over the mutated header.
  143. if !ipv4HeaderChecksumValid(pkt[:ipv4.HeaderLen]) {
  144. t.Fatalf("IPv4 header checksum invalid after CE fold: 0x%04x", binary.BigEndian.Uint16(pkt[10:12]))
  145. }
  146. if got, want := binary.BigEndian.Uint16(pkt[10:12]), ipv4HeaderChecksum(pkt[:ipv4.HeaderLen]); got != want {
  147. t.Fatalf("checksum = 0x%04x, full recompute = 0x%04x", got, want)
  148. }
  149. }
  150. // ipv4HeaderChecksum computes the RFC 1071 IPv4 header checksum over hdr,
  151. // treating the checksum field (bytes 10:12) as zero.
  152. func ipv4HeaderChecksum(hdr []byte) uint16 {
  153. var sum uint32
  154. for i := 0; i+1 < len(hdr); i += 2 {
  155. if i == 10 {
  156. continue // checksum field
  157. }
  158. sum += uint32(hdr[i])<<8 | uint32(hdr[i+1])
  159. }
  160. for sum > 0xffff {
  161. sum = (sum >> 16) + (sum & 0xffff)
  162. }
  163. return ^uint16(sum)
  164. }
  165. // ipv4HeaderChecksumValid reports whether the stored checksum matches a fresh
  166. // computation over the header.
  167. func ipv4HeaderChecksumValid(hdr []byte) bool {
  168. return binary.BigEndian.Uint16(hdr[10:12]) == ipv4HeaderChecksum(hdr)
  169. }