tun_linux_offload_test.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. //go:build linux && !android && !e2e_testing
  2. // +build linux,!android,!e2e_testing
  3. package overlay
  4. import (
  5. "encoding/binary"
  6. "os"
  7. "testing"
  8. "golang.org/x/sys/unix"
  9. )
  10. // verifyChecksum confirms that the one's-complement sum across `b`, optionally
  11. // seeded with a pseudo-header sum, folds to all-ones (valid).
  12. func verifyChecksum(b []byte, pseudo uint32) bool {
  13. sum := checksumBytes(b, pseudo)
  14. for sum>>16 != 0 {
  15. sum = (sum & 0xffff) + (sum >> 16)
  16. }
  17. return uint16(sum) == 0xffff
  18. }
  19. // buildTSOv4 builds a synthetic IPv4/TCP TSO superpacket with a payload of
  20. // `payLen` bytes split at `mss`.
  21. func buildTSOv4(t *testing.T, payLen, mss int) ([]byte, virtioNetHdr) {
  22. t.Helper()
  23. const ipLen = 20
  24. const tcpLen = 20
  25. pkt := make([]byte, ipLen+tcpLen+payLen)
  26. // IPv4 header
  27. pkt[0] = 0x45 // version 4, IHL 5
  28. // total length is meaningless for TSO but set it anyway
  29. binary.BigEndian.PutUint16(pkt[2:4], uint16(ipLen+tcpLen+payLen))
  30. binary.BigEndian.PutUint16(pkt[4:6], 0x4242) // original ID
  31. pkt[8] = 64 // TTL
  32. pkt[9] = unix.IPPROTO_TCP
  33. copy(pkt[12:16], []byte{10, 0, 0, 1}) // src
  34. copy(pkt[16:20], []byte{10, 0, 0, 2}) // dst
  35. // TCP header
  36. binary.BigEndian.PutUint16(pkt[20:22], 12345) // sport
  37. binary.BigEndian.PutUint16(pkt[22:24], 80) // dport
  38. binary.BigEndian.PutUint32(pkt[24:28], 10000) // seq
  39. binary.BigEndian.PutUint32(pkt[28:32], 20000) // ack
  40. pkt[32] = 0x50 // data offset 5 words
  41. pkt[33] = 0x18 // ACK | PSH
  42. binary.BigEndian.PutUint16(pkt[34:36], 65535) // window
  43. // payload
  44. for i := 0; i < payLen; i++ {
  45. pkt[ipLen+tcpLen+i] = byte(i & 0xff)
  46. }
  47. return pkt, virtioNetHdr{
  48. Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM,
  49. GSOType: unix.VIRTIO_NET_HDR_GSO_TCPV4,
  50. HdrLen: uint16(ipLen + tcpLen),
  51. GSOSize: uint16(mss),
  52. CsumStart: uint16(ipLen),
  53. CsumOffset: 16,
  54. }
  55. }
  56. func TestSegmentTCPv4(t *testing.T) {
  57. const mss = 100
  58. const numSeg = 3
  59. pkt, hdr := buildTSOv4(t, mss*numSeg, mss)
  60. scratch := make([]byte, tunSegBufSize)
  61. var out [][]byte
  62. if err := segmentTCP(pkt, hdr, &out, scratch); err != nil {
  63. t.Fatalf("segmentTCP: %v", err)
  64. }
  65. if len(out) != numSeg {
  66. t.Fatalf("expected %d segments, got %d", numSeg, len(out))
  67. }
  68. for i, seg := range out {
  69. if len(seg) != 40+mss {
  70. t.Errorf("seg %d: unexpected len %d", i, len(seg))
  71. }
  72. totalLen := binary.BigEndian.Uint16(seg[2:4])
  73. if totalLen != uint16(40+mss) {
  74. t.Errorf("seg %d: total_len=%d want %d", i, totalLen, 40+mss)
  75. }
  76. id := binary.BigEndian.Uint16(seg[4:6])
  77. if id != 0x4242+uint16(i) {
  78. t.Errorf("seg %d: ip id=%#x want %#x", i, id, 0x4242+uint16(i))
  79. }
  80. seq := binary.BigEndian.Uint32(seg[24:28])
  81. wantSeq := uint32(10000 + i*mss)
  82. if seq != wantSeq {
  83. t.Errorf("seg %d: seq=%d want %d", i, seq, wantSeq)
  84. }
  85. flags := seg[33]
  86. wantFlags := byte(0x10) // ACK only, PSH cleared
  87. if i == numSeg-1 {
  88. wantFlags = 0x18 // ACK | PSH preserved on last
  89. }
  90. if flags != wantFlags {
  91. t.Errorf("seg %d: flags=%#x want %#x", i, flags, wantFlags)
  92. }
  93. // IPv4 header checksum must verify against itself.
  94. if !verifyChecksum(seg[:20], 0) {
  95. t.Errorf("seg %d: bad IPv4 header checksum", i)
  96. }
  97. // TCP checksum must verify against the pseudo-header.
  98. psum := pseudoHeaderIPv4(seg[12:16], seg[16:20], unix.IPPROTO_TCP, 20+mss)
  99. if !verifyChecksum(seg[20:], psum) {
  100. t.Errorf("seg %d: bad TCP checksum", i)
  101. }
  102. }
  103. }
  104. func TestSegmentTCPv4OddTail(t *testing.T) {
  105. // Payload of 250 bytes with MSS 100 → segments of 100, 100, 50.
  106. pkt, hdr := buildTSOv4(t, 250, 100)
  107. scratch := make([]byte, tunSegBufSize)
  108. var out [][]byte
  109. if err := segmentTCP(pkt, hdr, &out, scratch); err != nil {
  110. t.Fatalf("segmentTCP: %v", err)
  111. }
  112. if len(out) != 3 {
  113. t.Fatalf("want 3 segments, got %d", len(out))
  114. }
  115. wantPayLens := []int{100, 100, 50}
  116. for i, seg := range out {
  117. if len(seg)-40 != wantPayLens[i] {
  118. t.Errorf("seg %d: pay len %d want %d", i, len(seg)-40, wantPayLens[i])
  119. }
  120. if !verifyChecksum(seg[:20], 0) {
  121. t.Errorf("seg %d: bad IPv4 header checksum", i)
  122. }
  123. psum := pseudoHeaderIPv4(seg[12:16], seg[16:20], unix.IPPROTO_TCP, 20+wantPayLens[i])
  124. if !verifyChecksum(seg[20:], psum) {
  125. t.Errorf("seg %d: bad TCP checksum", i)
  126. }
  127. }
  128. }
  129. func TestSegmentTCPv6(t *testing.T) {
  130. const ipLen = 40
  131. const tcpLen = 20
  132. const mss = 120
  133. const numSeg = 2
  134. payLen := mss * numSeg
  135. pkt := make([]byte, ipLen+tcpLen+payLen)
  136. // IPv6 header
  137. pkt[0] = 0x60 // version 6
  138. binary.BigEndian.PutUint16(pkt[4:6], uint16(tcpLen+payLen))
  139. pkt[6] = unix.IPPROTO_TCP
  140. pkt[7] = 64
  141. // src/dst fe80::1 / fe80::2
  142. pkt[8] = 0xfe
  143. pkt[9] = 0x80
  144. pkt[23] = 1
  145. pkt[24] = 0xfe
  146. pkt[25] = 0x80
  147. pkt[39] = 2
  148. // TCP header
  149. binary.BigEndian.PutUint16(pkt[40:42], 12345)
  150. binary.BigEndian.PutUint16(pkt[42:44], 80)
  151. binary.BigEndian.PutUint32(pkt[44:48], 7)
  152. binary.BigEndian.PutUint32(pkt[48:52], 99)
  153. pkt[52] = 0x50
  154. pkt[53] = 0x19 // FIN | ACK | PSH — exercise FIN clearing too
  155. binary.BigEndian.PutUint16(pkt[54:56], 65535)
  156. for i := 0; i < payLen; i++ {
  157. pkt[ipLen+tcpLen+i] = byte(i)
  158. }
  159. hdr := virtioNetHdr{
  160. Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM,
  161. GSOType: unix.VIRTIO_NET_HDR_GSO_TCPV6,
  162. HdrLen: uint16(ipLen + tcpLen),
  163. GSOSize: uint16(mss),
  164. CsumStart: uint16(ipLen),
  165. CsumOffset: 16,
  166. }
  167. scratch := make([]byte, tunSegBufSize)
  168. var out [][]byte
  169. if err := segmentTCP(pkt, hdr, &out, scratch); err != nil {
  170. t.Fatalf("segmentTCP: %v", err)
  171. }
  172. if len(out) != numSeg {
  173. t.Fatalf("want %d segments, got %d", numSeg, len(out))
  174. }
  175. for i, seg := range out {
  176. if len(seg) != ipLen+tcpLen+mss {
  177. t.Errorf("seg %d: len %d want %d", i, len(seg), ipLen+tcpLen+mss)
  178. }
  179. pl := binary.BigEndian.Uint16(seg[4:6])
  180. if pl != uint16(tcpLen+mss) {
  181. t.Errorf("seg %d: payload_length=%d want %d", i, pl, tcpLen+mss)
  182. }
  183. seq := binary.BigEndian.Uint32(seg[44:48])
  184. if seq != uint32(7+i*mss) {
  185. t.Errorf("seg %d: seq=%d want %d", i, seq, 7+i*mss)
  186. }
  187. flags := seg[53]
  188. // Original flags = 0x19 (FIN|ACK|PSH). FIN(0x01)+PSH(0x08) should be
  189. // cleared on all but the last; ACK(0x10) always preserved.
  190. wantFlags := byte(0x10)
  191. if i == numSeg-1 {
  192. wantFlags = 0x19
  193. }
  194. if flags != wantFlags {
  195. t.Errorf("seg %d: flags=%#x want %#x", i, flags, wantFlags)
  196. }
  197. psum := pseudoHeaderIPv6(seg[8:24], seg[24:40], unix.IPPROTO_TCP, tcpLen+mss)
  198. if !verifyChecksum(seg[ipLen:], psum) {
  199. t.Errorf("seg %d: bad TCP checksum", i)
  200. }
  201. }
  202. }
  203. func TestSegmentGSONonePassesThrough(t *testing.T) {
  204. pkt, hdr := buildTSOv4(t, 100, 100)
  205. hdr.GSOType = unix.VIRTIO_NET_HDR_GSO_NONE
  206. hdr.Flags = 0 // no NEEDS_CSUM, leave packet untouched
  207. scratch := make([]byte, tunSegBufSize)
  208. var out [][]byte
  209. if err := segmentInto(pkt, hdr, &out, scratch); err != nil {
  210. t.Fatalf("segmentInto: %v", err)
  211. }
  212. if len(out) != 1 {
  213. t.Fatalf("want 1 segment, got %d", len(out))
  214. }
  215. if len(out[0]) != len(pkt) {
  216. t.Fatalf("unexpected length: %d vs %d", len(out[0]), len(pkt))
  217. }
  218. }
  219. func TestSegmentRejectsUDP(t *testing.T) {
  220. hdr := virtioNetHdr{GSOType: unix.VIRTIO_NET_HDR_GSO_UDP}
  221. var out [][]byte
  222. if err := segmentInto(nil, hdr, &out, nil); err == nil {
  223. t.Fatalf("expected rejection for UDP GSO")
  224. }
  225. }
  226. func BenchmarkSegmentTCPv4(b *testing.B) {
  227. sizes := []struct {
  228. name string
  229. payLen int
  230. mss int
  231. }{
  232. {"64KiB_MSS1460", 65000, 1460},
  233. {"16KiB_MSS1460", 16384, 1460},
  234. {"4KiB_MSS1460", 4096, 1460},
  235. }
  236. for _, sz := range sizes {
  237. b.Run(sz.name, func(b *testing.B) {
  238. const ipLen = 20
  239. const tcpLen = 20
  240. pkt := make([]byte, ipLen+tcpLen+sz.payLen)
  241. pkt[0] = 0x45
  242. binary.BigEndian.PutUint16(pkt[2:4], uint16(ipLen+tcpLen+sz.payLen))
  243. binary.BigEndian.PutUint16(pkt[4:6], 0x4242)
  244. pkt[8] = 64
  245. pkt[9] = unix.IPPROTO_TCP
  246. copy(pkt[12:16], []byte{10, 0, 0, 1})
  247. copy(pkt[16:20], []byte{10, 0, 0, 2})
  248. binary.BigEndian.PutUint16(pkt[20:22], 12345)
  249. binary.BigEndian.PutUint16(pkt[22:24], 80)
  250. binary.BigEndian.PutUint32(pkt[24:28], 10000)
  251. binary.BigEndian.PutUint32(pkt[28:32], 20000)
  252. pkt[32] = 0x50
  253. pkt[33] = 0x18
  254. binary.BigEndian.PutUint16(pkt[34:36], 65535)
  255. for i := 0; i < sz.payLen; i++ {
  256. pkt[ipLen+tcpLen+i] = byte(i)
  257. }
  258. hdr := virtioNetHdr{
  259. Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM,
  260. GSOType: unix.VIRTIO_NET_HDR_GSO_TCPV4,
  261. HdrLen: uint16(ipLen + tcpLen),
  262. GSOSize: uint16(sz.mss),
  263. CsumStart: uint16(ipLen),
  264. CsumOffset: 16,
  265. }
  266. scratch := make([]byte, tunSegBufSize)
  267. out := make([][]byte, 0, 64)
  268. b.SetBytes(int64(len(pkt)))
  269. b.ResetTimer()
  270. for i := 0; i < b.N; i++ {
  271. out = out[:0]
  272. if err := segmentTCP(pkt, hdr, &out, scratch); err != nil {
  273. b.Fatal(err)
  274. }
  275. }
  276. })
  277. }
  278. }
  279. // TestTunFileWriteVnetHdrNoAlloc verifies the IFF_VNET_HDR fast-path write is
  280. // allocation-free. We write to /dev/null so every call succeeds synchronously.
  281. func TestTunFileWriteVnetHdrNoAlloc(t *testing.T) {
  282. fd, err := unix.Open("/dev/null", os.O_WRONLY, 0)
  283. if err != nil {
  284. t.Fatalf("open /dev/null: %v", err)
  285. }
  286. t.Cleanup(func() { _ = unix.Close(fd) })
  287. tf := &tunFile{fd: fd, vnetHdr: true}
  288. tf.writeIovs[0].Base = &validVnetHdr[0]
  289. tf.writeIovs[0].SetLen(virtioNetHdrLen)
  290. payload := make([]byte, 1400)
  291. // Warm up (first call may trigger one-time internal allocations elsewhere).
  292. if _, err := tf.Write(payload); err != nil {
  293. t.Fatalf("Write: %v", err)
  294. }
  295. allocs := testing.AllocsPerRun(1000, func() {
  296. if _, err := tf.Write(payload); err != nil {
  297. t.Fatalf("Write: %v", err)
  298. }
  299. })
  300. if allocs != 0 {
  301. t.Fatalf("Write allocated %.1f times per call, want 0", allocs)
  302. }
  303. }