commands.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. package nebula
  2. import (
  3. "encoding/json"
  4. "flag"
  5. "fmt"
  6. "io"
  7. "log/slog"
  8. "maps"
  9. "net/netip"
  10. "sort"
  11. "strings"
  12. "github.com/slackhq/nebula/cert"
  13. "github.com/slackhq/nebula/commands"
  14. "github.com/slackhq/nebula/config"
  15. "github.com/slackhq/nebula/header"
  16. "github.com/slackhq/nebula/logging"
  17. )
  18. // The commands nebula serves. attachCommands builds the one registry every surface
  19. // reads; the ssh debug server is a consumer of it, not its owner, which is why this
  20. // is not ssh.go. See the commands package for what a Command is.
  21. type listHostMapFlags struct {
  22. ByIndex bool
  23. }
  24. type printCertFlags struct {
  25. Raw bool
  26. }
  27. type changeRemoteFlags struct {
  28. Address string
  29. }
  30. type closeTunnelFlags struct {
  31. LocalOnly bool
  32. }
  33. type createTunnelFlags struct {
  34. Address string
  35. }
  36. // lighthouseInfo is one entry of list-lighthouse-addrmap.
  37. type lighthouseInfo struct {
  38. VpnAddr string `json:"vpnAddr"`
  39. Addrs *CacheMap `json:"addrs"`
  40. }
  41. // deviceInfo is the result of device-info.
  42. type deviceInfo struct {
  43. Name string `json:"name"`
  44. Cidr []netip.Prefix `json:"cidr"`
  45. }
  46. // certPEM is the result of print-cert -raw. The PEM form is a different thing to emit,
  47. // not a different formatting of the certificate, so it is its own shape.
  48. type certPEM struct {
  49. PEM string `json:"pem"`
  50. }
  51. // Result shapes for print-relays. Field names are the JSON keys and predate this
  52. // refactor; changing them would change the output.
  53. type relayFor struct {
  54. Error string `json:",omitempty"`
  55. Type string
  56. State string
  57. PeerAddr netip.Addr
  58. LocalIndex uint32
  59. RemoteIndex uint32
  60. RelayedThrough []netip.Addr
  61. }
  62. type relayOutput struct {
  63. NebulaAddr netip.Addr
  64. RelayForAddrs []relayFor
  65. }
  66. type relaysOutput struct {
  67. Relays []*relayOutput
  68. }
  69. type versionInfo struct {
  70. Version string `json:"version"`
  71. }
  72. type logLevelInfo struct {
  73. Level string `json:"level"`
  74. }
  75. type logFormatInfo struct {
  76. Format string `json:"format"`
  77. }
  78. // defineListHostMapFlags declares the flags list-hostmap and list-pending-hostmap share.
  79. func defineListHostMapFlags(fl *flag.FlagSet, s *listHostMapFlags) {
  80. fl.BoolVar(&s.ByIndex, "by-index", false, "gets all hosts in the hostmap from the index table")
  81. }
  82. func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Interface) {
  83. reg.Register(commands.Flagged(&commands.Command{
  84. Name: commands.ListHostmap,
  85. ShortDescription: "List all known previously connected hosts",
  86. Text: commands.Typed(textListHostMap),
  87. }, defineListHostMapFlags, func(s *listHostMapFlags, a []string) (commands.Result, error) {
  88. return runListHostMap(f.hostMap, s)
  89. }))
  90. reg.Register(commands.Flagged(&commands.Command{
  91. Name: commands.ListPendingHostmap,
  92. ShortDescription: "List all handshaking hosts",
  93. Text: commands.Typed(textListHostMap),
  94. }, defineListHostMapFlags, func(s *listHostMapFlags, a []string) (commands.Result, error) {
  95. return runListHostMap(f.handshakeManager, s)
  96. }))
  97. reg.Register(&commands.Command{
  98. Name: commands.ListLighthouseAddrmap,
  99. ShortDescription: "List all lighthouse map entries",
  100. Run: func(fs any, a []string) (commands.Result, error) {
  101. return runListLighthouseMap(f.lightHouse)
  102. },
  103. Text: commands.Typed(textListLighthouseMap),
  104. })
  105. reg.Register(&commands.Command{
  106. Name: commands.Reload,
  107. ShortDescription: "Reloads configuration from disk, same as sending HUP to the process",
  108. // The line goes out before the reload, as it always has: a reload that restarts
  109. // the ssh server ends the session that asked, and this is what it sees.
  110. Run: func(fs any, a []string) (commands.Result, error) {
  111. return commands.Did("Reloading config"), nil
  112. },
  113. Then: func() commands.Result {
  114. return runReload(l, c)
  115. },
  116. })
  117. reg.Register(&commands.Command{
  118. Name: commands.LogLevel,
  119. ShortDescription: "Gets or sets the current log level",
  120. Run: func(fs any, a []string) (commands.Result, error) {
  121. return runLogLevel(l, a)
  122. },
  123. Text: commands.Typed(textLogLevel),
  124. })
  125. reg.Register(&commands.Command{
  126. Name: commands.LogFormat,
  127. ShortDescription: "Gets or sets the current log format",
  128. Run: func(fs any, a []string) (commands.Result, error) {
  129. return runLogFormat(l, a)
  130. },
  131. Text: commands.Typed(textLogFormat),
  132. })
  133. reg.Register(&commands.Command{
  134. Name: commands.Version,
  135. ShortDescription: "Prints the currently running version of nebula",
  136. Run: func(fs any, a []string) (commands.Result, error) {
  137. return commands.Value(versionInfo{Version: f.version}), nil
  138. },
  139. Text: commands.Typed(textVersion),
  140. })
  141. reg.Register(&commands.Command{
  142. Name: commands.DeviceInfo,
  143. ShortDescription: "Prints information about the network device.",
  144. Run: func(fs any, a []string) (commands.Result, error) {
  145. return commands.Value(runDeviceInfo(f)), nil
  146. },
  147. Text: commands.Typed(textDeviceInfo),
  148. })
  149. reg.Register(commands.Flagged(&commands.Command{
  150. Name: commands.PrintCert,
  151. ShortDescription: "Prints the current certificate being used or the certificate for the provided vpn addr",
  152. Text: textPrintCert,
  153. }, func(fl *flag.FlagSet, s *printCertFlags) {
  154. fl.BoolVar(&s.Raw, "raw", false, "prints the PEM encoded certificate; with -json, an object holding it")
  155. }, func(s *printCertFlags, a []string) (commands.Result, error) {
  156. return runPrintCert(f, s, a)
  157. }))
  158. reg.Register(commands.Flagged(&commands.Command{
  159. Name: commands.Identity,
  160. ShortDescription: "Prints the certificate identity of the host owning the provided vpn addr",
  161. Help: "Identity is what the certificate says: name, vpn addrs, networks, groups, fingerprint, issuer and validity. The address may carry a port, so a connection's remote address can be passed through as is. For this node's own identity use whoami.",
  162. Identity: true,
  163. Text: commands.Typed(textIdentity),
  164. }, func(fl *flag.FlagSet, s *identityFlags) {
  165. fl.Func("address", "The vpn addr to look up, optionally with a port", s.setAddress)
  166. }, func(s *identityFlags, a []string) (commands.Result, error) {
  167. return runIdentity(f, s, a)
  168. }))
  169. reg.Register(&commands.Command{
  170. Name: commands.Whoami,
  171. ShortDescription: "Prints the certificate identity of this node",
  172. Help: "The fields identity prints, read from this node's own certificate.",
  173. Identity: true,
  174. Text: commands.Typed(textIdentity),
  175. Run: func(fs any, a []string) (commands.Result, error) {
  176. if len(a) > 0 {
  177. return commands.Failed("whoami takes no arguments"), nil
  178. }
  179. return runWhoami(f)
  180. },
  181. })
  182. reg.Register(&commands.Command{
  183. Name: commands.PrintTunnel,
  184. ShortDescription: "Prints json details about a tunnel for the provided vpn addr",
  185. Run: func(fs any, a []string) (commands.Result, error) {
  186. return runPrintTunnel(f, a)
  187. },
  188. // No Text on purpose: nothing human readable exists yet, and shipping an interim
  189. // form would invite scripts against it. ssh defaults these to JSON; ctl asks.
  190. JSONOnly: true,
  191. })
  192. reg.Register(&commands.Command{
  193. Name: commands.PrintRelays,
  194. ShortDescription: "Prints json details about all relay info",
  195. Run: func(fs any, a []string) (commands.Result, error) {
  196. return commands.Value(runPrintRelays(f)), nil
  197. },
  198. // No Text on purpose; see print-tunnel.
  199. JSONOnly: true,
  200. })
  201. reg.Register(commands.Flagged(&commands.Command{
  202. Name: commands.ChangeRemote,
  203. ShortDescription: "Changes the remote address used in the tunnel for the provided vpn addr",
  204. }, func(fl *flag.FlagSet, s *changeRemoteFlags) {
  205. fl.StringVar(&s.Address, "address", "", "The new remote address, ip:port")
  206. }, func(s *changeRemoteFlags, a []string) (commands.Result, error) {
  207. return runChangeRemote(f, s, a)
  208. }))
  209. reg.Register(commands.Flagged(&commands.Command{
  210. Name: commands.CloseTunnel,
  211. ShortDescription: "Closes a tunnel for the provided vpn addr",
  212. }, func(fl *flag.FlagSet, s *closeTunnelFlags) {
  213. fl.BoolVar(&s.LocalOnly, "local-only", false, "Disables notifying the remote that the tunnel is shutting down")
  214. }, func(s *closeTunnelFlags, a []string) (commands.Result, error) {
  215. return runCloseTunnel(f, s, a)
  216. }))
  217. reg.Register(commands.Flagged(&commands.Command{
  218. Name: commands.CreateTunnel,
  219. ShortDescription: "Creates a tunnel for the provided vpn address",
  220. Help: "The lighthouses will be queried for real addresses but you can provide one as well.",
  221. }, func(fl *flag.FlagSet, s *createTunnelFlags) {
  222. fl.StringVar(&s.Address, "address", "", "Optionally provide a real remote address, ip:port ")
  223. }, func(s *createTunnelFlags, a []string) (commands.Result, error) {
  224. return runCreateTunnel(f, s, a)
  225. }))
  226. reg.Register(&commands.Command{
  227. Name: commands.QueryLighthouse,
  228. ShortDescription: "Query the lighthouses for the provided vpn address",
  229. Help: "This command is asynchronous. Only currently known udp addresses will be printed.",
  230. Run: func(fs any, a []string) (commands.Result, error) {
  231. return runQueryLighthouse(f, a)
  232. },
  233. // No Text on purpose; see print-tunnel.
  234. JSONOnly: true,
  235. })
  236. }
  237. // runListHostMap computes the hostmap listing. Presentation is Render's job.
  238. func runListHostMap(hl controlHostLister, fs *listHostMapFlags) (commands.Result, error) {
  239. var hm []ControlHostInfo
  240. if fs.ByIndex {
  241. hm = listHostMapIndexes(hl)
  242. } else {
  243. hm = listHostMapHosts(hl)
  244. }
  245. sort.Slice(hm, func(i, j int) bool {
  246. return hm[i].VpnAddrs[0].Compare(hm[j].VpnAddrs[0]) < 0
  247. })
  248. return commands.Value(hm), nil
  249. }
  250. // textListHostMap is the human rendering, shared by every surface.
  251. func textListHostMap(hm []ControlHostInfo, w io.Writer) error {
  252. for _, h := range hm {
  253. if _, err := fmt.Fprintf(w, "%s: %s\n", h.VpnAddrs, h.RemoteAddrs); err != nil {
  254. return err
  255. }
  256. }
  257. return nil
  258. }
  259. func runListLighthouseMap(lightHouse *LightHouse) (commands.Result, error) {
  260. lightHouse.RLock()
  261. addrMap := make([]lighthouseInfo, len(lightHouse.addrMap))
  262. x := 0
  263. for k, v := range lightHouse.addrMap {
  264. addrMap[x] = lighthouseInfo{
  265. VpnAddr: k.String(),
  266. Addrs: v.CopyCache(),
  267. }
  268. x++
  269. }
  270. lightHouse.RUnlock()
  271. sort.Slice(addrMap, func(i, j int) bool {
  272. return strings.Compare(addrMap[i].VpnAddr, addrMap[j].VpnAddr) < 0
  273. })
  274. return commands.Value(addrMap), nil
  275. }
  276. func textListLighthouseMap(addrMap []lighthouseInfo, w io.Writer) error {
  277. for _, e := range addrMap {
  278. b, err := json.Marshal(e.Addrs)
  279. if err != nil {
  280. return err
  281. }
  282. if _, err := fmt.Fprintf(w, "%s: %s\n", e.VpnAddr, string(b)); err != nil {
  283. return err
  284. }
  285. }
  286. return nil
  287. }
  288. func textVersion(vi versionInfo, w io.Writer) error {
  289. _, err := fmt.Fprintf(w, "%s\n", vi.Version)
  290. return err
  291. }
  292. func runQueryLighthouse(ifce *Interface, a []string) (commands.Result, error) {
  293. if len(a) == 0 {
  294. return commands.Failed("No vpn address was provided"), nil
  295. }
  296. vpnAddr, err := netip.ParseAddr(a[0])
  297. if err != nil || !vpnAddr.IsValid() {
  298. return commands.Failed("The provided vpn address could not be parsed: %s", a[0]), nil
  299. }
  300. var cm *CacheMap
  301. rl := ifce.lightHouse.Query(vpnAddr)
  302. if rl != nil {
  303. cm = rl.CopyCache()
  304. }
  305. return commands.Value(cm), nil
  306. }
  307. func runCloseTunnel(ifce *Interface, flags *closeTunnelFlags, a []string) (commands.Result, error) {
  308. if len(a) == 0 {
  309. return commands.Failed("No vpn address was provided"), nil
  310. }
  311. vpnAddr, err := netip.ParseAddr(a[0])
  312. if err != nil || !vpnAddr.IsValid() {
  313. return commands.Failed("The provided vpn address could not be parsed: %s", a[0]), nil
  314. }
  315. hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
  316. if hostInfo == nil {
  317. return commands.Failed("Could not find tunnel for vpn address: %v", a[0]), nil
  318. }
  319. if !flags.LocalOnly {
  320. ifce.send(
  321. header.CloseTunnel,
  322. 0,
  323. hostInfo.ConnectionState,
  324. hostInfo,
  325. []byte{},
  326. make([]byte, 12, 12),
  327. make([]byte, mtu),
  328. )
  329. }
  330. ifce.closeTunnel(hostInfo)
  331. return commands.Did("Closed"), nil
  332. }
  333. func runCreateTunnel(ifce *Interface, flags *createTunnelFlags, a []string) (commands.Result, error) {
  334. if len(a) == 0 {
  335. return commands.Failed("No vpn address was provided"), nil
  336. }
  337. vpnAddr, err := netip.ParseAddr(a[0])
  338. if err != nil || !vpnAddr.IsValid() {
  339. return commands.Failed("The provided vpn address could not be parsed: %s", a[0]), nil
  340. }
  341. if hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr); hostInfo != nil {
  342. return commands.Failed("Tunnel already exists"), nil
  343. }
  344. if hostInfo := ifce.handshakeManager.QueryVpnAddr(vpnAddr); hostInfo != nil {
  345. return commands.Failed("Tunnel already handshaking"), nil
  346. }
  347. var addr netip.AddrPort
  348. if flags.Address != "" {
  349. addr, err = netip.ParseAddrPort(flags.Address)
  350. if err != nil {
  351. return commands.Failed("Address could not be parsed"), nil
  352. }
  353. }
  354. // Set the remote from the callback, which StartHandshake runs under the handshake manager lock
  355. // before the hostinfo reaches the handshake loop or the static-host trigger, so the address is
  356. // in the RemoteList before the first handshake is sent.
  357. ifce.handshakeManager.StartHandshake(vpnAddr, func(hh *HandshakeHostInfo) {
  358. if addr.IsValid() {
  359. hh.hostinfo.SetRemote(addr)
  360. }
  361. })
  362. return commands.Did("Created"), nil
  363. }
  364. func runChangeRemote(ifce *Interface, flags *changeRemoteFlags, a []string) (commands.Result, error) {
  365. if len(a) == 0 {
  366. return commands.Failed("No vpn address was provided"), nil
  367. }
  368. if flags.Address == "" {
  369. return commands.Failed("No address was provided"), nil
  370. }
  371. addr, err := netip.ParseAddrPort(flags.Address)
  372. if err != nil {
  373. return commands.Failed("Address could not be parsed"), nil
  374. }
  375. vpnAddr, err := netip.ParseAddr(a[0])
  376. if err != nil || !vpnAddr.IsValid() {
  377. return commands.Failed("The provided vpn address could not be parsed: %s", a[0]), nil
  378. }
  379. hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
  380. if hostInfo == nil {
  381. return commands.Failed("Could not find tunnel for vpn address: %v", a[0]), nil
  382. }
  383. hostInfo.SetRemote(addr)
  384. return commands.Did("Changed"), nil
  385. }
  386. func runLogLevel(l *slog.Logger, a []string) (commands.Result, error) {
  387. ctrl, ok := l.Handler().(interface {
  388. GetLevel() slog.Level
  389. SetLevel(slog.Level)
  390. })
  391. if !ok {
  392. return commands.Failed("Log level is not reconfigurable on this logger"), nil
  393. }
  394. if len(a) > 0 {
  395. level, err := logging.ParseLevel(strings.ToLower(a[0]))
  396. if err != nil {
  397. return commands.Failed("Unknown log level %s. Possible log levels: trace, debug, info, warn, error", a), nil
  398. }
  399. ctrl.SetLevel(level)
  400. }
  401. return commands.Value(logLevelInfo{Level: logging.LevelName(ctrl.GetLevel())}), nil
  402. }
  403. func textLogLevel(li logLevelInfo, w io.Writer) error {
  404. _, err := fmt.Fprintf(w, "Log level is: %s\n", li.Level)
  405. return err
  406. }
  407. func runLogFormat(l *slog.Logger, a []string) (commands.Result, error) {
  408. ctrl, ok := l.Handler().(interface {
  409. GetFormat() string
  410. SetFormat(string) error
  411. })
  412. if !ok {
  413. return commands.Failed("Log format is not reconfigurable on this logger"), nil
  414. }
  415. if len(a) > 0 {
  416. if err := ctrl.SetFormat(strings.ToLower(a[0])); err != nil {
  417. return commands.Failed("Unable to set log format: %s", err), nil
  418. }
  419. }
  420. return commands.Value(logFormatInfo{Format: ctrl.GetFormat()}), nil
  421. }
  422. func textLogFormat(fi logFormatInfo, w io.Writer) error {
  423. _, err := fmt.Fprintf(w, "Log format is: %s\n", fi.Format)
  424. return err
  425. }
  426. func runPrintCert(ifce *Interface, args *printCertFlags, a []string) (commands.Result, error) {
  427. c := ifce.pki.getCertState().GetDefaultCertificate()
  428. if len(a) > 0 {
  429. vpnAddr, err := netip.ParseAddr(a[0])
  430. if err != nil || !vpnAddr.IsValid() {
  431. return commands.Failed("The provided vpn addr could not be parsed: %s", a[0]), nil
  432. }
  433. hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
  434. if hostInfo == nil {
  435. return commands.Failed("Could not find tunnel for vpn addr: %v", a[0]), nil
  436. }
  437. c = hostInfo.GetCert().Certificate
  438. }
  439. if args.Raw {
  440. b, err := c.MarshalPEM()
  441. if err != nil {
  442. return commands.Result{}, err
  443. }
  444. return commands.Value(certPEM{PEM: string(b)}), nil
  445. }
  446. return commands.Value(c), nil
  447. }
  448. // textPrintCert takes the value untyped: print-cert returns a certificate or, with -raw,
  449. // its PEM, and those are two things to emit rather than two renderings of one.
  450. func textPrintCert(v any, w io.Writer) error {
  451. switch c := v.(type) {
  452. case certPEM:
  453. _, err := io.WriteString(w, c.PEM)
  454. return err
  455. case cert.Certificate:
  456. _, err := io.WriteString(w, c.String()+"\n")
  457. return err
  458. default:
  459. return fmt.Errorf("expected a certificate, got %T", v)
  460. }
  461. }
  462. func runPrintRelays(ifce *Interface) any {
  463. ifce.hostMap.RLock()
  464. relays := maps.Clone(ifce.hostMap.Relays)
  465. ifce.hostMap.RUnlock()
  466. co := relaysOutput{}
  467. for k, v := range relays {
  468. ro := relayOutput{NebulaAddr: v.vpnAddrs[0]}
  469. co.Relays = append(co.Relays, &ro)
  470. relayHI := ifce.hostMap.QueryVpnAddr(v.vpnAddrs[0])
  471. if relayHI == nil {
  472. ro.RelayForAddrs = append(ro.RelayForAddrs, relayFor{Error: "could not find hostinfo"})
  473. continue
  474. }
  475. for _, vpnAddr := range relayHI.relayState.CopyRelayForIps() {
  476. rf := relayFor{}
  477. r, ok := relayHI.relayState.GetRelayForByAddr(vpnAddr)
  478. if ok {
  479. t := ""
  480. switch r.Type {
  481. case ForwardingType:
  482. t = "forwarding"
  483. case TerminalType:
  484. t = "terminal"
  485. default:
  486. t = "unknown"
  487. }
  488. st := ""
  489. switch r.State {
  490. case Requested:
  491. st = "requested"
  492. case Established:
  493. st = "established"
  494. default:
  495. st = "unknown"
  496. }
  497. rf.LocalIndex = r.LocalIndex
  498. rf.RemoteIndex = r.RemoteIndex
  499. rf.PeerAddr = r.PeerAddr
  500. rf.Type = t
  501. rf.State = st
  502. if rf.LocalIndex != k {
  503. rf.Error = fmt.Sprintf("hostmap LocalIndex '%v' does not match RelayState LocalIndex", k)
  504. }
  505. }
  506. relayedHI := ifce.hostMap.QueryVpnAddr(vpnAddr)
  507. if relayedHI != nil {
  508. rf.RelayedThrough = append(rf.RelayedThrough, relayedHI.relayState.CopyRelayIps()...)
  509. }
  510. ro.RelayForAddrs = append(ro.RelayForAddrs, rf)
  511. }
  512. }
  513. return co
  514. }
  515. func runPrintTunnel(ifce *Interface, a []string) (commands.Result, error) {
  516. if len(a) == 0 {
  517. return commands.Failed("No vpn address was provided"), nil
  518. }
  519. vpnAddr, err := netip.ParseAddr(a[0])
  520. if err != nil || !vpnAddr.IsValid() {
  521. return commands.Failed("The provided vpn addr could not be parsed: %s", a[0]), nil
  522. }
  523. hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
  524. if hostInfo == nil {
  525. return commands.Failed("Could not find tunnel for vpn addr: %v", a[0]), nil
  526. }
  527. return commands.Value(copyHostInfo(hostInfo, ifce.hostMap.GetPreferredRanges())), nil
  528. }
  529. func runDeviceInfo(ifce *Interface) deviceInfo {
  530. d := deviceInfo{
  531. Name: ifce.inside.Name(),
  532. Cidr: make([]netip.Prefix, len(ifce.inside.Networks())),
  533. }
  534. copy(d.Cidr, ifce.inside.Networks())
  535. return d
  536. }
  537. func textDeviceInfo(d deviceInfo, w io.Writer) error {
  538. _, err := fmt.Fprintf(w, "name=%v cidr=%v\n", d.Name, d.Cidr)
  539. return err
  540. }
  541. // runReload reloads the config and reports how it went. A reload that failed is a
  542. // refusal, exit 1 over ssh; one that merely needs a restart is not.
  543. func runReload(l *slog.Logger, c *config.C) commands.Result {
  544. res := c.ReloadConfig()
  545. res.Log(l)
  546. if res.Err != nil || res.Failed {
  547. return commands.Failed("%s", res.String())
  548. }
  549. return commands.Did("%s", res.String())
  550. }