mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 18:56:52 +01:00
darwin: pipeline listenIn's sends so write batches can form
On darwin, listenIn encrypts into one SendBatch while a sender goroutine writes the other (batch.SendPipeline). The sender takes whatever built up during its last write, so writes overlap tun reads and packets queued behind a write leave in one WriteBatch, which a connected socket sends as one sendmsg_x. Two batches bound memory; a full producer waits for the swap rather than dropping. One producer and one sender keep per-peer order equal to message-counter order. Everywhere else the branch is a false constant and listenIn is unchanged. -tags send_pipeline builds the pipeline on any platform, so its tests and the e2e suite can run through it on linux. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
81f991f1fd
commit
64bec56404
6 files changed
+726
No files matched your search
@@ -0,0 +1,89 @@
|
||||
//go:build darwin || send_pipeline
|
||||
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/test"
|
||||
"github.com/slackhq/nebula/udp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
// scriptedQueue hands out reads from a script, then fails every Read with err.
|
||||
type scriptedQueue struct {
|
||||
reads [][]tio.Packet
|
||||
err error
|
||||
}
|
||||
|
||||
func (q *scriptedQueue) Read() ([]tio.Packet, error) {
|
||||
if len(q.reads) == 0 {
|
||||
return nil, q.err
|
||||
}
|
||||
r := q.reads[0]
|
||||
q.reads = q.reads[1:]
|
||||
return r, nil
|
||||
}
|
||||
func (q *scriptedQueue) Write(b []byte) (int, error) { return len(b), nil }
|
||||
func (q *scriptedQueue) Close() error { return nil }
|
||||
|
||||
type countingConn struct {
|
||||
udp.NoopConn
|
||||
mu sync.Mutex
|
||||
batches int
|
||||
}
|
||||
|
||||
func (c *countingConn) WriteBatch(bufs [][]byte, _ []netip.AddrPort) (int, error) {
|
||||
c.mu.Lock()
|
||||
c.batches++
|
||||
c.mu.Unlock()
|
||||
return len(bufs), nil
|
||||
}
|
||||
|
||||
// TestListenInPipelinedStops pins that the pipelined reader returns when its queue fails, stopping its sender, and
|
||||
// reports the error as fatal only when the interface isn't shutting down.
|
||||
func TestListenInPipelinedStops(t *testing.T) {
|
||||
for _, closing := range []bool{false, true} {
|
||||
t.Run(map[bool]string{false: "fatal", true: "shutdown"}[closing], func(t *testing.T) {
|
||||
defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
|
||||
readErr := errors.New("tun gone")
|
||||
conn := &countingConn{}
|
||||
// Packets too short to parse are dropped before the firewall, so this reads without a hostmap.
|
||||
junk := tio.Packet{Bytes: []byte{0x45, 0}}
|
||||
q := &scriptedQueue{reads: [][]tio.Packet{{junk}, {junk, junk, junk}, {}}, err: readErr}
|
||||
shutdown := make(chan struct{})
|
||||
f := &Interface{
|
||||
ctx: context.Background(),
|
||||
l: test.NewLogger(),
|
||||
writers: []udp.Conn{conn},
|
||||
triggerShutdown: func() { close(shutdown) },
|
||||
}
|
||||
f.closed.Store(closing)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
f.listenIn(q, 0)
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("listenIn did not return after its queue failed")
|
||||
}
|
||||
if closing {
|
||||
assert.Nil(t, f.fatalErr.Load())
|
||||
} else {
|
||||
<-shutdown
|
||||
assert.Equal(t, readErr, *f.fatalErr.Load())
|
||||
}
|
||||
assert.Zero(t, conn.batches, "dropped packets must not be written")
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user