Compare commits

...

15 Commits

Author SHA1 Message Date
JackDoan bc04261d0b linux: let the kernel resolve the tun.dev %d template atomically
Resolving the template in userspace (LinkList then TUNSETIFF) races with
concurrent instances: both can pick nebula0, and with multiqueue the loser
silently attaches as a second queue to the winner's device instead of
failing. TUNSETIFF natively substitutes a single %d via dev_alloc_name,
atomically and always allocating a fresh device, so keep the fail-fast
validation but pass the template through and read the resolved name back.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fg7oD3uZ4jZjjQ4gRsPAwB
2026-07-09 18:37:30 -05:00
JackDoan 57e1a9b6af linux: allow %d anywhere in the tun.dev template 2026-07-08 13:43:43 -05:00
Nate Brown c1eea118f4 fix firewall port/proto bypass in parseV6 from uint8 extension-header length overflow (#1789) 2026-07-07 20:43:26 -05:00
Nate Brown 1e66c0d3ee hostmap: unlink a multi-vpnAddr hostinfo from the shared chain exactly once on delete (#1788) 2026-07-07 17:32:25 -05:00
Nate Brown e5c0fdad8d Darwin and openbsd in line with the other bsds for tun support (#1703) 2026-07-07 17:05:12 -05:00
Nate Brown 7bd0bc285a sshd: guard trustedKeys/trustedCAs with a mutex to fix a concurrent map crash on reload (#1787) 2026-07-07 15:50:06 -05:00
Nate Brown 942ee522e0 lighthouse: unmap 4-in-6 addresses in protoV6AddrPortToNetAddrPort so remote_allow_list v4 rules apply (#1786) 2026-07-07 15:23:39 -05:00
dependabot[bot] 32149f3a93 Bump github.com/kardianos/service from 1.2.4 to 1.3.0 (#1782)
Bumps [github.com/kardianos/service](https://github.com/kardianos/service) from 1.2.4 to 1.3.0.
- [Commits](https://github.com/kardianos/service/compare/v1.2.4...v1.3.0)

---
updated-dependencies:
- dependency-name: github.com/kardianos/service
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 14:42:33 -05:00
Jack Doan 19ad3bb904 correctly discard nil proto addresses (#1785) 2026-07-07 14:41:01 -05:00
dependabot[bot] 647775d8c3 Bump golang.zx2c4.com/wireguard/windows (#1665)
Bumps the zx2c4-dependencies group with 1 update in the / directory: golang.zx2c4.com/wireguard/windows.


Updates `golang.zx2c4.com/wireguard/windows` from 0.6.1 to 1.0.1

---
updated-dependencies:
- dependency-name: golang.zx2c4.com/wireguard/windows
  dependency-version: 1.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: zx2c4-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 13:40:12 -05:00
Nate Brown abfeb502a8 iputil: fix infinite loop in ipv6FindUpperProtocol from uint8 extension-header length overflow (#1784) 2026-07-07 13:28:52 -05:00
John Maguire 0a953915bb Make HostInfo.remote atomic to fix torn reads on the send path (#1773) 2026-07-07 14:27:53 -04:00
Nate Brown 6aa3363d85 Add explicit unmarshaller for signing and key agreement public keys (#1777) 2026-07-07 13:16:57 -05:00
Jack Doan 95d98b1f4b firewall: move conntrack check after cert+IP verification (#1779) 2026-07-07 13:10:11 -05:00
Jack Doan 6afca0f461 correctly handle a test packet with a payload longer than the header (#1778)
smoke-extra / freebsd-amd64 (push) Failing after 16s
smoke-extra / linux-amd64-ipv6disable (push) Failing after 14s
smoke-extra / netbsd-amd64 (push) Failing after 15s
smoke-extra / openbsd-amd64 (push) Failing after 14s
smoke-extra / linux-386 (push) Failing after 15s
smoke / Run multi node smoke test (push) Failing after 1m26s
Build and test / Static checks (push) Successful in 1m44s
Build and test / Test linux (push) Failing after 1m39s
Build and test / Test linux-boringcrypto (push) Failing after 3m3s
Build and test / Test linux-pkcs11 (push) Failing after 3m11s
Build and test / Cross-build linux-arm (push) Successful in 3m3s
Build and test / Cross-build linux-mips (push) Successful in 3m44s
Build and test / Cross-build linux-other (push) Successful in 3m8s
Build and test / Cross-build windows (push) Successful in 1m1s
Build and test / Cross-build freebsd (push) Successful in 1m38s
Build and test / Cross-build netbsd (push) Successful in 1m35s
Build and test / Cross-build openbsd (push) Successful in 1m34s
Build and test / Cross-build mobile (push) Successful in 3m17s
smoke-extra / Run windows smoke test (push) Has been cancelled
Build and test / Test macos (push) Has been cancelled
Build and test / Test windows (push) Has been cancelled
Build and test / CI status (push) Has been cancelled
2026-07-03 11:06:22 -05:00
26 changed files with 1111 additions and 217 deletions
+32 -2
View File
@@ -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
View File
@@ -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")
+2 -2
View File
@@ -305,7 +305,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 +350,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 {
+161 -6
View File
@@ -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)
}
})
}
}
+85
View File
@@ -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
}
}
}
+75
View File
@@ -1535,3 +1535,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()
}
+4
View File
@@ -242,6 +242,10 @@ tun:
# When tun is disabled, a lighthouse can be started without a local tun interface (and therefore without root)
disabled: false
# Name of the device. If not set, a default will be chosen by the OS.
# For Linux: a single `%d` anywhere in the name is treated as a template and replaced with the
# lowest number that yields an unused device name (e.g. `nebula%d` becomes `nebula0`, then `nebula1`, and so on, `neb%dprod` becomes `neb0prod`).
# Only on Linux: `nebula%d` is the default if tun.dev is unset.
# The name, both before and after %d substitution, must be shorter than the kernel limit of 16 characters.
# For macOS: if set, must be in the form `utun[0-9]+`.
# For NetBSD: Required to be set, must be in the form `tun[0-9]+`
dev: nebula1
+5 -5
View File
@@ -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
+153
View File
@@ -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++ {
+4 -4
View File
@@ -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
)
+8 -8
View File
@@ -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=
+24 -31
View File
@@ -229,7 +229,7 @@ const (
)
type HostInfo struct {
remote netip.AddrPort
remote atomic.Pointer[netip.AddrPort]
remotes *RemoteList
promoteCounter atomic.Uint32
ConnectionState *ConnectionState
@@ -438,44 +438,30 @@ func (hm *HostMap) unlockedMakePrimary(hostinfo *HostInfo) {
}
func (hm *HostMap) unlockedDeleteHostInfo(hostinfo *HostInfo) {
for _, addr := range hostinfo.vpnAddrs {
h := hm.Hosts[addr]
for h != nil {
if h == hostinfo {
hm.unlockedInnerDeleteHostInfo(h, addr)
}
h = h.next
}
}
}
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
for _, addr := range hostinfo.vpnAddrs {
if hm.Hosts[addr] != hostinfo {
continue
}
if hostinfo.next != nil {
// Promote the next hostinfo in the shared chain to primary for this address
hm.Hosts[addr] = hostinfo.next
} else {
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
// Splice this hostinfo out of the shared chain exactly once
if hostinfo.prev != nil {
hostinfo.prev.next = hostinfo.next
}
if hostinfo.next != nil {
hostinfo.next.prev = hostinfo.prev
}
}
hostinfo.next = nil
hostinfo.prev = nil
@@ -684,7 +670,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 +712,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 +735,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
+101
View File
@@ -194,6 +194,107 @@ func TestHostMap_DeleteHostInfo(t *testing.T) {
assert.Nil(t, prim)
}
// 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 the shared chain
assert.Equal(t, head.localIndexId, hm.QueryVpnAddr(a).localIndexId)
assert.Equal(t, head.localIndexId, hm.QueryVpnAddr(b).localIndexId)
assert.Equal(t, other.localIndexId, head.next.localIndexId)
assert.Equal(t, head.localIndexId, other.prev.localIndexId)
// Delete the head. other is still live, so it must become primary for BOTH addresses.
hm.DeleteHostInfo(head)
// Pre-fix: QueryVpnAddr(b) came back nil here because the second address was deleted rather than
// promoted, leaving other unreachable at b.
require.NotNil(t, hm.QueryVpnAddr(a))
require.NotNil(t, hm.QueryVpnAddr(b))
assert.Equal(t, other.localIndexId, hm.QueryVpnAddr(a).localIndexId)
assert.Equal(t, other.localIndexId, hm.QueryVpnAddr(b).localIndexId)
// other is now the only hostinfo in the chain
assert.Nil(t, other.prev)
assert.Nil(t, other.next)
// head is fully detached
assert.Nil(t, head.prev)
assert.Nil(t, head.next)
assert.Nil(t, hm.QueryIndex(head.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 should have been pruned and fully detached
assert.Nil(t, oldest.next)
assert.Nil(t, oldest.prev)
assert.Nil(t, hm.QueryIndex(oldest.localIndexId))
// Both addresses resolve to the same head, and that head is one of the survivors (not the pruned one)
primA := hm.QueryVpnAddr(a)
primB := hm.QueryVpnAddr(b)
require.NotNil(t, primA)
require.NotNil(t, primB)
assert.Equal(t, primA.localIndexId, primB.localIndexId)
assert.NotEqual(t, oldest.localIndexId, primA.localIndexId)
// Walk the shared chain: exactly MaxHostInfosPerVpnIp survivors, no cycles, oldest absent
seen := map[uint32]struct{}{}
for h := primA; h != nil; h = h.next {
_, dup := seen[h.localIndexId]
require.False(t, dup, "cycle detected in hostinfo chain")
seen[h.localIndexId] = struct{}{}
if h.next != nil {
assert.Equal(t, h.localIndexId, h.next.prev.localIndexId, "prev pointer must mirror next")
}
}
assert.Len(t, seen, MaxHostInfosPerVpnIp)
_, prunedStillPresent := seen[oldest.localIndexId]
assert.False(t, prunedStillPresent)
}
func TestHostMap_reload(t *testing.T) {
l := test.NewLogger()
c := config.NewC(test.NewLogger())
+4 -4
View File
@@ -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,
+2 -2
View File
@@ -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
+9 -1
View File
@@ -1418,6 +1418,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 +1428,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 +1460,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,9 +1500,11 @@ func (d *NebulaMetaDetails) GetRelays() []netip.Addr {
if len(d.RelayVpnAddrs) > 0 {
for _, r := range d.RelayVpnAddrs {
if r != nil {
relays = append(relays, protoAddrToNetAddr(r))
}
}
}
return relays
}
+12 -11
View File
@@ -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
}
+35
View File
@@ -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")
}
+86 -28
View File
@@ -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,7 +121,7 @@ func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*t
}
t := &tun{
ReadWriteCloser: os.NewFile(uintptr(fd), ""),
f: os.NewFile(uintptr(fd), ""),
Device: name,
vpnNetworks: vpnNetworks,
DefaultMTU: c.GetInt("tun.mtu", DefaultMTU),
@@ -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 {
+35 -4
View File
@@ -5,6 +5,7 @@ package overlay
import (
"encoding/binary"
"errors"
"fmt"
"io"
"log/slog"
@@ -250,6 +251,17 @@ func newTunFromFd(c *config.C, l *slog.Logger, deviceFd int, vpnNetworks []netip
}
func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, multiqueue bool) (*tun, error) {
// Validate the device name up front so a bad tun.dev fails fast, before we
// open /dev/net/tun or leak a file descriptor. A single %d in the name is
// substituted by the kernel during TUNSETIFF (dev_alloc_name) with the
// lowest number that yields an unused device name. Resolving the template
// in the kernel keeps the pick-a-name/create-the-device pair atomic, so
// concurrent callers can never race each other to the same name.
tunName := c.GetString("tun.dev", "nebula%d")
if err := validateTunName(tunName); err != nil {
return nil, err
}
fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0)
if err != nil {
// If /dev/net/tun doesn't exist, try to create it (will happen in docker)
@@ -277,12 +289,11 @@ func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, multiqueue
if multiqueue {
req.Flags |= unix.IFF_MULTI_QUEUE
}
nameStr := c.GetString("tun.dev", "")
copy(req.Name[:], nameStr)
copy(req.Name[:], tunName)
if err = ioctl(uintptr(fd), uintptr(unix.TUNSETIFF), uintptr(unsafe.Pointer(&req))); err != nil {
_ = unix.Close(fd)
return nil, &NameError{
Name: nameStr,
Name: tunName,
Underlying: err,
}
}
@@ -298,6 +309,27 @@ func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, multiqueue
return t, nil
}
func validateTunName(tunName string) error {
if !strings.Contains(tunName, "%d") {
if len(tunName) >= unix.IFNAMSIZ {
return fmt.Errorf("tun.dev %q is not shorter than the maximum device name length of %d", tunName, unix.IFNAMSIZ)
}
return nil
}
if strings.Count(tunName, "%d") > 1 {
return fmt.Errorf("tun.dev template %q may only contain a single %%d", tunName)
}
if tunName == "%d" {
return errors.New("please don't name your tun device '%d'")
}
// The kernel substitutes the %d itself and requires the template, like a
// literal name, to be NUL-terminated within IFNAMSIZ bytes.
if len(tunName) >= unix.IFNAMSIZ {
return fmt.Errorf("tun.dev template %q is not shorter than the maximum device name length of %d", tunName, unix.IFNAMSIZ)
}
return nil
}
// newTunGeneric does all the stuff common to different tun initialization paths. It will close your files on error.
func newTunGeneric(c *config.C, l *slog.Logger, fd int, vpnNetworks []netip.Prefix) (*tun, error) {
tfd, err := newTunFd(fd)
@@ -768,7 +800,6 @@ func (t *tun) isGatewayInVpnNetworks(gwAddr netip.Addr) bool {
func (t *tun) getGatewaysFromRoute(r *netlink.Route) routing.Gateways {
var gateways routing.Gateways
link, err := netlink.LinkByName(t.Device)
if err != nil {
t.l.Error("Ignoring route update: failed to get link by name", "deviceName", t.Device)
+42 -1
View File
@@ -3,7 +3,12 @@
package overlay
import "testing"
import (
"strings"
"testing"
"golang.org/x/sys/unix"
)
var runAdvMSSTests = []struct {
name string
@@ -32,3 +37,39 @@ func TestTunAdvMSS(t *testing.T) {
})
}
}
func TestValidateTunName(t *testing.T) {
// A device name must be shorter than IFNAMSIZ (i.e. IFNAMSIZ-1 chars max).
maxLenName := strings.Repeat("a", unix.IFNAMSIZ-1)
tests := []struct {
name string
tmpl string
wantErr bool
}{
{"short literal name is fine", "nebula1", false},
{"literal name at the max length is fine", maxLenName, false},
{"literal name at IFNAMSIZ is rejected", strings.Repeat("a", unix.IFNAMSIZ), true},
{"trailing template is fine", "nebula%d", false},
{"mid-string template is fine", "neb%dprod", false},
{"leading template is fine", "%dnebula", false},
{"template at the max length is fine", strings.Repeat("a", unix.IFNAMSIZ-3) + "%d", false},
{"template at IFNAMSIZ is rejected", strings.Repeat("a", unix.IFNAMSIZ-2) + "%d", true},
{"bare %d is rejected", "%d", true},
{"multiple %d is rejected", "neb%d%dprod", true},
{"over-long template is rejected", strings.Repeat("a", unix.IFNAMSIZ-1) + "%d", true},
{"over-long mid-string template is rejected", "neb%d" + strings.Repeat("a", unix.IFNAMSIZ-3), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateTunName(tt.tmpl)
if tt.wantErr && err == nil {
t.Fatalf("expected an error for %q, got none", tt.tmpl)
}
if !tt.wantErr && err != nil {
t.Fatalf("unexpected error for %q: %v", tt.tmpl, err)
}
})
}
}
+82 -23
View File
@@ -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 {
+2 -2
View File
@@ -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)
}
}
+21 -3
View File
@@ -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,7 +104,7 @@ 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 {
hl.Info("Failed to add relay to hostmap", "relay", relay.String(), "error", err)
@@ -309,6 +309,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 +334,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 +416,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 +526,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
}
+12
View File
@@ -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)
+15
View File
@@ -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,