tun_linux_offload_test.go 9.4 KB

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