Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
e7b121c
better and batched tun interface
JackDoan Apr 17, 2026
187afac
GSO/GRO offloads, with TCP+ECN and UDP support
JackDoan Apr 17, 2026
f04dd3b
switch to ASM vector checksum
JackDoan May 4, 2026
ca84bcb
disable sort-on-RX, CPU pinning seems to work for now
JackDoan May 4, 2026
6944147
scoot stuff around for e2e
JackDoan May 4, 2026
264a253
scoot pinning around
JackDoan May 5, 2026
f31f6c5
make relays take the fast path maybe
JackDoan May 6, 2026
9104dc4
remove udp-level RX reorder buf
JackDoan May 6, 2026
5681e51
use clear()
JackDoan May 6, 2026
67a742d
no
JackDoan May 6, 2026
a66af84
faster
JackDoan May 6, 2026
f7cc437
fix
JackDoan May 11, 2026
22adae0
lint
JackDoan May 11, 2026
410bac9
go mod tidy
JackDoan May 11, 2026
3b16f1a
drop in a logger
JackDoan May 11, 2026
b50d627
clean up a comment a bit
JackDoan May 11, 2026
a081fba
use less ram pls
JackDoan May 11, 2026
9bdab87
udp setsockopt correctness fixes
JackDoan May 12, 2026
243c920
adapt Control lifecycle tests to the batched tio.Queue Device interface
JackDoan Jul 13, 2026
1e21873
SendVia: don't emit a zero-length packet when prepareSendVia fails
JackDoan Jul 13, 2026
7fee3a9
correctly shutdown the pprofserver
JackDoan Jul 13, 2026
733dc06
make mobile happy
JackDoan Jul 13, 2026
44dd2e9
datapath: fix 12 correctness findings from tun/UDP offload review
JackDoan Jul 13, 2026
c7918d1
lint
JackDoan Jul 13, 2026
1aca2f7
more fixes!
JackDoan Jul 13, 2026
ae17513
more fixes!
JackDoan Jul 13, 2026
8b14a6e
more ram -> more speed
JackDoan Jul 13, 2026
9e7c783
checkpt
JackDoan Jul 14, 2026
99bf613
checkpt
JackDoan Jul 14, 2026
0544352
make service test less annoying
JackDoan Jul 14, 2026
720990d
simplify making new Queues
JackDoan Jul 14, 2026
68e3fae
some tests
JackDoan Jul 14, 2026
7cc3732
re-align to master
JackDoan Jul 14, 2026
a6ae44d
batch: move shared-arena Reset ownership from lanes to their owner
JackDoan Jul 14, 2026
ed55cf4
batch: back SendBatch with Arena instead of a hand-rolled slab
JackDoan Jul 14, 2026
69fa9e4
unslop some comments
JackDoan Jul 14, 2026
1d768ac
tio: accept VIRTIO_NET_HDR_GSO_ECN-qualified superpackets
JackDoan Jul 14, 2026
c6ebe71
silly optimization
JackDoan Jul 15, 2026
ef0e301
decrypt in place
JackDoan Jul 15, 2026
0a0b240
put locks around the replay window
JackDoan Jul 15, 2026
92d51c0
fix it!
JackDoan Jul 17, 2026
8c91fa2
fix it!
JackDoan Jul 17, 2026
9688d32
make it nicer
JackDoan Jul 18, 2026
6bf424f
Transmit a computed-zero UDP checksum as all ones (#1823)
nbrownus Jul 25, 2026
88872a8
Don't fail on the batch at the first error (#1826)
nbrownus Jul 27, 2026
eea3c81
udp: disable GSO at runtime when the kernel rejects a GSO send
JackDoan Jul 27, 2026
be35d4f
udp: downgrade the per-batch sendmmsg failure log to debug
JackDoan Jul 27, 2026
c06bfb4
overlay/batch: remove end-of-batch debug scaffolding
JackDoan Jul 27, 2026
d74c5ac
overlay/batch: don't false-set PSH when merging a short-tail-sealed s…
JackDoan Jul 27, 2026
35596c7
another VIRTIO_NET_HDR_GSO_ECN mistake
JackDoan Jul 27, 2026
5c0a5ee
overlay/tio: guard Offload.Write against zero-length buffers
JackDoan Jul 27, 2026
6c0305c
overlay/tio: derive WriteGSO geometry from non-empty fragments
JackDoan Jul 27, 2026
56d2a38
tun: default pin_threads off
JackDoan Jul 27, 2026
8006b58
util: unlock the OS thread when CPU pinning fails
JackDoan Jul 27, 2026
6fb10c1
udp: make parseRecvCmsg's length check overflow-safe
JackDoan Jul 27, 2026
921ed43
overlay/tio: validate WriteGSO geometry instead of silently dropping
JackDoan Jul 27, 2026
fb0d20f
docs: note the GRO receive-scratch memory cost of listen.batch
JackDoan Jul 27, 2026
8cebecc
overlay: test the checksum-seeding math against an RFC 1071 reference
JackDoan Jul 27, 2026
865dc97
overlay/checksum: test each arch implementation directly
JackDoan Jul 27, 2026
3fe2cb9
udp: extract and test the GRO RX splitting
JackDoan Jul 27, 2026
66cb98e
udp: test the partial-sendmmsg rewind
JackDoan Jul 27, 2026
f9eb86d
udp: smoke-test that GSO actually engages, on real sockets in CI
JackDoan Jul 27, 2026
3803146
improve WriteGSO again
JackDoan Jul 27, 2026
b7d0661
Revert "tun: default pin_threads off"
JackDoan Jul 27, 2026
46a02b6
virtio: reject the GSO_ECN qualifier on non-TCP GSO types
JackDoan Jul 28, 2026
db54e05
checkpt
JackDoan Jul 28, 2026
0b817c5
rework tun-side segmenentation checksums
JackDoan Jul 28, 2026
ed88422
unslop a bit
JackDoan Jul 28, 2026
5c2a560
unslop a bit
JackDoan Jul 28, 2026
df89551
unslop a bit
JackDoan Jul 28, 2026
5c0f6e2
unslop, improve the tio interface
JackDoan Jul 28, 2026
b39bae5
tio.Offload.WriteGSO: reject 0-len packets, check seg lengths
JackDoan Jul 29, 2026
095a421
silly opinionated tweak
JackDoan Jul 29, 2026
38f11f6
udp: drop dead cmsg write
JackDoan Jul 29, 2026
ffab005
dead code
JackDoan Jul 29, 2026
5a80145
silly e2e
JackDoan Jul 29, 2026
8c06802
UDP stuff
JackDoan Jul 29, 2026
d43bb81
batch stuff
JackDoan Jul 29, 2026
0b0e455
small bugs
JackDoan Jul 29, 2026
7455ac5
outside: drop oversized test requests regardless of log level
JackDoan Jul 29, 2026
9ce0596
tun: don't leak the QueueSet shutdown eventfd when Add fails
JackDoan Jul 29, 2026
e987068
overlay/tio: error on multi-segment WriteGSO with a bogus IP version
JackDoan Jul 29, 2026
fbcdd83
overlay/tio: log dropped tun reads with bad virtio headers
JackDoan Jul 29, 2026
8044b40
udp: degrade outer-ECN RX per-family on dual-stack sockets
JackDoan Jul 29, 2026
69cf816
inside: mark traffic-out once per superpacket, not per segment
JackDoan Jul 29, 2026
2190900
overlay/batch: ship never-grown slots as plain writes
JackDoan Jul 29, 2026
9c68c60
overlay/batch: don't seal the open slot on a pure ACK
JackDoan Jul 29, 2026
7ee5e29
docs: align comments with the code they describe
JackDoan Jul 29, 2026
d13c53d
docs: mark six deferred review findings with TODOs
JackDoan Jul 29, 2026
e8d6be1
udp: resume partial sendmmsg in place instead of repacking
JackDoan Jul 29, 2026
16878ee
fable fixes
JackDoan Jul 30, 2026
575b979
tun_linux: use IFF_TUN_EXCL to prevent multiple nebulas
JackDoan Jul 31, 2026
b3002c2
clean out junk
JackDoan Jul 31, 2026
b7bf322
plumb ECN through via relays
JackDoan Jul 31, 2026
da8a640
QueueSet.Add errors if QueueSet is closed
JackDoan Jul 31, 2026
9d8e830
disallow listen.batch < 1
JackDoan Jul 31, 2026
245eb61
don't pin when only one routine
JackDoan Jul 31, 2026
6783c90
ecn default disable eventually
JackDoan Jul 31, 2026
d3779b6
ecn: CE-mark on decap when the receive queue runs deep (nebula-as-AQM)
JackDoan Jul 14, 2026
93946fa
drop ECN support for this release
JackDoan Jul 31, 2026
549de9f
drop ECN support for this release
JackDoan Jul 31, 2026
ddb90ad
drop ECN support for this release
JackDoan Jul 31, 2026
d7bcfb5
drop TxBatcher interface
JackDoan Jul 31, 2026
bc70bf4
cap RX buffers in UDP rather than callers
JackDoan Jul 31, 2026
096a062
improve naming in batch
JackDoan Jul 31, 2026
adfacc4
snip
JackDoan Jul 31, 2026
42c937d
snip
JackDoan Jul 31, 2026
e04473f
crazy core pinning junk
JackDoan Aug 3, 2026
cdfba18
crazy core pinning junk
JackDoan Aug 3, 2026
5ea48c1
stop trying to interpret TCP, reorder via message counter and hostinf…
JackDoan Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ bin-pkcs11: BUILD_ARGS += -tags pkcs11
bin-pkcs11: CGO_ENABLED = 1
bin-pkcs11: bin

# Build with the pprof debug server (serves on :6060). See startPprofServer.
debug: BUILD_ARGS += -tags debug
debug: bin

bin:
go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula${NEBULA_CMD_SUFFIX} ${NEBULA_CMD_PATH}
go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula-cert${NEBULA_CMD_SUFFIX} ./cmd/nebula-cert
Expand Down Expand Up @@ -280,5 +284,5 @@ smoke-vagrant/%: bin-docker build/%/nebula
cd .github/workflows/smoke/ && ./smoke-vagrant.sh $*

.FORCE:
.PHONY: all all-linux all-freebsd all-openbsd all-netbsd all-darwin all-windows all-cross-linux all-cross-linux-arm all-cross-linux-mips all-cross-linux-other all-cross-darwin all-cross-windows bench bench-cpu bench-cpu-long bin build-test-mobile e2e e2ev e2evv e2evvv e2evvvv proto release service smoke-docker smoke-docker-race test test-cov-html smoke-vagrant/%
.PHONY: all all-linux all-freebsd all-openbsd all-netbsd all-darwin all-windows all-cross-linux all-cross-linux-arm all-cross-linux-mips all-cross-linux-other all-cross-darwin all-cross-windows bench bench-cpu bench-cpu-long bin debug build-test-mobile e2e e2ev e2evv e2evvv e2evvvv proto release service smoke-docker smoke-docker-race test test-cov-html smoke-vagrant/%
.DEFAULT_GOAL := bin
27 changes: 21 additions & 6 deletions connection_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@ import (
"github.com/slackhq/nebula/noiseutil"
)

const ReplayWindow = 1024
const ReplayWindow = 8192

// sessionEpoch hands out a receiver-local ordinal to every ConnectionState as
// it is created. The RX staging sort (overlay/batch) orders packets by
// (epoch, message counter). A re-handshake never rekeys an existing tunnel; it
// brings up a whole new hostinfo (and ConnectionState) with its own counter
// space starting near zero, while the old tunnel keeps decrypting in-flight
// packets until it is torn down. During that cutover one flush batch can hold
// packets from both tunnels, and the epoch is what keeps the old tunnel's
// packets sorted ahead of the new tunnel's.
var sessionEpoch atomic.Uint64

type ConnectionState struct {
eKey noiseutil.CipherState
Expand All @@ -24,6 +34,8 @@ type ConnectionState struct {
window *Bits
decryptLock sync.Mutex
writeLock sync.Mutex
// epoch is this session's sessionEpoch ordinal. Immutable after creation.
epoch uint64
}

// newConnectionStateFromResult builds a fully-populated ConnectionState from a
Expand All @@ -38,6 +50,7 @@ func newConnectionStateFromResult(r *handshake.Result) *ConnectionState {
eKey: noiseutil.NewCipherState(r.EKey, r.Cipher),
dKey: noiseutil.NewCipherState(r.DKey, r.Cipher),
window: NewBits(ReplayWindow),
epoch: sessionEpoch.Add(1),
}
ci.messageCounter.Add(r.MessageIndex)
for i := uint64(1); i <= r.MessageIndex; i++ {
Expand All @@ -58,16 +71,15 @@ func (cs *ConnectionState) Curve() cert.Curve {
return cs.myCert.Curve()
}

func (cs *ConnectionState) Decrypt(l *slog.Logger, messageCounter uint64, out []byte, packet []byte, nb []byte) ([]byte, error) {
var err error
func (cs *ConnectionState) Decrypt(l *slog.Logger, messageCounter uint64, packet []byte, nb []byte) ([]byte, error) {
cs.decryptLock.Lock()
result := cs.window.Check(l, messageCounter)
cs.decryptLock.Unlock()
if !result {
return nil, ErrAlreadySeen
}

out, err = cs.dKey.DecryptDanger(out, packet[:header.Len], packet[header.Len:], messageCounter, nb)
out, err := cs.dKey.DecryptDanger(packet[header.Len:header.Len], packet[:header.Len], packet[header.Len:], messageCounter, nb)
if err != nil {
return nil, err
}
Expand All @@ -81,7 +93,6 @@ func (cs *ConnectionState) Decrypt(l *slog.Logger, messageCounter uint64, out []
return out, nil
}

// VerifyRelay verifies AEAD protected (but not encrypted) relay frames. packet must be length-checked by the caller.
func (cs *ConnectionState) VerifyRelay(l *slog.Logger, messageCounter uint64, packet []byte, nb []byte) error {
cs.decryptLock.Lock()
result := cs.window.Check(l, messageCounter)
Expand All @@ -90,6 +101,11 @@ func (cs *ConnectionState) VerifyRelay(l *slog.Logger, messageCounter uint64, pa
return ErrAlreadySeen
}

// The entire body is sent as AD, not encrypted.
// The packet consists of a 16-byte parsed Nebula header, Associated Data-protected payload, and a trailing 16-byte AEAD signature value.
// The packet is guaranteed to be at least 16 bytes at this point, b/c it got past the h.Parse() call above. If it's
// otherwise malformed (meaning, there is no trailing 16 byte AEAD value), then this will result in at worst a 0-length slice
// which will gracefully fail in the DecryptDanger call.
signedPayload := packet[:len(packet)-cs.dKey.Overhead()]
signatureValue := packet[len(packet)-cs.dKey.Overhead():]
_, err := cs.dKey.DecryptDanger(nil, signedPayload, signatureValue, messageCounter, nb)
Expand All @@ -103,6 +119,5 @@ func (cs *ConnectionState) VerifyRelay(l *slog.Logger, messageCounter uint64, pa
if !result {
return ErrAlreadySeen
}

return nil
}
63 changes: 40 additions & 23 deletions control_lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (

"github.com/gaissmai/bart"
"github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/overlay/batch"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/routing"
"github.com/slackhq/nebula/test"
"github.com/slackhq/nebula/udp"
Expand All @@ -30,9 +32,9 @@ func newFakeDevice() *fakeDevice {

// Read blocks until Close like a real tun with no traffic, then reports EOF
// the same way a closed device does
func (d *fakeDevice) Read(p []byte) (int, error) {
func (d *fakeDevice) Read() ([]tio.Packet, error) {
<-d.closedCh
return 0, io.EOF
return nil, io.EOF
}

func (d *fakeDevice) Write(p []byte) (int, error) { return len(p), nil }
Expand All @@ -49,10 +51,8 @@ func (d *fakeDevice) Activate() error { return nil }
func (d *fakeDevice) Networks() []netip.Prefix { return nil }
func (d *fakeDevice) Name() string { return "fake" }
func (d *fakeDevice) RoutesFor(netip.Addr) routing.Gateways { return nil }
func (d *fakeDevice) SupportsMultiqueue() bool { return false }
func (d *fakeDevice) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, errors.New("unsupported")
}

func (d *fakeDevice) Queues(int) ([]tio.Queue, error) { return []tio.Queue{d}, nil }

// newReadyControl hand-builds the minimum Control that Main would have
// produced right before Start, including the construction token NewInterface
Expand All @@ -78,7 +78,7 @@ func newReadyControl(t *testing.T) (*Control, *fakeDevice, *fakeConn) {
inside: dev,
outside: conn,
writers: []udp.Conn{conn},
readers: make([]io.ReadWriteCloser, 1),
batchers: make([]batch.RxBatcher, 1),
routines: 1,
hostMap: newHostMap(l),
lightHouse: lh,
Expand Down Expand Up @@ -109,7 +109,8 @@ func TestControl_StopBeforeStart(t *testing.T) {
require.NoError(t, c.Wait())

// A stopped control can never be started
require.ErrorIs(t, c.Start(), ErrAlreadyStopped)
err := c.Start()
require.ErrorIs(t, err, ErrAlreadyStopped)

// A second Stop is a harmless no-op
c.Stop()
Expand Down Expand Up @@ -143,19 +144,29 @@ type fakeConn struct {
rebinds int
}

func (c *fakeConn) Rebind() error { c.rebinds++; return nil }
func (c *fakeConn) LocalAddr() (netip.AddrPort, error) { return netip.AddrPort{}, nil }
func (c *fakeConn) ListenOut(_ udp.EncReader) error { return nil }
func (c *fakeConn) WriteTo(_ []byte, _ netip.AddrPort) error { return nil }
func (c *fakeConn) ReloadConfig(_ *config.C) {}
func (c *fakeConn) SupportsMultipleReaders() bool { return true }
func (c *fakeConn) Close() error { c.closed = true; return nil }
func (c *fakeConn) Rebind() error { c.rebinds++; return nil }
func (c *fakeConn) LocalAddr() (netip.AddrPort, error) { return netip.AddrPort{}, nil }
func (c *fakeConn) ListenOut(_ udp.EncReader, _ func()) error { return nil }
func (c *fakeConn) WriteTo(_ []byte, _ netip.AddrPort) error { return nil }
func (c *fakeConn) WriteBatch(bufs [][]byte, _ []netip.AddrPort) (int, error) {
return len(bufs), nil
}
func (c *fakeConn) ReloadConfig(_ *config.C) {}
func (c *fakeConn) SupportsMultipleReaders() bool { return true }
func (c *fakeConn) Close() error { c.closed = true; return nil }

type multiqueueDevice struct {
*fakeDevice
}

func (d *multiqueueDevice) SupportsMultiqueue() bool { return true }
// Queues claims multiqueue support but fails to open the second queue,
// exercising the activation error path.
func (d *multiqueueDevice) Queues(n int) ([]tio.Queue, error) {
if n > 1 {
return nil, errors.New("second queue failed to open")
}
return d.fakeDevice.Queues(n)
}

func TestControl_StartMultiqueueFailureReleases(t *testing.T) {
dev := &multiqueueDevice{fakeDevice: newFakeDevice()}
Expand All @@ -166,7 +177,7 @@ func TestControl_StartMultiqueueFailureReleases(t *testing.T) {
inside: dev,
outside: conn,
writers: []udp.Conn{conn},
readers: make([]io.ReadWriteCloser, 2),
batchers: make([]batch.RxBatcher, 2),
routines: 2,
l: test.NewLogger(),
}
Expand All @@ -181,7 +192,8 @@ func TestControl_StartMultiqueueFailureReleases(t *testing.T) {
}

// The second reader fails to open, everything must be released
require.Error(t, c.Start())
err := c.Start()
require.Error(t, err)
assert.Equal(t, StateStopped, c.State())
assert.True(t, dev.closed, "the tun device should have been closed")
assert.True(t, conn.closed, "the udp socket should have been closed")
Expand Down Expand Up @@ -251,15 +263,18 @@ func TestControl_ConcurrentStopAndStart(t *testing.T) {
// panic and Wait must observe the final state
require.NoError(t, c.Wait())
assert.Equal(t, StateStopped, c.State())
require.ErrorIs(t, c.Start(), ErrAlreadyStopped)
err := c.Start()
require.ErrorIs(t, err, ErrAlreadyStopped)
}

func TestControl_StartStopLifecycle(t *testing.T) {
c, dev, conn := newReadyControl(t)

require.NoError(t, c.Start())
err := c.Start()
require.NoError(t, err)
assert.Equal(t, StateStarted, c.State())
require.ErrorIs(t, c.Start(), ErrAlreadyStarted)
err = c.Start()
require.ErrorIs(t, err, ErrAlreadyStarted)

// Stop must unpark the reader blocked in the device and release everything
c.Stop()
Expand All @@ -270,7 +285,8 @@ func TestControl_StartStopLifecycle(t *testing.T) {

// The reader drained off a closed device, that is not a fatal error
require.NoError(t, c.Wait())
require.ErrorIs(t, c.Start(), ErrAlreadyStopped)
err = c.Start()
require.ErrorIs(t, err, ErrAlreadyStopped)
}

func TestControl_RebindIsGatedByState(t *testing.T) {
Expand All @@ -280,7 +296,8 @@ func TestControl_RebindIsGatedByState(t *testing.T) {
c.RebindUDPServer()
assert.Equal(t, 0, conn.rebinds, "rebind before start must be a no-op")

require.NoError(t, c.Start())
err := c.Start()
require.NoError(t, err)
c.RebindUDPServer()
assert.Equal(t, 1, conn.rebinds, "rebind while started must reach the conn")

Expand Down
Loading