mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 08:36:57 +02:00
179 lines
5.5 KiB
Go
179 lines
5.5 KiB
Go
package batch
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"slices"
|
|
|
|
"github.com/slackhq/nebula/firewall"
|
|
)
|
|
|
|
// MultiCoalescer stages plaintext packets with their (epoch, counter) sort
|
|
// keys, and at Flush replays them in sender-transmission order into
|
|
// lane-specific batchers selected by the IP/L4 protocol of the packet.
|
|
//
|
|
// Sorting *before* the lanes see anything is what makes the ordering story
|
|
// simple: each lane consumes packets in transmission order, builds its slots
|
|
// in that order, and emits them in creation order. Wire reorder inside a
|
|
// flush batch is repaired here, before it can fragment a lane's coalesce
|
|
// chains, so the lanes carry no reorder-repair machinery of their own.
|
|
//
|
|
// The ordering contract is per-tunnel transmission order within each lane:
|
|
// a sender's packets are emitted in the order it encrypted them. Two
|
|
// qualifications:
|
|
// - a pure TCP ACK may be overtaken by later same-flow data, because it
|
|
// does not close the flow's open coalesce chain (a late ACK is just a
|
|
// stale ACK; see TCPCoalescer.commitParsed);
|
|
// - an unparseable shape (fragment, IP options) seals every open chain in
|
|
// its lane — its flow is unknowable, so this is the only way to keep
|
|
// later data from extending a chain that would emit ahead of it. The
|
|
// packet then rides its lane as an in-lane passthrough, still in
|
|
// transmission order.
|
|
//
|
|
// Routing follows the flow, not the coalesceability: IPv4 fragments keep
|
|
// their L4 proto visible and IPv6 extension chains are walked to the
|
|
// terminal proto, so a flow's non-coalesceable shapes ride its lane rather
|
|
// than falling to the later-flushed pt lane.
|
|
//
|
|
// Cross-lane order is intentionally NOT preserved across the TCP/UDP/verbatim split.
|
|
type MultiCoalescer struct {
|
|
tcp *TCPCoalescer
|
|
udp *UDPCoalescer
|
|
pt *Passthrough
|
|
|
|
// staged holds this batch's packets and sort keys until Flush. Borrowed:
|
|
// the caller keeps each pkt alive until Flush returns.
|
|
staged []stagedPacket
|
|
}
|
|
|
|
// stagedPacket also carries the scalars dispatch needs from the firewall's
|
|
// ParsedPacket: pp itself is reused by the caller per packet and must not be
|
|
// retained past Commit, so the relevant fields are copied by value here.
|
|
type stagedPacket struct {
|
|
pkt []byte
|
|
key SortKey
|
|
proto byte
|
|
fragAny bool
|
|
ipHdrLen uint16
|
|
}
|
|
|
|
// NewMultiCoalescer builds a multi-lane batcher over w, based on available
|
|
// protocol support. The staging sort applies even when no GSO lane is
|
|
// available: passthrough-only platforms still get transmission-order repair.
|
|
func NewMultiCoalescer(w io.Writer, l *slog.Logger) RxBatcher {
|
|
m := &MultiCoalescer{
|
|
pt: NewPassthrough(w),
|
|
staged: make([]stagedPacket, 0, initialSlots),
|
|
}
|
|
m.tcp = NewTCPCoalescer(w, l)
|
|
m.udp = NewUDPCoalescer(w)
|
|
return m
|
|
}
|
|
|
|
// Commit stages pkt for the next Flush. All lane dispatch is deferred to
|
|
// Flush so it runs on packets already in transmission order. pp is the
|
|
// firewall's parse of pkt — the single source of truth for the packet's
|
|
// protocol and L4 offset — and is only borrowed for this call.
|
|
func (m *MultiCoalescer) Commit(pkt []byte, key SortKey, pp *firewall.ParsedPacket) error {
|
|
m.staged = append(m.staged, stagedPacket{
|
|
pkt: pkt,
|
|
key: key,
|
|
proto: pp.Protocol,
|
|
fragAny: pp.FragAny,
|
|
ipHdrLen: uint16(pp.IPHdrLen),
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// compareStaged orders staged packets by (epoch, counter): sender
|
|
// transmission order within a tunnel, tunnel-creation order across a
|
|
// re-handshake cutover. Keys are unique (see SortKey), so this is a total
|
|
// order and sort stability doesn't matter.
|
|
func compareStaged(a, b stagedPacket) int {
|
|
if a.key.Epoch != b.key.Epoch {
|
|
if a.key.Epoch < b.key.Epoch {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|
|
if a.key.Counter == b.key.Counter {
|
|
return 0
|
|
}
|
|
if a.key.Counter < b.key.Counter {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|
|
|
|
// dispatch routes one staged packet to its lane.
|
|
// The protocol and L4 offset come from the firewall's parse of the same packet.
|
|
// Any shape a lane can't coalesce seals every open chain in its lane
|
|
func (m *MultiCoalescer) dispatch(sp stagedPacket) error {
|
|
switch sp.proto {
|
|
case ipProtoTCP:
|
|
if m.tcp != nil {
|
|
if sp.fragAny {
|
|
m.tcp.sealAllOpen()
|
|
m.tcp.addVerbatim(sp.pkt)
|
|
return nil
|
|
}
|
|
info, ok := parseTCPAt(sp.pkt, int(sp.ipHdrLen))
|
|
if !ok {
|
|
m.tcp.sealAllOpen()
|
|
m.tcp.addVerbatim(sp.pkt)
|
|
return nil
|
|
}
|
|
return m.tcp.commitParsed(sp.pkt, info)
|
|
}
|
|
case ipProtoUDP:
|
|
if m.udp != nil {
|
|
if sp.fragAny {
|
|
m.udp.sealAllOpen()
|
|
m.udp.addVerbatim(sp.pkt)
|
|
return nil
|
|
}
|
|
info, ok := parseUDPAt(sp.pkt, int(sp.ipHdrLen))
|
|
if !ok {
|
|
m.udp.sealAllOpen()
|
|
m.udp.addVerbatim(sp.pkt)
|
|
return nil
|
|
}
|
|
return m.udp.commitParsed(sp.pkt, info)
|
|
}
|
|
}
|
|
return m.pt.enqueue(sp.pkt)
|
|
}
|
|
|
|
// Flush sorts the staged batch into transmission order, replays it into the
|
|
// lanes, then flushes each lane.
|
|
func (m *MultiCoalescer) Flush() error {
|
|
// Arrival order is already almost sorted (reorder is the exception, not
|
|
// the rule), which pdqsort detects and handles in near-linear time.
|
|
slices.SortFunc(m.staged, compareStaged)
|
|
|
|
var errs []error
|
|
for _, sp := range m.staged {
|
|
if err := m.dispatch(sp); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
clear(m.staged) // drop borrowed pkt refs
|
|
m.staged = m.staged[:0]
|
|
|
|
if m.tcp != nil {
|
|
if err := m.tcp.Flush(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
if m.udp != nil {
|
|
if err := m.udp.Flush(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
if err := m.pt.Flush(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|