tun_linux_offload.go 10 KB

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