segment_linux.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. //go:build linux && !android && !e2e_testing
  2. // +build linux,!android,!e2e_testing
  3. // Package virtio implements the pure validation, header-correction, and
  4. // per-segment slicing logic for kernel-supplied TSO/USO superpackets on
  5. // IFF_VNET_HDR TUN devices. It is FD-free and depends only on the byte
  6. // layout of the virtio_net_hdr and the IP/TCP/UDP headers it describes,
  7. // so it can be unit-tested in isolation from the tio Queue runtime.
  8. package virtio
  9. import (
  10. "encoding/binary"
  11. "errors"
  12. "fmt"
  13. "golang.org/x/sys/unix"
  14. "gvisor.dev/gvisor/pkg/tcpip/checksum"
  15. )
  16. // Protocol header size bounds used to validate / cap kernel-supplied offsets.
  17. const (
  18. ipv4HeaderMinLen = 20 // IHL=5, no options
  19. ipv4HeaderMaxLen = 60 // IHL=15, max options
  20. ipv6FixedLen = 40 // IPv6 base header; extensions would extend this
  21. tcpHeaderMinLen = 20 // data-offset=5, no options
  22. tcpHeaderMaxLen = 60 // data-offset=15, max options
  23. )
  24. // Byte offsets inside an IPv4 header.
  25. const (
  26. ipv4TotalLenOff = 2
  27. ipv4IDOff = 4
  28. ipv4ChecksumOff = 10
  29. ipv4SrcOff = 12
  30. ipv4AddrsEnd = 20 // end of dst address (ipv4SrcOff + 2*4)
  31. )
  32. // Byte offsets inside an IPv6 header.
  33. const (
  34. ipv6PayloadLenOff = 4
  35. ipv6SrcOff = 8
  36. ipv6AddrsEnd = 40 // end of dst address (ipv6SrcOff + 2*16)
  37. )
  38. // Byte offsets inside a TCP header (relative to its start, i.e. csumStart).
  39. const (
  40. tcpSeqOff = 4
  41. tcpDataOffOff = 12 // upper nibble is header len in 32-bit words
  42. tcpFlagsOff = 13
  43. tcpChecksumOff = 16
  44. )
  45. // UDP header is fixed at 8 bytes: {sport, dport, length, checksum}.
  46. const (
  47. udpHeaderLen = 8
  48. udpLengthOff = 4
  49. udpChecksumOff = 6
  50. )
  51. // tcpFinPshMask is cleared on every segment except the last of a TSO burst.
  52. const tcpFinPshMask = 0x09 // FIN(0x01) | PSH(0x08)
  53. // tcpCwrFlag is cleared on every segment except the first. Per RFC 3168
  54. // §6.1.2 the CWR bit signals a one-shot transition (the sender just halved
  55. // its window) and must appear on the first segment of a TSO burst only.
  56. const tcpCwrFlag = 0x80
  57. // CheckValid rejects packets whose virtio_net_hdr/IP combination would
  58. // cause a downstream miscompute. The TUN should never emit RSC_INFO and
  59. // the GSO type must agree with the IP version nibble.
  60. func CheckValid(pkt []byte, hdr Hdr) error {
  61. // When RSC_INFO is set the csum_start/csum_offset fields are repurposed to
  62. // carry coalescing info rather than checksum offsets. A TUN writing via
  63. // IFF_VNET_HDR should never emit this, but if it did we would silently
  64. // miscompute the segment checksums — refuse the packet instead.
  65. if hdr.Flags&unix.VIRTIO_NET_HDR_F_RSC_INFO != 0 {
  66. return fmt.Errorf("virtio RSC_INFO flag not supported on TUN reads")
  67. }
  68. if len(pkt) < ipv4HeaderMinLen {
  69. return fmt.Errorf("packet too short")
  70. }
  71. ipVersion := pkt[0] >> 4
  72. switch hdr.GSOType {
  73. case unix.VIRTIO_NET_HDR_GSO_TCPV4:
  74. if ipVersion != 4 {
  75. return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.GSOType)
  76. }
  77. case unix.VIRTIO_NET_HDR_GSO_TCPV6:
  78. if ipVersion != 6 {
  79. return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.GSOType)
  80. }
  81. case unix.VIRTIO_NET_HDR_GSO_UDP_L4:
  82. // USO carries either v4 or v6; the leading nibble disambiguates.
  83. if !(ipVersion == 4 || ipVersion == 6) {
  84. return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.GSOType)
  85. }
  86. default:
  87. if !(ipVersion == 6 || ipVersion == 4) {
  88. return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.GSOType)
  89. }
  90. }
  91. return nil
  92. }
  93. // CorrectHdrLen rewrites hdr.HdrLen based on the actual transport header
  94. // length read out of pkt. The kernel's hdr.HdrLen on the FORWARD path can
  95. // be the length of the entire first packet, so we don't trust it.
  96. func CorrectHdrLen(pkt []byte, hdr *Hdr) error {
  97. // Thank you wireguard-go for documenting these edge-cases
  98. // Don't trust hdr.hdrLen from the kernel as it can be equal to the length
  99. // of the entire first packet when the kernel is handling it as part of a
  100. // FORWARD path. Instead, parse the transport header length and add it onto
  101. // csumStart, which is synonymous for IP header length.
  102. if hdr.GSOType == unix.VIRTIO_NET_HDR_GSO_UDP_L4 {
  103. hdr.HdrLen = hdr.CsumStart + 8
  104. } else {
  105. if len(pkt) <= int(hdr.CsumStart+tcpDataOffOff) {
  106. return errors.New("packet is too short")
  107. }
  108. tcpHLen := uint16(pkt[hdr.CsumStart+tcpDataOffOff] >> 4 * 4)
  109. if tcpHLen < 20 || tcpHLen > 60 {
  110. // A TCP header must be between 20 and 60 bytes in length.
  111. return fmt.Errorf("tcp header len is invalid: %d", tcpHLen)
  112. }
  113. hdr.HdrLen = hdr.CsumStart + tcpHLen
  114. }
  115. if len(pkt) < int(hdr.HdrLen) {
  116. return fmt.Errorf("length of packet (%d) < virtioNetHdr.HdrLen (%d)", len(pkt), hdr.HdrLen)
  117. }
  118. if hdr.HdrLen < hdr.CsumStart {
  119. return fmt.Errorf("virtioNetHdr.HdrLen (%d) < virtioNetHdr.CsumStart (%d)", hdr.HdrLen, hdr.CsumStart)
  120. }
  121. cSumAt := int(hdr.CsumStart + hdr.CsumStart)
  122. if cSumAt+1 >= len(pkt) {
  123. return fmt.Errorf("end of checksum offset (%d) exceeds packet length (%d)", cSumAt+1, len(pkt))
  124. }
  125. return nil
  126. }
  127. // SegmentTCP walks a TSO superpacket pkt, yielding each segment as a
  128. // slice into pkt itself. Per-segment plaintext is laid out by sliding a
  129. // freshly-patched copy of the L3+L4 header into pkt at offset i*gsoSize,
  130. // where it sits immediately before that segment's payload chunk in the
  131. // original buffer. The slide is destructive: iter i's header write overwrites
  132. // the last hdrLen bytes of seg_{i-1}'s payload, which is dead by the time
  133. // the next iteration begins. pkt is consumed by this call and must not be
  134. // inspected by the caller after the final yield.
  135. func SegmentTCP(pkt []byte, hdrLenU, csumStartU, gsoSizeU uint16, yield func(seg []byte) error) error {
  136. if gsoSizeU == 0 {
  137. return fmt.Errorf("gso_size is zero")
  138. }
  139. if csumStartU == 0 {
  140. return fmt.Errorf("csum_start is zero")
  141. }
  142. headerLen := int(hdrLenU)
  143. csumStart := int(csumStartU)
  144. isV4 := pkt[0]>>4 == 4
  145. tcpHdrLen := int(pkt[csumStart+tcpDataOffOff]>>4) * 4
  146. payLen := len(pkt) - headerLen
  147. gsoSize := int(gsoSizeU)
  148. numSeg := (payLen + gsoSize - 1) / gsoSize
  149. if numSeg == 0 {
  150. numSeg = 1
  151. }
  152. origSeq := binary.BigEndian.Uint32(pkt[csumStart+tcpSeqOff : csumStart+tcpSeqOff+4])
  153. origFlags := pkt[csumStart+tcpFlagsOff]
  154. var tmp [tcpHeaderMaxLen]byte
  155. copy(tmp[:tcpHdrLen], pkt[csumStart:headerLen])
  156. tmp[tcpSeqOff], tmp[tcpSeqOff+1], tmp[tcpSeqOff+2], tmp[tcpSeqOff+3] = 0, 0, 0, 0
  157. tmp[tcpFlagsOff] = 0
  158. tmp[tcpChecksumOff], tmp[tcpChecksumOff+1] = 0, 0
  159. baseTcpHdrSum := uint32(checksum.Checksum(tmp[:tcpHdrLen], 0))
  160. var baseProtoSum uint32
  161. if isV4 {
  162. baseProtoSum = uint32(checksum.Checksum(pkt[ipv4SrcOff:ipv4AddrsEnd], 0))
  163. } else {
  164. baseProtoSum = uint32(checksum.Checksum(pkt[ipv6SrcOff:ipv6AddrsEnd], 0))
  165. }
  166. baseProtoSum += uint32(unix.IPPROTO_TCP)
  167. var origIPID uint16
  168. var baseIPHdrSum uint32
  169. if isV4 {
  170. origIPID = binary.BigEndian.Uint16(pkt[ipv4IDOff : ipv4IDOff+2])
  171. ihl := int(pkt[0]&0x0f) * 4
  172. if ihl < ipv4HeaderMinLen || ihl > csumStart {
  173. return fmt.Errorf("bad IPv4 IHL: %d", ihl)
  174. }
  175. var ipTmp [ipv4HeaderMaxLen]byte
  176. copy(ipTmp[:ihl], pkt[:ihl])
  177. ipTmp[ipv4TotalLenOff], ipTmp[ipv4TotalLenOff+1] = 0, 0
  178. ipTmp[ipv4IDOff], ipTmp[ipv4IDOff+1] = 0, 0
  179. ipTmp[ipv4ChecksumOff], ipTmp[ipv4ChecksumOff+1] = 0, 0
  180. baseIPHdrSum = uint32(checksum.Checksum(ipTmp[:ihl], 0))
  181. }
  182. for i := 0; i < numSeg; i++ {
  183. segStart := i * gsoSize
  184. segEnd := segStart + gsoSize
  185. if segEnd > payLen {
  186. segEnd = payLen
  187. }
  188. segPayLen := segEnd - segStart
  189. segLen := headerLen + segPayLen
  190. headerOff := i * gsoSize
  191. // Slide the header into place immediately before this segment's
  192. // payload. Iter 0's header is already at pkt[:headerLen]; for
  193. // i ≥ 1 we copy from there. The constant-byte fields of pkt[:headerLen]
  194. // survive iter 0's in-place patches (only seq/flags/cksum/totalLen/id
  195. // are touched), and iter 0's stale variable-field values are
  196. // overwritten by the per-segment patches below.
  197. if i > 0 {
  198. copy(pkt[headerOff:headerOff+headerLen], pkt[:headerLen])
  199. }
  200. seg := pkt[headerOff : headerOff+segLen]
  201. segSeq := origSeq + uint32(segStart)
  202. segFlags := origFlags
  203. if i != 0 {
  204. segFlags &^= tcpCwrFlag
  205. }
  206. if i != numSeg-1 {
  207. segFlags &^= tcpFinPshMask
  208. }
  209. totalLen := segLen
  210. if isV4 {
  211. segID := origIPID + uint16(i)
  212. binary.BigEndian.PutUint16(seg[ipv4TotalLenOff:ipv4TotalLenOff+2], uint16(totalLen))
  213. binary.BigEndian.PutUint16(seg[ipv4IDOff:ipv4IDOff+2], segID)
  214. ipSum := baseIPHdrSum + uint32(totalLen) + uint32(segID)
  215. binary.BigEndian.PutUint16(seg[ipv4ChecksumOff:ipv4ChecksumOff+2], foldComplement(ipSum))
  216. } else {
  217. binary.BigEndian.PutUint16(seg[ipv6PayloadLenOff:ipv6PayloadLenOff+2], uint16(headerLen-ipv6FixedLen+segPayLen))
  218. }
  219. binary.BigEndian.PutUint32(seg[csumStart+tcpSeqOff:csumStart+tcpSeqOff+4], segSeq)
  220. seg[csumStart+tcpFlagsOff] = segFlags
  221. tcpLen := tcpHdrLen + segPayLen
  222. // Payload bytes still live at their original offset in pkt. The
  223. // header slide above only writes into pkt[i*G : i*G+H], which is
  224. // the tail of seg_{i-1}'s payload (already consumed) and never
  225. // overlaps seg_i's own payload at pkt[H+i*G : H+(i+1)*G].
  226. paySum := uint32(checksum.Checksum(pkt[headerLen+segStart:headerLen+segEnd], 0))
  227. wide := uint64(baseTcpHdrSum) + uint64(paySum) + uint64(baseProtoSum)
  228. wide += uint64(segSeq) + uint64(segFlags) + uint64(tcpLen)
  229. wide = (wide & 0xffffffff) + (wide >> 32)
  230. wide = (wide & 0xffffffff) + (wide >> 32)
  231. binary.BigEndian.PutUint16(seg[csumStart+tcpChecksumOff:csumStart+tcpChecksumOff+2], foldComplement(uint32(wide)))
  232. if err := yield(seg); err != nil {
  233. return err
  234. }
  235. }
  236. return nil
  237. }
  238. // SegmentUDP walks a USO superpacket, sliding a per-segment-patched
  239. // L3+L4 header into pkt at offset i*gsoSize and yielding pkt[i*G:i*G+segLen]
  240. // to the caller. Per-segment patches are total_len + IPv4 csum (or IPv6
  241. // payload_len) plus the UDP length and checksum. pkt is consumed
  242. // destructively; see SegmentTCP for the layout reasoning.
  243. //
  244. // UDP-GSO leaves the IPv4 ID identical across segments (the kernel does not
  245. // bump it), which is why the IP-level per-segment work is limited to
  246. // total_len + IPv4 header checksum (v4) or payload_len (v6).
  247. func SegmentUDP(pkt []byte, hdrLenU, csumStartU, gsoSizeU uint16, yield func(seg []byte) error) error {
  248. if gsoSizeU == 0 {
  249. return fmt.Errorf("gso_size is zero")
  250. }
  251. if csumStartU == 0 {
  252. return fmt.Errorf("csum_start is zero")
  253. }
  254. isV4 := pkt[0]>>4 == 4
  255. headerLen := int(hdrLenU)
  256. csumStart := int(csumStartU)
  257. if headerLen-csumStart != udpHeaderLen {
  258. return fmt.Errorf("udp header len mismatch: %d", headerLen-csumStart)
  259. }
  260. payLen := len(pkt) - headerLen
  261. gsoSize := int(gsoSizeU)
  262. numSeg := (payLen + gsoSize - 1) / gsoSize
  263. if numSeg == 0 {
  264. numSeg = 1
  265. }
  266. var udpTmp [udpHeaderLen]byte
  267. copy(udpTmp[:], pkt[csumStart:headerLen])
  268. udpTmp[udpLengthOff], udpTmp[udpLengthOff+1] = 0, 0
  269. udpTmp[udpChecksumOff], udpTmp[udpChecksumOff+1] = 0, 0
  270. baseUDPHdrSum := uint32(checksum.Checksum(udpTmp[:], 0))
  271. var baseProtoSum uint32
  272. if isV4 {
  273. baseProtoSum = uint32(checksum.Checksum(pkt[ipv4SrcOff:ipv4AddrsEnd], 0))
  274. } else {
  275. baseProtoSum = uint32(checksum.Checksum(pkt[ipv6SrcOff:ipv6AddrsEnd], 0))
  276. }
  277. baseProtoSum += uint32(unix.IPPROTO_UDP)
  278. var baseIPHdrSum uint32
  279. if isV4 {
  280. ihl := int(pkt[0]&0x0f) * 4
  281. if ihl < ipv4HeaderMinLen || ihl > csumStart {
  282. return fmt.Errorf("bad IPv4 IHL: %d", ihl)
  283. }
  284. var ipTmp [ipv4HeaderMaxLen]byte
  285. copy(ipTmp[:ihl], pkt[:ihl])
  286. ipTmp[ipv4TotalLenOff], ipTmp[ipv4TotalLenOff+1] = 0, 0
  287. ipTmp[ipv4ChecksumOff], ipTmp[ipv4ChecksumOff+1] = 0, 0
  288. baseIPHdrSum = uint32(checksum.Checksum(ipTmp[:ihl], 0))
  289. }
  290. for i := 0; i < numSeg; i++ {
  291. segStart := i * gsoSize
  292. segEnd := segStart + gsoSize
  293. if segEnd > payLen {
  294. segEnd = payLen
  295. }
  296. segPayLen := segEnd - segStart
  297. segLen := headerLen + segPayLen
  298. headerOff := i * gsoSize
  299. if i > 0 {
  300. copy(pkt[headerOff:headerOff+headerLen], pkt[:headerLen])
  301. }
  302. seg := pkt[headerOff : headerOff+segLen]
  303. totalLen := segLen
  304. udpLen := udpHeaderLen + segPayLen
  305. if isV4 {
  306. binary.BigEndian.PutUint16(seg[ipv4TotalLenOff:ipv4TotalLenOff+2], uint16(totalLen))
  307. ipSum := baseIPHdrSum + uint32(totalLen)
  308. binary.BigEndian.PutUint16(seg[ipv4ChecksumOff:ipv4ChecksumOff+2], foldComplement(ipSum))
  309. } else {
  310. binary.BigEndian.PutUint16(seg[ipv6PayloadLenOff:ipv6PayloadLenOff+2], uint16(headerLen-ipv6FixedLen+segPayLen))
  311. }
  312. binary.BigEndian.PutUint16(seg[csumStart+udpLengthOff:csumStart+udpLengthOff+2], uint16(udpLen))
  313. paySum := uint32(checksum.Checksum(pkt[headerLen+segStart:headerLen+segEnd], 0))
  314. wide := uint64(baseUDPHdrSum) + uint64(paySum) + uint64(baseProtoSum)
  315. wide += uint64(udpLen) + uint64(udpLen)
  316. wide = (wide & 0xffffffff) + (wide >> 32)
  317. wide = (wide & 0xffffffff) + (wide >> 32)
  318. csum := foldComplement(uint32(wide))
  319. if csum == 0 {
  320. csum = 0xffff
  321. }
  322. binary.BigEndian.PutUint16(seg[csumStart+udpChecksumOff:csumStart+udpChecksumOff+2], csum)
  323. if err := yield(seg); err != nil {
  324. return err
  325. }
  326. }
  327. return nil
  328. }
  329. // FinishChecksum computes the L4 checksum for a non-GSO packet that the kernel
  330. // handed us with NEEDS_CSUM set. csum_start / csum_offset point at the 16-bit
  331. // checksum field; we zero it, fold a full sum (the field was pre-loaded with
  332. // the pseudo-header partial sum by the kernel), and store the result.
  333. func FinishChecksum(seg []byte, hdr Hdr) error {
  334. cs := int(hdr.CsumStart)
  335. co := int(hdr.CsumOffset)
  336. if cs+co+2 > len(seg) {
  337. return fmt.Errorf("csum offsets out of range: start=%d offset=%d len=%d", cs, co, len(seg))
  338. }
  339. // The kernel stores a partial pseudo-header sum at [cs+co:]; sum over the
  340. // L4 region starting at cs, folding the prior partial in as the seed.
  341. partial := binary.BigEndian.Uint16(seg[cs+co : cs+co+2])
  342. seg[cs+co] = 0
  343. seg[cs+co+1] = 0
  344. binary.BigEndian.PutUint16(seg[cs+co:cs+co+2], ^checksum.Checksum(seg[cs:], partial))
  345. return nil
  346. }
  347. // foldComplement folds a 32-bit one's-complement partial sum to 16 bits and
  348. // complements it, yielding the on-wire Internet checksum value.
  349. func foldComplement(sum uint32) uint16 {
  350. sum = (sum & 0xffff) + (sum >> 16)
  351. sum = (sum & 0xffff) + (sum >> 16)
  352. return ^uint16(sum)
  353. }