mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 12:57:01 +02:00
293 lines
8.1 KiB
Go
293 lines
8.1 KiB
Go
package nebula
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/netip"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gaissmai/bart"
|
|
"github.com/slackhq/nebula/config"
|
|
"github.com/slackhq/nebula/routing"
|
|
"github.com/slackhq/nebula/test"
|
|
"github.com/slackhq/nebula/udp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type fakeDevice struct {
|
|
closeOnce sync.Once
|
|
closedCh chan struct{}
|
|
closed bool
|
|
}
|
|
|
|
func newFakeDevice() *fakeDevice {
|
|
return &fakeDevice{closedCh: make(chan struct{})}
|
|
}
|
|
|
|
// 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) {
|
|
<-d.closedCh
|
|
return 0, io.EOF
|
|
}
|
|
|
|
func (d *fakeDevice) Write(p []byte) (int, error) { return len(p), nil }
|
|
|
|
func (d *fakeDevice) Close() error {
|
|
d.closeOnce.Do(func() {
|
|
d.closed = true
|
|
close(d.closedCh)
|
|
})
|
|
return nil
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
// newReadyControl hand-builds the minimum Control that Main would have
|
|
// produced right before Start, including the construction token NewInterface
|
|
// takes so waiters block until Close releases the resources
|
|
func newReadyControl(t *testing.T) (*Control, *fakeDevice, *fakeConn) {
|
|
l := test.NewLogger()
|
|
dev := newFakeDevice()
|
|
conn := &fakeConn{}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
myVpnNet := netip.MustParsePrefix("10.128.0.1/16")
|
|
nt := new(bart.Lite)
|
|
nt.Insert(myVpnNet)
|
|
cs := &CertState{
|
|
myVpnNetworks: []netip.Prefix{myVpnNet},
|
|
myVpnNetworksTable: nt,
|
|
}
|
|
lh, err := NewLightHouseFromConfig(ctx, l, config.NewC(l), cs, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
f := &Interface{
|
|
ctx: ctx,
|
|
inside: dev,
|
|
outside: conn,
|
|
writers: []udp.Conn{conn},
|
|
readers: make([]io.ReadWriteCloser, 1),
|
|
routines: 1,
|
|
hostMap: newHostMap(l),
|
|
lightHouse: lh,
|
|
l: l,
|
|
}
|
|
f.wg.Add(1)
|
|
|
|
return &Control{
|
|
state: StateReady,
|
|
f: f,
|
|
l: l,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}, dev, conn
|
|
}
|
|
|
|
func TestControl_StopBeforeStart(t *testing.T) {
|
|
c, dev, conn := newReadyControl(t)
|
|
|
|
// A Stop on a never started control must release everything Main acquired
|
|
c.Stop()
|
|
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")
|
|
require.ErrorIs(t, c.ctx.Err(), context.Canceled, "the service context should have been cancelled")
|
|
|
|
// Wait must return promptly now that the resources are released
|
|
require.NoError(t, c.Wait())
|
|
|
|
// A stopped control can never be started
|
|
require.ErrorIs(t, c.Start(), ErrAlreadyStopped)
|
|
|
|
// A second Stop is a harmless no-op
|
|
c.Stop()
|
|
assert.Equal(t, StateStopped, c.State())
|
|
require.NoError(t, c.Wait())
|
|
}
|
|
|
|
func TestControl_WaitBlocksUntilStop(t *testing.T) {
|
|
c, _, _ := newReadyControl(t)
|
|
|
|
done := make(chan error, 1)
|
|
go func() { done <- c.Wait() }()
|
|
|
|
select {
|
|
case <-done:
|
|
t.Fatal("Wait returned before Stop")
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
c.Stop()
|
|
select {
|
|
case err := <-done:
|
|
require.NoError(t, err)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Wait did not return after Stop")
|
|
}
|
|
}
|
|
|
|
type fakeConn struct {
|
|
closed bool
|
|
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 }
|
|
|
|
type multiqueueDevice struct {
|
|
*fakeDevice
|
|
}
|
|
|
|
func (d *multiqueueDevice) SupportsMultiqueue() bool { return true }
|
|
|
|
func TestControl_StartMultiqueueFailureReleases(t *testing.T) {
|
|
dev := &multiqueueDevice{fakeDevice: newFakeDevice()}
|
|
conn := &fakeConn{}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
f := &Interface{
|
|
ctx: ctx,
|
|
inside: dev,
|
|
outside: conn,
|
|
writers: []udp.Conn{conn},
|
|
readers: make([]io.ReadWriteCloser, 2),
|
|
routines: 2,
|
|
l: test.NewLogger(),
|
|
}
|
|
f.wg.Add(1)
|
|
|
|
c := &Control{
|
|
state: StateReady,
|
|
f: f,
|
|
l: test.NewLogger(),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
|
|
// The second reader fails to open, everything must be released
|
|
require.Error(t, c.Start())
|
|
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")
|
|
require.ErrorIs(t, c.ctx.Err(), context.Canceled)
|
|
|
|
// And Wait must not hang on the construction token
|
|
require.NoError(t, c.Wait())
|
|
}
|
|
|
|
func TestInterface_CloseIsIdempotent(t *testing.T) {
|
|
dev := newFakeDevice()
|
|
f := &Interface{
|
|
inside: dev,
|
|
l: test.NewLogger(),
|
|
}
|
|
f.wg.Add(1)
|
|
|
|
require.NoError(t, f.Close())
|
|
assert.True(t, dev.closed)
|
|
|
|
// A second Close must not double release the wg token or the device
|
|
require.NoError(t, f.Close())
|
|
require.NoError(t, f.wait())
|
|
}
|
|
|
|
func TestControl_FatalErrorReportsThroughWait(t *testing.T) {
|
|
c, dev, conn := newReadyControl(t)
|
|
|
|
// Mirror what Start wires up, without needing real packet readers
|
|
c.f.triggerShutdown = c.Stop
|
|
c.state = StateStarted
|
|
|
|
boom := errors.New("boom")
|
|
c.f.onFatal(boom)
|
|
|
|
require.ErrorIs(t, c.Wait(), boom)
|
|
assert.Equal(t, StateStopped, c.State())
|
|
assert.True(t, dev.closed)
|
|
assert.True(t, conn.closed)
|
|
|
|
// A second fatal error must not fire the shutdown again or replace the first
|
|
c.f.onFatal(errors.New("later"))
|
|
require.ErrorIs(t, c.Wait(), boom)
|
|
|
|
// Wait stays factual, a Stop after the death does not mask the error
|
|
c.Stop()
|
|
require.ErrorIs(t, c.Wait(), boom)
|
|
}
|
|
|
|
func TestControl_ConcurrentStopAndStart(t *testing.T) {
|
|
c, _, _ := newReadyControl(t)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 2; i++ {
|
|
wg.Go(func() { c.Stop() })
|
|
}
|
|
wg.Go(func() { _ = c.Start() })
|
|
wg.Go(func() {
|
|
_ = c.Wait()
|
|
// A returned Wait must always observe the final state, no matter how
|
|
// the race resolved
|
|
assert.Equal(t, StateStopped, c.State())
|
|
})
|
|
wg.Wait()
|
|
|
|
// However the race resolves, the control must end fully stopped with no
|
|
// 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)
|
|
}
|
|
|
|
func TestControl_StartStopLifecycle(t *testing.T) {
|
|
c, dev, conn := newReadyControl(t)
|
|
|
|
require.NoError(t, c.Start())
|
|
assert.Equal(t, StateStarted, c.State())
|
|
require.ErrorIs(t, c.Start(), ErrAlreadyStarted)
|
|
|
|
// Stop must unpark the reader blocked in the device and release everything
|
|
c.Stop()
|
|
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")
|
|
require.ErrorIs(t, c.ctx.Err(), context.Canceled)
|
|
|
|
// The reader drained off a closed device, that is not a fatal error
|
|
require.NoError(t, c.Wait())
|
|
require.ErrorIs(t, c.Start(), ErrAlreadyStopped)
|
|
}
|
|
|
|
func TestControl_RebindIsGatedByState(t *testing.T) {
|
|
c, _, conn := newReadyControl(t)
|
|
|
|
// A rebind before Start reaches nothing, the interface is not up
|
|
c.RebindUDPServer()
|
|
assert.Equal(t, 0, conn.rebinds, "rebind before start must be a no-op")
|
|
|
|
require.NoError(t, c.Start())
|
|
c.RebindUDPServer()
|
|
assert.Equal(t, 1, conn.rebinds, "rebind while started must reach the conn")
|
|
|
|
// A rebind racing a completed stop must not touch the closed conn
|
|
c.Stop()
|
|
require.NoError(t, c.Wait())
|
|
c.RebindUDPServer()
|
|
assert.Equal(t, 1, conn.rebinds, "rebind after stop must be a no-op")
|
|
}
|