tun_linux_offload.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. //go:build linux && !android && !e2e_testing
  2. // +build linux,!android,!e2e_testing
  3. package overlay
  4. import (
  5. "encoding/binary"
  6. "fmt"
  7. "golang.org/x/sys/unix"
  8. )
  9. // Size of the legacy struct virtio_net_hdr that the kernel prepends/expects on
  10. // a TUN opened with IFF_VNET_HDR (TUNSETVNETHDRSZ not set).
  11. const virtioNetHdrLen = 10
  12. // Maximum size we accept for a single read from a TUN with IFF_VNET_HDR. A
  13. // TSO superpacket can be up to 64KiB of payload plus a single L2/L3/L4 header
  14. // prefix plus the virtio header.
  15. const tunReadBufSize = 65535
  16. // Space for segmented output. Worst case is many small segments, each paying
  17. // an IP+TCP header. 128KiB comfortably covers the 64KiB payload ceiling.
  18. const tunSegBufSize = 131072
  19. // tunSegBufCap is the total size we allocate for the per-reader segment
  20. // buffer. It is sized as one worst-case TSO superpacket (tunSegBufSize) plus
  21. // the same again as drain headroom so a Read wake can accumulate
  22. // additional packets after an initial big read without overflowing.
  23. const tunSegBufCap = tunSegBufSize * 2
  24. // tunDrainCap caps how many packets a single Read will accumulate via
  25. // the post-wake drain loop. Sized to soak up a burst of small ACKs while
  26. // bounding how much work a single caller holds before handing off.
  27. const tunDrainCap = 64
  28. type virtioNetHdr struct {
  29. Flags uint8
  30. GSOType uint8
  31. HdrLen uint16
  32. GSOSize uint16
  33. CsumStart uint16
  34. CsumOffset uint16
  35. }
  36. // decode reads a virtio_net_hdr in host byte order (TUN default; we never
  37. // call TUNSETVNETLE so the kernel matches our endianness).
  38. func (h *virtioNetHdr) decode(b []byte) {
  39. h.Flags = b[0]
  40. h.GSOType = b[1]
  41. h.HdrLen = binary.NativeEndian.Uint16(b[2:4])
  42. h.GSOSize = binary.NativeEndian.Uint16(b[4:6])
  43. h.CsumStart = binary.NativeEndian.Uint16(b[6:8])
  44. h.CsumOffset = binary.NativeEndian.Uint16(b[8:10])
  45. }
  46. // encode is the inverse of decode: writes the virtio_net_hdr fields into b
  47. // (must be at least virtioNetHdrLen bytes). Used to emit a TSO superpacket
  48. // on egress.
  49. func (h *virtioNetHdr) encode(b []byte) {
  50. b[0] = h.Flags
  51. b[1] = h.GSOType
  52. binary.NativeEndian.PutUint16(b[2:4], h.HdrLen)
  53. binary.NativeEndian.PutUint16(b[4:6], h.GSOSize)
  54. binary.NativeEndian.PutUint16(b[6:8], h.CsumStart)
  55. binary.NativeEndian.PutUint16(b[8:10], h.CsumOffset)
  56. }
  57. // segmentInto splits a TUN-side packet described by hdr into one or more
  58. // IP packets, each appended to *out as a slice of scratch. scratch must be
  59. // sized to hold every segment (including replicated headers).
  60. func segmentInto(pkt []byte, hdr virtioNetHdr, out *[][]byte, scratch []byte) 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. switch hdr.GSOType {
  69. case unix.VIRTIO_NET_HDR_GSO_NONE:
  70. if len(pkt) > len(scratch) {
  71. return fmt.Errorf("packet larger than segment buffer: %d > %d", len(pkt), len(scratch))
  72. }
  73. copy(scratch, pkt)
  74. seg := scratch[:len(pkt)]
  75. if hdr.Flags&unix.VIRTIO_NET_HDR_F_NEEDS_CSUM != 0 {
  76. if err := finishChecksum(seg, hdr); err != nil {
  77. return err
  78. }
  79. }
  80. *out = append(*out, seg)
  81. return nil
  82. case unix.VIRTIO_NET_HDR_GSO_TCPV4, unix.VIRTIO_NET_HDR_GSO_TCPV6:
  83. return segmentTCP(pkt, hdr, out, scratch)
  84. default:
  85. return fmt.Errorf("unsupported virtio gso type: %d", hdr.GSOType)
  86. }
  87. }
  88. // finishChecksum computes the L4 checksum for a non-GSO packet that the kernel
  89. // handed us with NEEDS_CSUM set. csum_start / csum_offset point at the 16-bit
  90. // checksum field; we zero it, fold a full sum (the field was pre-loaded with
  91. // the pseudo-header partial sum by the kernel), and store the result.
  92. func finishChecksum(seg []byte, hdr virtioNetHdr) error {
  93. cs := int(hdr.CsumStart)
  94. co := int(hdr.CsumOffset)
  95. if cs+co+2 > len(seg) {
  96. return fmt.Errorf("csum offsets out of range: start=%d offset=%d len=%d", cs, co, len(seg))
  97. }
  98. // The kernel stores a partial pseudo-header sum at [cs+co:]; sum over the
  99. // L4 region starting at cs, folding the prior partial in as the seed.
  100. partial := uint32(binary.BigEndian.Uint16(seg[cs+co : cs+co+2]))
  101. seg[cs+co] = 0
  102. seg[cs+co+1] = 0
  103. sum := checksumBytes(seg[cs:], partial)
  104. binary.BigEndian.PutUint16(seg[cs+co:cs+co+2], checksumFold(sum))
  105. return nil
  106. }
  107. // segmentTCP software-segments a TSO superpacket into one IP packet per MSS
  108. // chunk. The caller guarantees hdr.GSOType is TCPV4 or TCPV6.
  109. //
  110. // Hot-path shape: the per-segment loop only sums the payload chunk. The TCP
  111. // header, the IPv4 header, and the pseudo-header src/dst/proto contributions
  112. // are each summed once up front — every segment reuses those three pre-folded
  113. // uint32 values and combines them with small per-segment deltas (seq, flags,
  114. // tcpLen, ip_id, total_len) that are cheap to fold in.
  115. func segmentTCP(pkt []byte, hdr virtioNetHdr, out *[][]byte, scratch []byte) error {
  116. if hdr.GSOSize == 0 {
  117. return fmt.Errorf("gso_size is zero")
  118. }
  119. if int(hdr.HdrLen) > len(pkt) || hdr.HdrLen == 0 {
  120. return fmt.Errorf("hdr_len %d out of range (pkt %d)", hdr.HdrLen, len(pkt))
  121. }
  122. if hdr.CsumStart == 0 || hdr.CsumStart >= hdr.HdrLen {
  123. return fmt.Errorf("csum_start %d out of range (hdr_len %d)", hdr.CsumStart, hdr.HdrLen)
  124. }
  125. isV4 := hdr.GSOType == unix.VIRTIO_NET_HDR_GSO_TCPV4
  126. headerLen := int(hdr.HdrLen)
  127. csumStart := int(hdr.CsumStart)
  128. if isV4 && csumStart < 20 {
  129. return fmt.Errorf("csum_start %d too small for IPv4", csumStart)
  130. }
  131. if !isV4 && csumStart < 40 {
  132. return fmt.Errorf("csum_start %d too small for IPv6", csumStart)
  133. }
  134. tcpHdrLen := headerLen - csumStart
  135. if tcpHdrLen < 20 {
  136. return fmt.Errorf("tcp header region too small: %d", tcpHdrLen)
  137. }
  138. payload := pkt[headerLen:]
  139. payLen := len(payload)
  140. gso := int(hdr.GSOSize)
  141. numSeg := (payLen + gso - 1) / gso
  142. if numSeg == 0 {
  143. numSeg = 1
  144. }
  145. need := numSeg*headerLen + payLen
  146. if need > len(scratch) {
  147. return fmt.Errorf("scratch too small for %d segments: need %d have %d", numSeg, need, len(scratch))
  148. }
  149. origSeq := binary.BigEndian.Uint32(pkt[csumStart+4 : csumStart+8])
  150. origFlags := pkt[csumStart+13]
  151. const tcpFinPsh = 0x09 // FIN(0x01) | PSH(0x08)
  152. // Precompute the TCP header sum with seq/flags/csum zeroed. The max TCP
  153. // header is 60 bytes; copy onto the stack, zero the per-segment-varying
  154. // fields, sum once.
  155. var tmp [60]byte
  156. copy(tmp[:tcpHdrLen], pkt[csumStart:headerLen])
  157. tmp[4], tmp[5], tmp[6], tmp[7] = 0, 0, 0, 0 // seq
  158. tmp[13] = 0 // flags
  159. tmp[16], tmp[17] = 0, 0 // csum
  160. baseTcpHdrSum := checksumBytes(tmp[:tcpHdrLen], 0)
  161. // Pseudo-header src+dst+proto contribution (tcpLen varies per segment).
  162. var baseProtoSum uint32
  163. if isV4 {
  164. baseProtoSum = checksumBytes(pkt[12:16], 0)
  165. baseProtoSum = checksumBytes(pkt[16:20], baseProtoSum)
  166. } else {
  167. baseProtoSum = checksumBytes(pkt[8:24], 0)
  168. baseProtoSum = checksumBytes(pkt[24:40], baseProtoSum)
  169. }
  170. baseProtoSum += uint32(unix.IPPROTO_TCP)
  171. // Precompute IPv4 header sum with total_len/id/csum zeroed.
  172. var origIPID uint16
  173. var ihl int
  174. var baseIPHdrSum uint32
  175. if isV4 {
  176. origIPID = binary.BigEndian.Uint16(pkt[4:6])
  177. ihl = int(pkt[0]&0x0f) * 4
  178. if ihl < 20 || ihl > csumStart {
  179. return fmt.Errorf("bad IPv4 IHL: %d", ihl)
  180. }
  181. var ipTmp [60]byte
  182. copy(ipTmp[:ihl], pkt[:ihl])
  183. ipTmp[2], ipTmp[3] = 0, 0 // total_len
  184. ipTmp[4], ipTmp[5] = 0, 0 // id
  185. ipTmp[10], ipTmp[11] = 0, 0 // checksum
  186. baseIPHdrSum = checksumBytes(ipTmp[:ihl], 0)
  187. }
  188. off := 0
  189. for i := 0; i < numSeg; i++ {
  190. segStart := i * gso
  191. segEnd := segStart + gso
  192. if segEnd > payLen {
  193. segEnd = payLen
  194. }
  195. segPayLen := segEnd - segStart
  196. copy(scratch[off:], pkt[:headerLen])
  197. copy(scratch[off+headerLen:], payload[segStart:segEnd])
  198. seg := scratch[off : off+headerLen+segPayLen]
  199. off += headerLen + segPayLen
  200. segSeq := origSeq + uint32(segStart)
  201. segFlags := origFlags
  202. if i != numSeg-1 {
  203. segFlags = origFlags &^ tcpFinPsh
  204. }
  205. totalLen := headerLen + segPayLen
  206. // Patch IP header and write the v4 header checksum from the precomputed base.
  207. if isV4 {
  208. segID := origIPID + uint16(i)
  209. binary.BigEndian.PutUint16(seg[2:4], uint16(totalLen))
  210. binary.BigEndian.PutUint16(seg[4:6], segID)
  211. ipSum := baseIPHdrSum + uint32(totalLen) + uint32(segID)
  212. binary.BigEndian.PutUint16(seg[10:12], checksumFold(ipSum))
  213. } else {
  214. // IPv6 payload length excludes the 40-byte fixed header but
  215. // includes any extension headers between [40:csumStart].
  216. binary.BigEndian.PutUint16(seg[4:6], uint16(headerLen-40+segPayLen))
  217. }
  218. // Patch TCP header.
  219. binary.BigEndian.PutUint32(seg[csumStart+4:csumStart+8], segSeq)
  220. seg[csumStart+13] = segFlags
  221. // (csum is written below; its prior contents in `seg` don't affect the
  222. // computation since we never sum over the segment's own header.)
  223. tcpLen := tcpHdrLen + segPayLen
  224. paySum := checksumBytes(payload[segStart:segEnd], 0)
  225. // Combine pre-folded uint32s into a wider accumulator, then fold. Using
  226. // uint64 guards against overflow when segSeq's high bits set.
  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+16:csumStart+18], checksumFold(uint32(wide)))
  232. *out = append(*out, seg)
  233. }
  234. return nil
  235. }
  236. // checksumBytes returns the Internet-checksum partial sum of b, seeded with
  237. // initial. Result is a 32-bit accumulator; the caller folds to 16.
  238. //
  239. // Each 4-byte load is added directly into a 64-bit accumulator. Two parallel
  240. // accumulators break the serial dependency through `sum` and let the CPU
  241. // overlap independent adds. The final fold from 64 → 32 → 16 handles the
  242. // carries that accumulated across the 32-bit lane boundary.
  243. func checksumBytes(b []byte, initial uint32) uint32 {
  244. s0 := uint64(initial)
  245. var s1 uint64
  246. for len(b) >= 32 {
  247. s0 += uint64(binary.BigEndian.Uint32(b[0:4]))
  248. s1 += uint64(binary.BigEndian.Uint32(b[4:8]))
  249. s0 += uint64(binary.BigEndian.Uint32(b[8:12]))
  250. s1 += uint64(binary.BigEndian.Uint32(b[12:16]))
  251. s0 += uint64(binary.BigEndian.Uint32(b[16:20]))
  252. s1 += uint64(binary.BigEndian.Uint32(b[20:24]))
  253. s0 += uint64(binary.BigEndian.Uint32(b[24:28]))
  254. s1 += uint64(binary.BigEndian.Uint32(b[28:32]))
  255. b = b[32:]
  256. }
  257. sum := s0 + s1
  258. for len(b) >= 4 {
  259. sum += uint64(binary.BigEndian.Uint32(b[:4]))
  260. b = b[4:]
  261. }
  262. if len(b) >= 2 {
  263. sum += uint64(binary.BigEndian.Uint16(b[:2]))
  264. b = b[2:]
  265. }
  266. if len(b) == 1 {
  267. sum += uint64(b[0]) << 8
  268. }
  269. sum = (sum & 0xffffffff) + (sum >> 32)
  270. sum = (sum & 0xffffffff) + (sum >> 32)
  271. return uint32(sum)
  272. }
  273. func checksumFold(sum uint32) uint16 {
  274. for sum>>16 != 0 {
  275. sum = (sum & 0xffff) + (sum >> 16)
  276. }
  277. return ^uint16(sum)
  278. }
  279. func pseudoHeaderIPv4(src, dst []byte, proto byte, tcpLen int) uint32 {
  280. sum := checksumBytes(src, 0)
  281. sum = checksumBytes(dst, sum)
  282. sum += uint32(proto)
  283. sum += uint32(tcpLen)
  284. return sum
  285. }
  286. func pseudoHeaderIPv6(src, dst []byte, proto byte, tcpLen int) uint32 {
  287. sum := checksumBytes(src, 0)
  288. sum = checksumBytes(dst, sum)
  289. sum += uint32(tcpLen >> 16)
  290. sum += uint32(tcpLen & 0xffff)
  291. sum += uint32(proto)
  292. return sum
  293. }