mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 08:36:57 +02:00
609 lines
20 KiB
Go
609 lines
20 KiB
Go
package batch
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"io"
|
|
"log/slog"
|
|
"net/netip"
|
|
|
|
"github.com/slackhq/nebula/overlay/tio"
|
|
)
|
|
|
|
// ipProtoTCP is the IANA protocol number for TCP. Defined here to help Windows out.
|
|
const ipProtoTCP = 6
|
|
|
|
// tcpCoalesceBufSize caps total bytes per superpacket. Mirrors the kernel's
|
|
// sk_gso_max_size of ~64KiB; anything beyond this would be rejected anyway.
|
|
const tcpCoalesceBufSize = 65535
|
|
|
|
// tcpCoalesceMaxSegs caps how many segments we'll coalesce into a single
|
|
// superpacket. Keeping this well below the kernel's TSO ceiling bounds latency.
|
|
const tcpCoalesceMaxSegs = 64
|
|
|
|
// tcpCoalesceHdrCap is the scratch space we copy a seed's IP+TCP header
|
|
// into. IPv6 (40) + TCP with full options (60) = 100 bytes.
|
|
const tcpCoalesceHdrCap = 100
|
|
|
|
// coalesceSlot is one entry in the coalescer's ordered event queue.
|
|
// When verbatim is true the slot holds a single borrowed packet that is
|
|
// emitted as-is (pure ACK, non-admissible TCP, unparseable, or oversize seed).
|
|
// When verbatim is false the slot is an in-progress coalesced superpacket.
|
|
// hdrBuf is a mutable copy of the seed's IP+TCP header, populated on the
|
|
// first append (we patch total length and pseudo-header partial at flush;
|
|
// a slot that never grows flushes from rawPkt and never touches hdrBuf)
|
|
// payIovs are *borrowed* slices from the caller's plaintext buffers.
|
|
// The caller (listenOut) must keep those buffers alive until Flush.
|
|
type coalesceSlot struct {
|
|
verbatim bool
|
|
// rawPkt is borrowed: the whole packet for verbatim slots, the seed
|
|
// packet for coalesce slots. A coalesce slot that never grows past one
|
|
// segment is emitted from rawPkt so its original (already valid) L4
|
|
// checksum ships DATA_VALID instead of making the kernel recompute it.
|
|
rawPkt []byte
|
|
|
|
fk flowKey
|
|
hdrBuf [tcpCoalesceHdrCap]byte
|
|
hdrLen int
|
|
ipHdrLen int
|
|
isV6 bool
|
|
gsoSize int
|
|
numSeg int
|
|
totalPay int
|
|
nextSeq uint32
|
|
payIovs [][]byte
|
|
}
|
|
|
|
// TCPCoalescer accumulates adjacent in-flow TCP data segments across multiple concurrent flows
|
|
// and emits each flow's run as a single TSO superpacket via tio.GSOWriter.
|
|
// It expects its input in sender-transmission order (MultiCoalescer sorts the
|
|
// staged batch by (epoch, counter) before dispatching here) and emits slots in
|
|
// creation order, which therefore reproduces transmission order — modulo the
|
|
// pure-ACK allowance in commitParsed.
|
|
// Owns no locks; one coalescer per TUN write queue.
|
|
type TCPCoalescer struct {
|
|
w tio.GSOWriter
|
|
|
|
// slots is the ordered event queue. Flush walks it once and emits each
|
|
// entry as either a WriteGSO (coalesced) or a w.Write (verbatim).
|
|
slots []*coalesceSlot
|
|
// openSlots maps a flow key to its still-open slot, so new segments can
|
|
// extend an in-progress superpacket in O(1). Membership here is what
|
|
// keeps a chain extendable: slots are removed when they close (PSH or
|
|
// short-last-segment), when a non-admissible packet for that flow
|
|
// arrives, or in Flush.
|
|
openSlots map[flowKey]*coalesceSlot
|
|
// lastSlot caches the most recently touched open slot. Bulk traffic
|
|
// arrives in same-flow runs (single-flow steady state, or GRO bursts
|
|
// under multi-flow), so comparing the incoming key against the cached
|
|
// slot's own fk lets the hot path skip the map lookup (and the aeshash
|
|
// of a 38-byte key) for the length of each run.
|
|
// Kept in lockstep with openSlots: nil whenever the slot it pointed
|
|
// at is removed.
|
|
lastSlot *coalesceSlot
|
|
pool []*coalesceSlot // free list for reuse
|
|
l *slog.Logger
|
|
}
|
|
|
|
// NewTCPCoalescer wraps w, returning nil if w can't accept GSO_TCP writes.
|
|
func NewTCPCoalescer(w io.Writer, l *slog.Logger) *TCPCoalescer {
|
|
gw, ok := tio.SupportsGSO(w, tio.GSOProtoTCP)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return &TCPCoalescer{
|
|
w: gw,
|
|
slots: make([]*coalesceSlot, 0, initialSlots),
|
|
openSlots: make(map[flowKey]*coalesceSlot, initialSlots),
|
|
pool: make([]*coalesceSlot, 0, initialSlots),
|
|
l: l,
|
|
}
|
|
}
|
|
|
|
// parsedTCP holds the fields extracted from a single parse so later steps
|
|
// (admission, slot lookup, canAppend) don't re-walk the header.
|
|
type parsedTCP struct {
|
|
fk flowKey
|
|
ipHdrLen int
|
|
tcpHdrLen int
|
|
hdrLen int
|
|
payLen int
|
|
seq uint32
|
|
flags byte
|
|
}
|
|
|
|
// parseTCPBase extracts the flow key and IP/TCP offsets for any TCP packet,
|
|
// regardless of whether it's admissible for coalescing. Returns ok=false for non-TCP or malformed input.
|
|
// Accepts IPv4 (no options or fragmentation) and IPv6 (no extension headers).
|
|
func parseTCPBase(pkt []byte) (parsedTCP, bool) {
|
|
ip, ok := parseIPPrologue(pkt, ipProtoTCP)
|
|
if !ok {
|
|
return parsedTCP{}, false
|
|
}
|
|
return parseTCPTail(ip)
|
|
}
|
|
|
|
// parseTCPAt is parseTCPBase for the dispatcher path: the packet is already
|
|
// known to be TCP and ipHdrLen is the upstream-resolved L4 offset (see parseIPAt).
|
|
func parseTCPAt(pkt []byte, ipHdrLen int) (parsedTCP, bool) {
|
|
ip, ok := parseIPAt(pkt, ipHdrLen)
|
|
if !ok {
|
|
return parsedTCP{}, false
|
|
}
|
|
return parseTCPTail(ip)
|
|
}
|
|
|
|
// parseTCPTail layers the TCP-header parse on a validated IP prologue.
|
|
func parseTCPTail(ip parsedIP) (parsedTCP, bool) {
|
|
var p parsedTCP
|
|
pkt := ip.pkt
|
|
p.fk = ip.fk
|
|
p.ipHdrLen = ip.ipHdrLen
|
|
|
|
if len(pkt) < p.ipHdrLen+20 {
|
|
return p, false
|
|
}
|
|
tcpOff := int(pkt[p.ipHdrLen+12]>>4) * 4
|
|
if tcpOff < 20 || tcpOff > 60 {
|
|
return p, false
|
|
}
|
|
if len(pkt) < p.ipHdrLen+tcpOff {
|
|
return p, false
|
|
}
|
|
p.tcpHdrLen = tcpOff
|
|
p.hdrLen = p.ipHdrLen + tcpOff
|
|
p.payLen = len(pkt) - p.hdrLen
|
|
p.fk.sport = binary.BigEndian.Uint16(pkt[p.ipHdrLen : p.ipHdrLen+2])
|
|
p.fk.dport = binary.BigEndian.Uint16(pkt[p.ipHdrLen+2 : p.ipHdrLen+4])
|
|
p.seq = binary.BigEndian.Uint32(pkt[p.ipHdrLen+4 : p.ipHdrLen+8])
|
|
p.flags = pkt[p.ipHdrLen+13]
|
|
return p, true
|
|
}
|
|
|
|
// TCP flag bits (byte 13 of the TCP header). Only the bits actually consulted
|
|
// by the coalescer are named; FIN/SYN/RST/URG/CWR are rejected via the
|
|
// negative mask in coalesceable, not by name.
|
|
const (
|
|
tcpFlagPsh = 0x08
|
|
tcpFlagAck = 0x10
|
|
tcpFlagEce = 0x40
|
|
)
|
|
|
|
// coalesceable reports whether a parsed TCP segment is eligible for
|
|
// coalescing. Accepts ACK, ACK|PSH, ACK|ECE, ACK|PSH|ECE with a
|
|
// non-empty payload. CWR is excluded because it marks a one-shot
|
|
// congestion-window-reduced transition the receiver must observe at a
|
|
// segment boundary.
|
|
func (p parsedTCP) coalesceable() bool {
|
|
if p.flags&tcpFlagAck == 0 {
|
|
return false
|
|
}
|
|
if p.flags&^(tcpFlagAck|tcpFlagPsh|tcpFlagEce) != 0 {
|
|
return false
|
|
}
|
|
return p.payLen > 0
|
|
}
|
|
|
|
// pureAck reports whether a parsed segment is a bare acknowledgment: no
|
|
// payload and nothing beyond ACK|PSH|ECE in the flags. These are the only
|
|
// non-coalesceable shape that may safely pass through WITHOUT sealing the
|
|
// flow's open slot — a late-delivered stale ACK is ignored by the receiver,
|
|
// whereas SYN/FIN/RST/CWR mark transitions the flow must observe in order.
|
|
func (p parsedTCP) pureAck() bool {
|
|
return p.payLen == 0 &&
|
|
p.flags&tcpFlagAck != 0 &&
|
|
p.flags&^(tcpFlagAck|tcpFlagPsh|tcpFlagEce) == 0
|
|
}
|
|
|
|
func (c *TCPCoalescer) Commit(pkt []byte) error {
|
|
info, ok := parseTCPBase(pkt)
|
|
if !ok {
|
|
// Unparseable shape: flow key unknowable, so seal every open chain to
|
|
// keep later data from extending a chain that would emit ahead of it.
|
|
c.sealAllOpen()
|
|
c.addVerbatim(pkt)
|
|
return nil
|
|
}
|
|
return c.commitParsed(pkt, info)
|
|
}
|
|
|
|
// sealAllOpen closes every open coalesce chain: nothing committed after this
|
|
// call can extend a slot created before it. Called when an unparseable packet
|
|
// arrives — its flow is unknown, so any open chain might be the one whose
|
|
// later data would otherwise leapfrog it.
|
|
func (c *TCPCoalescer) sealAllOpen() {
|
|
clear(c.openSlots)
|
|
c.lastSlot = nil
|
|
}
|
|
|
|
// commitParsed is the post-parse half of Commit. The caller must have
|
|
// already verified parseTCPBase succeeded (info is a valid TCP parse).
|
|
// Used by MultiCoalescer.Commit to avoid re-walking the IP/TCP header
|
|
// after the dispatcher has already done so.
|
|
func (c *TCPCoalescer) commitParsed(pkt []byte, info parsedTCP) error {
|
|
if !info.coalesceable() {
|
|
if info.pureAck() {
|
|
// A bare window/ack update carries no ordering obligation toward
|
|
// the flow's data: delivering it after later-transmitted data only
|
|
// makes it a stale ACK, which receivers ignore. Skipping the
|
|
// evict keeps a bidirectional flow's inbound data run coalescing
|
|
// across the peer ACKs interleaved into it — kernel GRO likewise
|
|
// doesn't flush held data on a pure ACK. This is the one place
|
|
// emission can deviate from transmission order.
|
|
c.addVerbatim(pkt)
|
|
return nil
|
|
}
|
|
// TCP but not admissible (SYN/FIN/RST/URG/CWR or a shape the flow
|
|
// must observe in sequence). Seal this flow's open slot so later
|
|
// in-flow packets don't extend it and emit ahead of this verbatim;
|
|
// with input in transmission order that pins the verbatim's exact
|
|
// in-flow position. The len guard skips hashing the 38-byte key on
|
|
// ack-dominant queues, where the map is almost always empty.
|
|
if len(c.openSlots) != 0 {
|
|
if last := c.lastSlot; last != nil && last.fk == info.fk {
|
|
c.lastSlot = nil
|
|
}
|
|
delete(c.openSlots, info.fk)
|
|
}
|
|
c.addVerbatim(pkt)
|
|
return nil
|
|
}
|
|
|
|
// Cached-slot fast path. Arrival isn't per-packet interleaved even with
|
|
// many flows: wire-side GRO delivers runs of same-flow packets
|
|
// (deliverSegments splits a superdatagram into up to 64), so the cache
|
|
// hits for the length of each run and a miss costs one fk compare
|
|
// before the map lookup carries the weight.
|
|
var open *coalesceSlot
|
|
if last := c.lastSlot; last != nil && last.fk == info.fk {
|
|
open = last
|
|
} else {
|
|
open = c.openSlots[info.fk]
|
|
}
|
|
if open != nil {
|
|
if c.canAppend(open, pkt, info) {
|
|
if c.appendPayload(open, pkt, info) {
|
|
// Chain closed (PSH or short segment): stop extending it.
|
|
delete(c.openSlots, info.fk)
|
|
c.lastSlot = nil
|
|
} else {
|
|
c.lastSlot = open
|
|
}
|
|
return nil
|
|
}
|
|
// Can't extend (seq gap from upstream loss, header change, or a full
|
|
// chain): evict it from openSlots and fall through to seed a fresh slot.
|
|
delete(c.openSlots, info.fk)
|
|
if c.lastSlot == open {
|
|
c.lastSlot = nil
|
|
}
|
|
}
|
|
c.seed(pkt, info)
|
|
return nil
|
|
}
|
|
|
|
func (c *TCPCoalescer) Flush() error {
|
|
if c.l.Enabled(context.Background(), slog.LevelDebug) {
|
|
c.logSeqGaps()
|
|
}
|
|
var first error
|
|
for _, s := range c.slots {
|
|
var err error
|
|
if s.verbatim || s.numSeg == 1 {
|
|
// A slot that never grew (nor absorbed a merge) is byte-identical
|
|
// to the packet it was seeded from; ship the original so its valid
|
|
// checksum rides the DATA_VALID path instead of paying a kernel
|
|
// software csum. appendPayload only touches hdrBuf once
|
|
// numSeg >= 2, so rawPkt is still pristine here.
|
|
_, err = c.w.Write(s.rawPkt)
|
|
} else {
|
|
err = c.flushSlot(s)
|
|
}
|
|
if err != nil && first == nil {
|
|
first = err
|
|
}
|
|
c.release(s)
|
|
}
|
|
clear(c.slots)
|
|
c.slots = c.slots[:0]
|
|
clear(c.openSlots)
|
|
c.lastSlot = nil
|
|
|
|
return first
|
|
}
|
|
|
|
func (c *TCPCoalescer) addVerbatim(pkt []byte) {
|
|
s := c.take()
|
|
s.verbatim = true
|
|
s.rawPkt = pkt
|
|
c.slots = append(c.slots, s)
|
|
}
|
|
|
|
func (c *TCPCoalescer) seed(pkt []byte, info parsedTCP) {
|
|
if info.hdrLen > tcpCoalesceHdrCap || info.hdrLen+info.payLen > tcpCoalesceBufSize {
|
|
// Pathological shape. Can't fit our scratch, emit as-is.
|
|
c.addVerbatim(pkt)
|
|
return
|
|
}
|
|
s := c.take()
|
|
s.verbatim = false
|
|
// rawPkt serves the numSeg==1 fast path in Flush and is the header
|
|
// source for canAppend until the first append copies it into hdrBuf.
|
|
s.rawPkt = pkt
|
|
s.hdrLen = info.hdrLen
|
|
s.ipHdrLen = info.ipHdrLen
|
|
s.isV6 = info.fk.isV6
|
|
s.fk = info.fk
|
|
s.gsoSize = info.payLen
|
|
s.numSeg = 1
|
|
s.totalPay = info.payLen
|
|
s.nextSeq = info.seq + uint32(info.payLen)
|
|
s.payIovs = append(s.payIovs[:0], pkt[info.hdrLen:info.hdrLen+info.payLen])
|
|
c.slots = append(c.slots, s)
|
|
if info.flags&tcpFlagPsh == 0 {
|
|
c.openSlots[info.fk] = s
|
|
c.lastSlot = s
|
|
} else if last := c.lastSlot; last != nil && last.fk == info.fk {
|
|
// PSH-on-seed closes the chain immediately: never registered as
|
|
// open. Any prior cached open slot for this flow has just been
|
|
// closed-and-replaced by this seed, so drop the cache too.
|
|
c.lastSlot = nil
|
|
}
|
|
}
|
|
|
|
// canAppend reports whether info's packet extends the slot's seed: same
|
|
// header shape and stable contents, adjacent seq, not oversized. A closed
|
|
// chain never reaches here — closing removes the slot from openSlots, and
|
|
// openSlots/lastSlot are the only paths in.
|
|
// Header reads go through rawPkt, not hdrBuf: hdrBuf is populated lazily on
|
|
// the first append, and every field consulted here is one the pre-flush
|
|
// patches never touch (headersMatch skips the flags byte, and PSH is the
|
|
// only bit patched before flush).
|
|
func (c *TCPCoalescer) canAppend(s *coalesceSlot, pkt []byte, info parsedTCP) bool {
|
|
if info.hdrLen != s.hdrLen {
|
|
return false
|
|
}
|
|
if info.seq != s.nextSeq {
|
|
return false
|
|
}
|
|
if s.numSeg >= tcpCoalesceMaxSegs {
|
|
return false
|
|
}
|
|
if info.payLen > s.gsoSize {
|
|
return false
|
|
}
|
|
if s.hdrLen+s.totalPay+info.payLen > tcpCoalesceBufSize {
|
|
return false
|
|
}
|
|
// ECE state must be stable across a burst.
|
|
// Receivers expect the flag set on every segment of a CE-echoing window or none.
|
|
seedFlags := s.rawPkt[s.ipHdrLen+13]
|
|
if (seedFlags^info.flags)&tcpFlagEce != 0 {
|
|
return false
|
|
}
|
|
if !s.isV6 && !ipv4CanCoalesceID(s.rawPkt, pkt, s.numSeg) {
|
|
return false
|
|
}
|
|
if !headersMatch(s.rawPkt[:s.hdrLen], pkt[:info.hdrLen], s.isV6, s.ipHdrLen) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// appendPayload folds info's packet into s and reports whether the chain is
|
|
// now closed: the segment was sub-gsoSize (kernel TSO allows only the final
|
|
// segment to be short) or carried PSH (a semantic delimiter). The caller
|
|
// must deregister a closed slot from openSlots.
|
|
func (c *TCPCoalescer) appendPayload(s *coalesceSlot, pkt []byte, info parsedTCP) bool {
|
|
if s.numSeg == 1 {
|
|
// First append: populate hdrBuf from the seed packet. Deferred out
|
|
// of seed so solo slots, which flush from rawPkt, never pay the copy.
|
|
copy(s.hdrBuf[:s.hdrLen], s.rawPkt[:s.hdrLen])
|
|
}
|
|
s.payIovs = append(s.payIovs, pkt[info.hdrLen:info.hdrLen+info.payLen])
|
|
s.numSeg++
|
|
s.totalPay += info.payLen
|
|
s.nextSeq = info.seq + uint32(info.payLen)
|
|
if info.flags&tcpFlagPsh != 0 {
|
|
// Propagate PSH into the seed header so kernel TSO sets it on the
|
|
// last segment. Without this the sender's push signal is dropped.
|
|
s.hdrBuf[s.ipHdrLen+13] |= tcpFlagPsh
|
|
}
|
|
return info.payLen < s.gsoSize || info.flags&tcpFlagPsh != 0
|
|
}
|
|
|
|
func (c *TCPCoalescer) take() *coalesceSlot {
|
|
if n := len(c.pool); n > 0 {
|
|
s := c.pool[n-1]
|
|
c.pool[n-1] = nil
|
|
c.pool = c.pool[:n-1]
|
|
return s
|
|
}
|
|
return &coalesceSlot{}
|
|
}
|
|
|
|
func (c *TCPCoalescer) release(s *coalesceSlot) {
|
|
s.verbatim = false
|
|
s.rawPkt = nil
|
|
clear(s.payIovs)
|
|
s.payIovs = s.payIovs[:0]
|
|
s.numSeg = 0
|
|
s.totalPay = 0
|
|
// Zero the identity fields too: addVerbatim doesn't set them, so a
|
|
// pooled slot reused as a verbatim must not carry a stale flow key
|
|
// that a future refactor could mistake for real.
|
|
s.fk = flowKey{}
|
|
s.hdrLen = 0
|
|
s.ipHdrLen = 0
|
|
s.isV6 = false
|
|
s.gsoSize = 0
|
|
s.nextSeq = 0
|
|
c.pool = append(c.pool, s)
|
|
}
|
|
|
|
// flushSlot patches the header and calls WriteGSO. Does not remove the slot from c.slots.
|
|
func (c *TCPCoalescer) flushSlot(s *coalesceSlot) error {
|
|
total := s.hdrLen + s.totalPay
|
|
l4Len := total - s.ipHdrLen
|
|
hdr := s.hdrBuf[:s.hdrLen]
|
|
|
|
if s.isV6 {
|
|
binary.BigEndian.PutUint16(hdr[4:6], uint16(l4Len))
|
|
} else {
|
|
binary.BigEndian.PutUint16(hdr[2:4], uint16(total))
|
|
hdr[10] = 0
|
|
hdr[11] = 0
|
|
binary.BigEndian.PutUint16(hdr[10:12], ipv4HdrChecksum(hdr[:s.ipHdrLen]))
|
|
}
|
|
|
|
var psum uint32
|
|
if s.isV6 {
|
|
psum = pseudoSumIPv6(hdr[8:24], hdr[24:40], ipProtoTCP, l4Len)
|
|
} else {
|
|
psum = pseudoSumIPv4(hdr[12:16], hdr[16:20], ipProtoTCP, l4Len)
|
|
}
|
|
tcsum := s.ipHdrLen + 16
|
|
binary.BigEndian.PutUint16(hdr[tcsum:tcsum+2], foldOnceNoInvert(psum))
|
|
|
|
return c.w.WriteGSO(hdr[:s.ipHdrLen], hdr[s.ipHdrLen:], s.payIovs, tio.GSOProtoTCP)
|
|
}
|
|
|
|
// headersMatch compares two IP+TCP header prefixes for byte-for-byte
|
|
// equality on every field that must be identical across coalesced
|
|
// segments. Size/IPID/IPCsum/seq/flags/tcpCsum are masked out.
|
|
func headersMatch(a, b []byte, isV6 bool, ipHdrLen int) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
if !ipHeadersMatch(a, b, isV6) {
|
|
return false
|
|
}
|
|
// TCP: compare [0:4] ports, [8:13] ack+dataoff, [14:16] window,
|
|
// [18:tcpHdrLen] options (incl. urgent).
|
|
tcp := ipHdrLen
|
|
if !bytes.Equal(a[tcp:tcp+4], b[tcp:tcp+4]) {
|
|
return false
|
|
}
|
|
if !bytes.Equal(a[tcp+8:tcp+13], b[tcp+8:tcp+13]) {
|
|
return false
|
|
}
|
|
if !bytes.Equal(a[tcp+14:tcp+16], b[tcp+14:tcp+16]) {
|
|
return false
|
|
}
|
|
if !bytes.Equal(a[tcp+18:], b[tcp+18:]) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// logSeqGaps reports same-flow seq discontinuities between consecutively
|
|
// created data slots. Input arrives in transmission order (MultiCoalescer
|
|
// sorts by (epoch, counter) before dispatch), so a gap here is traffic this
|
|
// batch never contained: loss upstream of nebula, a reorder spanning a flush
|
|
// boundary (which no intra-batch mechanism can repair), or a retransmit
|
|
// (negative gap). Logged so the operator can quantify how often that happens.
|
|
// The caller gates on debug level, so the map only allocates when asked for.
|
|
func (c *TCPCoalescer) logSeqGaps() {
|
|
prevByFlow := make(map[flowKey]*coalesceSlot, len(c.slots))
|
|
for _, s := range c.slots {
|
|
if s.verbatim {
|
|
continue
|
|
}
|
|
if prev, ok := prevByFlow[s.fk]; ok && prev.nextSeq != slotSeedSeq(s) {
|
|
gap := int64(slotSeedSeq(s)) - int64(prev.nextSeq)
|
|
c.l.Debug("tcp coalesce: cross-slot seq gap",
|
|
"src", flowKeyAddr(s.fk, false),
|
|
"dst", flowKeyAddr(s.fk, true),
|
|
"sport", s.fk.sport,
|
|
"dport", s.fk.dport,
|
|
"prev_seed_seq", slotSeedSeq(prev),
|
|
"prev_next_seq", prev.nextSeq,
|
|
"this_seed_seq", slotSeedSeq(s),
|
|
"gap_bytes", gap,
|
|
"prev_seg_count", prev.numSeg,
|
|
"prev_total_pay", prev.totalPay,
|
|
)
|
|
}
|
|
prevByFlow[s.fk] = s
|
|
}
|
|
}
|
|
|
|
// flowKeyAddr returns the src or dst address from fk as a netip.Addr for
|
|
// logging. Only used on the cold gap-log path so the netip allocation
|
|
// doesn't matter.
|
|
func flowKeyAddr(fk flowKey, dst bool) netip.Addr {
|
|
src := fk.src
|
|
if dst {
|
|
src = fk.dst
|
|
}
|
|
if fk.isV6 {
|
|
return netip.AddrFrom16(src)
|
|
}
|
|
var v4 [4]byte
|
|
copy(v4[:], src[:4])
|
|
return netip.AddrFrom4(v4)
|
|
}
|
|
|
|
// slotSeedSeq returns the TCP seq of the slot's seed (first segment).
|
|
// nextSeq tracks the seq just past the last appended byte; subtracting
|
|
// totalPay walks back to the seed. uint32 wraparound is the right TCP
|
|
// arithmetic so no special-casing is needed.
|
|
func slotSeedSeq(s *coalesceSlot) uint32 {
|
|
return s.nextSeq - uint32(s.totalPay)
|
|
}
|
|
|
|
// ipv4HdrChecksum computes the IPv4 header checksum over hdr (which must
|
|
// already have its checksum field zeroed) and returns the folded/inverted
|
|
// 16-bit value to store.
|
|
func ipv4HdrChecksum(hdr []byte) uint16 {
|
|
var sum uint32
|
|
for i := 0; i+1 < len(hdr); i += 2 {
|
|
sum += uint32(binary.BigEndian.Uint16(hdr[i : i+2]))
|
|
}
|
|
if len(hdr)%2 == 1 {
|
|
sum += uint32(hdr[len(hdr)-1]) << 8
|
|
}
|
|
for sum>>16 != 0 {
|
|
sum = (sum & 0xffff) + (sum >> 16)
|
|
}
|
|
return ^uint16(sum)
|
|
}
|
|
|
|
// pseudoSumIPv4 / pseudoSumIPv6 build the L4 pseudo-header partial sum
|
|
// expected by the virtio NEEDS_CSUM kernel path: the 32-bit accumulator
|
|
// before folding. proto selects the L4 (TCP or UDP); the UDP coalescer
|
|
// reuses these helpers.
|
|
func pseudoSumIPv4(src, dst []byte, proto byte, l4Len int) uint32 {
|
|
var sum uint32
|
|
sum += uint32(binary.BigEndian.Uint16(src[0:2]))
|
|
sum += uint32(binary.BigEndian.Uint16(src[2:4]))
|
|
sum += uint32(binary.BigEndian.Uint16(dst[0:2]))
|
|
sum += uint32(binary.BigEndian.Uint16(dst[2:4]))
|
|
sum += uint32(proto)
|
|
sum += uint32(l4Len)
|
|
return sum
|
|
}
|
|
|
|
func pseudoSumIPv6(src, dst []byte, proto byte, l4Len int) uint32 {
|
|
var sum uint32
|
|
for i := 0; i < 16; i += 2 {
|
|
sum += uint32(binary.BigEndian.Uint16(src[i : i+2]))
|
|
sum += uint32(binary.BigEndian.Uint16(dst[i : i+2]))
|
|
}
|
|
sum += uint32(l4Len >> 16)
|
|
sum += uint32(l4Len & 0xffff)
|
|
sum += uint32(proto)
|
|
return sum
|
|
}
|
|
|
|
// foldOnceNoInvert folds the 32-bit accumulator to 16 bits and returns it
|
|
// unchanged (no one's complement). This is what virtio NEEDS_CSUM wants in
|
|
// the L4 checksum field — the kernel will add the payload sum and invert.
|
|
func foldOnceNoInvert(sum uint32) uint16 {
|
|
for sum>>16 != 0 {
|
|
sum = (sum & 0xffff) + (sum >> 16)
|
|
}
|
|
return uint16(sum)
|
|
}
|