mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 07:06:52 +01:00
With tun.staged.workers set, each Linux UDP listener classifies a recvmmsg
batch, hands its data packets to that many decrypt workers, and an ordered
writer commits the batch in arrival order: replay window, roaming, firewall,
control packets and tun writes go through the same helpers inline receive
uses. Six batches per routine bound memory and backpressure. Conns that cannot
hand off their buffers stay inline.
The listener's classification is speculative. Before touching replay state the
writer resolves each data packet through its own batch-local hostmap cache,
exactly where inline receive would, so data after a close in the same batch is
still delivered while data after a removal or replacement between batches is
dropped, with the same RecvError reply inline receive sends. Known replays are
marked by a precheck and skip decryption, but keep their place so that reply
stays identical when an earlier RecvError deleted their tunnel.
New API: udp.StdConn.ListenOutHandoff, and the same on TesterConn for e2e,
runs the receive loop while handing each batch's buffers to the caller in
exchange for replacements. It is found through an unexported interface in
nebula, so udp.Conn and its other implementations are unchanged.
Metrics: flow.rx.batches, and flow.stage.{rx_read,rx_decrypt,rx_write}.q<N>.busy_ns.
Tests cover both ciphers, ordering, duplicate and corrupted packets from other
addresses, window jumps, control packets mid-batch, RecvError then replay,
tunnel replacement, firewall parity, fallback, real UDP sockets, partial
batches at close, and a handshake and traffic through e2e that checks pipeline
use on Linux and the inline fallback elsewhere. No isolated receive throughput
claim is made.
745 lines
23 KiB
Go
745 lines
23 KiB
Go
package nebula
|
|
|
|
import (
|
|
"context"
|
|
"crypto/fips140"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/netip"
|
|
"runtime"
|
|
"slices"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/gaissmai/bart"
|
|
"github.com/rcrowley/go-metrics"
|
|
"github.com/slackhq/nebula/util"
|
|
|
|
"github.com/slackhq/nebula/cert"
|
|
"github.com/slackhq/nebula/config"
|
|
"github.com/slackhq/nebula/firewall"
|
|
"github.com/slackhq/nebula/header"
|
|
"github.com/slackhq/nebula/overlay"
|
|
"github.com/slackhq/nebula/overlay/batch"
|
|
"github.com/slackhq/nebula/overlay/tio"
|
|
"github.com/slackhq/nebula/ratelimit"
|
|
"github.com/slackhq/nebula/udp"
|
|
)
|
|
|
|
const mtu = 9001
|
|
|
|
type InterfaceConfig struct {
|
|
HostMap *HostMap
|
|
Outside udp.Conn
|
|
Inside overlay.Device
|
|
pki *PKI
|
|
Cipher string
|
|
Firewall *Firewall
|
|
DnsServer *dnsServer
|
|
HandshakeManager *HandshakeManager
|
|
lightHouse *LightHouse
|
|
connectionManager *connectionManager
|
|
DropLocalBroadcast bool
|
|
DropMulticast bool
|
|
routines int
|
|
MessageMetrics *MessageMetrics
|
|
version string
|
|
relayManager *relayManager
|
|
punchy *Punchy
|
|
|
|
tryPromoteEvery uint32
|
|
reQueryEvery uint32
|
|
reQueryWait time.Duration
|
|
|
|
ConntrackCacheTimeout time.Duration
|
|
|
|
// 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. Staged sender i uses entry i+1,
|
|
// unless that is its reader's CPU. Empty uses the automatic order. Only
|
|
// consulted when PinThreads is true.
|
|
CpuAffinity []int
|
|
// defaultCpuAffinity is the automatic pin order for readers, kept apart
|
|
// from CpuAffinity because staged senders are only pinned to an explicit list.
|
|
defaultCpuAffinity []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 sendmmsg on one XPS-selected NIC TX ring so per-flow
|
|
// packets stay ordered on the wire.
|
|
PinThreads bool
|
|
staged stagedConfig
|
|
|
|
l *slog.Logger
|
|
}
|
|
|
|
type Interface struct {
|
|
hostMap *HostMap
|
|
outside udp.Conn
|
|
inside overlay.Device
|
|
pki *PKI
|
|
firewall *Firewall
|
|
connectionManager *connectionManager
|
|
handshakeManager *HandshakeManager
|
|
dnsServer *dnsServer
|
|
createTime time.Time
|
|
lightHouse *LightHouse
|
|
myBroadcastAddrsTable *bart.Lite
|
|
myVpnAddrs []netip.Addr // A list of addresses assigned to us via our certificate
|
|
myVpnAddrsTable *bart.Lite
|
|
myVpnNetworks []netip.Prefix // A list of networks assigned to us via our certificate
|
|
myVpnNetworksTable *bart.Lite
|
|
dropLocalBroadcast bool
|
|
dropMulticast bool
|
|
routines int
|
|
disconnectInvalid atomic.Bool
|
|
closed atomic.Bool
|
|
// 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
|
|
defaultCpuAffinity []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
|
|
staged stagedConfig
|
|
relayManager *relayManager
|
|
|
|
tryPromoteEvery atomic.Uint32
|
|
reQueryEvery atomic.Uint32
|
|
reQueryWait atomic.Int64
|
|
|
|
sendRecvErrorConfig recvErrorConfig
|
|
acceptRecvErrorConfig recvErrorConfig
|
|
|
|
// Bumped on every udp rebind, tunnels compare it to decide they need a punch from the far side
|
|
rebindEpoch atomic.Uint32
|
|
version string
|
|
|
|
conntrackCacheTimeout time.Duration
|
|
|
|
ctx context.Context
|
|
writers []udp.Conn
|
|
queues []tio.Queue
|
|
// batchers is one per tun queue, wrapping queues[i]. readOutsidePackets
|
|
// commits plaintext into the batcher; the plaintext is decrypted
|
|
// in place inside the UDP receive buffers, so listenOut must call Flush
|
|
// at the end of each UDP recvmmsg batch, before those buffers are
|
|
// reused (every udp.Conn ListenOut guarantees that ordering).
|
|
batchers []*batch.MultiCoalescer
|
|
wg sync.WaitGroup
|
|
|
|
udpTxErrLimiter *ratelimit.Limiter // udp direct transmit error log limiter
|
|
udpRelayTxErrLimiter *ratelimit.Limiter // udp relay transmit error log limiter
|
|
txOversizeLimiter *ratelimit.Limiter // oversized outbound packet drop log limiter
|
|
|
|
// fatalErr holds the first unexpected reader error that caused shutdown.
|
|
// nil means "no fatal error" (yet)
|
|
fatalErr atomic.Pointer[error]
|
|
// triggerShutdown is a function that will be run exactly once, when onFatal swaps something non-nil into fatalErr
|
|
triggerShutdown func()
|
|
|
|
metricHandshakes metrics.Histogram
|
|
messageMetrics *MessageMetrics
|
|
cachedPacketMetrics *cachedPacketMetrics
|
|
metricTxDropped metrics.Counter
|
|
|
|
l *slog.Logger
|
|
}
|
|
|
|
type EncWriter interface {
|
|
SendVia(via *HostInfo, relay *Relay, ad, nb, out []byte, nocopy bool, q int)
|
|
SendMessageToVpnAddr(t header.MessageType, st header.MessageSubType, vpnAddr netip.Addr, p, nb, out []byte)
|
|
SendMessageToHostInfo(t header.MessageType, st header.MessageSubType, hostinfo *HostInfo, p, nb, out []byte)
|
|
Handshake(vpnAddr netip.Addr)
|
|
GetHostInfo(vpnAddr netip.Addr) *HostInfo
|
|
GetCertState() *CertState
|
|
}
|
|
|
|
type recvErrorConfig uint8
|
|
|
|
const (
|
|
recvErrorAlways recvErrorConfig = iota
|
|
recvErrorNever
|
|
recvErrorPrivate
|
|
)
|
|
|
|
func (s recvErrorConfig) ShouldRecvError(endpoint netip.AddrPort) bool {
|
|
switch s {
|
|
case recvErrorPrivate:
|
|
return endpoint.Addr().IsPrivate()
|
|
case recvErrorAlways:
|
|
return true
|
|
case recvErrorNever:
|
|
return false
|
|
default:
|
|
panic(fmt.Errorf("invalid recvErrorConfig value: %d", s))
|
|
}
|
|
}
|
|
|
|
func (s recvErrorConfig) String() string {
|
|
switch s {
|
|
case recvErrorAlways:
|
|
return "always"
|
|
case recvErrorNever:
|
|
return "never"
|
|
case recvErrorPrivate:
|
|
return "private"
|
|
default:
|
|
return fmt.Sprintf("invalid(%d)", s)
|
|
}
|
|
}
|
|
|
|
func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
|
|
if c.Outside == nil {
|
|
return nil, errors.New("no outside connection")
|
|
}
|
|
if c.Inside == nil {
|
|
return nil, errors.New("no inside interface (tun)")
|
|
}
|
|
if c.pki == nil {
|
|
return nil, errors.New("no certificate state")
|
|
}
|
|
if c.Firewall == nil {
|
|
return nil, errors.New("no firewall rules")
|
|
}
|
|
if c.connectionManager == nil {
|
|
return nil, errors.New("no connection manager")
|
|
}
|
|
|
|
udpTxErrLimiter, err := ratelimit.New(1, time.Second)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
udpRelayTxErrLimiter, err := ratelimit.New(1, time.Second)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
txOversizeLimiter, err := ratelimit.New(1, time.Second)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if c.routines <= 1 {
|
|
c.PinThreads = false //pinning is not useful unless there's more than one tun reader
|
|
}
|
|
|
|
cs := c.pki.getCertState()
|
|
ifce := &Interface{
|
|
ctx: ctx,
|
|
pki: c.pki,
|
|
hostMap: c.HostMap,
|
|
outside: c.Outside,
|
|
inside: c.Inside,
|
|
firewall: c.Firewall,
|
|
dnsServer: c.DnsServer,
|
|
handshakeManager: c.HandshakeManager,
|
|
createTime: time.Now(),
|
|
lightHouse: c.lightHouse,
|
|
dropLocalBroadcast: c.DropLocalBroadcast,
|
|
dropMulticast: c.DropMulticast,
|
|
routines: c.routines,
|
|
version: c.version,
|
|
writers: make([]udp.Conn, c.routines),
|
|
batchers: make([]*batch.MultiCoalescer, c.routines),
|
|
myVpnNetworks: cs.myVpnNetworks,
|
|
myVpnNetworksTable: cs.myVpnNetworksTable,
|
|
myVpnAddrs: cs.myVpnAddrs,
|
|
myVpnAddrsTable: cs.myVpnAddrsTable,
|
|
myBroadcastAddrsTable: cs.myVpnBroadcastAddrsTable,
|
|
relayManager: c.relayManager,
|
|
connectionManager: c.connectionManager,
|
|
conntrackCacheTimeout: c.ConntrackCacheTimeout,
|
|
cpuAffinity: c.CpuAffinity,
|
|
defaultCpuAffinity: c.defaultCpuAffinity,
|
|
pinThreads: c.PinThreads,
|
|
staged: c.staged,
|
|
udpTxErrLimiter: udpTxErrLimiter,
|
|
udpRelayTxErrLimiter: udpRelayTxErrLimiter,
|
|
txOversizeLimiter: txOversizeLimiter,
|
|
|
|
metricHandshakes: metrics.GetOrRegisterHistogram("handshakes", nil, metrics.NewExpDecaySample(1028, 0.015)),
|
|
metricTxDropped: metrics.GetOrRegisterCounter("udp.tx.dropped", nil),
|
|
messageMetrics: c.MessageMetrics,
|
|
cachedPacketMetrics: &cachedPacketMetrics{
|
|
sent: metrics.GetOrRegisterCounter("hostinfo.cached_packets.sent", nil),
|
|
dropped: metrics.GetOrRegisterCounter("hostinfo.cached_packets.dropped", nil),
|
|
},
|
|
|
|
l: c.l,
|
|
}
|
|
|
|
ifce.tryPromoteEvery.Store(c.tryPromoteEvery)
|
|
ifce.reQueryEvery.Store(c.reQueryEvery)
|
|
ifce.reQueryWait.Store(int64(c.reQueryWait))
|
|
|
|
ifce.connectionManager.intf = ifce
|
|
|
|
// Held until Close so waiting on the interface blocks until the resources are actually released
|
|
ifce.wg.Add(1)
|
|
|
|
return ifce, nil
|
|
}
|
|
|
|
// activate creates the interface on the host. After the interface is created, any
|
|
// other services that want to bind listeners to its IP may do so successfully. However,
|
|
// the interface isn't going to process anything until run() is called.
|
|
func (f *Interface) activate() error {
|
|
// actually turn on tun dev
|
|
|
|
addr, err := f.outside.LocalAddr()
|
|
if err != nil {
|
|
f.l.Error("Failed to get udp listen address", "error", err)
|
|
}
|
|
|
|
f.l.Info("Nebula interface is active",
|
|
"interface", f.inside.Name(),
|
|
"networks", f.myVpnNetworks,
|
|
"build", f.version,
|
|
"udpAddr", addr,
|
|
"boringcrypto", boringEnabled(),
|
|
"fips140Version", fips140.Version(),
|
|
"fips140Enabled", fips140.Enabled(),
|
|
"fips140Enforced", fips140.Enforced(),
|
|
)
|
|
|
|
if f.routines > 1 && !f.outside.SupportsMultipleReaders() {
|
|
f.routines = 1
|
|
f.l.Warn("multiple udp readers are 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 {
|
|
// TODO: this clamp is only safe because it is unreachable when the
|
|
// udp side has multiple readers (linux Queues opens exactly n or
|
|
// errors; every other platform already clamped routines to 1 above).
|
|
// If a platform ever returns fewer queues than routines with
|
|
// SO_REUSEPORT sockets already bound, the surplus sockets get no
|
|
// listenOut and the kernel blackholes every flow it hashes to them —
|
|
// fail loudly or close the extra sockets instead.
|
|
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))
|
|
|
|
for i := range f.queues {
|
|
f.batchers[i] = batch.NewMultiCoalescer(f.queues[i], f.l)
|
|
}
|
|
|
|
// On error the caller owns the cleanup, Control.Start cancels the service context
|
|
// before releasing our resources so a waiter never observes a live context
|
|
if err = f.inside.Activate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *Interface) run() {
|
|
// Launch n queues to read packets from udp
|
|
for i := 0; i < f.routines; i++ {
|
|
f.wg.Go(func() {
|
|
f.listenOut(i)
|
|
})
|
|
}
|
|
|
|
// Launch n queues to read packets from tun dev
|
|
for i := 0; i < f.routines; i++ {
|
|
f.wg.Go(func() {
|
|
f.listenIn(f.queues[i], i)
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func (f *Interface) wait() error {
|
|
f.wg.Wait()
|
|
if e := f.fatalErr.Load(); e != nil {
|
|
return *e
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// onFatal stores the first fatal reader error, and calls triggerShutdown if it was the first one
|
|
func (f *Interface) onFatal(err error) {
|
|
swapped := f.fatalErr.CompareAndSwap(nil, &err)
|
|
if !swapped {
|
|
return
|
|
}
|
|
if f.triggerShutdown != nil {
|
|
f.triggerShutdown()
|
|
}
|
|
}
|
|
|
|
type rxContext struct {
|
|
q int
|
|
scratch []byte
|
|
// nb is a re-usable nonce buffer for decrypt calls to use
|
|
nb []byte
|
|
h *header.H
|
|
fwPacket *firewall.ParsedPacket
|
|
hostmapCache map[uint32]*HostInfo
|
|
lhh *LightHouseHandler
|
|
ctCache *firewall.ConntrackCacheTicker
|
|
}
|
|
|
|
func newRxContext(f *Interface, q int) *rxContext {
|
|
return &rxContext{
|
|
q: q,
|
|
scratch: make([]byte, mtu),
|
|
nb: make([]byte, 12, 12),
|
|
h: &header.H{},
|
|
fwPacket: &firewall.ParsedPacket{},
|
|
hostmapCache: map[uint32]*HostInfo{},
|
|
lhh: f.lightHouse.NewRequestHandler(),
|
|
ctCache: firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout),
|
|
}
|
|
}
|
|
|
|
func (f *Interface) listenOut(i int) {
|
|
var li udp.Conn
|
|
if i > 0 {
|
|
li = f.writers[i]
|
|
} else {
|
|
li = f.outside
|
|
}
|
|
|
|
rxc := newRxContext(f, i)
|
|
|
|
listener := func(fromUdpAddr netip.AddrPort, payload []byte) {
|
|
f.readOutsidePackets(ViaSender{UdpAddr: fromUdpAddr}, payload, rxc)
|
|
}
|
|
|
|
flusher := func() { f.flushOutside(rxc) }
|
|
|
|
var err error
|
|
if hc, ok := li.(handoffConn); ok && f.staged.rx {
|
|
// Every stage on its own goroutine, see flow_rx.go. rxc becomes the ordered writer's context.
|
|
err = newStagedRx(f, i, rxc).listen(hc)
|
|
} else {
|
|
if f.staged.rx {
|
|
f.l.Info("udp conn cannot hand off receive batches, receive stays inline", "reader", i)
|
|
}
|
|
err = li.ListenOut(listener, flusher)
|
|
}
|
|
|
|
// An error after teardown began is shutdown noise, the closed flag covers resources
|
|
// Close releases itself and the cancelled ctx covers ones torn down by their owners
|
|
// reacting to it, like the user device pipes
|
|
if err != nil && !f.closed.Load() && f.ctx.Err() == nil {
|
|
f.l.Error("Error while reading inbound packet, closing", "error", err)
|
|
f.onFatal(err)
|
|
}
|
|
|
|
f.l.Debug("underlay reader is done", "reader", i)
|
|
}
|
|
|
|
// threadCPU is the CPU tun reader i pins to.
|
|
func (f *Interface) threadCPU(i int) int {
|
|
if n := len(f.cpuAffinity); n > 0 {
|
|
// Explicit tun.cpu_affinity list wins; parseCpuAffinity already
|
|
// validated the entries against the allowed CPU set.
|
|
return f.cpuAffinity[i%n]
|
|
} else if n := len(f.defaultCpuAffinity); n > 0 {
|
|
return f.defaultCpuAffinity[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.
|
|
return allowed[i%len(allowed)]
|
|
}
|
|
return i % runtime.NumCPU()
|
|
}
|
|
|
|
func (f *Interface) pinThreadToCPU(i, cpu int) {
|
|
if err := util.PinThreadToCPU(cpu); err != nil {
|
|
f.l.Warn("failed to pin tun processing thread to CPU", "queue", i, "cpu", cpu, "err", err)
|
|
}
|
|
}
|
|
|
|
func (f *Interface) listenIn(queue tio.Queue, i int) {
|
|
rejectBuf := make([]byte, mtu)
|
|
arenaSize := batch.SendBatchCap * (udp.MTU + 32)
|
|
sb := batch.NewSendBatch(f.writers[i], batch.SendBatchCap, arenaSize)
|
|
fwPacket := &firewall.ParsedPacket{}
|
|
nb := make([]byte, 12, 12)
|
|
|
|
conntrackCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout)
|
|
|
|
if sq, ok := queue.(handoffQueue); ok && f.staged.tx {
|
|
// Every stage on its own goroutine, see flow_tx.go
|
|
err := newStagedTx(f, sq, i).run(fwPacket, rejectBuf, conntrackCache)
|
|
if !f.closed.Load() && f.ctx.Err() == nil {
|
|
f.l.Error("Error while reading outbound packet, closing", "error", err, "reader", i)
|
|
f.onFatal(err)
|
|
}
|
|
f.l.Debug("overlay reader is done", "reader", i)
|
|
return
|
|
}
|
|
if f.staged.tx {
|
|
f.l.Info("tun queue cannot hand off read batches, transmit stays inline", "reader", i)
|
|
}
|
|
|
|
// Pinning this thread (and goroutine) to a single CPU keeps every sendmmsg from this goroutine going through the
|
|
// same TX ring on the nic, so the wire sees per-flow order. Skip entirely when tun.pin_threads is false.
|
|
if f.pinThreads {
|
|
f.pinThreadToCPU(i, f.threadCPU(i))
|
|
}
|
|
|
|
for {
|
|
pkts, err := queue.Read()
|
|
if err != nil {
|
|
// Same shutdown noise handling as listenOut
|
|
if !f.closed.Load() && f.ctx.Err() == nil {
|
|
f.l.Error("Error while reading outbound packet, closing", "error", err, "reader", i)
|
|
f.onFatal(err)
|
|
}
|
|
break
|
|
}
|
|
|
|
for _, pkt := range pkts {
|
|
f.consumeInsidePacket(pkt, fwPacket, nb, sb, rejectBuf, i, conntrackCache.Get())
|
|
// Flush incrementally once a full sendmmsg batch has
|
|
// accumulated so the first packets of a deep read drain
|
|
// hit the wire while the rest are still being encrypted.
|
|
if sb.Len() >= batch.SendBatchCap {
|
|
f.flushSendBatch(sb, i)
|
|
}
|
|
}
|
|
f.flushSendBatch(sb, i)
|
|
}
|
|
|
|
f.l.Debug("overlay reader is done", "reader", i)
|
|
}
|
|
|
|
// flushSendBatch drains sb to the underlay and accounts for anything it could not deliver. A shortfall means
|
|
// specific destinations were undeliverable (a stale remote, a reject rule), which the backend logs per peer at
|
|
// debug; here it is only a counter, so one unreachable peer cannot spam a log line per batch.
|
|
func (f *Interface) flushSendBatch(sb *batch.SendBatch, q int) {
|
|
queued := sb.Len()
|
|
written, err := sb.Flush()
|
|
if err != nil {
|
|
f.l.Error("Failed to write outgoing batch", "error", err, "writer", q)
|
|
}
|
|
if dropped := queued - written; dropped > 0 {
|
|
f.metricTxDropped.Inc(int64(dropped))
|
|
}
|
|
}
|
|
|
|
func (f *Interface) RegisterConfigChangeCallbacks(c *config.C) {
|
|
c.RegisterReloadCallback(f.reloadFirewall)
|
|
for _, reload := range []func(*config.C){
|
|
f.reloadSendRecvError, f.reloadAcceptRecvError, f.reloadDisconnectInvalid, f.reloadMisc,
|
|
} {
|
|
c.RegisterReloadCallback(func(c *config.C, _ *config.Reload) { reload(c) })
|
|
}
|
|
|
|
for _, udpConn := range f.writers {
|
|
c.RegisterReloadCallback(udpConn.ReloadConfig)
|
|
}
|
|
}
|
|
|
|
func (f *Interface) reloadDisconnectInvalid(c *config.C) {
|
|
initial := c.InitialLoad()
|
|
if initial || c.HasChanged("pki.disconnect_invalid") {
|
|
f.disconnectInvalid.Store(c.GetBool("pki.disconnect_invalid", true))
|
|
if !initial {
|
|
f.l.Info("pki.disconnect_invalid changed", "value", f.disconnectInvalid.Load())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *Interface) reloadFirewall(c *config.C, r *config.Reload) {
|
|
cs := f.pki.getCertState()
|
|
curCert := cs.getCertificate(cert.Version2)
|
|
if curCert == nil {
|
|
curCert = cs.getCertificate(cert.Version1)
|
|
}
|
|
|
|
// The firewall builds its routableNetworks set from the certificate's UnsafeNetworks at construction.
|
|
// Check to see if that set has changed, and if so, rebuild the firewall.
|
|
certUnsafeChanged := curCert != nil && !slices.Equal(curCert.UnsafeNetworks(), f.firewall.unsafeNetworks)
|
|
|
|
if !c.HasChanged("firewall") && !certUnsafeChanged {
|
|
f.l.Debug("No firewall config change detected")
|
|
return
|
|
}
|
|
|
|
fw, err := NewFirewallFromConfig(f.l, cs, c)
|
|
if err != nil {
|
|
f.l.Error("Error while creating firewall during reload", "error", err)
|
|
r.Failed()
|
|
return
|
|
}
|
|
|
|
oldFw := f.firewall
|
|
conntrack := oldFw.Conntrack
|
|
conntrack.Lock()
|
|
defer conntrack.Unlock()
|
|
|
|
fw.rulesVersion = oldFw.rulesVersion + 1
|
|
// If rulesVersion is back to zero, we have wrapped all the way around. Be
|
|
// safe and just reset conntrack in this case.
|
|
if fw.rulesVersion == 0 {
|
|
f.l.Warn("firewall rulesVersion has overflowed, resetting conntrack",
|
|
"firewallHashes", fw.GetRuleHashes(),
|
|
"oldFirewallHashes", oldFw.GetRuleHashes(),
|
|
"rulesVersion", fw.rulesVersion,
|
|
)
|
|
} else {
|
|
fw.Conntrack = conntrack
|
|
}
|
|
|
|
f.firewall = fw
|
|
|
|
oldFw.Destroy()
|
|
f.l.Info("New firewall has been installed",
|
|
"firewallHashes", fw.GetRuleHashes(),
|
|
"oldFirewallHashes", oldFw.GetRuleHashes(),
|
|
"rulesVersion", fw.rulesVersion,
|
|
)
|
|
}
|
|
|
|
func (f *Interface) reloadSendRecvError(c *config.C) {
|
|
if c.InitialLoad() || c.HasChanged("listen.send_recv_error") {
|
|
stringValue := c.GetString("listen.send_recv_error", "always")
|
|
|
|
switch stringValue {
|
|
case "always":
|
|
f.sendRecvErrorConfig = recvErrorAlways
|
|
case "never":
|
|
f.sendRecvErrorConfig = recvErrorNever
|
|
case "private":
|
|
f.sendRecvErrorConfig = recvErrorPrivate
|
|
default:
|
|
if c.GetBool("listen.send_recv_error", true) {
|
|
f.sendRecvErrorConfig = recvErrorAlways
|
|
} else {
|
|
f.sendRecvErrorConfig = recvErrorNever
|
|
}
|
|
}
|
|
|
|
f.l.Info("Loaded send_recv_error config", "sendRecvError", f.sendRecvErrorConfig.String())
|
|
}
|
|
}
|
|
|
|
func (f *Interface) reloadAcceptRecvError(c *config.C) {
|
|
if c.InitialLoad() || c.HasChanged("listen.accept_recv_error") {
|
|
stringValue := c.GetString("listen.accept_recv_error", "always")
|
|
|
|
switch stringValue {
|
|
case "always":
|
|
f.acceptRecvErrorConfig = recvErrorAlways
|
|
case "never":
|
|
f.acceptRecvErrorConfig = recvErrorNever
|
|
case "private":
|
|
f.acceptRecvErrorConfig = recvErrorPrivate
|
|
default:
|
|
if c.GetBool("listen.accept_recv_error", true) {
|
|
f.acceptRecvErrorConfig = recvErrorAlways
|
|
} else {
|
|
f.acceptRecvErrorConfig = recvErrorNever
|
|
}
|
|
}
|
|
|
|
f.l.Info("Loaded accept_recv_error config", "acceptRecvError", f.acceptRecvErrorConfig.String())
|
|
}
|
|
}
|
|
|
|
func (f *Interface) reloadMisc(c *config.C) {
|
|
if c.HasChanged("counters.try_promote") {
|
|
n := c.GetUint32("counters.try_promote", defaultPromoteEvery)
|
|
f.tryPromoteEvery.Store(n)
|
|
f.l.Info("counters.try_promote has changed")
|
|
}
|
|
|
|
if c.HasChanged("counters.requery_every_packets") {
|
|
n := c.GetUint32("counters.requery_every_packets", defaultReQueryEvery)
|
|
f.reQueryEvery.Store(n)
|
|
f.l.Info("counters.requery_every_packets has changed")
|
|
}
|
|
|
|
if c.HasChanged("timers.requery_wait_duration") {
|
|
n := c.GetDuration("timers.requery_wait_duration", defaultReQueryWait)
|
|
f.reQueryWait.Store(int64(n))
|
|
f.l.Info("timers.requery_wait_duration has changed")
|
|
}
|
|
}
|
|
|
|
// statsEmitter updates the interface's gauges, the stats server runs it on its capture loop.
|
|
func (f *Interface) statsEmitter() func() {
|
|
udpStats := udp.NewUDPStatsEmitter(f.writers)
|
|
|
|
certExpirationGauge := metrics.GetOrRegisterGauge("certificate.ttl_seconds", nil)
|
|
certInitiatingVersion := metrics.GetOrRegisterGauge("certificate.initiating_version", nil)
|
|
certMaxVersion := metrics.GetOrRegisterGauge("certificate.max_version", nil)
|
|
|
|
return func() {
|
|
f.firewall.EmitStats()
|
|
f.handshakeManager.EmitStats()
|
|
udpStats()
|
|
|
|
certState := f.pki.getCertState()
|
|
defaultCrt := certState.GetDefaultCertificate()
|
|
certExpirationGauge.Update(int64(defaultCrt.NotAfter().Sub(time.Now()) / time.Second))
|
|
certInitiatingVersion.Update(int64(defaultCrt.Version()))
|
|
|
|
// Report the max certificate version we are capable of using
|
|
if certState.v2Cert != nil {
|
|
certMaxVersion.Update(int64(certState.v2Cert.Version()))
|
|
} else {
|
|
certMaxVersion.Update(int64(certState.v1Cert.Version()))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *Interface) GetHostInfo(vpnIp netip.Addr) *HostInfo {
|
|
return f.hostMap.QueryVpnAddr(vpnIp)
|
|
}
|
|
|
|
func (f *Interface) GetCertState() *CertState {
|
|
return f.pki.getCertState()
|
|
}
|
|
|
|
// Close releases the interface's resources: the udp sockets and the tun device.
|
|
// It is idempotent and safe to call at any point in the lifecycle, including on an interface that never activated,
|
|
// calls after the first return nil without doing anything.
|
|
func (f *Interface) Close() error {
|
|
if !f.closed.CompareAndSwap(false, true) {
|
|
return nil
|
|
}
|
|
|
|
var errs []error
|
|
|
|
// Release the udp readers
|
|
for i, u := range f.writers {
|
|
err := u.Close()
|
|
if err != nil {
|
|
f.l.Error("Error while closing udp socket", "error", err, "writer", i)
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
|
|
// Release the tun device (closing the tun also closes all readers)
|
|
closeErr := f.inside.Close()
|
|
if closeErr != nil {
|
|
errs = append(errs, closeErr)
|
|
}
|
|
|
|
// Release the construction token so waiters know the resources are gone
|
|
f.wg.Done()
|
|
return errors.Join(errs...)
|
|
}
|