coalesce_core.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. package batch
  2. import (
  3. "bytes"
  4. "encoding/binary"
  5. )
  6. // SortKey identifies a packet's position in its sender's transmission order.
  7. type SortKey struct {
  8. // Epoch is a receiver-local ordinal for the tunnel (ConnectionState) that decrypted the packet:
  9. // a re-handshake replaces the tunnel outright and the replacement's epoch is higher,
  10. // so the old tunnel's packets sort first during the cutover overlap.
  11. Epoch uint64
  12. // Counter is the packet's AEAD message counter within that tunnel.
  13. Counter uint64
  14. }
  15. // flowKey identifies a transport flow by {src, dst, sport, dport, family}.
  16. // Comparable, so map lookups and linear scans over the slot list stay tight.
  17. // Shared by the TCP and UDP coalescers; each coalescer keeps its own
  18. // openSlots map, so a TCP and UDP flow on the same 5-tuple-without-proto never alias.
  19. type flowKey struct {
  20. src, dst [16]byte
  21. sport, dport uint16
  22. isV6 bool
  23. }
  24. // initialSlots is the starting capacity of the slot pool.
  25. // One flow per packet is the worst case, so this matches a typical carrier-side recvmmsg batch on the UDP socket.
  26. const initialSlots = 64
  27. // parseIPAt validates the IP header for lane parsing. newPacket already resolved the L4 protocol
  28. // and offset for the firewall, so there is no proto sniff here; the caller's ipHdrLen is
  29. // cross-checked instead. A plain header (v4 IHL 20, v6 exactly 40) is the only coalesceable
  30. // shape. The v6 check is load-bearing: it rejects extension-header packets whose L4 is not at byte 40.
  31. //
  32. // The prologues fill fk's addresses and family in place (ports belong to the L4 parser; fk must
  33. // be zero on entry so the v4 path leaves src[4:]/dst[4:] clear for map equality) and return pkt
  34. // trimmed to the IP-declared length. The receiver-as-out-pointer shape is deliberate: these
  35. // functions are too big to inline, and returning structs by value put five 64-byte copies on the
  36. // per-packet path.
  37. func (fk *flowKey) parseIPAt(pkt []byte, ipHdrLen int) ([]byte, bool) {
  38. if len(pkt) < 20 {
  39. return nil, false
  40. }
  41. switch pkt[0] >> 4 {
  42. case 4:
  43. if ipHdrLen != 20 {
  44. return nil, false
  45. }
  46. return fk.parseIPv4Prologue(pkt)
  47. case 6:
  48. if ipHdrLen != 40 || len(pkt) < 40 {
  49. return nil, false
  50. }
  51. return fk.parseIPv6Prologue(pkt)
  52. }
  53. return nil, false
  54. }
  55. // parseIPv4Prologue is the shared IPv4 tail of the prologue entries; the caller has verified
  56. // len(pkt) >= 20 and the version.
  57. func (fk *flowKey) parseIPv4Prologue(pkt []byte) ([]byte, bool) {
  58. ihl := int(pkt[0]&0x0f) * 4
  59. if ihl != 20 {
  60. return nil, false
  61. }
  62. // Reject any fragmentation (MF or nonzero offset). The dispatcher already gated FragAny; kept
  63. // as defense in depth, since a fragment folded into a superpacket would corrupt reassembly.
  64. if binary.BigEndian.Uint16(pkt[6:8])&0x3fff != 0 {
  65. return nil, false
  66. }
  67. totalLen := int(binary.BigEndian.Uint16(pkt[2:4]))
  68. if totalLen > len(pkt) || totalLen < ihl {
  69. return nil, false
  70. }
  71. fk.isV6 = false
  72. copy(fk.src[:4], pkt[12:16])
  73. copy(fk.dst[:4], pkt[16:20])
  74. return pkt[:totalLen], true
  75. }
  76. // parseIPv6Prologue is the shared IPv6 tail; the caller has verified len(pkt) >= 40, the version,
  77. // and that the L4 header sits at byte 40.
  78. func (fk *flowKey) parseIPv6Prologue(pkt []byte) ([]byte, bool) {
  79. payloadLen := int(binary.BigEndian.Uint16(pkt[4:6]))
  80. if 40+payloadLen > len(pkt) {
  81. return nil, false
  82. }
  83. fk.isV6 = true
  84. copy(fk.src[:], pkt[8:24])
  85. copy(fk.dst[:], pkt[24:40])
  86. return pkt[:40+payloadLen], true
  87. }
  88. // ipHeadersMatch compares the IP portion of two packet header prefixes for
  89. // byte-for-byte equality on every field that must be identical across coalesced segments.
  90. // Size/IPID/IPCsum are masked out.
  91. // The full DSCP/ECN byte (IPv4 ToS / IPv6 traffic class) is compared, matching Linux kernel GRO:
  92. // segments with differing ECN codepoints must not coalesce,
  93. // otherwise ORing e.g. ECT(0) with ECT(1) would fabricate a false CE (congestion) mark or mark a Not-ECT flow as ECN-capable.
  94. //
  95. // The transport (L4) portion of the header is checked separately by the per-protocol matcher.
  96. func ipHeadersMatch(a, b []byte, isV6 bool) bool {
  97. if isV6 {
  98. // IPv6: [0:4] = version/TC/flow label (TC[1:0] is ECN, so the full TC byte must match),
  99. // [6:40] = next_hdr/hop + src + dst. Skip [4:6] payload_len.
  100. return bytes.Equal(a[:4], b[:4]) && bytes.Equal(a[6:40], b[6:40])
  101. }
  102. // IPv4: [0:2] = version/IHL + DSCP|ECN (full ECN byte must match),
  103. // [6:10] = flags/fragoff/TTL/proto, [12:20] = src+dst.
  104. // Skip [2:4] total len, [4:6] id, [10:12] csum.
  105. return bytes.Equal(a[:2], b[:2]) && bytes.Equal(a[6:10], b[6:10]) && bytes.Equal(a[12:20], b[12:20])
  106. }
  107. // ipv4FlagDF is the Don't Fragment bit in the IPv4 flags byte (header byte 6).
  108. const ipv4FlagDF = 0x40
  109. // ipv4CanCoalesceID reports whether an IPv4 packet whose header starts at
  110. // nextHdr may join a chain whose seed header is seedHdr as segment index seg
  111. // (the seed is segment 0). Kernel GSO re-stamps outgoing segment IDs as
  112. // seed_id+n, so coalescing is only transparent when that re-stamp is either
  113. // harmless (DF set: RFC 6864 atomic datagrams, the ID carries no meaning) or
  114. // reproduces the original IDs exactly (DF clear + IDs already sequential —
  115. // the same admission rule kernel GRO applies). Without this, a DF=0 sender
  116. // with non-sequential IDs (e.g. OpenBSD's randomized IDs) could have IDs
  117. // rewritten into ranges that collide across superpackets, corrupting
  118. // reassembly if the packets are fragmented after the TUN write.
  119. //
  120. // DF itself is guaranteed uniform across a chain by ipHeadersMatch (byte 6
  121. // is inside its compared range), so checking the seed's copy suffices.
  122. func ipv4CanCoalesceID(seedHdr, nextHdr []byte, seg int) bool {
  123. if seedHdr[6]&ipv4FlagDF != 0 {
  124. return true
  125. }
  126. expect := binary.BigEndian.Uint16(seedHdr[4:6]) + uint16(seg)
  127. return binary.BigEndian.Uint16(nextHdr[4:6]) == expect
  128. }
  129. // Arena is an injectable byte-slab that hands out non-overlapping borrowed
  130. // slices via Reserve and releases them in bulk via Reset.
  131. type Arena struct {
  132. buf []byte
  133. }
  134. // NewArena returns an Arena with a pre-allocated backing of the given capacity.
  135. func NewArena(capacity int) *Arena {
  136. return &Arena{buf: make([]byte, 0, capacity)}
  137. }
  138. // Reserve hands out a non-overlapping sz-byte slice from the arena.
  139. // If the request doesn't fit the current backing, a fresh, larger backing is allocated.
  140. // Already-borrowed slices reference the old backing and remain valid until Reset.
  141. func (a *Arena) Reserve(sz int) []byte {
  142. if len(a.buf)+sz > cap(a.buf) {
  143. newCap := max(cap(a.buf)*2, sz)
  144. a.buf = make([]byte, 0, newCap)
  145. }
  146. start := len(a.buf)
  147. a.buf = a.buf[:start+sz]
  148. return a.buf[start : start+sz : start+sz]
  149. }
  150. // Reset releases every slice handed out since the last Reset.
  151. // Callers must not use any previously-borrowed slice after this returns.
  152. // The underlying backing array is retained so subsequent Reserves don't re-allocate.
  153. func (a *Arena) Reset() {
  154. a.buf = a.buf[:0]
  155. }