mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 08:36:57 +02:00
crazy multiport stuff
This commit is contained in:
+409
@@ -0,0 +1,409 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/header"
|
||||
"github.com/slackhq/nebula/overlay/batch"
|
||||
"github.com/slackhq/nebula/overlay/overlaytest"
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/test"
|
||||
"github.com/slackhq/nebula/udp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestBaseHostInfo(vpnIp netip.Addr, localIdx, remoteIdx uint32, laneCount int) *HostInfo {
|
||||
base := &HostInfo{
|
||||
vpnAddrs: []netip.Addr{vpnIp},
|
||||
localIndexId: localIdx,
|
||||
remoteIndexId: remoteIdx,
|
||||
remotes: NewRemoteList([]netip.Addr{vpnIp}, nil),
|
||||
HandshakePacket: map[uint8][]byte{},
|
||||
}
|
||||
base.SetRemote(netip.MustParseAddrPort("192.0.2.1:4242"))
|
||||
base.lanes = newLaneState(laneCount, uint16(laneCount), 4242)
|
||||
return base
|
||||
}
|
||||
|
||||
func newTestLaneHostInfo(base *HostInfo, laneIndex uint16, localIdx, remoteIdx uint32, owned bool) *HostInfo {
|
||||
lane := &HostInfo{
|
||||
vpnAddrs: base.vpnAddrs,
|
||||
localIndexId: localIdx,
|
||||
remoteIndexId: remoteIdx,
|
||||
remotes: NewRemoteList(base.vpnAddrs, nil),
|
||||
HandshakePacket: map[uint8][]byte{},
|
||||
sockIdx: int(laneIndex),
|
||||
laneIndex: laneIndex,
|
||||
laneOwned: owned,
|
||||
parent: base,
|
||||
}
|
||||
lane.SetRemote(netip.MustParseAddrPort("192.0.2.1:4243"))
|
||||
return lane
|
||||
}
|
||||
|
||||
func TestLaneHostmapLifecycle(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hostMap := newHostMap(l)
|
||||
ifce := &Interface{l: l} // connectionManager nil is tolerated by unlockedAddLane
|
||||
|
||||
vpnIp := netip.MustParseAddr("172.1.1.2")
|
||||
base := newTestBaseHostInfo(vpnIp, 100, 200, 4)
|
||||
|
||||
hostMap.Lock()
|
||||
hostMap.unlockedAddHostInfo(base, ifce)
|
||||
hostMap.Unlock()
|
||||
|
||||
lane := newTestLaneHostInfo(base, 1, 101, 201, true)
|
||||
hostMap.Lock()
|
||||
hostMap.unlockedAddLane(lane, ifce)
|
||||
hostMap.Unlock()
|
||||
base.lanes.txLanes[1].Store(lane)
|
||||
|
||||
// The lane is reachable by index (RX demux, recv_error) but never a Hosts primary.
|
||||
assert.Equal(t, lane, hostMap.QueryIndex(101))
|
||||
assert.Equal(t, lane, hostMap.QueryReverseIndex(201))
|
||||
assert.Equal(t, base, hostMap.Hosts[vpnIp])
|
||||
|
||||
// A lane can never be promoted to primary.
|
||||
hostMap.Lock()
|
||||
assert.False(t, hostMap.unlockedMakePrimary(lane))
|
||||
hostMap.Unlock()
|
||||
assert.Equal(t, base, hostMap.Hosts[vpnIp])
|
||||
|
||||
// Deleting the lane clears only its slot, applies backoff, and never
|
||||
// reports "no more tunnels to peer" (final).
|
||||
final := hostMap.DeleteHostInfo(lane)
|
||||
assert.False(t, final)
|
||||
assert.Nil(t, hostMap.QueryIndex(101))
|
||||
assert.Equal(t, base, hostMap.Hosts[vpnIp])
|
||||
assert.Nil(t, base.lanes.txLanes[1].Load())
|
||||
base.lanes.Lock()
|
||||
assert.Equal(t, uint8(1), base.lanes.txFails[1])
|
||||
assert.False(t, base.lanes.txPending[1])
|
||||
assert.True(t, base.lanes.txRetryAt[1].After(time.Now()))
|
||||
base.lanes.Unlock()
|
||||
|
||||
// Idempotent: deleting again must not bump the backoff further.
|
||||
hostMap.DeleteHostInfo(lane)
|
||||
base.lanes.Lock()
|
||||
assert.Equal(t, uint8(2), base.lanes.txFails[1]) // noteLaneFailure still runs, but slot CAS is a no-op
|
||||
base.lanes.Unlock()
|
||||
}
|
||||
|
||||
func TestLaneHostmapCascadeDelete(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hostMap := newHostMap(l)
|
||||
ifce := &Interface{l: l}
|
||||
|
||||
vpnIp := netip.MustParseAddr("172.1.1.3")
|
||||
base := newTestBaseHostInfo(vpnIp, 300, 400, 4)
|
||||
|
||||
hostMap.Lock()
|
||||
hostMap.unlockedAddHostInfo(base, ifce)
|
||||
hostMap.Unlock()
|
||||
|
||||
owned := newTestLaneHostInfo(base, 1, 301, 401, true)
|
||||
peer := newTestLaneHostInfo(base, 2, 302, 402, false)
|
||||
hostMap.Lock()
|
||||
hostMap.unlockedAddLane(owned, ifce)
|
||||
hostMap.unlockedAddLane(peer, ifce)
|
||||
hostMap.Unlock()
|
||||
base.lanes.txLanes[1].Store(owned)
|
||||
base.lanes.Lock()
|
||||
base.lanes.peerLanes = append(base.lanes.peerLanes, peer)
|
||||
base.lanes.Unlock()
|
||||
|
||||
// Deleting the base takes the whole lane family with it.
|
||||
final := hostMap.DeleteHostInfo(base)
|
||||
assert.True(t, final)
|
||||
assert.Nil(t, hostMap.QueryIndex(300))
|
||||
assert.Nil(t, hostMap.QueryIndex(301))
|
||||
assert.Nil(t, hostMap.QueryIndex(302))
|
||||
assert.Nil(t, hostMap.Hosts[vpnIp])
|
||||
}
|
||||
|
||||
// Regression: deleting a hostinfo whose pending entry is NOT the one recorded
|
||||
// in vpnIps (e.g. a lane, whose vpnAddrs alias the base's) must not evict a
|
||||
// concurrently pending base handshake for the same address.
|
||||
func TestHandshakeManagerVpnIpsIdentityDelete(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hostMap := newHostMap(l)
|
||||
lh := newTestLighthouse()
|
||||
hm := NewHandshakeManager(l, hostMap, lh, &udp.NoopConn{}, defaultHandshakeConfig)
|
||||
|
||||
vpnIp := netip.MustParseAddr("172.1.1.4")
|
||||
pendingBase := hm.StartHandshake(vpnIp, nil)
|
||||
require.NotNil(t, pendingBase)
|
||||
|
||||
other := &HostInfo{vpnAddrs: []netip.Addr{vpnIp}, localIndexId: 999}
|
||||
hm.DeleteHostInfo(other)
|
||||
|
||||
// The pending base handshake must still be tracked.
|
||||
assert.Equal(t, pendingBase, hm.QueryVpnAddr(vpnIp))
|
||||
|
||||
// And deleting the actual owner still works.
|
||||
hm.DeleteHostInfo(pendingBase)
|
||||
assert.Nil(t, hm.QueryVpnAddr(vpnIp))
|
||||
}
|
||||
|
||||
func newLaneTestConnectionManager(hostMap *HostMap) (*connectionManager, *Interface) {
|
||||
l := test.NewLogger()
|
||||
lh := newTestLighthouse()
|
||||
cs := &CertState{
|
||||
initiatingVersion: cert.Version1,
|
||||
privateKey: []byte{},
|
||||
v1Cert: &dummyCert{version: cert.Version1},
|
||||
v1Credential: nil,
|
||||
}
|
||||
ifce := &Interface{
|
||||
hostMap: hostMap,
|
||||
inside: &overlaytest.NoopTun{},
|
||||
outside: &udp.NoopConn{},
|
||||
firewall: &Firewall{},
|
||||
lightHouse: lh,
|
||||
pki: &PKI{},
|
||||
handshakeManager: NewHandshakeManager(l, hostMap, lh, &udp.NoopConn{}, defaultHandshakeConfig),
|
||||
myVpnNetworksTable: new(bart.Lite),
|
||||
l: l,
|
||||
}
|
||||
ifce.pki.cs.Store(cs)
|
||||
|
||||
conf := config.NewC(test.NewLogger())
|
||||
punchy := NewPunchyFromConfig(test.NewLogger(), conf, nil)
|
||||
cm := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
|
||||
cm.intf = ifce
|
||||
ifce.connectionManager = cm
|
||||
ifce.handshakeManager.f = ifce
|
||||
return cm, ifce
|
||||
}
|
||||
|
||||
func TestLaneTrafficDecision(t *testing.T) {
|
||||
hostMap := newHostMap(test.NewLogger())
|
||||
cm, ifce := newLaneTestConnectionManager(hostMap)
|
||||
|
||||
vpnIp := netip.MustParseAddr("172.1.1.5")
|
||||
base := newTestBaseHostInfo(vpnIp, 500, 600, 4)
|
||||
base.ConnectionState = &ConnectionState{}
|
||||
hostMap.Lock()
|
||||
hostMap.unlockedAddHostInfo(base, ifce)
|
||||
hostMap.Unlock()
|
||||
|
||||
lane := newTestLaneHostInfo(base, 1, 501, 601, true)
|
||||
lane.ConnectionState = &ConnectionState{}
|
||||
hostMap.Lock()
|
||||
hostMap.unlockedAddLane(lane, ifce)
|
||||
hostMap.Unlock()
|
||||
base.lanes.txLanes[1].Store(lane)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// A lane with inbound traffic is alive and never swaps primary or
|
||||
// migrates relays.
|
||||
lane.in.Store(true)
|
||||
decision, resolved, _ := cm.makeTrafficDecision(lane.localIndexId, now)
|
||||
assert.Equal(t, doNothing, decision)
|
||||
assert.Equal(t, lane, resolved)
|
||||
assert.False(t, lane.pendingDeletion.Load())
|
||||
|
||||
// An idle lane gets an active keepalive test...
|
||||
decision, _, _ = cm.makeTrafficDecision(lane.localIndexId, now)
|
||||
assert.Equal(t, sendTestPacket, decision)
|
||||
assert.True(t, lane.pendingDeletion.Load())
|
||||
|
||||
// ...and is declared dead when the test goes unanswered.
|
||||
decision, _, _ = cm.makeTrafficDecision(lane.localIndexId, now)
|
||||
assert.Equal(t, deleteTunnel, decision)
|
||||
}
|
||||
|
||||
func TestBaseInactiveConsidersLanes(t *testing.T) {
|
||||
hostMap := newHostMap(test.NewLogger())
|
||||
cm, _ := newLaneTestConnectionManager(hostMap)
|
||||
cm.dropInactive.Store(true)
|
||||
cm.inactivityTimeout.Store(int64(10 * time.Minute))
|
||||
|
||||
now := time.Now()
|
||||
vpnIp := netip.MustParseAddr("172.1.1.6")
|
||||
base := newTestBaseHostInfo(vpnIp, 700, 800, 4)
|
||||
base.lastUsed = now.Add(-time.Hour)
|
||||
|
||||
// Base alone: inactive.
|
||||
_, inactive := cm.isInactive(base, now)
|
||||
assert.True(t, inactive)
|
||||
|
||||
// A recently used lane keeps the base alive.
|
||||
lane := newTestLaneHostInfo(base, 1, 701, 801, true)
|
||||
lane.lastUsed = now.Add(-time.Minute)
|
||||
base.lanes.txLanes[1].Store(lane)
|
||||
_, inactive = cm.isInactive(base, now)
|
||||
assert.False(t, inactive)
|
||||
|
||||
// Peer-owned lanes count too.
|
||||
base.lanes.txLanes[1].Store(nil)
|
||||
base.lanes.Lock()
|
||||
base.lanes.peerLanes = append(base.lanes.peerLanes, lane)
|
||||
base.lanes.Unlock()
|
||||
_, inactive = cm.isInactive(base, now)
|
||||
assert.False(t, inactive)
|
||||
}
|
||||
|
||||
// recordingBatchWriter satisfies batch's writer interface and records what
|
||||
// was flushed to it.
|
||||
type recordingBatchWriter struct {
|
||||
bufs [][]byte
|
||||
dsts []netip.AddrPort
|
||||
}
|
||||
|
||||
func (w *recordingBatchWriter) WriteBatch(bufs [][]byte, addrs []netip.AddrPort, outerECNs []byte) error {
|
||||
for i := range bufs {
|
||||
w.bufs = append(w.bufs, append([]byte(nil), bufs[i]...))
|
||||
w.dsts = append(w.dsts, addrs[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSendInsideMessageLaneSwap(t *testing.T) {
|
||||
hostMap := newHostMap(test.NewLogger())
|
||||
cm, ifce := newLaneTestConnectionManager(hostMap)
|
||||
_ = cm
|
||||
|
||||
vpnIp := netip.MustParseAddr("172.1.1.7")
|
||||
base := newTestBaseHostInfo(vpnIp, 900, 1000, 4)
|
||||
lane := newTestLaneHostInfo(base, 1, 901, 1001, true)
|
||||
|
||||
// Real cipher states from a real handshake so encryption works.
|
||||
baseInit, _ := runTestHandshake(t)
|
||||
laneInit, _ := runTestHandshake(t)
|
||||
base.ConnectionState = newConnectionStateFromResult(baseInit)
|
||||
lane.ConnectionState = newConnectionStateFromResult(laneInit)
|
||||
|
||||
baseWriter := &recordingBatchWriter{}
|
||||
laneWriter := &recordingBatchWriter{}
|
||||
tx := &txQueue{
|
||||
base: batch.NewSendBatch(baseWriter, batch.SendBatchCap, 1<<16),
|
||||
lane: batch.NewSendBatch(laneWriter, batch.SendBatchCap, 1<<16),
|
||||
}
|
||||
|
||||
pkt := tio.Packet{Bytes: []byte{0x45, 0, 0, 4, 1, 2, 3, 4}}
|
||||
nb := make([]byte, 12)
|
||||
|
||||
// With the lane published, routine 1's traffic uses the lane session and
|
||||
// the lane batch.
|
||||
base.lanes.txLanes[1].Store(lane)
|
||||
ifce.sendInsideMessage(base, pkt, nb, tx, 1)
|
||||
tx.flush(ifce.l, 1)
|
||||
require.Len(t, laneWriter.bufs, 1)
|
||||
require.Empty(t, baseWriter.bufs)
|
||||
assert.Equal(t, lane.GetRemote(), laneWriter.dsts[0])
|
||||
|
||||
h := &header.H{}
|
||||
require.NoError(t, h.Parse(laneWriter.bufs[0]))
|
||||
assert.Equal(t, lane.remoteIndexId, h.RemoteIndex)
|
||||
|
||||
// Routine 2 has no lane: base tunnel, base batch.
|
||||
ifce.sendInsideMessage(base, pkt, nb, tx, 2)
|
||||
tx.flush(ifce.l, 1)
|
||||
require.Len(t, baseWriter.bufs, 1)
|
||||
assert.Equal(t, base.GetRemote(), baseWriter.dsts[0])
|
||||
require.NoError(t, h.Parse(baseWriter.bufs[0]))
|
||||
assert.Equal(t, base.remoteIndexId, h.RemoteIndex)
|
||||
|
||||
// Lane death: slot cleared, instant fallback to base.
|
||||
base.lanes.txLanes[1].Store(nil)
|
||||
ifce.sendInsideMessage(base, pkt, nb, tx, 1)
|
||||
tx.flush(ifce.l, 1)
|
||||
require.Len(t, baseWriter.bufs, 2)
|
||||
require.Len(t, laneWriter.bufs, 1)
|
||||
}
|
||||
|
||||
func TestCompleteLaneResponder(t *testing.T) {
|
||||
hostMap := newHostMap(test.NewLogger())
|
||||
_, ifce := newLaneTestConnectionManager(hostMap)
|
||||
ifce.writers = []udp.Conn{&udp.NoopConn{}, &udp.NoopConn{}, &udp.NoopConn{}, &udp.NoopConn{}}
|
||||
ifce.messageMetrics = newMessageMetricsOnlyRecvError()
|
||||
|
||||
hm := ifce.handshakeManager
|
||||
hm.config.laneCount = 4
|
||||
hm.config.lanePortCount = 4
|
||||
hm.config.laneBasePort = 4242
|
||||
|
||||
// A real handshake supplies usable keys and a peer cert.
|
||||
_, respR := runTestHandshake(t)
|
||||
respR.PeerLaneIndex = 2
|
||||
respR.PeerPortCount = 4
|
||||
respR.PeerBasePort = 5353
|
||||
|
||||
via := ViaSender{UdpAddr: netip.MustParseAddrPort("192.0.2.9:5355"), SockIdx: 2}
|
||||
packet := make([]byte, header.Len+8)
|
||||
copy(packet[header.Len:], []byte("stage0!!"))
|
||||
vpnAddrs := []netip.Addr{netip.MustParseAddr("172.1.1.9")}
|
||||
|
||||
// No base tunnel: the lane handshake is dropped, nothing registered.
|
||||
hm.completeLaneResponder(via, packet, []byte("resp"), respR, vpnAddrs)
|
||||
assert.Nil(t, hostMap.QueryIndex(respR.LocalIndex))
|
||||
|
||||
// With a live base the lane attaches to it.
|
||||
base := newTestBaseHostInfo(vpnAddrs[0], 1300, 1400, 4)
|
||||
base.ConnectionState = &ConnectionState{}
|
||||
hostMap.Lock()
|
||||
hostMap.unlockedAddHostInfo(base, ifce)
|
||||
hostMap.Unlock()
|
||||
|
||||
hm.completeLaneResponder(via, packet, []byte("resp"), respR, vpnAddrs)
|
||||
lane := hostMap.QueryIndex(respR.LocalIndex)
|
||||
require.NotNil(t, lane)
|
||||
assert.True(t, lane.isLane())
|
||||
assert.False(t, lane.laneOwned)
|
||||
assert.Equal(t, uint16(2), lane.laneIndex)
|
||||
assert.Equal(t, 2, lane.sockIdx)
|
||||
assert.Equal(t, via.UdpAddr, lane.GetRemote())
|
||||
assert.Equal(t, base, hostMap.Hosts[vpnAddrs[0]], "lane must not displace the base as primary")
|
||||
base.lanes.Lock()
|
||||
assert.Len(t, base.lanes.peerLanes, 1)
|
||||
base.lanes.Unlock()
|
||||
|
||||
// A byte-identical stage-0 retransmit resends the cached response and
|
||||
// must not register a second lane.
|
||||
hm.completeLaneResponder(via, packet, []byte("resp"), respR, vpnAddrs)
|
||||
base.lanes.Lock()
|
||||
assert.Len(t, base.lanes.peerLanes, 1)
|
||||
base.lanes.Unlock()
|
||||
|
||||
// An out-of-range lane index is refused.
|
||||
respR2 := *respR
|
||||
respR2.PeerLaneIndex = 9
|
||||
respR2.LocalIndex = respR.LocalIndex + 1
|
||||
hm.completeLaneResponder(via, packet, []byte("resp"), &respR2, vpnAddrs)
|
||||
assert.Nil(t, hostMap.QueryIndex(respR2.LocalIndex))
|
||||
}
|
||||
|
||||
func TestEnsureLanesBackoffOnStage0Failure(t *testing.T) {
|
||||
hostMap := newHostMap(test.NewLogger())
|
||||
_, ifce := newLaneTestConnectionManager(hostMap)
|
||||
|
||||
// laneCount enables multiport in the manager; the dummy CertState has no
|
||||
// credential, so stage-0 construction must fail and release the slot with
|
||||
// backoff rather than leaving it claimed forever.
|
||||
hm := ifce.handshakeManager
|
||||
hm.config.laneCount = 4
|
||||
hm.config.lanePortCount = 4
|
||||
hm.config.laneBasePort = 4242
|
||||
|
||||
vpnIp := netip.MustParseAddr("172.1.1.8")
|
||||
base := newTestBaseHostInfo(vpnIp, 1100, 1200, 4)
|
||||
|
||||
hm.EnsureLanes(base)
|
||||
|
||||
base.lanes.Lock()
|
||||
defer base.lanes.Unlock()
|
||||
for i := 1; i < 4; i++ {
|
||||
assert.False(t, base.lanes.txPending[i], "slot %d still pending", i)
|
||||
assert.Equal(t, uint8(1), base.lanes.txFails[i], "slot %d fails", i)
|
||||
assert.True(t, base.lanes.txRetryAt[i].After(time.Now()), "slot %d retryAt", i)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user