tun_linux_offload.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. //go:build linux && !android && !e2e_testing
  2. // +build linux,!android,!e2e_testing
  3. package tio
  4. import (
  5. "encoding/binary"
  6. "errors"
  7. "fmt"
  8. "golang.org/x/sys/unix"
  9. "gvisor.dev/gvisor/pkg/tcpip/checksum"
  10. )
  11. // Protocol header size bounds used to validate / cap kernel-supplied offsets.
  12. const (
  13. ipv4HeaderMinLen = 20 // IHL=5, no options
  14. ipv4HeaderMaxLen = 60 // IHL=15, max options
  15. ipv6FixedLen = 40 // IPv6 base header; extensions would extend this
  16. tcpHeaderMinLen = 20 // data-offset=5, no options
  17. tcpHeaderMaxLen = 60 // data-offset=15, max options
  18. )
  19. // Byte offsets inside an IPv4 header.
  20. const (
  21. ipv4TotalLenOff = 2
  22. ipv4IDOff = 4
  23. ipv4ChecksumOff = 10
  24. ipv4SrcOff = 12
  25. ipv4AddrsEnd = 20 // end of dst address (ipv4SrcOff + 2*4)
  26. )
  27. // Byte offsets inside an IPv6 header.
  28. const (
  29. ipv6PayloadLenOff = 4
  30. ipv6SrcOff = 8
  31. ipv6AddrsEnd = 40 // end of dst address (ipv6SrcOff + 2*16)
  32. )
  33. // Byte offsets inside a TCP header (relative to its start, i.e. csumStart).
  34. const (
  35. tcpSeqOff = 4
  36. tcpDataOffOff = 12 // upper nibble is header len in 32-bit words
  37. tcpFlagsOff = 13
  38. tcpChecksumOff = 16
  39. )
  40. // tcpFinPshMask is cleared on every segment except the last of a TSO burst.
  41. const tcpFinPshMask = 0x09 // FIN(0x01) | PSH(0x08)
  42. func checkVirtioValid(pkt []byte, hdr VirtioNetHdr) error {
  43. // When RSC_INFO is set the csum_start/csum_offset fields are repurposed to
  44. // carry coalescing info rather than checksum offsets. A TUN writing via
  45. // IFF_VNET_HDR should never emit this, but if it did we would silently
  46. // miscompute the segment checksums — refuse the packet instead.
  47. if hdr.Flags&unix.VIRTIO_NET_HDR_F_RSC_INFO != 0 {
  48. return fmt.Errorf("virtio RSC_INFO flag not supported on TUN reads")
  49. }
  50. if len(pkt) < ipv4HeaderMinLen {
  51. return fmt.Errorf("packet too short")
  52. }
  53. ipVersion := pkt[0] >> 4
  54. switch hdr.GSOType {
  55. case unix.VIRTIO_NET_HDR_GSO_TCPV4:
  56. if ipVersion != 4 {
  57. return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.GSOType)
  58. }
  59. case unix.VIRTIO_NET_HDR_GSO_TCPV6:
  60. if ipVersion != 6 {
  61. return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.GSOType)
  62. }
  63. default:
  64. if !(ipVersion == 6 || ipVersion == 4) {
  65. return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.GSOType)
  66. }
  67. }
  68. return nil
  69. }
  70. func handleGSONone(pkt []byte, hdr VirtioNetHdr, out *[][]byte, scratch []byte) error {
  71. if len(pkt) > len(scratch) {
  72. return fmt.Errorf("packet larger than segment buffer: %d > %d", len(pkt), len(scratch))
  73. }
  74. copy(scratch, pkt)
  75. seg := scratch[:len(pkt)]
  76. if hdr.Flags&unix.VIRTIO_NET_HDR_F_NEEDS_CSUM != 0 {
  77. if err := finishChecksum(seg, hdr); err != nil {
  78. return err
  79. }
  80. }
  81. *out = append(*out, seg)
  82. return nil
  83. }
  84. func correctHdrLen(pkt []byte, hdr *VirtioNetHdr) error {
  85. // Thank you wireguard-go for documenting these edge-cases
  86. // Don't trust hdr.hdrLen from the kernel as it can be equal to the length
  87. // of the entire first packet when the kernel is handling it as part of a
  88. // FORWARD path. Instead, parse the transport header length and add it onto
  89. // csumStart, which is synonymous for IP header length.
  90. const tcpDataOffset = 12
  91. if hdr.GSOType == unix.VIRTIO_NET_HDR_GSO_UDP_L4 {
  92. hdr.HdrLen = hdr.CsumStart + 8
  93. } else {
  94. if len(pkt) <= int(hdr.CsumStart+tcpDataOffset) {
  95. return errors.New("packet is too short")
  96. }
  97. tcpHLen := uint16(pkt[hdr.CsumStart+tcpDataOffset] >> 4 * 4)
  98. if tcpHLen < 20 || tcpHLen > 60 {
  99. // A TCP header must be between 20 and 60 bytes in length.
  100. return fmt.Errorf("tcp header len is invalid: %d", tcpHLen)
  101. }
  102. hdr.HdrLen = hdr.CsumStart + tcpHLen
  103. }
  104. if len(pkt) < int(hdr.HdrLen) {
  105. return fmt.Errorf("length of packet (%d) < virtioNetHdr.HdrLen (%d)", len(pkt), hdr.HdrLen)
  106. }
  107. if hdr.HdrLen < hdr.CsumStart {
  108. return fmt.Errorf("virtioNetHdr.HdrLen (%d) < virtioNetHdr.CsumStart (%d)", hdr.HdrLen, hdr.CsumStart)
  109. }
  110. cSumAt := int(hdr.CsumStart + hdr.CsumStart)
  111. if cSumAt+1 >= len(pkt) {
  112. return fmt.Errorf("end of checksum offset (%d) exceeds packet length (%d)", cSumAt+1, len(pkt))
  113. }
  114. return nil
  115. }
  116. // segmentInto splits a TUN-side packet described by hdr into one or more
  117. // IP packets, each appended to *out as a slice of scratch. scratch must be
  118. // sized to hold every segment (including replicated headers).
  119. func segmentInto(pkt []byte, hdr VirtioNetHdr, out *[][]byte, scratch []byte) error {
  120. if err := checkVirtioValid(pkt, hdr); err != nil {
  121. return err
  122. }
  123. if hdr.GSOType == unix.VIRTIO_NET_HDR_GSO_NONE {
  124. return handleGSONone(pkt, hdr, out, scratch)
  125. }
  126. if err := correctHdrLen(pkt, &hdr); err != nil {
  127. return err
  128. }
  129. switch hdr.GSOType {
  130. case unix.VIRTIO_NET_HDR_GSO_TCPV4, unix.VIRTIO_NET_HDR_GSO_TCPV6:
  131. return segmentTCP(pkt, hdr, out, scratch)
  132. default:
  133. return fmt.Errorf("unsupported virtio gso type: %d", hdr.GSOType)
  134. }
  135. }
  136. // finishChecksum computes the L4 checksum for a non-GSO packet that the kernel
  137. // handed us with NEEDS_CSUM set. csum_start / csum_offset point at the 16-bit
  138. // checksum field; we zero it, fold a full sum (the field was pre-loaded with
  139. // the pseudo-header partial sum by the kernel), and store the result.
  140. func finishChecksum(seg []byte, hdr VirtioNetHdr) error {
  141. cs := int(hdr.CsumStart)
  142. co := int(hdr.CsumOffset)
  143. if cs+co+2 > len(seg) {
  144. return fmt.Errorf("csum offsets out of range: start=%d offset=%d len=%d", cs, co, len(seg))
  145. }
  146. // The kernel stores a partial pseudo-header sum at [cs+co:]; sum over the
  147. // L4 region starting at cs, folding the prior partial in as the seed.
  148. partial := binary.BigEndian.Uint16(seg[cs+co : cs+co+2])
  149. seg[cs+co] = 0
  150. seg[cs+co+1] = 0
  151. binary.BigEndian.PutUint16(seg[cs+co:cs+co+2], ^checksum.Checksum(seg[cs:], partial))
  152. return nil
  153. }
  154. // segmentTCP software-segments a TSO superpacket into one IP packet per MSS
  155. // chunk. The caller guarantees hdr.GSOType is TCPV4 or TCPV6.
  156. //
  157. // Hot-path shape: the per-segment loop only sums the payload chunk. The TCP
  158. // header, the IPv4 header, and the pseudo-header src/dst/proto contributions
  159. // are each summed once up front — every segment reuses those three pre-folded
  160. // uint32 values and combines them with small per-segment deltas (seq, flags,
  161. // tcpLen, ip_id, total_len) that are cheap to fold in.
  162. func segmentTCP(pkt []byte, hdr VirtioNetHdr, out *[][]byte, scratch []byte) error {
  163. if hdr.GSOSize == 0 {
  164. return fmt.Errorf("gso_size is zero")
  165. }
  166. if hdr.CsumStart == 0 {
  167. return fmt.Errorf("csum_start is zero")
  168. }
  169. isV4 := hdr.GSOType == unix.VIRTIO_NET_HDR_GSO_TCPV4
  170. headerLen := int(hdr.HdrLen) // already corrected by the caller
  171. csumStart := int(hdr.CsumStart)
  172. tcpHdrLen := int(pkt[csumStart+tcpDataOffOff]>>4) * 4
  173. payload := pkt[headerLen:]
  174. payLen := len(payload)
  175. gsoSize := int(hdr.GSOSize)
  176. numSeg := (payLen + gsoSize - 1) / gsoSize
  177. if numSeg == 0 {
  178. numSeg = 1
  179. }
  180. need := numSeg*headerLen + payLen
  181. if need > len(scratch) {
  182. return fmt.Errorf("scratch too small for %d segments: need %d have %d", numSeg, need, len(scratch))
  183. }
  184. origSeq := binary.BigEndian.Uint32(pkt[csumStart+tcpSeqOff : csumStart+tcpSeqOff+4])
  185. origFlags := pkt[csumStart+tcpFlagsOff]
  186. // Precompute the TCP header sum with seq/flags/csum zeroed. Copy onto
  187. // the stack, zero the per-segment-varying fields, sum once.
  188. var tmp [tcpHeaderMaxLen]byte
  189. copy(tmp[:tcpHdrLen], pkt[csumStart:headerLen])
  190. tmp[tcpSeqOff], tmp[tcpSeqOff+1], tmp[tcpSeqOff+2], tmp[tcpSeqOff+3] = 0, 0, 0, 0
  191. tmp[tcpFlagsOff] = 0
  192. tmp[tcpChecksumOff], tmp[tcpChecksumOff+1] = 0, 0
  193. baseTcpHdrSum := uint32(checksum.Checksum(tmp[:tcpHdrLen], 0))
  194. // Pseudo-header src+dst+proto contribution (tcpLen varies per segment).
  195. var baseProtoSum uint32
  196. if isV4 {
  197. baseProtoSum = uint32(checksum.Checksum(pkt[ipv4SrcOff:ipv4AddrsEnd], 0))
  198. } else {
  199. baseProtoSum = uint32(checksum.Checksum(pkt[ipv6SrcOff:ipv6AddrsEnd], 0))
  200. }
  201. baseProtoSum += uint32(unix.IPPROTO_TCP)
  202. // Precompute IPv4 header sum with total_len/id/csum zeroed.
  203. var origIPID uint16
  204. var ihl int
  205. var baseIPHdrSum uint32
  206. if isV4 {
  207. origIPID = binary.BigEndian.Uint16(pkt[ipv4IDOff : ipv4IDOff+2])
  208. ihl = int(pkt[0]&0x0f) * 4
  209. if ihl < ipv4HeaderMinLen || ihl > csumStart {
  210. return fmt.Errorf("bad IPv4 IHL: %d", ihl)
  211. }
  212. var ipTmp [ipv4HeaderMaxLen]byte
  213. copy(ipTmp[:ihl], pkt[:ihl])
  214. ipTmp[ipv4TotalLenOff], ipTmp[ipv4TotalLenOff+1] = 0, 0
  215. ipTmp[ipv4IDOff], ipTmp[ipv4IDOff+1] = 0, 0
  216. ipTmp[ipv4ChecksumOff], ipTmp[ipv4ChecksumOff+1] = 0, 0
  217. baseIPHdrSum = uint32(checksum.Checksum(ipTmp[:ihl], 0))
  218. }
  219. off := 0
  220. for i := 0; i < numSeg; i++ {
  221. segStart := i * gsoSize
  222. segEnd := segStart + gsoSize
  223. if segEnd > payLen {
  224. segEnd = payLen
  225. }
  226. segPayLen := segEnd - segStart
  227. copy(scratch[off:], pkt[:headerLen])
  228. copy(scratch[off+headerLen:], payload[segStart:segEnd])
  229. seg := scratch[off : off+headerLen+segPayLen]
  230. off += headerLen + segPayLen
  231. segSeq := origSeq + uint32(segStart)
  232. segFlags := origFlags
  233. if i != numSeg-1 {
  234. segFlags = origFlags &^ tcpFinPshMask
  235. }
  236. totalLen := headerLen + segPayLen
  237. // Patch IP header and write the v4 header checksum from the precomputed base.
  238. if isV4 {
  239. segID := origIPID + uint16(i)
  240. binary.BigEndian.PutUint16(seg[ipv4TotalLenOff:ipv4TotalLenOff+2], uint16(totalLen))
  241. binary.BigEndian.PutUint16(seg[ipv4IDOff:ipv4IDOff+2], segID)
  242. ipSum := baseIPHdrSum + uint32(totalLen) + uint32(segID)
  243. binary.BigEndian.PutUint16(seg[ipv4ChecksumOff:ipv4ChecksumOff+2], foldComplement(ipSum))
  244. } else {
  245. // IPv6 payload length excludes the fixed header but includes any
  246. // extension headers between [ipv6FixedLen:csumStart].
  247. binary.BigEndian.PutUint16(seg[ipv6PayloadLenOff:ipv6PayloadLenOff+2], uint16(headerLen-ipv6FixedLen+segPayLen))
  248. }
  249. // Patch TCP header.
  250. binary.BigEndian.PutUint32(seg[csumStart+tcpSeqOff:csumStart+tcpSeqOff+4], segSeq)
  251. seg[csumStart+tcpFlagsOff] = segFlags
  252. // (csum is written below; its prior contents in `seg` don't affect the
  253. // computation since we never sum over the segment's own header.)
  254. tcpLen := tcpHdrLen + segPayLen
  255. paySum := uint32(checksum.Checksum(payload[segStart:segEnd], 0))
  256. // Combine pre-folded uint32s into a wider accumulator, then fold. Using
  257. // uint64 guards against overflow when segSeq's high bits set.
  258. wide := uint64(baseTcpHdrSum) + uint64(paySum) + uint64(baseProtoSum)
  259. wide += uint64(segSeq) + uint64(segFlags) + uint64(tcpLen)
  260. wide = (wide & 0xffffffff) + (wide >> 32)
  261. wide = (wide & 0xffffffff) + (wide >> 32)
  262. binary.BigEndian.PutUint16(seg[csumStart+tcpChecksumOff:csumStart+tcpChecksumOff+2], foldComplement(uint32(wide)))
  263. *out = append(*out, seg)
  264. }
  265. return nil
  266. }
  267. // foldComplement folds a 32-bit one's-complement partial sum to 16 bits and
  268. // complements it, yielding the on-wire Internet checksum value.
  269. func foldComplement(sum uint32) uint16 {
  270. sum = (sum & 0xffff) + (sum >> 16)
  271. sum = (sum & 0xffff) + (sum >> 16)
  272. return ^uint16(sum)
  273. }
  274. // pseudoHeaderIPv4 returns the folded pseudo-header sum used to verify a TCP
  275. // segment's checksum in tests. src/dst are 4 bytes each.
  276. func pseudoHeaderIPv4(src, dst []byte, proto byte, tcpLen int) uint16 {
  277. s := uint32(checksum.Checksum(src, 0)) + uint32(checksum.Checksum(dst, 0))
  278. s += uint32(proto) + uint32(tcpLen)
  279. s = (s & 0xffff) + (s >> 16)
  280. s = (s & 0xffff) + (s >> 16)
  281. return uint16(s)
  282. }
  283. // pseudoHeaderIPv6 returns the folded pseudo-header sum used to verify a TCP
  284. // segment's checksum in tests. src/dst are 16 bytes each.
  285. func pseudoHeaderIPv6(src, dst []byte, proto byte, tcpLen int) uint16 {
  286. s := uint32(checksum.Checksum(src, 0)) + uint32(checksum.Checksum(dst, 0))
  287. s += uint32(tcpLen>>16) + uint32(tcpLen&0xffff) + uint32(proto)
  288. s = (s & 0xffff) + (s >> 16)
  289. s = (s & 0xffff) + (s >> 16)
  290. return uint16(s)
  291. }