spicy offload chkpt

This commit is contained in:
JackDoan
2026-08-03 16:40:36 -05:00
parent 4cd433309b
commit 3b1004588d
13 changed files with 363 additions and 163 deletions
+8 -3
View File
@@ -1,5 +1,7 @@
package batch
import "github.com/slackhq/nebula/firewall"
// SortKey identifies a packet's position in its sender's transmission order.
// Epoch is a receiver-local ordinal for the tunnel (ConnectionState) that
// decrypted the packet. A re-handshake replaces the tunnel outright — new
@@ -15,9 +17,12 @@ type SortKey struct {
type RxBatcher interface {
// Commit stages pkt to be flushed by the batch. key must carry the
// packet's session epoch and message counter. The caller must keep pkt
// valid until the next Flush, and not re-use it.
Commit(pkt []byte, key SortKey) error
// packet's session epoch and message counter; pp must be the firewall's
// parse of this same packet. The caller must keep pkt valid until the
// next Flush, and not re-use it. pp, by contrast, is borrowed only for
// the duration of the call — the caller reuses one ParsedPacket per
// receive loop — so implementations must copy what they need from it.
Commit(pkt []byte, key SortKey, pp *firewall.ParsedPacket) error
// Flush emits every staged packet. Packets are first sorted by key, so
// within each protocol lane emission follows the sender's transmission
// order regardless of arrival order. One shape may legally be overtaken
+73 -29
View File
@@ -40,34 +40,19 @@ type parsedIP struct {
// On success, p.pkt is len-trimmed to the IP-declared length so callers
// don't have to repeat the trim. wantProto is the IANA protocol number to
// require (6 for TCP, 17 for UDP); ok=false for any other value.
// This is the standalone-lane-Commit entry; the dispatcher path uses
// parseIPAt, where the protocol was already resolved upstream.
func parseIPPrologue(pkt []byte, wantProto byte) (parsedIP, bool) {
var p parsedIP
if len(pkt) < 20 {
return p, false
}
v := pkt[0] >> 4
switch v {
switch pkt[0] >> 4 {
case 4:
ihl := int(pkt[0]&0x0f) * 4
if ihl != 20 {
return p, false
}
if pkt[9] != wantProto {
return p, false
}
// Reject actual fragmentation (MF or non-zero frag offset).
if binary.BigEndian.Uint16(pkt[6:8])&0x3fff != 0 {
return p, false
}
totalLen := int(binary.BigEndian.Uint16(pkt[2:4]))
if totalLen > len(pkt) || totalLen < ihl {
return p, false
}
p.ipHdrLen = 20
p.fk.isV6 = false
copy(p.fk.src[:4], pkt[12:16])
copy(p.fk.dst[:4], pkt[16:20])
p.pkt = pkt[:totalLen]
return parseIPv4Prologue(pkt)
case 6:
if len(pkt) < 40 {
return p, false
@@ -75,18 +60,77 @@ func parseIPPrologue(pkt []byte, wantProto byte) (parsedIP, bool) {
if pkt[6] != wantProto {
return p, false
}
payloadLen := int(binary.BigEndian.Uint16(pkt[4:6]))
if 40+payloadLen > len(pkt) {
return p, false
}
p.ipHdrLen = 40
p.fk.isV6 = true
copy(p.fk.src[:], pkt[8:24])
copy(p.fk.dst[:], pkt[24:40])
p.pkt = pkt[:40+payloadLen]
default:
return parseIPv6Prologue(pkt)
}
return p, false
}
// parseIPAt is the dispatcher-path prologue: newPacket already resolved the
// L4 protocol and header offset once for the firewall, so the proto sniff is
// replaced by a cross-check of the caller's ipHdrLen. A plain header (v4:
// IHL 20, v6: exactly 40 — no options, no extension headers) is the only
// coalesceable shape, which is the same rule parseIPPrologue enforces
// through its own reads.
func parseIPAt(pkt []byte, ipHdrLen int) (parsedIP, bool) {
var p parsedIP
if len(pkt) < 20 {
return p, false
}
switch pkt[0] >> 4 {
case 4:
if ipHdrLen != 20 {
return p, false
}
return parseIPv4Prologue(pkt)
case 6:
if ipHdrLen != 40 || len(pkt) < 40 {
return p, false
}
return parseIPv6Prologue(pkt)
}
return p, false
}
// parseIPv4Prologue is the shared IPv4 tail of the two prologue entries.
// The caller has verified len(pkt) >= 20 and either the protocol
// (parseIPPrologue) or the upstream-resolved header length (parseIPAt).
func parseIPv4Prologue(pkt []byte) (parsedIP, bool) {
var p parsedIP
ihl := int(pkt[0]&0x0f) * 4
if ihl != 20 {
return p, false
}
// Reject actual fragmentation (MF or non-zero frag offset). On the
// dispatcher path FragAny was already gated; kept as defense in depth —
// a fragment folded into a superpacket would corrupt reassembly.
if binary.BigEndian.Uint16(pkt[6:8])&0x3fff != 0 {
return p, false
}
totalLen := int(binary.BigEndian.Uint16(pkt[2:4]))
if totalLen > len(pkt) || totalLen < ihl {
return p, false
}
p.ipHdrLen = 20
p.fk.isV6 = false
copy(p.fk.src[:4], pkt[12:16])
copy(p.fk.dst[:4], pkt[16:20])
p.pkt = pkt[:totalLen]
return p, true
}
// parseIPv6Prologue is the shared IPv6 tail; caller has verified
// len(pkt) >= 40 and version/proto-or-offset.
func parseIPv6Prologue(pkt []byte) (parsedIP, bool) {
var p parsedIP
payloadLen := int(binary.BigEndian.Uint16(pkt[4:6]))
if 40+payloadLen > len(pkt) {
return p, false
}
p.ipHdrLen = 40
p.fk.isV6 = true
copy(p.fk.src[:], pkt[8:24])
copy(p.fk.dst[:], pkt[24:40])
p.pkt = pkt[:40+payloadLen]
return p, true
}
+46 -70
View File
@@ -6,7 +6,7 @@ import (
"log/slog"
"slices"
"github.com/slackhq/nebula/iputil"
"github.com/slackhq/nebula/firewall"
)
// MultiCoalescer stages plaintext packets with their (epoch, counter) sort
@@ -47,9 +47,15 @@ type MultiCoalescer struct {
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
pkt []byte
key SortKey
proto byte
fragAny bool
ipHdrLen uint16
}
// NewMultiCoalescer builds a multi-lane batcher over w, based on available
@@ -65,32 +71,18 @@ func NewMultiCoalescer(w io.Writer, l *slog.Logger) RxBatcher {
return m
}
// IANA protocol numbers for the IPv6 extension headers
// iputil.IPv6FindUpperProtocol can step over. The set here must match what
// that walker walks: it is the hot path's cheap pre-guard, so the walk is
// only paid when it can actually make progress.
const (
ipProtoHopByHop = 0
ipProtoRouting = 43
ipProtoFragment = 44
ipProtoAH = 51
ipProtoDestOpts = 60
)
// isIPv6ExtHeader reports whether nh is an extension header the terminal-
// protocol walk knows how to step over.
func isIPv6ExtHeader(nh byte) bool {
switch nh {
case ipProtoHopByHop, ipProtoRouting, ipProtoFragment, ipProtoAH, ipProtoDestOpts:
return true
}
return false
}
// Commit stages pkt for the next Flush. All parsing and lane dispatch is
// deferred to Flush so it runs on packets already in transmission order.
func (m *MultiCoalescer) Commit(pkt []byte, key SortKey) error {
m.staged = append(m.staged, stagedPacket{pkt: pkt, key: key})
// 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
}
@@ -114,59 +106,43 @@ func compareStaged(a, b stagedPacket) int {
return 1
}
// dispatch routes one packet to the appropriate lane based on IP version +
// L4 proto. On the success path the IP/TCP-or-UDP parse happens here once
// and the parsed struct is handed to the lane via commitParsed so the lane
// doesn't re-walk the header.
func (m *MultiCoalescer) dispatch(pkt []byte) error {
if len(pkt) < 20 {
return m.pt.enqueue(pkt)
}
v := pkt[0] >> 4
var proto byte
switch v {
case 4:
proto = pkt[9]
case 6:
if len(pkt) < 40 {
return m.pt.enqueue(pkt)
}
proto = pkt[6]
if isIPv6ExtHeader(proto) {
// Walk to the terminal protocol so the packet routes to its flow's protocol lane.
// This protects flow ordering.
proto, _, _ = iputil.IPv6FindUpperProtocol(pkt)
}
default:
return m.pt.enqueue(pkt)
}
switch proto {
// 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 {
info, ok := parseTCPBase(pkt)
if !ok {
// Unsupported TCP shape (IP options, fragments, ...). Its flow
// key is unknowable, so seal every open chain: dispatch runs in
// transmission order, and sealing is what keeps later data from
// extending a chain that would emit ahead of this packet.
if sp.fragAny {
m.tcp.sealAllOpen()
m.tcp.addVerbatim(pkt)
m.tcp.addVerbatim(sp.pkt)
return nil
}
return m.tcp.commitParsed(pkt, info)
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 {
info, ok := parseUDP(pkt)
if !ok {
if sp.fragAny {
m.udp.sealAllOpen()
m.udp.addVerbatim(pkt)
m.udp.addVerbatim(sp.pkt)
return nil
}
return m.udp.commitParsed(pkt, info)
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(pkt)
return m.pt.enqueue(sp.pkt)
}
// Flush sorts the staged batch into transmission order, replays it into the
@@ -178,7 +154,7 @@ func (m *MultiCoalescer) Flush() error {
var errs []error
for _, sp := range m.staged {
if err := m.dispatch(sp.pkt); err != nil {
if err := m.dispatch(sp); err != nil {
errs = append(errs, err)
}
}
+56 -29
View File
@@ -6,6 +6,7 @@ import (
"io"
"testing"
"github.com/slackhq/nebula/firewall"
"github.com/slackhq/nebula/test"
)
@@ -48,19 +49,19 @@ func TestMultiCoalescerRoutesByProto(t *testing.T) {
icmp[3] = 28
icmp[9] = 1
if err := m.Commit(buildTCPv4(1000, tcpAck, tcpPay), k.next()); err != nil {
if err := m.Commit(buildTCPv4(1000, tcpAck, tcpPay), k.next(), testPP(buildTCPv4(1000, tcpAck, tcpPay))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildTCPv4(2200, tcpAck, tcpPay), k.next()); err != nil {
if err := m.Commit(buildTCPv4(2200, tcpAck, tcpPay), k.next(), testPP(buildTCPv4(2200, tcpAck, tcpPay))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildUDPv4(2000, 53, udpPay), k.next()); err != nil {
if err := m.Commit(buildUDPv4(2000, 53, udpPay), k.next(), testPP(buildUDPv4(2000, 53, udpPay))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildUDPv4(2000, 53, udpPay), k.next()); err != nil {
if err := m.Commit(buildUDPv4(2000, 53, udpPay), k.next(), testPP(buildUDPv4(2000, 53, udpPay))); err != nil {
t.Fatal(err)
}
if err := m.Commit(icmp, k.next()); err != nil {
if err := m.Commit(icmp, k.next(), testPP(icmp)); err != nil {
t.Fatal(err)
}
if err := m.Flush(); err != nil {
@@ -89,13 +90,13 @@ func TestMultiCoalescerRestoresTransmissionOrder(t *testing.T) {
// Transmission order: seq 1000 (c1), 2200 (c2), 3400 (c3).
// Arrival order: 3400, 1000, 2200.
if err := m.Commit(buildTCPv4(3400, tcpAck, pay), SortKey{Epoch: 1, Counter: 3}); err != nil {
if err := m.Commit(buildTCPv4(3400, tcpAck, pay), SortKey{Epoch: 1, Counter: 3}, testPP(buildTCPv4(3400, tcpAck, pay))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildTCPv4(1000, tcpAck, pay), SortKey{Epoch: 1, Counter: 1}); err != nil {
if err := m.Commit(buildTCPv4(1000, tcpAck, pay), SortKey{Epoch: 1, Counter: 1}, testPP(buildTCPv4(1000, tcpAck, pay))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildTCPv4(2200, tcpAck, pay), SortKey{Epoch: 1, Counter: 2}); err != nil {
if err := m.Commit(buildTCPv4(2200, tcpAck, pay), SortKey{Epoch: 1, Counter: 2}, testPP(buildTCPv4(2200, tcpAck, pay))); err != nil {
t.Fatal(err)
}
if err := m.Flush(); err != nil {
@@ -115,10 +116,10 @@ func TestMultiCoalescerRestoresTransmissionOrder(t *testing.T) {
// Retransmit: seq 1000 again but counter 4 — sorts after seq 4600 (c3).
w.writes, w.gsoWrites, w.order = nil, nil, nil
if err := m.Commit(buildTCPv4(1000, tcpAck, pay), SortKey{Epoch: 1, Counter: 4}); err != nil {
if err := m.Commit(buildTCPv4(1000, tcpAck, pay), SortKey{Epoch: 1, Counter: 4}, testPP(buildTCPv4(1000, tcpAck, pay))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildTCPv4(4600, tcpAck, pay), SortKey{Epoch: 1, Counter: 3}); err != nil {
if err := m.Commit(buildTCPv4(4600, tcpAck, pay), SortKey{Epoch: 1, Counter: 3}, testPP(buildTCPv4(4600, tcpAck, pay))); err != nil {
t.Fatal(err)
}
if err := m.Flush(); err != nil {
@@ -144,16 +145,16 @@ func TestMultiCoalescerRestoresOrderAcrossFlows(t *testing.T) {
// Transmission: A.100 (c1), B.500 (c2), A.1300 (c3), B.1700 (c4).
// Arrival: A.1300, B.1700, A.100, B.500.
if err := m.Commit(buildTCPv4Ports(1000, 2000, 1300, tcpAck, pay), SortKey{Epoch: 1, Counter: 3}); err != nil {
if err := m.Commit(buildTCPv4Ports(1000, 2000, 1300, tcpAck, pay), SortKey{Epoch: 1, Counter: 3}, testPP(buildTCPv4Ports(1000, 2000, 1300, tcpAck, pay))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildTCPv4Ports(3000, 2000, 1700, tcpAck, pay), SortKey{Epoch: 1, Counter: 4}); err != nil {
if err := m.Commit(buildTCPv4Ports(3000, 2000, 1700, tcpAck, pay), SortKey{Epoch: 1, Counter: 4}, testPP(buildTCPv4Ports(3000, 2000, 1700, tcpAck, pay))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay), SortKey{Epoch: 1, Counter: 1}); err != nil {
if err := m.Commit(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay), SortKey{Epoch: 1, Counter: 1}, testPP(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay), SortKey{Epoch: 1, Counter: 2}); err != nil {
if err := m.Commit(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay), SortKey{Epoch: 1, Counter: 2}, testPP(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay))); err != nil {
t.Fatal(err)
}
if err := m.Flush(); err != nil {
@@ -195,10 +196,10 @@ func TestMultiCoalescerEpochOrdersAcrossRehandshake(t *testing.T) {
pay := make([]byte, 1200)
// New session's first data arrives before the old session's last data.
if err := m.Commit(buildTCPv4(2200, tcpAck, pay), SortKey{Epoch: 8, Counter: 1}); err != nil {
if err := m.Commit(buildTCPv4(2200, tcpAck, pay), SortKey{Epoch: 8, Counter: 1}, testPP(buildTCPv4(2200, tcpAck, pay))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildTCPv4(1000, tcpAck, pay), SortKey{Epoch: 7, Counter: 9_000_000}); err != nil {
if err := m.Commit(buildTCPv4(1000, tcpAck, pay), SortKey{Epoch: 7, Counter: 9_000_000}, testPP(buildTCPv4(1000, tcpAck, pay))); err != nil {
t.Fatal(err)
}
if err := m.Flush(); err != nil {
@@ -227,10 +228,10 @@ func TestMultiCoalescerNoUSOFallsThrough(t *testing.T) {
t.Fatal("UDP lane must not come up without USO")
}
if err := m.Commit(buildUDPv4(1000, 53, make([]byte, 800)), k.next()); err != nil {
if err := m.Commit(buildUDPv4(1000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv4(1000, 53, make([]byte, 800)))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildUDPv4(1000, 53, make([]byte, 800)), k.next()); err != nil {
if err := m.Commit(buildUDPv4(1000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv4(1000, 53, make([]byte, 800)))); err != nil {
t.Fatal(err)
}
if err := m.Flush(); err != nil {
@@ -261,7 +262,7 @@ func TestMultiCoalescerNoOffloadsStillSorts(t *testing.T) {
}
// Committed in reverse transmission order; keys carry the truth.
for i := len(pkts) - 1; i >= 0; i-- {
if err := m.Commit(pkts[i], SortKey{Epoch: 1, Counter: uint64(i + 1)}); err != nil {
if err := m.Commit(pkts[i], SortKey{Epoch: 1, Counter: uint64(i + 1)}, testPP(pkts[i])); err != nil {
t.Fatal(err)
}
}
@@ -325,13 +326,13 @@ func TestMultiCoalescerIPv6FragmentStaysInLane(t *testing.T) {
m := newTestMultiCoalescer(t, w)
k := &keySeq{epoch: 1}
if err := m.Commit(buildUDPv6Fragment(2000, 53, make([]byte, 512)), k.next()); err != nil {
if err := m.Commit(buildUDPv6Fragment(2000, 53, make([]byte, 512)), k.next(), testPP(buildUDPv6Fragment(2000, 53, make([]byte, 512)))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next()); err != nil {
if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next()); err != nil {
if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
t.Fatal(err)
}
if err := m.Flush(); err != nil {
@@ -358,19 +359,19 @@ func TestMultiCoalescerFragmentSealsUDPChains(t *testing.T) {
m := newTestMultiCoalescer(t, w)
k := &keySeq{epoch: 1}
if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next()); err != nil {
if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next()); err != nil {
if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildUDPv6Fragment(2000, 53, make([]byte, 512)), k.next()); err != nil {
if err := m.Commit(buildUDPv6Fragment(2000, 53, make([]byte, 512)), k.next(), testPP(buildUDPv6Fragment(2000, 53, make([]byte, 512)))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next()); err != nil {
if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next()); err != nil {
if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
t.Fatal(err)
}
if err := m.Flush(); err != nil {
@@ -398,10 +399,10 @@ func TestMultiCoalescerNoTSOFallsThrough(t *testing.T) {
}
pay := make([]byte, 1200)
if err := m.Commit(buildTCPv4(1000, tcpAck, pay), k.next()); err != nil {
if err := m.Commit(buildTCPv4(1000, tcpAck, pay), k.next(), testPP(buildTCPv4(1000, tcpAck, pay))); err != nil {
t.Fatal(err)
}
if err := m.Commit(buildTCPv4(2200, tcpAck, pay), k.next()); err != nil {
if err := m.Commit(buildTCPv4(2200, tcpAck, pay), k.next(), testPP(buildTCPv4(2200, tcpAck, pay))); err != nil {
t.Fatal(err)
}
if err := m.Flush(); err != nil {
@@ -414,3 +415,29 @@ func TestMultiCoalescerNoTSOFallsThrough(t *testing.T) {
t.Errorf("TCP must pass through as 2 plain writes, got %d", len(w.writes))
}
}
// testPP derives the ParsedPacket newPacket would produce for the packet
// shapes the tests build: plain v4/v6, v4 with options or fragment bits set,
// and the single-fragment-header v6 shape from buildUDPv6Fragment. Anything
// unrecognizable stays zero (proto 0 routes to the passthrough lane).
func testPP(pkt []byte) *firewall.ParsedPacket {
pp := &firewall.ParsedPacket{}
if len(pkt) < 20 {
return pp
}
switch pkt[0] >> 4 {
case 4:
pp.Protocol = pkt[9]
pp.IPHdrLen = int(pkt[0]&0x0f) * 4
pp.FragAny = binary.BigEndian.Uint16(pkt[6:8])&0x3fff != 0
case 6:
pp.Protocol = pkt[6]
pp.IPHdrLen = 40
if pp.Protocol == 44 { // fragment extension header
pp.Protocol = pkt[40]
pp.IPHdrLen = 48
pp.FragAny = true
}
}
return pp
}
+3 -3
View File
@@ -2,6 +2,8 @@ package batch
import (
"io"
"github.com/slackhq/nebula/firewall"
)
// Passthrough is a RxBatcher that doesn't batch anything, it just accumulates and then sends packets.
@@ -17,9 +19,7 @@ func NewPassthrough(w io.Writer) *Passthrough {
}
}
// Commit ignores the sort key: a bare Passthrough (no MultiCoalescer in
// front) emits in arrival order, exactly as before keys existed.
func (p *Passthrough) Commit(pkt []byte, _ SortKey) error {
func (p *Passthrough) Commit(pkt []byte, _ SortKey, _ *firewall.ParsedPacket) error {
return p.enqueue(pkt)
}
+18 -3
View File
@@ -117,12 +117,27 @@ type parsedTCP struct {
// 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) {
var p parsedTCP
ip, ok := parseIPPrologue(pkt, ipProtoTCP)
if !ok {
return p, false
return parsedTCP{}, false
}
pkt = ip.pkt
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
+10 -3
View File
@@ -4,6 +4,7 @@ import (
"encoding/binary"
"testing"
"github.com/slackhq/nebula/firewall"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/test"
)
@@ -169,16 +170,22 @@ func BenchmarkCommitNonCoalesceableTCP(b *testing.B) {
// runMultiCommitBench drives MultiCoalescer.Commit with in-order keys, so
// it includes the staging sort's already-sorted fast path plus the
// dispatch-time parse — the full steady-state cost of the batcher.
// dispatch-time parse — the full steady-state cost of the batcher. The
// ParsedPackets are precomputed: in production they fall out of the
// firewall's newPacket, which this bench does not model.
func runMultiCommitBench(b *testing.B, pkts [][]byte, batchSize int) {
b.Helper()
m := NewMultiCoalescer(nopTunWriter{}, test.NewLogger())
pps := make([]*firewall.ParsedPacket, len(pkts))
for i, p := range pkts {
pps[i] = testPP(p)
}
b.ReportAllocs()
b.SetBytes(int64(len(pkts[0])))
b.ResetTimer()
for i := 0; i < b.N; i++ {
pkt := pkts[i%len(pkts)]
if err := m.Commit(pkt, SortKey{Epoch: 1, Counter: uint64(i + 1)}); err != nil {
j := i % len(pkts)
if err := m.Commit(pkts[j], SortKey{Epoch: 1, Counter: uint64(i + 1)}, pps[j]); err != nil {
b.Fatal(err)
}
if (i+1)%batchSize == 0 {
+18 -3
View File
@@ -87,12 +87,27 @@ type parsedUDP struct {
// Returns ok=false for non-UDP, malformed, or unsupported header shapes
// (IPv4 with options/fragmentation, IPv6 with extension headers).
func parseUDP(pkt []byte) (parsedUDP, bool) {
var p parsedUDP
ip, ok := parseIPPrologue(pkt, ipProtoUDP)
if !ok {
return p, false
return parsedUDP{}, false
}
pkt = ip.pkt
return parseUDPTail(ip)
}
// parseUDPAt is parseUDP for the dispatcher path: the packet is already
// known to be UDP and ipHdrLen is the upstream-resolved L4 offset (see parseIPAt).
func parseUDPAt(pkt []byte, ipHdrLen int) (parsedUDP, bool) {
ip, ok := parseIPAt(pkt, ipHdrLen)
if !ok {
return parsedUDP{}, false
}
return parseUDPTail(ip)
}
// parseUDPTail layers the UDP-header parse on a validated IP prologue.
func parseUDPTail(ip parsedIP) (parsedUDP, bool) {
var p parsedUDP
pkt := ip.pkt
p.fk = ip.fk
p.ipHdrLen = ip.ipHdrLen