| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450 |
- package nebula
- import (
- "context"
- "fmt"
- "log/slog"
- "math"
- "net"
- "net/netip"
- "runtime/debug"
- "slices"
- "strings"
- "time"
- "github.com/slackhq/nebula/config"
- "github.com/slackhq/nebula/overlay"
- "github.com/slackhq/nebula/sshd"
- "github.com/slackhq/nebula/udp"
- "github.com/slackhq/nebula/util"
- "go.yaml.in/yaml/v3"
- )
- type m = map[string]any
- func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, deviceFactory overlay.DeviceFactory) (retcon *Control, reterr error) {
- ctx, cancel := context.WithCancel(context.Background())
- // Automatically cancel the context if Main returns an error, to signal all created goroutines to quit.
- defer func() {
- if reterr != nil {
- cancel()
- }
- }()
- if buildVersion == "" {
- buildVersion = moduleVersion()
- }
- // Debug builds (-tags debug) serve pprof on :6060; a no-op otherwise.
- startPprofServer(ctx, l)
- // Print the config if in test, the exit comes later
- if configTest {
- b, err := yaml.Marshal(c.Settings)
- if err != nil {
- return nil, err
- }
- // Print the final config
- l.Info(string(b))
- }
- pki, err := NewPKIFromConfig(l, c)
- if err != nil {
- return nil, util.ContextualizeIfNeeded("Failed to load PKI from config", err)
- }
- fw, err := NewFirewallFromConfig(l, pki.getCertState(), c)
- if err != nil {
- return nil, util.ContextualizeIfNeeded("Error while loading firewall rules", err)
- }
- l.Info("Firewall started", "firewallHashes", fw.GetRuleHashes())
- ssh, err := sshd.NewSSHServer(ctx, l.With("subsystem", "sshd"))
- if err != nil {
- return nil, util.ContextualizeIfNeeded("Error while creating SSH server", err)
- }
- wireSSHReload(l, ssh, c)
- var sshStart func()
- if c.GetBool("sshd.enabled", false) {
- sshStart, err = configSSH(l, ssh, c)
- if err != nil {
- l.Warn("Failed to configure sshd, ssh debugging will not be available", "error", err)
- sshStart = nil
- }
- }
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // All non system modifying configuration consumption should live above this line
- // tun config, listeners, anything modifying the computer should be below
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- var routines int
- // If `routines` is set, use that and ignore the specific values
- if routines = c.GetInt("routines", 0); routines != 0 {
- if routines < 1 {
- routines = 1
- }
- if routines > 1 {
- l.Info("Using multiple routines", "routines", routines)
- }
- } else {
- // deprecated and undocumented
- tunQueues := c.GetInt("tun.routines", 1)
- udpQueues := c.GetInt("listen.routines", 1)
- routines = max(tunQueues, udpQueues)
- if routines != 1 {
- l.Warn("Setting tun.routines and listen.routines is deprecated. Use `routines` instead", "routines", routines)
- }
- }
- // EXPERIMENTAL
- // Intentionally not documented yet while we do more testing and determine
- // a good default value.
- conntrackCacheTimeout := c.GetDuration("firewall.conntrack.routine_cache_timeout", 0)
- if routines > 1 && !c.IsSet("firewall.conntrack.routine_cache_timeout") {
- // Use a different default if we are running with multiple routines
- conntrackCacheTimeout = 1 * time.Second
- }
- if conntrackCacheTimeout > 0 {
- l.Info("Using routine-local conntrack cache", "duration", conntrackCacheTimeout)
- }
- var tun overlay.Device
- if !configTest {
- c.CatchHUP(ctx)
- if deviceFactory == nil {
- deviceFactory = overlay.NewDeviceFromConfig
- }
- tun, err = deviceFactory(c, l, pki.getCertState().myVpnNetworks, routines)
- if err != nil {
- return nil, util.ContextualizeIfNeeded("Failed to get a tun/tap device", err)
- }
- defer func() {
- if reterr != nil {
- tun.Close()
- }
- }()
- }
- // set up our UDP listener
- udpConns := make([]udp.Conn, routines)
- port := c.GetInt("listen.port", 0)
- // Multiport lanes: bind `routines` consecutive UDP ports (listen.port+i)
- // instead of SO_REUSEPORT sharing one, and negotiate one extra tunnel per
- // routine with capable peers so each routine's traffic rides its own
- // underlay 5-tuple. Defaults on, degrading gracefully when preconditions
- // aren't met — managed deployments (dnclient) can't be hard-errored on
- // config they don't control.
- multiport := c.GetBool("multiport.enabled", true)
- if multiport && routines < 2 {
- l.Info("multiport disabled: requires routines > 1")
- multiport = false
- }
- if multiport && port != 0 && port+routines-1 > math.MaxUint16 {
- l.Warn("multiport disabled: would bind ports beyond 65535", "listen.port", port, "routines", routines)
- multiport = false
- }
- // Callers get no handle to these until the Control is returned, release them on any error.
- defer func() {
- if reterr != nil {
- for _, u := range udpConns {
- if u != nil {
- _ = u.Close()
- }
- }
- }
- }()
- if !configTest {
- rawListenHost := c.GetString("listen.host", "0.0.0.0")
- var listenHost netip.Addr
- if rawListenHost == "[::]" {
- // Old guidance was to provide the literal `[::]` in `listen.host` but that won't resolve.
- listenHost = netip.IPv6Unspecified()
- } else {
- ips, err := net.DefaultResolver.LookupNetIP(context.Background(), "ip", rawListenHost)
- if err != nil {
- return nil, util.ContextualizeIfNeeded("Failed to resolve listen.host", err)
- }
- if len(ips) == 0 {
- return nil, util.ContextualizeIfNeeded("Failed to resolve listen.host", err)
- }
- listenHost = ips[0].Unmap()
- }
- // Every lane socket needs its own reader; a platform that can't run
- // multiple readers would silently strand sockets 1..n-1 as blackholes.
- // Probe capability before committing to per-port binds.
- if multiport {
- probe, err := udp.NewListener(l, listenHost, 0, false, 1)
- if err == nil {
- if !probe.SupportsMultipleReaders() {
- l.Warn("multiport disabled: this platform does not support multiple udp readers")
- multiport = false
- }
- _ = probe.Close()
- }
- }
- // With a dynamic listen.port, multiport binds socket 0 dynamically and
- // then claims the next routines-1 ports above it; if that range turns
- // out to be partially occupied, re-roll with a fresh dynamic port.
- dynamic := port == 0
- var bindErr error
- for attempt := 0; attempt < 6; attempt++ {
- bindErr = nil
- for i := 0; i < routines; i++ {
- lPort := port
- if multiport {
- lPort = port + i
- }
- udpServer, err := udp.NewListener(l, listenHost, lPort, routines > 1 && !multiport, c.GetInt("listen.batch", 64))
- if err != nil {
- bindErr = util.NewContextualError("Failed to open udp listener", m{"queue": i}, err)
- break
- }
- udpServer.ReloadConfig(c)
- udpConns[i] = udpServer
- // If port is dynamic, discover it before the next pass through the for loop
- // This way all routines will use the same port correctly
- if port == 0 {
- uPort, err := udpServer.LocalAddr()
- if err != nil {
- return nil, util.NewContextualError("Failed to get listening port", nil, err)
- }
- port = int(uPort.Port())
- if multiport && port+routines-1 > math.MaxUint16 {
- bindErr = util.NewContextualError("multiport dynamic port too close to 65535", m{"port": port}, nil)
- break
- }
- }
- bound := port
- if multiport {
- bound = port + i
- }
- l.Info("listening", "addr", netip.AddrPortFrom(listenHost, uint16(bound)), "socket", i)
- }
- if bindErr == nil {
- break
- }
- if !(multiport && dynamic) {
- return nil, bindErr
- }
- for i := range udpConns {
- if udpConns[i] != nil {
- _ = udpConns[i].Close()
- udpConns[i] = nil
- }
- }
- port = 0
- l.Debug("multiport dynamic port range collided, retrying", "attempt", attempt+1)
- }
- if bindErr != nil {
- return nil, bindErr
- }
- }
- hostMap := NewHostMapFromConfig(l, c)
- punchy := NewPunchyFromConfig(l, c, udpConns[0])
- connManager := newConnectionManagerFromConfig(l, c, hostMap, punchy)
- lightHouse, err := NewLightHouseFromConfig(ctx, l, c, pki.getCertState(), udpConns[0], punchy)
- if err != nil {
- return nil, util.ContextualizeIfNeeded("Failed to initialize lighthouse handler", err)
- }
- var messageMetrics *MessageMetrics
- if c.GetBool("stats.message_metrics", false) {
- messageMetrics = newMessageMetrics()
- } else {
- messageMetrics = newMessageMetricsOnlyRecvError()
- }
- handshakeConfig := HandshakeConfig{
- tryInterval: c.GetDuration("handshakes.try_interval", DefaultHandshakeTryInterval),
- retries: int64(c.GetInt("handshakes.retries", DefaultHandshakeRetries)),
- triggerBuffer: c.GetInt("handshakes.trigger_buffer", DefaultHandshakeTriggerBuffer),
- messageMetrics: messageMetrics,
- }
- if multiport {
- lanes := c.GetInt("multiport.lanes", 0)
- if lanes <= 0 || lanes > routines {
- lanes = routines
- }
- handshakeConfig.laneCount = lanes
- handshakeConfig.lanePortCount = uint16(routines)
- handshakeConfig.laneBasePort = uint16(port)
- }
- handshakeManager := NewHandshakeManager(l, hostMap, lightHouse, udpConns[0], handshakeConfig)
- lightHouse.handshakeTrigger = handshakeManager.trigger
- ds, err := newDnsServerFromConfig(ctx, l, pki, hostMap, c)
- if err != nil {
- l.Warn("Failed to start DNS responder", "error", err)
- }
- ifConfig := &InterfaceConfig{
- HostMap: hostMap,
- Inside: tun,
- Outside: udpConns[0],
- pki: pki,
- Firewall: fw,
- DnsServer: ds,
- HandshakeManager: handshakeManager,
- connectionManager: connManager,
- lightHouse: lightHouse,
- tryPromoteEvery: c.GetUint32("counters.try_promote", defaultPromoteEvery),
- reQueryEvery: c.GetUint32("counters.requery_every_packets", defaultReQueryEvery),
- reQueryWait: c.GetDuration("timers.requery_wait_duration", defaultReQueryWait),
- DropLocalBroadcast: c.GetBool("tun.drop_local_broadcast", false),
- DropMulticast: c.GetBool("tun.drop_multicast", false),
- routines: routines,
- Multiport: multiport,
- LaneCount: handshakeConfig.laneCount,
- MessageMetrics: messageMetrics,
- version: buildVersion,
- relayManager: NewRelayManager(ctx, l, hostMap, c),
- punchy: punchy,
- ConntrackCacheTimeout: conntrackCacheTimeout,
- CpuAffinity: parseCpuAffinity(c, l, routines),
- PinThreads: c.GetBool("tun.pin_threads", true),
- l: l,
- }
- var ifce *Interface
- if !configTest {
- ifce, err = NewInterface(ctx, ifConfig)
- if err != nil {
- return nil, fmt.Errorf("failed to initialize interface: %s", err)
- }
- ifce.writers = udpConns
- lightHouse.ifce = ifce
- ifce.RegisterConfigChangeCallbacks(c)
- ifce.reloadDisconnectInvalid(c)
- ifce.reloadSendRecvError(c)
- ifce.reloadAcceptRecvError(c)
- ifce.reloadEcn(c)
- handshakeManager.f = ifce
- go handshakeManager.Run(ctx)
- punchy.Start(ctx, ifce, hostMap, lightHouse)
- }
- stats, err := newStatsServerFromConfig(ctx, l, c, buildVersion, configTest)
- if err != nil {
- return nil, util.ContextualizeIfNeeded("Failed to start stats emitter", err)
- }
- if configTest {
- return nil, nil
- }
- go ifce.emitStats(ctx, c.GetDuration("stats.interval", time.Second*10))
- attachCommands(l, c, ssh, ifce)
- return &Control{
- state: StateReady,
- f: ifce,
- l: l,
- ctx: ctx,
- cancel: cancel,
- sshStart: sshStart,
- statsStart: stats.Start,
- dnsStart: ds.Start,
- lighthouseStart: lightHouse.StartUpdateWorker,
- connectionManagerStart: connManager.Start,
- }, 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 {
- info, ok := debug.ReadBuildInfo()
- if !ok {
- return ""
- }
- for _, dep := range info.Deps {
- if dep.Path == "github.com/slackhq/nebula" {
- return strings.TrimPrefix(dep.Version, "v")
- }
- }
- return ""
- }
|