segment_linux.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. //go:build linux && !android
  2. // +build linux,!android
  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. "github.com/slackhq/nebula/overlay/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. // maxSegHdrLen bounds the L3+L4 header we snapshot before stamping each segment.
  25. // The largest header the segmenter supports is IPv4 (max IHL 60) plus TCP (max data-offset 60) = 120 bytes
  26. const maxSegHdrLen = ipv4HeaderMaxLen + tcpHeaderMaxLen // 120
  27. // Byte offsets inside an IPv4 header.
  28. const (
  29. ipv4TotalLenOff = 2
  30. ipv4IDOff = 4
  31. ipv4ChecksumOff = 10
  32. ipv4SrcOff = 12
  33. ipv4AddrsEnd = 20 // end of dst address (ipv4SrcOff + 2*4)
  34. )
  35. // Byte offsets inside an IPv6 header.
  36. const (
  37. ipv6PayloadLenOff = 4
  38. ipv6SrcOff = 8
  39. ipv6AddrsEnd = 40 // end of dst address (ipv6SrcOff + 2*16)
  40. )
  41. // Byte offsets inside a TCP header (relative to its start, i.e. csumStart).
  42. const (
  43. tcpSeqOff = 4
  44. tcpDataOffOff = 12 // upper nibble is header len in 32-bit words
  45. tcpFlagsOff = 13
  46. tcpChecksumOff = 16
  47. )
  48. // UDP header is fixed at 8 bytes: {sport, dport, length, checksum}.
  49. const (
  50. udpHeaderLen = 8
  51. udpLengthOff = 4
  52. udpChecksumOff = 6
  53. )
  54. var errPacketTooShort = errors.New("packet too short")
  55. // tcpFinPshMask is cleared on every segment except the last of a TSO burst.
  56. const tcpFinPshMask = 0x09 // FIN(0x01) | PSH(0x08)
  57. // tcpCwrFlag is cleared on every segment except the first.
  58. // Per RFC 3168 §6.1.2 the CWR bit signals a one-shot transition (the sender just halved its window)
  59. // and must appear on the first segment of a TSO burst only.
  60. const tcpCwrFlag = 0x80
  61. // CheckValid rejects packets whose virtio_net_hdr/IP combination would
  62. // cause a downstream miscompute. The TUN should never emit RSC_INFO and
  63. // the GSO type must agree with the IP version nibble.
  64. func CheckValid(pkt []byte, hdr Hdr) error {
  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 errPacketTooShort
  70. }
  71. ipVersion := pkt[0] >> 4
  72. if ipVersion == 6 && len(pkt) < ipv6FixedLen {
  73. return errPacketTooShort
  74. }
  75. gsoType := hdr.GSOType()
  76. if gsoType != unix.VIRTIO_NET_HDR_GSO_NONE && hdr.GSOSize == 0 {
  77. // A GSO type with no segment size would dodge IsSuperpacket() downstream and
  78. // travel as a plain jumbo datagram with an unfinished checksum.
  79. return fmt.Errorf("virtio GSO type %#x with zero gso_size", hdr.gsoType)
  80. }
  81. if hdr.HasECNFlag() && !(gsoType == unix.VIRTIO_NET_HDR_GSO_TCPV4 || gsoType == unix.VIRTIO_NET_HDR_GSO_TCPV6) {
  82. return fmt.Errorf("virtio GSO_ECN qualifier on non-TCP GSO type %#x", hdr.gsoType)
  83. }
  84. switch gsoType {
  85. case unix.VIRTIO_NET_HDR_GSO_TCPV4:
  86. if ipVersion != 4 {
  87. return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.gsoType)
  88. }
  89. case unix.VIRTIO_NET_HDR_GSO_TCPV6:
  90. if ipVersion != 6 {
  91. return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.gsoType)
  92. }
  93. case unix.VIRTIO_NET_HDR_GSO_UDP_L4:
  94. // USO carries either v4 or v6; the leading nibble disambiguates.
  95. if !(ipVersion == 4 || ipVersion == 6) {
  96. return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.gsoType)
  97. }
  98. default:
  99. if !(ipVersion == 6 || ipVersion == 4) {
  100. return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.gsoType)
  101. }
  102. }
  103. return nil
  104. }
  105. // CorrectHdrLen rewrites hdr.HdrLen based on the actual transport header length read out of pkt.
  106. // The kernel's hdr.HdrLen on the FORWARD path can be the length of the entire first packet, so we don't trust it.
  107. func CorrectHdrLen(pkt []byte, hdr *Hdr) error {
  108. // Thank you wireguard-go for documenting these edge-cases
  109. // Don't trust hdr.hdrLen from the kernel as it can be equal to the length
  110. // of the entire first packet when the kernel is handling it as part of a FORWARD path.
  111. // Instead, parse the transport header length and add it onto csumStart, which is synonymous for IP header length.
  112. if hdr.GSOType() == unix.VIRTIO_NET_HDR_GSO_UDP_L4 {
  113. hdr.HdrLen = hdr.CsumStart + 8
  114. } else {
  115. if len(pkt) <= int(hdr.CsumStart+tcpDataOffOff) {
  116. return errors.New("packet is too short")
  117. }
  118. tcpHLen := uint16(pkt[hdr.CsumStart+tcpDataOffOff] >> 4 * 4)
  119. if tcpHLen < tcpHeaderMinLen || tcpHLen > tcpHeaderMaxLen {
  120. return fmt.Errorf("tcp header len is invalid: %d", tcpHLen)
  121. }
  122. hdr.HdrLen = hdr.CsumStart + tcpHLen
  123. }
  124. if len(pkt) < int(hdr.HdrLen) {
  125. return fmt.Errorf("length of packet (%d) < virtioNetHdr.HdrLen (%d)", len(pkt), hdr.HdrLen)
  126. }
  127. if hdr.HdrLen < hdr.CsumStart {
  128. return fmt.Errorf("virtioNetHdr.HdrLen (%d) < virtioNetHdr.CsumStart (%d)", hdr.HdrLen, hdr.CsumStart)
  129. }
  130. cSumAt := int(hdr.CsumStart + hdr.CsumOffset)
  131. if cSumAt+1 >= len(pkt) {
  132. return fmt.Errorf("end of checksum offset (%d) exceeds packet length (%d)", cSumAt+1, len(pkt))
  133. }
  134. return nil
  135. }
  136. // segCount returns how many segments a payload of payLen bytes splits into at gsoSize,
  137. // with a floor of one so a header-only superpacket still yields a single segment.
  138. func segCount(payLen, gsoSize int) int {
  139. n := (payLen + gsoSize - 1) / gsoSize
  140. if n == 0 {
  141. return 1
  142. }
  143. return n
  144. }
  145. // basePseudoSum folds the part of the L4 pseudo-header sum that is identical
  146. // for every segment: the source and destination addresses plus the protocol
  147. // number. The per-segment L4 length is added by the caller inside the loop.
  148. func basePseudoSum(pkt []byte, isV4 bool, proto uint32) uint32 {
  149. if isV4 {
  150. return uint32(checksum.Checksum(pkt[ipv4SrcOff:ipv4AddrsEnd], 0)) + proto
  151. }
  152. return uint32(checksum.Checksum(pkt[ipv6SrcOff:ipv6AddrsEnd], 0)) + proto
  153. }
  154. // baseIPv4HdrSum folds the IPv4 header checksum over the fields that stay constant across segments.
  155. // csumStart is the L3 header length, which bounds a valid IHL.
  156. func baseIPv4HdrSum(pkt []byte, csumStart int) (uint32, error) {
  157. ihl := int(pkt[0]&0x0f) * 4
  158. if ihl < ipv4HeaderMinLen || ihl > csumStart {
  159. return 0, fmt.Errorf("bad IPv4 IHL: %d", ihl)
  160. }
  161. // total_len, the ID, and the checksum field itself are excluded: all three are rewritten per segment.
  162. sum := uint32(checksum.Checksum(pkt[:ihl], 0))
  163. sum += uint32(^binary.BigEndian.Uint16(pkt[ipv4TotalLenOff : ipv4TotalLenOff+2]))
  164. sum += uint32(^binary.BigEndian.Uint16(pkt[ipv4ChecksumOff : ipv4ChecksumOff+2]))
  165. sum += uint32(^binary.BigEndian.Uint16(pkt[ipv4IDOff : ipv4IDOff+2]))
  166. sum = (sum & 0xffff) + (sum >> 16)
  167. sum = (sum & 0xffff) + (sum >> 16)
  168. return sum, nil
  169. }
  170. // baseTCPHdrSum folds the TCP header checksum over everything the segment loop does not rewrite
  171. func baseTCPHdrSum(pkt []byte, csumStart, headerLen int) uint32 {
  172. seq := binary.BigEndian.Uint32(pkt[csumStart+tcpSeqOff : csumStart+tcpSeqOff+4])
  173. flags := uint16(pkt[csumStart+tcpFlagsOff])
  174. sum := uint32(checksum.Checksum(pkt[csumStart:headerLen], 0))
  175. sum += uint32(^uint16(seq >> 16))
  176. sum += uint32(^uint16(seq))
  177. sum += uint32(^flags)
  178. sum += uint32(^binary.BigEndian.Uint16(pkt[csumStart+tcpChecksumOff : csumStart+tcpChecksumOff+2]))
  179. sum = (sum & 0xffff) + (sum >> 16)
  180. sum = (sum & 0xffff) + (sum >> 16)
  181. return sum
  182. }
  183. // SegmentTCP walks a TSO superpacket pkt, yielding each segment as a slice into pkt.
  184. // Per-segment plaintext is laid out by stamping a copy of the original L3+L4 header into pkt at offset i*gsoSize,
  185. // where it sits immediately before that segment's payload chunk in the original buffer.
  186. // pkt is consumed by this call and must not be inspected by the caller after the final yield.
  187. func SegmentTCP(pkt []byte, hdrLenU, csumStartU, gsoSizeU uint16, yield func(seg []byte) error) error {
  188. if gsoSizeU == 0 {
  189. return fmt.Errorf("gso_size is zero")
  190. }
  191. if csumStartU == 0 {
  192. return fmt.Errorf("csum_start is zero")
  193. }
  194. headerLen := int(hdrLenU)
  195. csumStart := int(csumStartU)
  196. if headerLen > maxSegHdrLen {
  197. return fmt.Errorf("header len %d exceeds max %d", headerLen, maxSegHdrLen)
  198. }
  199. isV4 := pkt[0]>>4 == 4
  200. tcpHdrLen := int(pkt[csumStart+tcpDataOffOff]>>4) * 4
  201. payLen := len(pkt) - headerLen
  202. gsoSize := int(gsoSizeU)
  203. numSeg := segCount(payLen, gsoSize)
  204. origSeq := binary.BigEndian.Uint32(pkt[csumStart+tcpSeqOff : csumStart+tcpSeqOff+4])
  205. origFlags := pkt[csumStart+tcpFlagsOff]
  206. baseProtoSum := basePseudoSum(pkt, isV4, unix.IPPROTO_TCP)
  207. baseTcpHdrSum := baseTCPHdrSum(pkt, csumStart, headerLen)
  208. var origIPID uint16
  209. var baseIPHdrSum uint32
  210. if isV4 {
  211. origIPID = binary.BigEndian.Uint16(pkt[ipv4IDOff : ipv4IDOff+2])
  212. var err error
  213. // TSO bumps the ID per segment, so it stays out of the base sum.
  214. baseIPHdrSum, err = baseIPv4HdrSum(pkt, csumStart)
  215. if err != nil {
  216. return err
  217. }
  218. }
  219. // Snapshot the pristine L3+L4 header once. '
  220. // Every segment's header is stamped from this copy, so overlapping stamps (gsoSize < headerLen) can never corrupt the source.
  221. var savedHdr [maxSegHdrLen]byte
  222. copy(savedHdr[:headerLen], pkt[:headerLen])
  223. for i := 0; i < numSeg; i++ {
  224. segStart := i * gsoSize
  225. segEnd := segStart + gsoSize
  226. if segEnd > payLen {
  227. segEnd = payLen
  228. }
  229. segPayLen := segEnd - segStart
  230. segLen := headerLen + segPayLen
  231. headerOff := i * gsoSize
  232. // Stamp the header into place immediately before this segment's payload, sourced from the snapshot.
  233. // The per-segment patches below overwrite the variable fields. (seq/flags/cksum/totalLen/id)
  234. if i > 0 {
  235. // Iter 0's header is already at pkt[:headerLen] (identical to savedHdr), so only i >= 1 needs the stamp
  236. copy(pkt[headerOff:headerOff+headerLen], savedHdr[:headerLen])
  237. }
  238. seg := pkt[headerOff : headerOff+segLen]
  239. segSeq := origSeq + uint32(segStart)
  240. segFlags := origFlags
  241. if i != 0 {
  242. segFlags &^= tcpCwrFlag
  243. }
  244. if i != numSeg-1 {
  245. segFlags &^= tcpFinPshMask
  246. }
  247. totalLen := segLen
  248. if isV4 {
  249. segID := origIPID + uint16(i)
  250. binary.BigEndian.PutUint16(seg[ipv4TotalLenOff:ipv4TotalLenOff+2], uint16(totalLen))
  251. binary.BigEndian.PutUint16(seg[ipv4IDOff:ipv4IDOff+2], segID)
  252. ipSum := baseIPHdrSum + uint32(totalLen) + uint32(segID)
  253. binary.BigEndian.PutUint16(seg[ipv4ChecksumOff:ipv4ChecksumOff+2], foldComplement(ipSum))
  254. } else {
  255. binary.BigEndian.PutUint16(seg[ipv6PayloadLenOff:ipv6PayloadLenOff+2], uint16(headerLen-ipv6FixedLen+segPayLen))
  256. }
  257. binary.BigEndian.PutUint32(seg[csumStart+tcpSeqOff:csumStart+tcpSeqOff+4], segSeq)
  258. seg[csumStart+tcpFlagsOff] = segFlags
  259. tcpLen := tcpHdrLen + segPayLen
  260. // Payload bytes still live at their original offset in pkt.
  261. // The header slide above only writes into pkt[i*GSOSize : i*GSOSize+header], which is the tail of seg_{i-1}'s payload (already consumed)
  262. // and never overlaps seg_i's own payload at pkt[header+i*GSOSize : header+(i+1)*GSOSize].
  263. paySum := uint32(checksum.Checksum(pkt[headerLen+segStart:headerLen+segEnd], 0))
  264. wide := uint64(baseTcpHdrSum) + uint64(paySum) + uint64(baseProtoSum)
  265. wide += uint64(segSeq) + uint64(segFlags) + uint64(tcpLen)
  266. wide = (wide & 0xffffffff) + (wide >> 32)
  267. wide = (wide & 0xffffffff) + (wide >> 32)
  268. binary.BigEndian.PutUint16(seg[csumStart+tcpChecksumOff:csumStart+tcpChecksumOff+2], foldComplement(uint32(wide)))
  269. if err := yield(seg); err != nil {
  270. return err
  271. }
  272. }
  273. return nil
  274. }
  275. // SegmentUDP walks a USO superpacket, stamping a per-segment-patched copy of the original L3+L4 header
  276. // into pkt at offset i*GSOSize and yielding pkt[i*GSOSize:i*GSOSize+segLen] to the caller.
  277. // Per-segment patches are total_len + IPv4 csum (or IPv6 payload_len) plus the UDP length and checksum.
  278. // pkt is consumed destructively.
  279. func SegmentUDP(pkt []byte, hdrLenU, csumStartU, gsoSizeU uint16, yield func(seg []byte) error) error {
  280. if gsoSizeU == 0 {
  281. return fmt.Errorf("gso_size is zero")
  282. }
  283. if csumStartU == 0 {
  284. return fmt.Errorf("csum_start is zero")
  285. }
  286. isV4 := pkt[0]>>4 == 4
  287. headerLen := int(hdrLenU)
  288. csumStart := int(csumStartU)
  289. if headerLen > maxSegHdrLen {
  290. return fmt.Errorf("header len %d exceeds max %d", headerLen, maxSegHdrLen)
  291. }
  292. if headerLen-csumStart != udpHeaderLen {
  293. return fmt.Errorf("udp header len mismatch: %d", headerLen-csumStart)
  294. }
  295. payLen := len(pkt) - headerLen
  296. gsoSize := int(gsoSizeU)
  297. numSeg := segCount(payLen, gsoSize)
  298. baseProtoSum := basePseudoSum(pkt, isV4, unix.IPPROTO_UDP)
  299. var origIPID uint16
  300. var baseIPHdrSum uint32
  301. if isV4 {
  302. origIPID = binary.BigEndian.Uint16(pkt[ipv4IDOff : ipv4IDOff+2])
  303. var err error
  304. // Software UDP GSO bumps the ID per segment just like TSO
  305. // (inet_gso_segment's fixed-ID case is TCP-only), so it stays out of the base sum.
  306. baseIPHdrSum, err = baseIPv4HdrSum(pkt, csumStart)
  307. if err != nil {
  308. return err
  309. }
  310. }
  311. // Snapshot the pristine L3+L4 header once and stamp every segment from it
  312. var savedHdr [maxSegHdrLen]byte
  313. copy(savedHdr[:headerLen], pkt[:headerLen])
  314. for i := 0; i < numSeg; i++ {
  315. segStart := i * gsoSize
  316. segEnd := segStart + gsoSize
  317. if segEnd > payLen {
  318. segEnd = payLen
  319. }
  320. segPayLen := segEnd - segStart
  321. segLen := headerLen + segPayLen
  322. headerOff := i * gsoSize
  323. if i > 0 {
  324. copy(pkt[headerOff:headerOff+headerLen], savedHdr[:headerLen])
  325. }
  326. seg := pkt[headerOff : headerOff+segLen]
  327. totalLen := segLen
  328. udpLen := udpHeaderLen + segPayLen
  329. if isV4 {
  330. segID := origIPID + uint16(i)
  331. binary.BigEndian.PutUint16(seg[ipv4TotalLenOff:ipv4TotalLenOff+2], uint16(totalLen))
  332. binary.BigEndian.PutUint16(seg[ipv4IDOff:ipv4IDOff+2], segID)
  333. ipSum := baseIPHdrSum + uint32(totalLen) + uint32(segID)
  334. binary.BigEndian.PutUint16(seg[ipv4ChecksumOff:ipv4ChecksumOff+2], foldComplement(ipSum))
  335. } else {
  336. binary.BigEndian.PutUint16(seg[ipv6PayloadLenOff:ipv6PayloadLenOff+2], uint16(headerLen-ipv6FixedLen+segPayLen))
  337. }
  338. binary.BigEndian.PutUint16(seg[csumStart+udpLengthOff:csumStart+udpLengthOff+2], uint16(udpLen))
  339. // Sum the UDP header (length just written, checksum zeroed) together with
  340. // this segment's payload in one pass, seeded with the pseudo-header sum.
  341. seg[csumStart+udpChecksumOff], seg[csumStart+udpChecksumOff+1] = 0, 0
  342. pseudo := baseProtoSum + uint32(udpLen)
  343. pseudo = (pseudo & 0xffff) + (pseudo >> 16)
  344. pseudo = (pseudo & 0xffff) + (pseudo >> 16)
  345. csum := ^checksum.Checksum(seg[csumStart:], uint16(pseudo))
  346. if csum == 0 {
  347. csum = 0xffff
  348. }
  349. binary.BigEndian.PutUint16(seg[csumStart+udpChecksumOff:csumStart+udpChecksumOff+2], csum)
  350. if err := yield(seg); err != nil {
  351. return err
  352. }
  353. }
  354. return nil
  355. }
  356. // FinishChecksum computes the L4 checksum for a non-GSO packet that the kernel handed us with NEEDS_CSUM set.
  357. // CsumStart / CsumOffset point at the 16-bit checksum field.
  358. // We zero it, fold a full sum from the partial one that the kernel provided, and store the result.
  359. func FinishChecksum(seg []byte, hdr Hdr) error {
  360. cs := int(hdr.CsumStart)
  361. co := int(hdr.CsumOffset)
  362. if cs+co+2 > len(seg) {
  363. return fmt.Errorf("csum offsets out of range: start=%d offset=%d len=%d", cs, co, len(seg))
  364. }
  365. // The kernel stores a partial pseudo-header sum at [cs+co:]; sum over the
  366. // L4 region starting at cs, folding the prior partial in as the seed.
  367. partial := binary.BigEndian.Uint16(seg[cs+co : cs+co+2])
  368. seg[cs+co] = 0
  369. seg[cs+co+1] = 0
  370. csum := ^checksum.Checksum(seg[cs:], partial)
  371. // RFC 768: UDP transmits a computed zero as all ones, since all-zero is the reserved "no checksum" value.
  372. if co == udpChecksumOff && csum == 0 {
  373. csum = 0xffff
  374. }
  375. binary.BigEndian.PutUint16(seg[cs+co:cs+co+2], csum)
  376. return nil
  377. }
  378. // foldComplement folds a 32-bit one's-complement partial sum to 16 bits and
  379. // complements it, yielding the on-wire Internet checksum value.
  380. func foldComplement(sum uint32) uint16 {
  381. sum = (sum & 0xffff) + (sum >> 16)
  382. sum = (sum & 0xffff) + (sum >> 16)
  383. return ^uint16(sum)
  384. }