checksum_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. package iputil
  2. import (
  3. "encoding/binary"
  4. "net"
  5. "testing"
  6. "github.com/google/gopacket"
  7. "github.com/google/gopacket/layers"
  8. "github.com/stretchr/testify/assert"
  9. "github.com/stretchr/testify/require"
  10. "golang.org/x/net/ipv6"
  11. )
  12. // serialize builds a packet with gopacket, whose checksums are computed
  13. // independently of this package.
  14. func serialize(t *testing.T, ls ...gopacket.SerializableLayer) []byte {
  15. buf := gopacket.NewSerializeBuffer()
  16. require.NoError(t, gopacket.SerializeLayers(buf, gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true}, ls...))
  17. return append([]byte(nil), buf.Bytes()...)
  18. }
  19. // withExtensionHeader inserts an 8 byte IPv6 extension header of the given
  20. // type between the IPv6 header and its payload. The transport checksum does not
  21. // change: the pseudo-header counts only upper-layer bytes.
  22. func withExtensionHeader(pkt []byte, typ layers.IPProtocol, hdr [8]byte) []byte {
  23. hdr[0] = pkt[6]
  24. out := make([]byte, 0, len(pkt)+8)
  25. out = append(out, pkt[:40]...)
  26. out = append(out, hdr[:]...)
  27. out = append(out, pkt[40:]...)
  28. out[6] = byte(typ)
  29. binary.BigEndian.PutUint16(out[4:6], binary.BigEndian.Uint16(pkt[4:6])+8)
  30. return out
  31. }
  32. // truncate copies the first n bytes into a buffer of exactly that capacity, so
  33. // a read past the length panics instead of quietly succeeding.
  34. func truncate(pkt []byte, n int) []byte {
  35. out := make([]byte, n)
  36. copy(out, pkt)
  37. return out
  38. }
  39. // extChain builds an IPv6 packet fronted by n Destination Options headers. Each
  40. // points at another one, so the walk spends its whole budget without reaching a
  41. // transport header. lastExtLen inflates the final header's declared length,
  42. // which is how the walk ends up past the end of the packet.
  43. func extChain(n int, lastExtLen byte) []byte {
  44. pkt := make([]byte, ipv6.HeaderLen)
  45. pkt[0], pkt[6], pkt[7] = 0x60, 60, 64
  46. for i := range n {
  47. h := make([]byte, 8)
  48. h[0] = 60
  49. if i == n-1 {
  50. h[1] = lastExtLen
  51. }
  52. pkt = append(pkt, h...)
  53. }
  54. pkt = append(pkt, make([]byte, 20)...)
  55. binary.BigEndian.PutUint16(pkt[4:6], uint16(len(pkt)-ipv6.HeaderLen))
  56. return pkt
  57. }
  58. func TestSetTransportChecksum(t *testing.T) {
  59. // Source and destination differ so that a pseudo-header built from the wrong
  60. // one, or from the two swapped, does not land on the same checksum anyway.
  61. v4 := func(proto layers.IPProtocol) *layers.IPv4 {
  62. return &layers.IPv4{Version: 4, TTL: 64, Id: 0x1234, Protocol: proto, SrcIP: net.IPv4(192, 0, 2, 1).To4(), DstIP: net.IPv4(198, 51, 100, 2).To4()}
  63. }
  64. v6 := func(proto layers.IPProtocol) *layers.IPv6 {
  65. return &layers.IPv6{Version: 6, HopLimit: 64, NextHeader: proto, SrcIP: net.ParseIP("2001:db8::1"), DstIP: net.ParseIP("2001:db8:1::2")}
  66. }
  67. tcp := func(ip gopacket.NetworkLayer) *layers.TCP {
  68. l := &layers.TCP{SrcPort: 49152, DstPort: 443, SYN: true, Window: 65535}
  69. require.NoError(t, l.SetNetworkLayerForChecksum(ip))
  70. return l
  71. }
  72. udp := func(ip gopacket.NetworkLayer) *layers.UDP {
  73. l := &layers.UDP{SrcPort: 49152, DstPort: 53}
  74. require.NoError(t, l.SetNetworkLayerForChecksum(ip))
  75. return l
  76. }
  77. payload := gopacket.Payload("self")
  78. nop := layers.IPv4Option{OptionType: 1, OptionLength: 1}
  79. ip4tcp := v4(layers.IPProtocolTCP)
  80. ip4opts := v4(layers.IPProtocolTCP)
  81. ip4opts.Options = []layers.IPv4Option{nop, nop, nop, nop}
  82. ip4udp := v4(layers.IPProtocolUDP)
  83. ip6tcp := v6(layers.IPProtocolTCP)
  84. ip6udp := v6(layers.IPProtocolUDP)
  85. hopByHop := [8]byte{0, 0, 1, 4} // next header, length 0, PadN of 4
  86. // Bytes past the length the IP header declares are not part of the
  87. // datagram and must not be summed.
  88. trailing4 := append(serialize(t, ip4tcp, tcp(ip4tcp), payload), []byte("trailing")...)
  89. trailing6 := append(serialize(t, ip6tcp, tcp(ip6tcp), payload), []byte("trailing")...)
  90. // A datagram padded out past the length UDP declares: the pseudo-header
  91. // counts the UDP Length field, so the checksum is the unpadded one.
  92. padded4 := append(serialize(t, ip4udp, udp(ip4udp), payload), []byte("pad!")...)
  93. binary.BigEndian.PutUint16(padded4[2:4], uint16(len(padded4)))
  94. padded6 := append(serialize(t, ip6udp, udp(ip6udp), payload), []byte("pad!")...)
  95. binary.BigEndian.PutUint16(padded6[4:6], uint16(len(padded6)-ipv6.HeaderLen))
  96. // Corrupting the checksum and asking for it back must yield gopacket's
  97. // packet, byte for byte.
  98. recomputed := []struct {
  99. name string
  100. pkt []byte
  101. cksum int
  102. }{
  103. {"v4 tcp", serialize(t, ip4tcp, tcp(ip4tcp), payload), 20 + 16},
  104. {"v4 tcp with ip options", serialize(t, ip4opts, tcp(ip4opts), payload), 24 + 16},
  105. {"v4 udp", serialize(t, ip4udp, udp(ip4udp), payload), 20 + 6},
  106. {"v4 tcp header only", serialize(t, ip4tcp, tcp(ip4tcp)), 20 + 16},
  107. {"v4 udp header only", serialize(t, ip4udp, udp(ip4udp)), 20 + 6},
  108. {"v6 tcp", serialize(t, ip6tcp, tcp(ip6tcp), payload), 40 + 16},
  109. {"v6 udp", serialize(t, ip6udp, udp(ip6udp), payload), 40 + 6},
  110. {"v6 udp header only", serialize(t, ip6udp, udp(ip6udp)), 40 + 6},
  111. {"v6 tcp behind hop-by-hop", withExtensionHeader(serialize(t, ip6tcp, tcp(ip6tcp), payload), layers.IPProtocolIPv6HopByHop, hopByHop), 48 + 16},
  112. {"v4 tcp with bytes past the total length", trailing4, 20 + 16},
  113. {"v6 tcp with bytes past the payload length", trailing6, 40 + 16},
  114. {"v4 udp padded past its declared length", padded4, 20 + 6},
  115. {"v6 udp padded past its declared length", padded6, 40 + 6},
  116. }
  117. for _, tt := range recomputed {
  118. t.Run(tt.name, func(t *testing.T) {
  119. got := append([]byte(nil), tt.pkt...)
  120. binary.BigEndian.PutUint16(got[tt.cksum:], 0x1234)
  121. require.NotEqual(t, tt.pkt, got)
  122. SetTransportChecksum(got)
  123. assert.Equal(t, tt.pkt, got)
  124. })
  125. }
  126. ip4frag := v4(layers.IPProtocolTCP)
  127. ip4frag.Flags = layers.IPv4MoreFragments
  128. ip4later := v4(layers.IPProtocolTCP)
  129. ip4later.FragOffset = 1
  130. ip4icmp := v4(layers.IPProtocolICMPv4)
  131. badIHL := serialize(t, ip4tcp, tcp(ip4tcp), payload)
  132. badIHL[0] = 0x44 // header length 16, shorter than an ipv4 header
  133. shortTotalLen := serialize(t, ip4tcp, tcp(ip4tcp), payload)
  134. binary.BigEndian.PutUint16(shortTotalLen[2:4], 10) // shorter than the header it introduces
  135. cutTCP := serialize(t, ip4tcp, tcp(ip4tcp), payload)
  136. binary.BigEndian.PutUint16(cutTCP[2:4], 20+19) // one byte short of a tcp header
  137. cutTCP = truncate(cutTCP, 20+19)
  138. cutUDP := serialize(t, ip4udp, udp(ip4udp), payload)
  139. binary.BigEndian.PutUint16(cutUDP[2:4], 20+7) // one byte short of a udp header
  140. cutUDP = truncate(cutUDP, 20+7)
  141. // Two bytes short, so a transport header survives whole and the minimum
  142. // length check cannot stand in for the bounds check.
  143. cutV6 := truncate(serialize(t, ip6tcp, tcp(ip6tcp), payload), 62)
  144. fragment := [8]byte{0, 0, 0, 1, 0, 0, 0, 1} // next header, reserved, offset 0 with M set, id
  145. overrun4 := serialize(t, ip4udp, udp(ip4udp), payload)
  146. binary.BigEndian.PutUint16(overrun4[24:26], uint16(len(overrun4)-20+1)) // one byte past what ip delivered
  147. overrun6 := serialize(t, ip6udp, udp(ip6udp), payload)
  148. binary.BigEndian.PutUint16(overrun6[44:46], uint16(len(overrun6)-ipv6.HeaderLen+1))
  149. shortUDPLen := serialize(t, ip4udp, udp(ip4udp), payload)
  150. binary.BigEndian.PutUint16(shortUDPLen[24:26], 7) // shorter than the header it counts
  151. // Where the checksum cannot be completed the packet is left as it came.
  152. untouched := []struct {
  153. name string
  154. pkt []byte
  155. cksum int
  156. }{
  157. {"v4 first fragment", serialize(t, ip4frag, tcp(ip4frag), payload), 20 + 16},
  158. {"v4 later fragment", serialize(t, ip4later, tcp(ip4later), payload), 20 + 16},
  159. {"v4 icmp", serialize(t, ip4icmp, &layers.ICMPv4{TypeCode: layers.CreateICMPv4TypeCode(8, 0), Id: 1, Seq: 1}, payload), 20 + 2},
  160. {"v4 header length below the minimum", badIHL, 20 + 16},
  161. {"v4 total length below the header length", shortTotalLen, 20 + 16},
  162. {"v4 truncated below its total length", truncate(serialize(t, ip4tcp, tcp(ip4tcp), payload), 30), -1},
  163. {"v4 tcp header cut short", cutTCP, 20 + 16},
  164. {"v4 udp header cut short", cutUDP, -1},
  165. {"v6 fragment", withExtensionHeader(serialize(t, ip6tcp, tcp(ip6tcp), payload), layers.IPProtocolIPv6Fragment, fragment), 48 + 16},
  166. {"v6 truncated below its payload length", truncate(serialize(t, ip6tcp, tcp(ip6tcp), payload), 50), -1},
  167. {"v6 truncated with a whole transport header still present", cutV6, 40 + 16},
  168. {"v6 extension header chain longer than the walk", extChain(9, 0), 112 + 16},
  169. {"v6 extension header chain running past the packet", extChain(8, 255), 104 + 16},
  170. {"v4 udp length past the end of the datagram", overrun4, 20 + 6},
  171. {"v6 udp length past the end of the datagram", overrun6, 40 + 6},
  172. {"v4 udp length below a udp header", shortUDPLen, 20 + 6},
  173. }
  174. for _, tt := range untouched {
  175. t.Run(tt.name, func(t *testing.T) {
  176. if tt.cksum >= 0 {
  177. binary.BigEndian.PutUint16(tt.pkt[tt.cksum:], 0x1234)
  178. }
  179. want := append([]byte(nil), tt.pkt...)
  180. SetTransportChecksum(tt.pkt)
  181. assert.Equal(t, want, tt.pkt)
  182. })
  183. }
  184. t.Run("too short to carry a header", func(t *testing.T) {
  185. for _, pkt := range [][]byte{nil, {}, {0x45}, {0x60}} {
  186. assert.NotPanics(t, func() { SetTransportChecksum(pkt) })
  187. }
  188. })
  189. t.Run("tcp checksum of zero goes out as zero", func(t *testing.T) {
  190. pkt := serialize(t, ip4tcp, tcp(ip4tcp), gopacket.Payload{0, 0})
  191. c := binary.BigEndian.Uint16(pkt[36:38])
  192. require.NotZero(t, c)
  193. // Only udp reserves zero to mean "not computed", so tcp keeps it.
  194. binary.BigEndian.PutUint16(pkt[40:42], c)
  195. SetTransportChecksum(pkt)
  196. assert.Zero(t, binary.BigEndian.Uint16(pkt[36:38]))
  197. })
  198. t.Run("udp checksum of zero goes out as 0xffff", func(t *testing.T) {
  199. pkt := serialize(t, ip4udp, udp(ip4udp), gopacket.Payload{0, 0})
  200. c := binary.BigEndian.Uint16(pkt[26:28])
  201. require.NotZero(t, c)
  202. // The one's complement sum is now 0xffff - c; adding c to the payload
  203. // makes it 0xffff, whose complement is zero.
  204. binary.BigEndian.PutUint16(pkt[28:30], c)
  205. SetTransportChecksum(pkt)
  206. assert.Equal(t, uint16(0xffff), binary.BigEndian.Uint16(pkt[26:28]))
  207. })
  208. }
  209. func TestFold(t *testing.T) {
  210. // 0xffff is the fold's fixed point, so a loop bound one notch tight never
  211. // terminates on it.
  212. for _, tt := range []struct {
  213. in uint32
  214. want uint16
  215. }{
  216. {0, 0},
  217. {0xffff, 0xffff},
  218. {0x10000, 1},
  219. {0x1fffe, 0xffff},
  220. {0xffffffff, 0xffff},
  221. } {
  222. assert.Equal(t, tt.want, fold(tt.in))
  223. }
  224. }