mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 12:07:01 +02:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 459cfc6f83 | |||
| 86733864fe | |||
| ab736e4c6b | |||
| 5ecdd4eaa9 | |||
| 1b84bd0050 | |||
| 384610f81a | |||
| c1eea118f4 | |||
| 1e66c0d3ee | |||
| e5c0fdad8d | |||
| 7bd0bc285a | |||
| 942ee522e0 | |||
| 32149f3a93 | |||
| 19ad3bb904 | |||
| 647775d8c3 | |||
| abfeb502a8 | |||
| 0a953915bb | |||
| 6aa3363d85 | |||
| 95d98b1f4b | |||
| 6afca0f461 |
+32
-2
@@ -148,6 +148,9 @@ func MarshalSigningPublicKeyToPEM(curve Curve, b []byte) []byte {
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalPublicKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
|
||||
// consumed data or an error on failure. Only key-agreement (ECDH) public key banners are accepted.
|
||||
// Use UnmarshalSigningPublicKeyFromPEM for Ed25519/ECDSA banners.
|
||||
func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
k, r := pem.Decode(b)
|
||||
if k == nil {
|
||||
@@ -156,10 +159,10 @@ func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
var expectedLen int
|
||||
var curve Curve
|
||||
switch k.Type {
|
||||
case X25519PublicKeyBanner, Ed25519PublicKeyBanner:
|
||||
case X25519PublicKeyBanner:
|
||||
expectedLen = 32
|
||||
curve = Curve_CURVE25519
|
||||
case P256PublicKeyBanner, ECDSAP256PublicKeyBanner:
|
||||
case P256PublicKeyBanner:
|
||||
// Uncompressed
|
||||
expectedLen = 65
|
||||
curve = Curve_P256
|
||||
@@ -172,6 +175,33 @@ func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
return k.Bytes, r, curve, nil
|
||||
}
|
||||
|
||||
// UnmarshalSigningPublicKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
|
||||
// consumed data or an error on failure. Only Ed25519/ECDSA public key banners are accepted.
|
||||
// Use UnmarshalPublicKeyFromPEM for X25519/P256 (ECDH) banners.
|
||||
func UnmarshalSigningPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
k, r := pem.Decode(b)
|
||||
if k == nil {
|
||||
return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
|
||||
}
|
||||
var expectedLen int
|
||||
var curve Curve
|
||||
switch k.Type {
|
||||
case Ed25519PublicKeyBanner:
|
||||
expectedLen = 32
|
||||
curve = Curve_CURVE25519
|
||||
case ECDSAP256PublicKeyBanner:
|
||||
// Uncompressed
|
||||
expectedLen = 65
|
||||
curve = Curve_P256
|
||||
default:
|
||||
return nil, r, 0, fmt.Errorf("bytes did not contain a proper Ed25519/ECDSA public key banner")
|
||||
}
|
||||
if len(k.Bytes) != expectedLen {
|
||||
return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid %s public key", expectedLen, curve)
|
||||
}
|
||||
return k.Bytes, r, curve, nil
|
||||
}
|
||||
|
||||
func MarshalPrivateKeyToPEM(curve Curve, b []byte) []byte {
|
||||
switch curve {
|
||||
case Curve_CURVE25519:
|
||||
|
||||
+88
-68
@@ -255,60 +255,6 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
func TestUnmarshalPublicKeyFromPEM(t *testing.T) {
|
||||
t.Parallel()
|
||||
pubKey := []byte(`# A good key
|
||||
-----BEGIN NEBULA ED25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA ED25519 PUBLIC KEY-----
|
||||
`)
|
||||
shortKey := []byte(`# A short key
|
||||
-----BEGIN NEBULA ED25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
||||
-----END NEBULA ED25519 PUBLIC KEY-----
|
||||
`)
|
||||
invalidBanner := []byte(`# Invalid banner
|
||||
-----BEGIN NOT A NEBULA PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NOT A NEBULA PUBLIC KEY-----
|
||||
`)
|
||||
invalidPem := []byte(`# Not a valid PEM format
|
||||
-BEGIN NEBULA ED25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-END NEBULA ED25519 PUBLIC KEY-----`)
|
||||
|
||||
keyBundle := appendByteSlices(pubKey, shortKey, invalidBanner, invalidPem)
|
||||
|
||||
// Success test case
|
||||
k, rest, curve, err := UnmarshalPublicKeyFromPEM(keyBundle)
|
||||
assert.Len(t, k, 32)
|
||||
assert.Equal(t, Curve_CURVE25519, curve)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
|
||||
|
||||
// Fail due to short key
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, Curve_CURVE25519, curve)
|
||||
assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
|
||||
require.EqualError(t, err, "key was not 32 bytes, is invalid CURVE25519 public key")
|
||||
|
||||
// Fail due to invalid banner
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, Curve_CURVE25519, curve)
|
||||
require.EqualError(t, err, "bytes did not contain a proper public key banner")
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
|
||||
// Fail due to invalid PEM format, because
|
||||
// it's missing the requisite pre-encapsulation boundary.
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, Curve_CURVE25519, curve)
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
||||
}
|
||||
|
||||
func TestUnmarshalX25519PublicKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
pubKey := []byte(`# A good key
|
||||
-----BEGIN NEBULA X25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA X25519 PUBLIC KEY-----
|
||||
@@ -319,7 +265,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA P256 PUBLIC KEY-----
|
||||
`)
|
||||
oldPubP256Key := []byte(`# A good key
|
||||
signingKey := []byte(`# A signing key has the wrong scope for this function
|
||||
-----BEGIN NEBULA ECDSA P256 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||
@@ -340,44 +286,118 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-END NEBULA X25519 PUBLIC KEY-----`)
|
||||
|
||||
keyBundle := appendByteSlices(pubKey, pubP256Key, oldPubP256Key, shortKey, invalidBanner, invalidPem)
|
||||
keyBundle := appendByteSlices(pubKey, pubP256Key, signingKey, shortKey, invalidBanner, invalidPem)
|
||||
|
||||
// Success test case
|
||||
// X25519 key
|
||||
k, rest, curve, err := UnmarshalPublicKeyFromPEM(keyBundle)
|
||||
assert.Len(t, k, 32)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rest, appendByteSlices(pubP256Key, oldPubP256Key, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, rest, appendByteSlices(pubP256Key, signingKey, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, Curve_CURVE25519, curve)
|
||||
|
||||
// Success test case
|
||||
// P256 key
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Len(t, k, 65)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rest, appendByteSlices(oldPubP256Key, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, rest, appendByteSlices(signingKey, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, Curve_P256, curve)
|
||||
|
||||
// Success test case
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Len(t, k, 65)
|
||||
require.NoError(t, err)
|
||||
// Reject a signing public key (Ed25519/ECDSA banner)
|
||||
k, rest, _, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, Curve_P256, curve)
|
||||
require.EqualError(t, err, "bytes did not contain a proper public key banner")
|
||||
|
||||
// Fail due to short key
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
k, rest, _, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
|
||||
require.EqualError(t, err, "key was not 32 bytes, is invalid CURVE25519 public key")
|
||||
|
||||
// Fail due to invalid banner
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
k, rest, _, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
require.EqualError(t, err, "bytes did not contain a proper public key banner")
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
|
||||
// Fail due to invalid PEM format, because
|
||||
// it's missing the requisite pre-encapsulation boundary.
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
k, rest, _, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
||||
}
|
||||
|
||||
func TestUnmarshalSigningPublicKeyFromPEM(t *testing.T) {
|
||||
t.Parallel()
|
||||
pubKey := []byte(`# A good key
|
||||
-----BEGIN NEBULA ED25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA ED25519 PUBLIC KEY-----
|
||||
`)
|
||||
pubP256Key := []byte(`# A good key
|
||||
-----BEGIN NEBULA ECDSA P256 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA ECDSA P256 PUBLIC KEY-----
|
||||
`)
|
||||
ecdhKey := []byte(`# A key-agreement key has the wrong scope for this function
|
||||
-----BEGIN NEBULA X25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA X25519 PUBLIC KEY-----
|
||||
`)
|
||||
shortKey := []byte(`# A short key
|
||||
-----BEGIN NEBULA ED25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
||||
-----END NEBULA ED25519 PUBLIC KEY-----
|
||||
`)
|
||||
invalidBanner := []byte(`# Invalid banner
|
||||
-----BEGIN NOT A NEBULA PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NOT A NEBULA PUBLIC KEY-----
|
||||
`)
|
||||
invalidPem := []byte(`# Not a valid PEM format
|
||||
-BEGIN NEBULA ED25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-END NEBULA ED25519 PUBLIC KEY-----`)
|
||||
|
||||
keyBundle := appendByteSlices(pubKey, pubP256Key, ecdhKey, shortKey, invalidBanner, invalidPem)
|
||||
|
||||
// Ed25519 key
|
||||
k, rest, curve, err := UnmarshalSigningPublicKeyFromPEM(keyBundle)
|
||||
assert.Len(t, k, 32)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rest, appendByteSlices(pubP256Key, ecdhKey, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, Curve_CURVE25519, curve)
|
||||
|
||||
// ECDSA P256 key
|
||||
k, rest, curve, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||
assert.Len(t, k, 65)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rest, appendByteSlices(ecdhKey, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, Curve_P256, curve)
|
||||
|
||||
// Reject a key-agreement public key (X25519/P256 banner)
|
||||
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
|
||||
require.EqualError(t, err, "bytes did not contain a proper Ed25519/ECDSA public key banner")
|
||||
|
||||
// Fail due to short key
|
||||
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
|
||||
require.EqualError(t, err, "key was not 32 bytes, is invalid CURVE25519 public key")
|
||||
|
||||
// Fail due to invalid banner
|
||||
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
require.EqualError(t, err, "bytes did not contain a proper Ed25519/ECDSA public key banner")
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
|
||||
// Fail due to invalid PEM format, because
|
||||
// it's missing the requisite pre-encapsulation boundary.
|
||||
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
||||
|
||||
@@ -53,7 +53,12 @@ func main() {
|
||||
l := logging.NewLogger(os.Stdout)
|
||||
|
||||
if *serviceFlag != "" {
|
||||
if err := doService(configPath, configTest, Build, serviceFlag); err != nil {
|
||||
if *configTest {
|
||||
fmt.Println("-test is not supported with -service, run the config test without -service")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := doService(configPath, Build, serviceFlag); err != nil {
|
||||
l.Error("Service command failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -93,15 +98,14 @@ func main() {
|
||||
}
|
||||
|
||||
if !*configTest {
|
||||
wait, err := ctrl.Start()
|
||||
if err != nil {
|
||||
if err := ctrl.Start(); err != nil {
|
||||
util.LogWithContextIfNeeded("Error while running", err, l)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
go ctrl.ShutdownBlock()
|
||||
|
||||
if err := wait(); err != nil {
|
||||
if err := ctrl.Wait(); err != nil {
|
||||
l.Error("Nebula stopped due to fatal error", "error", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
"github.com/slackhq/nebula"
|
||||
@@ -14,7 +15,6 @@ var logger service.Logger
|
||||
|
||||
type program struct {
|
||||
configPath *string
|
||||
configTest *bool
|
||||
build string
|
||||
control *nebula.Control
|
||||
}
|
||||
@@ -40,22 +40,41 @@ func (p *program) Start(s service.Service) error {
|
||||
}
|
||||
})
|
||||
|
||||
p.control, err = nebula.Main(c, *p.configTest, Build, l, nil)
|
||||
p.control, err = nebula.Main(c, false, Build, l, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.control.Start()
|
||||
if err := p.control.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Nebula can stop itself on a fatal packet reader error, make sure to log it if it happens.
|
||||
go func() {
|
||||
if err := p.control.Wait(); err != nil {
|
||||
logger.Error(fmt.Sprintf("Nebula stopped due to fatal error: %v", err))
|
||||
os.Exit(2)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *program) Stop(s service.Service) error {
|
||||
logger.Info("Nebula service stopping.")
|
||||
if p.control == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.control.Stop()
|
||||
|
||||
// block until nebula has fully drained before reporting stopped.
|
||||
// error logging is handled by Start.
|
||||
_ = p.control.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func doService(configPath *string, configTest *bool, build string, serviceFlag *string) error {
|
||||
func doService(configPath *string, build string, serviceFlag *string) error {
|
||||
if *configPath == "" {
|
||||
p, err := config.DefaultPath()
|
||||
if err != nil {
|
||||
@@ -73,7 +92,6 @@ func doService(configPath *string, configTest *bool, build string, serviceFlag *
|
||||
|
||||
prg := &program{
|
||||
configPath: configPath,
|
||||
configTest: configTest,
|
||||
build: build,
|
||||
}
|
||||
|
||||
@@ -105,8 +123,9 @@ func doService(configPath *string, configTest *bool, build string, serviceFlag *
|
||||
switch *serviceFlag {
|
||||
case "run":
|
||||
if err := s.Run(); err != nil {
|
||||
// Route any errors to the system logger
|
||||
// Route any errors to the system logger and report the failure
|
||||
logger.Error(err)
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if err := service.Control(s, *serviceFlag); err != nil {
|
||||
|
||||
+2
-3
@@ -84,8 +84,7 @@ func main() {
|
||||
}
|
||||
|
||||
if !*configTest {
|
||||
wait, err := ctrl.Start()
|
||||
if err != nil {
|
||||
if err := ctrl.Start(); err != nil {
|
||||
util.LogWithContextIfNeeded("Error while running", err, l)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -93,7 +92,7 @@ func main() {
|
||||
go ctrl.ShutdownBlock()
|
||||
notifyReady(l)
|
||||
|
||||
if err := wait(); err != nil {
|
||||
if err := ctrl.Wait(); err != nil {
|
||||
l.Error("Nebula stopped due to fatal error", "error", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
+51
-25
@@ -69,29 +69,29 @@ type ControlHostInfo struct {
|
||||
}
|
||||
|
||||
// Start actually runs nebula, this is a nonblocking call.
|
||||
// The returned function blocks until nebula has fully stopped and returns the
|
||||
// first fatal reader error (if any). A nil error means nebula shut down
|
||||
// gracefully; a non-nil error means a reader hit an unexpected failure that
|
||||
// triggered the shutdown.
|
||||
func (c *Control) Start() (func() error, error) {
|
||||
// Use Wait to block until nebula has fully stopped and to learn whether a fatal reader error caused the shutdown.
|
||||
func (c *Control) Start() error {
|
||||
c.stateLock.Lock()
|
||||
defer c.stateLock.Unlock()
|
||||
switch c.state {
|
||||
case StateReady:
|
||||
//yay!
|
||||
case StateStopped, StateStopping:
|
||||
return nil, ErrAlreadyStopped
|
||||
return ErrAlreadyStopped
|
||||
case StateStarted:
|
||||
return nil, ErrAlreadyStarted
|
||||
return ErrAlreadyStarted
|
||||
default:
|
||||
return nil, ErrUnknownState
|
||||
return ErrUnknownState
|
||||
}
|
||||
|
||||
// Activate the interface
|
||||
err := c.f.activate()
|
||||
if err != nil {
|
||||
// Cancel before Close so a caller returning from Wait always observes a dead Context
|
||||
c.cancel()
|
||||
_ = c.f.Close()
|
||||
c.state = StateStopped
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
// Call all the delayed funcs that waited patiently for the interface to be created.
|
||||
@@ -114,13 +114,9 @@ func (c *Control) Start() (func() error, error) {
|
||||
c.f.triggerShutdown = c.Stop
|
||||
|
||||
// Start reading packets.
|
||||
out, err := c.f.run()
|
||||
if err != nil {
|
||||
c.state = StateStopped
|
||||
return nil, err
|
||||
}
|
||||
c.f.run()
|
||||
c.state = StateStarted
|
||||
return out, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Control) State() RunState {
|
||||
@@ -133,10 +129,26 @@ func (c *Control) Context() context.Context {
|
||||
return c.ctx
|
||||
}
|
||||
|
||||
// Stop is a non-blocking call that signals nebula to close all tunnels and shut down
|
||||
// Stop tears nebula down, closing all tunnels and releasing everything it holds.
|
||||
// Use Wait to block until the shutdown has completed.
|
||||
// A Control that has been stopped cannot be started again, Start will return ErrAlreadyStopped.
|
||||
func (c *Control) Stop() {
|
||||
c.stateLock.Lock()
|
||||
if c.state != StateStarted {
|
||||
switch c.state {
|
||||
case StateStarted:
|
||||
// Fall through to the full teardown below
|
||||
|
||||
case StateReady:
|
||||
// Never started
|
||||
c.cancel()
|
||||
c.state = StateStopped
|
||||
if err := c.f.Close(); err != nil {
|
||||
c.l.Error("Close interface failed", "error", err)
|
||||
}
|
||||
c.stateLock.Unlock()
|
||||
return
|
||||
|
||||
default:
|
||||
c.stateLock.Unlock()
|
||||
// We are stopping or stopped already
|
||||
return
|
||||
@@ -145,19 +157,26 @@ func (c *Control) Stop() {
|
||||
c.state = StateStopping
|
||||
c.stateLock.Unlock()
|
||||
|
||||
// Stop the handshakeManager (and other services), to prevent new tunnels from
|
||||
// being created while we're shutting them all down.
|
||||
// Closing tunnels can be slow with a large hostmap, don't hold the lock for it
|
||||
c.cancel()
|
||||
|
||||
c.CloseAllTunnels(false)
|
||||
|
||||
c.stateLock.Lock()
|
||||
c.state = StateStopped
|
||||
if err := c.f.Close(); err != nil {
|
||||
c.l.Error("Close interface failed", "error", err)
|
||||
}
|
||||
c.stateLock.Lock()
|
||||
c.state = StateStopped
|
||||
c.stateLock.Unlock()
|
||||
}
|
||||
|
||||
// Wait blocks until nebula has fully stopped, either via Stop or an internal fatal error,
|
||||
// and returns the first fatal packet reader error if there was one.
|
||||
// It is safe to call from multiple goroutines and at any point in the lifecycle,
|
||||
// but a Wait on a Control that is never started and never stopped will block forever.
|
||||
func (c *Control) Wait() error {
|
||||
return c.f.wait()
|
||||
}
|
||||
|
||||
// ShutdownBlock will listen for and block on term and interrupt signals, calling Control.Stop() once signalled
|
||||
func (c *Control) ShutdownBlock() {
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
@@ -170,8 +189,15 @@ func (c *Control) ShutdownBlock() {
|
||||
c.Stop()
|
||||
}
|
||||
|
||||
// RebindUDPServer asks the UDP listener to rebind it's listener. Mainly used on mobile clients when interfaces change
|
||||
// RebindUDPServer asks the UDP listener to rebind it's listener. Mainly used on mobile clients when interfaces change.
|
||||
func (c *Control) RebindUDPServer() {
|
||||
c.stateLock.Lock()
|
||||
defer c.stateLock.Unlock()
|
||||
|
||||
if c.state != StateStarted {
|
||||
return
|
||||
}
|
||||
|
||||
_ = c.f.outside.Rebind()
|
||||
|
||||
// Trigger a lighthouse update, useful for mobile clients that should have an update interval of 0
|
||||
@@ -305,7 +331,7 @@ func (c *Control) CloseAllTunnels(excludeLighthouses bool) (closed int) {
|
||||
|
||||
c.l.Debug("Sending close tunnel message",
|
||||
"vpnAddrs", h.vpnAddrs,
|
||||
"udpAddr", h.remote,
|
||||
"udpAddr", h.GetRemote(),
|
||||
)
|
||||
closed++
|
||||
}
|
||||
@@ -350,7 +376,7 @@ func copyHostInfo(h *HostInfo, preferredRanges []netip.Prefix) ControlHostInfo {
|
||||
RemoteAddrs: h.remotes.CopyAddrs(preferredRanges),
|
||||
CurrentRelaysToMe: h.relayState.CopyRelayIps(),
|
||||
CurrentRelaysThroughMe: h.relayState.CopyRelayForIps(),
|
||||
CurrentRemote: h.remote,
|
||||
CurrentRemote: h.GetRemote(),
|
||||
}
|
||||
|
||||
for i, a := range h.vpnAddrs {
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
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")
|
||||
}
|
||||
+161
-6
@@ -1,6 +1,8 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
@@ -9,6 +11,7 @@ import (
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
||||
@@ -42,8 +45,7 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
||||
assert.True(t, ok)
|
||||
|
||||
crt := &dummyCert{}
|
||||
hm.unlockedAddHostInfo(&HostInfo{
|
||||
remote: remote1,
|
||||
hi := &HostInfo{
|
||||
remotes: remotes,
|
||||
ConnectionState: &ConnectionState{
|
||||
peerCert: &cert.CachedCertificate{Certificate: crt},
|
||||
@@ -56,13 +58,14 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
||||
relayForByAddr: map[netip.Addr]*Relay{},
|
||||
relayForByIdx: map[uint32]*Relay{},
|
||||
},
|
||||
}, &Interface{})
|
||||
}
|
||||
hi.remote.Store(&remote1)
|
||||
hm.unlockedAddHostInfo(hi, &Interface{})
|
||||
|
||||
vpnIp2, ok := netip.AddrFromSlice(ipNet2.IP)
|
||||
assert.True(t, ok)
|
||||
|
||||
hm.unlockedAddHostInfo(&HostInfo{
|
||||
remote: remote1,
|
||||
hi2 := &HostInfo{
|
||||
remotes: remotes,
|
||||
ConnectionState: &ConnectionState{
|
||||
peerCert: nil,
|
||||
@@ -75,7 +78,9 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
||||
relayForByAddr: map[netip.Addr]*Relay{},
|
||||
relayForByIdx: map[uint32]*Relay{},
|
||||
},
|
||||
}, &Interface{})
|
||||
}
|
||||
hi2.remote.Store(&remote1)
|
||||
hm.unlockedAddHostInfo(hi2, &Interface{})
|
||||
|
||||
c := Control{
|
||||
state: StateReady,
|
||||
@@ -119,3 +124,153 @@ func assertFields(t *testing.T, expected []string, actualStruct any) {
|
||||
|
||||
assert.Equal(t, expected, fields)
|
||||
}
|
||||
|
||||
// alwaysAllowV4/V6 are check funcs that accept every entry (including nil pointers),
|
||||
// letting us inject a nil *V4AddrPort/*V6AddrPort into a RemoteList's reported cache
|
||||
// the same way a malformed proto message off the wire could.
|
||||
func alwaysAllowV4(netip.Addr, *V4AddrPort) bool { return true }
|
||||
func alwaysAllowV6(netip.Addr, *V6AddrPort) bool { return true }
|
||||
|
||||
// TestGetRelays_SkipsNilRelayAddrs proves GetRelays tolerates nil entries in the
|
||||
// RelayVpnAddrs proto slice (which protoAddrToNetAddr would nil-deref on) and still
|
||||
// returns the valid relays, including the legacy OldRelayVpnAddrs.
|
||||
func TestGetRelays_SkipsNilRelayAddrs(t *testing.T) {
|
||||
good := netip.MustParseAddr("10.0.0.9")
|
||||
|
||||
d := &NebulaMetaDetails{
|
||||
OldRelayVpnAddrs: []uint32{0x0a000001}, // 10.0.0.1
|
||||
RelayVpnAddrs: []*Addr{
|
||||
nil,
|
||||
netAddrToProtoAddr(good),
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
var relays []netip.Addr
|
||||
require.NotPanics(t, func() { relays = d.GetRelays() })
|
||||
|
||||
assert.Equal(t, []netip.Addr{
|
||||
netip.MustParseAddr("10.0.0.1"),
|
||||
good,
|
||||
}, relays)
|
||||
}
|
||||
|
||||
// TestGetRelays_AllNil ensures an all-nil RelayVpnAddrs slice yields no relays and no panic.
|
||||
func TestGetRelays_AllNil(t *testing.T) {
|
||||
d := &NebulaMetaDetails{RelayVpnAddrs: []*Addr{nil, nil}}
|
||||
var relays []netip.Addr
|
||||
require.NotPanics(t, func() { relays = d.GetRelays() })
|
||||
assert.Empty(t, relays)
|
||||
}
|
||||
|
||||
// TestRemoteList_CopyCache_SkipsNilReported proves CopyCache skips nil reported
|
||||
// pointers (v4 and v6) instead of nil-dereferencing them in protoV*AddrPortToNetAddrPort.
|
||||
func TestRemoteList_CopyCache_SkipsNilReported(t *testing.T) {
|
||||
owner := netip.MustParseAddr("10.0.0.1")
|
||||
rl := NewRemoteList([]netip.Addr{owner}, nil)
|
||||
|
||||
rl.unlockedSetV4(owner, owner, []*V4AddrPort{
|
||||
nil,
|
||||
newIp4AndPortFromString("1.2.3.4:5"),
|
||||
nil,
|
||||
}, alwaysAllowV4)
|
||||
|
||||
rl.unlockedSetV6(owner, owner, []*V6AddrPort{
|
||||
nil,
|
||||
newIp6AndPortFromString("[1::1]:6"),
|
||||
nil,
|
||||
}, alwaysAllowV6)
|
||||
|
||||
var cm *CacheMap
|
||||
require.NotPanics(t, func() { cm = rl.CopyCache() })
|
||||
|
||||
c := (*cm)[owner.String()]
|
||||
require.NotNil(t, c)
|
||||
assert.ElementsMatch(t, []netip.AddrPort{
|
||||
netip.MustParseAddrPort("1.2.3.4:5"),
|
||||
netip.MustParseAddrPort("[1::1]:6"),
|
||||
}, c.Reported)
|
||||
}
|
||||
|
||||
// TestRemoteList_Rebuild_SkipsNilReported drives unlockedCollect (via Rebuild) with
|
||||
// nil reported entries and confirms only the valid addresses survive, with no panic.
|
||||
func TestRemoteList_Rebuild_SkipsNilReported(t *testing.T) {
|
||||
owner := netip.MustParseAddr("10.0.0.1")
|
||||
rl := NewRemoteList([]netip.Addr{owner}, nil)
|
||||
|
||||
rl.unlockedSetV4(owner, owner, []*V4AddrPort{
|
||||
nil,
|
||||
newIp4AndPortFromString("1.2.3.4:5"),
|
||||
}, alwaysAllowV4)
|
||||
rl.unlockedSetV6(owner, owner, []*V6AddrPort{
|
||||
newIp6AndPortFromString("[1::1]:6"),
|
||||
nil,
|
||||
}, alwaysAllowV6)
|
||||
|
||||
require.NotPanics(t, func() { rl.Rebuild([]netip.Prefix{}) })
|
||||
|
||||
assert.ElementsMatch(t, []netip.AddrPort{
|
||||
netip.MustParseAddrPort("1.2.3.4:5"),
|
||||
netip.MustParseAddrPort("[1::1]:6"),
|
||||
}, rl.addrs)
|
||||
}
|
||||
|
||||
// newRelayControl marshals a NebulaControl the way it arrives on the wire so we can feed
|
||||
// it through HandleControlMsg's unmarshal + validate path.
|
||||
func newRelayControl(t *testing.T, typ NebulaControl_MessageType, from, to *Addr) []byte {
|
||||
t.Helper()
|
||||
msg := &NebulaControl{
|
||||
Type: typ,
|
||||
RelayFromAddr: from,
|
||||
RelayToAddr: to,
|
||||
}
|
||||
b, err := msg.Marshal()
|
||||
require.NoError(t, err)
|
||||
return b
|
||||
}
|
||||
|
||||
// TestRelayManager_HandleControlMsg_NilRelayAddrs verifies the validation block added to
|
||||
// HandleControlMsg: CreateRelay{Request,Response} carrying a nil RelayFromAddr or
|
||||
// RelayToAddr are dropped with a debug log rather than nil-dereferencing downstream.
|
||||
func TestRelayManager_HandleControlMsg_NilRelayAddrs(t *testing.T) {
|
||||
good := netAddrToProtoAddr(netip.MustParseAddr("10.0.0.9"))
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
typ NebulaControl_MessageType
|
||||
from *Addr
|
||||
to *Addr
|
||||
wantLog string // debug substring expected, "" == expect no drop log
|
||||
}{
|
||||
{"request nil from", NebulaControl_CreateRelayRequest, nil, good, "nil RelayFromAddr"},
|
||||
{"request nil to", NebulaControl_CreateRelayRequest, good, nil, "nil RelayToAddr"},
|
||||
{"request both nil", NebulaControl_CreateRelayRequest, nil, nil, "nil RelayFromAddr"},
|
||||
{"response nil from", NebulaControl_CreateRelayResponse, nil, good, "nil RelayFromAddr"},
|
||||
{"response nil to", NebulaControl_CreateRelayResponse, good, nil, "nil RelayToAddr"},
|
||||
// A non-relay control type is not subject to the relay-addr validation and must
|
||||
// pass through it untouched (the final switch simply no-ops on it).
|
||||
{"unrelated type nil addrs", NebulaControl_None, nil, nil, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
l := test.NewLoggerWithOutputAndLevel(&buf, slog.LevelDebug)
|
||||
rm := &relayManager{l: l, hostmap: newHostMap(l)}
|
||||
rm.useRelays.Store(true)
|
||||
|
||||
f := &Interface{l: l}
|
||||
h := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.2")}, localIndexId: 1}
|
||||
|
||||
d := newRelayControl(t, tc.typ, tc.from, tc.to)
|
||||
|
||||
require.NotPanics(t, func() { rm.HandleControlMsg(h, d, f) })
|
||||
|
||||
if tc.wantLog == "" {
|
||||
assert.NotContains(t, buf.String(), "nil Relay")
|
||||
} else {
|
||||
assert.Contains(t, buf.String(), tc.wantLog)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,14 @@ func (c *Control) GetHostmap() *HostMap {
|
||||
return c.f.hostMap
|
||||
}
|
||||
|
||||
// GetHostmapIndexCount returns the number of entries in the main hostmap Indexes table, holding
|
||||
// the hostmap read lock so tests can poll it while connection manager churns tunnels.
|
||||
func (c *Control) GetHostmapIndexCount() int {
|
||||
c.f.hostMap.RLock()
|
||||
defer c.f.hostMap.RUnlock()
|
||||
return len(c.f.hostMap.Indexes)
|
||||
}
|
||||
|
||||
func (c *Control) GetF() *Interface {
|
||||
return c.f
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//go:build e2e_testing
|
||||
// +build e2e_testing
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/cert_test"
|
||||
"github.com/slackhq/nebula/e2e/router"
|
||||
"github.com/slackhq/nebula/header"
|
||||
"github.com/slackhq/nebula/udp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func assertTestRequestEchoed(t *testing.T, cipher string) {
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
over := m{"cipher": cipher}
|
||||
a, aNet, aUdp, _ := newSimpleServer(cert.Version1, ca, caKey, "a", "10.128.0.1/24", over)
|
||||
b, bNet, bUdp, _ := newSimpleServer(cert.Version1, ca, caKey, "b", "10.128.0.2/24", over)
|
||||
|
||||
a.InjectLightHouseAddr(bNet[0].Addr(), bUdp)
|
||||
b.InjectLightHouseAddr(aNet[0].Addr(), aUdp)
|
||||
a.Start()
|
||||
b.Start()
|
||||
t.Cleanup(func() { a.Stop(); b.Stop() })
|
||||
r := router.NewR(t, a, b)
|
||||
defer r.RenderFlow()
|
||||
|
||||
assertTunnel(t, aNet[0].Addr(), bNet[0].Addr(), a, b, r)
|
||||
drainUDPTx(a)
|
||||
drainUDPTx(b)
|
||||
|
||||
payload := []byte("a test payload well over sixteen bytes long, wow it's so very long long long!")
|
||||
require.Greater(t, len(payload), header.Len)
|
||||
a.GetF().SendMessageToVpnAddr(header.Test, header.TestRequest, bNet[0].Addr(), payload, make([]byte, 12, 12), make([]byte, udp.MTU))
|
||||
|
||||
// Deliver A's request to B; B must echo a reply back
|
||||
b.InjectUDPPacket(a.GetFromUDP(true))
|
||||
reply := nextUDPTxOfType(t, b, header.Test, header.TestReply, 2*time.Second)
|
||||
|
||||
assert.Equal(t, aUdp, reply.To, "the reply must go back to the requester")
|
||||
// header + echoed payload + 16-byte AEAD tag: proves the whole payload
|
||||
// round-tripped rather than being dropped or truncated.
|
||||
assert.Equal(t, header.Len+len(payload)+16, len(reply.Data), "the full payload must be echoed back")
|
||||
}
|
||||
|
||||
func TestTestRequestEchoesLongPayloadAES(t *testing.T) {
|
||||
assertTestRequestEchoed(t, "aes")
|
||||
}
|
||||
|
||||
func TestTestRequestEchoesLongPayloadChaChaPoly(t *testing.T) {
|
||||
assertTestRequestEchoed(t, "chachapoly")
|
||||
}
|
||||
|
||||
// drainUDPTx empties a control's UDP tx queue without blocking.
|
||||
func drainUDPTx(c *nebula.Control) {
|
||||
for c.GetFromUDP(false) != nil {
|
||||
}
|
||||
}
|
||||
|
||||
// nextUDPTxOfType returns the next packet a control transmits whose nebula
|
||||
// header matches (wantType, wantSub), skipping unrelated packets.
|
||||
// It fails the test if none arrives within the timeout.
|
||||
func nextUDPTxOfType(t *testing.T, c *nebula.Control, wantType header.MessageType, wantSub header.MessageSubType, within time.Duration) *udp.Packet {
|
||||
t.Helper()
|
||||
ch := c.GetUDPTxChan()
|
||||
timeout := time.After(within)
|
||||
for {
|
||||
select {
|
||||
case p := <-ch:
|
||||
var h header.H
|
||||
if err := h.Parse(p.Data); err == nil && h.Type == wantType && h.Subtype == wantSub {
|
||||
return p
|
||||
}
|
||||
case <-timeout:
|
||||
t.Fatalf("timed out waiting for a %v/%v packet on the udp tx queue", wantType, wantSub)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
-30
@@ -405,7 +405,7 @@ func TestStage1Race(t *testing.T) {
|
||||
|
||||
r.Log("Spin until connection manager tears down a tunnel")
|
||||
|
||||
for len(myControl.GetHostmap().Indexes)+len(theirControl.GetHostmap().Indexes) > 2 {
|
||||
for myControl.GetHostmapIndexCount()+theirControl.GetHostmapIndexCount() > 2 {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
t.Log("Connection manager hasn't ticked yet")
|
||||
time.Sleep(time.Second)
|
||||
@@ -453,9 +453,11 @@ func TestUncleanShutdownRaceLoser(t *testing.T) {
|
||||
|
||||
r.Log("Nuke my hostmap")
|
||||
myHostmap := myControl.GetHostmap()
|
||||
myHostmap.Lock()
|
||||
myHostmap.Hosts = map[netip.Addr]*nebula.HostInfo{}
|
||||
myHostmap.Indexes = map[uint32]*nebula.HostInfo{}
|
||||
myHostmap.RemoteIndexes = map[uint32]*nebula.HostInfo{}
|
||||
myHostmap.Unlock()
|
||||
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me again")))
|
||||
p = r.RouteForAllUntilTxTun(theirControl)
|
||||
@@ -465,10 +467,10 @@ func TestUncleanShutdownRaceLoser(t *testing.T) {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
r.Log("Wait for the dead index to go away")
|
||||
start := len(theirControl.GetHostmap().Indexes)
|
||||
start := theirControl.GetHostmapIndexCount()
|
||||
for {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
if len(theirControl.GetHostmap().Indexes) < start {
|
||||
if theirControl.GetHostmapIndexCount() < start {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
@@ -504,9 +506,11 @@ func TestUncleanShutdownRaceWinner(t *testing.T) {
|
||||
|
||||
r.Log("Nuke my hostmap")
|
||||
theirHostmap := theirControl.GetHostmap()
|
||||
theirHostmap.Lock()
|
||||
theirHostmap.Hosts = map[netip.Addr]*nebula.HostInfo{}
|
||||
theirHostmap.Indexes = map[uint32]*nebula.HostInfo{}
|
||||
theirHostmap.RemoteIndexes = map[uint32]*nebula.HostInfo{}
|
||||
theirHostmap.Unlock()
|
||||
|
||||
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them again")))
|
||||
p = r.RouteForAllUntilTxTun(myControl)
|
||||
@@ -517,10 +521,10 @@ func TestUncleanShutdownRaceWinner(t *testing.T) {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
r.Log("Wait for the dead index to go away")
|
||||
start := len(myControl.GetHostmap().Indexes)
|
||||
start := myControl.GetHostmapIndexCount()
|
||||
for {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
if len(myControl.GetHostmap().Indexes) < start {
|
||||
if myControl.GetHostmapIndexCount() < start {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
@@ -628,10 +632,10 @@ func TestReestablishRelays(t *testing.T) {
|
||||
r.Log("Close the tunnel")
|
||||
relayControl.CloseTunnel(theirVpnIpNet[0].Addr(), true)
|
||||
|
||||
start := len(myControl.GetHostmap().Indexes)
|
||||
curIndexes := len(myControl.GetHostmap().Indexes)
|
||||
start := myControl.GetHostmapIndexCount()
|
||||
curIndexes := myControl.GetHostmapIndexCount()
|
||||
for curIndexes >= start {
|
||||
curIndexes = len(myControl.GetHostmap().Indexes)
|
||||
curIndexes = myControl.GetHostmapIndexCount()
|
||||
r.Logf("Wait for the dead index to go away:start=%v indexes, current=%v indexes", start, curIndexes)
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me should fail")))
|
||||
|
||||
@@ -819,18 +823,18 @@ func TestStage1RaceRelays2(t *testing.T) {
|
||||
|
||||
t.Log("Wait until we remove extra tunnels")
|
||||
t.Logf("Waiting for hostinfos to be removed... myControl=%d theirControl=%d relayControl=%d",
|
||||
len(myControl.GetHostmap().Indexes),
|
||||
len(theirControl.GetHostmap().Indexes),
|
||||
len(relayControl.GetHostmap().Indexes),
|
||||
myControl.GetHostmapIndexCount(),
|
||||
theirControl.GetHostmapIndexCount(),
|
||||
relayControl.GetHostmapIndexCount(),
|
||||
)
|
||||
hostInfos := len(myControl.GetHostmap().Indexes) + len(theirControl.GetHostmap().Indexes) + len(relayControl.GetHostmap().Indexes)
|
||||
hostInfos := myControl.GetHostmapIndexCount() + theirControl.GetHostmapIndexCount() + relayControl.GetHostmapIndexCount()
|
||||
retries := 60
|
||||
for hostInfos > 6 && retries > 0 {
|
||||
hostInfos = len(myControl.GetHostmap().Indexes) + len(theirControl.GetHostmap().Indexes) + len(relayControl.GetHostmap().Indexes)
|
||||
hostInfos = myControl.GetHostmapIndexCount() + theirControl.GetHostmapIndexCount() + relayControl.GetHostmapIndexCount()
|
||||
t.Logf("Waiting for hostinfos to be removed... myControl=%d theirControl=%d relayControl=%d",
|
||||
len(myControl.GetHostmap().Indexes),
|
||||
len(theirControl.GetHostmap().Indexes),
|
||||
len(relayControl.GetHostmap().Indexes),
|
||||
myControl.GetHostmapIndexCount(),
|
||||
theirControl.GetHostmapIndexCount(),
|
||||
relayControl.GetHostmapIndexCount(),
|
||||
)
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
t.Log("Connection manager hasn't ticked yet")
|
||||
@@ -924,24 +928,24 @@ func TestRehandshakingRelays(t *testing.T) {
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.RenderHostmaps("working hostmaps", myControl, relayControl, theirControl)
|
||||
// We should have two hostinfos on all sides
|
||||
for len(myControl.GetHostmap().Indexes) != 2 {
|
||||
t.Logf("Waiting for myControl hostinfos (%v != 2) to get cleaned up from lack of use...", len(myControl.GetHostmap().Indexes))
|
||||
for myControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for myControl hostinfos (%v != 2) to get cleaned up from lack of use...", myControl.GetHostmapIndexCount())
|
||||
r.Log("Assert the relay tunnel still works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.Log("yupitdoes")
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
t.Logf("myControl hostinfos got cleaned up!")
|
||||
for len(theirControl.GetHostmap().Indexes) != 2 {
|
||||
t.Logf("Waiting for theirControl hostinfos (%v != 2) to get cleaned up from lack of use...", len(theirControl.GetHostmap().Indexes))
|
||||
for theirControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for theirControl hostinfos (%v != 2) to get cleaned up from lack of use...", theirControl.GetHostmapIndexCount())
|
||||
r.Log("Assert the relay tunnel still works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.Log("yupitdoes")
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
t.Logf("theirControl hostinfos got cleaned up!")
|
||||
for len(relayControl.GetHostmap().Indexes) != 2 {
|
||||
t.Logf("Waiting for relayControl hostinfos (%v != 2) to get cleaned up from lack of use...", len(relayControl.GetHostmap().Indexes))
|
||||
for relayControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for relayControl hostinfos (%v != 2) to get cleaned up from lack of use...", relayControl.GetHostmapIndexCount())
|
||||
r.Log("Assert the relay tunnel still works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.Log("yupitdoes")
|
||||
@@ -1029,24 +1033,24 @@ func TestRehandshakingRelaysPrimary(t *testing.T) {
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.RenderHostmaps("working hostmaps", myControl, relayControl, theirControl)
|
||||
// We should have two hostinfos on all sides
|
||||
for len(myControl.GetHostmap().Indexes) != 2 {
|
||||
t.Logf("Waiting for myControl hostinfos (%v != 2) to get cleaned up from lack of use...", len(myControl.GetHostmap().Indexes))
|
||||
for myControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for myControl hostinfos (%v != 2) to get cleaned up from lack of use...", myControl.GetHostmapIndexCount())
|
||||
r.Log("Assert the relay tunnel still works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.Log("yupitdoes")
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
t.Logf("myControl hostinfos got cleaned up!")
|
||||
for len(theirControl.GetHostmap().Indexes) != 2 {
|
||||
t.Logf("Waiting for theirControl hostinfos (%v != 2) to get cleaned up from lack of use...", len(theirControl.GetHostmap().Indexes))
|
||||
for theirControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for theirControl hostinfos (%v != 2) to get cleaned up from lack of use...", theirControl.GetHostmapIndexCount())
|
||||
r.Log("Assert the relay tunnel still works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.Log("yupitdoes")
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
t.Logf("theirControl hostinfos got cleaned up!")
|
||||
for len(relayControl.GetHostmap().Indexes) != 2 {
|
||||
t.Logf("Waiting for relayControl hostinfos (%v != 2) to get cleaned up from lack of use...", len(relayControl.GetHostmap().Indexes))
|
||||
for relayControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for relayControl hostinfos (%v != 2) to get cleaned up from lack of use...", relayControl.GetHostmapIndexCount())
|
||||
r.Log("Assert the relay tunnel still works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.Log("yupitdoes")
|
||||
@@ -1123,7 +1127,7 @@ func TestRehandshaking(t *testing.T) {
|
||||
theirConfig.ReloadConfigString(string(rc))
|
||||
|
||||
r.Log("Spin until there is only 1 tunnel")
|
||||
for len(myControl.GetHostmap().Indexes)+len(theirControl.GetHostmap().Indexes) > 2 {
|
||||
for myControl.GetHostmapIndexCount()+theirControl.GetHostmapIndexCount() > 2 {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
t.Log("Connection manager hasn't ticked yet")
|
||||
time.Sleep(time.Second)
|
||||
@@ -1223,7 +1227,7 @@ func TestRehandshakingLoser(t *testing.T) {
|
||||
myConfig.ReloadConfigString(string(rc))
|
||||
|
||||
r.Log("Spin until there is only 1 tunnel")
|
||||
for len(myControl.GetHostmap().Indexes)+len(theirControl.GetHostmap().Indexes) > 2 {
|
||||
for myControl.GetHostmapIndexCount()+theirControl.GetHostmapIndexCount() > 2 {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
t.Log("Connection manager hasn't ticked yet")
|
||||
time.Sleep(time.Second)
|
||||
@@ -1535,3 +1539,78 @@ func TestGoodHandshakeUnsafeDest(t *testing.T) {
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestMultiVpnAddrDeletePrimaryKeepsSecondAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Regression for the hostmap multi-vpnAddr delete bug. A dual-stack (v4+v6) V2-cert peer that
|
||||
// handshakes twice at once ends up with two hostinfos linked in the shared next/prev chain, with the
|
||||
// primary owning both addresses. Deleting that primary (e.g. connection manager dropping it, a
|
||||
// CloseTunnel, a collision) must promote the surviving sibling for EVERY address. The pre-fix code
|
||||
// unlinked the chain once per address, so it promoted the sibling for the first address and orphaned
|
||||
// the second: the peer stayed reachable at its v4 addr but not its v6 addr despite a live tunnel.
|
||||
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "me ", "10.128.0.1/24,fd00::1/64", nil)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "them", "10.128.0.2/24,fd00::2/64", nil)
|
||||
|
||||
// This bug only exists for peers carrying more than one vpn address
|
||||
require.Len(t, theirVpnIpNet, 2)
|
||||
theirV4 := theirVpnIpNet[0].Addr()
|
||||
theirV6 := theirVpnIpNet[1].Addr()
|
||||
|
||||
// Put their info in our lighthouse and vice versa
|
||||
myControl.InjectLightHouseAddr(theirV4, theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
|
||||
// Build a router so we don't have to reason who gets which packet
|
||||
r := router.NewR(t, myControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
// Race a handshake so both of us build a hostinfo for the other, leaving my hostmap with a single
|
||||
// host (them) backed by two linked hostinfos, just like TestStage1Race.
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirV4, 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirV4, 80, []byte("Hi from them")))
|
||||
|
||||
myHsForThem := myControl.GetFromUDP(true)
|
||||
theirHsForMe := theirControl.GetFromUDP(true)
|
||||
|
||||
r.InjectUDPPacket(theirControl, myControl, theirHsForMe)
|
||||
r.InjectUDPPacket(myControl, theirControl, myHsForThem)
|
||||
|
||||
r.RouteForAllUntilTxTun(theirControl)
|
||||
r.RouteForAllUntilTxTun(myControl)
|
||||
|
||||
r.RenderHostmaps("Racing hostmaps", myControl, theirControl)
|
||||
|
||||
// Two hostinfos for them means the shared next/prev chain has a sibling to promote. The Hosts map has
|
||||
// one entry per vpn address (two, for dual stack), so the index count is what tells us there are two
|
||||
// hostinfos.
|
||||
require.Len(t, myControl.ListHostmapIndexes(false), 2)
|
||||
|
||||
// The primary owns both of their addresses
|
||||
primaryV4 := myControl.GetHostInfoByVpnAddr(theirV4, false)
|
||||
primaryV6 := myControl.GetHostInfoByVpnAddr(theirV6, false)
|
||||
require.NotNil(t, primaryV4)
|
||||
require.NotNil(t, primaryV6)
|
||||
require.Equal(t, primaryV4.LocalIndex, primaryV6.LocalIndex, "both addrs should point at the same primary")
|
||||
|
||||
// Delete the primary tunnel. localOnly so we don't perturb their side, we only care about my hostmap.
|
||||
require.True(t, myControl.CloseTunnel(theirV4, true))
|
||||
|
||||
// The surviving sibling must still serve BOTH addresses.
|
||||
survivorV4 := myControl.GetHostInfoByVpnAddr(theirV4, false)
|
||||
survivorV6 := myControl.GetHostInfoByVpnAddr(theirV6, false)
|
||||
require.NotNil(t, survivorV4, "v4 addr should still resolve to the surviving tunnel")
|
||||
// Pre-fix this is nil: the second address was orphaned when the primary was deleted.
|
||||
require.NotNil(t, survivorV6, "v6 addr was orphaned after deleting the primary (multi-vpnAddr delete bug)")
|
||||
assert.Equal(t, survivorV4.LocalIndex, survivorV6.LocalIndex, "both addrs should promote to the same survivor")
|
||||
assert.NotEqual(t, primaryV4.LocalIndex, survivorV4.LocalIndex, "a different hostinfo should now be primary")
|
||||
|
||||
r.RenderHostmaps("Final hostmaps", myControl, theirControl)
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
+6
-6
@@ -43,8 +43,8 @@ func TestDropInactiveTunnels(t *testing.T) {
|
||||
r.Log("Go inactive and wait for the tunnels to get dropped")
|
||||
waitStart := time.Now()
|
||||
for {
|
||||
myIndexes := len(myControl.GetHostmap().Indexes)
|
||||
theirIndexes := len(theirControl.GetHostmap().Indexes)
|
||||
myIndexes := myControl.GetHostmapIndexCount()
|
||||
theirIndexes := theirControl.GetHostmapIndexCount()
|
||||
if myIndexes == 0 && theirIndexes == 0 {
|
||||
break
|
||||
}
|
||||
@@ -493,8 +493,8 @@ func TestCloseTunnelAuthenticated(t *testing.T) {
|
||||
|
||||
waitStart := time.Now()
|
||||
for {
|
||||
myIndexes := len(myControl.GetHostmap().Indexes)
|
||||
theirIndexes := len(theirControl.GetHostmap().Indexes)
|
||||
myIndexes := myControl.GetHostmapIndexCount()
|
||||
theirIndexes := theirControl.GetHostmapIndexCount()
|
||||
if myIndexes == 0 && theirIndexes == 0 {
|
||||
break
|
||||
}
|
||||
@@ -548,8 +548,8 @@ func TestCloseTunnelAuthenticated(t *testing.T) {
|
||||
r.Log("Injected bogus close tunnel. Let's see!")
|
||||
waitStart = time.Now()
|
||||
for {
|
||||
myIndexes := len(myControl.GetHostmap().Indexes)
|
||||
theirIndexes := len(theirControl.GetHostmap().Indexes)
|
||||
myIndexes := myControl.GetHostmapIndexCount()
|
||||
theirIndexes := theirControl.GetHostmapIndexCount()
|
||||
if myIndexes == 0 {
|
||||
t.Fatal("myIndexes should not be 0")
|
||||
}
|
||||
|
||||
@@ -110,6 +110,15 @@ lighthouse:
|
||||
#- "1.1.1.1:4242"
|
||||
#- "1.2.3.4:0" # port will be replaced with the real listening port
|
||||
|
||||
# Locally discovered addresses are checked against the MTU of the link they were found on. If the link cannot fit
|
||||
# a full-size packet from the nebula tun device (`tun.mtu` plus encapsulation overhead, which is larger for relayed
|
||||
# traffic) without fragmenting, a warning is logged.
|
||||
# When omit_low_mtu_addrs is true, addresses whose links cannot fit normal nebula traffic are dropped from
|
||||
# lighthouse reports entirely.
|
||||
# Addresses that can fit normal nebula traffic but not relayed traffic are always still advertised.
|
||||
# This does not apply to addresses listed in advertise_addrs.
|
||||
#omit_low_mtu_addrs: false
|
||||
|
||||
# EXPERIMENTAL: This option may change or disappear in the future.
|
||||
# This setting allows us to "guess" what the remote might be for a host
|
||||
# while we wait for the lighthouse response.
|
||||
|
||||
+5
-5
@@ -423,11 +423,6 @@ var ErrNoMatchingRule = errors.New("no matching rule in firewall table")
|
||||
// Drop returns an error if the packet should be dropped, explaining why. It
|
||||
// returns nil if the packet should not be dropped.
|
||||
func (f *Firewall) Drop(fp firewall.Packet, incoming bool, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) error {
|
||||
// Check if we spoke to this tuple, if we did then allow this packet
|
||||
if f.inConns(fp, h, caPool, localCache) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make sure remote address matches nebula certificate, and determine how to treat it
|
||||
if h.networks == nil {
|
||||
// Simple case: Certificate has one address and no unsafe networks
|
||||
@@ -461,6 +456,11 @@ func (f *Firewall) Drop(fp firewall.Packet, incoming bool, h *HostInfo, caPool *
|
||||
return ErrInvalidLocalIP
|
||||
}
|
||||
|
||||
// Check if we spoke to this tuple, if we did then allow this packet
|
||||
if f.inConns(fp, h, caPool, localCache) {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := f.OutRules
|
||||
if incoming {
|
||||
table = f.InRules
|
||||
|
||||
@@ -916,6 +916,159 @@ func TestFirewall_DropIPSpoofing(t *testing.T) {
|
||||
assert.Equal(t, fw.Drop(p, true, &h1, cp, nil), ErrInvalidRemoteIP)
|
||||
}
|
||||
|
||||
func TestFirewall_ConntrackSourceSpoofingAcrossPeers(t *testing.T) {
|
||||
l := test.NewLoggerWithOutput(&bytes.Buffer{})
|
||||
|
||||
myVpnNetworksTable := new(bart.Lite)
|
||||
myVpnNetworksTable.Insert(netip.MustParsePrefix("192.0.2.1/24"))
|
||||
|
||||
owner := &dummyCert{
|
||||
name: "owner",
|
||||
networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/24")},
|
||||
}
|
||||
|
||||
victim := &cert.CachedCertificate{
|
||||
Certificate: &dummyCert{
|
||||
name: "victim",
|
||||
networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/24")},
|
||||
},
|
||||
}
|
||||
victimHI := HostInfo{
|
||||
ConnectionState: &ConnectionState{peerCert: victim},
|
||||
vpnAddrs: []netip.Addr{netip.MustParseAddr("192.0.2.2")},
|
||||
}
|
||||
victimHI.buildNetworks(myVpnNetworksTable, victim.Certificate)
|
||||
|
||||
attacker := &cert.CachedCertificate{
|
||||
Certificate: &dummyCert{
|
||||
name: "attacker",
|
||||
networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.3/24")},
|
||||
},
|
||||
}
|
||||
attackerHI := HostInfo{
|
||||
ConnectionState: &ConnectionState{peerCert: attacker},
|
||||
vpnAddrs: []netip.Addr{netip.MustParseAddr("192.0.2.3")},
|
||||
}
|
||||
attackerHI.buildNetworks(myVpnNetworksTable, attacker.Certificate)
|
||||
|
||||
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, owner)
|
||||
// Allow any inbound traffic that passes the cert / source-IP checks.
|
||||
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
|
||||
cp := cert.NewCAPool()
|
||||
|
||||
flow := firewall.Packet{
|
||||
LocalAddr: netip.MustParseAddr("192.0.2.1"),
|
||||
RemoteAddr: netip.MustParseAddr("192.0.2.2"),
|
||||
LocalPort: 443,
|
||||
RemotePort: 55000,
|
||||
Protocol: firewall.ProtoUDP,
|
||||
}
|
||||
|
||||
require.NoError(t, fw.Drop(flow, true, &victimHI, cp, nil),
|
||||
"victim's own traffic from its own overlay IP must be allowed")
|
||||
|
||||
unseen := flow
|
||||
unseen.RemotePort = 55001
|
||||
assert.Equal(t, ErrInvalidRemoteIP, fw.Drop(unseen, true, &attackerHI, cp, nil),
|
||||
"sanity: attacker forging victim's source IP must be rejected when no conntrack entry exists")
|
||||
|
||||
got := fw.Drop(flow, true, &attackerHI, cp, nil)
|
||||
t.Logf("attacker replaying victim's 4-tuple: Drop returned %v (nil == packet ALLOWED == spoof succeeded)", got)
|
||||
assert.Equal(t, ErrInvalidRemoteIP, got,
|
||||
"SECURITY: attacker spoofed victim's overlay source IP (192.0.2.2) by reusing an existing conntrack 4-tuple; Drop returned %v instead of rejecting", got)
|
||||
}
|
||||
|
||||
// BenchmarkFirewallDropConntrackHit measures Drop on an already-established flow
|
||||
// (a conntrack hit). This is the fast path that the source-IP<->cert binding
|
||||
// reordering adds work to, so it quantifies the cost of moving the address checks
|
||||
// ahead of the conntrack lookup. Cases:
|
||||
// - simple: peer cert has one address, no unsafe networks (h.networks == nil),
|
||||
// so the remote-address check is a single netip.Addr compare.
|
||||
// - complex: peer cert has unsafe networks (h.networks populated), so the
|
||||
// remote-address check is a BART lookup.
|
||||
// - noCache/localCache: whether a per-batch ConntrackCache is supplied, which in
|
||||
// the original code let the fast path skip straight past the address checks.
|
||||
func BenchmarkFirewallDropConntrackHit(b *testing.B) {
|
||||
l := test.NewLoggerWithOutput(&bytes.Buffer{})
|
||||
|
||||
myVpnNetworksTable := new(bart.Lite)
|
||||
myVpnNetworksTable.Insert(netip.MustParsePrefix("192.0.2.1/24"))
|
||||
|
||||
owner := &dummyCert{
|
||||
name: "owner",
|
||||
networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/24")},
|
||||
}
|
||||
|
||||
simpleCert := &cert.CachedCertificate{
|
||||
Certificate: &dummyCert{
|
||||
name: "simple",
|
||||
networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/24")},
|
||||
},
|
||||
}
|
||||
simpleHost := &HostInfo{
|
||||
ConnectionState: &ConnectionState{peerCert: simpleCert},
|
||||
vpnAddrs: []netip.Addr{netip.MustParseAddr("192.0.2.2")},
|
||||
}
|
||||
simpleHost.buildNetworks(myVpnNetworksTable, simpleCert.Certificate)
|
||||
|
||||
complexCert := &cert.CachedCertificate{
|
||||
Certificate: &dummyCert{
|
||||
name: "complex",
|
||||
networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/24")},
|
||||
unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("198.51.100.0/24")},
|
||||
},
|
||||
}
|
||||
complexHost := &HostInfo{
|
||||
ConnectionState: &ConnectionState{peerCert: complexCert},
|
||||
vpnAddrs: []netip.Addr{netip.MustParseAddr("192.0.2.2")},
|
||||
}
|
||||
complexHost.buildNetworks(myVpnNetworksTable, complexCert.Certificate)
|
||||
|
||||
cp := cert.NewCAPool()
|
||||
|
||||
flow := firewall.Packet{
|
||||
LocalAddr: netip.MustParseAddr("192.0.2.1"),
|
||||
RemoteAddr: netip.MustParseAddr("192.0.2.2"),
|
||||
LocalPort: 443,
|
||||
RemotePort: 55000,
|
||||
Protocol: firewall.ProtoUDP,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
host *HostInfo
|
||||
useCache bool
|
||||
}{
|
||||
{"simple/noCache", simpleHost, false},
|
||||
{"simple/localCache", simpleHost, true},
|
||||
{"complex/noCache", complexHost, false},
|
||||
{"complex/localCache", complexHost, true},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, owner)
|
||||
require.NoError(b, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
|
||||
|
||||
// Establish the conntrack entry so every benchmarked Drop is a hit.
|
||||
require.NoError(b, fw.Drop(flow, true, tc.host, cp, nil))
|
||||
|
||||
var cache firewall.ConntrackCache
|
||||
if tc.useCache {
|
||||
cache = firewall.ConntrackCache{}
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := fw.Drop(flow, true, tc.host, cp, cache); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLookup(b *testing.B) {
|
||||
ml := func(m map[string]struct{}, a [][]string) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
|
||||
@@ -12,7 +12,7 @@ require (
|
||||
github.com/gaissmai/bart v0.28.0
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/kardianos/service v1.2.4
|
||||
github.com/kardianos/service v1.3.0
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/miekg/pkcs11 v1.1.2
|
||||
github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f
|
||||
@@ -32,7 +32,7 @@ require (
|
||||
golang.org/x/term v0.44.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
|
||||
golang.zx2c4.com/wireguard/windows v0.6.1
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gvisor.dev/gvisor v0.0.0-20240423190808-9d7a357edefe
|
||||
@@ -50,7 +50,7 @@ require (
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
)
|
||||
|
||||
@@ -66,8 +66,8 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk=
|
||||
github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
|
||||
github.com/kardianos/service v1.3.0 h1:/LGy+xPP2TM+GLTiCZ2di7cy0Jd/qrawlTUfqKYFdTI=
|
||||
github.com/kardianos/service v1.3.0/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
@@ -170,8 +170,8 @@ golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPI
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -223,8 +223,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -233,8 +233,8 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4=
|
||||
golang.zx2c4.com/wireguard/windows v0.6.1 h1:XMaKojH1Hs/raMrmnir4n35nTvzvWj7NmSYzHn2F4qU=
|
||||
golang.zx2c4.com/wireguard/windows v0.6.1/go.mod h1:04aqInu5GYuTFvMuDw/rKBAF7mHrltW/3rekpfbbZDM=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
|
||||
@@ -430,14 +430,11 @@ func (hm *HandshakeManager) CheckAndComplete(hostinfo *HostInfo, handshakePacket
|
||||
// Check if we already have a tunnel with this vpn ip
|
||||
existingHostInfo, found := hm.mainHostMap.Hosts[hostinfo.vpnAddrs[0]]
|
||||
if found && existingHostInfo != nil {
|
||||
testHostInfo := existingHostInfo
|
||||
for testHostInfo != nil {
|
||||
// Is it just a delayed handshake packet?
|
||||
// Is it just a delayed handshake packet? Check every hostinfo we hold for this address.
|
||||
for _, testHostInfo := range hm.mainHostMap.unlockedGetHostList(hostinfo.vpnAddrs[0]) {
|
||||
if bytes.Equal(hostinfo.HandshakePacket[handshakePacket], testHostInfo.HandshakePacket[handshakePacket]) {
|
||||
return testHostInfo, ErrAlreadySeen
|
||||
}
|
||||
|
||||
testHostInfo = testHostInfo.next
|
||||
}
|
||||
|
||||
// Is this a newer handshake?
|
||||
|
||||
+176
-120
@@ -56,11 +56,20 @@ type Relay struct {
|
||||
}
|
||||
|
||||
type HostMap struct {
|
||||
sync.RWMutex //Because we concurrently read and write to our maps
|
||||
Indexes map[uint32]*HostInfo
|
||||
Relays map[uint32]*HostInfo // Maps a Relay IDX to a Relay HostInfo object
|
||||
RemoteIndexes map[uint32]*HostInfo
|
||||
sync.RWMutex //Because we concurrently read and write to our maps
|
||||
Indexes map[uint32]*HostInfo
|
||||
Relays map[uint32]*HostInfo // Maps a Relay IDX to a Relay HostInfo object
|
||||
RemoteIndexes map[uint32]*HostInfo
|
||||
// Hosts maps a vpn address to its primary hostinfo, one entry per address we hold a tunnel
|
||||
// for. moreHosts only has an entry while an address is held by 2 or more hostinfos and stores
|
||||
// the full most-recent-first list; moreHosts[a][0] is always the same hostinfo as Hosts[a].
|
||||
// Each address gets its own independent list, so a hostinfo owning multiple addresses can
|
||||
// never corrupt another address's ordering the way the old shared next/prev chain could.
|
||||
// Entries in moreHosts are only ever written by unlockedSetHostsForAddr; Hosts is written
|
||||
// directly only in the single-hostinfo fast paths where moreHosts is known to have no entry,
|
||||
// and unlockedDeleteHostInfo swaps either map for a fresh one when it fully drains.
|
||||
Hosts map[netip.Addr]*HostInfo
|
||||
moreHosts map[netip.Addr][]*HostInfo
|
||||
preferredRanges atomic.Pointer[[]netip.Prefix]
|
||||
l *slog.Logger
|
||||
}
|
||||
@@ -229,7 +238,7 @@ const (
|
||||
)
|
||||
|
||||
type HostInfo struct {
|
||||
remote netip.AddrPort
|
||||
remote atomic.Pointer[netip.AddrPort]
|
||||
remotes *RemoteList
|
||||
promoteCounter atomic.Uint32
|
||||
ConnectionState *ConnectionState
|
||||
@@ -266,10 +275,6 @@ type HostInfo struct {
|
||||
lastRoam time.Time
|
||||
lastRoamRemote netip.AddrPort
|
||||
|
||||
// Used to track other hostinfos for this vpn ip since only 1 can be primary
|
||||
// Synchronised via hostmap lock and not the hostinfo lock.
|
||||
next, prev *HostInfo
|
||||
|
||||
//TODO: in, out, and others might benefit from being an atomic.Int32. We could collapse connectionManager pendingDeletion, relayUsed, and in/out into this 1 thing
|
||||
in, out, pendingDeletion atomic.Bool
|
||||
|
||||
@@ -334,6 +339,7 @@ func newHostMap(l *slog.Logger) *HostMap {
|
||||
Relays: map[uint32]*HostInfo{},
|
||||
RemoteIndexes: map[uint32]*HostInfo{},
|
||||
Hosts: map[netip.Addr]*HostInfo{},
|
||||
moreHosts: map[netip.Addr][]*HostInfo{},
|
||||
l: l,
|
||||
}
|
||||
}
|
||||
@@ -382,13 +388,55 @@ func (hm *HostMap) EmitStats() {
|
||||
metrics.GetOrRegisterGauge("hostmap.main.relayIndexes", nil).Update(int64(relaysLen))
|
||||
}
|
||||
|
||||
// DeleteHostInfo will fully unlink the hostinfo and return true if it was the final hostinfo for this vpn ip
|
||||
// unlockedSetHostsForAddr stores the per-address hostinfo list (list[0] is the primary). An empty
|
||||
// list removes the address. This is the one place Hosts and moreHosts are written together, keep
|
||||
// it that way. Callers must hold the write lock.
|
||||
func (hm *HostMap) unlockedSetHostsForAddr(addr netip.Addr, list []*HostInfo) {
|
||||
if len(list) == 0 {
|
||||
delete(hm.Hosts, addr)
|
||||
delete(hm.moreHosts, addr)
|
||||
return
|
||||
}
|
||||
hm.Hosts[addr] = list[0]
|
||||
if len(list) > 1 {
|
||||
hm.moreHosts[addr] = list
|
||||
} else {
|
||||
delete(hm.moreHosts, addr)
|
||||
}
|
||||
}
|
||||
|
||||
// unlockedGetHostList returns every hostinfo holding addr, primary first, or nil if we have no
|
||||
// tunnel for addr. The common single-hostinfo case builds a fresh one element list, so keep this
|
||||
// off the packet hot path; the primary is a direct Hosts read. Callers must hold the lock (read
|
||||
// or write).
|
||||
func (hm *HostMap) unlockedGetHostList(addr netip.Addr) []*HostInfo {
|
||||
if list, ok := hm.moreHosts[addr]; ok {
|
||||
return list
|
||||
}
|
||||
if h, ok := hm.Hosts[addr]; ok {
|
||||
return []*HostInfo{h}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeHostInfo returns list with hi removed (order preserved), or list unchanged if hi is
|
||||
// absent. It deletes in place: every mutator holds the hostmap write lock and no reader ever
|
||||
// retains a slice across a mutation (readers iterate under RLock), so there is no snapshot to
|
||||
// invalidate.
|
||||
func removeHostInfo(list []*HostInfo, hi *HostInfo) []*HostInfo {
|
||||
idx := slices.Index(list, hi)
|
||||
if idx < 0 {
|
||||
return list
|
||||
}
|
||||
return slices.Delete(list, idx, idx+1)
|
||||
}
|
||||
|
||||
// DeleteHostInfo will fully unlink the hostinfo and return true if no other hostinfo still holds
|
||||
// any of its vpn addrs, meaning we no longer have a tunnel to the peer
|
||||
func (hm *HostMap) DeleteHostInfo(hostinfo *HostInfo) bool {
|
||||
// Delete the host itself, ensuring it's not modified anymore
|
||||
hm.Lock()
|
||||
// If we have a previous or next hostinfo then we are not the last one for this vpn ip
|
||||
final := (hostinfo.next == nil && hostinfo.prev == nil)
|
||||
hm.unlockedDeleteHostInfo(hostinfo)
|
||||
final := hm.unlockedDeleteHostInfo(hostinfo)
|
||||
hm.Unlock()
|
||||
|
||||
return final
|
||||
@@ -400,85 +448,66 @@ func (hm *HostMap) MakePrimary(hostinfo *HostInfo) {
|
||||
hm.unlockedMakePrimary(hostinfo)
|
||||
}
|
||||
|
||||
func (hm *HostMap) unlockedMakePrimary(hostinfo *HostInfo) {
|
||||
// Get the current primary, if it exists
|
||||
oldHostinfo := hm.Hosts[hostinfo.vpnAddrs[0]]
|
||||
|
||||
// Every address in the hostinfo gets elevated to primary
|
||||
for _, vpnAddr := range hostinfo.vpnAddrs {
|
||||
//NOTE: It is possible that we leave a dangling hostinfo here but connection manager works on
|
||||
// indexes so it should be fine.
|
||||
hm.Hosts[vpnAddr] = hostinfo
|
||||
// unlockedMakePrimary reports whether hostinfo is (now) the primary for each of its addresses,
|
||||
// false only when it is no longer in the hostmap at all.
|
||||
func (hm *HostMap) unlockedMakePrimary(hostinfo *HostInfo) bool {
|
||||
// A hostinfo that is no longer in the hostmap must not be re-inserted here. Callers can race
|
||||
// tunnel teardown, deciding to promote under the read lock and only taking the write lock
|
||||
// after a delete fully unlinked the hostinfo (connection manager swapPrimary, AddRelay). Every
|
||||
// live hostinfo is registered in Indexes by unlockedAddHostInfo, so this is a membership test.
|
||||
if hm.Indexes[hostinfo.localIndexId] != hostinfo {
|
||||
return false
|
||||
}
|
||||
|
||||
// If we are already primary then we won't bother re-linking
|
||||
if oldHostinfo == hostinfo {
|
||||
return
|
||||
}
|
||||
|
||||
// Unlink this hostinfo
|
||||
if hostinfo.prev != nil {
|
||||
hostinfo.prev.next = hostinfo.next
|
||||
}
|
||||
if hostinfo.next != nil {
|
||||
hostinfo.next.prev = hostinfo.prev
|
||||
}
|
||||
|
||||
// If there wasn't a previous primary then clear out any links
|
||||
if oldHostinfo == nil {
|
||||
hostinfo.next = nil
|
||||
hostinfo.prev = nil
|
||||
return
|
||||
}
|
||||
|
||||
// Relink the hostinfo as primary
|
||||
hostinfo.next = oldHostinfo
|
||||
oldHostinfo.prev = hostinfo
|
||||
hostinfo.prev = nil
|
||||
}
|
||||
|
||||
func (hm *HostMap) unlockedDeleteHostInfo(hostinfo *HostInfo) {
|
||||
// Move hostinfo to the front (primary) of each of its address lists. The lists are
|
||||
// independent per address, so this can never leave a dangling entry the way promoting
|
||||
// against a single shared chain could.
|
||||
for _, addr := range hostinfo.vpnAddrs {
|
||||
h := hm.Hosts[addr]
|
||||
for h != nil {
|
||||
if h == hostinfo {
|
||||
hm.unlockedInnerDeleteHostInfo(h, addr)
|
||||
}
|
||||
h = h.next
|
||||
if hm.Hosts[addr] == hostinfo {
|
||||
// Already primary for this address, the list is already in the right order
|
||||
continue
|
||||
}
|
||||
list := removeHostInfo(hm.unlockedGetHostList(addr), hostinfo)
|
||||
list = append([]*HostInfo{hostinfo}, list...)
|
||||
hm.unlockedSetHostsForAddr(addr, list)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (hm *HostMap) unlockedInnerDeleteHostInfo(hostinfo *HostInfo, addr netip.Addr) {
|
||||
primary, ok := hm.Hosts[addr]
|
||||
isLastHostinfo := hostinfo.next == nil && hostinfo.prev == nil
|
||||
if ok && primary == hostinfo {
|
||||
// The vpn addr pointer points to the same hostinfo as the local index id, we can remove it
|
||||
delete(hm.Hosts, addr)
|
||||
if len(hm.Hosts) == 0 {
|
||||
hm.Hosts = map[netip.Addr]*HostInfo{}
|
||||
}
|
||||
|
||||
if hostinfo.next != nil {
|
||||
// We had more than 1 hostinfo at this vpn addr, promote the next in the list to primary
|
||||
hm.Hosts[addr] = hostinfo.next
|
||||
// It is primary, there is no previous hostinfo now
|
||||
hostinfo.next.prev = nil
|
||||
}
|
||||
|
||||
} else {
|
||||
// Relink if we were in the middle of multiple hostinfos for this vpn addr
|
||||
if hostinfo.prev != nil {
|
||||
hostinfo.prev.next = hostinfo.next
|
||||
}
|
||||
|
||||
if hostinfo.next != nil {
|
||||
hostinfo.next.prev = hostinfo.prev
|
||||
// unlockedDeleteHostInfo removes hostinfo from every one of its address lists and from the index
|
||||
// maps. It returns true if this was the last hostinfo for all of its addresses (we no longer have
|
||||
// any tunnel to the peer), which the caller uses to decide whether to clear learned lighthouse
|
||||
// state and disestablish relays.
|
||||
func (hm *HostMap) unlockedDeleteHostInfo(hostinfo *HostInfo) bool {
|
||||
// Remove this hostinfo from each of its address lists. The lists are independent, so a
|
||||
// sibling is never promoted to an address it does not own and no other list is touched.
|
||||
final := true
|
||||
for _, addr := range hostinfo.vpnAddrs {
|
||||
if list, ok := hm.moreHosts[addr]; ok {
|
||||
list = removeHostInfo(list, hostinfo)
|
||||
hm.unlockedSetHostsForAddr(addr, list)
|
||||
if len(list) > 0 {
|
||||
final = false
|
||||
}
|
||||
} else if existing, ok := hm.Hosts[addr]; ok {
|
||||
if existing == hostinfo {
|
||||
// Common case, the only hostinfo for this address. moreHosts has no entry to clean up.
|
||||
delete(hm.Hosts, addr)
|
||||
} else {
|
||||
// We don't hold this address but another hostinfo does, we still have a tunnel to the peer
|
||||
final = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hostinfo.next = nil
|
||||
hostinfo.prev = nil
|
||||
// Go maps never shrink their buckets, replace fully drained maps so a node that churned
|
||||
// through a large peer count gives the memory back. Same idiom as the index maps below.
|
||||
if len(hm.Hosts) == 0 {
|
||||
hm.Hosts = map[netip.Addr]*HostInfo{}
|
||||
}
|
||||
if len(hm.moreHosts) == 0 {
|
||||
hm.moreHosts = map[netip.Addr][]*HostInfo{}
|
||||
}
|
||||
|
||||
// The remote index uses index ids outside our control so lets make sure we are only removing
|
||||
// the remote index pointer here if it points to the hostinfo we are deleting
|
||||
@@ -502,7 +531,7 @@ func (hm *HostMap) unlockedInnerDeleteHostInfo(hostinfo *HostInfo, addr netip.Ad
|
||||
)
|
||||
}
|
||||
|
||||
if isLastHostinfo {
|
||||
if final {
|
||||
// I have lost connectivity to my peers. My relay tunnel is likely broken. Mark the next
|
||||
// hops as 'Requested' so that new relay tunnels are created in the future.
|
||||
hm.unlockedDisestablishVpnAddrRelayFor(hostinfo)
|
||||
@@ -511,6 +540,8 @@ func (hm *HostMap) unlockedInnerDeleteHostInfo(hostinfo *HostInfo, addr netip.Ad
|
||||
for _, localRelayIdx := range hostinfo.relayState.CopyRelayForIdxs() {
|
||||
delete(hm.Relays, localRelayIdx)
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
||||
|
||||
func (hm *HostMap) QueryIndex(index uint32) *HostInfo {
|
||||
@@ -554,19 +585,30 @@ func (hm *HostMap) QueryVpnAddrsRelayFor(targetIps []netip.Addr, relayHostIp net
|
||||
hm.RLock()
|
||||
defer hm.RUnlock()
|
||||
|
||||
// This runs per relayed packet, so check the primary with a single map probe and only consult
|
||||
// moreHosts when the primary can't relay for us.
|
||||
h, ok := hm.Hosts[relayHostIp]
|
||||
if !ok {
|
||||
return nil, nil, errors.New("unable to find host")
|
||||
}
|
||||
|
||||
for h != nil {
|
||||
for _, targetIp := range targetIps {
|
||||
r, ok := h.relayState.QueryRelayForByIp(targetIp)
|
||||
if ok && r.State == Established {
|
||||
return h, r, nil
|
||||
for _, targetIp := range targetIps {
|
||||
r, ok := h.relayState.QueryRelayForByIp(targetIp)
|
||||
if ok && r.State == Established {
|
||||
return h, r, nil
|
||||
}
|
||||
}
|
||||
|
||||
if list, ok := hm.moreHosts[relayHostIp]; ok {
|
||||
// list[0] is the primary we already checked
|
||||
for _, h := range list[1:] {
|
||||
for _, targetIp := range targetIps {
|
||||
r, ok := h.relayState.QueryRelayForByIp(targetIp)
|
||||
if ok && r.State == Established {
|
||||
return h, r, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
h = h.next
|
||||
}
|
||||
|
||||
return nil, nil, errors.New("unable to find host with relay")
|
||||
@@ -574,20 +616,14 @@ func (hm *HostMap) QueryVpnAddrsRelayFor(targetIps []netip.Addr, relayHostIp net
|
||||
|
||||
func (hm *HostMap) unlockedDisestablishVpnAddrRelayFor(hi *HostInfo) {
|
||||
for _, relayHostIp := range hi.relayState.CopyRelayIps() {
|
||||
if h, ok := hm.Hosts[relayHostIp]; ok {
|
||||
for h != nil {
|
||||
h.relayState.UpdateRelayForByIpState(hi.vpnAddrs[0], Disestablished)
|
||||
h = h.next
|
||||
}
|
||||
for _, h := range hm.unlockedGetHostList(relayHostIp) {
|
||||
h.relayState.UpdateRelayForByIpState(hi.vpnAddrs[0], Disestablished)
|
||||
}
|
||||
}
|
||||
for _, rs := range hi.relayState.CopyAllRelayFor() {
|
||||
if rs.Type == ForwardingType {
|
||||
if h, ok := hm.Hosts[rs.PeerAddr]; ok {
|
||||
for h != nil {
|
||||
h.relayState.UpdateRelayForByIpState(hi.vpnAddrs[0], Disestablished)
|
||||
h = h.next
|
||||
}
|
||||
for _, h := range hm.unlockedGetHostList(rs.PeerAddr) {
|
||||
h.relayState.UpdateRelayForByIpState(hi.vpnAddrs[0], Disestablished)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -637,22 +673,27 @@ func (hm *HostMap) unlockedAddHostInfo(hostinfo *HostInfo, f *Interface) {
|
||||
}
|
||||
|
||||
func (hm *HostMap) unlockedInnerAddHostInfo(vpnAddr netip.Addr, hostinfo *HostInfo, f *Interface) {
|
||||
existing := hm.Hosts[vpnAddr]
|
||||
hm.Hosts[vpnAddr] = hostinfo
|
||||
|
||||
if existing != nil && existing != hostinfo {
|
||||
hostinfo.next = existing
|
||||
existing.prev = hostinfo
|
||||
existing, ok := hm.Hosts[vpnAddr]
|
||||
if !ok {
|
||||
// Common case, the first hostinfo for this address. moreHosts stays empty.
|
||||
hm.Hosts[vpnAddr] = hostinfo
|
||||
return
|
||||
}
|
||||
|
||||
i := 1
|
||||
check := hostinfo
|
||||
for check != nil {
|
||||
if i > MaxHostInfosPerVpnIp {
|
||||
hm.unlockedDeleteHostInfo(check)
|
||||
}
|
||||
check = check.next
|
||||
i++
|
||||
// The new hostinfo becomes the primary for this address. Remove any stale copy of it first so
|
||||
// we never hold a duplicate, then prepend.
|
||||
list, ok := hm.moreHosts[vpnAddr]
|
||||
if !ok {
|
||||
list = []*HostInfo{existing}
|
||||
}
|
||||
list = removeHostInfo(list, hostinfo)
|
||||
list = append([]*HostInfo{hostinfo}, list...)
|
||||
hm.unlockedSetHostsForAddr(vpnAddr, list)
|
||||
|
||||
// Enforce the per-address cap by fully retiring the oldest hostinfo once we exceed it.
|
||||
// Deleting it removes it from all of its addresses and the index maps, matching prior behavior.
|
||||
if len(list) > MaxHostInfosPerVpnIp {
|
||||
hm.unlockedDeleteHostInfo(list[len(list)-1])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,7 +725,7 @@ func (hm *HostMap) ForEachIndex(f controlEach) {
|
||||
func (i *HostInfo) TryPromoteBest(preferredRanges []netip.Prefix, ifce *Interface) {
|
||||
c := i.promoteCounter.Add(1)
|
||||
if c%ifce.tryPromoteEvery.Load() == 0 {
|
||||
remote := i.remote
|
||||
remote := i.GetRemote()
|
||||
|
||||
// return early if we are already on a preferred remote
|
||||
if remote.IsValid() {
|
||||
@@ -726,11 +767,18 @@ func (i *HostInfo) GetCert() *cert.CachedCertificate {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *HostInfo) GetRemote() netip.AddrPort {
|
||||
if p := i.remote.Load(); p != nil {
|
||||
return *p
|
||||
}
|
||||
return netip.AddrPort{}
|
||||
}
|
||||
|
||||
// TODO: Maybe use ViaSender here?
|
||||
func (i *HostInfo) SetRemote(remote netip.AddrPort) {
|
||||
// We copy here because we likely got this remote from a source that reuses the object
|
||||
if i.remote != remote {
|
||||
i.remote = remote
|
||||
if i.GetRemote() != remote {
|
||||
i.remote.Store(&remote)
|
||||
i.remotes.LearnRemote(i.vpnAddrs[0], remote)
|
||||
}
|
||||
}
|
||||
@@ -742,7 +790,7 @@ func (i *HostInfo) SetRemoteIfPreferred(hm *HostMap, via ViaSender) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
currentRemote := i.remote
|
||||
currentRemote := i.GetRemote()
|
||||
if !currentRemote.IsValid() {
|
||||
i.SetRemote(via.UdpAddr)
|
||||
return true
|
||||
@@ -821,9 +869,17 @@ func (i *HostInfo) logger(l *slog.Logger) *slog.Logger {
|
||||
|
||||
// Utility functions
|
||||
|
||||
func localAddrs(l *slog.Logger, allowList *LocalAllowList) []netip.Addr {
|
||||
// localAddr is a locally discovered address candidate for lighthouse
|
||||
// advertisement, along with details about the link it was found on.
|
||||
type localAddr struct {
|
||||
addr netip.Addr
|
||||
ifName string
|
||||
linkMTU int // MTU reported for the link, or <= 0 if unknown
|
||||
}
|
||||
|
||||
func localAddrs(l *slog.Logger, allowList *LocalAllowList) []localAddr {
|
||||
//FIXME: This function is pretty garbage
|
||||
var finalAddrs []netip.Addr
|
||||
var finalAddrs []localAddr
|
||||
ifaces, _ := net.Interfaces()
|
||||
for _, i := range ifaces {
|
||||
allow := allowList.AllowName(i.Name)
|
||||
@@ -868,7 +924,7 @@ func localAddrs(l *slog.Logger, allowList *LocalAllowList) []netip.Addr {
|
||||
continue
|
||||
}
|
||||
|
||||
finalAddrs = append(finalAddrs, addr)
|
||||
finalAddrs = append(finalAddrs, localAddr{addr: addr, ifName: i.Name, linkMTU: i.MTU})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+294
-137
@@ -2,6 +2,7 @@ package nebula
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/slackhq/nebula/config"
|
||||
@@ -10,78 +11,84 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// chainIds returns the localIndexIds of the hostinfos holding addr, primary (index 0) first. It
|
||||
// also validates the Hosts/moreHosts sync contract on every call so a mutation that broke it
|
||||
// fails fast.
|
||||
func chainIds(t *testing.T, hm *HostMap, addr netip.Addr) []uint32 {
|
||||
t.Helper()
|
||||
assertHostMapInvariants(t, hm)
|
||||
list := hm.unlockedGetHostList(addr)
|
||||
ids := make([]uint32, len(list))
|
||||
for i, h := range list {
|
||||
ids[i] = h.localIndexId
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// assertHostMapInvariants checks the Hosts/moreHosts contract: moreHosts only holds addresses
|
||||
// with 2 or more hostinfos, its first entry is always the primary in Hosts, lists never hold
|
||||
// duplicates, every hostinfo in a list owns the address and is registered in Indexes, and every
|
||||
// indexed hostinfo is reachable through each of its addresses.
|
||||
func assertHostMapInvariants(t *testing.T, hm *HostMap) {
|
||||
t.Helper()
|
||||
for addr, list := range hm.moreHosts {
|
||||
require.GreaterOrEqualf(t, len(list), 2, "moreHosts[%s] must hold at least 2 hostinfos", addr)
|
||||
require.Samef(t, hm.Hosts[addr], list[0], "moreHosts[%s][0] must match the primary in Hosts", addr)
|
||||
seen := map[*HostInfo]bool{}
|
||||
for _, h := range list {
|
||||
require.NotNilf(t, h, "moreHosts[%s] must never hold a nil hostinfo", addr)
|
||||
require.Falsef(t, seen[h], "moreHosts[%s] holds hostinfo %d twice", addr, h.localIndexId)
|
||||
seen[h] = true
|
||||
require.Samef(t, hm.Indexes[h.localIndexId], h, "moreHosts[%s] member %d is not registered in Indexes", addr, h.localIndexId)
|
||||
require.Truef(t, slices.Contains(h.vpnAddrs, addr), "moreHosts[%s] member %d does not own the address", addr, h.localIndexId)
|
||||
}
|
||||
}
|
||||
for addr, h := range hm.Hosts {
|
||||
require.NotNilf(t, h, "Hosts[%s] must never be nil", addr)
|
||||
require.Samef(t, hm.Indexes[h.localIndexId], h, "Hosts[%s] primary %d is not registered in Indexes", addr, h.localIndexId)
|
||||
require.Truef(t, slices.Contains(h.vpnAddrs, addr), "Hosts[%s] primary (index %d) does not own the address", addr, h.localIndexId)
|
||||
}
|
||||
for idx, h := range hm.Indexes {
|
||||
require.Equalf(t, idx, h.localIndexId, "Indexes[%d] holds hostinfo with localIndexId %d", idx, h.localIndexId)
|
||||
for _, va := range h.vpnAddrs {
|
||||
require.Truef(t, slices.Contains(hm.unlockedGetHostList(va), h), "indexed hostinfo %d is missing from the list for %s", idx, va)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostMap_MakePrimary(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hm := newHostMap(l)
|
||||
|
||||
f := &Interface{}
|
||||
a := netip.MustParseAddr("0.0.0.1")
|
||||
|
||||
h1 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 1}
|
||||
h2 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 2}
|
||||
h3 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 3}
|
||||
h4 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 4}
|
||||
h1 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 1}
|
||||
h2 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 2}
|
||||
h3 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 3}
|
||||
h4 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 4}
|
||||
|
||||
hm.unlockedAddHostInfo(h4, f)
|
||||
hm.unlockedAddHostInfo(h3, f)
|
||||
hm.unlockedAddHostInfo(h2, f)
|
||||
hm.unlockedAddHostInfo(h1, f)
|
||||
|
||||
// Make sure we go h1 -> h2 -> h3 -> h4
|
||||
prim := hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h1.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h1.localIndexId, h2.prev.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h2.next.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h3.prev.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h3.next.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h4.prev.localIndexId)
|
||||
assert.Nil(t, h4.next)
|
||||
// Most-recently-added is primary: h1, h2, h3, h4
|
||||
assert.Equal(t, []uint32{1, 2, 3, 4}, chainIds(t, hm, a))
|
||||
assert.Equal(t, h1, hm.QueryVpnAddr(a))
|
||||
|
||||
// Swap h3/middle to primary
|
||||
// Swap the middle to primary: h3, h1, h2, h4
|
||||
hm.MakePrimary(h3)
|
||||
assert.Equal(t, []uint32{3, 1, 2, 4}, chainIds(t, hm, a))
|
||||
assert.Equal(t, h3, hm.QueryVpnAddr(a))
|
||||
|
||||
// Make sure we go h3 -> h1 -> h2 -> h4
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h3.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h1.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h2.localIndexId, h1.next.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h1.prev.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h2.next.localIndexId)
|
||||
assert.Equal(t, h1.localIndexId, h2.prev.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h4.prev.localIndexId)
|
||||
assert.Nil(t, h4.next)
|
||||
|
||||
// Swap h4/tail to primary
|
||||
// Swap the tail to primary: h4, h3, h1, h2
|
||||
hm.MakePrimary(h4)
|
||||
assert.Equal(t, []uint32{4, 3, 1, 2}, chainIds(t, hm, a))
|
||||
|
||||
// Make sure we go h4 -> h3 -> h1 -> h2
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h4.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h1.localIndexId, h3.next.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h3.prev.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h1.next.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h1.prev.localIndexId)
|
||||
assert.Equal(t, h1.localIndexId, h2.prev.localIndexId)
|
||||
assert.Nil(t, h2.next)
|
||||
|
||||
// Swap h4 again should be no-op
|
||||
// Swapping the current primary again is a no-op
|
||||
hm.MakePrimary(h4)
|
||||
|
||||
// Make sure we go h4 -> h3 -> h1 -> h2
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h4.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h1.localIndexId, h3.next.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h3.prev.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h1.next.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h1.prev.localIndexId)
|
||||
assert.Equal(t, h1.localIndexId, h2.prev.localIndexId)
|
||||
assert.Nil(t, h2.next)
|
||||
assert.Equal(t, []uint32{4, 3, 1, 2}, chainIds(t, hm, a))
|
||||
}
|
||||
|
||||
func TestHostMap_DeleteHostInfo(t *testing.T) {
|
||||
@@ -89,13 +96,14 @@ func TestHostMap_DeleteHostInfo(t *testing.T) {
|
||||
hm := newHostMap(l)
|
||||
|
||||
f := &Interface{}
|
||||
a := netip.MustParseAddr("0.0.0.1")
|
||||
|
||||
h1 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 1}
|
||||
h2 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 2}
|
||||
h3 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 3}
|
||||
h4 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 4}
|
||||
h5 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 5}
|
||||
h6 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 6}
|
||||
h1 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 1}
|
||||
h2 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 2}
|
||||
h3 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 3}
|
||||
h4 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 4}
|
||||
h5 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 5}
|
||||
h6 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 6}
|
||||
|
||||
hm.unlockedAddHostInfo(h6, f)
|
||||
hm.unlockedAddHostInfo(h5, f)
|
||||
@@ -104,94 +112,243 @@ func TestHostMap_DeleteHostInfo(t *testing.T) {
|
||||
hm.unlockedAddHostInfo(h2, f)
|
||||
hm.unlockedAddHostInfo(h1, f)
|
||||
|
||||
// h6 should be deleted
|
||||
assert.Nil(t, h6.next)
|
||||
assert.Nil(t, h6.prev)
|
||||
h := hm.QueryIndex(h6.localIndexId)
|
||||
assert.Nil(t, h)
|
||||
// h6 is evicted by the MaxHostInfosPerVpnIp cap; the rest are newest-first.
|
||||
assert.Nil(t, hm.QueryIndex(h6.localIndexId))
|
||||
assert.Equal(t, []uint32{1, 2, 3, 4, 5}, chainIds(t, hm, a))
|
||||
|
||||
// Make sure we go h1 -> h2 -> h3 -> h4 -> h5
|
||||
prim := hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h1.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h1.localIndexId, h2.prev.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h2.next.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h3.prev.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h3.next.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h4.prev.localIndexId)
|
||||
assert.Equal(t, h5.localIndexId, h4.next.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h5.prev.localIndexId)
|
||||
assert.Nil(t, h5.next)
|
||||
// Delete primary; not final since siblings remain.
|
||||
assert.False(t, hm.DeleteHostInfo(h1))
|
||||
assert.Equal(t, []uint32{2, 3, 4, 5}, chainIds(t, hm, a))
|
||||
|
||||
// Delete primary
|
||||
hm.DeleteHostInfo(h1)
|
||||
assert.Nil(t, h1.prev)
|
||||
assert.Nil(t, h1.next)
|
||||
// Deleting the same hostinfo again must not report final while siblings remain and must not
|
||||
// disturb the list. The old chain code got this wrong: the first delete nil'd next/prev, so a
|
||||
// second delete looked final and wiped lighthouse state out from under the live sibling.
|
||||
assert.False(t, hm.DeleteHostInfo(h1))
|
||||
assert.Equal(t, []uint32{2, 3, 4, 5}, chainIds(t, hm, a))
|
||||
|
||||
// Make sure we go h2 -> h3 -> h4 -> h5
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h2.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h3.localIndexId, h2.next.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h3.prev.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h3.next.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h4.prev.localIndexId)
|
||||
assert.Equal(t, h5.localIndexId, h4.next.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h5.prev.localIndexId)
|
||||
assert.Nil(t, h5.next)
|
||||
// Delete a middle node.
|
||||
assert.False(t, hm.DeleteHostInfo(h3))
|
||||
assert.Equal(t, []uint32{2, 4, 5}, chainIds(t, hm, a))
|
||||
|
||||
// Delete in the middle
|
||||
hm.DeleteHostInfo(h3)
|
||||
assert.Nil(t, h3.prev)
|
||||
assert.Nil(t, h3.next)
|
||||
// Delete the tail.
|
||||
assert.False(t, hm.DeleteHostInfo(h5))
|
||||
assert.Equal(t, []uint32{2, 4}, chainIds(t, hm, a))
|
||||
|
||||
// Make sure we go h2 -> h4 -> h5
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h2.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h4.localIndexId, h2.next.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h4.prev.localIndexId)
|
||||
assert.Equal(t, h5.localIndexId, h4.next.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h5.prev.localIndexId)
|
||||
assert.Nil(t, h5.next)
|
||||
// Delete the head; h4 remains and becomes primary.
|
||||
assert.False(t, hm.DeleteHostInfo(h2))
|
||||
assert.Equal(t, []uint32{4}, chainIds(t, hm, a))
|
||||
assert.Equal(t, h4, hm.QueryVpnAddr(a))
|
||||
|
||||
// Delete the tail
|
||||
hm.DeleteHostInfo(h5)
|
||||
assert.Nil(t, h5.prev)
|
||||
assert.Nil(t, h5.next)
|
||||
// Delete the only remaining item; final is true and the address is gone.
|
||||
assert.True(t, hm.DeleteHostInfo(h4))
|
||||
assert.Empty(t, chainIds(t, hm, a))
|
||||
assert.Nil(t, hm.QueryVpnAddr(a))
|
||||
|
||||
// Make sure we go h2 -> h4
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h2.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h4.localIndexId, h2.next.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h4.prev.localIndexId)
|
||||
assert.Nil(t, h4.next)
|
||||
// Deleting an already-gone hostinfo is still final; nothing holds the address anymore.
|
||||
assert.True(t, hm.DeleteHostInfo(h4))
|
||||
assert.Empty(t, chainIds(t, hm, a))
|
||||
}
|
||||
|
||||
// Delete the head
|
||||
hm.DeleteHostInfo(h2)
|
||||
assert.Nil(t, h2.prev)
|
||||
assert.Nil(t, h2.next)
|
||||
// TestHostMap_MakePrimary_DeletedHostInfo covers promoting a hostinfo that lost a race with
|
||||
// tunnel teardown: swapPrimary and AddRelay decide to promote while holding a stale pointer and
|
||||
// only take the write lock after a delete fully unlinked the hostinfo. MakePrimary must be a
|
||||
// no-op, not a resurrection that installs an unmanaged primary.
|
||||
func TestHostMap_MakePrimary_DeletedHostInfo(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hm := newHostMap(l)
|
||||
f := &Interface{}
|
||||
a := netip.MustParseAddr("0.0.0.1")
|
||||
|
||||
// Make sure we only have h4
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h4.localIndexId, prim.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Nil(t, prim.next)
|
||||
assert.Nil(t, h4.next)
|
||||
h1 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 1}
|
||||
h2 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 2}
|
||||
hm.unlockedAddHostInfo(h1, f)
|
||||
hm.unlockedAddHostInfo(h2, f)
|
||||
|
||||
// Delete the only item
|
||||
hm.DeleteHostInfo(h4)
|
||||
assert.Nil(t, h4.prev)
|
||||
assert.Nil(t, h4.next)
|
||||
// h1 is fully deleted while another goroutine still holds a pointer to it.
|
||||
assert.False(t, hm.DeleteHostInfo(h1))
|
||||
assert.Equal(t, []uint32{2}, chainIds(t, hm, a))
|
||||
|
||||
// Make sure we have nil
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Nil(t, prim)
|
||||
// The stale promote must not bring it back.
|
||||
hm.MakePrimary(h1)
|
||||
assert.Equal(t, []uint32{2}, chainIds(t, hm, a))
|
||||
assert.Equal(t, h2, hm.QueryVpnAddr(a))
|
||||
assert.Nil(t, hm.QueryIndex(h1.localIndexId))
|
||||
}
|
||||
|
||||
// TestHostMap_QueryVpnAddrsRelayFor_NonPrimary makes sure a relay established on an older
|
||||
// hostinfo is still found after a newer tunnel without relay state takes primary for the same
|
||||
// address. The lookup checks the primary first and falls back to the rest of the list.
|
||||
func TestHostMap_QueryVpnAddrsRelayFor_NonPrimary(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hm := newHostMap(l)
|
||||
f := &Interface{}
|
||||
relayAddr := netip.MustParseAddr("0.0.0.9")
|
||||
target := netip.MustParseAddr("0.0.0.1")
|
||||
|
||||
older := &HostInfo{
|
||||
vpnAddrs: []netip.Addr{relayAddr},
|
||||
localIndexId: 1,
|
||||
relayState: RelayState{
|
||||
relayForByAddr: map[netip.Addr]*Relay{},
|
||||
relayForByIdx: map[uint32]*Relay{},
|
||||
},
|
||||
}
|
||||
older.relayState.InsertRelay(target, 100, &Relay{Type: ForwardingType, State: Established, LocalIndex: 100, PeerAddr: target})
|
||||
hm.unlockedAddHostInfo(older, f)
|
||||
|
||||
// The relay is found on the primary.
|
||||
h, r, err := hm.QueryVpnAddrsRelayFor([]netip.Addr{target}, relayAddr)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, older, h)
|
||||
assert.Equal(t, uint32(100), r.LocalIndex)
|
||||
|
||||
// A re-handshake with no relay state takes primary; the established relay on the older
|
||||
// hostinfo must still be found through the fallback.
|
||||
newer := &HostInfo{vpnAddrs: []netip.Addr{relayAddr}, localIndexId: 2}
|
||||
hm.unlockedAddHostInfo(newer, f)
|
||||
assert.Equal(t, []uint32{2, 1}, chainIds(t, hm, relayAddr))
|
||||
|
||||
h, r, err = hm.QueryVpnAddrsRelayFor([]netip.Addr{target}, relayAddr)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, older, h)
|
||||
assert.Equal(t, uint32(100), r.LocalIndex)
|
||||
|
||||
// No hostinfo at all is a plain miss.
|
||||
_, _, err = hm.QueryVpnAddrsRelayFor([]netip.Addr{target}, netip.MustParseAddr("0.0.0.42"))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// TestHostMap_DeleteHostInfo_MultipleVpnAddrs exercises the case where a hostinfo carries more than one
|
||||
// vpnAddr and shares its next/prev chain with a live sibling. Deleting the head must not corrupt the
|
||||
// sibling: every address the sibling owns has to keep pointing at it. The pre-fix code unlinked the shared
|
||||
// chain once per vpnAddr, so on the first address it nil'd next/prev, and on the second address the node
|
||||
// looked already-detached: it dropped the map entry instead of promoting the sibling (and tripped the
|
||||
// isLastHostinfo relay teardown). See unlockedDeleteHostInfo.
|
||||
func TestHostMap_DeleteHostInfo_MultipleVpnAddrs(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hm := newHostMap(l)
|
||||
|
||||
f := &Interface{}
|
||||
|
||||
a := netip.MustParseAddr("0.0.0.1")
|
||||
b := netip.MustParseAddr("0.0.0.2")
|
||||
|
||||
// Two tunnels for the same peer, each reachable at both a and b.
|
||||
other := &HostInfo{vpnAddrs: []netip.Addr{a, b}, localIndexId: 1}
|
||||
head := &HostInfo{vpnAddrs: []netip.Addr{a, b}, localIndexId: 2}
|
||||
|
||||
hm.unlockedAddHostInfo(other, f)
|
||||
hm.unlockedAddHostInfo(head, f)
|
||||
|
||||
// head is primary for both addresses, other is next in each address's list.
|
||||
assert.Equal(t, head, hm.QueryVpnAddr(a))
|
||||
assert.Equal(t, head, hm.QueryVpnAddr(b))
|
||||
assert.Equal(t, []uint32{2, 1}, chainIds(t, hm, a))
|
||||
assert.Equal(t, []uint32{2, 1}, chainIds(t, hm, b))
|
||||
|
||||
// Delete the head. other is still live, so it must become primary for BOTH addresses.
|
||||
assert.False(t, hm.DeleteHostInfo(head))
|
||||
assert.Equal(t, other, hm.QueryVpnAddr(a))
|
||||
assert.Equal(t, other, hm.QueryVpnAddr(b))
|
||||
assert.Equal(t, []uint32{1}, chainIds(t, hm, a))
|
||||
assert.Equal(t, []uint32{1}, chainIds(t, hm, b))
|
||||
|
||||
// head is fully removed from the index map.
|
||||
assert.Nil(t, hm.QueryIndex(head.localIndexId))
|
||||
}
|
||||
|
||||
// TestHostMap_DeleteHostInfo_DivergentVpnAddrs covers chained hostinfos for the same peer whose
|
||||
// vpnAddrs sets differ (a re-handshake cert added a second address). Deleting the superset node
|
||||
// must not promote a sibling to an address it does not own.
|
||||
func TestHostMap_DeleteHostInfo_DivergentVpnAddrs(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hm := newHostMap(l)
|
||||
f := &Interface{}
|
||||
a := netip.MustParseAddr("0.0.0.1")
|
||||
b := netip.MustParseAddr("0.0.0.2")
|
||||
|
||||
// sub owns only a; super (a newer handshake) owns a and b.
|
||||
sub := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 1}
|
||||
super := &HostInfo{vpnAddrs: []netip.Addr{a, b}, localIndexId: 2}
|
||||
hm.unlockedAddHostInfo(sub, f)
|
||||
hm.unlockedAddHostInfo(super, f)
|
||||
|
||||
assert.Equal(t, []uint32{2, 1}, chainIds(t, hm, a))
|
||||
assert.Equal(t, []uint32{2}, chainIds(t, hm, b))
|
||||
|
||||
// Delete super: a promotes to sub (which owns it); b has no remaining owner and must be
|
||||
// removed, not dangled at sub (which does not own b).
|
||||
assert.False(t, hm.DeleteHostInfo(super))
|
||||
assert.Equal(t, []uint32{1}, chainIds(t, hm, a))
|
||||
assert.Empty(t, chainIds(t, hm, b))
|
||||
assert.Equal(t, sub, hm.QueryVpnAddr(a))
|
||||
assert.Nil(t, hm.QueryVpnAddr(b))
|
||||
assert.Nil(t, hm.QueryIndex(super.localIndexId))
|
||||
|
||||
// Deleting sub cleans up fully.
|
||||
assert.True(t, hm.DeleteHostInfo(sub))
|
||||
assert.Nil(t, hm.QueryVpnAddr(a))
|
||||
assertHostMapInvariants(t, hm)
|
||||
}
|
||||
|
||||
// TestHostMap_AddDivergentOverlap covers a new hostinfo claiming addresses currently owned by two
|
||||
// DIFFERENT hostinfos. The old single shared next/prev chain overwrote a pointer and orphaned one
|
||||
// of them (in Indexes but unreachable via its address); independent per-address lists cannot.
|
||||
func TestHostMap_AddDivergentOverlap(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hm := newHostMap(l)
|
||||
f := &Interface{}
|
||||
a := netip.MustParseAddr("0.0.0.1")
|
||||
b := netip.MustParseAddr("0.0.0.2")
|
||||
|
||||
hiA := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 1}
|
||||
hiP := &HostInfo{vpnAddrs: []netip.Addr{b}, localIndexId: 2}
|
||||
hm.unlockedAddHostInfo(hiA, f)
|
||||
hm.unlockedAddHostInfo(hiP, f)
|
||||
|
||||
hiB := &HostInfo{vpnAddrs: []netip.Addr{a, b}, localIndexId: 3}
|
||||
hm.unlockedAddHostInfo(hiB, f)
|
||||
|
||||
assert.Equal(t, []uint32{3, 1}, chainIds(t, hm, a))
|
||||
assert.Equal(t, []uint32{3, 2}, chainIds(t, hm, b))
|
||||
// hiA is still reachable via its address (not orphaned) and still indexed.
|
||||
assert.Contains(t, chainIds(t, hm, a), hiA.localIndexId)
|
||||
assert.NotNil(t, hm.QueryIndex(hiA.localIndexId))
|
||||
}
|
||||
|
||||
// TestHostMap_MaxHostInfosPerVpnIp_MultipleVpnAddrs verifies the MaxHostInfosPerVpnIp overflow prune
|
||||
// (unlockedInnerAddHostInfo calls unlockedDeleteHostInfo on the oldest node once the chain is too long)
|
||||
// still behaves when hostinfos carry more than one vpnAddr. The pruned node is always the tail, so it is
|
||||
// primary for none of the addresses, and both address chains must stay consistent afterwards.
|
||||
func TestHostMap_MaxHostInfosPerVpnIp_MultipleVpnAddrs(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hm := newHostMap(l)
|
||||
|
||||
f := &Interface{}
|
||||
|
||||
a := netip.MustParseAddr("0.0.0.1")
|
||||
b := netip.MustParseAddr("0.0.0.2")
|
||||
|
||||
// Add one more than the cap, newest last so it becomes head. Every hostinfo owns both a and b.
|
||||
hostinfos := make([]*HostInfo, 0, MaxHostInfosPerVpnIp+1)
|
||||
for i := 0; i <= MaxHostInfosPerVpnIp; i++ {
|
||||
hostinfos = append(hostinfos, &HostInfo{vpnAddrs: []netip.Addr{a, b}, localIndexId: uint32(i + 1)})
|
||||
}
|
||||
// Add oldest first (highest index in our slice) so the very first one added is the overflow victim.
|
||||
for i := len(hostinfos) - 1; i >= 0; i-- {
|
||||
hm.unlockedAddHostInfo(hostinfos[i], f)
|
||||
}
|
||||
|
||||
oldest := hostinfos[len(hostinfos)-1]
|
||||
|
||||
// The oldest hostinfo was pruned from both lists and the index map.
|
||||
assert.Nil(t, hm.QueryIndex(oldest.localIndexId))
|
||||
|
||||
// Both addresses hold exactly MaxHostInfosPerVpnIp survivors in the same order; oldest is absent.
|
||||
require.Len(t, chainIds(t, hm, a), MaxHostInfosPerVpnIp)
|
||||
assert.Equal(t, chainIds(t, hm, a), chainIds(t, hm, b), "both addresses must list the same survivors in the same order")
|
||||
assert.NotContains(t, chainIds(t, hm, a), oldest.localIndexId)
|
||||
assert.Equal(t, hm.QueryVpnAddr(a), hm.QueryVpnAddr(b))
|
||||
}
|
||||
|
||||
func TestHostMap_reload(t *testing.T) {
|
||||
|
||||
@@ -333,7 +333,7 @@ func (f *Interface) SendVia(via *HostInfo,
|
||||
via.logger(f.l).Info("Failed to EncryptDanger in sendVia", "error", err)
|
||||
return
|
||||
}
|
||||
err = f.writers[0].WriteTo(out, via.remote)
|
||||
err = f.writers[0].WriteTo(out, via.GetRemote())
|
||||
if err != nil {
|
||||
via.logger(f.l).Info("Failed to WriteTo in sendVia", "error", err)
|
||||
}
|
||||
@@ -344,7 +344,7 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType
|
||||
if ci.eKey == nil {
|
||||
return
|
||||
}
|
||||
useRelay := !remote.IsValid() && !hostinfo.remote.IsValid()
|
||||
useRelay := !remote.IsValid() && !hostinfo.GetRemote().IsValid()
|
||||
fullOut := out
|
||||
|
||||
if useRelay {
|
||||
@@ -403,8 +403,8 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType
|
||||
"udpAddr", remote,
|
||||
)
|
||||
}
|
||||
} else if hostinfo.remote.IsValid() {
|
||||
err = f.writers[q].WriteTo(out, hostinfo.remote)
|
||||
} else if hr := hostinfo.GetRemote(); hr.IsValid() {
|
||||
err = f.writers[q].WriteTo(out, hr)
|
||||
if err != nil {
|
||||
hostinfo.logger(f.l).Error("Failed to write outgoing packet",
|
||||
"error", err,
|
||||
|
||||
+29
-14
@@ -215,6 +215,9 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
|
||||
|
||||
ifce.connectionManager.intf = ifce
|
||||
|
||||
// Held until Close so waiting on the interface blocks until the resources are actually released
|
||||
ifce.wg.Add(1)
|
||||
|
||||
return ifce, nil
|
||||
}
|
||||
|
||||
@@ -258,17 +261,16 @@ func (f *Interface) activate() error {
|
||||
f.readers[i] = reader
|
||||
}
|
||||
|
||||
f.wg.Add(1) // for us to wait on Close() to return
|
||||
// On error the caller owns the cleanup, Control.Start cancels the service context
|
||||
// before releasing our resources so a waiter never observes a live context
|
||||
if err = f.inside.Activate(); err != nil {
|
||||
f.wg.Done()
|
||||
f.inside.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Interface) run() (func() error, error) {
|
||||
func (f *Interface) run() {
|
||||
// Launch n queues to read packets from udp
|
||||
for i := 0; i < f.routines; i++ {
|
||||
f.wg.Go(func() {
|
||||
@@ -283,13 +285,14 @@ func (f *Interface) run() (func() error, error) {
|
||||
})
|
||||
}
|
||||
|
||||
return func() error {
|
||||
f.wg.Wait()
|
||||
if e := f.fatalErr.Load(); e != nil {
|
||||
return *e
|
||||
}
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *Interface) wait() error {
|
||||
f.wg.Wait()
|
||||
if e := f.fatalErr.Load(); e != nil {
|
||||
return *e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// onFatal stores the first fatal reader error, and calls triggerShutdown if it was the first one
|
||||
@@ -322,7 +325,10 @@ func (f *Interface) listenOut(i int) {
|
||||
f.readOutsidePackets(ViaSender{UdpAddr: fromUdpAddr}, plaintext[:0], payload, h, fwPacket, lhh, nb, i, ctCache.Get())
|
||||
})
|
||||
|
||||
if err != nil && !f.closed.Load() {
|
||||
// An error after teardown began is shutdown noise, the closed flag covers resources
|
||||
// Close releases itself and the cancelled ctx covers ones torn down by their owners
|
||||
// reacting to it, like the user device pipes
|
||||
if err != nil && !f.closed.Load() && f.ctx.Err() == nil {
|
||||
f.l.Error("Error while reading inbound packet, closing", "error", err)
|
||||
f.onFatal(err)
|
||||
}
|
||||
@@ -341,7 +347,8 @@ func (f *Interface) listenIn(reader io.ReadWriteCloser, i int) {
|
||||
for {
|
||||
n, err := reader.Read(packet)
|
||||
if err != nil {
|
||||
if !f.closed.Load() {
|
||||
// Same shutdown noise handling as listenOut
|
||||
if !f.closed.Load() && f.ctx.Err() == nil {
|
||||
f.l.Error("Error while reading outbound packet, closing", "error", err, "reader", i)
|
||||
f.onFatal(err)
|
||||
}
|
||||
@@ -542,9 +549,15 @@ func (f *Interface) GetCertState() *CertState {
|
||||
return f.pki.getCertState()
|
||||
}
|
||||
|
||||
// Close releases the interface's resources: the udp sockets and the tun device.
|
||||
// It is idempotent and safe to call at any point in the lifecycle, including on an interface that never activated,
|
||||
// calls after the first return nil without doing anything.
|
||||
func (f *Interface) Close() error {
|
||||
if !f.closed.CompareAndSwap(false, true) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
f.closed.Store(true)
|
||||
|
||||
// Release the udp readers
|
||||
for i, u := range f.writers {
|
||||
@@ -560,6 +573,8 @@ func (f *Interface) Close() error {
|
||||
if closeErr != nil {
|
||||
errs = append(errs, closeErr)
|
||||
}
|
||||
|
||||
// Release the construction token so waiters know the resources are gone
|
||||
f.wg.Done()
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
+2
-2
@@ -344,7 +344,7 @@ func ipv6FindUpperProtocol(packet []byte) (nextHeader uint8, offset int, isFragm
|
||||
return nextHeader, offset, isFragment
|
||||
}
|
||||
nextHeader = packet[offset]
|
||||
offset += int(packet[offset+1]+1) << 3
|
||||
offset += (int(packet[offset+1]) + 1) << 3
|
||||
|
||||
case 44: // Fragment
|
||||
if len(packet) < offset+8 {
|
||||
@@ -361,7 +361,7 @@ func ipv6FindUpperProtocol(packet []byte) (nextHeader uint8, offset int, isFragm
|
||||
return nextHeader, offset, isFragment
|
||||
}
|
||||
nextHeader = packet[offset]
|
||||
offset += int(packet[offset+1]+2) << 2
|
||||
offset += (int(packet[offset+1]) + 2) << 2
|
||||
|
||||
default:
|
||||
return nextHeader, offset, isFragment
|
||||
|
||||
+153
-6
@@ -19,8 +19,11 @@ import (
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/header"
|
||||
"github.com/slackhq/nebula/logging"
|
||||
"github.com/slackhq/nebula/overlay"
|
||||
"github.com/slackhq/nebula/udp"
|
||||
"github.com/slackhq/nebula/util"
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
)
|
||||
|
||||
var ErrHostNotKnown = errors.New("host not known")
|
||||
@@ -65,6 +68,18 @@ type LightHouse struct {
|
||||
|
||||
advertiseAddrs atomic.Pointer[[]netip.AddrPort]
|
||||
|
||||
// tunMTU mirrors tun.mtu so locally discovered addrs can be checked for
|
||||
// links too small to carry a full-size nebula packet without fragmenting.
|
||||
tunMTU atomic.Int64
|
||||
// omitLowMTUAddrs drops such addrs from lighthouse updates (and demotes
|
||||
// the associated warnings to debug logs) instead of advertising them.
|
||||
omitLowMTUAddrs atomic.Bool
|
||||
// mtuWarned tracks the last classification logged per local addr so a
|
||||
// warning is only emitted when the classification changes, not on every
|
||||
// periodic update.
|
||||
mtuWarnLock sync.Mutex
|
||||
mtuWarned map[mtuWarnKey]linkMTUTier
|
||||
|
||||
// Addr's of relays that can be used by peers to access me
|
||||
relaysForMe atomic.Pointer[[]netip.Addr]
|
||||
|
||||
@@ -105,6 +120,7 @@ func NewLightHouseFromConfig(ctx context.Context, l *slog.Logger, c *config.C, c
|
||||
punchy: p,
|
||||
updateTrigger: make(chan struct{}, 1),
|
||||
queryChan: make(chan netip.Addr, c.GetUint32("handshakes.query_buffer", 64)),
|
||||
mtuWarned: make(map[mtuWarnKey]linkMTUTier),
|
||||
l: l,
|
||||
}
|
||||
lighthouses := make([]netip.Addr, 0)
|
||||
@@ -216,6 +232,23 @@ func (lh *LightHouse) reload(c *config.C, initial bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
if initial || c.HasChanged("tun.mtu") || c.HasChanged("lighthouse.omit_low_mtu_addrs") {
|
||||
lh.tunMTU.Store(int64(c.GetInt("tun.mtu", overlay.DefaultMTU)))
|
||||
lh.omitLowMTUAddrs.Store(c.GetBool("lighthouse.omit_low_mtu_addrs", false))
|
||||
|
||||
// Re-log any addrs whose links are still too small under the new values
|
||||
lh.mtuWarnLock.Lock()
|
||||
clear(lh.mtuWarned)
|
||||
lh.mtuWarnLock.Unlock()
|
||||
|
||||
if !initial {
|
||||
lh.l.Info("tun.mtu and/or lighthouse.omit_low_mtu_addrs has changed",
|
||||
"tunMTU", lh.tunMTU.Load(),
|
||||
"omitLowMTUAddrs", lh.omitLowMTUAddrs.Load(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if initial || c.HasChanged("lighthouse.interval") {
|
||||
lh.interval.Store(int64(c.GetInt("lighthouse.interval", 10)))
|
||||
|
||||
@@ -905,6 +938,108 @@ func (lh *LightHouse) TriggerUpdate() {
|
||||
}
|
||||
}
|
||||
|
||||
// linkMTUTier classifies how well a local addr's link MTU can carry
|
||||
// full-size nebula packets built from a tun packet of tun.mtu bytes.
|
||||
type linkMTUTier uint8
|
||||
|
||||
// mtuWarnKey identifies a local addr for MTU warning dedup purposes. The
|
||||
// interface name is included because the same addr can exist on multiple
|
||||
// links with different MTUs.
|
||||
type mtuWarnKey struct {
|
||||
ifName string
|
||||
addr netip.Addr
|
||||
}
|
||||
|
||||
const (
|
||||
// The link can carry both normal and relayed nebula traffic
|
||||
linkMTUOk linkMTUTier = iota
|
||||
// The link can carry normal nebula traffic, but relayed traffic (which
|
||||
// adds a second layer of encapsulation) will not fit
|
||||
linkMTUTooSmallForRelay
|
||||
// Even normal nebula traffic will not fit
|
||||
linkMTUTooSmall
|
||||
)
|
||||
|
||||
const (
|
||||
// Both AES-256-GCM and ChaCha20-Poly1305 append a 16 byte AEAD tag
|
||||
cipherTagLen = 16
|
||||
udpHeaderLen = 8
|
||||
)
|
||||
|
||||
// requiredLinkMTU returns the minimum underlay link MTU that can carry a
|
||||
// full-size tun packet to an addr of the given family without fragmentation,
|
||||
// both directly and via a relay (which wraps the packet in a second nebula
|
||||
// header and AEAD tag).
|
||||
func requiredLinkMTU(tunMTU int, is4 bool) (direct, relayed int) {
|
||||
ipHeaderLen := ipv6.HeaderLen
|
||||
if is4 {
|
||||
ipHeaderLen = ipv4.HeaderLen
|
||||
}
|
||||
|
||||
direct = tunMTU + header.Len + cipherTagLen + udpHeaderLen + ipHeaderLen
|
||||
relayed = direct + header.Len + cipherTagLen
|
||||
return direct, relayed
|
||||
}
|
||||
|
||||
// checkLocalLinkMTU classifies e's link MTU, logs when the classification
|
||||
// changes, and reports whether e should be advertised to lighthouses.
|
||||
func (lh *LightHouse) checkLocalLinkMTU(e localAddr) bool {
|
||||
tunMTU := int(lh.tunMTU.Load())
|
||||
omit := lh.omitLowMTUAddrs.Load()
|
||||
|
||||
tier := linkMTUOk
|
||||
direct, relayed := requiredLinkMTU(tunMTU, e.addr.Is4())
|
||||
if e.linkMTU > 0 { // links with an unknown MTU are advertised as-is
|
||||
if e.linkMTU < direct {
|
||||
tier = linkMTUTooSmall
|
||||
} else if e.linkMTU < relayed {
|
||||
tier = linkMTUTooSmallForRelay
|
||||
}
|
||||
}
|
||||
advertise := tier != linkMTUTooSmall || !omit
|
||||
|
||||
key := mtuWarnKey{ifName: e.ifName, addr: e.addr}
|
||||
lh.mtuWarnLock.Lock()
|
||||
changed := lh.mtuWarned[key] != tier
|
||||
if changed {
|
||||
if tier == linkMTUOk {
|
||||
delete(lh.mtuWarned, key)
|
||||
} else {
|
||||
lh.mtuWarned[key] = tier
|
||||
}
|
||||
}
|
||||
lh.mtuWarnLock.Unlock()
|
||||
|
||||
if !changed || tier == linkMTUOk {
|
||||
return advertise
|
||||
}
|
||||
|
||||
level := slog.LevelWarn
|
||||
if omit {
|
||||
level = slog.LevelDebug
|
||||
}
|
||||
|
||||
if lh.l.Enabled(context.Background(), level) {
|
||||
msg := "Link MTU too small for nebula traffic, expect fragmentation or drops"
|
||||
if !advertise {
|
||||
msg = "Omitting addr with too-small link MTU from lighthouse report"
|
||||
} else if tier == linkMTUTooSmallForRelay {
|
||||
msg = "Link MTU too small for relayed nebula traffic"
|
||||
}
|
||||
|
||||
lh.l.Log(context.Background(), level, msg,
|
||||
"localAddr", e.addr,
|
||||
"interface", e.ifName,
|
||||
"linkMTU", e.linkMTU,
|
||||
"requiredMTU", direct,
|
||||
"requiredRelayMTU", relayed,
|
||||
"tunMTU", tunMTU,
|
||||
)
|
||||
}
|
||||
|
||||
return advertise
|
||||
}
|
||||
|
||||
func (lh *LightHouse) SendUpdate() {
|
||||
var v4 []*V4AddrPort
|
||||
var v6 []*V6AddrPort
|
||||
@@ -919,15 +1054,19 @@ func (lh *LightHouse) SendUpdate() {
|
||||
|
||||
lal := lh.GetLocalAllowList()
|
||||
for _, e := range localAddrs(lh.l, lal) {
|
||||
if lh.myVpnNetworksTable.Contains(e) {
|
||||
if lh.myVpnNetworksTable.Contains(e.addr) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !lh.checkLocalLinkMTU(e) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only add addrs that aren't my VPN/tun networks
|
||||
if e.Is4() {
|
||||
v4 = append(v4, netAddrToProtoV4AddrPort(e, uint16(lh.nebulaPort)))
|
||||
if e.addr.Is4() {
|
||||
v4 = append(v4, netAddrToProtoV4AddrPort(e.addr, uint16(lh.nebulaPort)))
|
||||
} else {
|
||||
v6 = append(v6, netAddrToProtoV6AddrPort(e, uint16(lh.nebulaPort)))
|
||||
v6 = append(v6, netAddrToProtoV6AddrPort(e.addr, uint16(lh.nebulaPort)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1418,6 +1557,9 @@ func (lhh *LightHouseHandler) handleHostPunchNotification(n *NebulaMeta, fromVpn
|
||||
|
||||
remoteAllowList := lhh.lh.GetRemoteAllowList()
|
||||
for _, a := range n.Details.V4AddrPorts {
|
||||
if a == nil {
|
||||
continue
|
||||
}
|
||||
b := protoV4AddrPortToNetAddrPort(a)
|
||||
if remoteAllowList.Allow(detailsVpnAddr, b.Addr()) {
|
||||
lhh.lh.punchy.Schedule(b, detailsVpnAddr)
|
||||
@@ -1425,6 +1567,9 @@ func (lhh *LightHouseHandler) handleHostPunchNotification(n *NebulaMeta, fromVpn
|
||||
}
|
||||
|
||||
for _, a := range n.Details.V6AddrPorts {
|
||||
if a == nil {
|
||||
continue
|
||||
}
|
||||
b := protoV6AddrPortToNetAddrPort(a)
|
||||
if remoteAllowList.Allow(detailsVpnAddr, b.Addr()) {
|
||||
lhh.lh.punchy.Schedule(b, detailsVpnAddr)
|
||||
@@ -1454,7 +1599,7 @@ func protoV6AddrPortToNetAddrPort(ap *V6AddrPort) netip.AddrPort {
|
||||
b := [16]byte{}
|
||||
binary.BigEndian.PutUint64(b[:8], ap.Hi)
|
||||
binary.BigEndian.PutUint64(b[8:], ap.Lo)
|
||||
return netip.AddrPortFrom(netip.AddrFrom16(b), uint16(ap.Port))
|
||||
return netip.AddrPortFrom(netip.AddrFrom16(b).Unmap(), uint16(ap.Port))
|
||||
}
|
||||
|
||||
func netAddrToProtoAddr(addr netip.Addr) *Addr {
|
||||
@@ -1494,7 +1639,9 @@ func (d *NebulaMetaDetails) GetRelays() []netip.Addr {
|
||||
|
||||
if len(d.RelayVpnAddrs) > 0 {
|
||||
for _, r := range d.RelayVpnAddrs {
|
||||
relays = append(relays, protoAddrToNetAddr(r))
|
||||
if r != nil {
|
||||
relays = append(relays, protoAddrToNetAddr(r))
|
||||
}
|
||||
}
|
||||
}
|
||||
return relays
|
||||
|
||||
@@ -738,3 +738,86 @@ func TestLighthouse_DeletesWork(t *testing.T) {
|
||||
out = lh.Query(testHost)
|
||||
assert.Nil(t, out)
|
||||
}
|
||||
|
||||
func Test_requiredLinkMTU(t *testing.T) {
|
||||
// tun packet + nebula header (16) + AEAD tag (16) + udp (8) + ip header
|
||||
direct, relayed := requiredLinkMTU(1300, true)
|
||||
assert.Equal(t, 1360, direct)
|
||||
assert.Equal(t, 1392, relayed)
|
||||
|
||||
direct, relayed = requiredLinkMTU(1300, false)
|
||||
assert.Equal(t, 1380, direct)
|
||||
assert.Equal(t, 1412, relayed)
|
||||
}
|
||||
|
||||
func Test_checkLocalLinkMTU(t *testing.T) {
|
||||
lh := &LightHouse{l: test.NewLogger(), mtuWarned: make(map[mtuWarnKey]linkMTUTier)}
|
||||
lh.tunMTU.Store(1300)
|
||||
|
||||
v4 := netip.MustParseAddr("192.168.1.2")
|
||||
v6 := netip.MustParseAddr("fd00::2")
|
||||
mkAddr := func(a netip.Addr, mtu int) localAddr {
|
||||
return localAddr{addr: a, ifName: "test0", linkMTU: mtu}
|
||||
}
|
||||
|
||||
// Plenty of room, no state recorded
|
||||
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v4, 1500)))
|
||||
assert.Empty(t, lh.mtuWarned)
|
||||
|
||||
// Unknown link MTU is not classified
|
||||
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v4, 0)))
|
||||
assert.Empty(t, lh.mtuWarned)
|
||||
|
||||
// Too small for even normal traffic, still advertised by default
|
||||
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v4, 1359)))
|
||||
assert.Equal(t, linkMTUTooSmall, lh.mtuWarned[mtuWarnKey{ifName: "test0", addr: v4}])
|
||||
|
||||
// Fits normal traffic but not relayed traffic
|
||||
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v4, 1360)))
|
||||
assert.Equal(t, linkMTUTooSmallForRelay, lh.mtuWarned[mtuWarnKey{ifName: "test0", addr: v4}])
|
||||
|
||||
// Exactly enough for relayed traffic clears the state
|
||||
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v4, 1392)))
|
||||
assert.Empty(t, lh.mtuWarned)
|
||||
|
||||
// v6 addrs need 20 more bytes of headroom
|
||||
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v6, 1380)))
|
||||
assert.Equal(t, linkMTUTooSmallForRelay, lh.mtuWarned[mtuWarnKey{ifName: "test0", addr: v6}])
|
||||
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v6, 1379)))
|
||||
assert.Equal(t, linkMTUTooSmall, lh.mtuWarned[mtuWarnKey{ifName: "test0", addr: v6}])
|
||||
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v6, 1412)))
|
||||
assert.Empty(t, lh.mtuWarned)
|
||||
|
||||
// With omit enabled, only addrs that can't fit normal traffic are dropped
|
||||
lh.omitLowMTUAddrs.Store(true)
|
||||
assert.False(t, lh.checkLocalLinkMTU(mkAddr(v4, 1359)))
|
||||
assert.Equal(t, linkMTUTooSmall, lh.mtuWarned[mtuWarnKey{ifName: "test0", addr: v4}])
|
||||
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v4, 1360)))
|
||||
assert.Equal(t, linkMTUTooSmallForRelay, lh.mtuWarned[mtuWarnKey{ifName: "test0", addr: v4}])
|
||||
assert.False(t, lh.checkLocalLinkMTU(mkAddr(v4, 1359)))
|
||||
}
|
||||
|
||||
func Test_lighthouseMTUConfig(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
myVpnNet := netip.MustParsePrefix("10.128.0.1/16")
|
||||
nt := new(bart.Lite)
|
||||
nt.Insert(myVpnNet)
|
||||
cs := &CertState{
|
||||
myVpnNetworks: []netip.Prefix{myVpnNet},
|
||||
myVpnNetworksTable: nt,
|
||||
}
|
||||
|
||||
c := config.NewC(l)
|
||||
lh, err := NewLightHouseFromConfig(t.Context(), l, c, cs, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1300), lh.tunMTU.Load())
|
||||
assert.False(t, lh.omitLowMTUAddrs.Load())
|
||||
|
||||
c = config.NewC(l)
|
||||
c.Settings["tun"] = map[string]any{"mtu": 8000}
|
||||
c.Settings["lighthouse"] = map[string]any{"omit_low_mtu_addrs": true}
|
||||
lh, err = NewLightHouseFromConfig(t.Context(), l, c, cs, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(8000), lh.tunMTU.Load())
|
||||
assert.True(t, lh.omitLowMTUAddrs.Load())
|
||||
}
|
||||
|
||||
@@ -130,6 +130,17 @@ func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, dev
|
||||
udpConns := make([]udp.Conn, routines)
|
||||
port := c.GetInt("listen.port", 0)
|
||||
|
||||
// Callers get no handle to these until the Control is returned, release them on any error.
|
||||
defer func() {
|
||||
if reterr != nil {
|
||||
for _, u := range udpConns {
|
||||
if u != nil {
|
||||
_ = u.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if !configTest {
|
||||
rawListenHost := c.GetString("listen.host", "0.0.0.0")
|
||||
var listenHost netip.Addr
|
||||
|
||||
+12
-11
@@ -150,7 +150,8 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
|
||||
case header.TestReply:
|
||||
// No-op, useful for the Roaming and connectionManager side-effects above
|
||||
case header.TestRequest:
|
||||
f.send(header.Test, header.TestReply, ci, hostinfo, out, nb, out)
|
||||
//recycle the input packet ciphertext as our output buffer
|
||||
f.send(header.Test, header.TestReply, ci, hostinfo, out, nb, packet)
|
||||
default:
|
||||
hostinfo.logger(f.l).Error("IsValidSubType was true, but unexpected test subtype seen", "from", via, "header", h)
|
||||
return
|
||||
@@ -276,7 +277,8 @@ func (f *Interface) sendCloseTunnel(h *HostInfo) {
|
||||
}
|
||||
|
||||
func (f *Interface) handleHostRoaming(hostinfo *HostInfo, via ViaSender) {
|
||||
if !via.IsRelayed && hostinfo.remote != via.UdpAddr {
|
||||
curRemote := hostinfo.GetRemote()
|
||||
if !via.IsRelayed && curRemote != via.UdpAddr {
|
||||
if !f.lightHouse.GetRemoteAllowList().AllowAll(hostinfo.vpnAddrs, via.UdpAddr.Addr()) {
|
||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
hostinfo.logger(f.l).Debug("lighthouse.remote_allow_list denied roaming", "newAddr", via.UdpAddr)
|
||||
@@ -288,7 +290,7 @@ func (f *Interface) handleHostRoaming(hostinfo *HostInfo, via ViaSender) {
|
||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
hostinfo.logger(f.l).Debug("Suppressing roam back to previous remote",
|
||||
"suppressSeconds", RoamingSuppressSeconds,
|
||||
"udpAddr", hostinfo.remote,
|
||||
"udpAddr", curRemote,
|
||||
"newAddr", via.UdpAddr,
|
||||
)
|
||||
}
|
||||
@@ -296,11 +298,11 @@ func (f *Interface) handleHostRoaming(hostinfo *HostInfo, via ViaSender) {
|
||||
}
|
||||
|
||||
hostinfo.logger(f.l).Info("Host roamed to new udp ip/port.",
|
||||
"udpAddr", hostinfo.remote,
|
||||
"udpAddr", curRemote,
|
||||
"newAddr", via.UdpAddr,
|
||||
)
|
||||
hostinfo.lastRoam = time.Now()
|
||||
hostinfo.lastRoamRemote = hostinfo.remote
|
||||
hostinfo.lastRoamRemote = curRemote
|
||||
hostinfo.SetRemote(via.UdpAddr)
|
||||
}
|
||||
|
||||
@@ -420,16 +422,14 @@ func parseV6(data []byte, incoming bool, fp *firewall.Packet) error {
|
||||
if dataLen <= offset+1 {
|
||||
break
|
||||
}
|
||||
|
||||
next = int(data[offset+1]+2) << 2
|
||||
next = (int(data[offset+1]) + 2) << 2
|
||||
|
||||
default:
|
||||
// Normal ipv6 header length processing
|
||||
if dataLen <= offset+1 {
|
||||
break
|
||||
}
|
||||
|
||||
next = int(data[offset+1]+1) << 3
|
||||
next = (int(data[offset+1]) + 1) << 3
|
||||
}
|
||||
|
||||
if next <= 0 {
|
||||
@@ -589,10 +589,11 @@ func (f *Interface) handleRecvError(addr netip.AddrPort, h *header.H) {
|
||||
return
|
||||
}
|
||||
|
||||
if hostinfo.remote.IsValid() && hostinfo.remote != addr {
|
||||
hr := hostinfo.GetRemote()
|
||||
if hr.IsValid() && hr != addr {
|
||||
f.l.Info("Someone spoofing recv_errors?",
|
||||
"addr", addr,
|
||||
"hostinfoRemote", hostinfo.remote,
|
||||
"hostinfoRemote", hr,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -640,3 +640,38 @@ func serializeAH(ah *layers.IPSecAH) []byte {
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// Test_newPacket_v6ExtHeaderOverflow is a regression test for the IPv6 extension-header
|
||||
// length uint8 overflow in parseV6. A Destination-Options header with HdrExtLen=255 spans
|
||||
// (255+1)*8 = 2048 bytes, so the real transport header sits at offset 2088. Before the fix
|
||||
// the advance was computed in uint8 and wrapped to 0 (then clamped to 8), so the firewall
|
||||
// read the transport header ~2KB too early from attacker-controlled option bytes while the
|
||||
// host OS parses the real header, a firewall port/proto bypass. The fix makes parseV6 land
|
||||
// on the same offset the host does.
|
||||
func Test_newPacket_v6ExtHeaderOverflow(t *testing.T) {
|
||||
p := &firewall.Packet{}
|
||||
|
||||
const (
|
||||
hdrLen = 40 // IPv6 header
|
||||
extLen = 2048 // (255+1)*8, the true Destination-Options header size
|
||||
realTCPAt = hdrLen + extLen // 2088, where the host reads the transport header
|
||||
forgedTCPAt = hdrLen + 8 // 48, where the pre-fix wrapped+clamped walk landed
|
||||
)
|
||||
|
||||
pkt := make([]byte, realTCPAt+4)
|
||||
pkt[0] = 0x60 // version 6
|
||||
pkt[6] = byte(layers.IPProtocolIPv6Destination) // NextHeader -> Destination Options
|
||||
pkt[40] = byte(firewall.ProtoTCP) // Dest-Options NextHeader -> TCP
|
||||
pkt[41] = 255 // HdrExtLen = 255
|
||||
|
||||
// Forged transport header at the pre-fix (wrong) offset: dst port 443.
|
||||
binary.BigEndian.PutUint16(pkt[forgedTCPAt+2:forgedTCPAt+4], 443)
|
||||
// Real transport header at the offset the host actually uses: dst port 22.
|
||||
binary.BigEndian.PutUint16(pkt[realTCPAt+2:realTCPAt+4], 22)
|
||||
|
||||
require.NoError(t, newPacket(pkt, true, p))
|
||||
assert.Equal(t, uint8(firewall.ProtoTCP), p.Protocol)
|
||||
// LocalPort is the destination port for incoming traffic. It must be the real port (22)
|
||||
// the host delivers to, not the forged 443 at the overflowed offset.
|
||||
assert.Equal(t, uint16(22), p.LocalPort, "firewall must parse the real transport header, not the overflowed offset")
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ func newTunFromFd(c *config.C, l *slog.Logger, deviceFd int, vpnNetworks []netip
|
||||
|
||||
err := t.reload(c, true)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
+90
-32
@@ -23,7 +23,7 @@ import (
|
||||
)
|
||||
|
||||
type tun struct {
|
||||
io.ReadWriteCloser
|
||||
f *os.File
|
||||
Device string
|
||||
vpnNetworks []netip.Prefix
|
||||
DefaultMTU int
|
||||
@@ -31,9 +31,6 @@ type tun struct {
|
||||
routeTree atomic.Pointer[bart.Table[routing.Gateways]]
|
||||
linkAddr *netroute.LinkAddr
|
||||
l *slog.Logger
|
||||
|
||||
// cache out buffer since we need to prepend 4 bytes for tun metadata
|
||||
out []byte
|
||||
}
|
||||
|
||||
type ifReq struct {
|
||||
@@ -124,11 +121,11 @@ func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*t
|
||||
}
|
||||
|
||||
t := &tun{
|
||||
ReadWriteCloser: os.NewFile(uintptr(fd), ""),
|
||||
Device: name,
|
||||
vpnNetworks: vpnNetworks,
|
||||
DefaultMTU: c.GetInt("tun.mtu", DefaultMTU),
|
||||
l: l,
|
||||
f: os.NewFile(uintptr(fd), ""),
|
||||
Device: name,
|
||||
vpnNetworks: vpnNetworks,
|
||||
DefaultMTU: c.GetInt("tun.mtu", DefaultMTU),
|
||||
l: l,
|
||||
}
|
||||
|
||||
err = t.reload(c, true)
|
||||
@@ -158,8 +155,8 @@ func newTunFromFd(_ *config.C, _ *slog.Logger, _ int, _ []netip.Prefix) (*tun, e
|
||||
}
|
||||
|
||||
func (t *tun) Close() error {
|
||||
if t.ReadWriteCloser != nil {
|
||||
return t.ReadWriteCloser.Close()
|
||||
if t.f != nil {
|
||||
return t.f.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -502,42 +499,103 @@ func delRoute(prefix netip.Prefix, gateway netroute.Addr) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// tunWritev and tunReadv are linkname'd to x/sys/unix's libc-routed writev/readv stubs so the
|
||||
// calls go through libSystem's pinned trampoline. A raw syscall.Syscall(SYS_WRITEV/SYS_READV, ...)
|
||||
// on darwin/arm64 emits an SVC #0x80 trap (see $GOROOT/src/syscall/asm_darwin_arm64.s), the path
|
||||
// Apple keeps warning they will eventually disallow. We pull the low-level stubs instead of calling
|
||||
// unix.Writev/unix.Readv because those take [][]byte and rebuild the []Iovec every call, which
|
||||
// heap-allocates the header; linkname'ing the stubs lets us hand them our own stack-allocated
|
||||
// iovecs. See golang/go#78049.
|
||||
|
||||
//go:linkname tunWritev golang.org/x/sys/unix.writev
|
||||
//go:noescape
|
||||
func tunWritev(fd int, iovecs []unix.Iovec) (n int, err error)
|
||||
|
||||
//go:linkname tunReadv golang.org/x/sys/unix.readv
|
||||
//go:noescape
|
||||
func tunReadv(fd int, iovecs []unix.Iovec) (n int, err error)
|
||||
|
||||
// Read pulls one IP packet off the utun device, scattering the 4 byte protocol header away from
|
||||
// the packet so the payload lands directly in to.
|
||||
func (t *tun) Read(to []byte) (int, error) {
|
||||
buf := make([]byte, len(to)+4)
|
||||
var head [4]byte
|
||||
|
||||
n, err := t.ReadWriteCloser.Read(buf)
|
||||
rc, err := t.f.SyscallConn()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
copy(to, buf[4:])
|
||||
return n - 4, err
|
||||
var n int
|
||||
var callErr error
|
||||
err = rc.Read(func(fd uintptr) bool {
|
||||
iovecs := []unix.Iovec{
|
||||
{Base: &head[0], Len: 4},
|
||||
{Base: &to[0], Len: uint64(len(to))},
|
||||
}
|
||||
n, callErr = tunReadv(int(fd), iovecs)
|
||||
if errno, ok := callErr.(syscall.Errno); ok && errno.Temporary() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if callErr != nil {
|
||||
return 0, callErr
|
||||
}
|
||||
if n < 4 {
|
||||
return 0, nil
|
||||
}
|
||||
return n - 4, nil
|
||||
}
|
||||
|
||||
// Write is only valid for single threaded use
|
||||
// Write pushes one IP packet onto the utun device.
|
||||
func (t *tun) Write(from []byte) (int, error) {
|
||||
buf := t.out
|
||||
if cap(buf) < len(from)+4 {
|
||||
buf = make([]byte, len(from)+4)
|
||||
t.out = buf
|
||||
}
|
||||
buf = buf[:len(from)+4]
|
||||
|
||||
if len(from) == 0 {
|
||||
return 0, syscall.EIO
|
||||
}
|
||||
|
||||
// Determine the IP Family for the NULL L2 Header
|
||||
ipVer := from[0] >> 4
|
||||
if ipVer == 4 {
|
||||
buf[3] = syscall.AF_INET
|
||||
} else if ipVer == 6 {
|
||||
buf[3] = syscall.AF_INET6
|
||||
} else {
|
||||
var head [4]byte
|
||||
switch ipVer {
|
||||
case 4:
|
||||
head[3] = syscall.AF_INET
|
||||
case 6:
|
||||
head[3] = syscall.AF_INET6
|
||||
default:
|
||||
return 0, fmt.Errorf("unable to determine IP version from packet")
|
||||
}
|
||||
|
||||
copy(buf[4:], from)
|
||||
// Grab rc as a local so the compiler can devirtualize the call and keep the closure on the stack.
|
||||
rc, err := t.f.SyscallConn()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n, err := t.ReadWriteCloser.Write(buf)
|
||||
return n - 4, err
|
||||
var n int
|
||||
var callErr error
|
||||
err = rc.Write(func(fd uintptr) bool {
|
||||
iovecs := []unix.Iovec{
|
||||
{Base: &head[0], Len: 4},
|
||||
{Base: &from[0], Len: uint64(len(from))},
|
||||
}
|
||||
n, callErr = tunWritev(int(fd), iovecs)
|
||||
// Type-assert to syscall.Errno so the EAGAIN/EWOULDBLOCK/EINTR check doesn't box the errno
|
||||
// constants into error interfaces on every call.
|
||||
if errno, ok := callErr.(syscall.Errno); ok && errno.Temporary() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if callErr != nil {
|
||||
return 0, callErr
|
||||
}
|
||||
|
||||
return n - 4, nil
|
||||
}
|
||||
|
||||
func (t *tun) Networks() []netip.Prefix {
|
||||
|
||||
@@ -659,7 +659,6 @@ func addRoute(prefix netip.Prefix, gateway netroute.Addr) error {
|
||||
return fmt.Errorf("failed to create route.RouteMessage for change: %w", err)
|
||||
}
|
||||
_, err = unix.Write(sock, data[:])
|
||||
fmt.Println("DOING CHANGE")
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("failed to write route.RouteMessage to socket: %w", err)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/routing"
|
||||
"github.com/slackhq/nebula/util"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type tun struct {
|
||||
@@ -33,6 +34,12 @@ func newTun(_ *config.C, _ *slog.Logger, _ []netip.Prefix, _ bool) (*tun, error)
|
||||
}
|
||||
|
||||
func newTunFromFd(c *config.C, l *slog.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) {
|
||||
if err := unix.SetNonblock(deviceFd, true); err != nil {
|
||||
// We own the fd from the moment it is handed to us, same as the reload error path below
|
||||
_ = unix.Close(deviceFd)
|
||||
return nil, fmt.Errorf("failed to set the tun fd to non-blocking mode: %w", err)
|
||||
}
|
||||
|
||||
file := os.NewFile(uintptr(deviceFd), "/dev/tun")
|
||||
t := &tun{
|
||||
vpnNetworks: vpnNetworks,
|
||||
@@ -42,6 +49,7 @@ func newTunFromFd(c *config.C, l *slog.Logger, deviceFd int, vpnNetworks []netip
|
||||
|
||||
err := t.reload(c, true)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
+82
-23
@@ -57,8 +57,6 @@ type tun struct {
|
||||
l *slog.Logger
|
||||
f *os.File
|
||||
fd int
|
||||
// cache out buffer since we need to prepend 4 bytes for tun metadata
|
||||
out []byte
|
||||
}
|
||||
|
||||
var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`)
|
||||
@@ -124,42 +122,103 @@ func (t *tun) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// tunWritev and tunReadv are linkname'd to x/sys/unix's libc-routed writev/readv stubs so the
|
||||
// calls go through libc's pinned trampoline. OpenBSD's pinsyscall protection rejects a raw
|
||||
// syscall.Syscall(SYS_WRITEV/SYS_READV, ...) because it doesn't originate from a libc-pinned
|
||||
// address, so we can't use the syscall.Syscall pattern that freebsd / netbsd use. We pull the
|
||||
// low-level stubs instead of calling unix.Writev/unix.Readv because those take [][]byte and rebuild
|
||||
// the []Iovec every call, which heap-allocates the header; linkname'ing the stubs lets us hand them
|
||||
// our own stack-allocated iovecs. See golang/go#78049.
|
||||
|
||||
//go:linkname tunWritev golang.org/x/sys/unix.writev
|
||||
//go:noescape
|
||||
func tunWritev(fd int, iovecs []unix.Iovec) (n int, err error)
|
||||
|
||||
//go:linkname tunReadv golang.org/x/sys/unix.readv
|
||||
//go:noescape
|
||||
func tunReadv(fd int, iovecs []unix.Iovec) (n int, err error)
|
||||
|
||||
// Read pulls one IP packet off the tun device, scattering the 4 byte protocol header away from the
|
||||
// packet so the payload lands directly in to.
|
||||
func (t *tun) Read(to []byte) (int, error) {
|
||||
buf := make([]byte, len(to)+4)
|
||||
var head [4]byte
|
||||
|
||||
n, err := t.f.Read(buf)
|
||||
rc, err := t.f.SyscallConn()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
copy(to, buf[4:])
|
||||
return n - 4, err
|
||||
var n int
|
||||
var callErr error
|
||||
err = rc.Read(func(fd uintptr) bool {
|
||||
iovecs := []unix.Iovec{
|
||||
{Base: &head[0], Len: 4},
|
||||
{Base: &to[0], Len: uint64(len(to))},
|
||||
}
|
||||
n, callErr = tunReadv(int(fd), iovecs)
|
||||
if errno, ok := callErr.(syscall.Errno); ok && errno.Temporary() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if callErr != nil {
|
||||
return 0, callErr
|
||||
}
|
||||
if n < 4 {
|
||||
return 0, nil
|
||||
}
|
||||
return n - 4, nil
|
||||
}
|
||||
|
||||
// Write is only valid for single threaded use
|
||||
// Write pushes one IP packet onto the tun device.
|
||||
func (t *tun) Write(from []byte) (int, error) {
|
||||
buf := t.out
|
||||
if cap(buf) < len(from)+4 {
|
||||
buf = make([]byte, len(from)+4)
|
||||
t.out = buf
|
||||
}
|
||||
buf = buf[:len(from)+4]
|
||||
|
||||
if len(from) == 0 {
|
||||
return 0, syscall.EIO
|
||||
}
|
||||
|
||||
// Determine the IP Family for the NULL L2 Header
|
||||
ipVer := from[0] >> 4
|
||||
if ipVer == 4 {
|
||||
buf[3] = syscall.AF_INET
|
||||
} else if ipVer == 6 {
|
||||
buf[3] = syscall.AF_INET6
|
||||
} else {
|
||||
var head [4]byte
|
||||
switch ipVer {
|
||||
case 4:
|
||||
head[3] = syscall.AF_INET
|
||||
case 6:
|
||||
head[3] = syscall.AF_INET6
|
||||
default:
|
||||
return 0, fmt.Errorf("unable to determine IP version from packet")
|
||||
}
|
||||
|
||||
copy(buf[4:], from)
|
||||
// Grab rc as a local so the compiler can devirtualize the call and keep the closure on the stack.
|
||||
rc, err := t.f.SyscallConn()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n, err := t.f.Write(buf)
|
||||
return n - 4, err
|
||||
var n int
|
||||
var callErr error
|
||||
err = rc.Write(func(fd uintptr) bool {
|
||||
iovecs := []unix.Iovec{
|
||||
{Base: &head[0], Len: 4},
|
||||
{Base: &from[0], Len: uint64(len(from))},
|
||||
}
|
||||
n, callErr = tunWritev(int(fd), iovecs)
|
||||
// Type-assert to syscall.Errno so the EAGAIN/EWOULDBLOCK/EINTR check doesn't box the errno
|
||||
// constants into error interfaces on every call.
|
||||
if errno, ok := callErr.(syscall.Errno); ok && errno.Temporary() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if callErr != nil {
|
||||
return 0, callErr
|
||||
}
|
||||
|
||||
return n - 4, nil
|
||||
}
|
||||
|
||||
func (t *tun) addIp(cidr netip.Prefix) error {
|
||||
|
||||
@@ -174,9 +174,9 @@ func (p *Punchy) SendPunch(hostinfo *HostInfo) {
|
||||
|
||||
if p.punchEverything.Load() {
|
||||
p.sendPunchToAllRemotes(hostinfo)
|
||||
} else if hostinfo.remote.IsValid() {
|
||||
} else if hr := hostinfo.GetRemote(); hr.IsValid() {
|
||||
p.metricPunchyTx.Inc(1)
|
||||
p.punchConn.WriteTo([]byte{1}, hostinfo.remote)
|
||||
p.punchConn.WriteTo([]byte{1}, hr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+30
-4
@@ -94,7 +94,7 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hh *Handshak
|
||||
}
|
||||
|
||||
relayHostInfo := rm.hostmap.QueryVpnAddr(relay)
|
||||
if relayHostInfo == nil || !relayHostInfo.remote.IsValid() {
|
||||
if relayHostInfo == nil || !relayHostInfo.GetRemote().IsValid() {
|
||||
hl.Log(context.Background(), level, "Establish tunnel to relay target", "relay", relay.String())
|
||||
f.Handshake(relay)
|
||||
continue
|
||||
@@ -104,10 +104,13 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hh *Handshak
|
||||
existingRelay, ok := relayHostInfo.relayState.QueryRelayForByIp(vpnIp)
|
||||
if !ok {
|
||||
// No relays exist or requested yet.
|
||||
if relayHostInfo.remote.IsValid() {
|
||||
if relayHostInfo.GetRemote().IsValid() {
|
||||
idx, err := AddRelay(rm.l, relayHostInfo, rm.hostmap, vpnIp, nil, TerminalType, Requested)
|
||||
if err != nil {
|
||||
// No local relay state was installed, so a CreateRelayRequest would hand the
|
||||
// peer an index we could never resolve. Skip it.
|
||||
hl.Info("Failed to add relay to hostmap", "relay", relay.String(), "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
m := NebulaControl{
|
||||
@@ -237,7 +240,12 @@ func AddRelay(l *slog.Logger, relayHostInfo *HostInfo, hm *HostMap, vpnIp netip.
|
||||
// Avoid standing up a relay that can't be used since only the primary hostinfo
|
||||
// will be pointed to by the relay logic
|
||||
//TODO: if there was an existing primary and it had relay state, should we merge?
|
||||
hm.unlockedMakePrimary(relayHostInfo)
|
||||
if !hm.unlockedMakePrimary(relayHostInfo) {
|
||||
// The tunnel was torn down after the caller grabbed relayHostInfo. A relay standing
|
||||
// on an unlinked hostinfo would never carry traffic, and its Relays entry could
|
||||
// never be reclaimed since the delete-time cleanup has already run.
|
||||
return 0, errors.New("relay hostinfo is no longer in the hostmap")
|
||||
}
|
||||
|
||||
hm.Relays[index] = relayHostInfo
|
||||
newRelay := Relay{
|
||||
@@ -309,6 +317,22 @@ func (rm *relayManager) HandleControlMsg(h *HostInfo, d []byte, f *Interface) {
|
||||
v = cert.Version2
|
||||
}
|
||||
|
||||
// validate:
|
||||
switch msg.Type {
|
||||
case NebulaControl_CreateRelayRequest, NebulaControl_CreateRelayResponse:
|
||||
if msg.RelayFromAddr == nil {
|
||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
h.logger(f.l).Debug("Control message received with nil RelayFromAddr", "type", msg.Type)
|
||||
}
|
||||
return
|
||||
} else if msg.RelayToAddr == nil {
|
||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
h.logger(f.l).Debug("Control message received with nil RelayToAddr", "type", msg.Type)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case NebulaControl_CreateRelayRequest:
|
||||
rm.handleCreateRelayRequest(v, h, f, msg)
|
||||
@@ -318,6 +342,7 @@ func (rm *relayManager) HandleControlMsg(h *HostInfo, d []byte, f *Interface) {
|
||||
}
|
||||
|
||||
func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f *Interface, m *NebulaControl) {
|
||||
//nil-checks for protoAddrToNetAddr handled by caller
|
||||
relayFrom := protoAddrToNetAddr(m.RelayFromAddr)
|
||||
relayTo := protoAddrToNetAddr(m.RelayToAddr)
|
||||
rm.l.Info("handleCreateRelayResponse",
|
||||
@@ -399,6 +424,7 @@ func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f
|
||||
}
|
||||
|
||||
func (rm *relayManager) handleCreateRelayRequest(v cert.Version, h *HostInfo, f *Interface, m *NebulaControl) {
|
||||
//nil-checks for protoAddrToNetAddr handled by caller
|
||||
from := protoAddrToNetAddr(m.RelayFromAddr)
|
||||
target := protoAddrToNetAddr(m.RelayToAddr)
|
||||
|
||||
@@ -508,7 +534,7 @@ func (rm *relayManager) handleCreateRelayRequest(v cert.Version, h *HostInfo, f
|
||||
f.Handshake(target)
|
||||
return
|
||||
}
|
||||
if !peer.remote.IsValid() {
|
||||
if !peer.GetRemote().IsValid() {
|
||||
// Only create relays to peers for whom I have a direct connection
|
||||
return
|
||||
}
|
||||
|
||||
@@ -344,6 +344,9 @@ func (r *RemoteList) CopyCache() *CacheMap {
|
||||
}
|
||||
|
||||
for _, a := range mc.v4.reported {
|
||||
if a == nil {
|
||||
continue
|
||||
}
|
||||
c.Reported = append(c.Reported, protoV4AddrPortToNetAddrPort(a))
|
||||
}
|
||||
}
|
||||
@@ -354,6 +357,9 @@ func (r *RemoteList) CopyCache() *CacheMap {
|
||||
}
|
||||
|
||||
for _, a := range mc.v6.reported {
|
||||
if a == nil {
|
||||
continue
|
||||
}
|
||||
c.Reported = append(c.Reported, protoV6AddrPortToNetAddrPort(a))
|
||||
}
|
||||
}
|
||||
@@ -582,6 +588,9 @@ func (r *RemoteList) unlockedCollect() {
|
||||
}
|
||||
|
||||
for _, v := range c.v4.reported {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
u := protoV4AddrPortToNetAddrPort(v)
|
||||
if !r.unlockedIsBad(u) {
|
||||
addrs = append(addrs, u)
|
||||
@@ -598,6 +607,9 @@ func (r *RemoteList) unlockedCollect() {
|
||||
}
|
||||
|
||||
for _, v := range c.v6.reported {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
u := protoV6AddrPortToNetAddrPort(v)
|
||||
if !r.unlockedIsBad(u) {
|
||||
addrs = append(addrs, u)
|
||||
|
||||
+16
-8
@@ -43,12 +43,25 @@ type Service struct {
|
||||
}
|
||||
}
|
||||
|
||||
func New(control *nebula.Control) (*Service, error) {
|
||||
wait, err := control.Start()
|
||||
func New(control *nebula.Control) (_ *Service, reterr error) {
|
||||
// Check this before Start so a failure doesn't leave a running nebula
|
||||
device, ok := control.Device().(*overlay.UserDevice)
|
||||
if !ok {
|
||||
return nil, errors.New("must be using user device")
|
||||
}
|
||||
|
||||
err := control.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Anything that fails after a successful Start must tear nebula back down
|
||||
defer func() {
|
||||
if reterr != nil {
|
||||
control.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
ctx := control.Context()
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
s := Service{
|
||||
@@ -57,11 +70,6 @@ func New(control *nebula.Control) (*Service, error) {
|
||||
}
|
||||
s.mu.listeners = map[uint16]*tcpListener{}
|
||||
|
||||
device, ok := control.Device().(*overlay.UserDevice)
|
||||
if !ok {
|
||||
return nil, errors.New("must be using user device")
|
||||
}
|
||||
|
||||
s.ipstack = stack.New(stack.Options{
|
||||
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
|
||||
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol4, icmp.NewProtocol6},
|
||||
@@ -147,7 +155,7 @@ func New(control *nebula.Control) (*Service, error) {
|
||||
// Add the nebula wait function to the group so a fatal reader error
|
||||
// propagates out through errgroup.Wait().
|
||||
eg.Go(func() error {
|
||||
return wait()
|
||||
return control.Wait()
|
||||
})
|
||||
|
||||
return &s, nil
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/armon/go-radix"
|
||||
"golang.org/x/crypto/ssh"
|
||||
@@ -18,6 +19,8 @@ type SSHServer struct {
|
||||
|
||||
certChecker *ssh.CertChecker
|
||||
|
||||
// authLock guards trustedKeys and trustedCAs
|
||||
authLock sync.RWMutex
|
||||
// Map of user -> authorized keys
|
||||
trustedKeys map[string]map[string]bool
|
||||
trustedCAs []ssh.PublicKey
|
||||
@@ -45,6 +48,8 @@ func NewSSHServer(ctx context.Context, l *slog.Logger) (*SSHServer, error) {
|
||||
|
||||
cc := ssh.CertChecker{
|
||||
IsUserAuthority: func(auth ssh.PublicKey) bool {
|
||||
s.authLock.RLock()
|
||||
defer s.authLock.RUnlock()
|
||||
for _, ca := range s.trustedCAs {
|
||||
if bytes.Equal(ca.Marshal(), auth.Marshal()) {
|
||||
return true
|
||||
@@ -57,6 +62,8 @@ func NewSSHServer(ctx context.Context, l *slog.Logger) (*SSHServer, error) {
|
||||
pk := string(pubKey.Marshal())
|
||||
fp := ssh.FingerprintSHA256(pubKey)
|
||||
|
||||
s.authLock.RLock()
|
||||
defer s.authLock.RUnlock()
|
||||
tk, ok := s.trustedKeys[c.User()]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown user %s", c.User())
|
||||
@@ -105,11 +112,15 @@ func (s *SSHServer) SetHostKey(hostPrivateKey []byte) error {
|
||||
}
|
||||
|
||||
func (s *SSHServer) ClearTrustedCAs() {
|
||||
s.authLock.Lock()
|
||||
s.trustedCAs = []ssh.PublicKey{}
|
||||
s.authLock.Unlock()
|
||||
}
|
||||
|
||||
func (s *SSHServer) ClearAuthorizedKeys() {
|
||||
s.authLock.Lock()
|
||||
s.trustedKeys = make(map[string]map[string]bool)
|
||||
s.authLock.Unlock()
|
||||
}
|
||||
|
||||
// AddTrustedCA adds a trusted CA for user certificates
|
||||
@@ -119,7 +130,9 @@ func (s *SSHServer) AddTrustedCA(pubKey string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
s.authLock.Lock()
|
||||
s.trustedCAs = append(s.trustedCAs, pk)
|
||||
s.authLock.Unlock()
|
||||
s.l.Info("Trusted CA key", "sshKey", pubKey)
|
||||
return nil
|
||||
}
|
||||
@@ -131,6 +144,7 @@ func (s *SSHServer) AddAuthorizedKey(user, pubKey string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
s.authLock.Lock()
|
||||
tk, ok := s.trustedKeys[user]
|
||||
if !ok {
|
||||
tk = make(map[string]bool)
|
||||
@@ -138,6 +152,7 @@ func (s *SSHServer) AddAuthorizedKey(user, pubKey string) error {
|
||||
}
|
||||
|
||||
tk[string(pk.Marshal())] = true
|
||||
s.authLock.Unlock()
|
||||
s.l.Info("Authorized ssh key",
|
||||
"sshKey", pubKey,
|
||||
"sshUser", user,
|
||||
|
||||
Reference in New Issue
Block a user