main.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. package nebula
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "log/slog"
  7. "net"
  8. "net/netip"
  9. "os"
  10. "runtime/debug"
  11. "slices"
  12. "strings"
  13. "time"
  14. "github.com/slackhq/nebula/config"
  15. "github.com/slackhq/nebula/cpupick"
  16. "github.com/slackhq/nebula/noiseutil"
  17. "github.com/slackhq/nebula/overlay"
  18. "github.com/slackhq/nebula/sshd"
  19. "github.com/slackhq/nebula/udp"
  20. "github.com/slackhq/nebula/util"
  21. "go.yaml.in/yaml/v3"
  22. )
  23. type m = map[string]any
  24. // maxRoutines caps routines below the RejectHeadroom nonce gap so concurrent senders can't race the counter past wrap.
  25. const maxRoutines = 1 << 16
  26. // The reject headroom must exceed every sender that can be mid-reservation at once, about two per routine.
  27. const _ = noiseutil.RejectHeadroom - 4*maxRoutines
  28. func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, deviceFactory overlay.DeviceFactory) (retcon *Control, reterr error) {
  29. // Apply before starting services or selecting per-reader pin CPUs.
  30. if err := configureAvoidCPUs(c, l, configTest); err != nil {
  31. return nil, err
  32. }
  33. ctx, cancel := context.WithCancel(context.Background())
  34. // The goroutines started below stop only when this context does, and only a caller holding the
  35. // Control can arrange that. Cancel whenever we are not handing one back, which covers an error
  36. // and a config test alike: a config test used to leave the lighthouse query worker, and a
  37. // hostname resolver per dns named static host, running for the life of the process.
  38. defer func() {
  39. if retcon == nil {
  40. cancel()
  41. }
  42. }()
  43. if buildVersion == "" {
  44. buildVersion = moduleVersion()
  45. }
  46. // Debug builds (-tags debug) serve pprof on :6060; a no-op otherwise.
  47. startPprofServer(ctx, l)
  48. // Print the config if in test, the exit comes later
  49. if configTest {
  50. b, err := yaml.Marshal(c.Settings)
  51. if err != nil {
  52. return nil, err
  53. }
  54. // Print the final config
  55. l.Info(string(b))
  56. }
  57. pki, err := NewPKIFromConfig(l, c)
  58. if err != nil {
  59. return nil, util.ContextualizeIfNeeded("Failed to load PKI from config", err)
  60. }
  61. fw, err := NewFirewallFromConfig(l, pki.getCertState(), c)
  62. if err != nil {
  63. return nil, util.ContextualizeIfNeeded("Error while loading firewall rules", err)
  64. }
  65. l.Info("Firewall started", "firewallHashes", fw.GetRuleHashes())
  66. ssh, err := sshd.NewSSHServer(ctx, l.With("subsystem", "sshd"))
  67. if err != nil {
  68. return nil, util.ContextualizeIfNeeded("Error while creating SSH server", err)
  69. }
  70. wireSSHReload(l, ssh, c)
  71. var sshStart func()
  72. if c.GetBool("sshd.enabled", false) {
  73. sshStart, err = configSSH(l, ssh, c)
  74. if err != nil {
  75. l.Warn("Failed to configure sshd, ssh debugging will not be available", "error", err)
  76. sshStart = nil
  77. }
  78. }
  79. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  80. // All non system modifying configuration consumption should live above this line
  81. // tun config, listeners, anything modifying the computer should be below
  82. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  83. var routines int
  84. // If `routines` is set, use that and ignore the specific values
  85. if routines = c.GetInt("routines", 0); routines != 0 {
  86. if routines < 1 {
  87. routines = 1
  88. }
  89. } else {
  90. // deprecated and undocumented
  91. tunQueues := c.GetInt("tun.routines", 1)
  92. udpQueues := c.GetInt("listen.routines", 1)
  93. routines = max(tunQueues, udpQueues)
  94. if routines != 1 {
  95. l.Warn("Setting tun.routines and listen.routines is deprecated. Use `routines` instead", "routines", routines)
  96. }
  97. }
  98. if routines > maxRoutines {
  99. l.Warn("Using multiple routines", "routines", maxRoutines, "clamped", true, "requestedRoutines", routines)
  100. routines = maxRoutines
  101. } else if routines > 1 {
  102. l.Info("Using multiple routines", "routines", routines)
  103. }
  104. // EXPERIMENTAL
  105. // Intentionally not documented yet while we do more testing and determine
  106. // a good default value.
  107. conntrackCacheTimeout := c.GetDuration("firewall.conntrack.routine_cache_timeout", 0)
  108. if routines > 1 && !c.IsSet("firewall.conntrack.routine_cache_timeout") {
  109. // Use a different default if we are running with multiple routines
  110. conntrackCacheTimeout = 1 * time.Second
  111. }
  112. if conntrackCacheTimeout > 0 {
  113. l.Info("Using routine-local conntrack cache", "duration", conntrackCacheTimeout)
  114. }
  115. var tun overlay.Device
  116. if !configTest {
  117. c.CatchHUP(ctx)
  118. if deviceFactory == nil {
  119. deviceFactory = overlay.NewDeviceFromConfig
  120. }
  121. tun, err = deviceFactory(c, l, pki.getCertState().myVpnNetworks, routines)
  122. if err != nil {
  123. return nil, util.ContextualizeIfNeeded("Failed to get a tun/tap device", err)
  124. }
  125. defer func() {
  126. if reterr != nil {
  127. tun.Close()
  128. }
  129. }()
  130. }
  131. // set up our UDP listener
  132. udpConns := make([]udp.Conn, routines)
  133. port := c.GetInt("listen.port", 0)
  134. // Callers get no handle to these until the Control is returned, release them on any error.
  135. defer func() {
  136. if reterr != nil {
  137. for _, u := range udpConns {
  138. if u != nil {
  139. _ = u.Close()
  140. }
  141. }
  142. }
  143. }()
  144. if !configTest {
  145. rawListenHost := c.GetString("listen.host", "0.0.0.0")
  146. var listenHost netip.Addr
  147. if rawListenHost == "[::]" {
  148. // Old guidance was to provide the literal `[::]` in `listen.host` but that won't resolve.
  149. listenHost = netip.IPv6Unspecified()
  150. } else {
  151. ips, err := net.DefaultResolver.LookupNetIP(context.Background(), "ip", rawListenHost)
  152. if err != nil {
  153. return nil, util.ContextualizeIfNeeded("Failed to resolve listen.host", err)
  154. }
  155. if len(ips) == 0 {
  156. return nil, util.ContextualizeIfNeeded("Failed to resolve listen.host", err)
  157. }
  158. listenHost = ips[0].Unmap()
  159. }
  160. for i := 0; i < routines; i++ {
  161. listen := netip.AddrPortFrom(listenHost, uint16(port))
  162. l.Info("listening", "addr", listen)
  163. batchSize := c.GetInt("listen.batch", 64)
  164. if batchSize < 1 {
  165. oldBatch := batchSize
  166. batchSize = 1
  167. l.Warn("listen.batch size is invalid", "provided", oldBatch, "overridden to", batchSize)
  168. }
  169. udpSettings := udp.Settings{
  170. Listen: listen,
  171. Multi: routines > 1,
  172. Batch: batchSize,
  173. Offloads: c.GetBool("listen.udp_offloads", false),
  174. }
  175. udpServer, err := udp.NewListener(l, udpSettings)
  176. if err != nil {
  177. return nil, util.NewContextualError("Failed to open udp listener", m{"queue": i}, err)
  178. }
  179. udpServer.ReloadConfig(c)
  180. udpConns[i] = udpServer
  181. // If port is dynamic, discover it before the next pass through the for loop
  182. // This way all routines will use the same port correctly
  183. if port == 0 {
  184. uPort, err := udpServer.LocalAddr()
  185. if err != nil {
  186. return nil, util.NewContextualError("Failed to get listening port", nil, err)
  187. }
  188. port = int(uPort.Port())
  189. }
  190. }
  191. }
  192. hostMap := NewHostMapFromConfig(l, c)
  193. punchy := NewPunchyFromConfig(l, c, udpConns[0])
  194. connManager := newConnectionManagerFromConfig(l, c, hostMap, punchy)
  195. lightHouse, err := NewLightHouseFromConfig(ctx, l, c, pki.getCertState(), udpConns[0], punchy)
  196. if err != nil {
  197. return nil, util.ContextualizeIfNeeded("Failed to initialize lighthouse handler", err)
  198. }
  199. var messageMetrics *MessageMetrics
  200. if c.GetBool("stats.message_metrics", false) {
  201. messageMetrics = newMessageMetrics()
  202. } else {
  203. messageMetrics = newMessageMetricsOnlyRecvError()
  204. }
  205. handshakeConfig := HandshakeConfig{
  206. tryInterval: c.GetDuration("handshakes.try_interval", DefaultHandshakeTryInterval),
  207. retries: int64(c.GetInt("handshakes.retries", DefaultHandshakeRetries)),
  208. triggerBuffer: c.GetInt("handshakes.trigger_buffer", DefaultHandshakeTriggerBuffer),
  209. messageMetrics: messageMetrics,
  210. }
  211. handshakeManager := NewHandshakeManager(l, hostMap, lightHouse, udpConns[0], handshakeConfig)
  212. lightHouse.handshakeTrigger = handshakeManager.trigger
  213. ds, err := newDnsServerFromConfig(ctx, l, pki, hostMap, c)
  214. if err != nil {
  215. l.Warn("Failed to start DNS responder", "error", err)
  216. }
  217. pinThreads := c.GetBool("tun.pin_threads", true)
  218. cpuAffinity := parseCpuAffinity(c, l, routines)
  219. if pinThreads && routines > 1 && len(cpuAffinity) == 0 && !configTest {
  220. // The operator didn't choose pin CPUs, so pick a default set that
  221. // prefers performance cores and doesn't stack co-located instances
  222. // onto allowed[0].
  223. // key is used to seed the spreading of routines->cores.
  224. // use PID if you want to ensure many different Nebulas in VMs or containers land on different cores
  225. // use port if you want to always end up on the same cores, ideal for benchmarking.
  226. key := uint64(os.Getpid()) //default to PID
  227. pinKeyStr := strings.ToLower(c.GetString("tun.pin_threads_key", ""))
  228. switch pinKeyStr {
  229. case "":
  230. l.Debug("tun.pin_threads_key is empty, using PID")
  231. case "pid":
  232. l.Debug("tun.pin_threads_key is PID")
  233. case "port":
  234. if ap, err := udpConns[0].LocalAddr(); err == nil && ap.Port() != 0 {
  235. l.Info("tun.pin_threads_key is port number")
  236. key = uint64(ap.Port())
  237. } else {
  238. l.Warn("Failed to get a port number for tun.pin_threads_key, falling back to PID", "err", err)
  239. }
  240. default:
  241. l.Warn("tun.pin_threads_key is invalid, using PID")
  242. }
  243. cpuAffinity = cpupick.Default(routines, key, l)
  244. }
  245. ifConfig := &InterfaceConfig{
  246. HostMap: hostMap,
  247. Inside: tun,
  248. Outside: udpConns[0],
  249. pki: pki,
  250. Firewall: fw,
  251. DnsServer: ds,
  252. HandshakeManager: handshakeManager,
  253. connectionManager: connManager,
  254. lightHouse: lightHouse,
  255. tryPromoteEvery: c.GetUint32("counters.try_promote", defaultPromoteEvery),
  256. reQueryEvery: c.GetUint32("counters.requery_every_packets", defaultReQueryEvery),
  257. reQueryWait: c.GetDuration("timers.requery_wait_duration", defaultReQueryWait),
  258. DropLocalBroadcast: c.GetBool("tun.drop_local_broadcast", false),
  259. DropMulticast: c.GetBool("tun.drop_multicast", false),
  260. routines: routines,
  261. MessageMetrics: messageMetrics,
  262. version: buildVersion,
  263. relayManager: NewRelayManager(ctx, l, hostMap, c),
  264. punchy: punchy,
  265. ConntrackCacheTimeout: conntrackCacheTimeout,
  266. CpuAffinity: cpuAffinity,
  267. PinThreads: pinThreads,
  268. l: l,
  269. }
  270. var ifce *Interface
  271. var emitStats func()
  272. if !configTest {
  273. ifce, err = NewInterface(ctx, ifConfig)
  274. if err != nil {
  275. return nil, fmt.Errorf("failed to initialize interface: %s", err)
  276. }
  277. ifce.writers = udpConns
  278. lightHouse.ifce = ifce
  279. ifce.RegisterConfigChangeCallbacks(c)
  280. ifce.reloadDisconnectInvalid(c)
  281. ifce.reloadSendRecvError(c)
  282. ifce.reloadAcceptRecvError(c)
  283. handshakeManager.f = ifce
  284. go handshakeManager.Run(ctx)
  285. punchy.Start(ctx, ifce, hostMap, lightHouse)
  286. // On the stats server's loop, so the gauges follow the interval it accepted
  287. emitStats = ifce.statsEmitter()
  288. }
  289. stats, err := newStatsServerFromConfig(ctx, l, c, buildVersion, configTest, emitStats)
  290. if err != nil {
  291. return nil, util.ContextualizeIfNeeded("Failed to start stats emitter", err)
  292. }
  293. if configTest {
  294. return nil, nil
  295. }
  296. attachCommands(l, c, ssh, ifce)
  297. networkChanges := udp.NewNetworkChangeMonitor(ctx, l, c)
  298. return &Control{
  299. state: StateReady,
  300. f: ifce,
  301. l: l,
  302. ctx: ctx,
  303. cancel: cancel,
  304. sshStart: sshStart,
  305. statsStart: stats.Start,
  306. dnsStart: ds.Start,
  307. lighthouseStart: lightHouse.StartUpdateWorker,
  308. networkChangeStart: networkChanges.Start,
  309. connectionManagerStart: connManager.Start,
  310. }, nil
  311. }
  312. // parseCpuAffinity reads `tun.cpu_affinity` from the config — a list of
  313. // integer CPU IDs, one per TUN reader goroutine. Empty / unset returns nil
  314. // (listenIn falls back to spreading queues across the allowed CPU set).
  315. // Length mismatch with `routines` is a warning, not an error: shorter lists
  316. // are modulo-cycled across queues, longer lists' tail is ignored. Invalid
  317. // entries (non-integer, or a CPU ID we're not allowed to run on) are also a
  318. // warning and disable the override entirely so we don't silently pin to the
  319. // wrong CPU. Entries are validated against the process's current affinity
  320. // mask (util.AllowedCPUs) rather than 0..NumCPU-1: under a cgroup cpuset or
  321. // taskset the runnable IDs are frequently not that contiguous range, and
  322. // pinning to an unrunnable ID always fails. If the allowed set can't be
  323. // determined we fall back to a plain non-negative check.
  324. func parseCpuAffinity(c *config.C, l *slog.Logger, routines int) []int {
  325. raw := c.Get("tun.cpu_affinity")
  326. if raw == nil {
  327. return nil
  328. }
  329. rv, ok := raw.([]any)
  330. if !ok {
  331. l.Warn("tun.cpu_affinity must be a list of integers; ignoring", "value", raw)
  332. return nil
  333. }
  334. // allowed is the set of CPU IDs we're actually permitted to run on. A nil
  335. // slice (unsupported platform or lookup error) means "can't tell", so we
  336. // only apply the weaker non-negative check in that case.
  337. allowed, err := util.AllowedCPUs()
  338. if err != nil {
  339. l.Warn("could not determine allowed CPUs; validating tun.cpu_affinity against non-negative only", "error", err)
  340. allowed = nil
  341. }
  342. cpus := make([]int, 0, len(rv))
  343. for i, e := range rv {
  344. var cpu int
  345. switch v := e.(type) {
  346. case int:
  347. cpu = v
  348. case int64:
  349. cpu = int(v)
  350. case float64:
  351. cpu = int(v)
  352. default:
  353. l.Warn("tun.cpu_affinity entry not an integer; ignoring affinity",
  354. "index", i, "value", e)
  355. return nil
  356. }
  357. if cpu < 0 {
  358. l.Warn("tun.cpu_affinity entry out of range; ignoring affinity",
  359. "index", i, "cpu", cpu)
  360. return nil
  361. }
  362. if len(allowed) > 0 && !slices.Contains(allowed, cpu) {
  363. l.Warn("tun.cpu_affinity entry not in allowed CPU set; ignoring affinity",
  364. "index", i, "cpu", cpu, "allowed", allowed)
  365. return nil
  366. }
  367. cpus = append(cpus, cpu)
  368. }
  369. if len(cpus) != routines {
  370. l.Warn("tun.cpu_affinity length doesn't match routines; queues will modulo-cycle through the list",
  371. "affinity_len", len(cpus), "routines", routines)
  372. }
  373. return cpus
  374. }
  375. // configureAvoidCPUs applies tun.avoid_cpus, a list of CPU IDs to keep every thread of the process off. Unlike
  376. // tun.cpu_affinity, a bad entry fails startup rather than being ignored. Configuration tests only validate it.
  377. func configureAvoidCPUs(c *config.C, l *slog.Logger, configTest bool) error {
  378. raw := c.Get("tun.avoid_cpus")
  379. if raw == nil {
  380. return nil
  381. }
  382. values, ok := raw.([]any)
  383. if !ok {
  384. return fmt.Errorf("tun.avoid_cpus must be a list of non-negative integers")
  385. }
  386. cpus := make([]int, 0, len(values))
  387. for _, value := range values {
  388. cpu, ok := value.(int)
  389. if !ok || cpu < 0 {
  390. return fmt.Errorf("tun.avoid_cpus must contain non-negative integers, got %v", value)
  391. }
  392. cpus = append(cpus, cpu)
  393. }
  394. if len(cpus) == 0 {
  395. return nil
  396. }
  397. if err := util.AvoidCPUs(cpus, !configTest); err != nil {
  398. if errors.Is(err, errors.ErrUnsupported) {
  399. l.Warn("tun.avoid_cpus is only supported on Linux; ignoring")
  400. return nil
  401. }
  402. return fmt.Errorf("tun.avoid_cpus: %w", err)
  403. }
  404. if !configTest {
  405. l.Info("Excluded CPUs from process threads", "cpus", cpus)
  406. }
  407. return nil
  408. }
  409. func moduleVersion() string {
  410. info, ok := debug.ReadBuildInfo()
  411. if !ok {
  412. return ""
  413. }
  414. for _, dep := range info.Deps {
  415. if dep.Path == "github.com/slackhq/nebula" {
  416. return strings.TrimPrefix(dep.Version, "v")
  417. }
  418. }
  419. return ""
  420. }