mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 07:56:59 +02:00
913a37cfee
Device loses io.ReadWriteCloser + NewMultiQueueReader in favor of Queues(n), which returns up to n tio.Queue objects; platforms without multiqueue hand back their single queue and the interface sizes its reader routines to what it actually got. Queue.Read returns a batch of borrowed packets (single-element for every current backend) so a future backend can deliver more than one packet per syscall without another interface change. The Linux poll/eventfd machinery moves out of tun_linux.go into the new overlay/tio package: nonblocking fds, a shared shutdown eventfd owned by the queue set, and pollfd arrays built on the stack so concurrent writers parked in blockOnWrite no longer share Revents storage. Other platforms wrap their existing one-datagram Read/Write in a singleQueue adapter that owns a private scratch buffer, so multiqueue-by-sharing devices (user, disabled) no longer race concurrent readers on one buffer. This is the tun-interface subset of better-tun-interface-ordering, extracted at 18dc13b with none of the GSO/GRO offload mechanics and no udp/sendmmsg changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
583 lines
16 KiB
Go
583 lines
16 KiB
Go
package nebula
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/netip"
|
|
"slices"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/gaissmai/bart"
|
|
"github.com/rcrowley/go-metrics"
|
|
|
|
"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/tio"
|
|
"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
|
|
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
|
|
relayManager *relayManager
|
|
|
|
tryPromoteEvery atomic.Uint32
|
|
reQueryEvery atomic.Uint32
|
|
reQueryWait atomic.Int64
|
|
|
|
sendRecvErrorConfig recvErrorConfig
|
|
acceptRecvErrorConfig recvErrorConfig
|
|
|
|
// rebindCount is used to decide if an active tunnel should trigger a punch notification through a lighthouse
|
|
rebindCount int8
|
|
version string
|
|
|
|
conntrackCacheTimeout time.Duration
|
|
|
|
ctx context.Context
|
|
writers []udp.Conn
|
|
queues []tio.Queue
|
|
wg sync.WaitGroup
|
|
|
|
// 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
|
|
|
|
l *slog.Logger
|
|
}
|
|
|
|
type EncWriter interface {
|
|
SendVia(via *HostInfo,
|
|
relay *Relay,
|
|
ad,
|
|
nb,
|
|
out []byte,
|
|
nocopy bool,
|
|
)
|
|
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")
|
|
}
|
|
|
|
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),
|
|
myVpnNetworks: cs.myVpnNetworks,
|
|
myVpnNetworksTable: cs.myVpnNetworksTable,
|
|
myVpnAddrs: cs.myVpnAddrs,
|
|
myVpnAddrsTable: cs.myVpnAddrsTable,
|
|
myBroadcastAddrsTable: cs.myVpnBroadcastAddrsTable,
|
|
relayManager: c.relayManager,
|
|
connectionManager: c.connectionManager,
|
|
conntrackCacheTimeout: c.ConntrackCacheTimeout,
|
|
|
|
metricHandshakes: metrics.GetOrRegisterHistogram("handshakes", nil, metrics.NewExpDecaySample(1028, 0.015)),
|
|
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(),
|
|
)
|
|
|
|
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 {
|
|
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))
|
|
|
|
// 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()
|
|
}
|
|
}
|
|
|
|
func (f *Interface) listenOut(i int) {
|
|
var li udp.Conn
|
|
if i > 0 {
|
|
li = f.writers[i]
|
|
} else {
|
|
li = f.outside
|
|
}
|
|
|
|
ctCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout)
|
|
lhh := f.lightHouse.NewRequestHandler()
|
|
plaintext := make([]byte, udp.MTU)
|
|
h := &header.H{}
|
|
fwPacket := &firewall.Packet{}
|
|
nb := make([]byte, 12, 12)
|
|
|
|
err := li.ListenOut(func(fromUdpAddr netip.AddrPort, payload []byte) {
|
|
f.readOutsidePackets(ViaSender{UdpAddr: fromUdpAddr}, plaintext[:0], payload, h, fwPacket, lhh, nb, i, ctCache.Get())
|
|
})
|
|
|
|
// 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)
|
|
}
|
|
|
|
func (f *Interface) listenIn(queue tio.Queue, i int) {
|
|
out := make([]byte, mtu)
|
|
fwPacket := &firewall.Packet{}
|
|
nb := make([]byte, 12, 12)
|
|
|
|
conntrackCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout)
|
|
|
|
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 {
|
|
// 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)
|
|
}
|
|
|
|
func (f *Interface) RegisterConfigChangeCallbacks(c *config.C) {
|
|
c.RegisterReloadCallback(f.reloadFirewall)
|
|
c.RegisterReloadCallback(f.reloadSendRecvError)
|
|
c.RegisterReloadCallback(f.reloadAcceptRecvError)
|
|
c.RegisterReloadCallback(f.reloadDisconnectInvalid)
|
|
c.RegisterReloadCallback(f.reloadMisc)
|
|
|
|
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) {
|
|
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)
|
|
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")
|
|
}
|
|
}
|
|
|
|
func (f *Interface) emitStats(ctx context.Context, i time.Duration) {
|
|
ticker := time.NewTicker(i)
|
|
defer ticker.Stop()
|
|
|
|
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)
|
|
|
|
emit := 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()))
|
|
}
|
|
}
|
|
|
|
// Prime gauges so a Prometheus scrape that lands before the first tick
|
|
// sees real values instead of the zero defaults (issue #907).
|
|
emit()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
emit()
|
|
}
|
|
}
|
|
}
|
|
|
|
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...)
|
|
}
|