mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 08:36:57 +02:00
spicy offload chkpt
This commit is contained in:
@@ -65,3 +65,12 @@ func (fp Packet) MarshalJSON() ([]byte, error) {
|
|||||||
"Fragment": fp.Fragment,
|
"Fragment": fp.Fragment,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParsedPacket is a Packet plus the parse byproducts the RX path reuses
|
||||||
|
type ParsedPacket struct {
|
||||||
|
Packet
|
||||||
|
IPHdrLen int
|
||||||
|
// FragAny reports any fragmentation at all: MF flag or nonzero offset for IPv4, a fragment extension header for IPv6.
|
||||||
|
// Distinct from Packet.Fragment, which is true only for NON-FIRST fragments
|
||||||
|
FragAny bool
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (f *Interface) consumeInsidePacket(pkt tio.Packet, fwPacket *firewall.Packet, nb []byte, sendBatch *batch.SendBatch, rejectBuf []byte, q int, localCache firewall.ConntrackCache) {
|
func (f *Interface) consumeInsidePacket(pkt tio.Packet, fwPacket *firewall.ParsedPacket, nb []byte, sendBatch *batch.SendBatch, rejectBuf []byte, q int, localCache firewall.ConntrackCache) {
|
||||||
// borrowed: pkt.Bytes is owned by the originating tio.Queue and is
|
// borrowed: pkt.Bytes is owned by the originating tio.Queue and is
|
||||||
// only valid until the next Read on that queue. Every consumer below
|
// only valid until the next Read on that queue. Every consumer below
|
||||||
// (parse, self-forward, handshake cache, sendInsideMessage) reads it
|
// (parse, self-forward, handshake cache, sendInsideMessage) reads it
|
||||||
@@ -74,7 +74,7 @@ func (f *Interface) consumeInsidePacket(pkt tio.Packet, fwPacket *firewall.Packe
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hostinfo, ready := f.getOrHandshakeConsiderRouting(fwPacket, func(hh *HandshakeHostInfo) {
|
hostinfo, ready := f.getOrHandshakeConsiderRouting(&fwPacket.Packet, func(hh *HandshakeHostInfo) {
|
||||||
// borrowed: SegmentSuperpacket builds each segment in the kernel-supplied pkt
|
// borrowed: SegmentSuperpacket builds each segment in the kernel-supplied pkt
|
||||||
// bytes underneath. cachePacket explicitly copies its argument (handshake_manager.go cachePacket),
|
// bytes underneath. cachePacket explicitly copies its argument (handshake_manager.go cachePacket),
|
||||||
// so retaining segments past the loop is safe.
|
// so retaining segments past the loop is safe.
|
||||||
@@ -105,7 +105,7 @@ func (f *Interface) consumeInsidePacket(pkt tio.Packet, fwPacket *firewall.Packe
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dropReason := f.firewall.Drop(*fwPacket, false, hostinfo, f.pki.GetCAPool(), localCache)
|
dropReason := f.firewall.Drop(fwPacket.Packet, false, hostinfo, f.pki.GetCAPool(), localCache)
|
||||||
if dropReason == nil {
|
if dropReason == nil {
|
||||||
f.sendInsideMessage(hostinfo, pkt, nb, sendBatch)
|
f.sendInsideMessage(hostinfo, pkt, nb, sendBatch)
|
||||||
} else {
|
} else {
|
||||||
@@ -371,7 +371,7 @@ func (f *Interface) getOrHandshakeConsiderRouting(fwPacket *firewall.Packet, cac
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Interface) sendMessageNow(t header.MessageType, st header.MessageSubType, hostinfo *HostInfo, p, nb, out []byte) {
|
func (f *Interface) sendMessageNow(t header.MessageType, st header.MessageSubType, hostinfo *HostInfo, p, nb, out []byte) {
|
||||||
fp := &firewall.Packet{}
|
fp := &firewall.ParsedPacket{}
|
||||||
err := newPacket(p, false, fp)
|
err := newPacket(p, false, fp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.l.Warn("error while parsing outgoing packet for firewall check", "error", err)
|
f.l.Warn("error while parsing outgoing packet for firewall check", "error", err)
|
||||||
@@ -379,7 +379,7 @@ func (f *Interface) sendMessageNow(t header.MessageType, st header.MessageSubTyp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if packet is in outbound fw rules
|
// check if packet is in outbound fw rules
|
||||||
dropReason := f.firewall.Drop(*fp, false, hostinfo, f.pki.GetCAPool(), nil)
|
dropReason := f.firewall.Drop(fp.Packet, false, hostinfo, f.pki.GetCAPool(), nil)
|
||||||
if dropReason != nil {
|
if dropReason != nil {
|
||||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||||
f.l.Debug("dropping cached packet",
|
f.l.Debug("dropping cached packet",
|
||||||
|
|||||||
+2
-2
@@ -360,7 +360,7 @@ func (f *Interface) listenOut(i int) {
|
|||||||
ctCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout)
|
ctCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout)
|
||||||
lhh := f.lightHouse.NewRequestHandler()
|
lhh := f.lightHouse.NewRequestHandler()
|
||||||
h := &header.H{}
|
h := &header.H{}
|
||||||
fwPacket := &firewall.Packet{}
|
fwPacket := &firewall.ParsedPacket{}
|
||||||
nb := make([]byte, 12, 12)
|
nb := make([]byte, 12, 12)
|
||||||
scratch := make([]byte, mtu)
|
scratch := make([]byte, mtu)
|
||||||
|
|
||||||
@@ -416,7 +416,7 @@ func (f *Interface) listenIn(queue tio.Queue, i int) {
|
|||||||
rejectBuf := make([]byte, mtu)
|
rejectBuf := make([]byte, mtu)
|
||||||
arenaSize := batch.SendBatchCap * (udp.MTU + 32)
|
arenaSize := batch.SendBatchCap * (udp.MTU + 32)
|
||||||
sb := batch.NewSendBatch(f.writers[i], batch.SendBatchCap, arenaSize)
|
sb := batch.NewSendBatch(f.writers[i], batch.SendBatchCap, arenaSize)
|
||||||
fwPacket := &firewall.Packet{}
|
fwPacket := &firewall.ParsedPacket{}
|
||||||
nb := make([]byte, 12, 12)
|
nb := make([]byte, 12, 12)
|
||||||
|
|
||||||
conntrackCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout)
|
conntrackCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout)
|
||||||
|
|||||||
+25
-8
@@ -26,7 +26,7 @@ var ErrOutOfWindow = errors.New("out of window packet")
|
|||||||
// readOutsidePackets processes one received underlay packet.
|
// readOutsidePackets processes one received underlay packet.
|
||||||
// Message payloads are decrypted IN PLACE, so packet must stay untouched
|
// Message payloads are decrypted IN PLACE, so packet must stay untouched
|
||||||
// by the caller until the batcher for queue q has been flushed
|
// by the caller until the batcher for queue q has been flushed
|
||||||
func (f *Interface) readOutsidePackets(via ViaSender, scratch []byte, packet []byte, h *header.H, fwPacket *firewall.Packet, lhf *LightHouseHandler, nb []byte, q int, localCache firewall.ConntrackCache) {
|
func (f *Interface) readOutsidePackets(via ViaSender, scratch []byte, packet []byte, h *header.H, fwPacket *firewall.ParsedPacket, lhf *LightHouseHandler, nb []byte, q int, localCache firewall.ConntrackCache) {
|
||||||
err := h.Parse(packet)
|
err := h.Parse(packet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Hole punch packets are 0 or 1 byte big, so lets ignore printing those errors
|
// Hole punch packets are 0 or 1 byte big, so lets ignore printing those errors
|
||||||
@@ -186,7 +186,7 @@ func (f *Interface) readOutsidePackets(via ViaSender, scratch []byte, packet []b
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Interface) handleOutsideRelayPacket(hostinfo *HostInfo, via ViaSender, scratch []byte, packet []byte, h *header.H, fwPacket *firewall.Packet, lhf *LightHouseHandler, nb []byte, q int, localCache firewall.ConntrackCache) {
|
func (f *Interface) handleOutsideRelayPacket(hostinfo *HostInfo, via ViaSender, scratch []byte, packet []byte, h *header.H, fwPacket *firewall.ParsedPacket, lhf *LightHouseHandler, nb []byte, q int, localCache firewall.ConntrackCache) {
|
||||||
// Successfully validated the thing. Get rid of the Relay header and the AEAD tag
|
// Successfully validated the thing. Get rid of the Relay header and the AEAD tag
|
||||||
signedPayload := packet[header.Len : len(packet)-hostinfo.ConnectionState.dKey.Overhead()]
|
signedPayload := packet[header.Len : len(packet)-hostinfo.ConnectionState.dKey.Overhead()]
|
||||||
// Pull the Roaming parts up here, and return in all call paths.
|
// Pull the Roaming parts up here, and return in all call paths.
|
||||||
@@ -315,7 +315,11 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// newPacket validates and parses the interesting bits for the firewall out of the ip and sub protocol headers
|
// newPacket validates and parses the interesting bits for the firewall out of the ip and sub protocol headers
|
||||||
func newPacket(data []byte, incoming bool, fp *firewall.Packet) error {
|
func newPacket(data []byte, incoming bool, fp *firewall.ParsedPacket) error {
|
||||||
|
// fp is reused across packets; reset the parse byproducts here so a
|
||||||
|
// parser's early-error return can't leak the previous packet's offsets.
|
||||||
|
fp.IPHdrLen = 0
|
||||||
|
fp.FragAny = false
|
||||||
if len(data) < 1 {
|
if len(data) < 1 {
|
||||||
return ErrPacketTooShort
|
return ErrPacketTooShort
|
||||||
}
|
}
|
||||||
@@ -330,7 +334,7 @@ func newPacket(data []byte, incoming bool, fp *firewall.Packet) error {
|
|||||||
return ErrUnknownIPVersion
|
return ErrUnknownIPVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseV6(data []byte, incoming bool, fp *firewall.Packet) error {
|
func parseV6(data []byte, incoming bool, fp *firewall.ParsedPacket) error {
|
||||||
dataLen := len(data)
|
dataLen := len(data)
|
||||||
if dataLen < ipv6.HeaderLen {
|
if dataLen < ipv6.HeaderLen {
|
||||||
return ErrIPv6PacketTooShort
|
return ErrIPv6PacketTooShort
|
||||||
@@ -356,6 +360,7 @@ func parseV6(data []byte, incoming bool, fp *firewall.Packet) error {
|
|||||||
switch proto {
|
switch proto {
|
||||||
case layers.IPProtocolESP, layers.IPProtocolNoNextHeader:
|
case layers.IPProtocolESP, layers.IPProtocolNoNextHeader:
|
||||||
fp.Protocol = uint8(proto)
|
fp.Protocol = uint8(proto)
|
||||||
|
fp.IPHdrLen = offset
|
||||||
fp.RemotePort = 0
|
fp.RemotePort = 0
|
||||||
fp.LocalPort = 0
|
fp.LocalPort = 0
|
||||||
fp.Fragment = false
|
fp.Fragment = false
|
||||||
@@ -366,6 +371,7 @@ func parseV6(data []byte, incoming bool, fp *firewall.Packet) error {
|
|||||||
return ErrIPv6PacketTooShort
|
return ErrIPv6PacketTooShort
|
||||||
}
|
}
|
||||||
fp.Protocol = uint8(proto)
|
fp.Protocol = uint8(proto)
|
||||||
|
fp.IPHdrLen = offset
|
||||||
fp.LocalPort = 0 //incoming vs outgoing doesn't matter for icmpv6
|
fp.LocalPort = 0 //incoming vs outgoing doesn't matter for icmpv6
|
||||||
icmptype := data[offset+1]
|
icmptype := data[offset+1]
|
||||||
switch icmptype {
|
switch icmptype {
|
||||||
@@ -383,6 +389,9 @@ func parseV6(data []byte, incoming bool, fp *firewall.Packet) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fp.Protocol = uint8(proto)
|
fp.Protocol = uint8(proto)
|
||||||
|
// offset is the L4 header start: 40 for a plain packet, past the
|
||||||
|
// extension chain otherwise. The coalescer only accepts 40.
|
||||||
|
fp.IPHdrLen = offset
|
||||||
if incoming {
|
if incoming {
|
||||||
fp.RemotePort = binary.BigEndian.Uint16(data[offset : offset+2])
|
fp.RemotePort = binary.BigEndian.Uint16(data[offset : offset+2])
|
||||||
fp.LocalPort = binary.BigEndian.Uint16(data[offset+2 : offset+4])
|
fp.LocalPort = binary.BigEndian.Uint16(data[offset+2 : offset+4])
|
||||||
@@ -400,6 +409,10 @@ func parseV6(data []byte, incoming bool, fp *firewall.Packet) error {
|
|||||||
return ErrIPv6PacketTooShort
|
return ErrIPv6PacketTooShort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Either way this packet is a fragment shape the coalescer must
|
||||||
|
// not touch, first fragment included.
|
||||||
|
fp.FragAny = true
|
||||||
|
|
||||||
// Check if this is the first fragment
|
// Check if this is the first fragment
|
||||||
fragmentOffset := binary.BigEndian.Uint16(data[offset+2:offset+4]) &^ uint16(0x7) // Remove the reserved and M flag bits
|
fragmentOffset := binary.BigEndian.Uint16(data[offset+2:offset+4]) &^ uint16(0x7) // Remove the reserved and M flag bits
|
||||||
if fragmentOffset != 0 {
|
if fragmentOffset != 0 {
|
||||||
@@ -441,7 +454,7 @@ func parseV6(data []byte, incoming bool, fp *firewall.Packet) error {
|
|||||||
return ErrIPv6CouldNotFindPayload
|
return ErrIPv6CouldNotFindPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseV4(data []byte, incoming bool, fp *firewall.Packet) error {
|
func parseV4(data []byte, incoming bool, fp *firewall.ParsedPacket) error {
|
||||||
// Do we at least have an ipv4 header worth of data?
|
// Do we at least have an ipv4 header worth of data?
|
||||||
if len(data) < ipv4.HeaderLen {
|
if len(data) < ipv4.HeaderLen {
|
||||||
return ErrIPv4PacketTooShort
|
return ErrIPv4PacketTooShort
|
||||||
@@ -458,6 +471,10 @@ func parseV4(data []byte, incoming bool, fp *firewall.Packet) error {
|
|||||||
// Check if this is the second or further fragment of a fragmented packet.
|
// Check if this is the second or further fragment of a fragmented packet.
|
||||||
flagsfrags := binary.BigEndian.Uint16(data[6:8])
|
flagsfrags := binary.BigEndian.Uint16(data[6:8])
|
||||||
fp.Fragment = (flagsfrags & 0x1FFF) != 0
|
fp.Fragment = (flagsfrags & 0x1FFF) != 0
|
||||||
|
// Any fragmentation at all (MF or offset): first fragments have readable
|
||||||
|
// ports for the firewall but must never be coalesced.
|
||||||
|
fp.FragAny = (flagsfrags & 0x3fff) != 0
|
||||||
|
fp.IPHdrLen = ihl
|
||||||
|
|
||||||
// Firewall handles protocol checks
|
// Firewall handles protocol checks
|
||||||
fp.Protocol = data[9]
|
fp.Protocol = data[9]
|
||||||
@@ -501,7 +518,7 @@ func parseV4(data []byte, incoming bool, fp *firewall.Packet) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Interface) handleOutsideMessagePacket(hostinfo *HostInfo, messageCounter uint64, out []byte, scratch []byte, fwPacket *firewall.Packet, nb []byte, q int, localCache firewall.ConntrackCache) {
|
func (f *Interface) handleOutsideMessagePacket(hostinfo *HostInfo, messageCounter uint64, out []byte, scratch []byte, fwPacket *firewall.ParsedPacket, nb []byte, q int, localCache firewall.ConntrackCache) {
|
||||||
err := newPacket(out, true, fwPacket)
|
err := newPacket(out, true, fwPacket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hostinfo.logger(f.l).Warn("Error while validating inbound packet",
|
hostinfo.logger(f.l).Warn("Error while validating inbound packet",
|
||||||
@@ -511,7 +528,7 @@ func (f *Interface) handleOutsideMessagePacket(hostinfo *HostInfo, messageCounte
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dropReason := f.firewall.Drop(*fwPacket, true, hostinfo, f.pki.GetCAPool(), localCache)
|
dropReason := f.firewall.Drop(fwPacket.Packet, true, hostinfo, f.pki.GetCAPool(), localCache)
|
||||||
if dropReason != nil {
|
if dropReason != nil {
|
||||||
f.rejectOutside(out, hostinfo.ConnectionState, hostinfo, nb, scratch, q)
|
f.rejectOutside(out, hostinfo.ConnectionState, hostinfo, nb, scratch, q)
|
||||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||||
@@ -523,7 +540,7 @@ func (f *Interface) handleOutsideMessagePacket(hostinfo *HostInfo, messageCounte
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = f.batchers[q].Commit(out, batch.SortKey{Epoch: hostinfo.ConnectionState.epoch, Counter: messageCounter})
|
err = f.batchers[q].Commit(out, batch.SortKey{Epoch: hostinfo.ConnectionState.epoch, Counter: messageCounter}, fwPacket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.l.Error("Failed to write to tun", "error", err)
|
f.l.Error("Failed to write to tun", "error", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+90
-5
@@ -17,7 +17,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Test_newPacket(t *testing.T) {
|
func Test_newPacket(t *testing.T) {
|
||||||
p := &firewall.Packet{}
|
p := &firewall.ParsedPacket{}
|
||||||
|
|
||||||
// length fails
|
// length fails
|
||||||
err := newPacket([]byte{}, true, p)
|
err := newPacket([]byte{}, true, p)
|
||||||
@@ -96,7 +96,7 @@ func Test_newPacket(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_newPacket_v6(t *testing.T) {
|
func Test_newPacket_v6(t *testing.T) {
|
||||||
p := &firewall.Packet{}
|
p := &firewall.ParsedPacket{}
|
||||||
|
|
||||||
// invalid ipv6
|
// invalid ipv6
|
||||||
ip := layers.IPv6{
|
ip := layers.IPv6{
|
||||||
@@ -345,7 +345,7 @@ func Test_newPacket_v6(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_newPacket_ipv6Fragment(t *testing.T) {
|
func Test_newPacket_ipv6Fragment(t *testing.T) {
|
||||||
p := &firewall.Packet{}
|
p := &firewall.ParsedPacket{}
|
||||||
|
|
||||||
ip := &layers.IPv6{
|
ip := &layers.IPv6{
|
||||||
Version: 6,
|
Version: 6,
|
||||||
@@ -525,7 +525,7 @@ func BenchmarkParseV6(b *testing.B) {
|
|||||||
secondFrag = append(secondFrag, fragHeader...)
|
secondFrag = append(secondFrag, fragHeader...)
|
||||||
secondFrag = append(secondFrag, []byte{0xde, 0xad, 0xbe, 0xef}...)
|
secondFrag = append(secondFrag, []byte{0xde, 0xad, 0xbe, 0xef}...)
|
||||||
|
|
||||||
fp := &firewall.Packet{}
|
fp := &firewall.ParsedPacket{}
|
||||||
|
|
||||||
b.Run("Normal", func(b *testing.B) {
|
b.Run("Normal", func(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
@@ -649,7 +649,7 @@ func serializeAH(ah *layers.IPSecAH) []byte {
|
|||||||
// host OS parses the real header, a firewall port/proto bypass. The fix makes parseV6 land
|
// host OS parses the real header, a firewall port/proto bypass. The fix makes parseV6 land
|
||||||
// on the same offset the host does.
|
// on the same offset the host does.
|
||||||
func Test_newPacket_v6ExtHeaderOverflow(t *testing.T) {
|
func Test_newPacket_v6ExtHeaderOverflow(t *testing.T) {
|
||||||
p := &firewall.Packet{}
|
p := &firewall.ParsedPacket{}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
hdrLen = 40 // IPv6 header
|
hdrLen = 40 // IPv6 header
|
||||||
@@ -675,3 +675,88 @@ func Test_newPacket_v6ExtHeaderOverflow(t *testing.T) {
|
|||||||
// the host delivers to, not the forged 443 at the overflowed offset.
|
// the host delivers to, not the forged 443 at the overflowed offset.
|
||||||
assert.Equal(t, uint16(22), p.LocalPort, "firewall must parse the real transport header, not the overflowed offset")
|
assert.Equal(t, uint16(22), p.LocalPort, "firewall must parse the real transport header, not the overflowed offset")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test_newPacket_parsedFields pins the ParsedPacket byproducts the RX
|
||||||
|
// batcher consumes: IPHdrLen (the true L4 offset) and FragAny (any fragment
|
||||||
|
// shape at all — unlike Packet.Fragment, which is port-oriented and true
|
||||||
|
// only for non-first fragments).
|
||||||
|
func Test_newPacket_parsedFields(t *testing.T) {
|
||||||
|
p := &firewall.ParsedPacket{}
|
||||||
|
|
||||||
|
// Plain IPv4 TCP, IHL 20: L4 offset 20, no fragment shape.
|
||||||
|
v4 := make([]byte, 28)
|
||||||
|
v4[0] = 0x45
|
||||||
|
v4[9] = firewall.ProtoTCP
|
||||||
|
binary.BigEndian.PutUint16(v4[6:8], 0x4000) // DF only
|
||||||
|
require.NoError(t, newPacket(v4, true, p))
|
||||||
|
assert.Equal(t, 20, p.IPHdrLen)
|
||||||
|
assert.False(t, p.FragAny)
|
||||||
|
assert.False(t, p.Fragment)
|
||||||
|
|
||||||
|
// IPv4 first fragment (MF set, offset 0): the firewall can read ports
|
||||||
|
// (Fragment false) but the coalescer must not touch it (FragAny true).
|
||||||
|
ff := make([]byte, 28)
|
||||||
|
ff[0] = 0x45
|
||||||
|
ff[9] = firewall.ProtoUDP
|
||||||
|
binary.BigEndian.PutUint16(ff[6:8], 0x2000) // MF, offset 0
|
||||||
|
require.NoError(t, newPacket(ff, true, p))
|
||||||
|
assert.False(t, p.Fragment)
|
||||||
|
assert.True(t, p.FragAny)
|
||||||
|
assert.Equal(t, 20, p.IPHdrLen)
|
||||||
|
|
||||||
|
// IPv4 non-first fragment (nonzero offset): both flags set.
|
||||||
|
nf := make([]byte, 28)
|
||||||
|
nf[0] = 0x45
|
||||||
|
nf[9] = firewall.ProtoUDP
|
||||||
|
binary.BigEndian.PutUint16(nf[6:8], 0x00b9)
|
||||||
|
require.NoError(t, newPacket(nf, true, p))
|
||||||
|
assert.True(t, p.Fragment)
|
||||||
|
assert.True(t, p.FragAny)
|
||||||
|
|
||||||
|
// IPv4 with options (IHL 24): IPHdrLen tracks the real L4 offset.
|
||||||
|
opts := make([]byte, 32)
|
||||||
|
opts[0] = 0x46
|
||||||
|
opts[9] = firewall.ProtoTCP
|
||||||
|
binary.BigEndian.PutUint16(opts[6:8], 0x4000)
|
||||||
|
require.NoError(t, newPacket(opts, true, p))
|
||||||
|
assert.Equal(t, 24, p.IPHdrLen)
|
||||||
|
assert.False(t, p.FragAny)
|
||||||
|
|
||||||
|
// Plain IPv6 TCP: L4 at 40.
|
||||||
|
v6 := make([]byte, 60)
|
||||||
|
v6[0] = 0x60
|
||||||
|
v6[6] = firewall.ProtoTCP
|
||||||
|
require.NoError(t, newPacket(v6, true, p))
|
||||||
|
assert.Equal(t, 40, p.IPHdrLen)
|
||||||
|
assert.False(t, p.FragAny)
|
||||||
|
|
||||||
|
// IPv6 hop-by-hop then TCP: IPHdrLen lands past the extension header.
|
||||||
|
hbh := make([]byte, 60)
|
||||||
|
hbh[0] = 0x60
|
||||||
|
hbh[6] = 0 // hop-by-hop
|
||||||
|
hbh[40] = firewall.ProtoTCP
|
||||||
|
hbh[41] = 0 // HdrExtLen 0 -> 8-byte header
|
||||||
|
require.NoError(t, newPacket(hbh, true, p))
|
||||||
|
assert.Equal(t, 48, p.IPHdrLen)
|
||||||
|
assert.False(t, p.FragAny)
|
||||||
|
|
||||||
|
// IPv6 first fragment: terminal proto resolved, FragAny set, Fragment not.
|
||||||
|
f6 := make([]byte, 60)
|
||||||
|
f6[0] = 0x60
|
||||||
|
f6[6] = 44 // fragment extension header
|
||||||
|
f6[40] = firewall.ProtoUDP
|
||||||
|
require.NoError(t, newPacket(f6, true, p))
|
||||||
|
assert.True(t, p.FragAny)
|
||||||
|
assert.False(t, p.Fragment)
|
||||||
|
assert.Equal(t, uint8(firewall.ProtoUDP), p.Protocol)
|
||||||
|
|
||||||
|
// IPv6 non-first fragment: both set, walk stops at the fragment header.
|
||||||
|
f6n := make([]byte, 60)
|
||||||
|
f6n[0] = 0x60
|
||||||
|
f6n[6] = 44
|
||||||
|
f6n[40] = firewall.ProtoUDP
|
||||||
|
binary.BigEndian.PutUint16(f6n[42:44], 0x0008)
|
||||||
|
require.NoError(t, newPacket(f6n, true, p))
|
||||||
|
assert.True(t, p.Fragment)
|
||||||
|
assert.True(t, p.FragAny)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package batch
|
package batch
|
||||||
|
|
||||||
|
import "github.com/slackhq/nebula/firewall"
|
||||||
|
|
||||||
// SortKey identifies a packet's position in its sender's transmission order.
|
// SortKey identifies a packet's position in its sender's transmission order.
|
||||||
// Epoch is a receiver-local ordinal for the tunnel (ConnectionState) that
|
// Epoch is a receiver-local ordinal for the tunnel (ConnectionState) that
|
||||||
// decrypted the packet. A re-handshake replaces the tunnel outright — new
|
// decrypted the packet. A re-handshake replaces the tunnel outright — new
|
||||||
@@ -15,9 +17,12 @@ type SortKey struct {
|
|||||||
|
|
||||||
type RxBatcher interface {
|
type RxBatcher interface {
|
||||||
// Commit stages pkt to be flushed by the batch. key must carry the
|
// 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
|
// packet's session epoch and message counter; pp must be the firewall's
|
||||||
// valid until the next Flush, and not re-use it.
|
// parse of this same packet. The caller must keep pkt valid until the
|
||||||
Commit(pkt []byte, key SortKey) error
|
// 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
|
// Flush emits every staged packet. Packets are first sorted by key, so
|
||||||
// within each protocol lane emission follows the sender's transmission
|
// within each protocol lane emission follows the sender's transmission
|
||||||
// order regardless of arrival order. One shape may legally be overtaken
|
// order regardless of arrival order. One shape may legally be overtaken
|
||||||
|
|||||||
@@ -40,34 +40,19 @@ type parsedIP struct {
|
|||||||
// On success, p.pkt is len-trimmed to the IP-declared length so callers
|
// 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
|
// 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.
|
// 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) {
|
func parseIPPrologue(pkt []byte, wantProto byte) (parsedIP, bool) {
|
||||||
var p parsedIP
|
var p parsedIP
|
||||||
if len(pkt) < 20 {
|
if len(pkt) < 20 {
|
||||||
return p, false
|
return p, false
|
||||||
}
|
}
|
||||||
v := pkt[0] >> 4
|
switch pkt[0] >> 4 {
|
||||||
switch v {
|
|
||||||
case 4:
|
case 4:
|
||||||
ihl := int(pkt[0]&0x0f) * 4
|
|
||||||
if ihl != 20 {
|
|
||||||
return p, false
|
|
||||||
}
|
|
||||||
if pkt[9] != wantProto {
|
if pkt[9] != wantProto {
|
||||||
return p, false
|
return p, false
|
||||||
}
|
}
|
||||||
// Reject actual fragmentation (MF or non-zero frag offset).
|
return parseIPv4Prologue(pkt)
|
||||||
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]
|
|
||||||
case 6:
|
case 6:
|
||||||
if len(pkt) < 40 {
|
if len(pkt) < 40 {
|
||||||
return p, false
|
return p, false
|
||||||
@@ -75,18 +60,77 @@ func parseIPPrologue(pkt []byte, wantProto byte) (parsedIP, bool) {
|
|||||||
if pkt[6] != wantProto {
|
if pkt[6] != wantProto {
|
||||||
return p, false
|
return p, false
|
||||||
}
|
}
|
||||||
payloadLen := int(binary.BigEndian.Uint16(pkt[4:6]))
|
return parseIPv6Prologue(pkt)
|
||||||
if 40+payloadLen > len(pkt) {
|
}
|
||||||
return p, false
|
return p, false
|
||||||
}
|
}
|
||||||
p.ipHdrLen = 40
|
|
||||||
p.fk.isV6 = true
|
// parseIPAt is the dispatcher-path prologue: newPacket already resolved the
|
||||||
copy(p.fk.src[:], pkt[8:24])
|
// L4 protocol and header offset once for the firewall, so the proto sniff is
|
||||||
copy(p.fk.dst[:], pkt[24:40])
|
// replaced by a cross-check of the caller's ipHdrLen. A plain header (v4:
|
||||||
p.pkt = pkt[:40+payloadLen]
|
// IHL 20, v6: exactly 40 — no options, no extension headers) is the only
|
||||||
default:
|
// 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
|
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
|
return p, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/slackhq/nebula/iputil"
|
"github.com/slackhq/nebula/firewall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MultiCoalescer stages plaintext packets with their (epoch, counter) sort
|
// MultiCoalescer stages plaintext packets with their (epoch, counter) sort
|
||||||
@@ -47,9 +47,15 @@ type MultiCoalescer struct {
|
|||||||
staged []stagedPacket
|
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 {
|
type stagedPacket struct {
|
||||||
pkt []byte
|
pkt []byte
|
||||||
key SortKey
|
key SortKey
|
||||||
|
proto byte
|
||||||
|
fragAny bool
|
||||||
|
ipHdrLen uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMultiCoalescer builds a multi-lane batcher over w, based on available
|
// 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
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
// IANA protocol numbers for the IPv6 extension headers
|
// Commit stages pkt for the next Flush. All lane dispatch is deferred to
|
||||||
// iputil.IPv6FindUpperProtocol can step over. The set here must match what
|
// Flush so it runs on packets already in transmission order. pp is the
|
||||||
// that walker walks: it is the hot path's cheap pre-guard, so the walk is
|
// firewall's parse of pkt — the single source of truth for the packet's
|
||||||
// only paid when it can actually make progress.
|
// protocol and L4 offset — and is only borrowed for this call.
|
||||||
const (
|
func (m *MultiCoalescer) Commit(pkt []byte, key SortKey, pp *firewall.ParsedPacket) error {
|
||||||
ipProtoHopByHop = 0
|
m.staged = append(m.staged, stagedPacket{
|
||||||
ipProtoRouting = 43
|
pkt: pkt,
|
||||||
ipProtoFragment = 44
|
key: key,
|
||||||
ipProtoAH = 51
|
proto: pp.Protocol,
|
||||||
ipProtoDestOpts = 60
|
fragAny: pp.FragAny,
|
||||||
)
|
ipHdrLen: uint16(pp.IPHdrLen),
|
||||||
|
})
|
||||||
// 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})
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,59 +106,43 @@ func compareStaged(a, b stagedPacket) int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// dispatch routes one packet to the appropriate lane based on IP version +
|
// dispatch routes one staged packet to its lane.
|
||||||
// L4 proto. On the success path the IP/TCP-or-UDP parse happens here once
|
// The protocol and L4 offset come from the firewall's parse of the same packet.
|
||||||
// and the parsed struct is handed to the lane via commitParsed so the lane
|
// Any shape a lane can't coalesce seals every open chain in its lane
|
||||||
// doesn't re-walk the header.
|
func (m *MultiCoalescer) dispatch(sp stagedPacket) error {
|
||||||
func (m *MultiCoalescer) dispatch(pkt []byte) error {
|
switch sp.proto {
|
||||||
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 {
|
|
||||||
case ipProtoTCP:
|
case ipProtoTCP:
|
||||||
if m.tcp != nil {
|
if m.tcp != nil {
|
||||||
info, ok := parseTCPBase(pkt)
|
if sp.fragAny {
|
||||||
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.
|
|
||||||
m.tcp.sealAllOpen()
|
m.tcp.sealAllOpen()
|
||||||
m.tcp.addVerbatim(pkt)
|
m.tcp.addVerbatim(sp.pkt)
|
||||||
return nil
|
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:
|
case ipProtoUDP:
|
||||||
if m.udp != nil {
|
if m.udp != nil {
|
||||||
info, ok := parseUDP(pkt)
|
if sp.fragAny {
|
||||||
if !ok {
|
|
||||||
m.udp.sealAllOpen()
|
m.udp.sealAllOpen()
|
||||||
m.udp.addVerbatim(pkt)
|
m.udp.addVerbatim(sp.pkt)
|
||||||
return nil
|
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
|
// Flush sorts the staged batch into transmission order, replays it into the
|
||||||
@@ -178,7 +154,7 @@ func (m *MultiCoalescer) Flush() error {
|
|||||||
|
|
||||||
var errs []error
|
var errs []error
|
||||||
for _, sp := range m.staged {
|
for _, sp := range m.staged {
|
||||||
if err := m.dispatch(sp.pkt); err != nil {
|
if err := m.dispatch(sp); err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/firewall"
|
||||||
"github.com/slackhq/nebula/test"
|
"github.com/slackhq/nebula/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,19 +49,19 @@ func TestMultiCoalescerRoutesByProto(t *testing.T) {
|
|||||||
icmp[3] = 28
|
icmp[3] = 28
|
||||||
icmp[9] = 1
|
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)
|
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)
|
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)
|
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)
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := m.Flush(); err != nil {
|
if err := m.Flush(); err != nil {
|
||||||
@@ -89,13 +90,13 @@ func TestMultiCoalescerRestoresTransmissionOrder(t *testing.T) {
|
|||||||
|
|
||||||
// Transmission order: seq 1000 (c1), 2200 (c2), 3400 (c3).
|
// Transmission order: seq 1000 (c1), 2200 (c2), 3400 (c3).
|
||||||
// Arrival order: 3400, 1000, 2200.
|
// 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)
|
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)
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := m.Flush(); err != nil {
|
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).
|
// Retransmit: seq 1000 again but counter 4 — sorts after seq 4600 (c3).
|
||||||
w.writes, w.gsoWrites, w.order = nil, nil, nil
|
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)
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := m.Flush(); err != nil {
|
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).
|
// Transmission: A.100 (c1), B.500 (c2), A.1300 (c3), B.1700 (c4).
|
||||||
// Arrival: A.1300, B.1700, A.100, B.500.
|
// 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)
|
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)
|
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)
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := m.Flush(); err != nil {
|
if err := m.Flush(); err != nil {
|
||||||
@@ -195,10 +196,10 @@ func TestMultiCoalescerEpochOrdersAcrossRehandshake(t *testing.T) {
|
|||||||
pay := make([]byte, 1200)
|
pay := make([]byte, 1200)
|
||||||
|
|
||||||
// New session's first data arrives before the old session's last data.
|
// 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)
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := m.Flush(); err != nil {
|
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")
|
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)
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := m.Flush(); err != nil {
|
if err := m.Flush(); err != nil {
|
||||||
@@ -261,7 +262,7 @@ func TestMultiCoalescerNoOffloadsStillSorts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Committed in reverse transmission order; keys carry the truth.
|
// Committed in reverse transmission order; keys carry the truth.
|
||||||
for i := len(pkts) - 1; i >= 0; i-- {
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,13 +326,13 @@ func TestMultiCoalescerIPv6FragmentStaysInLane(t *testing.T) {
|
|||||||
m := newTestMultiCoalescer(t, w)
|
m := newTestMultiCoalescer(t, w)
|
||||||
k := &keySeq{epoch: 1}
|
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)
|
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)
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := m.Flush(); err != nil {
|
if err := m.Flush(); err != nil {
|
||||||
@@ -358,19 +359,19 @@ func TestMultiCoalescerFragmentSealsUDPChains(t *testing.T) {
|
|||||||
m := newTestMultiCoalescer(t, w)
|
m := newTestMultiCoalescer(t, w)
|
||||||
k := &keySeq{epoch: 1}
|
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)
|
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)
|
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)
|
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)
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := m.Flush(); err != nil {
|
if err := m.Flush(); err != nil {
|
||||||
@@ -398,10 +399,10 @@ func TestMultiCoalescerNoTSOFallsThrough(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pay := make([]byte, 1200)
|
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)
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := m.Flush(); err != nil {
|
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))
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package batch
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/firewall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Passthrough is a RxBatcher that doesn't batch anything, it just accumulates and then sends packets.
|
// 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
|
func (p *Passthrough) Commit(pkt []byte, _ SortKey, _ *firewall.ParsedPacket) error {
|
||||||
// front) emits in arrival order, exactly as before keys existed.
|
|
||||||
func (p *Passthrough) Commit(pkt []byte, _ SortKey) error {
|
|
||||||
return p.enqueue(pkt)
|
return p.enqueue(pkt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,12 +117,27 @@ type parsedTCP struct {
|
|||||||
// regardless of whether it's admissible for coalescing. Returns ok=false for non-TCP or malformed input.
|
// 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).
|
// Accepts IPv4 (no options or fragmentation) and IPv6 (no extension headers).
|
||||||
func parseTCPBase(pkt []byte) (parsedTCP, bool) {
|
func parseTCPBase(pkt []byte) (parsedTCP, bool) {
|
||||||
var p parsedTCP
|
|
||||||
ip, ok := parseIPPrologue(pkt, ipProtoTCP)
|
ip, ok := parseIPPrologue(pkt, ipProtoTCP)
|
||||||
if !ok {
|
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.fk = ip.fk
|
||||||
p.ipHdrLen = ip.ipHdrLen
|
p.ipHdrLen = ip.ipHdrLen
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/firewall"
|
||||||
"github.com/slackhq/nebula/overlay/tio"
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/test"
|
"github.com/slackhq/nebula/test"
|
||||||
)
|
)
|
||||||
@@ -169,16 +170,22 @@ func BenchmarkCommitNonCoalesceableTCP(b *testing.B) {
|
|||||||
|
|
||||||
// runMultiCommitBench drives MultiCoalescer.Commit with in-order keys, so
|
// runMultiCommitBench drives MultiCoalescer.Commit with in-order keys, so
|
||||||
// it includes the staging sort's already-sorted fast path plus the
|
// 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) {
|
func runMultiCommitBench(b *testing.B, pkts [][]byte, batchSize int) {
|
||||||
b.Helper()
|
b.Helper()
|
||||||
m := NewMultiCoalescer(nopTunWriter{}, test.NewLogger())
|
m := NewMultiCoalescer(nopTunWriter{}, test.NewLogger())
|
||||||
|
pps := make([]*firewall.ParsedPacket, len(pkts))
|
||||||
|
for i, p := range pkts {
|
||||||
|
pps[i] = testPP(p)
|
||||||
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
b.SetBytes(int64(len(pkts[0])))
|
b.SetBytes(int64(len(pkts[0])))
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
pkt := pkts[i%len(pkts)]
|
j := i % len(pkts)
|
||||||
if err := m.Commit(pkt, SortKey{Epoch: 1, Counter: uint64(i + 1)}); err != nil {
|
if err := m.Commit(pkts[j], SortKey{Epoch: 1, Counter: uint64(i + 1)}, pps[j]); err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
if (i+1)%batchSize == 0 {
|
if (i+1)%batchSize == 0 {
|
||||||
|
|||||||
@@ -87,12 +87,27 @@ type parsedUDP struct {
|
|||||||
// Returns ok=false for non-UDP, malformed, or unsupported header shapes
|
// Returns ok=false for non-UDP, malformed, or unsupported header shapes
|
||||||
// (IPv4 with options/fragmentation, IPv6 with extension headers).
|
// (IPv4 with options/fragmentation, IPv6 with extension headers).
|
||||||
func parseUDP(pkt []byte) (parsedUDP, bool) {
|
func parseUDP(pkt []byte) (parsedUDP, bool) {
|
||||||
var p parsedUDP
|
|
||||||
ip, ok := parseIPPrologue(pkt, ipProtoUDP)
|
ip, ok := parseIPPrologue(pkt, ipProtoUDP)
|
||||||
if !ok {
|
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.fk = ip.fk
|
||||||
p.ipHdrLen = ip.ipHdrLen
|
p.ipHdrLen = ip.ipHdrLen
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user