| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 |
- package nebula
- import (
- "context"
- "fmt"
- "log/slog"
- "net"
- "net/netip"
- "os"
- "runtime/debug"
- "slices"
- "strings"
- "time"
- "github.com/slackhq/nebula/config"
- "github.com/slackhq/nebula/cpupick"
- "github.com/slackhq/nebula/noiseutil"
- "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
- // maxRoutines caps routines below the RejectHeadroom nonce gap so concurrent senders can't race the counter past wrap.
- const maxRoutines = 1 << 16
- // The reject headroom must exceed every sender that can be mid-reservation at once, about two per routine.
- const _ = noiseutil.RejectHeadroom - 4*maxRoutines
- 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())
- // The goroutines started below stop only when this context does, and only a caller holding the
- // Control can arrange that. Cancel whenever we are not handing one back, which covers an error
- // and a config test alike: a config test used to leave the lighthouse query worker, and a
- // hostname resolver per dns named static host, running for the life of the process.
- defer func() {
- if retcon == 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
- }
- } 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)
- }
- }
- if routines > maxRoutines {
- l.Warn("Using multiple routines", "routines", maxRoutines, "clamped", true, "requestedRoutines", routines)
- routines = maxRoutines
- } else if routines > 1 {
- l.Info("Using multiple routines", "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)
- // 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()
- }
- for i := 0; i < routines; i++ {
- listen := netip.AddrPortFrom(listenHost, uint16(port))
- l.Info("listening", "addr", listen)
- batchSize := c.GetInt("listen.batch", 64)
- if batchSize < 1 {
- oldBatch := batchSize
- batchSize = 1
- l.Warn("listen.batch size is invalid", "provided", oldBatch, "overridden to", batchSize)
- }
- udpSettings := udp.Settings{
- Listen: listen,
- Multi: routines > 1,
- Batch: batchSize,
- Offloads: c.GetBool("listen.udp_offloads", false),
- }
- udpServer, err := udp.NewListener(l, udpSettings)
- if err != nil {
- return nil, util.NewContextualError("Failed to open udp listener", m{"queue": i}, err)
- }
- 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())
- }
- }
- }
- 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,
- }
- 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)
- }
- pinThreads := c.GetBool("tun.pin_threads", true)
- cpuAffinity := parseCpuAffinity(c, l, routines)
- staged := parseStagedConfig(c, l)
- if pinThreads && routines > 1 && len(cpuAffinity) == 0 && !configTest {
- // The operator didn't choose pin CPUs, so pick a default set that
- // prefers performance cores and doesn't stack co-located instances
- // onto allowed[0].
- // key is used to seed the spreading of routines->cores.
- // use PID if you want to ensure many different Nebulas in VMs or containers land on different cores
- // use port if you want to always end up on the same cores, ideal for benchmarking.
- key := uint64(os.Getpid()) //default to PID
- pinKeyStr := strings.ToLower(c.GetString("tun.pin_threads_key", ""))
- switch pinKeyStr {
- case "":
- l.Debug("tun.pin_threads_key is empty, using PID")
- case "pid":
- l.Debug("tun.pin_threads_key is PID")
- case "port":
- if ap, err := udpConns[0].LocalAddr(); err == nil && ap.Port() != 0 {
- l.Info("tun.pin_threads_key is port number")
- key = uint64(ap.Port())
- } else {
- l.Warn("Failed to get a port number for tun.pin_threads_key, falling back to PID", "err", err)
- }
- default:
- l.Warn("tun.pin_threads_key is invalid, using PID")
- }
- cpuAffinity = cpupick.Default(routines, key, l)
- }
- 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,
- MessageMetrics: messageMetrics,
- version: buildVersion,
- relayManager: NewRelayManager(ctx, l, hostMap, c),
- punchy: punchy,
- ConntrackCacheTimeout: conntrackCacheTimeout,
- CpuAffinity: cpuAffinity,
- PinThreads: pinThreads,
- staged: staged,
- l: l,
- }
- var ifce *Interface
- var emitStats func()
- 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)
- handshakeManager.f = ifce
- go handshakeManager.Run(ctx)
- punchy.Start(ctx, ifce, hostMap, lightHouse)
- // On the stats server's loop, so the gauges follow the interval it accepted
- emitStats = ifce.statsEmitter()
- }
- stats, err := newStatsServerFromConfig(ctx, l, c, buildVersion, configTest, emitStats)
- if err != nil {
- return nil, util.ContextualizeIfNeeded("Failed to start stats emitter", err)
- }
- if configTest {
- return nil, nil
- }
- attachCommands(l, c, ssh, ifce)
- networkChanges := udp.NewNetworkChangeMonitor(ctx, l, c)
- return &Control{
- state: StateReady,
- f: ifce,
- l: l,
- ctx: ctx,
- cancel: cancel,
- sshStart: sshStart,
- statsStart: stats.Start,
- dnsStart: ds.Start,
- lighthouseStart: lightHouse.StartUpdateWorker,
- networkChangeStart: networkChanges.Start,
- 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 ""
- }
|