mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 12:06:52 +01:00
smoke-extra / freebsd-amd64 (push) Failing after 23s
smoke-extra / linux-amd64-ipv6disable (push) Failing after 14s
smoke-extra / netbsd-amd64 (push) Failing after 12s
smoke-extra / openbsd-amd64 (push) Failing after 11s
smoke-extra / linux-386 (push) Failing after 13s
smoke / Run multi node smoke test (push) Failing after 1m42s
Build and test / Static checks (push) Successful in 2m15s
Build and test / Test linux (push) Failing after 2m17s
Build and test / Test linux-pkcs11 (push) Failing after 2m10s
Build and test / Test linux-boringcrypto (push) Failing after 2m58s
Build and test / Test linux-fips140 (push) Failing after 2m54s
Build and test / Cross-build linux-arm (push) Successful in 3m6s
Build and test / Cross-build linux-mips (push) Successful in 3m46s
Build and test / Cross-build linux-other (push) Successful in 3m10s
Build and test / Cross-build windows (push) Successful in 1m3s
Build and test / Cross-build freebsd (push) Successful in 1m33s
Build and test / Cross-build netbsd (push) Successful in 1m34s
Build and test / Cross-build openbsd (push) Successful in 1m33s
Build and test / Cross-build mobile (push) Successful in 3m28s
smoke-extra / Run windows smoke test (push) Canceled after 0s
smoke / Run self traffic smoke test on macOS (push) Canceled after 0s
Build and test / Test macos (push) Canceled after 0s
Build and test / Test windows (push) Canceled after 0s
Build and test / CI status (push) Canceled after 0s
392 lines
13 KiB
Go
392 lines
13 KiB
Go
package nebula
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/netip"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/flynn/noise"
|
|
"github.com/gaissmai/bart"
|
|
"github.com/rcrowley/go-metrics"
|
|
"github.com/slackhq/nebula/cert"
|
|
ct "github.com/slackhq/nebula/cert_test"
|
|
"github.com/slackhq/nebula/handshake"
|
|
"github.com/slackhq/nebula/header"
|
|
"github.com/slackhq/nebula/test"
|
|
"github.com/slackhq/nebula/udp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func Test_NewHandshakeManagerVpnIp(t *testing.T) {
|
|
l := test.NewLogger()
|
|
localrange := netip.MustParsePrefix("10.1.1.1/24")
|
|
ip := netip.MustParseAddr("172.1.1.2")
|
|
|
|
preferredRanges := []netip.Prefix{localrange}
|
|
mainHM := newHostMap(l)
|
|
mainHM.preferredRanges.Store(&preferredRanges)
|
|
|
|
lh := newTestLighthouse()
|
|
|
|
cs := &CertState{
|
|
initiatingVersion: cert.Version1,
|
|
privateKey: []byte{},
|
|
v1Cert: &dummyCert{version: cert.Version1},
|
|
v1Credential: nil,
|
|
}
|
|
|
|
blah := NewHandshakeManager(l, mainHM, lh, &udp.NoopConn{}, defaultHandshakeConfig)
|
|
blah.f = &Interface{handshakeManager: blah, pki: &PKI{}, l: l}
|
|
blah.f.pki.cs.Store(cs)
|
|
|
|
now := time.Now()
|
|
blah.NextOutboundHandshakeTimerTick(now)
|
|
|
|
i := blah.StartHandshake(ip, nil)
|
|
i2 := blah.StartHandshake(ip, nil)
|
|
assert.Same(t, i, i2)
|
|
|
|
// Adding something to pending should not affect the main hostmap
|
|
assert.Empty(t, mainHM.Hosts)
|
|
|
|
// Confirm they are in the pending index list
|
|
assert.Contains(t, blah.vpnIps, ip)
|
|
|
|
// Jump ahead `HandshakeRetries` ticks, offset by one to get the sleep logic right
|
|
for i := 1; i <= DefaultHandshakeRetries+1; i++ {
|
|
now = now.Add(time.Duration(i) * DefaultHandshakeTryInterval)
|
|
blah.NextOutboundHandshakeTimerTick(now)
|
|
}
|
|
|
|
// Confirm they are still in the pending index list
|
|
assert.Contains(t, blah.vpnIps, ip)
|
|
|
|
// Tick 1 more time, a minute will certainly flush it out
|
|
blah.NextOutboundHandshakeTimerTick(now.Add(time.Minute))
|
|
|
|
// Confirm they have been removed
|
|
assert.NotContains(t, blah.vpnIps, ip)
|
|
}
|
|
|
|
func testCountTimerWheelEntries(tw *LockingTimerWheel[netip.Addr]) (c int) {
|
|
for _, i := range tw.t.wheel {
|
|
n := i.Head
|
|
for n != nil {
|
|
c++
|
|
n = n.Next
|
|
}
|
|
}
|
|
return c
|
|
}
|
|
|
|
type mockEncWriter struct {
|
|
}
|
|
|
|
func (mw *mockEncWriter) SendMessageToVpnAddr(_ header.MessageType, _ header.MessageSubType, _ netip.Addr, _, _, _ []byte) {
|
|
return
|
|
}
|
|
|
|
func (mw *mockEncWriter) SendVia(via *HostInfo, relay *Relay, ad, nb, out []byte, nocopy bool, q int) {
|
|
return
|
|
}
|
|
|
|
func (mw *mockEncWriter) SendMessageToHostInfo(_ header.MessageType, _ header.MessageSubType, _ *HostInfo, _, _, _ []byte) {
|
|
return
|
|
}
|
|
|
|
func (mw *mockEncWriter) Handshake(_ netip.Addr) {}
|
|
|
|
func (mw *mockEncWriter) GetHostInfo(_ netip.Addr) *HostInfo {
|
|
return nil
|
|
}
|
|
|
|
func (mw *mockEncWriter) GetCertState() *CertState {
|
|
return &CertState{initiatingVersion: cert.Version2}
|
|
}
|
|
|
|
func TestValidatePeerCert(t *testing.T) {
|
|
l := test.NewLogger()
|
|
|
|
myNetwork := netip.MustParsePrefix("10.0.0.1/24")
|
|
myAddrTable := new(bart.Lite)
|
|
myAddrTable.Insert(netip.PrefixFrom(myNetwork.Addr(), myNetwork.Addr().BitLen()))
|
|
myNetTable := new(bart.Lite)
|
|
myNetTable.Insert(myNetwork.Masked())
|
|
|
|
newHM := func() *HandshakeManager {
|
|
hm := NewHandshakeManager(l, newHostMap(l), newTestLighthouse(), &udp.NoopConn{}, defaultHandshakeConfig)
|
|
hm.f = &Interface{
|
|
handshakeManager: hm,
|
|
pki: &PKI{},
|
|
l: l,
|
|
myVpnAddrsTable: myAddrTable,
|
|
myVpnNetworksTable: myNetTable,
|
|
lightHouse: hm.lightHouse,
|
|
}
|
|
return hm
|
|
}
|
|
|
|
cached := func(networks ...netip.Prefix) *cert.CachedCertificate {
|
|
return &cert.CachedCertificate{
|
|
Certificate: &dummyCert{name: "peer", networks: networks},
|
|
}
|
|
}
|
|
|
|
via := ViaSender{
|
|
UdpAddr: netip.MustParseAddrPort("198.51.100.7:4242"),
|
|
IsRelayed: true, // skip the remote allow list (covered separately)
|
|
}
|
|
|
|
t.Run("addr inside our networks sets anyVpnAddrsInCommon", func(t *testing.T) {
|
|
hm := newHM()
|
|
// 10.0.0.2 falls inside our 10.0.0.0/24
|
|
addrs, common, ok := hm.validatePeerCert(via, cached(netip.MustParsePrefix("10.0.0.2/24")))
|
|
assert.True(t, ok)
|
|
assert.True(t, common)
|
|
assert.Equal(t, []netip.Addr{netip.MustParseAddr("10.0.0.2")}, addrs)
|
|
})
|
|
|
|
t.Run("addr outside our networks leaves anyVpnAddrsInCommon false", func(t *testing.T) {
|
|
hm := newHM()
|
|
addrs, common, ok := hm.validatePeerCert(via, cached(netip.MustParsePrefix("192.168.1.5/24")))
|
|
assert.True(t, ok)
|
|
assert.False(t, common)
|
|
assert.Equal(t, []netip.Addr{netip.MustParseAddr("192.168.1.5")}, addrs)
|
|
})
|
|
|
|
t.Run("any matching network is enough", func(t *testing.T) {
|
|
hm := newHM()
|
|
addrs, common, ok := hm.validatePeerCert(via, cached(
|
|
netip.MustParsePrefix("192.168.1.5/24"),
|
|
netip.MustParsePrefix("10.0.0.42/24"),
|
|
))
|
|
assert.True(t, ok)
|
|
assert.True(t, common)
|
|
assert.Len(t, addrs, 2)
|
|
})
|
|
|
|
t.Run("self-handshake is rejected", func(t *testing.T) {
|
|
hm := newHM()
|
|
// 10.0.0.1 is in myVpnAddrsTable
|
|
addrs, common, ok := hm.validatePeerCert(via, cached(netip.MustParsePrefix("10.0.0.1/24")))
|
|
assert.False(t, ok)
|
|
assert.False(t, common)
|
|
assert.Nil(t, addrs)
|
|
})
|
|
|
|
t.Run("cert with no networks is rejected", func(t *testing.T) {
|
|
hm := newHM()
|
|
addrs, common, ok := hm.validatePeerCert(via, cached())
|
|
assert.False(t, ok)
|
|
assert.False(t, common)
|
|
assert.Nil(t, addrs)
|
|
})
|
|
}
|
|
|
|
func TestHandleIncomingDispatch(t *testing.T) {
|
|
l := test.NewLogger()
|
|
|
|
newHM := func() *HandshakeManager {
|
|
hm := NewHandshakeManager(l, newHostMap(l), newTestLighthouse(), &udp.NoopConn{}, defaultHandshakeConfig)
|
|
hm.f = &Interface{
|
|
handshakeManager: hm,
|
|
pki: &PKI{},
|
|
l: l,
|
|
}
|
|
return hm
|
|
}
|
|
|
|
via := ViaSender{
|
|
UdpAddr: netip.MustParseAddrPort("198.51.100.7:4242"),
|
|
IsRelayed: true, // bypass remote allow list
|
|
}
|
|
|
|
// A packet body of zero length is fine for these tests: dispatch is
|
|
// gated on header fields, and we assert that we never reach noise/cert
|
|
// processing for any of the malformed shapes here.
|
|
pkt := make([]byte, header.Len)
|
|
|
|
t.Run("unsupported subtype dropped", func(t *testing.T) {
|
|
hm := newHM()
|
|
h := &header.H{Type: header.Handshake, Subtype: header.MessageSubType(99), MessageCounter: 1}
|
|
hm.HandleIncoming(via, pkt, h)
|
|
assert.Empty(t, hm.indexes, "no pending handshake should be created")
|
|
})
|
|
|
|
t.Run("stage-1 with non-zero RemoteIndex dropped", func(t *testing.T) {
|
|
hm := newHM()
|
|
h := &header.H{
|
|
Type: header.Handshake,
|
|
Subtype: header.HandshakeIXPSK0,
|
|
RemoteIndex: 0xdeadbeef,
|
|
MessageCounter: 1,
|
|
}
|
|
hm.HandleIncoming(via, pkt, h)
|
|
assert.Empty(t, hm.indexes, "spoofed stage-1 must not create a pending machine")
|
|
})
|
|
|
|
t.Run("continuation with no matching pending index dropped", func(t *testing.T) {
|
|
hm := newHM()
|
|
h := &header.H{
|
|
Type: header.Handshake,
|
|
Subtype: header.HandshakeIXPSK0,
|
|
RemoteIndex: 0xcafef00d,
|
|
MessageCounter: 2,
|
|
}
|
|
hm.HandleIncoming(via, pkt, h)
|
|
assert.Empty(t, hm.indexes, "orphan stage-2 must not create state")
|
|
})
|
|
}
|
|
|
|
func TestBlocklistedHandshakeLogLevelAndCounter(t *testing.T) {
|
|
ca, _, caKey, _ := ct.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Time{}, time.Time{}, nil, nil, nil)
|
|
pool := ct.NewTestCAPool(ca)
|
|
|
|
newCredential := func(name, network string) (cert.Certificate, *handshake.Credential) {
|
|
c, _, rawKey, _ := ct.NewTestCert(cert.Version2, cert.Curve_CURVE25519, ca, caKey,
|
|
name, ca.NotBefore(), ca.NotAfter(), []netip.Prefix{netip.MustParsePrefix(network)}, nil, nil)
|
|
privateKey, _, _, err := cert.UnmarshalPrivateKeyFromPEM(rawKey)
|
|
require.NoError(t, err)
|
|
certBytes, err := c.MarshalForHandshakes()
|
|
require.NoError(t, err)
|
|
cipherSuite := noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, noise.HashSHA256)
|
|
return c, handshake.NewCredential(c, certBytes, privateKey, cipherSuite)
|
|
}
|
|
|
|
initiatorCert, initiatorCredential := newCredential("initiator", "10.0.0.1/24")
|
|
responderCert, responderCredential := newCredential("responder", "10.0.0.2/24")
|
|
fingerprint, err := initiatorCert.Fingerprint()
|
|
require.NoError(t, err)
|
|
pool.BlocklistFingerprint(fingerprint)
|
|
|
|
initiator, err := handshake.NewMachine(cert.Version2,
|
|
func(cert.Version) *handshake.Credential { return initiatorCredential },
|
|
func(c cert.Certificate) (*cert.CachedCertificate, error) {
|
|
return pool.VerifyCertificate(time.Now(), c)
|
|
},
|
|
func() (uint32, error) { return 1, nil }, true, header.HandshakeIXPSK0)
|
|
require.NoError(t, err)
|
|
packet, err := initiator.Initiate(nil)
|
|
require.NoError(t, err)
|
|
|
|
handler := &capturingHandler{}
|
|
logger := slog.New(handler)
|
|
pki := &PKI{}
|
|
pki.cs.Store(&CertState{initiatingVersion: cert.Version2, v2Credential: responderCredential})
|
|
pki.caPool.Store(pool)
|
|
hm := NewHandshakeManager(logger, nil, nil, nil, defaultHandshakeConfig)
|
|
hm.metricRejectedBlocklisted = metrics.NewCounter()
|
|
hm.f = &Interface{pki: pki, l: logger}
|
|
via := ViaSender{UdpAddr: netip.MustParseAddrPort("198.51.100.7:4242")}
|
|
|
|
hm.beginHandshake(via, packet, nil)
|
|
entry := findEntry(t, handler.entries, "Failed to process handshake packet")
|
|
assert.Equal(t, slog.LevelDebug, entry.Level)
|
|
require.ErrorIs(t, entry.Attrs["error"].(error), cert.ErrBlockListed)
|
|
assert.Equal(t, int64(1), hm.metricRejectedBlocklisted.Count())
|
|
|
|
handler.entries = nil
|
|
hm.beginHandshake(via, nil, nil)
|
|
entry = findEntry(t, handler.entries, "Failed to process handshake packet")
|
|
assert.Equal(t, slog.LevelError, entry.Level)
|
|
assert.Equal(t, int64(1), hm.metricRejectedBlocklisted.Count())
|
|
|
|
// The initiator sees the same rejection in continueHandshake, after a
|
|
// responder has accepted its first packet and sent a valid reply.
|
|
cleanPool := ct.NewTestCAPool(ca)
|
|
responder, err := handshake.NewMachine(cert.Version2,
|
|
func(cert.Version) *handshake.Credential { return responderCredential },
|
|
func(c cert.Certificate) (*cert.CachedCertificate, error) {
|
|
return cleanPool.VerifyCertificate(time.Now(), c)
|
|
},
|
|
func() (uint32, error) { return 2, nil }, false, header.HandshakeIXPSK0)
|
|
require.NoError(t, err)
|
|
response, _, err := responder.ProcessPacket(nil, packet)
|
|
require.NoError(t, err)
|
|
|
|
responderFingerprint, err := responderCert.Fingerprint()
|
|
require.NoError(t, err)
|
|
pool.BlocklistFingerprint(responderFingerprint)
|
|
|
|
initiatingHM := NewHandshakeManager(logger, nil, nil, nil, defaultHandshakeConfig)
|
|
initiatingHM.metricRejectedBlocklisted = metrics.NewCounter()
|
|
initiatingHM.f = &Interface{l: logger}
|
|
hh := &HandshakeHostInfo{
|
|
machine: initiator,
|
|
hostinfo: &HostInfo{localIndexId: 1, vpnAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.2")}},
|
|
}
|
|
initiatingHM.indexes[1] = hh
|
|
via.IsRelayed = true // skip the remote allow list before certificate validation
|
|
initiatingHM.continueHandshake(via, hh, response)
|
|
assert.Equal(t, int64(1), initiatingHM.metricRejectedBlocklisted.Count())
|
|
}
|
|
|
|
// Test_PendingHandshakeHasRemotes covers create-tunnel -address: a pending hostinfo must already
|
|
// carry its RemoteList when StartHandshake runs its callback, so SetRemote there, before the
|
|
// handshake loop has run, is safe and the address becomes one the handshake is sent to.
|
|
func Test_PendingHandshakeHasRemotes(t *testing.T) {
|
|
l := test.NewLogger()
|
|
mainHM := newHostMap(l)
|
|
preferredRanges := []netip.Prefix{}
|
|
mainHM.preferredRanges.Store(&preferredRanges)
|
|
|
|
hm := NewHandshakeManager(l, mainHM, newTestLighthouse(), &udp.NoopConn{}, defaultHandshakeConfig)
|
|
|
|
vpnIp := netip.MustParseAddr("172.1.1.2")
|
|
remote := netip.MustParseAddrPort("192.168.1.9:4242")
|
|
|
|
hi := hm.StartHandshake(vpnIp, func(hh *HandshakeHostInfo) {
|
|
require.NotNil(t, hh.hostinfo.remotes, "a pending handshake must never be without its RemoteList")
|
|
hh.hostinfo.SetRemote(remote)
|
|
})
|
|
assert.Contains(t, hi.remotes.CopyAddrs(preferredRanges), remote, "the handshake must be sent to the supplied remote")
|
|
assert.Equal(t, remote, hi.GetRemote())
|
|
|
|
// A RemoteList keeps one learned address per host, so a second remote replaces the
|
|
// first — the same behaviour change-remote has on an established tunnel.
|
|
other := netip.MustParseAddrPort("192.168.1.10:4242")
|
|
hi.SetRemote(other)
|
|
assert.Equal(t, []netip.AddrPort{other}, hi.remotes.CopyAddrs(preferredRanges))
|
|
assert.Equal(t, other, hi.GetRemote())
|
|
}
|
|
|
|
// newRemotesRaceFixture builds a handshake manager with an established tunnel to vpnAddr whose
|
|
// RemoteList is the lighthouse's cached list, the state every teardown-during-handshake race starts from.
|
|
func newRemotesRaceFixture(t *testing.T, vpnAddr netip.Addr) (*HandshakeManager, *Interface, *HostInfo) {
|
|
t.Helper()
|
|
l := test.NewLogger()
|
|
mainHM := newHostMap(l)
|
|
preferredRanges := []netip.Prefix{}
|
|
mainHM.preferredRanges.Store(&preferredRanges)
|
|
|
|
lh := newTestLighthouse()
|
|
hm := NewHandshakeManager(l, mainHM, lh, &udp.NoopConn{}, defaultHandshakeConfig)
|
|
f := &Interface{hostMap: mainHM, lightHouse: lh, handshakeManager: hm, l: l}
|
|
hm.f = f
|
|
|
|
established := &HostInfo{
|
|
vpnAddrs: []netip.Addr{vpnAddr},
|
|
localIndexId: 1,
|
|
remoteIndexId: 101,
|
|
remotes: lh.QueryCache([]netip.Addr{vpnAddr}),
|
|
HandshakePacket: map[uint8][]byte{},
|
|
}
|
|
mainHM.Lock()
|
|
mainHM.unlockedAddHostInfo(established, f)
|
|
mainHM.Unlock()
|
|
|
|
return hm, f, established
|
|
}
|
|
|
|
// With no handshake pending, tearing down the last tunnel still evicts the list.
|
|
func Test_CloseTunnelEvictsWithoutPendingHandshake(t *testing.T) {
|
|
vpnAddr := netip.MustParseAddr("172.1.1.2")
|
|
_, f, established := newRemotesRaceFixture(t, vpnAddr)
|
|
|
|
f.closeTunnel(established)
|
|
assert.NotContains(t, f.hostMap.Hosts, vpnAddr)
|
|
assert.NotContains(t, f.lightHouse.addrMap, vpnAddr)
|
|
}
|