mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 22:47:02 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10e9514e44 | |||
| 913a37cfee |
@@ -12,7 +12,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -80,7 +80,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -125,7 +125,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
//go:build linux && !android && !e2e_testing
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/slackhq/nebula"
|
|
||||||
"github.com/slackhq/nebula/cert"
|
|
||||||
cert_test "github.com/slackhq/nebula/cert_test"
|
|
||||||
"github.com/slackhq/nebula/config"
|
|
||||||
"github.com/slackhq/nebula/test"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestControlStopClosesOnTimer reproduces the dnclient lifecycle: nebula runs as
|
|
||||||
// a library, and on a config update dnclient calls Stop() in-process to tear the
|
|
||||||
// old instance down before starting a new one. This boots a real nebula (real
|
|
||||||
// blocking UDP sockets, tun disabled), lets it run, then Stop()s it on a timer
|
|
||||||
// and asserts it actually closes. If the reader goroutines parked in recvmmsg
|
|
||||||
// don't wake on Close(), Wait() blocks forever and this fails with a goroutine
|
|
||||||
// dump instead of relying on a process signal to unstick them.
|
|
||||||
func TestControlStopClosesOnTimer(t *testing.T) {
|
|
||||||
l := test.NewLogger()
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
before := time.Now().Add(-time.Hour)
|
|
||||||
after := time.Now().Add(time.Hour)
|
|
||||||
ca, _, caKey, caPEM := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, before, after, nil, nil, nil)
|
|
||||||
networks := []netip.Prefix{netip.MustParsePrefix("10.0.0.1/24")}
|
|
||||||
_, _, keyPEM, certPEM := cert_test.NewTestCert(cert.Version2, cert.Curve_CURVE25519, ca, caKey, "close-on-timer", before, after, networks, nil, nil)
|
|
||||||
|
|
||||||
caPath := filepath.Join(dir, "ca.pem")
|
|
||||||
certPath := filepath.Join(dir, "cert.pem")
|
|
||||||
keyPath := filepath.Join(dir, "key.pem")
|
|
||||||
require.NoError(t, os.WriteFile(caPath, caPEM, 0o600))
|
|
||||||
require.NoError(t, os.WriteFile(certPath, certPEM, 0o600))
|
|
||||||
require.NoError(t, os.WriteFile(keyPath, keyPEM, 0o600))
|
|
||||||
|
|
||||||
// tun disabled so no device/root is needed; routines: 2 so we exercise the
|
|
||||||
// multi-socket (SO_REUSEPORT) teardown, which is where dnclient runs.
|
|
||||||
configBody := fmt.Sprintf(`
|
|
||||||
pki:
|
|
||||||
ca: %s
|
|
||||||
cert: %s
|
|
||||||
key: %s
|
|
||||||
listen:
|
|
||||||
host: 127.0.0.1
|
|
||||||
port: 0
|
|
||||||
tun:
|
|
||||||
disabled: true
|
|
||||||
firewall:
|
|
||||||
outbound:
|
|
||||||
- port: any
|
|
||||||
proto: any
|
|
||||||
host: any
|
|
||||||
inbound:
|
|
||||||
- port: any
|
|
||||||
proto: any
|
|
||||||
host: any
|
|
||||||
routines: 2
|
|
||||||
`, caPath, certPath, keyPath)
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "config.yml"), []byte(configBody), 0o600))
|
|
||||||
|
|
||||||
c := config.NewC(l)
|
|
||||||
require.NoError(t, c.Load(dir))
|
|
||||||
|
|
||||||
ctrl, err := nebula.Main(c, false, "close-on-timer", l, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, ctrl.Start())
|
|
||||||
|
|
||||||
// Run like a live nebula, then close on a timer, exactly as dnclient does.
|
|
||||||
<-time.NewTimer(5 * time.Second).C
|
|
||||||
|
|
||||||
stopped := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
ctrl.Stop() // closes the udp sockets (shutdown(2)) and the tun
|
|
||||||
ctrl.Wait() // blocks until every reader goroutine has returned
|
|
||||||
close(stopped)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-stopped:
|
|
||||||
t.Log("nebula closed cleanly on timer")
|
|
||||||
case <-time.After(10 * time.Second):
|
|
||||||
buf := make([]byte, 1<<20)
|
|
||||||
n := runtime.Stack(buf, true)
|
|
||||||
t.Fatalf("nebula did NOT close within 10s of Stop(): a blocking reader never woke\n%s", buf[:n])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -44,11 +44,6 @@ type connectionManager struct {
|
|||||||
inactivityTimeout atomic.Int64
|
inactivityTimeout atomic.Int64
|
||||||
dropInactive atomic.Bool
|
dropInactive atomic.Bool
|
||||||
|
|
||||||
// Wake-from-sleep handling, sampled once per tick in Start
|
|
||||||
wakeDetector *wakeDetector
|
|
||||||
clearOnWake atomic.Bool
|
|
||||||
wakeClearThreshold atomic.Int64
|
|
||||||
|
|
||||||
l *slog.Logger
|
l *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +54,6 @@ func newConnectionManagerFromConfig(l *slog.Logger, c *config.C, hm *HostMap, p
|
|||||||
punchy: p,
|
punchy: p,
|
||||||
relayUsed: make(map[uint32]struct{}),
|
relayUsed: make(map[uint32]struct{}),
|
||||||
relayUsedLock: &sync.RWMutex{},
|
relayUsedLock: &sync.RWMutex{},
|
||||||
wakeDetector: newWakeDetector(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cm.reload(c, true)
|
cm.reload(c, true)
|
||||||
@@ -104,38 +98,12 @@ func (cm *connectionManager) reload(c *config.C, initial bool) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if initial || c.HasChanged("tunnels.clear_on_wake") {
|
|
||||||
old := cm.clearOnWake.Load()
|
|
||||||
cm.clearOnWake.Store(c.GetBool("tunnels.clear_on_wake", true))
|
|
||||||
if !initial {
|
|
||||||
cm.l.Info("Clear on wake setting has changed",
|
|
||||||
"oldBool", old,
|
|
||||||
"newBool", cm.clearOnWake.Load(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if initial || c.HasChanged("tunnels.wake_clear_threshold") {
|
|
||||||
old := cm.getWakeClearThreshold()
|
|
||||||
cm.wakeClearThreshold.Store((int64)(c.GetDuration("tunnels.wake_clear_threshold", 30*time.Second)))
|
|
||||||
if !initial {
|
|
||||||
cm.l.Info("Wake clear threshold has changed",
|
|
||||||
"oldDuration", old,
|
|
||||||
"newDuration", cm.getWakeClearThreshold(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *connectionManager) getInactivityTimeout() time.Duration {
|
func (cm *connectionManager) getInactivityTimeout() time.Duration {
|
||||||
return (time.Duration)(cm.inactivityTimeout.Load())
|
return (time.Duration)(cm.inactivityTimeout.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *connectionManager) getWakeClearThreshold() time.Duration {
|
|
||||||
return (time.Duration)(cm.wakeClearThreshold.Load())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *connectionManager) In(h *HostInfo) {
|
func (cm *connectionManager) In(h *HostInfo) {
|
||||||
h.in.Store(true)
|
h.in.Store(true)
|
||||||
}
|
}
|
||||||
@@ -168,73 +136,6 @@ func (cm *connectionManager) getAndResetTrafficCheck(h *HostInfo, now time.Time)
|
|||||||
return in, out
|
return in, out
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkWake runs once per tick and clears every tunnel when the machine has just returned from system sleep.
|
|
||||||
// Tunnels rarely survive a suspend: our NAT mappings have expired and our address has usually changed, so every
|
|
||||||
// established hostinfo is a corpse that will eat 15-20s of traffic checks before the wheel declares it dead.
|
|
||||||
// Clearing now means the first packet after wake starts a fresh handshake immediately.
|
|
||||||
//
|
|
||||||
// The suspend itself costs nothing here: the ticker driving us is frozen with the rest of the process and this
|
|
||||||
// fires within one tick of resume.
|
|
||||||
func (cm *connectionManager) checkWake() {
|
|
||||||
slept, ok := cm.wakeDetector.Sample()
|
|
||||||
if !ok || slept == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// The clock pair is read non-atomically, so scheduling jitter shows up as tiny sub-millisecond "sleeps".
|
|
||||||
// Keep the floor well above that so a zero/nonsense threshold can't clear tunnels on every tick.
|
|
||||||
threshold := max(cm.getWakeClearThreshold(), time.Second)
|
|
||||||
|
|
||||||
if slept < threshold {
|
|
||||||
// Short suspends (lid closed and quickly reopened) often come back before NAT state expires; those
|
|
||||||
// tunnels may well be alive, leave them to the normal traffic checks.
|
|
||||||
if slept >= time.Second {
|
|
||||||
cm.l.Debug("Woke from sleep below the clear threshold, leaving tunnels alone",
|
|
||||||
"sleptFor", slept,
|
|
||||||
"threshold", threshold,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cm.clearOnWake.Load() {
|
|
||||||
cm.l.Info("Woke from sleep, tunnels.clear_on_wake is disabled so tunnels are left to the normal traffic checks", "sleptFor", slept)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
closed := cm.clearAllTunnels()
|
|
||||||
cm.l.Info("Woke from sleep, cleared tunnels", "sleptFor", slept, "tunnelsCleared", closed)
|
|
||||||
|
|
||||||
// Our public address almost certainly changed; get it to the lighthouses as soon as possible so peers can
|
|
||||||
// find us again. The update rides over a fresh lighthouse handshake. If the network isn't back up yet these
|
|
||||||
// sends fail harmlessly and the periodic update worker retries within lighthouse.interval.
|
|
||||||
cm.intf.lightHouse.TriggerUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// clearAllTunnels closes every tunnel in the hostmap locally, without notifying the remotes. It is the wake-from-
|
|
||||||
// sleep counterpart to Control.CloseAllTunnels: after a suspend the remotes stopped hearing from us long ago, and
|
|
||||||
// close packets fired into a network that may not even be up yet are wasted, so we only tear down our own state
|
|
||||||
// and let the next packet to each host start a fresh handshake.
|
|
||||||
func (cm *connectionManager) clearAllTunnels() int {
|
|
||||||
cm.hostMap.RLock()
|
|
||||||
hostinfos := make([]*HostInfo, 0, len(cm.hostMap.Indexes))
|
|
||||||
for _, h := range cm.hostMap.Indexes {
|
|
||||||
hostinfos = append(hostinfos, h)
|
|
||||||
}
|
|
||||||
cm.hostMap.RUnlock()
|
|
||||||
|
|
||||||
for _, h := range hostinfos {
|
|
||||||
cm.intf.closeTunnel(h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// With every tunnel gone no relay can be in use, drop the usage tracking wholesale.
|
|
||||||
cm.relayUsedLock.Lock()
|
|
||||||
clear(cm.relayUsed)
|
|
||||||
cm.relayUsedLock.Unlock()
|
|
||||||
|
|
||||||
return len(hostinfos)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *connectionManager) Start(ctx context.Context) {
|
func (cm *connectionManager) Start(ctx context.Context) {
|
||||||
clockSource := time.NewTicker(cm.trafficTimer.t.tickDuration)
|
clockSource := time.NewTicker(cm.trafficTimer.t.tickDuration)
|
||||||
defer clockSource.Stop()
|
defer clockSource.Stop()
|
||||||
@@ -249,7 +150,6 @@ func (cm *connectionManager) Start(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
|
|
||||||
case now := <-clockSource.C:
|
case now := <-clockSource.C:
|
||||||
cm.checkWake()
|
|
||||||
cm.trafficTimer.Advance(now)
|
cm.trafficTimer.Advance(now)
|
||||||
for {
|
for {
|
||||||
localIndex, has := cm.trafficTimer.Purge()
|
localIndex, has := cm.trafficTimer.Purge()
|
||||||
|
|||||||
@@ -501,85 +501,3 @@ func (d *dummyCert) MarshalJSON() ([]byte, error) {
|
|||||||
func (d *dummyCert) Copy() cert.Certificate {
|
func (d *dummyCert) Copy() cert.Certificate {
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConnectionManager_WakeClear(t *testing.T) {
|
|
||||||
l := test.NewLogger()
|
|
||||||
localrange := netip.MustParsePrefix("10.1.1.1/24")
|
|
||||||
vpnIp := netip.MustParseAddr("172.1.1.2")
|
|
||||||
preferredRanges := []netip.Prefix{localrange}
|
|
||||||
|
|
||||||
// Very incomplete mock objects
|
|
||||||
hostMap := newHostMap(l)
|
|
||||||
hostMap.preferredRanges.Store(&preferredRanges)
|
|
||||||
|
|
||||||
cs := &CertState{
|
|
||||||
initiatingVersion: cert.Version1,
|
|
||||||
privateKey: []byte{},
|
|
||||||
v1Cert: &dummyCert{version: cert.Version1},
|
|
||||||
v1Credential: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
lh := newTestLighthouse()
|
|
||||||
ifce := &Interface{
|
|
||||||
hostMap: hostMap,
|
|
||||||
inside: &overlaytest.NoopTun{},
|
|
||||||
outside: &udp.NoopConn{},
|
|
||||||
firewall: &Firewall{},
|
|
||||||
lightHouse: lh,
|
|
||||||
pki: &PKI{},
|
|
||||||
handshakeManager: NewHandshakeManager(l, hostMap, lh, &udp.NoopConn{}, defaultHandshakeConfig),
|
|
||||||
l: l,
|
|
||||||
}
|
|
||||||
ifce.pki.cs.Store(cs)
|
|
||||||
|
|
||||||
// Create manager
|
|
||||||
conf := config.NewC(test.NewLogger())
|
|
||||||
punchy := NewPunchyFromConfig(test.NewLogger(), conf, nil)
|
|
||||||
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
|
|
||||||
nc.intf = ifce
|
|
||||||
|
|
||||||
// Drive the wake detector from a fake clock pair
|
|
||||||
suspended := time.Duration(0)
|
|
||||||
nc.wakeDetector = &wakeDetector{read: func() (time.Duration, bool) { return suspended, true }}
|
|
||||||
nc.checkWake() // primes the baseline
|
|
||||||
|
|
||||||
addTunnel := func(localIndex uint32) *HostInfo {
|
|
||||||
hostinfo := &HostInfo{
|
|
||||||
vpnAddrs: []netip.Addr{vpnIp},
|
|
||||||
localIndexId: localIndex,
|
|
||||||
remoteIndexId: 9901,
|
|
||||||
}
|
|
||||||
hostinfo.ConnectionState = &ConnectionState{
|
|
||||||
myCert: &dummyCert{version: cert.Version1},
|
|
||||||
}
|
|
||||||
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
|
||||||
return hostinfo
|
|
||||||
}
|
|
||||||
|
|
||||||
addTunnel(1099)
|
|
||||||
nc.RelayUsed(5000)
|
|
||||||
|
|
||||||
// No suspend, nothing happens
|
|
||||||
nc.checkWake()
|
|
||||||
assert.Contains(t, nc.hostMap.Indexes, uint32(1099))
|
|
||||||
|
|
||||||
// A suspend below the threshold leaves tunnels alone
|
|
||||||
suspended += 5 * time.Second
|
|
||||||
nc.checkWake()
|
|
||||||
assert.Contains(t, nc.hostMap.Indexes, uint32(1099))
|
|
||||||
|
|
||||||
// A suspend past the threshold clears everything, including relay usage tracking
|
|
||||||
suspended += time.Hour
|
|
||||||
nc.checkWake()
|
|
||||||
assert.Empty(t, nc.hostMap.Indexes)
|
|
||||||
assert.Empty(t, nc.hostMap.Hosts)
|
|
||||||
assert.Empty(t, nc.relayUsed)
|
|
||||||
|
|
||||||
// With clear_on_wake disabled the tunnels survive a long suspend
|
|
||||||
addTunnel(1100)
|
|
||||||
nc.clearOnWake.Store(false)
|
|
||||||
suspended += time.Hour
|
|
||||||
nc.checkWake()
|
|
||||||
assert.Contains(t, nc.hostMap.Indexes, uint32(1100))
|
|
||||||
assert.Contains(t, nc.hostMap.Hosts, vpnIp)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ package nebula
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log/slog"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/slackhq/nebula/handshake"
|
"github.com/slackhq/nebula/handshake"
|
||||||
"github.com/slackhq/nebula/header"
|
|
||||||
"github.com/slackhq/nebula/noiseutil"
|
"github.com/slackhq/nebula/noiseutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,7 +20,6 @@ type ConnectionState struct {
|
|||||||
initiator bool
|
initiator bool
|
||||||
messageCounter atomic.Uint64
|
messageCounter atomic.Uint64
|
||||||
window *Bits
|
window *Bits
|
||||||
decryptLock sync.Mutex
|
|
||||||
writeLock sync.Mutex
|
writeLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,52 +54,3 @@ func (cs *ConnectionState) MarshalJSON() ([]byte, error) {
|
|||||||
func (cs *ConnectionState) Curve() cert.Curve {
|
func (cs *ConnectionState) Curve() cert.Curve {
|
||||||
return cs.myCert.Curve()
|
return cs.myCert.Curve()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *ConnectionState) Decrypt(l *slog.Logger, messageCounter uint64, out []byte, packet []byte, nb []byte) ([]byte, error) {
|
|
||||||
var err error
|
|
||||||
cs.decryptLock.Lock()
|
|
||||||
result := cs.window.Check(l, messageCounter)
|
|
||||||
cs.decryptLock.Unlock()
|
|
||||||
if !result {
|
|
||||||
return nil, ErrAlreadySeen
|
|
||||||
}
|
|
||||||
|
|
||||||
out, err = cs.dKey.DecryptDanger(out, packet[:header.Len], packet[header.Len:], messageCounter, nb)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cs.decryptLock.Lock()
|
|
||||||
result = cs.window.Update(l, messageCounter)
|
|
||||||
cs.decryptLock.Unlock()
|
|
||||||
if !result {
|
|
||||||
return nil, ErrAlreadySeen
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyRelay verifies AEAD protected (but not encrypted) relay frames. packet must be length-checked by the caller.
|
|
||||||
func (cs *ConnectionState) VerifyRelay(l *slog.Logger, messageCounter uint64, packet []byte, nb []byte) error {
|
|
||||||
cs.decryptLock.Lock()
|
|
||||||
result := cs.window.Check(l, messageCounter)
|
|
||||||
cs.decryptLock.Unlock()
|
|
||||||
if !result {
|
|
||||||
return ErrAlreadySeen
|
|
||||||
}
|
|
||||||
|
|
||||||
signedPayload := packet[:len(packet)-cs.dKey.Overhead()]
|
|
||||||
signatureValue := packet[len(packet)-cs.dKey.Overhead():]
|
|
||||||
_, err := cs.dKey.DecryptDanger(nil, signedPayload, signatureValue, messageCounter, nb)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cs.decryptLock.Lock()
|
|
||||||
result = cs.window.Update(l, messageCounter)
|
|
||||||
cs.decryptLock.Unlock()
|
|
||||||
if !result {
|
|
||||||
return ErrAlreadySeen
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gaissmai/bart"
|
"github.com/gaissmai/bart"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
"github.com/slackhq/nebula/test"
|
"github.com/slackhq/nebula/test"
|
||||||
"github.com/slackhq/nebula/udp"
|
"github.com/slackhq/nebula/udp"
|
||||||
@@ -30,9 +31,9 @@ func newFakeDevice() *fakeDevice {
|
|||||||
|
|
||||||
// Read blocks until Close like a real tun with no traffic, then reports EOF
|
// Read blocks until Close like a real tun with no traffic, then reports EOF
|
||||||
// the same way a closed device does
|
// the same way a closed device does
|
||||||
func (d *fakeDevice) Read(p []byte) (int, error) {
|
func (d *fakeDevice) Read() ([]tio.Packet, error) {
|
||||||
<-d.closedCh
|
<-d.closedCh
|
||||||
return 0, io.EOF
|
return nil, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *fakeDevice) Write(p []byte) (int, error) { return len(p), nil }
|
func (d *fakeDevice) Write(p []byte) (int, error) { return len(p), nil }
|
||||||
@@ -49,10 +50,8 @@ func (d *fakeDevice) Activate() error { return nil }
|
|||||||
func (d *fakeDevice) Networks() []netip.Prefix { return nil }
|
func (d *fakeDevice) Networks() []netip.Prefix { return nil }
|
||||||
func (d *fakeDevice) Name() string { return "fake" }
|
func (d *fakeDevice) Name() string { return "fake" }
|
||||||
func (d *fakeDevice) RoutesFor(netip.Addr) routing.Gateways { return nil }
|
func (d *fakeDevice) RoutesFor(netip.Addr) routing.Gateways { return nil }
|
||||||
func (d *fakeDevice) SupportsMultiqueue() bool { return false }
|
|
||||||
func (d *fakeDevice) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
func (d *fakeDevice) Queues(int) ([]tio.Queue, error) { return []tio.Queue{d}, nil }
|
||||||
return nil, errors.New("unsupported")
|
|
||||||
}
|
|
||||||
|
|
||||||
// newReadyControl hand-builds the minimum Control that Main would have
|
// newReadyControl hand-builds the minimum Control that Main would have
|
||||||
// produced right before Start, including the construction token NewInterface
|
// produced right before Start, including the construction token NewInterface
|
||||||
@@ -78,7 +77,6 @@ func newReadyControl(t *testing.T) (*Control, *fakeDevice, *fakeConn) {
|
|||||||
inside: dev,
|
inside: dev,
|
||||||
outside: conn,
|
outside: conn,
|
||||||
writers: []udp.Conn{conn},
|
writers: []udp.Conn{conn},
|
||||||
readers: make([]io.ReadWriteCloser, 1),
|
|
||||||
routines: 1,
|
routines: 1,
|
||||||
hostMap: newHostMap(l),
|
hostMap: newHostMap(l),
|
||||||
lightHouse: lh,
|
lightHouse: lh,
|
||||||
@@ -155,7 +153,14 @@ type multiqueueDevice struct {
|
|||||||
*fakeDevice
|
*fakeDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *multiqueueDevice) SupportsMultiqueue() bool { return true }
|
// Queues claims multiqueue support but fails to open the second queue,
|
||||||
|
// exercising the activation error path.
|
||||||
|
func (d *multiqueueDevice) Queues(n int) ([]tio.Queue, error) {
|
||||||
|
if n > 1 {
|
||||||
|
return nil, errors.New("second queue failed to open")
|
||||||
|
}
|
||||||
|
return d.fakeDevice.Queues(n)
|
||||||
|
}
|
||||||
|
|
||||||
func TestControl_StartMultiqueueFailureReleases(t *testing.T) {
|
func TestControl_StartMultiqueueFailureReleases(t *testing.T) {
|
||||||
dev := &multiqueueDevice{fakeDevice: newFakeDevice()}
|
dev := &multiqueueDevice{fakeDevice: newFakeDevice()}
|
||||||
@@ -166,7 +171,6 @@ func TestControl_StartMultiqueueFailureReleases(t *testing.T) {
|
|||||||
inside: dev,
|
inside: dev,
|
||||||
outside: conn,
|
outside: conn,
|
||||||
writers: []udp.Conn{conn},
|
writers: []udp.Conn{conn},
|
||||||
readers: make([]io.ReadWriteCloser, 2),
|
|
||||||
routines: 2,
|
routines: 2,
|
||||||
l: test.NewLogger(),
|
l: test.NewLogger(),
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-16
@@ -97,7 +97,8 @@ func (d *dnsServer) reload(c *config.C, initial bool) error {
|
|||||||
newAddr := getDnsServerAddr(c)
|
newAddr := getDnsServerAddr(c)
|
||||||
|
|
||||||
d.serverMu.Lock()
|
d.serverMu.Lock()
|
||||||
running := d.server != nil
|
running := d.server
|
||||||
|
runningStarted := d.started
|
||||||
sameAddr := d.addr == newAddr
|
sameAddr := d.addr == newAddr
|
||||||
d.addr = newAddr
|
d.addr = newAddr
|
||||||
d.enabled.Store(enabled)
|
d.enabled.Store(enabled)
|
||||||
@@ -111,7 +112,7 @@ func (d *dnsServer) reload(c *config.C, initial bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !enabled {
|
if !enabled {
|
||||||
if running {
|
if running != nil {
|
||||||
d.Stop()
|
d.Stop()
|
||||||
}
|
}
|
||||||
// Drop any records that accumulated while enabled; a later re-enable
|
// Drop any records that accumulated while enabled; a later re-enable
|
||||||
@@ -120,12 +121,12 @@ func (d *dnsServer) reload(c *config.C, initial bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !running {
|
if running == nil {
|
||||||
// Was disabled (or never started); bring it up now.
|
// Was disabled (or never started); bring it up now.
|
||||||
go d.Start()
|
go d.Start()
|
||||||
} else if !sameAddr {
|
} else if !sameAddr {
|
||||||
// Stop clears the slot before shutting down, otherwise the Start below can find the dying server and refuse
|
d.shutdownServer(running, runningStarted, "reload")
|
||||||
d.Stop()
|
// Old Start goroutine has now exited; bring up a fresh listener on the new address.
|
||||||
go d.Start()
|
go d.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,9 +162,7 @@ func (d *dnsServer) Start() {
|
|||||||
|
|
||||||
started := make(chan struct{})
|
started := make(chan struct{})
|
||||||
d.serverMu.Lock()
|
d.serverMu.Lock()
|
||||||
// Re-check enabled under the lock, a disable that raced our check above snapshots the slot under it too.
|
if d.ctx.Err() != nil {
|
||||||
// Two reloads in quick succession can both spawn a Start, the loser would orphan the live listener past Stop
|
|
||||||
if d.ctx.Err() != nil || d.server != nil || !d.enabled.Load() {
|
|
||||||
d.serverMu.Unlock()
|
d.serverMu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -201,14 +200,6 @@ func (d *dnsServer) Start() {
|
|||||||
close(started)
|
close(started)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release our slot, unless a reload already replaced us, so a dead listener can't block a future Start
|
|
||||||
d.serverMu.Lock()
|
|
||||||
if d.server == server {
|
|
||||||
d.server = nil
|
|
||||||
d.started = nil
|
|
||||||
}
|
|
||||||
d.serverMu.Unlock()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.l.Warn("Failed to run the DNS responder", "error", err)
|
d.l.Warn("Failed to run the DNS responder", "error", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-206
@@ -194,51 +194,14 @@ func TestDnsServer_reload_initial_serveDnsWithoutLighthouse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDnsServer_reload_sameAddr_noOp(t *testing.T) {
|
func TestDnsServer_reload_sameAddr_noOp(t *testing.T) {
|
||||||
port := freeUDPPort(t)
|
|
||||||
ds, c := newTestDnsServer(t)
|
ds, c := newTestDnsServer(t)
|
||||||
setDnsConfig(c, "127.0.0.1", port, true, true)
|
setDnsConfig(c, "127.0.0.1", "0", true, true)
|
||||||
|
|
||||||
require.NoError(t, ds.reload(c, true))
|
require.NoError(t, ds.reload(c, true))
|
||||||
|
// No server running yet, no addr change. Reload should not spawn anything.
|
||||||
go ds.Start()
|
|
||||||
waitForBind(t, ds)
|
|
||||||
|
|
||||||
ds.serverMu.Lock()
|
|
||||||
before := ds.server
|
|
||||||
ds.serverMu.Unlock()
|
|
||||||
require.NotNil(t, before)
|
|
||||||
|
|
||||||
// Same address, so the running listener must be left alone rather than rebuilt under live queries
|
|
||||||
require.NoError(t, ds.reload(c, false))
|
require.NoError(t, ds.reload(c, false))
|
||||||
assert.True(t, ds.enabled.Load())
|
assert.True(t, ds.enabled.Load())
|
||||||
|
assert.Nil(t, ds.server)
|
||||||
ds.serverMu.Lock()
|
|
||||||
after := ds.server
|
|
||||||
ds.serverMu.Unlock()
|
|
||||||
assert.Same(t, before, after, "a same-address reload must not restart the listener")
|
|
||||||
|
|
||||||
ds.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// The branch the old sameAddr test was accidentally hitting: enabled with nothing running means reload starts it.
|
|
||||||
func TestDnsServer_reload_whenNotRunning_starts(t *testing.T) {
|
|
||||||
port := freeUDPPort(t)
|
|
||||||
ds, c := newTestDnsServer(t)
|
|
||||||
setDnsConfig(c, "127.0.0.1", port, true, true)
|
|
||||||
|
|
||||||
// initial only records config, it never starts anything
|
|
||||||
require.NoError(t, ds.reload(c, true))
|
|
||||||
ds.serverMu.Lock()
|
|
||||||
assert.Nil(t, ds.server, "the initial reload must not start a listener")
|
|
||||||
ds.serverMu.Unlock()
|
|
||||||
|
|
||||||
require.NoError(t, ds.reload(c, false))
|
|
||||||
waitForBind(t, ds)
|
|
||||||
|
|
||||||
ds.serverMu.Lock()
|
|
||||||
assert.NotNil(t, ds.server, "a reload with nothing running should bring DNS up")
|
|
||||||
ds.serverMu.Unlock()
|
|
||||||
|
|
||||||
ds.Stop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDnsServer_StartStop_lifecycle(t *testing.T) {
|
func TestDnsServer_StartStop_lifecycle(t *testing.T) {
|
||||||
@@ -464,168 +427,3 @@ func waitFor(t *testing.T, cond func() bool) {
|
|||||||
}
|
}
|
||||||
t.Fatal("timed out waiting for condition")
|
t.Fatal("timed out waiting for condition")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two reloads in quick succession, or a HUP before Control.Start, can race two Starts at the same listener.
|
|
||||||
func TestDnsServer_Start_isIdempotent(t *testing.T) {
|
|
||||||
port := freeUDPPort(t)
|
|
||||||
ds, c := newTestDnsServer(t)
|
|
||||||
setDnsConfig(c, "127.0.0.1", port, true, true)
|
|
||||||
require.NoError(t, ds.reload(c, true))
|
|
||||||
|
|
||||||
go ds.Start()
|
|
||||||
waitForBind(t, ds)
|
|
||||||
|
|
||||||
ds.serverMu.Lock()
|
|
||||||
first := ds.server
|
|
||||||
ds.serverMu.Unlock()
|
|
||||||
require.NotNil(t, first)
|
|
||||||
|
|
||||||
// If the second Start replaces the tracked server, Stop kills the wrong one and the port leaks
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
ds.Start()
|
|
||||||
close(done)
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
case <-time.After(time.Second * 5):
|
|
||||||
t.Fatal("second Start never returned")
|
|
||||||
}
|
|
||||||
|
|
||||||
ds.serverMu.Lock()
|
|
||||||
second := ds.server
|
|
||||||
ds.serverMu.Unlock()
|
|
||||||
assert.Same(t, first, second, "a second Start must not replace the running server")
|
|
||||||
|
|
||||||
// The real proof, after Stop the port must actually be free
|
|
||||||
ds.Stop()
|
|
||||||
waitFor(t, func() bool {
|
|
||||||
pc, err := net.ListenPacket("udp", "127.0.0.1:"+port)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_ = pc.Close()
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// An address change must actually end up listening on the new port. Start's guard refuses when a server is already
|
|
||||||
// installed, so reload has to clear the slot before shutting the old one down.
|
|
||||||
func TestDnsServer_reload_addrChange_restarts(t *testing.T) {
|
|
||||||
first := freeUDPPort(t)
|
|
||||||
second := freeUDPPort(t)
|
|
||||||
|
|
||||||
ds, c := newTestDnsServer(t)
|
|
||||||
setDnsConfig(c, "127.0.0.1", first, true, true)
|
|
||||||
require.NoError(t, ds.reload(c, true))
|
|
||||||
|
|
||||||
go ds.Start()
|
|
||||||
waitForBind(t, ds)
|
|
||||||
|
|
||||||
// Cycle a few times, the failure this guards against depends on which goroutine wins serverMu
|
|
||||||
for i := range 8 {
|
|
||||||
want := second
|
|
||||||
if i%2 == 1 {
|
|
||||||
want = first
|
|
||||||
}
|
|
||||||
setDnsConfig(c, "127.0.0.1", want, true, true)
|
|
||||||
require.NoError(t, ds.reload(c, false))
|
|
||||||
waitForBind(t, ds)
|
|
||||||
|
|
||||||
ds.serverMu.Lock()
|
|
||||||
srv := ds.server
|
|
||||||
ds.serverMu.Unlock()
|
|
||||||
require.NotNil(t, srv, "reload left DNS down instead of restarting it")
|
|
||||||
require.Equal(t, "127.0.0.1:"+want, srv.Addr, "reload should be serving the new address")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Land back on second so the port assertions below are meaningful
|
|
||||||
setDnsConfig(c, "127.0.0.1", second, true, true)
|
|
||||||
require.NoError(t, ds.reload(c, false))
|
|
||||||
waitForBind(t, ds)
|
|
||||||
|
|
||||||
// The old port must be released and the new one actually held
|
|
||||||
waitFor(t, func() bool {
|
|
||||||
pc, err := net.ListenPacket("udp", "127.0.0.1:"+first)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_ = pc.Close()
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
_, err := net.ListenPacket("udp", "127.0.0.1:"+second)
|
|
||||||
require.Error(t, err, "the new address should be bound by the DNS responder")
|
|
||||||
|
|
||||||
ds.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// A listener that dies on its own must release the slot, or a later same-addr reload sees it as running and no-ops.
|
|
||||||
func TestDnsServer_Start_bindFailure_releasesSlot(t *testing.T) {
|
|
||||||
port := freeUDPPort(t)
|
|
||||||
blocker, err := net.ListenPacket("udp", "127.0.0.1:"+port)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ds, c := newTestDnsServer(t)
|
|
||||||
setDnsConfig(c, "127.0.0.1", port, true, true)
|
|
||||||
require.NoError(t, ds.reload(c, true))
|
|
||||||
|
|
||||||
ds.Start() // returns once the bind fails
|
|
||||||
|
|
||||||
ds.serverMu.Lock()
|
|
||||||
assert.Nil(t, ds.server, "a listener that failed to bind must not stay parked in the slot")
|
|
||||||
ds.serverMu.Unlock()
|
|
||||||
|
|
||||||
// With the slot released, a reload can retry once the port frees up
|
|
||||||
require.NoError(t, blocker.Close())
|
|
||||||
require.NoError(t, ds.reload(c, false))
|
|
||||||
waitForBind(t, ds)
|
|
||||||
|
|
||||||
ds.serverMu.Lock()
|
|
||||||
assert.NotNil(t, ds.server, "a same-addr reload should retry after a failed bind")
|
|
||||||
ds.serverMu.Unlock()
|
|
||||||
|
|
||||||
ds.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// A disable that lands while Start is between its unlocked check and the guard must not leave a listener behind.
|
|
||||||
func TestDnsServer_Start_refusesWhenDisabledUnderLock(t *testing.T) {
|
|
||||||
port := freeUDPPort(t)
|
|
||||||
ds, c := newTestDnsServer(t)
|
|
||||||
setDnsConfig(c, "127.0.0.1", port, true, true)
|
|
||||||
require.NoError(t, ds.reload(c, true))
|
|
||||||
require.True(t, ds.enabled.Load())
|
|
||||||
|
|
||||||
// Holding serverMu parks Start on the lock, the only way to land the disable in that window on purpose
|
|
||||||
ds.serverMu.Lock()
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
ds.Start()
|
|
||||||
close(done)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
ds.serverMu.Unlock()
|
|
||||||
t.Fatal("Start returned early, the test never exercised the window")
|
|
||||||
case <-time.After(time.Millisecond * 100):
|
|
||||||
}
|
|
||||||
|
|
||||||
// The disable reload's critical section. It sees nothing running, so it never calls Stop.
|
|
||||||
ds.enabled.Store(false)
|
|
||||||
ds.serverMu.Unlock()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
case <-time.After(time.Second * 5):
|
|
||||||
t.Fatal("Start never returned")
|
|
||||||
}
|
|
||||||
|
|
||||||
ds.serverMu.Lock()
|
|
||||||
assert.Nil(t, ds.server, "Start must not install a listener a disable already cancelled")
|
|
||||||
ds.serverMu.Unlock()
|
|
||||||
|
|
||||||
pc, err := net.ListenPacket("udp", "127.0.0.1:"+port)
|
|
||||||
require.NoError(t, err, "an orphaned listener is still holding the port")
|
|
||||||
_ = pc.Close()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -725,70 +725,6 @@ func TestReestablishRelays(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRelayHandshakeOverDisestablishedEntry(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
// If them tears down the tunnel while me keeps Established relay state, me's next
|
|
||||||
// handshake flows through the relay with no fresh CreateRelayRequest and lands on
|
|
||||||
// them's Disestablished terminal relay entry. them must re-establish that entry, or
|
|
||||||
// its first transmit deletes its only relay and the tunnel is born transmit-dead:
|
|
||||||
// them can receive but every send is silently dropped.
|
|
||||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
|
||||||
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version1, ca, caKey, "me ", "10.128.0.1/24", m{"relay": m{"use_relays": true}})
|
|
||||||
relayControl, relayVpnIpNet, relayUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "relay ", "10.128.0.128/24", m{"relay": m{"am_relay": true}})
|
|
||||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them ", "10.128.0.2/24", m{"relay": m{"use_relays": true}})
|
|
||||||
|
|
||||||
// Teach my how to get to the relay and that their can be reached via the relay
|
|
||||||
myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr)
|
|
||||||
myControl.InjectRelays(theirVpnIpNet[0].Addr(), []netip.Addr{relayVpnIpNet[0].Addr()})
|
|
||||||
relayControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
|
||||||
|
|
||||||
// Build a router so we don't have to reason who gets which packet
|
|
||||||
r := router.NewR(t, myControl, relayControl, theirControl)
|
|
||||||
defer r.RenderFlow()
|
|
||||||
|
|
||||||
// Start the servers
|
|
||||||
myControl.Start()
|
|
||||||
relayControl.Start()
|
|
||||||
theirControl.Start()
|
|
||||||
|
|
||||||
t.Log("Trigger a handshake from me to them via the relay")
|
|
||||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
|
||||||
|
|
||||||
p := r.RouteForAllUntilTxTun(theirControl)
|
|
||||||
assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
|
|
||||||
oldIdx := myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), false).LocalIndex
|
|
||||||
|
|
||||||
t.Log("Close the tunnel on them only, marking their relay entry Disestablished")
|
|
||||||
theirControl.CloseTunnel(myVpnIpNet[0].Addr(), true)
|
|
||||||
|
|
||||||
t.Log("Re-handshake from me, riding the still-Established relay state")
|
|
||||||
myControl.ReHandshake(theirVpnIpNet[0].Addr())
|
|
||||||
for {
|
|
||||||
h := myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), false)
|
|
||||||
if h != nil && h.LocalIndex != oldIdx && h.RemoteIndex != 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
r.RouteForAllExitFunc(func(*udp.Packet, *nebula.Control) router.ExitType {
|
|
||||||
return router.RouteAndExit
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
hAtThem := theirControl.GetHostInfoByVpnAddr(myVpnIpNet[0].Addr(), false)
|
|
||||||
require.NotNil(t, hAtThem, "them should have completed the relayed handshake")
|
|
||||||
require.Equal(t, []netip.Addr{relayVpnIpNet[0].Addr()}, hAtThem.CurrentRelaysToMe, "them should know a relay for the new tunnel")
|
|
||||||
|
|
||||||
t.Log("Send from them to me; their only relay entry must survive the transmit")
|
|
||||||
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them")))
|
|
||||||
require.Never(t, func() bool {
|
|
||||||
h := theirControl.GetHostInfoByVpnAddr(myVpnIpNet[0].Addr(), false)
|
|
||||||
return h == nil || len(h.CurrentRelaysToMe) == 0
|
|
||||||
}, time.Second, 10*time.Millisecond, "them deleted its only relay entry; the tunnel is permanently transmit-dead")
|
|
||||||
|
|
||||||
p = r.RouteForAllUntilTxTun(myControl)
|
|
||||||
assertUdpPacket(t, []byte("Hi from them"), p, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), 80, 80)
|
|
||||||
r.RenderHostmaps("Final hostmaps", myControl, relayControl, theirControl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStage1RaceRelays(t *testing.T) {
|
func TestStage1RaceRelays(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//NOTE: this is a race between me and relay resulting in a full tunnel from me to them via relay
|
//NOTE: this is a race between me and relay resulting in a full tunnel from me to them via relay
|
||||||
|
|||||||
+14
-14
@@ -254,6 +254,20 @@ tun:
|
|||||||
# Default MTU for every packet, safe setting is (and the default) 1300 for internet based traffic
|
# Default MTU for every packet, safe setting is (and the default) 1300 for internet based traffic
|
||||||
mtu: 1300
|
mtu: 1300
|
||||||
|
|
||||||
|
# Linux only. pin_threads pins each tun reader/encrypt OS thread to a single CPU. This keeps every goroutine's
|
||||||
|
# sends flowing through one XPS-selected NIC TX ring, so packets within a flow stay ordered on the wire
|
||||||
|
# instead of being sprayed across multiple TX rings and reordered. Not reloadable.
|
||||||
|
#pin_threads: true
|
||||||
|
|
||||||
|
# Linux only. cpu_affinity overrides which CPUs the tun reader threads pin to: a list of CPU IDs, one per routine
|
||||||
|
# (see the top-level `routines` setting). Lists shorter than `routines` are modulo-cycled across the queues; extra
|
||||||
|
# entries are ignored. IDs must be within the process's allowed CPU set, so this respects taskset / cgroup cpusets;
|
||||||
|
# a non-integer or not-allowed entry disables the override and falls back to spreading queues across the allowed
|
||||||
|
# CPUs. Only meaningful while pin_threads is true. Not reloadable.
|
||||||
|
#cpu_affinity:
|
||||||
|
# - 2
|
||||||
|
# - 4
|
||||||
|
|
||||||
# Route based MTU overrides, you have known vpn ip paths that can support larger MTUs you can increase/decrease them here
|
# Route based MTU overrides, you have known vpn ip paths that can support larger MTUs you can increase/decrease them here
|
||||||
routes:
|
routes:
|
||||||
#- mtu: 8800
|
#- mtu: 8800
|
||||||
@@ -390,20 +404,6 @@ logging:
|
|||||||
# This setting is reloadable
|
# This setting is reloadable
|
||||||
#inactivity_timeout: 10m
|
#inactivity_timeout: 10m
|
||||||
|
|
||||||
# clear_on_wake controls whether all tunnels are immediately torn down (locally, without notifying the remotes)
|
|
||||||
# when the machine detects it has just woken from system sleep. Tunnels rarely survive a suspend: NAT mappings
|
|
||||||
# expire and the machine's address usually changes, so waiting for the normal liveness checks costs 15-20 seconds
|
|
||||||
# of black-holed traffic per tunnel after wake. Clearing them means the first packet after wake starts a fresh
|
|
||||||
# handshake right away.
|
|
||||||
# This setting is reloadable
|
|
||||||
#clear_on_wake: true
|
|
||||||
|
|
||||||
# wake_clear_threshold is the minimum time the machine must have been suspended for clear_on_wake to act.
|
|
||||||
# Suspends shorter than this often come back before NAT state expires, so those tunnels may still be alive and
|
|
||||||
# are left to the normal liveness checks. Values below 1s are treated as 1s.
|
|
||||||
# This setting is reloadable
|
|
||||||
#wake_clear_threshold: 30s
|
|
||||||
|
|
||||||
# Nebula security group configuration
|
# Nebula security group configuration
|
||||||
firewall:
|
firewall:
|
||||||
# Action to take when a packet is not allowed by the firewall rules.
|
# Action to take when a packet is not allowed by the firewall rules.
|
||||||
|
|||||||
@@ -8,15 +8,6 @@ Before=sshd.service
|
|||||||
Type=notify
|
Type=notify
|
||||||
NotifyAccess=main
|
NotifyAccess=main
|
||||||
SyslogIdentifier=nebula
|
SyslogIdentifier=nebula
|
||||||
|
|
||||||
# Uncomment to run as an unprivileged user with only CAP_NET_ADMIN. Requires a
|
|
||||||
# nebula user that owns the config directory. Add CAP_NET_BIND_SERVICE to both
|
|
||||||
# lines if any listener (lighthouse DNS, listen.port, stats, sshd) binds <1024.
|
|
||||||
#User=nebula
|
|
||||||
#Group=nebula
|
|
||||||
#CapabilityBoundingSet=CAP_NET_ADMIN
|
|
||||||
#AmbientCapabilities=CAP_NET_ADMIN
|
|
||||||
|
|
||||||
ExecReload=/bin/kill -HUP $MAINPID
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
ExecStart=/usr/local/bin/nebula -config /etc/nebula/config.yml
|
ExecStart=/usr/local/bin/nebula -config /etc/nebula/config.yml
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ require (
|
|||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
go.uber.org/goleak v1.3.0
|
go.uber.org/goleak v1.3.0
|
||||||
go.yaml.in/yaml/v3 v3.0.4
|
go.yaml.in/yaml/v3 v3.0.4
|
||||||
golang.org/x/crypto v0.54.0
|
golang.org/x/crypto v0.53.0
|
||||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
|
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
|
||||||
golang.org/x/net v0.57.0
|
golang.org/x/net v0.56.0
|
||||||
golang.org/x/sync v0.22.0
|
golang.org/x/sync v0.21.0
|
||||||
golang.org/x/sys v0.47.0
|
golang.org/x/sys v0.46.0
|
||||||
golang.org/x/term v0.45.0
|
golang.org/x/term v0.44.0
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
|
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
|
||||||
golang.zx2c4.com/wireguard/windows v1.0.1
|
golang.zx2c4.com/wireguard/windows v1.0.1
|
||||||
|
|||||||
@@ -162,8 +162,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
|
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
|
||||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
@@ -182,8 +182,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -191,8 +191,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -208,11 +208,11 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
|||||||
@@ -529,9 +529,7 @@ func (hm *HandshakeManager) DeleteHostInfo(hostinfo *HostInfo) {
|
|||||||
|
|
||||||
func (hm *HandshakeManager) unlockedDeleteHostInfo(hostinfo *HostInfo) {
|
func (hm *HandshakeManager) unlockedDeleteHostInfo(hostinfo *HostInfo) {
|
||||||
for _, addr := range hostinfo.vpnAddrs {
|
for _, addr := range hostinfo.vpnAddrs {
|
||||||
if cur, ok := hm.vpnIps[addr]; ok && cur.hostinfo == hostinfo {
|
delete(hm.vpnIps, addr)
|
||||||
delete(hm.vpnIps, addr)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(hm.vpnIps) == 0 {
|
if len(hm.vpnIps) == 0 {
|
||||||
@@ -1079,7 +1077,7 @@ func (hm *HandshakeManager) sendHandshakeResponse(via ViaSender, msg []byte, hos
|
|||||||
hostinfo.relayState.InsertRelayTo(via.relayHI.vpnAddrs[0])
|
hostinfo.relayState.InsertRelayTo(via.relayHI.vpnAddrs[0])
|
||||||
// We received a valid handshake on this relay, so make sure the relay
|
// We received a valid handshake on this relay, so make sure the relay
|
||||||
// state reflects that, in case it had been marked Disestablished.
|
// state reflects that, in case it had been marked Disestablished.
|
||||||
via.relayHI.relayState.UpdateRelayForByIdxState(via.relay.LocalIndex, Established)
|
via.relayHI.relayState.UpdateRelayForByIdxState(via.remoteIdx, Established)
|
||||||
f.SendVia(via.relayHI, via.relay, msg, make([]byte, 12), make([]byte, mtu), false)
|
f.SendVia(via.relayHI, via.relay, msg, make([]byte, 12), make([]byte, mtu), false)
|
||||||
f.l.Info("Handshake message sent", append(logFields, "relay", via.relayHI.vpnAddrs[0])...)
|
f.l.Info("Handshake message sent", append(logFields, "relay", via.relayHI.vpnAddrs[0])...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,6 +287,7 @@ type HostInfo struct {
|
|||||||
type ViaSender struct {
|
type ViaSender struct {
|
||||||
UdpAddr netip.AddrPort
|
UdpAddr netip.AddrPort
|
||||||
relayHI *HostInfo // relayHI is the host info object of the relay
|
relayHI *HostInfo // relayHI is the host info object of the relay
|
||||||
|
remoteIdx uint32 // remoteIdx is the index included in the header of the received packet
|
||||||
relay *Relay // relay contains the rest of the relay information, including the PeerIP of the host trying to communicate with us.
|
relay *Relay // relay contains the rest of the relay information, including the PeerIP of the host trying to communicate with us.
|
||||||
IsRelayed bool // IsRelayed is true if the packet was sent through a relay
|
IsRelayed bool // IsRelayed is true if the packet was sent through a relay
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet
|
|||||||
// routes packets from the Nebula addr to the Nebula addr through the Nebula
|
// routes packets from the Nebula addr to the Nebula addr through the Nebula
|
||||||
// TUN device.
|
// TUN device.
|
||||||
if immediatelyForwardToSelf {
|
if immediatelyForwardToSelf {
|
||||||
_, err := f.readers[q].Write(packet)
|
_, err := f.queues[q].Write(packet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.l.Error("Failed to forward to tun", "error", err)
|
f.l.Error("Failed to forward to tun", "error", err)
|
||||||
}
|
}
|
||||||
@@ -96,7 +96,7 @@ func (f *Interface) rejectInside(packet []byte, out []byte, q int) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := f.readers[q].Write(out)
|
_, err := f.queues[q].Write(out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.l.Error("Failed to write to tun", "error", err)
|
f.l.Error("Failed to write to tun", "error", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-27
@@ -4,9 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -20,7 +20,9 @@ import (
|
|||||||
"github.com/slackhq/nebula/firewall"
|
"github.com/slackhq/nebula/firewall"
|
||||||
"github.com/slackhq/nebula/header"
|
"github.com/slackhq/nebula/header"
|
||||||
"github.com/slackhq/nebula/overlay"
|
"github.com/slackhq/nebula/overlay"
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/udp"
|
"github.com/slackhq/nebula/udp"
|
||||||
|
"github.com/slackhq/nebula/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const mtu = 9001
|
const mtu = 9001
|
||||||
@@ -49,7 +51,19 @@ type InterfaceConfig struct {
|
|||||||
reQueryWait time.Duration
|
reQueryWait time.Duration
|
||||||
|
|
||||||
ConntrackCacheTimeout time.Duration
|
ConntrackCacheTimeout time.Duration
|
||||||
l *slog.Logger
|
|
||||||
|
// CpuAffinity, when non-empty, names the CPUs each TUN reader goroutine
|
||||||
|
// should pin to. Queue i pins to CpuAffinity[i % len(CpuAffinity)] —
|
||||||
|
// shorter lists than `routines` cycle. Empty list keeps the default
|
||||||
|
// pin-to-(i % NumCPU) behavior. Only consulted when PinThreads is true.
|
||||||
|
CpuAffinity []int
|
||||||
|
// PinThreads controls whether each TUN reader OS thread is pinned to a
|
||||||
|
// single CPU (via tun.pin_threads, default true). Pinning keeps each
|
||||||
|
// goroutine's UDP sends on one XPS-selected NIC TX ring so per-flow
|
||||||
|
// packets stay ordered on the wire.
|
||||||
|
PinThreads bool
|
||||||
|
|
||||||
|
l *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
type Interface struct {
|
type Interface struct {
|
||||||
@@ -73,7 +87,16 @@ type Interface struct {
|
|||||||
routines int
|
routines int
|
||||||
disconnectInvalid atomic.Bool
|
disconnectInvalid atomic.Bool
|
||||||
closed atomic.Bool
|
closed atomic.Bool
|
||||||
relayManager *relayManager
|
// cpuAffinity, when non-empty, names the CPUs each TUN reader goroutine
|
||||||
|
// should pin to. Queue i pins to cpuAffinity[i % len(cpuAffinity)].
|
||||||
|
// Empty falls back to the default pin-to-(allowed CPU) behavior.
|
||||||
|
// Only consulted when pinThreads is true.
|
||||||
|
cpuAffinity []int
|
||||||
|
// pinThreads controls whether listenIn pins each TUN reader OS thread to
|
||||||
|
// a CPU at all (tun.pin_threads, default true). When false, threads are
|
||||||
|
// left free to migrate as on stock nebula.
|
||||||
|
pinThreads bool
|
||||||
|
relayManager *relayManager
|
||||||
|
|
||||||
tryPromoteEvery atomic.Uint32
|
tryPromoteEvery atomic.Uint32
|
||||||
reQueryEvery atomic.Uint32
|
reQueryEvery atomic.Uint32
|
||||||
@@ -90,7 +113,7 @@ type Interface struct {
|
|||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
writers []udp.Conn
|
writers []udp.Conn
|
||||||
readers []io.ReadWriteCloser
|
queues []tio.Queue
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
|
||||||
// fatalErr holds the first unexpected reader error that caused shutdown.
|
// fatalErr holds the first unexpected reader error that caused shutdown.
|
||||||
@@ -189,7 +212,6 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
|
|||||||
routines: c.routines,
|
routines: c.routines,
|
||||||
version: c.version,
|
version: c.version,
|
||||||
writers: make([]udp.Conn, c.routines),
|
writers: make([]udp.Conn, c.routines),
|
||||||
readers: make([]io.ReadWriteCloser, c.routines),
|
|
||||||
myVpnNetworks: cs.myVpnNetworks,
|
myVpnNetworks: cs.myVpnNetworks,
|
||||||
myVpnNetworksTable: cs.myVpnNetworksTable,
|
myVpnNetworksTable: cs.myVpnNetworksTable,
|
||||||
myVpnAddrs: cs.myVpnAddrs,
|
myVpnAddrs: cs.myVpnAddrs,
|
||||||
@@ -198,6 +220,8 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
|
|||||||
relayManager: c.relayManager,
|
relayManager: c.relayManager,
|
||||||
connectionManager: c.connectionManager,
|
connectionManager: c.connectionManager,
|
||||||
conntrackCacheTimeout: c.ConntrackCacheTimeout,
|
conntrackCacheTimeout: c.ConntrackCacheTimeout,
|
||||||
|
cpuAffinity: c.CpuAffinity,
|
||||||
|
pinThreads: c.PinThreads,
|
||||||
|
|
||||||
metricHandshakes: metrics.GetOrRegisterHistogram("handshakes", nil, metrics.NewExpDecaySample(1028, 0.015)),
|
metricHandshakes: metrics.GetOrRegisterHistogram("handshakes", nil, metrics.NewExpDecaySample(1028, 0.015)),
|
||||||
messageMetrics: c.MessageMetrics,
|
messageMetrics: c.MessageMetrics,
|
||||||
@@ -240,27 +264,27 @@ func (f *Interface) activate() error {
|
|||||||
"boringcrypto", boringEnabled(),
|
"boringcrypto", boringEnabled(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if f.routines > 1 {
|
if f.routines > 1 && !f.outside.SupportsMultipleReaders() {
|
||||||
if !f.inside.SupportsMultiqueue() || !f.outside.SupportsMultipleReaders() {
|
f.routines = 1
|
||||||
f.routines = 1
|
f.l.Warn("multiple udp readers are not supported on this platform, falling back to a single routine")
|
||||||
f.l.Warn("routines is not supported on this platform, falling back to a single routine")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare the tun queues. A device that can't open that many hands back
|
||||||
|
// fewer (a single queue on platforms without multiqueue support) and we
|
||||||
|
// size the reader routines to what we actually got.
|
||||||
|
queues, err := f.inside.Queues(f.routines)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(queues) < f.routines {
|
||||||
|
f.l.Warn("tun multiqueue is not supported on this platform, falling back to fewer routines",
|
||||||
|
"requested", f.routines, "opened", len(queues))
|
||||||
|
f.routines = len(queues)
|
||||||
|
}
|
||||||
|
f.queues = queues
|
||||||
|
|
||||||
metrics.GetOrRegisterGauge("routines", nil).Update(int64(f.routines))
|
metrics.GetOrRegisterGauge("routines", nil).Update(int64(f.routines))
|
||||||
|
|
||||||
// Prepare n tun queues
|
|
||||||
var reader io.ReadWriteCloser = f.inside
|
|
||||||
for i := 0; i < f.routines; i++ {
|
|
||||||
if i > 0 {
|
|
||||||
reader, err = f.inside.NewMultiQueueReader()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.readers[i] = reader
|
|
||||||
}
|
|
||||||
|
|
||||||
// On error the caller owns the cleanup, Control.Start cancels the service context
|
// 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
|
// before releasing our resources so a waiter never observes a live context
|
||||||
if err = f.inside.Activate(); err != nil {
|
if err = f.inside.Activate(); err != nil {
|
||||||
@@ -281,7 +305,7 @@ func (f *Interface) run() {
|
|||||||
// Launch n queues to read packets from tun dev
|
// Launch n queues to read packets from tun dev
|
||||||
for i := 0; i < f.routines; i++ {
|
for i := 0; i < f.routines; i++ {
|
||||||
f.wg.Go(func() {
|
f.wg.Go(func() {
|
||||||
f.listenIn(f.readers[i], i)
|
f.listenIn(f.queues[i], i)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,8 +360,29 @@ func (f *Interface) listenOut(i int) {
|
|||||||
f.l.Debug("underlay reader is done", "reader", i)
|
f.l.Debug("underlay reader is done", "reader", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Interface) listenIn(reader io.ReadWriteCloser, i int) {
|
func (f *Interface) listenIn(queue tio.Queue, i int) {
|
||||||
packet := make([]byte, mtu)
|
// Pinning this thread (and goroutine) to a single CPU keeps every UDP send from this goroutine going through
|
||||||
|
// the same TX ring on the nic (XPS selects the ring by CPU), so the wire sees per-flow order. Skip entirely
|
||||||
|
// when tun.pin_threads is false.
|
||||||
|
if f.pinThreads {
|
||||||
|
var cpu int
|
||||||
|
if n := len(f.cpuAffinity); n > 0 {
|
||||||
|
// Explicit tun.cpu_affinity list wins; parseCpuAffinity already
|
||||||
|
// validated the entries against the allowed CPU set.
|
||||||
|
cpu = f.cpuAffinity[i%n]
|
||||||
|
} else if allowed, err := util.AllowedCPUs(); err == nil && len(allowed) > 0 {
|
||||||
|
// Default: spread queues across the CPUs we're actually allowed to
|
||||||
|
// run on. Under a cpuset/taskset mask these aren't 0..NumCPU-1, so
|
||||||
|
// i % NumCPU would pick unrunnable IDs and every pin would fail.
|
||||||
|
cpu = allowed[i%len(allowed)]
|
||||||
|
} else {
|
||||||
|
cpu = i % runtime.NumCPU()
|
||||||
|
}
|
||||||
|
if err := util.PinThreadToCPU(cpu); err != nil {
|
||||||
|
f.l.Warn("failed to pin tun reader to CPU", "queue", i, "cpu", cpu, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
out := make([]byte, mtu)
|
out := make([]byte, mtu)
|
||||||
fwPacket := &firewall.Packet{}
|
fwPacket := &firewall.Packet{}
|
||||||
nb := make([]byte, 12, 12)
|
nb := make([]byte, 12, 12)
|
||||||
@@ -345,7 +390,7 @@ func (f *Interface) listenIn(reader io.ReadWriteCloser, i int) {
|
|||||||
conntrackCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout)
|
conntrackCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
n, err := reader.Read(packet)
|
pkts, err := queue.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Same shutdown noise handling as listenOut
|
// Same shutdown noise handling as listenOut
|
||||||
if !f.closed.Load() && f.ctx.Err() == nil {
|
if !f.closed.Load() && f.ctx.Err() == nil {
|
||||||
@@ -355,7 +400,11 @@ func (f *Interface) listenIn(reader io.ReadWriteCloser, i int) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
f.consumeInsidePacket(packet[:n], fwPacket, nb, out, i, conntrackCache.Get())
|
for _, pkt := range pkts {
|
||||||
|
// borrowed: pkt.Bytes is owned by the queue and only valid until
|
||||||
|
// the next Read; consumeInsidePacket reads it synchronously.
|
||||||
|
f.consumeInsidePacket(pkt.Bytes, fwPacket, nb, out, i, conntrackCache.Get())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f.l.Debug("overlay reader is done", "reader", i)
|
f.l.Debug("overlay reader is done", "reader", i)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -231,6 +232,8 @@ func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, dev
|
|||||||
relayManager: NewRelayManager(ctx, l, hostMap, c),
|
relayManager: NewRelayManager(ctx, l, hostMap, c),
|
||||||
punchy: punchy,
|
punchy: punchy,
|
||||||
ConntrackCacheTimeout: conntrackCacheTimeout,
|
ConntrackCacheTimeout: conntrackCacheTimeout,
|
||||||
|
CpuAffinity: parseCpuAffinity(c, l, routines),
|
||||||
|
PinThreads: c.GetBool("tun.pin_threads", true),
|
||||||
l: l,
|
l: l,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +285,70 @@ func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, dev
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseCpuAffinity reads `tun.cpu_affinity` from the config — a list of
|
||||||
|
// integer CPU IDs, one per TUN reader goroutine. Empty / unset returns nil
|
||||||
|
// (listenIn falls back to spreading queues across the allowed CPU set).
|
||||||
|
// Length mismatch with `routines` is a warning, not an error: shorter lists
|
||||||
|
// are modulo-cycled across queues, longer lists' tail is ignored. Invalid
|
||||||
|
// entries (non-integer, or a CPU ID we're not allowed to run on) are also a
|
||||||
|
// warning and disable the override entirely so we don't silently pin to the
|
||||||
|
// wrong CPU. Entries are validated against the process's current affinity
|
||||||
|
// mask (util.AllowedCPUs) rather than 0..NumCPU-1: under a cgroup cpuset or
|
||||||
|
// taskset the runnable IDs are frequently not that contiguous range, and
|
||||||
|
// pinning to an unrunnable ID always fails. If the allowed set can't be
|
||||||
|
// determined we fall back to a plain non-negative check.
|
||||||
|
func parseCpuAffinity(c *config.C, l *slog.Logger, routines int) []int {
|
||||||
|
raw := c.Get("tun.cpu_affinity")
|
||||||
|
if raw == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rv, ok := raw.([]any)
|
||||||
|
if !ok {
|
||||||
|
l.Warn("tun.cpu_affinity must be a list of integers; ignoring", "value", raw)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// allowed is the set of CPU IDs we're actually permitted to run on. A nil
|
||||||
|
// slice (unsupported platform or lookup error) means "can't tell", so we
|
||||||
|
// only apply the weaker non-negative check in that case.
|
||||||
|
allowed, err := util.AllowedCPUs()
|
||||||
|
if err != nil {
|
||||||
|
l.Warn("could not determine allowed CPUs; validating tun.cpu_affinity against non-negative only", "error", err)
|
||||||
|
allowed = nil
|
||||||
|
}
|
||||||
|
cpus := make([]int, 0, len(rv))
|
||||||
|
for i, e := range rv {
|
||||||
|
var cpu int
|
||||||
|
switch v := e.(type) {
|
||||||
|
case int:
|
||||||
|
cpu = v
|
||||||
|
case int64:
|
||||||
|
cpu = int(v)
|
||||||
|
case float64:
|
||||||
|
cpu = int(v)
|
||||||
|
default:
|
||||||
|
l.Warn("tun.cpu_affinity entry not an integer; ignoring affinity",
|
||||||
|
"index", i, "value", e)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cpu < 0 {
|
||||||
|
l.Warn("tun.cpu_affinity entry out of range; ignoring affinity",
|
||||||
|
"index", i, "cpu", cpu)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(allowed) > 0 && !slices.Contains(allowed, cpu) {
|
||||||
|
l.Warn("tun.cpu_affinity entry not in allowed CPU set; ignoring affinity",
|
||||||
|
"index", i, "cpu", cpu, "allowed", allowed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cpus = append(cpus, cpu)
|
||||||
|
}
|
||||||
|
if len(cpus) != routines {
|
||||||
|
l.Warn("tun.cpu_affinity length doesn't match routines; queues will modulo-cycle through the list",
|
||||||
|
"affinity_len", len(cpus), "routines", routines)
|
||||||
|
}
|
||||||
|
return cpus
|
||||||
|
}
|
||||||
|
|
||||||
func moduleVersion() string {
|
func moduleVersion() string {
|
||||||
info, ok := debug.ReadBuildInfo()
|
info, ok := debug.ReadBuildInfo()
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package nebula
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/config"
|
||||||
|
"github.com/slackhq/nebula/test"
|
||||||
|
"github.com/slackhq/nebula/util"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseCpuAffinity(t *testing.T) {
|
||||||
|
l := test.NewLogger()
|
||||||
|
|
||||||
|
// newConfig returns a config.C with tun.cpu_affinity set to v. A nil v
|
||||||
|
// leaves the key unset.
|
||||||
|
newConfig := func(v any) *config.C {
|
||||||
|
c := config.NewC(l)
|
||||||
|
if v != nil {
|
||||||
|
c.Settings["tun"] = map[string]any{"cpu_affinity": v}
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// unset -> nil (listenIn falls back to spreading across the allowed set)
|
||||||
|
assert.Nil(t, parseCpuAffinity(newConfig(nil), l, 1))
|
||||||
|
|
||||||
|
// Pick a CPU we're actually allowed to run on so a valid list survives
|
||||||
|
// validation regardless of the host's affinity mask.
|
||||||
|
allowed, _ := util.AllowedCPUs()
|
||||||
|
validCPU := 0
|
||||||
|
if len(allowed) > 0 {
|
||||||
|
validCPU = allowed[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// valid list -> parsed through unchanged
|
||||||
|
assert.Equal(t, []int{validCPU, validCPU}, parseCpuAffinity(newConfig([]any{validCPU, validCPU}), l, 2))
|
||||||
|
|
||||||
|
// a negative entry is out of range on every platform -> disables the override
|
||||||
|
assert.Nil(t, parseCpuAffinity(newConfig([]any{validCPU, -1}), l, 2))
|
||||||
|
|
||||||
|
// a non-integer entry -> disables the override
|
||||||
|
assert.Nil(t, parseCpuAffinity(newConfig([]any{validCPU, "not-a-cpu"}), l, 2))
|
||||||
|
|
||||||
|
// a CPU id outside the allowed set -> disables the override. Only assertable
|
||||||
|
// where we can enumerate the allowed set (e.g. linux); 1<<20 is far beyond
|
||||||
|
// any representable CPU id so it can never be in the mask.
|
||||||
|
if len(allowed) > 0 {
|
||||||
|
assert.Nil(t, parseCpuAffinity(newConfig([]any{1 << 20}), l, 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
+56
-27
@@ -102,31 +102,27 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(packet) < header.Len+hostinfo.ConnectionState.dKey.Overhead() {
|
|
||||||
f.messageMetrics.RxInvalid(1)
|
|
||||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
|
||||||
f.l.Debug("packet too small", "from", via, "length", len(packet))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// All remaining packets are encrypted
|
// All remaining packets are encrypted
|
||||||
if isMessageRelay {
|
ci := hostinfo.ConnectionState
|
||||||
// Relay packets are special, this branch should always early-return
|
if !ci.window.Check(f.l, h.MessageCounter) {
|
||||||
if err = hostinfo.ConnectionState.VerifyRelay(f.l, h.MessageCounter, packet, nb); err != nil {
|
|
||||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
|
||||||
hostinfo.logger(f.l).Debug("Failed to verify relay packet", "error", err, "from", via, "header", h)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f.handleOutsideRelayPacket(hostinfo, via, out, packet, h, fwPacket, lhf, nb, q, localCache)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err = hostinfo.ConnectionState.Decrypt(f.l, h.MessageCounter, out, packet, nb)
|
// Relay packets are special
|
||||||
|
if isMessageRelay {
|
||||||
|
f.handleOutsideRelayPacket(hostinfo, via, out, packet, h, fwPacket, lhf, nb, q, localCache)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err = f.decrypt(hostinfo, h.MessageCounter, out, packet, h, nb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||||
hostinfo.logger(f.l).Debug("Failed to decrypt packet", "error", err, "from", via, "header", h)
|
hostinfo.logger(f.l).Debug("Failed to decrypt packet",
|
||||||
|
"error", err,
|
||||||
|
"from", via,
|
||||||
|
"header", h,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -155,7 +151,7 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
|
|||||||
// No-op, useful for the Roaming and connectionManager side-effects above
|
// No-op, useful for the Roaming and connectionManager side-effects above
|
||||||
case header.TestRequest:
|
case header.TestRequest:
|
||||||
//recycle the input packet ciphertext as our output buffer
|
//recycle the input packet ciphertext as our output buffer
|
||||||
f.send(header.Test, header.TestReply, hostinfo.ConnectionState, hostinfo, out, nb, packet)
|
f.send(header.Test, header.TestReply, ci, hostinfo, out, nb, packet)
|
||||||
default:
|
default:
|
||||||
hostinfo.logger(f.l).Error("IsValidSubType was true, but unexpected test subtype seen", "from", via, "header", h)
|
hostinfo.logger(f.l).Error("IsValidSubType was true, but unexpected test subtype seen", "from", via, "header", h)
|
||||||
return
|
return
|
||||||
@@ -174,8 +170,27 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Interface) handleOutsideRelayPacket(hostinfo *HostInfo, via ViaSender, out []byte, packet []byte, h *header.H, fwPacket *firewall.Packet, lhf *LightHouseHandler, nb []byte, q int, localCache firewall.ConntrackCache) {
|
func (f *Interface) handleOutsideRelayPacket(hostinfo *HostInfo, via ViaSender, out []byte, packet []byte, h *header.H, fwPacket *firewall.Packet, lhf *LightHouseHandler, nb []byte, q int, localCache firewall.ConntrackCache) {
|
||||||
// Successfully validated the thing. Get rid of the Relay header and the AEAD tag
|
// The entire body is sent as AD, not encrypted.
|
||||||
signedPayload := packet[header.Len : len(packet)-hostinfo.ConnectionState.dKey.Overhead()]
|
// The packet consists of a 16-byte parsed Nebula header, Associated Data-protected payload, and a trailing 16-byte AEAD signature value.
|
||||||
|
// The packet is guaranteed to be at least 16 bytes at this point, b/c it got past the h.Parse() call above. If it's
|
||||||
|
// otherwise malformed (meaning, there is no trailing 16 byte AEAD value), then this will result in at worst a 0-length slice
|
||||||
|
// which will gracefully fail in the DecryptDanger call.
|
||||||
|
signedPayload := packet[:len(packet)-hostinfo.ConnectionState.dKey.Overhead()]
|
||||||
|
signatureValue := packet[len(packet)-hostinfo.ConnectionState.dKey.Overhead():]
|
||||||
|
var err error
|
||||||
|
out, err = hostinfo.ConnectionState.dKey.DecryptDanger(out, signedPayload, signatureValue, h.MessageCounter, nb)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Advance the replay window now that the frame is authenticated
|
||||||
|
if !hostinfo.ConnectionState.window.Update(f.l, h.MessageCounter) {
|
||||||
|
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||||
|
hostinfo.logger(f.l).Debug("dropping out of window relay packet", "header", h)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Successfully validated the thing. Get rid of the Relay header.
|
||||||
|
signedPayload = signedPayload[header.Len:]
|
||||||
// Pull the Roaming parts up here, and return in all call paths.
|
// Pull the Roaming parts up here, and return in all call paths.
|
||||||
f.handleHostRoaming(hostinfo, via)
|
f.handleHostRoaming(hostinfo, via)
|
||||||
// Track usage of both the HostInfo and the Relay for the received & authenticated packet
|
// Track usage of both the HostInfo and the Relay for the received & authenticated packet
|
||||||
@@ -199,6 +214,7 @@ func (f *Interface) handleOutsideRelayPacket(hostinfo *HostInfo, via ViaSender,
|
|||||||
via = ViaSender{
|
via = ViaSender{
|
||||||
UdpAddr: via.UdpAddr,
|
UdpAddr: via.UdpAddr,
|
||||||
relayHI: hostinfo,
|
relayHI: hostinfo,
|
||||||
|
remoteIdx: relay.RemoteIndex,
|
||||||
relay: relay,
|
relay: relay,
|
||||||
IsRelayed: true,
|
IsRelayed: true,
|
||||||
}
|
}
|
||||||
@@ -219,10 +235,9 @@ func (f *Interface) handleOutsideRelayPacket(hostinfo *HostInfo, via ViaSender,
|
|||||||
if targetRelay.State == Established {
|
if targetRelay.State == Established {
|
||||||
switch targetRelay.Type {
|
switch targetRelay.Type {
|
||||||
case ForwardingType:
|
case ForwardingType:
|
||||||
// Forward this packet through the relay tunnel, rebuilding it in place.
|
// Forward this packet through the relay tunnel
|
||||||
// Encode overwrites the old outer header, and the new AEAD tag lands where the old one was
|
// Find the target HostInfo
|
||||||
fwdBuf := packet[:0:len(packet)] // Cap to len(packet) to protect memory from a larger parent buffer
|
f.SendVia(targetHI, targetRelay, signedPayload, nb, out, false)
|
||||||
f.SendVia(targetHI, targetRelay, signedPayload, nb, fwdBuf, true)
|
|
||||||
case TerminalType:
|
case TerminalType:
|
||||||
hostinfo.logger(f.l).Error("Unexpected Relay Type of Terminal")
|
hostinfo.logger(f.l).Error("Unexpected Relay Type of Terminal")
|
||||||
return
|
return
|
||||||
@@ -489,6 +504,20 @@ func parseV4(data []byte, incoming bool, fp *firewall.Packet) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Interface) decrypt(hostinfo *HostInfo, mc uint64, out []byte, packet []byte, h *header.H, nb []byte) ([]byte, error) {
|
||||||
|
var err error
|
||||||
|
out, err = hostinfo.ConnectionState.dKey.DecryptDanger(out, packet[:header.Len], packet[header.Len:], mc, nb)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hostinfo.ConnectionState.window.Update(f.l, mc) {
|
||||||
|
return nil, ErrOutOfWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Interface) handleOutsideMessagePacket(hostinfo *HostInfo, out []byte, packet []byte, fwPacket *firewall.Packet, nb []byte, q int, localCache firewall.ConntrackCache) {
|
func (f *Interface) handleOutsideMessagePacket(hostinfo *HostInfo, out []byte, packet []byte, fwPacket *firewall.Packet, nb []byte, q int, localCache firewall.ConntrackCache) {
|
||||||
err := newPacket(out, true, fwPacket)
|
err := newPacket(out, true, fwPacket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -513,7 +542,7 @@ func (f *Interface) handleOutsideMessagePacket(hostinfo *HostInfo, out []byte, p
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = f.readers[q].Write(out)
|
_, err = f.queues[q].Write(out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.l.Error("Failed to write to tun", "error", err)
|
f.l.Error("Failed to write to tun", "error", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-3
@@ -4,15 +4,25 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// defaultBatchBufSize is the per-Queue scratch size for Read. 65535 covers
|
||||||
|
// any single IP packet.
|
||||||
|
const defaultBatchBufSize = 65535
|
||||||
|
|
||||||
type Device interface {
|
type Device interface {
|
||||||
io.ReadWriteCloser
|
io.Closer
|
||||||
Activate() error
|
Activate() error
|
||||||
Networks() []netip.Prefix
|
Networks() []netip.Prefix
|
||||||
Name() string
|
Name() string
|
||||||
RoutesFor(netip.Addr) routing.Gateways
|
RoutesFor(netip.Addr) routing.Gateways
|
||||||
SupportsMultiqueue() bool
|
// Queues returns the device's packet queues, opening additional ones as
|
||||||
NewMultiQueueReader() (io.ReadWriteCloser, error)
|
// needed until there are n. Platforms without multiqueue support return
|
||||||
|
// their single queue regardless of n, so callers must size reader loops
|
||||||
|
// to len(result), not n; implementations never return more than n. An
|
||||||
|
// error means a queue that should have opened could not; the caller owns
|
||||||
|
// cleanup via Close. Called once, during interface activation.
|
||||||
|
Queues(n int) ([]tio.Queue, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
package overlaytest
|
package overlaytest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,20 +30,16 @@ func (NoopTun) Name() string {
|
|||||||
return "noop"
|
return "noop"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (NoopTun) Read([]byte) (int, error) {
|
func (NoopTun) Read() ([]tio.Packet, error) {
|
||||||
return 0, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (NoopTun) Write([]byte) (int, error) {
|
func (NoopTun) Write([]byte) (int, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (NoopTun) SupportsMultiqueue() bool {
|
func (NoopTun) Queues(int) ([]tio.Queue, error) {
|
||||||
return false
|
return []tio.Queue{NoopTun{}}, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (NoopTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
|
||||||
return nil, errors.New("unsupported")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (NoopTun) Close() error {
|
func (NoopTun) Close() error {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//go:build linux && !android
|
||||||
|
// +build linux,!android
|
||||||
|
|
||||||
|
package tio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// blockOn parks the calling goroutine until fd is ready (events is POLLIN for
|
||||||
|
// reads, POLLOUT for writes) or shutdownFd signals teardown. It builds the
|
||||||
|
// pollfd array on the stack every call, so concurrent callers on the same
|
||||||
|
// Queue never share Revents storage.
|
||||||
|
//
|
||||||
|
// Returns os.ErrClosed when shutdown was signaled (POLLIN on shutdownFd)
|
||||||
|
// or either fd reported a problem condition (POLLHUP|POLLNVAL|POLLERR).
|
||||||
|
func blockOn(fd, shutdownFd int32, events int16) error {
|
||||||
|
const problemFlags = unix.POLLHUP | unix.POLLNVAL | unix.POLLERR
|
||||||
|
pfds := [2]unix.PollFd{
|
||||||
|
{Fd: fd, Events: events},
|
||||||
|
{Fd: shutdownFd, Events: unix.POLLIN},
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
for {
|
||||||
|
_, err = unix.Poll(pfds[:], -1)
|
||||||
|
if err != unix.EINTR {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tunEvents := pfds[0].Revents
|
||||||
|
shutdownEvents := pfds[1].Revents
|
||||||
|
// Check err before trusting the potentially bogus bits we just got.
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if shutdownEvents&(unix.POLLIN|problemFlags) != 0 {
|
||||||
|
return os.ErrClosed
|
||||||
|
}
|
||||||
|
if tunEvents&problemFlags != 0 {
|
||||||
|
return os.ErrClosed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
//go:build linux && !android
|
||||||
|
// +build linux,!android
|
||||||
|
|
||||||
|
package tio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pollQueueSet struct {
|
||||||
|
pq []*Poll
|
||||||
|
// pqi is exactly the same as pq, but stored as the interface type
|
||||||
|
pqi []Queue
|
||||||
|
shutdownFd int
|
||||||
|
closed atomic.Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPollQueueSet() (QueueSet, error) {
|
||||||
|
shutdownFd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create eventfd: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &pollQueueSet{
|
||||||
|
pq: []*Poll{},
|
||||||
|
pqi: []Queue{},
|
||||||
|
shutdownFd: shutdownFd,
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pollQueueSet) Queues() []Queue {
|
||||||
|
return c.pqi
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pollQueueSet) Add(fd int) error {
|
||||||
|
x, err := newPoll(fd, c.shutdownFd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.pq = append(c.pq, x)
|
||||||
|
c.pqi = append(c.pqi, x)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pollQueueSet) wakeForShutdown() error {
|
||||||
|
var buf [8]byte
|
||||||
|
binary.NativeEndian.PutUint64(buf[:], 1)
|
||||||
|
_, err := unix.Write(int(c.shutdownFd), buf[:])
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pollQueueSet) Close() error {
|
||||||
|
if c.closed.Swap(true) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := []error{}
|
||||||
|
|
||||||
|
// Wake any reader blocked in poll so it observes POLLIN on the shutdown
|
||||||
|
// eventfd and returns os.ErrClosed.
|
||||||
|
if err := c.wakeForShutdown(); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the per-queue tun fds; this also unblocks any in-flight reads.
|
||||||
|
// The per-queue Close deliberately leaves shutdownFd alone - it belongs
|
||||||
|
// to this container.
|
||||||
|
for _, x := range c.pq {
|
||||||
|
if err := x.Close(); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the shutdown eventfd last: every reader's pollfd set references
|
||||||
|
// it, so it must outlive the wake + per-queue teardown above.
|
||||||
|
if err := unix.Close(c.shutdownFd); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
c.shutdownFd = -1
|
||||||
|
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package tio
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
// singleQueue adapts a legacy one-datagram-per-Read source into a Queue.
|
||||||
|
// Read fills a private scratch buffer and returns exactly one Packet whose
|
||||||
|
// Bytes borrow from that buffer, valid only until the next Read, per the
|
||||||
|
// Queue contract. Single-reader like every Queue; Write is exactly as safe
|
||||||
|
// for concurrent use as the underlying source's Write.
|
||||||
|
type singleQueue struct {
|
||||||
|
rw io.ReadWriter
|
||||||
|
closer io.Closer // nil: Close is a no-op (the source is shared and owned elsewhere)
|
||||||
|
buf []byte
|
||||||
|
ret [1]Packet
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSingleQueue wraps a one-datagram-per-Read ReadWriteCloser (a legacy tun
|
||||||
|
// device) into a Queue. bufSize is the per-queue read scratch size and must
|
||||||
|
// be at least the largest datagram the source can return. Close closes rwc.
|
||||||
|
func NewSingleQueue(rwc io.ReadWriteCloser, bufSize int) Queue {
|
||||||
|
return &singleQueue{rw: rwc, closer: rwc, buf: make([]byte, bufSize)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSingleQueueNoClose is NewSingleQueue for a source owned by someone else,
|
||||||
|
// e.g. several queues sharing one device. Close on the returned Queue is a
|
||||||
|
// no-op so one queue can't tear the shared source out from under its
|
||||||
|
// siblings; the owner remains responsible for closing the source itself.
|
||||||
|
func NewSingleQueueNoClose(rw io.ReadWriter, bufSize int) Queue {
|
||||||
|
return &singleQueue{rw: rw, buf: make([]byte, bufSize)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *singleQueue) Read() ([]Packet, error) {
|
||||||
|
n, err := q.rw.Read(q.buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
q.ret[0] = Packet{Bytes: q.buf[:n]}
|
||||||
|
return q.ret[:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *singleQueue) Write(p []byte) (int, error) {
|
||||||
|
return q.rw.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *singleQueue) Close() error {
|
||||||
|
if q.closer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return q.closer.Close()
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package tio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QueueSet holds one or many Queue objects and helps close them in an orderly way.
|
||||||
|
type QueueSet interface {
|
||||||
|
io.Closer
|
||||||
|
Queues() []Queue
|
||||||
|
|
||||||
|
// Add takes a tun fd, adds it to the set, and prepares it for use as a Queue.
|
||||||
|
Add(fd int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue is a readable/writable packet queue. Concurrency contract: a single
|
||||||
|
// read goroutine drives Read; plain Write is safe for concurrent callers.
|
||||||
|
type Queue interface {
|
||||||
|
io.Closer
|
||||||
|
|
||||||
|
// Read returns one or more packets. The returned Packet.Bytes slices
|
||||||
|
// are borrowed from the Queue's internal buffer and are only valid
|
||||||
|
// until the next Read or Close on this Queue - callers must encrypt
|
||||||
|
// or copy each slice before the next call. Single-reader only: not
|
||||||
|
// safe for concurrent Reads (it reuses per-queue rx scratch each call).
|
||||||
|
Read() ([]Packet, error)
|
||||||
|
|
||||||
|
// Write emits a single packet on the plaintext (outside→inside)
|
||||||
|
// delivery path. Safe for concurrent use.
|
||||||
|
Write(p []byte) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packet is the unit Queue.Read returns. Bytes points into the queue's
|
||||||
|
// internal buffer and is only valid until the next Read or Close on the
|
||||||
|
// queue that produced it.
|
||||||
|
type Packet struct {
|
||||||
|
Bytes []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone returns a Packet whose Bytes is a freshly allocated copy of p.Bytes,
|
||||||
|
// safe to retain past the next Read or Close on the originating Queue.
|
||||||
|
// Use this only when a caller genuinely needs to outlive the borrowed-slice
|
||||||
|
// contract — the hot path reads should continue to consume the borrow
|
||||||
|
// synchronously to avoid the allocation.
|
||||||
|
func (p Packet) Clone() Packet {
|
||||||
|
if p.Bytes == nil {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
cp := make([]byte, len(p.Bytes))
|
||||||
|
copy(cp, p.Bytes)
|
||||||
|
return Packet{Bytes: cp}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
//go:build linux && !android
|
||||||
|
// +build linux,!android
|
||||||
|
|
||||||
|
package tio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Maximum size we accept for a single read from a TUN. 65535 covers any
|
||||||
|
// single IP packet.
|
||||||
|
const tunReadBufSize = 65535
|
||||||
|
|
||||||
|
type Poll struct {
|
||||||
|
fd int
|
||||||
|
shutdownFd int
|
||||||
|
closed atomic.Bool
|
||||||
|
|
||||||
|
readBuf []byte
|
||||||
|
batchRet [1]Packet
|
||||||
|
}
|
||||||
|
|
||||||
|
// newPoll wraps an existing tun fd. On failure it does NOT close fd: the
|
||||||
|
// caller owns fd and is the sole closer (see pollQueueSet.Add callers in
|
||||||
|
// overlay/tun_linux.go, which unix.Close on Add error). This keeps closes
|
||||||
|
// at exactly one on every path.
|
||||||
|
func newPoll(fd int, shutdownFd int) (*Poll, error) {
|
||||||
|
if err := unix.SetNonblock(fd, true); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to set Poll device as nonblocking: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &Poll{
|
||||||
|
fd: fd,
|
||||||
|
shutdownFd: shutdownFd,
|
||||||
|
readBuf: make([]byte, tunReadBufSize),
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// blockOnRead waits until the Poll fd is readable or shutdown has been signaled.
|
||||||
|
// Returns os.ErrClosed if Close was called.
|
||||||
|
func (t *Poll) blockOnRead() error {
|
||||||
|
return blockOn(int32(t.fd), int32(t.shutdownFd), unix.POLLIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Poll) blockOnWrite() error {
|
||||||
|
return blockOn(int32(t.fd), int32(t.shutdownFd), unix.POLLOUT)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Poll) Read() ([]Packet, error) {
|
||||||
|
n, err := t.readOne(t.readBuf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t.batchRet[0] = Packet{Bytes: t.readBuf[:n]}
|
||||||
|
return t.batchRet[:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Poll) readOne(to []byte) (int, error) {
|
||||||
|
for {
|
||||||
|
n, errno := unix.Read(t.fd, to)
|
||||||
|
if errno == nil {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
switch errno {
|
||||||
|
case unix.EAGAIN:
|
||||||
|
if err := t.blockOnRead(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
case unix.EINTR:
|
||||||
|
// retry
|
||||||
|
case unix.EBADF:
|
||||||
|
return 0, os.ErrClosed
|
||||||
|
default:
|
||||||
|
return 0, errno
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write is safe for concurrent use
|
||||||
|
func (t *Poll) Write(from []byte) (int, error) {
|
||||||
|
for {
|
||||||
|
n, errno := unix.Write(t.fd, from)
|
||||||
|
if errno == nil {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
switch errno {
|
||||||
|
case unix.EAGAIN:
|
||||||
|
if err := t.blockOnWrite(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
case unix.EINTR:
|
||||||
|
// retry
|
||||||
|
case unix.EBADF:
|
||||||
|
return 0, os.ErrClosed
|
||||||
|
default:
|
||||||
|
return 0, errno
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Poll) Close() error {
|
||||||
|
if t.closed.Swap(true) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
//shutdownFd is owned by the container, so we should not close it
|
||||||
|
// Close the underlying fd but do NOT null t.fd: a reader may still be
|
||||||
|
// loading it in readOne, and mutating the field would race that load.
|
||||||
|
// It gets EBADF -> os.ErrClosed (or wakes via the shutdown eventfd's
|
||||||
|
// ppoll first). closed.Swap already guarantees we only close once.
|
||||||
|
return unix.Close(t.fd)
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
//go:build linux && !android && !e2e_testing
|
||||||
|
// +build linux,!android,!e2e_testing
|
||||||
|
|
||||||
|
package tio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newReadPipe returns a read fd. The matching write fd is registered for cleanup.
|
||||||
|
// The caller takes ownership of the read fd (pass it into a QueueSet).
|
||||||
|
func newReadPipe(t *testing.T) int {
|
||||||
|
t.Helper()
|
||||||
|
var fds [2]int
|
||||||
|
if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil {
|
||||||
|
t.Fatalf("pipe2: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = unix.Close(fds[1]) })
|
||||||
|
return fds[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoll_WakeForShutdown_WakesFriends(t *testing.T) {
|
||||||
|
pipe1 := newReadPipe(t)
|
||||||
|
pipe2 := newReadPipe(t)
|
||||||
|
parent, err := NewPollQueueSet()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, parent.Add(pipe1))
|
||||||
|
require.NoError(t, parent.Add(pipe2))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = unix.Close(pipe1)
|
||||||
|
_ = unix.Close(pipe2)
|
||||||
|
})
|
||||||
|
|
||||||
|
readers := parent.Queues()
|
||||||
|
errs := make([]error, len(readers))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i, r := range readers {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int, r Queue) {
|
||||||
|
defer wg.Done()
|
||||||
|
_, errs[i] = r.Read()
|
||||||
|
}(i, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
if err := parent.Close(); err != nil {
|
||||||
|
t.Fatalf("Close: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() { wg.Wait(); close(done) }()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("readers did not wake")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, err := range errs {
|
||||||
|
if !errors.Is(err, os.ErrClosed) {
|
||||||
|
t.Errorf("reader %d: expected os.ErrClosed, got %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPoll_ConcurrentWrite_NoRace hammers a single Poll queue from two writer
|
||||||
|
// goroutines while a reader drains the other end of the pipe. The writers
|
||||||
|
// overflow the pipe buffer, so both repeatedly park in blockOnWrite at the same
|
||||||
|
// time — the exact scenario that raced on the old shared writePoll member
|
||||||
|
// array. Run under -race; a shared-array regression trips the detector here.
|
||||||
|
func TestPoll_ConcurrentWrite_NoRace(t *testing.T) {
|
||||||
|
var fds [2]int
|
||||||
|
require.NoError(t, unix.Pipe2(fds[:], unix.O_CLOEXEC))
|
||||||
|
readFd, writeFd := fds[0], fds[1]
|
||||||
|
|
||||||
|
shutdownFd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() { _ = unix.Close(shutdownFd) })
|
||||||
|
|
||||||
|
p, err := newPoll(writeFd, shutdownFd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
const writers = 2
|
||||||
|
const perWriter = 4000
|
||||||
|
payload := make([]byte, 100)
|
||||||
|
total := writers * perWriter * len(payload)
|
||||||
|
|
||||||
|
// Reader: drain the read end (blocking) until every writer's bytes are
|
||||||
|
// consumed, so the writers keep making progress rather than wedging on a
|
||||||
|
// permanently full pipe.
|
||||||
|
readDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(readDone)
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
got := 0
|
||||||
|
for got < total {
|
||||||
|
n, rerr := unix.Read(readFd, buf)
|
||||||
|
got += n
|
||||||
|
if rerr != nil {
|
||||||
|
if rerr == unix.EINTR {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n == 0 { // EOF
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for w := 0; w < writers; w++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for i := 0; i < perWriter; i++ {
|
||||||
|
if _, werr := p.Write(payload); werr != nil {
|
||||||
|
t.Errorf("write: %v", werr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-readDone:
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.Fatal("reader did not drain")
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, p.Close())
|
||||||
|
_ = unix.Close(readFd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPoll_NewPoll_DoesNotCloseFdOnFailure pins the ownership rule: when
|
||||||
|
// newPoll fails, it must leave fd open so the caller (pollQueueSet.Add's
|
||||||
|
// callers in tun_linux.go) is the sole closer. If newPoll also closed fd,
|
||||||
|
// the poll path would double-close on Add error. We force the failure with
|
||||||
|
// an O_PATH descriptor: fcntl(F_SETFL) — which SetNonblock performs — is not
|
||||||
|
// permitted on O_PATH fds and fails with EBADF, while the fd itself stays
|
||||||
|
// open so we can observe that newPoll left it alone.
|
||||||
|
func TestPoll_NewPoll_DoesNotCloseFdOnFailure(t *testing.T) {
|
||||||
|
fd, err := unix.Open("/", unix.O_PATH|unix.O_CLOEXEC, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() { _ = unix.Close(fd) })
|
||||||
|
|
||||||
|
p, err := newPoll(fd, 1)
|
||||||
|
require.Error(t, err, "SetNonblock on an O_PATH fd should fail")
|
||||||
|
require.Nil(t, p)
|
||||||
|
|
||||||
|
// If newPoll had closed fd, F_GETFD would report it closed. It staying
|
||||||
|
// open proves newPoll left the fd for the caller to close exactly once.
|
||||||
|
require.True(t, fdOpen(t, fd), "newPoll must not close fd on failure; caller is the sole closer")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoll_Close_Idempotent(t *testing.T) {
|
||||||
|
tf, err := newPoll(newReadPipe(t), 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if err := tf.Close(); err != nil {
|
||||||
|
t.Fatalf("first Close: %v", err)
|
||||||
|
}
|
||||||
|
if err := tf.Close(); err != nil {
|
||||||
|
t.Fatalf("second Close should be a no-op, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fdOpen reports whether fd currently refers to an open file description.
|
||||||
|
// A closed (or never-allocated) fd makes F_GETFD fail with EBADF.
|
||||||
|
func fdOpen(t *testing.T, fd int) bool {
|
||||||
|
t.Helper()
|
||||||
|
_, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
|
||||||
|
if err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if errors.Is(err, unix.EBADF) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t.Fatalf("unexpected fcntl(F_GETFD) error on fd %d: %v", fd, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPollQueueSet_Close_ClosesShutdownFd is the regression test for the
|
||||||
|
// leaked shutdown eventfd: the container that owns shutdownFd must close it in
|
||||||
|
// Close, and a second Close must be a safe no-op.
|
||||||
|
func TestPollQueueSet_Close_ClosesShutdownFd(t *testing.T) {
|
||||||
|
qs, err := NewPollQueueSet()
|
||||||
|
require.NoError(t, err)
|
||||||
|
c, ok := qs.(*pollQueueSet)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.NoError(t, qs.Add(newReadPipe(t)))
|
||||||
|
|
||||||
|
shutdownFd := c.shutdownFd
|
||||||
|
require.True(t, fdOpen(t, shutdownFd), "shutdown eventfd should be open before Close")
|
||||||
|
|
||||||
|
require.NoError(t, qs.Close())
|
||||||
|
require.False(t, fdOpen(t, shutdownFd), "shutdown eventfd should be closed after Close")
|
||||||
|
|
||||||
|
// Second Close must not touch fds (shutdownFd is now -1) and must return nil.
|
||||||
|
require.NoError(t, qs.Close())
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gaissmai/bart"
|
"github.com/gaissmai/bart"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
"github.com/slackhq/nebula/util"
|
"github.com/slackhq/nebula/util"
|
||||||
)
|
)
|
||||||
@@ -63,7 +64,7 @@ func (t *tun) RoutesFor(ip netip.Addr) routing.Gateways {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t tun) Activate() error {
|
func (t *tun) Activate() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,10 +97,6 @@ func (t *tun) Name() string {
|
|||||||
return "android"
|
return "android"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tun) SupportsMultiqueue() bool {
|
func (t *tun) Queues(int) ([]tio.Queue, error) {
|
||||||
return false
|
return []tio.Queue{tio.NewSingleQueue(t, defaultBatchBufSize)}, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
|
||||||
return nil, fmt.Errorf("TODO: multiqueue not implemented for android")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ package overlay
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
@@ -16,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gaissmai/bart"
|
"github.com/gaissmai/bart"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
"github.com/slackhq/nebula/util"
|
"github.com/slackhq/nebula/util"
|
||||||
netroute "golang.org/x/net/route"
|
netroute "golang.org/x/net/route"
|
||||||
@@ -606,10 +606,6 @@ func (t *tun) Name() string {
|
|||||||
return t.Device
|
return t.Device
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tun) SupportsMultiqueue() bool {
|
func (t *tun) Queues(int) ([]tio.Queue, error) {
|
||||||
return false
|
return []tio.Queue{tio.NewSingleQueue(t, defaultBatchBufSize)}, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
|
||||||
return nil, fmt.Errorf("TODO: multiqueue not implemented for darwin")
|
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-24
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/rcrowley/go-metrics"
|
"github.com/rcrowley/go-metrics"
|
||||||
"github.com/slackhq/nebula/iputil"
|
"github.com/slackhq/nebula/iputil"
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,6 +24,23 @@ type disabledTun struct {
|
|||||||
l *slog.Logger
|
l *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read hands the next queued packet to a reader, copying it into b. Reads
|
||||||
|
// from concurrent queues are safe: the channel receive serializes them and
|
||||||
|
// each queue copies into its own private scratch buffer.
|
||||||
|
func (t *disabledTun) Read(b []byte) (int, error) {
|
||||||
|
r, ok := <-t.read
|
||||||
|
if !ok {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
t.tx.Inc(1)
|
||||||
|
if t.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||||
|
t.l.Debug("Write payload", "raw", prettyPacket(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy(b, r), nil
|
||||||
|
}
|
||||||
|
|
||||||
func newDisabledTun(vpnNetworks []netip.Prefix, queueLen int, metricsEnabled bool, l *slog.Logger) *disabledTun {
|
func newDisabledTun(vpnNetworks []netip.Prefix, queueLen int, metricsEnabled bool, l *slog.Logger) *disabledTun {
|
||||||
tun := &disabledTun{
|
tun := &disabledTun{
|
||||||
vpnNetworks: vpnNetworks,
|
vpnNetworks: vpnNetworks,
|
||||||
@@ -57,24 +75,6 @@ func (*disabledTun) Name() string {
|
|||||||
return "disabled"
|
return "disabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *disabledTun) Read(b []byte) (int, error) {
|
|
||||||
r, ok := <-t.read
|
|
||||||
if !ok {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r) > len(b) {
|
|
||||||
return 0, fmt.Errorf("packet larger than mtu: %d > %d bytes", len(r), len(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
t.tx.Inc(1)
|
|
||||||
if t.l.Enabled(context.Background(), slog.LevelDebug) {
|
|
||||||
t.l.Debug("Write payload", "raw", prettyPacket(r))
|
|
||||||
}
|
|
||||||
|
|
||||||
return copy(b, r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *disabledTun) handleICMPEchoRequest(b []byte) bool {
|
func (t *disabledTun) handleICMPEchoRequest(b []byte) bool {
|
||||||
out := make([]byte, len(b))
|
out := make([]byte, len(b))
|
||||||
out = iputil.CreateICMPEchoResponse(b, out)
|
out = iputil.CreateICMPEchoResponse(b, out)
|
||||||
@@ -106,12 +106,14 @@ func (t *disabledTun) Write(b []byte) (int, error) {
|
|||||||
return len(b), nil
|
return len(b), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *disabledTun) SupportsMultiqueue() bool {
|
func (t *disabledTun) Queues(n int) ([]tio.Queue, error) {
|
||||||
return true
|
out := make([]tio.Queue, n)
|
||||||
}
|
for i := range out {
|
||||||
|
// NoClose: the shared channel and metrics are owned by the
|
||||||
func (t *disabledTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
// disabledTun; Close on the device tears them down once for everybody.
|
||||||
return t, nil
|
out[i] = tio.NewSingleQueueNoClose(t, defaultBatchBufSize)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *disabledTun) Close() error {
|
func (t *disabledTun) Close() error {
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
//go:build linux && !android && !e2e_testing
|
|
||||||
// +build linux,!android,!e2e_testing
|
|
||||||
|
|
||||||
package overlay
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
// newReadPipe returns a read fd. The matching write fd is registered for cleanup.
|
|
||||||
// The caller takes ownership of the read fd (pass it to newTunFd / newFriend).
|
|
||||||
func newReadPipe(t *testing.T) int {
|
|
||||||
t.Helper()
|
|
||||||
var fds [2]int
|
|
||||||
if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil {
|
|
||||||
t.Fatalf("pipe2: %v", err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { _ = unix.Close(fds[1]) })
|
|
||||||
return fds[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTunFile_WakeForShutdown_UnblocksRead(t *testing.T) {
|
|
||||||
tf, err := newTunFd(newReadPipe(t))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("newTunFd: %v", err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { _ = tf.Close() })
|
|
||||||
|
|
||||||
done := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
_, err := tf.Read(make([]byte, 64))
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Verify Read is actually blocked in poll.
|
|
||||||
select {
|
|
||||||
case err := <-done:
|
|
||||||
t.Fatalf("Read returned before shutdown signal: %v", err)
|
|
||||||
case <-time.After(50 * time.Millisecond):
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tf.wakeForShutdown(); err != nil {
|
|
||||||
t.Fatalf("wakeForShutdown: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err := <-done:
|
|
||||||
if !errors.Is(err, os.ErrClosed) {
|
|
||||||
t.Fatalf("expected os.ErrClosed, got %v", err)
|
|
||||||
}
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
t.Fatal("Read did not wake on shutdown")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTunFile_WakeForShutdown_WakesFriends(t *testing.T) {
|
|
||||||
parent, err := newTunFd(newReadPipe(t))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("newTunFd: %v", err)
|
|
||||||
}
|
|
||||||
friend, err := parent.newFriend(newReadPipe(t))
|
|
||||||
if err != nil {
|
|
||||||
_ = parent.Close()
|
|
||||||
t.Fatalf("newFriend: %v", err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() {
|
|
||||||
_ = friend.Close()
|
|
||||||
_ = parent.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
readers := []*tunFile{parent, friend}
|
|
||||||
errs := make([]error, len(readers))
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for i, r := range readers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(i int, r *tunFile) {
|
|
||||||
defer wg.Done()
|
|
||||||
_, errs[i] = r.Read(make([]byte, 64))
|
|
||||||
}(i, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
if err := parent.wakeForShutdown(); err != nil {
|
|
||||||
t.Fatalf("wakeForShutdown: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() { wg.Wait(); close(done) }()
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
t.Fatal("readers did not wake")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, err := range errs {
|
|
||||||
if !errors.Is(err, os.ErrClosed) {
|
|
||||||
t.Errorf("reader %d: expected os.ErrClosed, got %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTunFile_Close_Idempotent(t *testing.T) {
|
|
||||||
tf, err := newTunFd(newReadPipe(t))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("newTunFd: %v", err)
|
|
||||||
}
|
|
||||||
if err := tf.Close(); err != nil {
|
|
||||||
t.Fatalf("first Close: %v", err)
|
|
||||||
}
|
|
||||||
if err := tf.Close(); err != nil {
|
|
||||||
t.Fatalf("second Close should be a no-op, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -20,7 +19,7 @@ import (
|
|||||||
"github.com/gaissmai/bart"
|
"github.com/gaissmai/bart"
|
||||||
|
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
"github.com/slackhq/nebula/util"
|
"github.com/slackhq/nebula/util"
|
||||||
netroute "golang.org/x/net/route"
|
netroute "golang.org/x/net/route"
|
||||||
@@ -561,12 +560,8 @@ func (t *tun) Name() string {
|
|||||||
return t.Device
|
return t.Device
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tun) SupportsMultiqueue() bool {
|
func (t *tun) Queues(int) ([]tio.Queue, error) {
|
||||||
return false
|
return []tio.Queue{tio.NewSingleQueue(t, defaultBatchBufSize)}, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
|
||||||
return nil, fmt.Errorf("TODO: multiqueue not implemented for freebsd")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tun) addRoutes(logErrors bool) error {
|
func (t *tun) addRoutes(logErrors bool) error {
|
||||||
|
|||||||
+3
-6
@@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gaissmai/bart"
|
"github.com/gaissmai/bart"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
"github.com/slackhq/nebula/util"
|
"github.com/slackhq/nebula/util"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
@@ -159,10 +160,6 @@ func (t *tun) Name() string {
|
|||||||
return "iOS"
|
return "iOS"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tun) SupportsMultiqueue() bool {
|
func (t *tun) Queues(int) ([]tio.Queue, error) {
|
||||||
return false
|
return []tio.Queue{tio.NewSingleQueue(t, defaultBatchBufSize)}, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
|
||||||
return nil, fmt.Errorf("TODO: multiqueue not implemented for ios")
|
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-240
@@ -4,9 +4,7 @@
|
|||||||
package overlay
|
package overlay
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -19,180 +17,15 @@ import (
|
|||||||
|
|
||||||
"github.com/gaissmai/bart"
|
"github.com/gaissmai/bart"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
"github.com/slackhq/nebula/util"
|
"github.com/slackhq/nebula/util"
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// tunFile wraps a TUN file descriptor with poll-based reads. The FD provided will be changed to non-blocking.
|
|
||||||
// A shared eventfd allows Close to wake all readers blocked in poll.
|
|
||||||
type tunFile struct {
|
|
||||||
fd int
|
|
||||||
shutdownFd int
|
|
||||||
lastOne bool
|
|
||||||
readPoll [2]unix.PollFd
|
|
||||||
writePoll [2]unix.PollFd
|
|
||||||
closed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// newFriend makes a tunFile for a MultiQueueReader that copies the shutdown eventfd from the parent tun
|
|
||||||
func (r *tunFile) newFriend(fd int) (*tunFile, error) {
|
|
||||||
if err := unix.SetNonblock(fd, true); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to set tun fd non-blocking: %w", err)
|
|
||||||
}
|
|
||||||
return &tunFile{
|
|
||||||
fd: fd,
|
|
||||||
shutdownFd: r.shutdownFd,
|
|
||||||
readPoll: [2]unix.PollFd{
|
|
||||||
{Fd: int32(fd), Events: unix.POLLIN},
|
|
||||||
{Fd: int32(r.shutdownFd), Events: unix.POLLIN},
|
|
||||||
},
|
|
||||||
writePoll: [2]unix.PollFd{
|
|
||||||
{Fd: int32(fd), Events: unix.POLLOUT},
|
|
||||||
{Fd: int32(r.shutdownFd), Events: unix.POLLIN},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTunFd(fd int) (*tunFile, error) {
|
|
||||||
if err := unix.SetNonblock(fd, true); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to set tun fd non-blocking: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdownFd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create eventfd: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
out := &tunFile{
|
|
||||||
fd: fd,
|
|
||||||
shutdownFd: shutdownFd,
|
|
||||||
lastOne: true,
|
|
||||||
readPoll: [2]unix.PollFd{
|
|
||||||
{Fd: int32(fd), Events: unix.POLLIN},
|
|
||||||
{Fd: int32(shutdownFd), Events: unix.POLLIN},
|
|
||||||
},
|
|
||||||
writePoll: [2]unix.PollFd{
|
|
||||||
{Fd: int32(fd), Events: unix.POLLOUT},
|
|
||||||
{Fd: int32(shutdownFd), Events: unix.POLLIN},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *tunFile) blockOnRead() error {
|
|
||||||
const problemFlags = unix.POLLHUP | unix.POLLNVAL | unix.POLLERR
|
|
||||||
var err error
|
|
||||||
for {
|
|
||||||
_, err = unix.Poll(r.readPoll[:], -1)
|
|
||||||
if err != unix.EINTR {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//always reset these!
|
|
||||||
tunEvents := r.readPoll[0].Revents
|
|
||||||
shutdownEvents := r.readPoll[1].Revents
|
|
||||||
r.readPoll[0].Revents = 0
|
|
||||||
r.readPoll[1].Revents = 0
|
|
||||||
//do the err check before trusting the potentially bogus bits we just got
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if shutdownEvents&(unix.POLLIN|problemFlags) != 0 {
|
|
||||||
return os.ErrClosed
|
|
||||||
} else if tunEvents&problemFlags != 0 {
|
|
||||||
return os.ErrClosed
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *tunFile) blockOnWrite() error {
|
|
||||||
const problemFlags = unix.POLLHUP | unix.POLLNVAL | unix.POLLERR
|
|
||||||
var err error
|
|
||||||
for {
|
|
||||||
_, err = unix.Poll(r.writePoll[:], -1)
|
|
||||||
if err != unix.EINTR {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//always reset these!
|
|
||||||
tunEvents := r.writePoll[0].Revents
|
|
||||||
shutdownEvents := r.writePoll[1].Revents
|
|
||||||
r.writePoll[0].Revents = 0
|
|
||||||
r.writePoll[1].Revents = 0
|
|
||||||
//do the err check before trusting the potentially bogus bits we just got
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if shutdownEvents&(unix.POLLIN|problemFlags) != 0 {
|
|
||||||
return os.ErrClosed
|
|
||||||
} else if tunEvents&problemFlags != 0 {
|
|
||||||
return os.ErrClosed
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *tunFile) Read(buf []byte) (int, error) {
|
|
||||||
for {
|
|
||||||
if n, err := unix.Read(r.fd, buf); err == nil {
|
|
||||||
return n, nil
|
|
||||||
} else if err == unix.EAGAIN {
|
|
||||||
if err = r.blockOnRead(); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
} else if err == unix.EINTR {
|
|
||||||
continue
|
|
||||||
} else if err == unix.EBADF {
|
|
||||||
return 0, os.ErrClosed
|
|
||||||
} else {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *tunFile) Write(buf []byte) (int, error) {
|
|
||||||
for {
|
|
||||||
if n, err := unix.Write(r.fd, buf); err == nil {
|
|
||||||
return n, nil
|
|
||||||
} else if err == unix.EAGAIN {
|
|
||||||
if err = r.blockOnWrite(); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
} else if err == unix.EINTR {
|
|
||||||
continue
|
|
||||||
} else if err == unix.EBADF {
|
|
||||||
return 0, os.ErrClosed
|
|
||||||
} else {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *tunFile) wakeForShutdown() error {
|
|
||||||
var buf [8]byte
|
|
||||||
binary.NativeEndian.PutUint64(buf[:], 1)
|
|
||||||
_, err := unix.Write(int(r.readPoll[1].Fd), buf[:])
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *tunFile) Close() error {
|
|
||||||
if r.closed { // avoid closing more than once. Technically a fd could get re-used, which would be a problem
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
r.closed = true
|
|
||||||
if r.lastOne {
|
|
||||||
_ = unix.Close(r.shutdownFd)
|
|
||||||
}
|
|
||||||
return unix.Close(r.fd)
|
|
||||||
}
|
|
||||||
|
|
||||||
type tun struct {
|
type tun struct {
|
||||||
*tunFile
|
readers tio.QueueSet
|
||||||
readers []*tunFile
|
|
||||||
closeLock sync.Mutex
|
closeLock sync.Mutex
|
||||||
Device string
|
Device string
|
||||||
vpnNetworks []netip.Prefix
|
vpnNetworks []netip.Prefix
|
||||||
@@ -249,44 +82,57 @@ func newTunFromFd(c *config.C, l *slog.Logger, deviceFd int, vpnNetworks []netip
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, multiqueue bool) (*tun, error) {
|
// openTunDev opens /dev/net/tun, creating the device node first if it's
|
||||||
|
// missing (docker containers occasionally omit it).
|
||||||
|
func openTunDev() (int, error) {
|
||||||
fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0)
|
fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
// If /dev/net/tun doesn't exist, try to create it (will happen in docker)
|
return fd, nil
|
||||||
if os.IsNotExist(err) {
|
|
||||||
err = os.MkdirAll("/dev/net", 0755)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("/dev/net/tun doesn't exist, failed to mkdir -p /dev/net: %w", err)
|
|
||||||
}
|
|
||||||
err = unix.Mknod("/dev/net/tun", unix.S_IFCHR|0600, int(unix.Mkdev(10, 200)))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create /dev/net/tun: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fd, err = unix.Open("/dev/net/tun", os.O_RDWR, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("created /dev/net/tun, but still failed: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
if err = os.MkdirAll("/dev/net", 0755); err != nil {
|
||||||
|
return -1, fmt.Errorf("/dev/net/tun doesn't exist, failed to mkdir -p /dev/net: %w", err)
|
||||||
|
}
|
||||||
|
if err = unix.Mknod("/dev/net/tun", unix.S_IFCHR|0600, int(unix.Mkdev(10, 200))); err != nil {
|
||||||
|
return -1, fmt.Errorf("failed to create /dev/net/tun: %w", err)
|
||||||
|
}
|
||||||
|
fd, err = unix.Open("/dev/net/tun", os.O_RDWR, 0)
|
||||||
|
if err != nil {
|
||||||
|
return -1, fmt.Errorf("created /dev/net/tun, but still failed: %w", err)
|
||||||
|
}
|
||||||
|
return fd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// tunSetIff runs TUNSETIFF with the given flags and returns the kernel-chosen
|
||||||
|
// device name on success.
|
||||||
|
func tunSetIff(fd int, name string, flags uint16) (string, error) {
|
||||||
var req ifReq
|
var req ifReq
|
||||||
req.Flags = uint16(unix.IFF_TUN | unix.IFF_NO_PI)
|
req.Flags = flags
|
||||||
|
copy(req.Name[:], name)
|
||||||
|
if err := ioctl(uintptr(fd), uintptr(unix.TUNSETIFF), uintptr(unsafe.Pointer(&req))); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.Trim(string(req.Name[:]), "\x00"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, multiqueue bool) (*tun, error) {
|
||||||
|
baseFlags := uint16(unix.IFF_TUN | unix.IFF_NO_PI)
|
||||||
if multiqueue {
|
if multiqueue {
|
||||||
req.Flags |= unix.IFF_MULTI_QUEUE
|
baseFlags |= unix.IFF_MULTI_QUEUE
|
||||||
}
|
}
|
||||||
nameStr := c.GetString("tun.dev", "")
|
nameStr := c.GetString("tun.dev", "")
|
||||||
copy(req.Name[:], nameStr)
|
|
||||||
if err = ioctl(uintptr(fd), uintptr(unix.TUNSETIFF), uintptr(unsafe.Pointer(&req))); err != nil {
|
fd, err := openTunDev()
|
||||||
_ = unix.Close(fd)
|
if err != nil {
|
||||||
return nil, &NameError{
|
return nil, err
|
||||||
Name: nameStr,
|
}
|
||||||
Underlying: err,
|
name, err := tunSetIff(fd, nameStr, baseFlags)
|
||||||
}
|
if err != nil {
|
||||||
|
_ = unix.Close(fd)
|
||||||
|
return nil, &NameError{Name: nameStr, Underlying: err}
|
||||||
}
|
}
|
||||||
name := strings.Trim(string(req.Name[:]), "\x00")
|
|
||||||
|
|
||||||
t, err := newTunGeneric(c, l, fd, vpnNetworks)
|
t, err := newTunGeneric(c, l, fd, vpnNetworks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -298,16 +144,22 @@ func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, multiqueue
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newTunGeneric does all the stuff common to different tun initialization paths. It will close your files on error.
|
// 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) {
|
func newTunGeneric(c *config.C, l *slog.Logger, fd int, vpnNetworks []netip.Prefix) (*tun, error) {
|
||||||
tfd, err := newTunFd(fd)
|
qs, err := tio.NewPollQueueSet()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = unix.Close(fd)
|
_ = unix.Close(fd)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
err = qs.Add(fd)
|
||||||
|
if err != nil {
|
||||||
|
_ = unix.Close(fd)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
t := &tun{
|
t := &tun{
|
||||||
tunFile: tfd,
|
readers: qs,
|
||||||
readers: []*tunFile{tfd},
|
|
||||||
closeLock: sync.Mutex{},
|
closeLock: sync.Mutex{},
|
||||||
vpnNetworks: vpnNetworks,
|
vpnNetworks: vpnNetworks,
|
||||||
TXQueueLen: c.GetInt("tun.tx_queue", 500),
|
TXQueueLen: c.GetInt("tun.tx_queue", 500),
|
||||||
@@ -406,36 +258,41 @@ func (t *tun) reload(c *config.C, initial bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tun) SupportsMultiqueue() bool {
|
// Queues opens additional kernel multiqueue fds until the device has n
|
||||||
return true
|
// queues, then returns them all. The first queue was opened by newTun.
|
||||||
|
func (t *tun) Queues(n int) ([]tio.Queue, error) {
|
||||||
|
for len(t.readers.Queues()) < n {
|
||||||
|
if err := t.addQueue(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t.readers.Queues(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
// addQueue opens one more IFF_MULTI_QUEUE fd on the device and adds it to
|
||||||
|
// the queue set.
|
||||||
|
func (t *tun) addQueue() error {
|
||||||
t.closeLock.Lock()
|
t.closeLock.Lock()
|
||||||
defer t.closeLock.Unlock()
|
defer t.closeLock.Unlock()
|
||||||
|
|
||||||
fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0)
|
fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var req ifReq
|
flags := uint16(unix.IFF_TUN | unix.IFF_NO_PI | unix.IFF_MULTI_QUEUE)
|
||||||
req.Flags = uint16(unix.IFF_TUN | unix.IFF_NO_PI | unix.IFF_MULTI_QUEUE)
|
if _, err = tunSetIff(fd, t.Device, flags); err != nil {
|
||||||
copy(req.Name[:], t.Device)
|
|
||||||
if err = ioctl(uintptr(fd), uintptr(unix.TUNSETIFF), uintptr(unsafe.Pointer(&req))); err != nil {
|
|
||||||
_ = unix.Close(fd)
|
_ = unix.Close(fd)
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := t.tunFile.newFriend(fd)
|
err = t.readers.Add(fd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = unix.Close(fd)
|
_ = unix.Close(fd)
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
t.readers = append(t.readers, out)
|
return nil
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tun) RoutesFor(ip netip.Addr) routing.Gateways {
|
func (t *tun) RoutesFor(ip netip.Addr) routing.Gateways {
|
||||||
@@ -878,32 +735,10 @@ func (t *tun) Close() error {
|
|||||||
t.routeChan = nil
|
t.routeChan = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signal all readers blocked in poll to wake up and exit
|
|
||||||
_ = t.tunFile.wakeForShutdown()
|
|
||||||
|
|
||||||
if t.ioctlFd > 0 {
|
if t.ioctlFd > 0 {
|
||||||
_ = unix.Close(int(t.ioctlFd))
|
_ = unix.Close(int(t.ioctlFd))
|
||||||
t.ioctlFd = 0
|
t.ioctlFd = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range t.readers {
|
return t.readers.Close()
|
||||||
if i == 0 {
|
|
||||||
continue //we want to close the zeroth reader last
|
|
||||||
}
|
|
||||||
err := t.readers[i].Close()
|
|
||||||
if err != nil {
|
|
||||||
t.l.Error("error closing tun reader", "reader", i, "error", err)
|
|
||||||
} else {
|
|
||||||
t.l.Info("closed tun reader", "reader", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//this is t.readers[0] too
|
|
||||||
err := t.tunFile.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.l.Error("error closing tun reader", "reader", 0, "error", err)
|
|
||||||
} else {
|
|
||||||
t.l.Info("closed tun reader", "reader", 0)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ package overlay
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
@@ -17,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gaissmai/bart"
|
"github.com/gaissmai/bart"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
"github.com/slackhq/nebula/util"
|
"github.com/slackhq/nebula/util"
|
||||||
netroute "golang.org/x/net/route"
|
netroute "golang.org/x/net/route"
|
||||||
@@ -390,12 +390,8 @@ func (t *tun) Name() string {
|
|||||||
return t.Device
|
return t.Device
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tun) SupportsMultiqueue() bool {
|
func (t *tun) Queues(int) ([]tio.Queue, error) {
|
||||||
return false
|
return []tio.Queue{tio.NewSingleQueue(t, defaultBatchBufSize)}, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
|
||||||
return nil, fmt.Errorf("TODO: multiqueue not implemented for netbsd")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tun) addRoutes(logErrors bool) error {
|
func (t *tun) addRoutes(logErrors bool) error {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ package overlay
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
@@ -17,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gaissmai/bart"
|
"github.com/gaissmai/bart"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
"github.com/slackhq/nebula/util"
|
"github.com/slackhq/nebula/util"
|
||||||
netroute "golang.org/x/net/route"
|
netroute "golang.org/x/net/route"
|
||||||
@@ -138,8 +138,8 @@ func tunWritev(fd int, iovecs []unix.Iovec) (n int, err error)
|
|||||||
//go:noescape
|
//go:noescape
|
||||||
func tunReadv(fd int, iovecs []unix.Iovec) (n int, err error)
|
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
|
// Read pulls one IP packet off the tun device, scattering the 4 byte protocol header away from
|
||||||
// packet so the payload lands directly in to.
|
// the packet so the payload lands directly in to.
|
||||||
func (t *tun) Read(to []byte) (int, error) {
|
func (t *tun) Read(to []byte) (int, error) {
|
||||||
var head [4]byte
|
var head [4]byte
|
||||||
|
|
||||||
@@ -369,12 +369,8 @@ func (t *tun) Name() string {
|
|||||||
return t.Device
|
return t.Device
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tun) SupportsMultiqueue() bool {
|
func (t *tun) Queues(int) ([]tio.Queue, error) {
|
||||||
return false
|
return []tio.Queue{tio.NewSingleQueue(t, defaultBatchBufSize)}, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
|
||||||
return nil, fmt.Errorf("TODO: multiqueue not implemented for openbsd")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tun) addRoutes(logErrors bool) error {
|
func (t *tun) addRoutes(logErrors bool) error {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gaissmai/bart"
|
"github.com/gaissmai/bart"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
"github.com/slackhq/nebula/udp"
|
"github.com/slackhq/nebula/udp"
|
||||||
)
|
)
|
||||||
@@ -177,10 +178,6 @@ func (t *TestTun) Read(b []byte) (int, error) {
|
|||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TestTun) SupportsMultiqueue() bool {
|
func (t *TestTun) Queues(int) ([]tio.Queue, error) {
|
||||||
return false
|
return []tio.Queue{tio.NewSingleQueue(t, udp.MTU)}, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TestTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
|
||||||
return nil, fmt.Errorf("TODO: multiqueue not implemented")
|
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-11
@@ -6,7 +6,6 @@ package overlay
|
|||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
@@ -18,6 +17,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gaissmai/bart"
|
"github.com/gaissmai/bart"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
"github.com/slackhq/nebula/util"
|
"github.com/slackhq/nebula/util"
|
||||||
"github.com/slackhq/nebula/wintun"
|
"github.com/slackhq/nebula/wintun"
|
||||||
@@ -47,6 +47,10 @@ type winTun struct {
|
|||||||
tun *wintun.NativeTun
|
tun *wintun.NativeTun
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *winTun) Read(b []byte) (int, error) {
|
||||||
|
return t.tun.Read(b, 0)
|
||||||
|
}
|
||||||
|
|
||||||
func newTunFromFd(_ *config.C, _ *slog.Logger, _ int, _ []netip.Prefix) (Device, error) {
|
func newTunFromFd(_ *config.C, _ *slog.Logger, _ int, _ []netip.Prefix) (Device, error) {
|
||||||
return nil, fmt.Errorf("newTunFromFd not supported in Windows")
|
return nil, fmt.Errorf("newTunFromFd not supported in Windows")
|
||||||
}
|
}
|
||||||
@@ -255,20 +259,12 @@ func (t *winTun) Name() string {
|
|||||||
return t.Device
|
return t.Device
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *winTun) Read(b []byte) (int, error) {
|
|
||||||
return t.tun.Read(b, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *winTun) Write(b []byte) (int, error) {
|
func (t *winTun) Write(b []byte) (int, error) {
|
||||||
return t.tun.Write(b, 0)
|
return t.tun.Write(b, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *winTun) SupportsMultiqueue() bool {
|
func (t *winTun) Queues(int) ([]tio.Queue, error) {
|
||||||
return false
|
return []tio.Queue{tio.NewSingleQueue(t, defaultBatchBufSize)}, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (t *winTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
|
||||||
return nil, fmt.Errorf("TODO: multiqueue not implemented for windows")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *winTun) Close() error {
|
func (t *winTun) Close() error {
|
||||||
|
|||||||
+13
-6
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,12 +47,16 @@ func (d *UserDevice) RoutesFor(ip netip.Addr) routing.Gateways {
|
|||||||
return routing.Gateways{routing.NewGateway(ip, 1)}
|
return routing.Gateways{routing.NewGateway(ip, 1)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *UserDevice) SupportsMultiqueue() bool {
|
func (d *UserDevice) Queues(n int) ([]tio.Queue, error) {
|
||||||
return true
|
out := make([]tio.Queue, n)
|
||||||
}
|
for i := range out {
|
||||||
|
// All queues share the underlying pipes (the io.Pipe serializes
|
||||||
func (d *UserDevice) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
// concurrent callers) but each owns a private scratch buffer so
|
||||||
return d, nil
|
// concurrent Reads across queues never alias. NoClose: the pipes are
|
||||||
|
// owned by the UserDevice and torn down once by UserDevice.Close.
|
||||||
|
out[i] = tio.NewSingleQueueNoClose(d, defaultBatchBufSize)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *UserDevice) Pipe() (*io.PipeReader, *io.PipeWriter) {
|
func (d *UserDevice) Pipe() (*io.PipeReader, *io.PipeWriter) {
|
||||||
@@ -61,9 +66,11 @@ func (d *UserDevice) Pipe() (*io.PipeReader, *io.PipeWriter) {
|
|||||||
func (d *UserDevice) Read(p []byte) (n int, err error) {
|
func (d *UserDevice) Read(p []byte) (n int, err error) {
|
||||||
return d.outboundReader.Read(p)
|
return d.outboundReader.Read(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *UserDevice) Write(p []byte) (n int, err error) {
|
func (d *UserDevice) Write(p []byte) (n int, err error) {
|
||||||
return d.inboundWriter.Write(p)
|
return d.inboundWriter.Write(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *UserDevice) Close() error {
|
func (d *UserDevice) Close() error {
|
||||||
d.inboundWriter.Close()
|
d.inboundWriter.Close()
|
||||||
d.outboundWriter.Close()
|
d.outboundWriter.Close()
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package overlay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/overlay/tio"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newTestUserDevice returns the concrete *UserDevice so tests can reach Pipe()
|
||||||
|
// and the internal queue plumbing.
|
||||||
|
func newTestUserDevice(t *testing.T) *UserDevice {
|
||||||
|
t.Helper()
|
||||||
|
dev, err := NewUserDevice([]netip.Prefix{netip.MustParsePrefix("10.0.0.1/24")})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewUserDevice: %v", err)
|
||||||
|
}
|
||||||
|
ud, ok := dev.(*UserDevice)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("NewUserDevice returned %T, want *UserDevice", dev)
|
||||||
|
}
|
||||||
|
return ud
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUserDeviceReadersDistinctBuffers ensures each Queue is actually different
|
||||||
|
func TestUserDeviceReadersDistinctBuffers(t *testing.T) {
|
||||||
|
d := newTestUserDevice(t)
|
||||||
|
|
||||||
|
readers, err := d.Queues(2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Queues: %v", err)
|
||||||
|
}
|
||||||
|
if len(readers) != 2 {
|
||||||
|
t.Fatalf("Queues(2) returned %d queues, want 2", len(readers))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distinct queue objects.
|
||||||
|
if readers[0] == readers[1] {
|
||||||
|
t.Fatal("Queues(2) returned the same queue object twice")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drive one packet through each queue and confirm the borrowed bytes from
|
||||||
|
// the first read are NOT clobbered by the second read. With a shared
|
||||||
|
// buffer, reading pkt1 into q1 would corrupt q0's still-borrowed slice.
|
||||||
|
_, ow := d.Pipe()
|
||||||
|
|
||||||
|
pkt0 := []byte("packet-zero-aaaaaaaa")
|
||||||
|
pkt1 := []byte("packet-one-bbbbbbbbb")
|
||||||
|
|
||||||
|
// The pipe is unbuffered, so writes block until a reader consumes them.
|
||||||
|
// Serialize: write pkt0 (read on q0), then write pkt1 (read on q1).
|
||||||
|
go func() {
|
||||||
|
if _, err := ow.Write(pkt0); err != nil {
|
||||||
|
t.Errorf("write pkt0: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := ow.Write(pkt1); err != nil {
|
||||||
|
t.Errorf("write pkt1: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
got0, err := readers[0].Read()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("q0.Read: %v", err)
|
||||||
|
}
|
||||||
|
if len(got0) != 1 || string(got0[0].Bytes) != string(pkt0) {
|
||||||
|
t.Fatalf("q0 first read = %q, want %q", firstBytes(got0), pkt0)
|
||||||
|
}
|
||||||
|
// Hold onto q0's borrowed slice across q1's read.
|
||||||
|
borrowed := got0[0].Bytes
|
||||||
|
|
||||||
|
got1, err := readers[1].Read()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("q1.Read: %v", err)
|
||||||
|
}
|
||||||
|
if len(got1) != 1 || string(got1[0].Bytes) != string(pkt1) {
|
||||||
|
t.Fatalf("q1 read = %q, want %q", firstBytes(got1), pkt1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// q0's borrowed bytes must still hold pkt0 - a shared buffer would now
|
||||||
|
// show pkt1's contents.
|
||||||
|
if string(borrowed) != string(pkt0) {
|
||||||
|
t.Fatalf("q0 borrowed bytes were clobbered by q1's read: got %q, want %q", borrowed, pkt0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUserDeviceReadersConcurrentRace exercises two queues reading distinct
|
||||||
|
// packets concurrently. Run it under `go test -race`: with the old
|
||||||
|
// shared-buffer implementation the concurrent Reads raced on readBuf/batchRet
|
||||||
|
// and corrupted each other's returned slices.
|
||||||
|
func TestUserDeviceReadersConcurrentRace(t *testing.T) {
|
||||||
|
d := newTestUserDevice(t)
|
||||||
|
readers, err := d.Queues(2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Queues: %v", err)
|
||||||
|
}
|
||||||
|
_, ow := d.Pipe()
|
||||||
|
|
||||||
|
const iterations = 200
|
||||||
|
|
||||||
|
errs := make(chan error, 3)
|
||||||
|
|
||||||
|
// Each reader parks in Read on the shared outboundReader; io.Pipe hands
|
||||||
|
// each write to whichever reader is currently waiting. We only care that
|
||||||
|
// concurrent Reads into distinct buffers are race-free, so any parked
|
||||||
|
// reader may serve any write.
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
run := func(idx int) {
|
||||||
|
defer wg.Done()
|
||||||
|
for i := 0; i < iterations; i++ {
|
||||||
|
pkts, err := readers[idx].Read()
|
||||||
|
if err != nil {
|
||||||
|
errs <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(pkts) != 1 {
|
||||||
|
errs <- fmt.Errorf("reader %d: got %d packets, want 1", idx, len(pkts))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Touch every byte of the borrowed slice while the other reader
|
||||||
|
// may be mid-Read; a shared buffer would race here.
|
||||||
|
total := 0
|
||||||
|
for _, c := range pkts[0].Bytes {
|
||||||
|
total += int(c)
|
||||||
|
}
|
||||||
|
_ = total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(2)
|
||||||
|
go run(0)
|
||||||
|
go run(1)
|
||||||
|
|
||||||
|
// Feed 2*iterations packets. io.Pipe copies each write straight into the
|
||||||
|
// waiting reader's private buffer, so reusing buf between writes is safe.
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 32)
|
||||||
|
for i := 0; i < 2*iterations; i++ {
|
||||||
|
for j := range buf {
|
||||||
|
buf[j] = byte(i + j)
|
||||||
|
}
|
||||||
|
if _, err := ow.Write(buf); err != nil {
|
||||||
|
errs <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
select {
|
||||||
|
case err := <-errs:
|
||||||
|
t.Fatalf("concurrent reader failed: %v", err)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstBytes(p []tio.Packet) []byte {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p[0].Bytes
|
||||||
|
}
|
||||||
+166
-167
@@ -4,13 +4,12 @@
|
|||||||
package udp
|
package udp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sync/atomic"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
@@ -20,51 +19,58 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type StdConn struct {
|
type StdConn struct {
|
||||||
sysFd int
|
udpConn *net.UDPConn
|
||||||
closed atomic.Bool
|
rawConn syscall.RawConn
|
||||||
isV4 bool
|
isV4 bool
|
||||||
l *slog.Logger
|
l *slog.Logger
|
||||||
batch int
|
batch int
|
||||||
|
}
|
||||||
|
|
||||||
|
func setReusePort(network, address string, c syscall.RawConn) error {
|
||||||
|
var opErr error
|
||||||
|
err := c.Control(func(fd uintptr) {
|
||||||
|
opErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
|
||||||
|
//CloseOnExec already set by the runtime
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return opErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewListener(l *slog.Logger, ip netip.Addr, port int, multi bool, batch int) (Conn, error) {
|
func NewListener(l *slog.Logger, ip netip.Addr, port int, multi bool, batch int) (Conn, error) {
|
||||||
af := unix.AF_INET6
|
listen := netip.AddrPortFrom(ip, uint16(port))
|
||||||
if ip.Is4() {
|
lc := net.ListenConfig{}
|
||||||
af = unix.AF_INET
|
|
||||||
}
|
|
||||||
syscall.ForkLock.RLock()
|
|
||||||
fd, err := unix.Socket(af, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
|
|
||||||
if err == nil {
|
|
||||||
unix.CloseOnExec(fd)
|
|
||||||
}
|
|
||||||
syscall.ForkLock.RUnlock()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to open socket: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if multi {
|
if multi {
|
||||||
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
lc.Control = setReusePort
|
||||||
_ = unix.Close(fd)
|
}
|
||||||
return nil, fmt.Errorf("unable to set SO_REUSEPORT: %w", err)
|
//this context is only used during the bind operation, you can't cancel it to kill the socket
|
||||||
}
|
pc, err := lc.ListenPacket(context.Background(), "udp", listen.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to open socket: %s", err)
|
||||||
|
}
|
||||||
|
udpConn := pc.(*net.UDPConn)
|
||||||
|
rawConn, err := udpConn.SyscallConn()
|
||||||
|
if err != nil {
|
||||||
|
_ = udpConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
//gotta find out if we got an AF_INET6 socket or not:
|
||||||
|
out := &StdConn{
|
||||||
|
udpConn: udpConn,
|
||||||
|
rawConn: rawConn,
|
||||||
|
l: l,
|
||||||
|
batch: batch,
|
||||||
}
|
}
|
||||||
|
|
||||||
var sa unix.Sockaddr
|
af, err := out.getSockOptInt(unix.SO_DOMAIN)
|
||||||
if ip.Is4() {
|
if err != nil {
|
||||||
sa4 := &unix.SockaddrInet4{Port: port}
|
_ = out.Close()
|
||||||
sa4.Addr = ip.As4()
|
return nil, err
|
||||||
sa = sa4
|
|
||||||
} else {
|
|
||||||
sa6 := &unix.SockaddrInet6{Port: port}
|
|
||||||
sa6.Addr = ip.As16()
|
|
||||||
sa = sa6
|
|
||||||
}
|
|
||||||
if err = unix.Bind(fd, sa); err != nil {
|
|
||||||
_ = unix.Close(fd)
|
|
||||||
return nil, fmt.Errorf("unable to bind to socket: %w", err)
|
|
||||||
}
|
}
|
||||||
|
out.isV4 = af == unix.AF_INET
|
||||||
|
|
||||||
return &StdConn{sysFd: fd, isV4: ip.Is4(), l: l, batch: batch}, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) SupportsMultipleReaders() bool {
|
func (u *StdConn) SupportsMultipleReaders() bool {
|
||||||
@@ -75,111 +81,134 @@ func (u *StdConn) Rebind() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *StdConn) getSockOptInt(opt int) (int, error) {
|
||||||
|
if u.rawConn == nil {
|
||||||
|
return 0, fmt.Errorf("no UDP connection")
|
||||||
|
}
|
||||||
|
var out int
|
||||||
|
var opErr error
|
||||||
|
err := u.rawConn.Control(func(fd uintptr) {
|
||||||
|
out, opErr = unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, opt)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return out, opErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *StdConn) setSockOptInt(opt int, n int) error {
|
||||||
|
if u.rawConn == nil {
|
||||||
|
return fmt.Errorf("no UDP connection")
|
||||||
|
}
|
||||||
|
var opErr error
|
||||||
|
err := u.rawConn.Control(func(fd uintptr) {
|
||||||
|
opErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, opt, n)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return opErr
|
||||||
|
}
|
||||||
|
|
||||||
func (u *StdConn) SetRecvBuffer(n int) error {
|
func (u *StdConn) SetRecvBuffer(n int) error {
|
||||||
return unix.SetsockoptInt(u.sysFd, unix.SOL_SOCKET, unix.SO_RCVBUFFORCE, n)
|
return u.setSockOptInt(unix.SO_RCVBUFFORCE, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) SetSendBuffer(n int) error {
|
func (u *StdConn) SetSendBuffer(n int) error {
|
||||||
return unix.SetsockoptInt(u.sysFd, unix.SOL_SOCKET, unix.SO_SNDBUFFORCE, n)
|
return u.setSockOptInt(unix.SO_SNDBUFFORCE, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) SetSoMark(mark int) error {
|
func (u *StdConn) SetSoMark(mark int) error {
|
||||||
return unix.SetsockoptInt(u.sysFd, unix.SOL_SOCKET, unix.SO_MARK, mark)
|
return u.setSockOptInt(unix.SO_MARK, mark)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) GetRecvBuffer() (int, error) {
|
func (u *StdConn) GetRecvBuffer() (int, error) {
|
||||||
return unix.GetsockoptInt(u.sysFd, unix.SOL_SOCKET, unix.SO_RCVBUF)
|
return u.getSockOptInt(unix.SO_RCVBUF)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) GetSendBuffer() (int, error) {
|
func (u *StdConn) GetSendBuffer() (int, error) {
|
||||||
return unix.GetsockoptInt(u.sysFd, unix.SOL_SOCKET, unix.SO_SNDBUF)
|
return u.getSockOptInt(unix.SO_SNDBUF)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) GetSoMark() (int, error) {
|
func (u *StdConn) GetSoMark() (int, error) {
|
||||||
return unix.GetsockoptInt(u.sysFd, unix.SOL_SOCKET, unix.SO_MARK)
|
return u.getSockOptInt(unix.SO_MARK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) LocalAddr() (netip.AddrPort, error) {
|
func (u *StdConn) LocalAddr() (netip.AddrPort, error) {
|
||||||
sa, err := unix.Getsockname(u.sysFd)
|
a := u.udpConn.LocalAddr()
|
||||||
if err != nil {
|
|
||||||
return netip.AddrPort{}, err
|
switch v := a.(type) {
|
||||||
}
|
case *net.UDPAddr:
|
||||||
switch sa := sa.(type) {
|
addr, ok := netip.AddrFromSlice(v.IP)
|
||||||
case *unix.SockaddrInet4:
|
if !ok {
|
||||||
return netip.AddrPortFrom(netip.AddrFrom4(sa.Addr), uint16(sa.Port)), nil
|
return netip.AddrPort{}, fmt.Errorf("LocalAddr returned invalid IP address: %s", v.IP)
|
||||||
case *unix.SockaddrInet6:
|
}
|
||||||
return netip.AddrPortFrom(netip.AddrFrom16(sa.Addr), uint16(sa.Port)), nil
|
return netip.AddrPortFrom(addr, uint16(v.Port)), nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return netip.AddrPort{}, fmt.Errorf("unsupported sock type: %T", sa)
|
return netip.AddrPort{}, fmt.Errorf("LocalAddr returned: %#v", a)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// recvmmsg does one blocking recvmmsg (MSG_WAITFORONE), reading up to len(msgs) datagrams
|
func recvmmsg(fd uintptr, msgs []rawMessage) (int, bool, error) {
|
||||||
func (u *StdConn) recvmmsg(msgs []rawMessage) (int, error) {
|
var errno syscall.Errno
|
||||||
r, _, errno := unix.Syscall6(
|
n, _, errno := unix.Syscall6(
|
||||||
unix.SYS_RECVMMSG,
|
unix.SYS_RECVMMSG,
|
||||||
uintptr(u.sysFd),
|
fd,
|
||||||
uintptr(unsafe.Pointer(&msgs[0])),
|
uintptr(unsafe.Pointer(&msgs[0])),
|
||||||
uintptr(len(msgs)),
|
uintptr(len(msgs)),
|
||||||
unix.MSG_WAITFORONE,
|
unix.MSG_WAITFORONE,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
if errno == syscall.EAGAIN || errno == syscall.EWOULDBLOCK {
|
||||||
|
// No data available, block for I/O and try again.
|
||||||
|
return int(n), false, nil
|
||||||
|
}
|
||||||
if errno != 0 {
|
if errno != 0 {
|
||||||
if u.closed.Load() {
|
return int(n), true, &net.OpError{Op: "recvmmsg", Err: errno}
|
||||||
return 0, net.ErrClosed
|
|
||||||
}
|
|
||||||
return 0, &net.OpError{Op: "recvmmsg", Err: errno}
|
|
||||||
}
|
}
|
||||||
n := int(r)
|
return int(n), true, nil
|
||||||
if (n == 0 || msgs[0].Len == 0) && u.closed.Load() {
|
|
||||||
return 0, net.ErrClosed
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// recvmsg does one blocking recvmsg into msgs[0]
|
func (u *StdConn) listenOutSingle(r EncReader) error {
|
||||||
func (u *StdConn) recvmsg(msgs []rawMessage) (int, error) {
|
var err error
|
||||||
r, _, errno := unix.Syscall6(
|
var n int
|
||||||
unix.SYS_RECVMSG,
|
var from netip.AddrPort
|
||||||
uintptr(u.sysFd),
|
buffer := make([]byte, MTU)
|
||||||
uintptr(unsafe.Pointer(&msgs[0].Hdr)),
|
|
||||||
0,
|
for {
|
||||||
0,
|
n, from, err = u.udpConn.ReadFromUDPAddrPort(buffer)
|
||||||
0,
|
if err != nil {
|
||||||
0,
|
return err
|
||||||
)
|
|
||||||
if errno != 0 {
|
|
||||||
if u.closed.Load() {
|
|
||||||
return 0, net.ErrClosed
|
|
||||||
}
|
}
|
||||||
return 0, &net.OpError{Op: "recvmsg", Err: errno}
|
from = netip.AddrPortFrom(from.Addr().Unmap(), from.Port())
|
||||||
|
r(from, buffer[:n])
|
||||||
}
|
}
|
||||||
if r == 0 && u.closed.Load() {
|
|
||||||
return 0, net.ErrClosed
|
|
||||||
}
|
|
||||||
msgs[0].Len = uint32(r)
|
|
||||||
return 1, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) ListenOut(r EncReader) error {
|
func (u *StdConn) listenOutBatch(r EncReader) error {
|
||||||
var ip netip.Addr
|
var ip netip.Addr
|
||||||
|
var n int
|
||||||
|
var operr error
|
||||||
|
|
||||||
msgs, buffers, names := u.PrepareRawMessages(u.batch)
|
msgs, buffers, names := u.PrepareRawMessages(u.batch)
|
||||||
read := u.recvmmsg
|
|
||||||
if u.batch == 1 {
|
//reader needs to capture variables from this function, since it's used as a lambda with rawConn.Read
|
||||||
read = u.recvmsg
|
//defining it outside the loop so it gets re-used
|
||||||
|
reader := func(fd uintptr) (done bool) {
|
||||||
|
n, done, operr = recvmmsg(fd, msgs)
|
||||||
|
return done
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
n, err := read(msgs)
|
err := u.rawConn.Read(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, unix.EINTR) {
|
|
||||||
continue // interrupted by a signal, retry the read
|
|
||||||
}
|
|
||||||
// net.ErrClosed after Close() is teardown, absorbed by the caller's
|
|
||||||
// closed flag like the other platforms; anything else is a real error.
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if operr != nil {
|
||||||
|
return operr
|
||||||
|
}
|
||||||
|
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
// Its ok to skip the ok check here, the slicing is the only error that can occur and it will panic
|
// Its ok to skip the ok check here, the slicing is the only error that can occur and it will panic
|
||||||
@@ -193,68 +222,26 @@ func (u *StdConn) ListenOut(r EncReader) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *StdConn) ListenOut(r EncReader) error {
|
||||||
|
if u.batch == 1 {
|
||||||
|
return u.listenOutSingle(r)
|
||||||
|
} else {
|
||||||
|
return u.listenOutBatch(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (u *StdConn) WriteTo(b []byte, ip netip.AddrPort) error {
|
func (u *StdConn) WriteTo(b []byte, ip netip.AddrPort) error {
|
||||||
if u.isV4 {
|
_, err := u.udpConn.WriteToUDPAddrPort(b, ip)
|
||||||
return u.writeTo4(b, ip)
|
return err
|
||||||
}
|
|
||||||
return u.writeTo6(b, ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *StdConn) writeTo6(b []byte, ip netip.AddrPort) error {
|
|
||||||
var rsa unix.RawSockaddrInet6
|
|
||||||
rsa.Family = unix.AF_INET6
|
|
||||||
rsa.Addr = ip.Addr().As16()
|
|
||||||
binary.BigEndian.PutUint16((*[2]byte)(unsafe.Pointer(&rsa.Port))[:], ip.Port())
|
|
||||||
|
|
||||||
for {
|
|
||||||
_, _, err := unix.Syscall6(
|
|
||||||
unix.SYS_SENDTO,
|
|
||||||
uintptr(u.sysFd),
|
|
||||||
uintptr(unsafe.Pointer(&b[0])),
|
|
||||||
uintptr(len(b)),
|
|
||||||
uintptr(0),
|
|
||||||
uintptr(unsafe.Pointer(&rsa)),
|
|
||||||
uintptr(unix.SizeofSockaddrInet6),
|
|
||||||
)
|
|
||||||
if err != 0 {
|
|
||||||
return &net.OpError{Op: "sendto", Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *StdConn) writeTo4(b []byte, ip netip.AddrPort) error {
|
|
||||||
if !ip.Addr().Is4() {
|
|
||||||
return ErrInvalidIPv6RemoteForSocket
|
|
||||||
}
|
|
||||||
|
|
||||||
var rsa unix.RawSockaddrInet4
|
|
||||||
rsa.Family = unix.AF_INET
|
|
||||||
rsa.Addr = ip.Addr().As4()
|
|
||||||
binary.BigEndian.PutUint16((*[2]byte)(unsafe.Pointer(&rsa.Port))[:], ip.Port())
|
|
||||||
|
|
||||||
for {
|
|
||||||
_, _, err := unix.Syscall6(
|
|
||||||
unix.SYS_SENDTO,
|
|
||||||
uintptr(u.sysFd),
|
|
||||||
uintptr(unsafe.Pointer(&b[0])),
|
|
||||||
uintptr(len(b)),
|
|
||||||
uintptr(0),
|
|
||||||
uintptr(unsafe.Pointer(&rsa)),
|
|
||||||
uintptr(unix.SizeofSockaddrInet4),
|
|
||||||
)
|
|
||||||
if err != 0 {
|
|
||||||
return &net.OpError{Op: "sendto", Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) ReloadConfig(c *config.C) {
|
func (u *StdConn) ReloadConfig(c *config.C) {
|
||||||
b := c.GetInt("listen.read_buffer", 0)
|
b := c.GetInt("listen.read_buffer", 0)
|
||||||
if b > 0 {
|
if b > 0 {
|
||||||
if err := u.SetRecvBuffer(b); err == nil {
|
err := u.SetRecvBuffer(b)
|
||||||
if s, err := u.GetRecvBuffer(); err == nil {
|
if err == nil {
|
||||||
|
s, err := u.GetRecvBuffer()
|
||||||
|
if err == nil {
|
||||||
u.l.Info("listen.read_buffer was set", "size", s)
|
u.l.Info("listen.read_buffer was set", "size", s)
|
||||||
} else {
|
} else {
|
||||||
u.l.Warn("Failed to get listen.read_buffer", "error", err)
|
u.l.Warn("Failed to get listen.read_buffer", "error", err)
|
||||||
@@ -266,8 +253,10 @@ func (u *StdConn) ReloadConfig(c *config.C) {
|
|||||||
|
|
||||||
b = c.GetInt("listen.write_buffer", 0)
|
b = c.GetInt("listen.write_buffer", 0)
|
||||||
if b > 0 {
|
if b > 0 {
|
||||||
if err := u.SetSendBuffer(b); err == nil {
|
err := u.SetSendBuffer(b)
|
||||||
if s, err := u.GetSendBuffer(); err == nil {
|
if err == nil {
|
||||||
|
s, err := u.GetSendBuffer()
|
||||||
|
if err == nil {
|
||||||
u.l.Info("listen.write_buffer was set", "size", s)
|
u.l.Info("listen.write_buffer was set", "size", s)
|
||||||
} else {
|
} else {
|
||||||
u.l.Warn("Failed to get listen.write_buffer", "error", err)
|
u.l.Warn("Failed to get listen.write_buffer", "error", err)
|
||||||
@@ -280,8 +269,10 @@ func (u *StdConn) ReloadConfig(c *config.C) {
|
|||||||
b = c.GetInt("listen.so_mark", 0)
|
b = c.GetInt("listen.so_mark", 0)
|
||||||
s, err := u.GetSoMark()
|
s, err := u.GetSoMark()
|
||||||
if b > 0 || (err == nil && s != 0) {
|
if b > 0 || (err == nil && s != 0) {
|
||||||
if err := u.SetSoMark(b); err == nil {
|
err := u.SetSoMark(b)
|
||||||
if s, err := u.GetSoMark(); err == nil {
|
if err == nil {
|
||||||
|
s, err := u.GetSoMark()
|
||||||
|
if err == nil {
|
||||||
u.l.Info("listen.so_mark was set", "mark", s)
|
u.l.Info("listen.so_mark was set", "mark", s)
|
||||||
} else {
|
} else {
|
||||||
u.l.Warn("Failed to get listen.so_mark", "error", err)
|
u.l.Warn("Failed to get listen.so_mark", "error", err)
|
||||||
@@ -294,20 +285,28 @@ func (u *StdConn) ReloadConfig(c *config.C) {
|
|||||||
|
|
||||||
func (u *StdConn) getMemInfo(meminfo *[unix.SK_MEMINFO_VARS]uint32) error {
|
func (u *StdConn) getMemInfo(meminfo *[unix.SK_MEMINFO_VARS]uint32) error {
|
||||||
var vallen uint32 = 4 * unix.SK_MEMINFO_VARS
|
var vallen uint32 = 4 * unix.SK_MEMINFO_VARS
|
||||||
_, _, err := unix.Syscall6(unix.SYS_GETSOCKOPT, uintptr(u.sysFd), uintptr(unix.SOL_SOCKET), uintptr(unix.SO_MEMINFO), uintptr(unsafe.Pointer(meminfo)), uintptr(unsafe.Pointer(&vallen)), 0)
|
|
||||||
if err != 0 {
|
if u.rawConn == nil {
|
||||||
|
return fmt.Errorf("no UDP connection")
|
||||||
|
}
|
||||||
|
var opErr error
|
||||||
|
err := u.rawConn.Control(func(fd uintptr) {
|
||||||
|
_, _, syserr := unix.Syscall6(unix.SYS_GETSOCKOPT, fd, uintptr(unix.SOL_SOCKET), uintptr(unix.SO_MEMINFO), uintptr(unsafe.Pointer(meminfo)), uintptr(unsafe.Pointer(&vallen)), 0)
|
||||||
|
if syserr != 0 {
|
||||||
|
opErr = syserr
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return opErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) Close() error {
|
func (u *StdConn) Close() error {
|
||||||
u.closed.Store(true)
|
if u.udpConn != nil {
|
||||||
// Wake the reader parked in recvmmsg/recvmsg. shutdown(2) on an unconnected socket
|
return u.udpConn.Close()
|
||||||
// returns ENOTCONN but still wakes it, so ignore the error.
|
}
|
||||||
// The reader then sees closed and stops touching the fd, making the Close below safe.
|
return nil
|
||||||
_ = unix.Shutdown(u.sysFd, unix.SHUT_RDWR)
|
|
||||||
return unix.Close(u.sysFd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUDPStatsEmitter(udpConns []Conn) func() {
|
func NewUDPStatsEmitter(udpConns []Conn) func() {
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
//go:build linux && !android && !e2e_testing
|
|
||||||
|
|
||||||
package udp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
func testLogger() *slog.Logger {
|
|
||||||
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestShutdownWakesAfterRx_Mechanism exercises the kernel quirk our teardown
|
|
||||||
// relies on: once a socket has received a packet, shutdown(2) wakes a blocked
|
|
||||||
// recvmmsg with n>=1/Len==0 (not n==0). recvmmsg must turn that into net.ErrClosed
|
|
||||||
// once Close set closed, so a parked reader exits instead of spinning.
|
|
||||||
func TestShutdownWakesAfterRx_Mechanism(t *testing.T) {
|
|
||||||
c, err := NewListener(testLogger(), netip.MustParseAddr("127.0.0.1"), 0, true, 64)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewListener: %v", err)
|
|
||||||
}
|
|
||||||
sc := c.(*StdConn)
|
|
||||||
addr, err := sc.LocalAddr()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("LocalAddr: %v", err)
|
|
||||||
}
|
|
||||||
msgs, _, _ := sc.PrepareRawMessages(sc.batch)
|
|
||||||
|
|
||||||
// Receive a real packet so the socket has carried data.
|
|
||||||
send, err := net.Dial("udp", addr.String())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("dial: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := send.Write([]byte("hello")); err != nil {
|
|
||||||
t.Fatalf("write: %v", err)
|
|
||||||
}
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
n, err := sc.recvmmsg(msgs)
|
|
||||||
t.Logf("drain of real packet: n=%d err=%v msgs[0].Len=%d", n, err, msgs[0].Len)
|
|
||||||
_ = send.Close()
|
|
||||||
|
|
||||||
// Block a reader on the now-empty queue, then tear down as Close() does.
|
|
||||||
// recvmmsg must return net.ErrClosed (not hang, not spin) even post-rx.
|
|
||||||
done := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
_, err := sc.recvmmsg(msgs)
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
time.Sleep(150 * time.Millisecond) // let it park in recvmmsg
|
|
||||||
|
|
||||||
sc.closed.Store(true)
|
|
||||||
if serr := unix.Shutdown(sc.sysFd, unix.SHUT_RDWR); serr != nil {
|
|
||||||
t.Logf("shutdown returned %v (expected ENOTCONN on unconnected UDP)", serr)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err := <-done:
|
|
||||||
if !errors.Is(err, net.ErrClosed) {
|
|
||||||
t.Errorf("recvmmsg after post-rx shutdown returned %v, want net.ErrClosed", err)
|
|
||||||
}
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
t.Fatalf("HANG: recvmmsg did not return after shutdown following a received packet")
|
|
||||||
}
|
|
||||||
_ = unix.Close(sc.sysFd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestListenOutTeardown_TrafficPatterns reproduces the field report: a blocking
|
|
||||||
// reader must tear down cleanly on Close() regardless of what the socket has
|
|
||||||
// carried. The three cases the report called out:
|
|
||||||
//
|
|
||||||
// no traffic ever -> works (shutdown wakes recvmmsg with n==0)
|
|
||||||
// ping once, then idle -> historically HUNG: once the socket has received a
|
|
||||||
// packet, shutdown(2) wakes recvmmsg with n>=1/Len==0,
|
|
||||||
// which an n==0-only teardown check misses
|
|
||||||
// continuous traffic -> works (a real packet is always arriving)
|
|
||||||
//
|
|
||||||
// All three must return within the deadline; a hang dumps goroutines so the
|
|
||||||
// stuck reader is visible.
|
|
||||||
func TestListenOutTeardown_TrafficPatterns(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
traffic func(send net.Conn, stop <-chan struct{})
|
|
||||||
}{
|
|
||||||
{"no_traffic_ever", func(net.Conn, <-chan struct{}) {}},
|
|
||||||
{"ping_once_then_idle", func(send net.Conn, _ <-chan struct{}) {
|
|
||||||
_, _ = send.Write([]byte("hello"))
|
|
||||||
}},
|
|
||||||
{"continuous", func(send net.Conn, stop <-chan struct{}) {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
_, _ = send.Write([]byte("hello"))
|
|
||||||
time.Sleep(2 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
|
|
||||||
// batch 1 exercises the recvmsg path, batch 64 the recvmmsg path; both must
|
|
||||||
// tear down cleanly.
|
|
||||||
for _, batch := range []int{1, 64} {
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(fmt.Sprintf("batch%d/%s", batch, tc.name), func(t *testing.T) {
|
|
||||||
runTeardownCase(t, batch, tc.name, tc.traffic)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runTeardownCase(t *testing.T, batch int, name string, traffic func(send net.Conn, stop <-chan struct{})) {
|
|
||||||
c, err := NewListener(testLogger(), netip.MustParseAddr("127.0.0.1"), 0, true, batch)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewListener: %v", err)
|
|
||||||
}
|
|
||||||
sc := c.(*StdConn)
|
|
||||||
addr, err := sc.LocalAddr()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("LocalAddr: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var received atomic.Int64
|
|
||||||
loopDone := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
loopDone <- sc.ListenOut(func(netip.AddrPort, []byte) {
|
|
||||||
received.Add(1)
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
|
|
||||||
send, err := net.Dial("udp", addr.String())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("dial: %v", err)
|
|
||||||
}
|
|
||||||
defer send.Close()
|
|
||||||
|
|
||||||
stop := make(chan struct{})
|
|
||||||
trafficDone := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
traffic(send, stop)
|
|
||||||
close(trafficDone)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Let the pattern run and, for the idle case, the reader park again on an
|
|
||||||
// empty queue with the socket already having received a packet.
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
|
|
||||||
start := time.Now()
|
|
||||||
if err := sc.Close(); err != nil {
|
|
||||||
t.Fatalf("Close: %v", err)
|
|
||||||
}
|
|
||||||
close(stop)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err := <-loopDone:
|
|
||||||
// Clean teardown surfaces as net.ErrClosed (propagated like the other
|
|
||||||
// platforms); the caller absorbs it via its closed flag.
|
|
||||||
if err != nil && !errors.Is(err, net.ErrClosed) {
|
|
||||||
t.Fatalf("%s: ListenOut returned unexpected error on teardown: %v", name, err)
|
|
||||||
}
|
|
||||||
t.Logf("%s: closed in %v (received %d packets)", name, time.Since(start), received.Load())
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
buf := make([]byte, 1<<20)
|
|
||||||
n := runtime.Stack(buf, true)
|
|
||||||
t.Fatalf("%s: HANG, ListenOut did not return within 3s of Close\n%s", name, buf[:n])
|
|
||||||
}
|
|
||||||
<-trafficDone
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
//go:build linux && !android && !e2e_testing
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PinThreadToCPU restricts the calling OS thread to the given CPU via
|
||||||
|
// sched_setaffinity(2). Combined with runtime.LockOSThread on the
|
||||||
|
// goroutine, this prevents the kernel from migrating us across CPUs and
|
||||||
|
// in turn keeps every UDP send from this goroutine going through the
|
||||||
|
// same XPS-selected TX ring, eliminating the wire-side reorder that
|
||||||
|
// otherwise fragments one nebula flow across multiple rings.
|
||||||
|
func PinThreadToCPU(cpu int) error {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
var set unix.CPUSet
|
||||||
|
set.Zero()
|
||||||
|
set.Set(cpu)
|
||||||
|
return unix.SchedSetaffinity(0, &set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowedCPUs returns the CPU IDs the calling process is currently allowed to
|
||||||
|
// run on, as reported by sched_getaffinity(2). Under a cgroup cpuset or a
|
||||||
|
// `taskset` mask the allowed IDs are frequently not the contiguous range
|
||||||
|
// 0..NumCPU-1 (e.g. pinned to CPUs 4-7: NumCPU reports 4 while the valid IDs
|
||||||
|
// are 4,5,6,7). Callers that need a real CPU to pin to must choose from this
|
||||||
|
// set rather than assuming i % NumCPU is runnable, or every pin fails.
|
||||||
|
func AllowedCPUs() ([]int, error) {
|
||||||
|
var set unix.CPUSet
|
||||||
|
if err := unix.SchedGetaffinity(0, &set); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cpus := make([]int, 0, set.Count())
|
||||||
|
for cpu := 0; cpu < len(set)*64; cpu++ {
|
||||||
|
if set.IsSet(cpu) {
|
||||||
|
cpus = append(cpus, cpu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cpus, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//go:build !linux || android || e2e_testing
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
// PinThreadToCPU is a no-op outside Linux: only Linux exposes a stable
|
||||||
|
// per-thread CPU affinity API and only Linux has XPS-driven TX ring
|
||||||
|
// selection in the first place. On every other platform there's nothing
|
||||||
|
// to fix here.
|
||||||
|
func PinThreadToCPU(_ int) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowedCPUs has no meaningful answer off Linux (no sched_getaffinity), so it
|
||||||
|
// reports "unknown" by returning a nil slice and nil error. Callers treat an
|
||||||
|
// empty result as "fall back to the default CPU choice".
|
||||||
|
func AllowedCPUs() ([]int, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
//go:build darwin
|
|
||||||
|
|
||||||
package nebula
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
// suspendClockDelta returns CLOCK_MONOTONIC - CLOCK_UPTIME_RAW. On macOS CLOCK_MONOTONIC keeps counting across
|
|
||||||
// system sleep while CLOCK_UPTIME_RAW (mach_absolute_time) pauses, so the difference grows by time spent asleep.
|
|
||||||
//
|
|
||||||
// The pausing clock is read first so scheduling jitter between the two reads biases the delta positive; the
|
|
||||||
// wakeDetector clamps out the noise.
|
|
||||||
//
|
|
||||||
// Caveat: on Apple Silicon the hardware timebase keeps ticking through sleep, which can make both clocks advance
|
|
||||||
// and the spread stay flat, leaving this detector blind. That fails safe (no clears, behavior as before); IOKit
|
|
||||||
// power notifications are the follow-up for full coverage on those machines.
|
|
||||||
func suspendClockDelta() (time.Duration, bool) {
|
|
||||||
var uptime, mono unix.Timespec
|
|
||||||
if err := unix.ClockGettime(unix.CLOCK_UPTIME_RAW, &uptime); err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &mono); err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
return time.Duration(mono.Nano() - uptime.Nano()), true
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
//go:build !linux && !darwin && !windows
|
|
||||||
|
|
||||||
package nebula
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// suspendClockDelta reports that this platform has no usable clock pair for detecting system sleep; the wake
|
|
||||||
// detector stays dormant and dead tunnels are left to the normal traffic checks.
|
|
||||||
func suspendClockDelta() (time.Duration, bool) {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
//go:build linux
|
|
||||||
|
|
||||||
package nebula
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
// suspendClockDelta returns CLOCK_BOOTTIME - CLOCK_MONOTONIC. CLOCK_MONOTONIC pauses while the system is suspended
|
|
||||||
// and CLOCK_BOOTTIME does not, so the difference only ever grows, and only by time spent suspended. Both reads are
|
|
||||||
// vDSO calls, cheap enough for a hot ticker.
|
|
||||||
//
|
|
||||||
// The pausing clock is read first so scheduling jitter between the two reads biases the delta positive; the
|
|
||||||
// wakeDetector clamps out the noise.
|
|
||||||
func suspendClockDelta() (time.Duration, bool) {
|
|
||||||
var mono, boot unix.Timespec
|
|
||||||
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &mono); err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
if err := unix.ClockGettime(unix.CLOCK_BOOTTIME, &boot); err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
return time.Duration(boot.Nano() - mono.Nano()), true
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
//go:build windows
|
|
||||||
|
|
||||||
package nebula
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
procQueryInterruptTime = windows.NewLazySystemDLL("kernelbase.dll").NewProc("QueryInterruptTime")
|
|
||||||
procQueryUnbiasedInterruptTime = windows.NewLazySystemDLL("kernel32.dll").NewProc("QueryUnbiasedInterruptTime")
|
|
||||||
|
|
||||||
// QueryInterruptTime needs Windows 10; probe once and stay dormant on anything older.
|
|
||||||
wakeClockAvailable = sync.OnceValue(func() bool {
|
|
||||||
return procQueryInterruptTime.Find() == nil && procQueryUnbiasedInterruptTime.Find() == nil
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// suspendClockDelta returns interrupt time minus unbiased interrupt time, both in 100ns units. The unbiased count
|
|
||||||
// excludes time the system spends suspended while the biased one includes it, so the difference grows by exactly
|
|
||||||
// the time spent asleep.
|
|
||||||
//
|
|
||||||
// The pausing (unbiased) clock is read first so scheduling jitter between the two reads biases the delta positive;
|
|
||||||
// the wakeDetector clamps out the noise.
|
|
||||||
func suspendClockDelta() (time.Duration, bool) {
|
|
||||||
if !wakeClockAvailable() {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
var unbiased, biased uint64
|
|
||||||
if r1, _, _ := procQueryUnbiasedInterruptTime.Call(uintptr(unsafe.Pointer(&unbiased))); r1 == 0 {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
// Returns void, cannot fail once resolved.
|
|
||||||
_, _, _ = procQueryInterruptTime.Call(uintptr(unsafe.Pointer(&biased)))
|
|
||||||
return time.Duration(int64(biased-unbiased)) * 100, true
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package nebula
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// wakeDetector notices when the machine has returned from system sleep and measures how long it was suspended.
|
|
||||||
//
|
|
||||||
// It samples the spread between two kernel clocks: one that pauses across a suspend and one that keeps counting
|
|
||||||
// (suspendClockDelta, per platform). While the machine is awake the spread is constant no matter how starved,
|
|
||||||
// stopped, or stepped this process is — SIGSTOP, debugger pauses, scheduler starvation, and NTP adjustments move
|
|
||||||
// both clocks together or neither, so none of them can fake a wake. A true suspend is the only thing that grows
|
|
||||||
// the spread, and it grows by exactly the time spent suspended.
|
|
||||||
//
|
|
||||||
// Sample is intended to piggyback on a ticker the caller already runs; it costs two clock reads. It is not safe
|
|
||||||
// for concurrent use.
|
|
||||||
type wakeDetector struct {
|
|
||||||
// read returns the current spread between the two clocks, false if this platform can't provide one.
|
|
||||||
read func() (time.Duration, bool)
|
|
||||||
last time.Duration
|
|
||||||
primed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newWakeDetector() *wakeDetector {
|
|
||||||
return &wakeDetector{read: suspendClockDelta}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sample returns how long the machine was suspended since the previous call, 0 if it wasn't, and false if the
|
|
||||||
// platform has no way to tell. The first call primes the baseline and always reports 0.
|
|
||||||
func (w *wakeDetector) Sample() (time.Duration, bool) {
|
|
||||||
delta, ok := w.read()
|
|
||||||
if !ok {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !w.primed {
|
|
||||||
w.primed = true
|
|
||||||
w.last = delta
|
|
||||||
return 0, true
|
|
||||||
}
|
|
||||||
|
|
||||||
slept := delta - w.last
|
|
||||||
w.last = delta
|
|
||||||
if slept < 0 {
|
|
||||||
// The clock pair is read non-atomically so tiny negative jitter is possible; it is never a wake.
|
|
||||||
slept = 0
|
|
||||||
}
|
|
||||||
return slept, true
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package nebula
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestWakeDetector(t *testing.T) {
|
|
||||||
delta := time.Duration(0)
|
|
||||||
ok := true
|
|
||||||
w := &wakeDetector{read: func() (time.Duration, bool) { return delta, ok }}
|
|
||||||
|
|
||||||
// The first sample primes the baseline and never reports a wake, even with a pre-existing spread
|
|
||||||
delta = 3 * time.Hour
|
|
||||||
slept, sok := w.Sample()
|
|
||||||
assert.True(t, sok)
|
|
||||||
assert.Equal(t, time.Duration(0), slept)
|
|
||||||
|
|
||||||
// A stable spread means the machine never slept
|
|
||||||
slept, sok = w.Sample()
|
|
||||||
assert.True(t, sok)
|
|
||||||
assert.Equal(t, time.Duration(0), slept)
|
|
||||||
|
|
||||||
// The spread grows by exactly the time spent suspended
|
|
||||||
delta += 42 * time.Second
|
|
||||||
slept, sok = w.Sample()
|
|
||||||
assert.True(t, sok)
|
|
||||||
assert.Equal(t, 42*time.Second, slept)
|
|
||||||
|
|
||||||
// A wake is reported once, then the baseline moves with it
|
|
||||||
slept, sok = w.Sample()
|
|
||||||
assert.True(t, sok)
|
|
||||||
assert.Equal(t, time.Duration(0), slept)
|
|
||||||
|
|
||||||
// Negative jitter from the non-atomic clock pair reads clamps to zero
|
|
||||||
delta -= time.Microsecond
|
|
||||||
slept, sok = w.Sample()
|
|
||||||
assert.True(t, sok)
|
|
||||||
assert.Equal(t, time.Duration(0), slept)
|
|
||||||
|
|
||||||
// Consecutive suspends both report; the clamped jitter moved the baseline so it is not double-counted
|
|
||||||
delta += time.Minute
|
|
||||||
slept, _ = w.Sample()
|
|
||||||
assert.Equal(t, time.Minute, slept)
|
|
||||||
delta += time.Hour
|
|
||||||
slept, _ = w.Sample()
|
|
||||||
assert.Equal(t, time.Hour, slept)
|
|
||||||
|
|
||||||
// An unsupported platform read reports not-ok
|
|
||||||
ok = false
|
|
||||||
_, sok = w.Sample()
|
|
||||||
assert.False(t, sok)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestWakeDetectorPlatformClock smoke tests the real clock pair: two samples close together must not report a
|
|
||||||
// wake on a machine that isn't suspending mid-test.
|
|
||||||
func TestWakeDetectorPlatformClock(t *testing.T) {
|
|
||||||
w := newWakeDetector()
|
|
||||||
if _, ok := w.Sample(); !ok {
|
|
||||||
t.Skip("platform has no suspend clock pair")
|
|
||||||
}
|
|
||||||
slept, ok := w.Sample()
|
|
||||||
assert.True(t, ok)
|
|
||||||
assert.Less(t, slept, time.Second)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user