checksum_seed_test.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. package batch
  2. import (
  3. "encoding/binary"
  4. "math/rand"
  5. "testing"
  6. )
  7. // The checksum-seeding helpers feed the virtio NEEDS_CSUM contract: the L4
  8. // checksum field is pre-loaded with the folded (not inverted) pseudo-header
  9. // sum, and the kernel later adds the L4 byte sum and inverts. A wrong seed
  10. // produces packets every receiver silently drops, with nothing failing on
  11. // our side — so these tests check the helpers against an independent
  12. // RFC 1071 reference built from explicit pseudo-header bytes, never against
  13. // the production checksum code.
  14. // refSum accumulates big-endian 16-bit words of b (odd tail zero-padded)
  15. // into a wide one's-complement accumulator.
  16. func refSum(b []byte) uint64 {
  17. var s uint64
  18. for i := 0; i+1 < len(b); i += 2 {
  19. s += uint64(b[i])<<8 | uint64(b[i+1])
  20. }
  21. if len(b)%2 == 1 {
  22. s += uint64(b[len(b)-1]) << 8
  23. }
  24. return s
  25. }
  26. // refFold folds a wide one's-complement accumulator to 16 bits.
  27. func refFold(s uint64) uint16 {
  28. for s>>16 != 0 {
  29. s = s&0xffff + s>>16
  30. }
  31. return uint16(s)
  32. }
  33. func TestFoldOnceNoInvertEdgeCases(t *testing.T) {
  34. cases := []uint32{
  35. 0, 1, 0xffff,
  36. 0x10000, // single carry
  37. 0x1fffe, // 0xffff + 0xffff: carry produces another 0xffff
  38. 0xffff0000, // high half only
  39. 0xfffeffff, // fold yields 0x1fffd: needs a second fold
  40. 0xffffffff, // worst case
  41. 0x00010001, // simple two-word
  42. }
  43. for _, c := range cases {
  44. want := refFold(uint64(c))
  45. if got := foldOnceNoInvert(c); got != want {
  46. t.Errorf("foldOnceNoInvert(%#x) = %#x, want %#x", c, got, want)
  47. }
  48. // Folding a folded value must be a no-op.
  49. if got := foldOnceNoInvert(uint32(foldOnceNoInvert(c))); got != foldOnceNoInvert(c) {
  50. t.Errorf("foldOnceNoInvert not idempotent at %#x", c)
  51. }
  52. }
  53. }
  54. func TestPseudoSumIPv4MatchesReference(t *testing.T) {
  55. cases := []struct {
  56. name string
  57. src, dst [4]byte
  58. proto byte
  59. l4Len int
  60. }{
  61. {"simple", [4]byte{10, 0, 0, 1}, [4]byte{10, 0, 0, 2}, 6, 20},
  62. {"zero-len", [4]byte{192, 168, 1, 1}, [4]byte{192, 168, 1, 2}, 17, 0},
  63. {"max-len", [4]byte{1, 2, 3, 4}, [4]byte{5, 6, 7, 8}, 6, 65535},
  64. {"carry-heavy", [4]byte{255, 255, 255, 255}, [4]byte{255, 255, 255, 254}, 17, 65535},
  65. {"broadcastish", [4]byte{255, 255, 255, 255}, [4]byte{255, 255, 255, 255}, 255, 65535},
  66. }
  67. for _, c := range cases {
  68. t.Run(c.name, func(t *testing.T) {
  69. // RFC 793 pseudo-header: src(4) dst(4) zero(1) proto(1) len(2).
  70. ph := make([]byte, 12)
  71. copy(ph[0:4], c.src[:])
  72. copy(ph[4:8], c.dst[:])
  73. ph[9] = c.proto
  74. binary.BigEndian.PutUint16(ph[10:12], uint16(c.l4Len))
  75. want := refFold(refSum(ph))
  76. got := foldOnceNoInvert(pseudoSumIPv4(c.src[:], c.dst[:], c.proto, c.l4Len))
  77. if got != want {
  78. t.Errorf("fold(pseudoSumIPv4) = %#x, want %#x", got, want)
  79. }
  80. })
  81. }
  82. }
  83. func TestPseudoSumIPv6MatchesReference(t *testing.T) {
  84. ones := func(b byte) (a [16]byte) {
  85. for i := range a {
  86. a[i] = b
  87. }
  88. return
  89. }
  90. cases := []struct {
  91. name string
  92. src, dst [16]byte
  93. proto byte
  94. l4Len int
  95. }{
  96. {"simple", [16]byte{0xfe, 0x80, 15: 1}, [16]byte{0xfe, 0x80, 15: 2}, 6, 20},
  97. {"zero-len", [16]byte{0x20, 0x01, 15: 9}, [16]byte{0x20, 0x01, 15: 8}, 17, 0},
  98. {"max-u16-len", ones(0xff), ones(0xfe), 6, 65535},
  99. {"len-past-u16", ones(0xff), ones(0xff), 17, 0x12345}, // exercises the 32-bit split
  100. }
  101. for _, c := range cases {
  102. t.Run(c.name, func(t *testing.T) {
  103. // RFC 8200 pseudo-header: src(16) dst(16) len(4) zero(3) next(1).
  104. ph := make([]byte, 40)
  105. copy(ph[0:16], c.src[:])
  106. copy(ph[16:32], c.dst[:])
  107. binary.BigEndian.PutUint32(ph[32:36], uint32(c.l4Len))
  108. ph[39] = c.proto
  109. want := refFold(refSum(ph))
  110. got := foldOnceNoInvert(pseudoSumIPv6(c.src[:], c.dst[:], c.proto, c.l4Len))
  111. if got != want {
  112. t.Errorf("fold(pseudoSumIPv6) = %#x, want %#x", got, want)
  113. }
  114. })
  115. }
  116. }
  117. func TestIPv4HdrChecksumMatchesReference(t *testing.T) {
  118. rng := rand.New(rand.NewSource(0x1791))
  119. for _, hdrLen := range []int{20, 24, 40, 60} {
  120. for trial := range 200 {
  121. hdr := make([]byte, hdrLen)
  122. rng.Read(hdr)
  123. hdr[0] = 0x40 | byte(hdrLen/4)
  124. hdr[10], hdr[11] = 0, 0 // checksum field zeroed, as the contract requires
  125. want := ^refFold(refSum(hdr))
  126. got := ipv4HdrChecksum(hdr)
  127. if got != want {
  128. t.Fatalf("ipv4HdrChecksum(len=%d trial=%d) = %#x, want %#x", hdrLen, trial, got, want)
  129. }
  130. // Receiver-side property: with the checksum stored, the full
  131. // header must sum to all-ones.
  132. binary.BigEndian.PutUint16(hdr[10:12], got)
  133. if v := refFold(refSum(hdr)); v != 0xffff {
  134. t.Fatalf("stored checksum does not validate: full-header fold = %#x", v)
  135. }
  136. }
  137. }
  138. }
  139. // TestChecksumSeedReceiverAcceptance is the end-to-end property the helpers
  140. // exist for: seed the TCP checksum field with fold(pseudoSum), do what the
  141. // kernel's NEEDS_CSUM completion does (one's-complement sum over the L4
  142. // bytes including the seed, then invert, then store), and verify the result
  143. // the way a receiver does (pseudo-header + L4 must sum to all-ones).
  144. func TestChecksumSeedReceiverAcceptance(t *testing.T) {
  145. rng := rand.New(rand.NewSource(0x1826))
  146. for trial := range 200 {
  147. src := [4]byte{byte(rng.Intn(256)), byte(rng.Intn(256)), byte(rng.Intn(256)), byte(rng.Intn(256))}
  148. dst := [4]byte{byte(rng.Intn(256)), byte(rng.Intn(256)), byte(rng.Intn(256)), byte(rng.Intn(256))}
  149. payLen := rng.Intn(1500)
  150. l4 := make([]byte, 20+payLen)
  151. rng.Read(l4)
  152. // Seed exactly as flushSlot does.
  153. seed := foldOnceNoInvert(pseudoSumIPv4(src[:], dst[:], 6, len(l4)))
  154. binary.BigEndian.PutUint16(l4[16:18], seed)
  155. // Kernel NEEDS_CSUM completion: sum the L4 region (seed included,
  156. // which is equivalent to summing with the field zeroed and folding
  157. // the seed in), invert, store.
  158. final := ^refFold(refSum(l4[:16]) + uint64(seed) + refSum(l4[18:]))
  159. binary.BigEndian.PutUint16(l4[16:18], final)
  160. // Receiver validation.
  161. ph := make([]byte, 12)
  162. copy(ph[0:4], src[:])
  163. copy(ph[4:8], dst[:])
  164. ph[9] = 6
  165. binary.BigEndian.PutUint16(ph[10:12], uint16(len(l4)))
  166. if v := refFold(refSum(ph) + refSum(l4)); v != 0xffff {
  167. t.Fatalf("trial %d: receiver rejects packet: fold = %#x (seed=%#x final=%#x payLen=%d)",
  168. trial, v, seed, final, payLen)
  169. }
  170. }
  171. }