ssh.go 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096
  1. package nebula
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "errors"
  6. "flag"
  7. "fmt"
  8. "log/slog"
  9. "maps"
  10. "net"
  11. "net/netip"
  12. "os"
  13. "path/filepath"
  14. "runtime"
  15. "runtime/pprof"
  16. "slices"
  17. "sort"
  18. "strconv"
  19. "strings"
  20. "github.com/slackhq/nebula/config"
  21. "github.com/slackhq/nebula/header"
  22. "github.com/slackhq/nebula/logging"
  23. "github.com/slackhq/nebula/sshd"
  24. )
  25. type sshListHostMapFlags struct {
  26. Json bool
  27. Pretty bool
  28. ByIndex bool
  29. }
  30. type sshPrintCertFlags struct {
  31. Json bool
  32. Pretty bool
  33. Raw bool
  34. }
  35. type sshPrintTunnelFlags struct {
  36. Pretty bool
  37. }
  38. type sshChangeRemoteFlags struct {
  39. Address string
  40. }
  41. type sshCloseTunnelFlags struct {
  42. LocalOnly bool
  43. }
  44. type sshCreateTunnelFlags struct {
  45. Address string
  46. }
  47. type sshDeviceInfoFlags struct {
  48. Json bool
  49. Pretty bool
  50. }
  51. func wireSSHReload(l *slog.Logger, ssh *sshd.SSHServer, c *config.C) {
  52. c.RegisterReloadCallback(func(c *config.C, r *config.Reload) {
  53. // sshd.sandbox_dir is read once, when the commands are attached
  54. r.RestartRequiredIfChanged("sshd.sandbox_dir")
  55. if c.GetBool("sshd.enabled", false) {
  56. // Restarting the server drops every open session, the one that asked for this reload included. Only restart
  57. // when it isn't running or its config changed. The host key file can change under the same config, new
  58. // handshakes use the new key without a restart
  59. if ssh.Running() && !slices.ContainsFunc(sshdServerKeys, c.HasChanged) {
  60. key, err := loadSSHHostKey(c)
  61. if err == nil {
  62. err = ssh.SetHostKey(key)
  63. }
  64. if err != nil {
  65. l.Error("Failed to reconfigure the sshd", "error", err)
  66. ssh.Stop()
  67. r.Failed()
  68. }
  69. return
  70. }
  71. sshRun, err := configSSH(l, ssh, c)
  72. if err != nil {
  73. l.Error("Failed to reconfigure the sshd", "error", err)
  74. ssh.Stop()
  75. r.Failed()
  76. }
  77. if sshRun != nil {
  78. go sshRun()
  79. }
  80. } else {
  81. ssh.Stop()
  82. }
  83. })
  84. }
  85. // sshdServerKeys are what configSSH reads. sshd.sandbox_dir isn't one, it's read once at startup and restarting the
  86. // server doesn't apply it
  87. var sshdServerKeys = []string{"sshd.enabled", "sshd.listen", "sshd.host_key", "sshd.trusted_cas", "sshd.authorized_users"}
  88. // loadSSHHostKey returns sshd.host_key, read from its file when it names one.
  89. func loadSSHHostKey(c *config.C) ([]byte, error) {
  90. hostKeyPathOrKey := c.GetString("sshd.host_key", "")
  91. if hostKeyPathOrKey == "" {
  92. return nil, fmt.Errorf("sshd.host_key must be provided")
  93. }
  94. if strings.Contains(hostKeyPathOrKey, "-----BEGIN") {
  95. return []byte(hostKeyPathOrKey), nil
  96. }
  97. b, err := os.ReadFile(hostKeyPathOrKey)
  98. if err != nil {
  99. return nil, fmt.Errorf("error while loading sshd.host_key file: %s", err)
  100. }
  101. return b, nil
  102. }
  103. // configSSH reads the ssh info out of the passed-in Config and
  104. // updates the passed-in SSHServer. On success, it returns a function
  105. // that callers may invoke to run the configured ssh server. On
  106. // failure, it returns nil, error.
  107. func configSSH(l *slog.Logger, ssh *sshd.SSHServer, c *config.C) (func(), error) {
  108. listen := c.GetString("sshd.listen", "")
  109. if listen == "" {
  110. return nil, fmt.Errorf("sshd.listen must be provided")
  111. }
  112. _, port, err := net.SplitHostPort(listen)
  113. if err != nil {
  114. return nil, fmt.Errorf("invalid sshd.listen address: %s", err)
  115. }
  116. if port == "22" {
  117. return nil, fmt.Errorf("sshd.listen can not use port 22")
  118. }
  119. hostKeyBytes, err := loadSSHHostKey(c)
  120. if err != nil {
  121. return nil, err
  122. }
  123. err = ssh.SetHostKey(hostKeyBytes)
  124. if err != nil {
  125. return nil, fmt.Errorf("error while adding sshd.host_key: %s", err)
  126. }
  127. // Clear existing trusted CAs and authorized keys
  128. ssh.ClearTrustedCAs()
  129. ssh.ClearAuthorizedKeys()
  130. rawCAs := c.GetStringSlice("sshd.trusted_cas", []string{})
  131. for _, caAuthorizedKey := range rawCAs {
  132. err := ssh.AddTrustedCA(caAuthorizedKey)
  133. if err != nil {
  134. l.Warn("SSH CA had an error, ignoring", "error", err, "sshCA", caAuthorizedKey)
  135. continue
  136. }
  137. }
  138. rawKeys := c.Get("sshd.authorized_users")
  139. keys, ok := rawKeys.([]any)
  140. if ok {
  141. for _, rk := range keys {
  142. kDef, ok := rk.(map[string]any)
  143. if !ok {
  144. l.Warn("Authorized user had an error, ignoring", "sshKeyConfig", rk)
  145. continue
  146. }
  147. user, ok := kDef["user"].(string)
  148. if !ok {
  149. l.Warn("Authorized user is missing the user field", "sshKeyConfig", rk)
  150. continue
  151. }
  152. k := kDef["keys"]
  153. switch v := k.(type) {
  154. case string:
  155. err := ssh.AddAuthorizedKey(user, v)
  156. if err != nil {
  157. l.Warn("Failed to authorize key",
  158. "error", err,
  159. "sshKeyConfig", rk,
  160. "sshKey", v,
  161. )
  162. continue
  163. }
  164. case []any:
  165. for _, subK := range v {
  166. sk, ok := subK.(string)
  167. if !ok {
  168. l.Warn("Did not understand ssh key",
  169. "sshKeyConfig", rk,
  170. "sshKey", subK,
  171. )
  172. continue
  173. }
  174. err := ssh.AddAuthorizedKey(user, sk)
  175. if err != nil {
  176. l.Warn("Failed to authorize key",
  177. "error", err,
  178. "sshKeyConfig", sk,
  179. )
  180. continue
  181. }
  182. }
  183. default:
  184. l.Warn("Authorized user is missing the keys field or was not understood", "sshKeyConfig", rk)
  185. }
  186. }
  187. } else {
  188. l.Info("no ssh users to authorize")
  189. }
  190. var runner func()
  191. if c.GetBool("sshd.enabled", false) {
  192. ssh.Stop()
  193. runner = func() {
  194. if err := ssh.Run(listen); err != nil {
  195. l.Warn("Failed to run the SSH server", "error", err)
  196. }
  197. }
  198. } else {
  199. ssh.Stop()
  200. }
  201. return runner, nil
  202. }
  203. func attachCommands(l *slog.Logger, c *config.C, ssh *sshd.SSHServer, f *Interface) {
  204. // sandboxDir defaults to a dir in temp. The intention is that end user will
  205. // create this dir as needed. Overriding this config value to "" allows
  206. // writing to anywhere in the system.
  207. defaultDir := filepath.Join(os.TempDir(), "nebula-debug")
  208. sandboxDir := c.GetString("sshd.sandbox_dir", defaultDir)
  209. ssh.RegisterCommand(&sshd.Command{
  210. Name: "list-hostmap",
  211. ShortDescription: "List all known previously connected hosts",
  212. Flags: func() (*flag.FlagSet, any) {
  213. fl := flag.NewFlagSet("", flag.ContinueOnError)
  214. s := sshListHostMapFlags{}
  215. fl.BoolVar(&s.Json, "json", false, "outputs as json with more information")
  216. fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
  217. fl.BoolVar(&s.ByIndex, "by-index", false, "gets all hosts in the hostmap from the index table")
  218. return fl, &s
  219. },
  220. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  221. return sshListHostMap(f.hostMap, fs, w)
  222. },
  223. })
  224. ssh.RegisterCommand(&sshd.Command{
  225. Name: "list-pending-hostmap",
  226. ShortDescription: "List all handshaking hosts",
  227. Flags: func() (*flag.FlagSet, any) {
  228. fl := flag.NewFlagSet("", flag.ContinueOnError)
  229. s := sshListHostMapFlags{}
  230. fl.BoolVar(&s.Json, "json", false, "outputs as json with more information")
  231. fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
  232. fl.BoolVar(&s.ByIndex, "by-index", false, "gets all hosts in the hostmap from the index table")
  233. return fl, &s
  234. },
  235. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  236. return sshListHostMap(f.handshakeManager, fs, w)
  237. },
  238. })
  239. ssh.RegisterCommand(&sshd.Command{
  240. Name: "list-lighthouse-addrmap",
  241. ShortDescription: "List all lighthouse map entries",
  242. Flags: func() (*flag.FlagSet, any) {
  243. fl := flag.NewFlagSet("", flag.ContinueOnError)
  244. s := sshListHostMapFlags{}
  245. fl.BoolVar(&s.Json, "json", false, "outputs as json with more information")
  246. fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
  247. return fl, &s
  248. },
  249. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  250. return sshListLighthouseMap(f.lightHouse, fs, w)
  251. },
  252. })
  253. ssh.RegisterCommand(&sshd.Command{
  254. Name: "reload",
  255. ShortDescription: "Reloads configuration from disk, same as sending HUP to the process",
  256. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  257. return sshReload(l, c, w)
  258. },
  259. })
  260. ssh.RegisterCommand(&sshd.Command{
  261. Name: "start-cpu-profile",
  262. ShortDescription: "Starts a cpu profile and write output to the provided file, ex: `cpu-profile.pb.gz`",
  263. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  264. return sshStartCpuProfile(sandboxDir, fs, a, w)
  265. },
  266. })
  267. ssh.RegisterCommand(&sshd.Command{
  268. Name: "stop-cpu-profile",
  269. ShortDescription: "Stops a cpu profile and writes output to the previously provided file",
  270. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  271. pprof.StopCPUProfile()
  272. return w.WriteLine("If a CPU profile was running it is now stopped")
  273. },
  274. })
  275. ssh.RegisterCommand(&sshd.Command{
  276. Name: "save-heap-profile",
  277. ShortDescription: "Saves a heap profile to the provided path, ex: `heap-profile.pb.gz`",
  278. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  279. return sshGetHeapProfile(sandboxDir, fs, a, w)
  280. },
  281. })
  282. ssh.RegisterCommand(&sshd.Command{
  283. Name: "mutex-profile-fraction",
  284. ShortDescription: "Gets or sets runtime.SetMutexProfileFraction",
  285. Callback: sshMutexProfileFraction,
  286. })
  287. ssh.RegisterCommand(&sshd.Command{
  288. Name: "save-mutex-profile",
  289. ShortDescription: "Saves a mutex profile to the provided path, ex: `mutex-profile.pb.gz`",
  290. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  291. return sshGetMutexProfile(sandboxDir, fs, a, w)
  292. },
  293. })
  294. ssh.RegisterCommand(&sshd.Command{
  295. Name: "log-level",
  296. ShortDescription: "Gets or sets the current log level",
  297. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  298. return sshLogLevel(l, fs, a, w)
  299. },
  300. })
  301. ssh.RegisterCommand(&sshd.Command{
  302. Name: "log-format",
  303. ShortDescription: "Gets or sets the current log format",
  304. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  305. return sshLogFormat(l, fs, a, w)
  306. },
  307. })
  308. ssh.RegisterCommand(&sshd.Command{
  309. Name: "version",
  310. ShortDescription: "Prints the currently running version of nebula",
  311. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  312. return sshVersion(f, fs, a, w)
  313. },
  314. })
  315. ssh.RegisterCommand(&sshd.Command{
  316. Name: "device-info",
  317. ShortDescription: "Prints information about the network device.",
  318. Flags: func() (*flag.FlagSet, any) {
  319. fl := flag.NewFlagSet("", flag.ContinueOnError)
  320. s := sshDeviceInfoFlags{}
  321. fl.BoolVar(&s.Json, "json", false, "outputs as json with more information")
  322. fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
  323. return fl, &s
  324. },
  325. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  326. return sshDeviceInfo(f, fs, w)
  327. },
  328. })
  329. ssh.RegisterCommand(&sshd.Command{
  330. Name: "print-cert",
  331. ShortDescription: "Prints the current certificate being used or the certificate for the provided vpn addr",
  332. Flags: func() (*flag.FlagSet, any) {
  333. fl := flag.NewFlagSet("", flag.ContinueOnError)
  334. s := sshPrintCertFlags{}
  335. fl.BoolVar(&s.Json, "json", false, "outputs as json")
  336. fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
  337. fl.BoolVar(&s.Raw, "raw", false, "raw prints the PEM encoded certificate, not compatible with -json or -pretty")
  338. return fl, &s
  339. },
  340. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  341. return sshPrintCert(f, fs, a, w)
  342. },
  343. })
  344. ssh.RegisterCommand(&sshd.Command{
  345. Name: "print-tunnel",
  346. ShortDescription: "Prints json details about a tunnel for the provided vpn addr",
  347. Flags: func() (*flag.FlagSet, any) {
  348. fl := flag.NewFlagSet("", flag.ContinueOnError)
  349. s := sshPrintTunnelFlags{}
  350. fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json")
  351. return fl, &s
  352. },
  353. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  354. return sshPrintTunnel(f, fs, a, w)
  355. },
  356. })
  357. ssh.RegisterCommand(&sshd.Command{
  358. Name: "print-relays",
  359. ShortDescription: "Prints json details about all relay info",
  360. Flags: func() (*flag.FlagSet, any) {
  361. fl := flag.NewFlagSet("", flag.ContinueOnError)
  362. s := sshPrintTunnelFlags{}
  363. fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json")
  364. return fl, &s
  365. },
  366. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  367. return sshPrintRelays(f, fs, a, w)
  368. },
  369. })
  370. ssh.RegisterCommand(&sshd.Command{
  371. Name: "change-remote",
  372. ShortDescription: "Changes the remote address used in the tunnel for the provided vpn addr",
  373. Flags: func() (*flag.FlagSet, any) {
  374. fl := flag.NewFlagSet("", flag.ContinueOnError)
  375. s := sshChangeRemoteFlags{}
  376. fl.StringVar(&s.Address, "address", "", "The new remote address, ip:port")
  377. return fl, &s
  378. },
  379. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  380. return sshChangeRemote(f, fs, a, w)
  381. },
  382. })
  383. ssh.RegisterCommand(&sshd.Command{
  384. Name: "close-tunnel",
  385. ShortDescription: "Closes a tunnel for the provided vpn addr",
  386. Flags: func() (*flag.FlagSet, any) {
  387. fl := flag.NewFlagSet("", flag.ContinueOnError)
  388. s := sshCloseTunnelFlags{}
  389. fl.BoolVar(&s.LocalOnly, "local-only", false, "Disables notifying the remote that the tunnel is shutting down")
  390. return fl, &s
  391. },
  392. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  393. return sshCloseTunnel(f, fs, a, w)
  394. },
  395. })
  396. ssh.RegisterCommand(&sshd.Command{
  397. Name: "create-tunnel",
  398. ShortDescription: "Creates a tunnel for the provided vpn address",
  399. Help: "The lighthouses will be queried for real addresses but you can provide one as well.",
  400. Flags: func() (*flag.FlagSet, any) {
  401. fl := flag.NewFlagSet("", flag.ContinueOnError)
  402. s := sshCreateTunnelFlags{}
  403. fl.StringVar(&s.Address, "address", "", "Optionally provide a real remote address, ip:port ")
  404. return fl, &s
  405. },
  406. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  407. return sshCreateTunnel(f, fs, a, w)
  408. },
  409. })
  410. ssh.RegisterCommand(&sshd.Command{
  411. Name: "query-lighthouse",
  412. ShortDescription: "Query the lighthouses for the provided vpn address",
  413. Help: "This command is asynchronous. Only currently known udp addresses will be printed.",
  414. Callback: func(fs any, a []string, w sshd.StringWriter) error {
  415. return sshQueryLighthouse(f, fs, a, w)
  416. },
  417. })
  418. }
  419. func sshListHostMap(hl controlHostLister, a any, w sshd.StringWriter) error {
  420. fs, ok := a.(*sshListHostMapFlags)
  421. if !ok {
  422. return nil
  423. }
  424. var hm []ControlHostInfo
  425. if fs.ByIndex {
  426. hm = listHostMapIndexes(hl)
  427. } else {
  428. hm = listHostMapHosts(hl)
  429. }
  430. sort.Slice(hm, func(i, j int) bool {
  431. return hm[i].VpnAddrs[0].Compare(hm[j].VpnAddrs[0]) < 0
  432. })
  433. if fs.Json || fs.Pretty {
  434. js := json.NewEncoder(w.GetWriter())
  435. if fs.Pretty {
  436. js.SetIndent("", " ")
  437. }
  438. err := js.Encode(hm)
  439. if err != nil {
  440. return nil
  441. }
  442. } else {
  443. for _, v := range hm {
  444. err := w.WriteLine(fmt.Sprintf("%s: %s", v.VpnAddrs, v.RemoteAddrs))
  445. if err != nil {
  446. return err
  447. }
  448. }
  449. }
  450. return nil
  451. }
  452. func sshListLighthouseMap(lightHouse *LightHouse, a any, w sshd.StringWriter) error {
  453. fs, ok := a.(*sshListHostMapFlags)
  454. if !ok {
  455. return nil
  456. }
  457. type lighthouseInfo struct {
  458. VpnAddr string `json:"vpnAddr"`
  459. Addrs *CacheMap `json:"addrs"`
  460. }
  461. lightHouse.RLock()
  462. addrMap := make([]lighthouseInfo, len(lightHouse.addrMap))
  463. x := 0
  464. for k, v := range lightHouse.addrMap {
  465. addrMap[x] = lighthouseInfo{
  466. VpnAddr: k.String(),
  467. Addrs: v.CopyCache(),
  468. }
  469. x++
  470. }
  471. lightHouse.RUnlock()
  472. sort.Slice(addrMap, func(i, j int) bool {
  473. return strings.Compare(addrMap[i].VpnAddr, addrMap[j].VpnAddr) < 0
  474. })
  475. if fs.Json || fs.Pretty {
  476. js := json.NewEncoder(w.GetWriter())
  477. if fs.Pretty {
  478. js.SetIndent("", " ")
  479. }
  480. err := js.Encode(addrMap)
  481. if err != nil {
  482. return nil
  483. }
  484. } else {
  485. for _, v := range addrMap {
  486. b, err := json.Marshal(v.Addrs)
  487. if err != nil {
  488. return err
  489. }
  490. err = w.WriteLine(fmt.Sprintf("%s: %s", v.VpnAddr, string(b)))
  491. if err != nil {
  492. return err
  493. }
  494. }
  495. }
  496. return nil
  497. }
  498. // sshSanitizeFilePath validates that the given file path is within the sandbox directory.
  499. // If sandboxDir is empty, the path is returned as-is for backwards compatibility.
  500. func sshSanitizeFilePath(sandboxDir, filePath string) (string, error) {
  501. if sandboxDir == "" {
  502. return filePath, nil
  503. }
  504. // Clean and resolve the path relative to the sandbox directory
  505. if !filepath.IsAbs(filePath) {
  506. filePath = filepath.Join(sandboxDir, filePath)
  507. }
  508. cleaned := filepath.Clean(filePath)
  509. // Ensure the resolved path is within the sandbox directory
  510. cleanedSandbox := filepath.Clean(sandboxDir)
  511. if cleaned == cleanedSandbox {
  512. return "", fmt.Errorf("path %q resolves to the sandbox directory itself %q", filePath, sandboxDir)
  513. }
  514. if !strings.HasPrefix(cleaned, cleanedSandbox+string(filepath.Separator)) {
  515. return "", fmt.Errorf("path %q is outside the sandbox directory %q", filePath, sandboxDir)
  516. }
  517. return cleaned, nil
  518. }
  519. func sshStartCpuProfile(sandboxDir string, fs any, a []string, w sshd.StringWriter) error {
  520. if len(a) == 0 {
  521. err := w.WriteLine("No path to write profile provided")
  522. return err
  523. }
  524. filePath, err := sshSanitizeFilePath(sandboxDir, a[0])
  525. if err != nil {
  526. return w.WriteLine(err.Error())
  527. }
  528. file, err := os.Create(filePath)
  529. if err != nil {
  530. err = w.WriteLine(fmt.Sprintf("Unable to create profile file: %s", err))
  531. return err
  532. }
  533. err = pprof.StartCPUProfile(file)
  534. if err != nil {
  535. err = w.WriteLine(fmt.Sprintf("Unable to start cpu profile: %s", err))
  536. return err
  537. }
  538. err = w.WriteLine(fmt.Sprintf("Started cpu profile, issue stop-cpu-profile to write the output to %s", a))
  539. return err
  540. }
  541. func sshVersion(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
  542. return w.WriteLine(fmt.Sprintf("%s", ifce.version))
  543. }
  544. func sshQueryLighthouse(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
  545. if len(a) == 0 {
  546. return w.WriteLine("No vpn address was provided")
  547. }
  548. vpnAddr, err := netip.ParseAddr(a[0])
  549. if err != nil {
  550. return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
  551. }
  552. if !vpnAddr.IsValid() {
  553. return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
  554. }
  555. var cm *CacheMap
  556. rl := ifce.lightHouse.Query(vpnAddr)
  557. if rl != nil {
  558. cm = rl.CopyCache()
  559. }
  560. return json.NewEncoder(w.GetWriter()).Encode(cm)
  561. }
  562. func sshCloseTunnel(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
  563. flags, ok := fs.(*sshCloseTunnelFlags)
  564. if !ok {
  565. return nil
  566. }
  567. if len(a) == 0 {
  568. return w.WriteLine("No vpn address was provided")
  569. }
  570. vpnAddr, err := netip.ParseAddr(a[0])
  571. if err != nil {
  572. return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
  573. }
  574. if !vpnAddr.IsValid() {
  575. return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
  576. }
  577. hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
  578. if hostInfo == nil {
  579. return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn address: %v", a[0]))
  580. }
  581. if !flags.LocalOnly {
  582. ifce.send(
  583. header.CloseTunnel,
  584. 0,
  585. hostInfo.ConnectionState,
  586. hostInfo,
  587. []byte{},
  588. make([]byte, 12, 12),
  589. make([]byte, mtu),
  590. )
  591. }
  592. ifce.closeTunnel(hostInfo)
  593. return w.WriteLine("Closed")
  594. }
  595. func sshCreateTunnel(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
  596. flags, ok := fs.(*sshCreateTunnelFlags)
  597. if !ok {
  598. return nil
  599. }
  600. if len(a) == 0 {
  601. return w.WriteLine("No vpn address was provided")
  602. }
  603. vpnAddr, err := netip.ParseAddr(a[0])
  604. if err != nil {
  605. return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
  606. }
  607. if !vpnAddr.IsValid() {
  608. return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
  609. }
  610. hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
  611. if hostInfo != nil {
  612. return w.WriteLine(fmt.Sprintf("Tunnel already exists"))
  613. }
  614. hostInfo = ifce.handshakeManager.QueryVpnAddr(vpnAddr)
  615. if hostInfo != nil {
  616. return w.WriteLine(fmt.Sprintf("Tunnel already handshaking"))
  617. }
  618. var addr netip.AddrPort
  619. if flags.Address != "" {
  620. addr, err = netip.ParseAddrPort(flags.Address)
  621. if err != nil {
  622. return w.WriteLine("Address could not be parsed")
  623. }
  624. }
  625. // Set the remote from the callback, which StartHandshake runs under the handshake manager lock
  626. // before the hostinfo reaches the handshake loop or the static-host trigger, so the address is
  627. // in the RemoteList before the first handshake is sent.
  628. ifce.handshakeManager.StartHandshake(vpnAddr, func(hh *HandshakeHostInfo) {
  629. if addr.IsValid() {
  630. hh.hostinfo.SetRemote(addr)
  631. }
  632. })
  633. return w.WriteLine("Created")
  634. }
  635. func sshChangeRemote(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
  636. flags, ok := fs.(*sshChangeRemoteFlags)
  637. if !ok {
  638. return nil
  639. }
  640. if len(a) == 0 {
  641. return w.WriteLine("No vpn address was provided")
  642. }
  643. if flags.Address == "" {
  644. return w.WriteLine("No address was provided")
  645. }
  646. addr, err := netip.ParseAddrPort(flags.Address)
  647. if err != nil {
  648. return w.WriteLine("Address could not be parsed")
  649. }
  650. vpnAddr, err := netip.ParseAddr(a[0])
  651. if err != nil {
  652. return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
  653. }
  654. if !vpnAddr.IsValid() {
  655. return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
  656. }
  657. hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
  658. if hostInfo == nil {
  659. return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn address: %v", a[0]))
  660. }
  661. hostInfo.SetRemote(addr)
  662. return w.WriteLine("Changed")
  663. }
  664. func sshGetHeapProfile(sandboxDir string, fs any, a []string, w sshd.StringWriter) error {
  665. if len(a) == 0 {
  666. return w.WriteLine("No path to write profile provided")
  667. }
  668. filePath, err := sshSanitizeFilePath(sandboxDir, a[0])
  669. if err != nil {
  670. return w.WriteLine(err.Error())
  671. }
  672. file, err := os.Create(filePath)
  673. if err != nil {
  674. err = w.WriteLine(fmt.Sprintf("Unable to create profile file: %s", err))
  675. return err
  676. }
  677. err = pprof.WriteHeapProfile(file)
  678. if err != nil {
  679. err = w.WriteLine(fmt.Sprintf("Unable to write profile: %s", err))
  680. return err
  681. }
  682. err = w.WriteLine(fmt.Sprintf("Mem profile created at %s", a))
  683. return err
  684. }
  685. func sshMutexProfileFraction(fs any, a []string, w sshd.StringWriter) error {
  686. if len(a) == 0 {
  687. rate := runtime.SetMutexProfileFraction(-1)
  688. return w.WriteLine(fmt.Sprintf("Current value: %d", rate))
  689. }
  690. newRate, err := strconv.Atoi(a[0])
  691. if err != nil {
  692. return w.WriteLine(fmt.Sprintf("Invalid argument: %s", a[0]))
  693. }
  694. oldRate := runtime.SetMutexProfileFraction(newRate)
  695. return w.WriteLine(fmt.Sprintf("New value: %d. Old value: %d", newRate, oldRate))
  696. }
  697. func sshGetMutexProfile(sandboxDir string, fs any, a []string, w sshd.StringWriter) error {
  698. if len(a) == 0 {
  699. return w.WriteLine("No path to write profile provided")
  700. }
  701. filePath, err := sshSanitizeFilePath(sandboxDir, a[0])
  702. if err != nil {
  703. return w.WriteLine(err.Error())
  704. }
  705. file, err := os.Create(filePath)
  706. if err != nil {
  707. return w.WriteLine(fmt.Sprintf("Unable to create profile file: %s", err))
  708. }
  709. defer file.Close()
  710. mutexProfile := pprof.Lookup("mutex")
  711. if mutexProfile == nil {
  712. return w.WriteLine("Unable to get pprof.Lookup(\"mutex\")")
  713. }
  714. err = mutexProfile.WriteTo(file, 0)
  715. if err != nil {
  716. return w.WriteLine(fmt.Sprintf("Unable to write profile: %s", err))
  717. }
  718. return w.WriteLine(fmt.Sprintf("Mutex profile created at %s", a))
  719. }
  720. func sshLogLevel(l *slog.Logger, fs any, a []string, w sshd.StringWriter) error {
  721. ctrl, ok := l.Handler().(interface {
  722. GetLevel() slog.Level
  723. SetLevel(slog.Level)
  724. })
  725. if !ok {
  726. return w.WriteLine("Log level is not reconfigurable on this logger")
  727. }
  728. if len(a) == 0 {
  729. return w.WriteLine(fmt.Sprintf("Log level is: %s", logging.LevelName(ctrl.GetLevel())))
  730. }
  731. level, err := logging.ParseLevel(strings.ToLower(a[0]))
  732. if err != nil {
  733. return w.WriteLine(fmt.Sprintf("Unknown log level %s. Possible log levels: trace, debug, info, warn, error", a))
  734. }
  735. ctrl.SetLevel(level)
  736. return w.WriteLine(fmt.Sprintf("Log level is: %s", logging.LevelName(ctrl.GetLevel())))
  737. }
  738. func sshLogFormat(l *slog.Logger, fs any, a []string, w sshd.StringWriter) error {
  739. ctrl, ok := l.Handler().(interface {
  740. GetFormat() string
  741. SetFormat(string) error
  742. })
  743. if !ok {
  744. return w.WriteLine("Log format is not reconfigurable on this logger")
  745. }
  746. if len(a) == 0 {
  747. return w.WriteLine(fmt.Sprintf("Log format is: %s", ctrl.GetFormat()))
  748. }
  749. if err := ctrl.SetFormat(strings.ToLower(a[0])); err != nil {
  750. return err
  751. }
  752. return w.WriteLine(fmt.Sprintf("Log format is: %s", ctrl.GetFormat()))
  753. }
  754. func sshPrintCert(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
  755. args, ok := fs.(*sshPrintCertFlags)
  756. if !ok {
  757. return nil
  758. }
  759. cert := ifce.pki.getCertState().GetDefaultCertificate()
  760. if len(a) > 0 {
  761. vpnAddr, err := netip.ParseAddr(a[0])
  762. if err != nil {
  763. return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
  764. }
  765. if !vpnAddr.IsValid() {
  766. return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
  767. }
  768. hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
  769. if hostInfo == nil {
  770. return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn addr: %v", a[0]))
  771. }
  772. cert = hostInfo.GetCert().Certificate
  773. }
  774. if args.Json || args.Pretty {
  775. b, err := cert.MarshalJSON()
  776. if err != nil {
  777. return nil
  778. }
  779. if args.Pretty {
  780. buf := new(bytes.Buffer)
  781. err := json.Indent(buf, b, "", " ")
  782. b = buf.Bytes()
  783. if err != nil {
  784. return nil
  785. }
  786. }
  787. return w.WriteBytes(b)
  788. }
  789. if args.Raw {
  790. b, err := cert.MarshalPEM()
  791. if err != nil {
  792. return nil
  793. }
  794. return w.WriteBytes(b)
  795. }
  796. return w.WriteLine(cert.String())
  797. }
  798. func sshPrintRelays(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
  799. args, ok := fs.(*sshPrintTunnelFlags)
  800. if !ok {
  801. w.WriteLine(fmt.Sprintf("sshPrintRelays failed to convert args type"))
  802. return nil
  803. }
  804. relays := map[uint32]*HostInfo{}
  805. ifce.hostMap.Lock()
  806. maps.Copy(relays, ifce.hostMap.Relays)
  807. ifce.hostMap.Unlock()
  808. type RelayFor struct {
  809. Error error
  810. Type string
  811. State string
  812. PeerAddr netip.Addr
  813. LocalIndex uint32
  814. RemoteIndex uint32
  815. RelayedThrough []netip.Addr
  816. }
  817. type RelayOutput struct {
  818. NebulaAddr netip.Addr
  819. RelayForAddrs []RelayFor
  820. }
  821. type CmdOutput struct {
  822. Relays []*RelayOutput
  823. }
  824. co := CmdOutput{}
  825. enc := json.NewEncoder(w.GetWriter())
  826. if args.Pretty {
  827. enc.SetIndent("", " ")
  828. }
  829. for k, v := range relays {
  830. ro := RelayOutput{NebulaAddr: v.vpnAddrs[0]}
  831. co.Relays = append(co.Relays, &ro)
  832. relayHI := ifce.hostMap.QueryVpnAddr(v.vpnAddrs[0])
  833. if relayHI == nil {
  834. ro.RelayForAddrs = append(ro.RelayForAddrs, RelayFor{Error: errors.New("could not find hostinfo")})
  835. continue
  836. }
  837. for _, vpnAddr := range relayHI.relayState.CopyRelayForIps() {
  838. rf := RelayFor{Error: nil}
  839. r, ok := relayHI.relayState.GetRelayForByAddr(vpnAddr)
  840. if ok {
  841. t := ""
  842. switch r.Type {
  843. case ForwardingType:
  844. t = "forwarding"
  845. case TerminalType:
  846. t = "terminal"
  847. default:
  848. t = "unknown"
  849. }
  850. s := ""
  851. switch r.State {
  852. case Requested:
  853. s = "requested"
  854. case Established:
  855. s = "established"
  856. default:
  857. s = "unknown"
  858. }
  859. rf.LocalIndex = r.LocalIndex
  860. rf.RemoteIndex = r.RemoteIndex
  861. rf.PeerAddr = r.PeerAddr
  862. rf.Type = t
  863. rf.State = s
  864. if rf.LocalIndex != k {
  865. rf.Error = fmt.Errorf("hostmap LocalIndex '%v' does not match RelayState LocalIndex", k)
  866. }
  867. }
  868. relayedHI := ifce.hostMap.QueryVpnAddr(vpnAddr)
  869. if relayedHI != nil {
  870. rf.RelayedThrough = append(rf.RelayedThrough, relayedHI.relayState.CopyRelayIps()...)
  871. }
  872. ro.RelayForAddrs = append(ro.RelayForAddrs, rf)
  873. }
  874. }
  875. err := enc.Encode(co)
  876. if err != nil {
  877. return err
  878. }
  879. return nil
  880. }
  881. func sshPrintTunnel(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
  882. args, ok := fs.(*sshPrintTunnelFlags)
  883. if !ok {
  884. return nil
  885. }
  886. if len(a) == 0 {
  887. return w.WriteLine("No vpn address was provided")
  888. }
  889. vpnAddr, err := netip.ParseAddr(a[0])
  890. if err != nil {
  891. return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
  892. }
  893. if !vpnAddr.IsValid() {
  894. return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
  895. }
  896. hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
  897. if hostInfo == nil {
  898. return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn addr: %v", a[0]))
  899. }
  900. enc := json.NewEncoder(w.GetWriter())
  901. if args.Pretty {
  902. enc.SetIndent("", " ")
  903. }
  904. return enc.Encode(copyHostInfo(hostInfo, ifce.hostMap.GetPreferredRanges()))
  905. }
  906. func sshDeviceInfo(ifce *Interface, fs any, w sshd.StringWriter) error {
  907. data := struct {
  908. Name string `json:"name"`
  909. Cidr []netip.Prefix `json:"cidr"`
  910. }{
  911. Name: ifce.inside.Name(),
  912. Cidr: make([]netip.Prefix, len(ifce.inside.Networks())),
  913. }
  914. copy(data.Cidr, ifce.inside.Networks())
  915. flags, ok := fs.(*sshDeviceInfoFlags)
  916. if !ok {
  917. return fmt.Errorf("internal error: expected flags to be sshDeviceInfoFlags but was %+v", fs)
  918. }
  919. if flags.Json || flags.Pretty {
  920. js := json.NewEncoder(w.GetWriter())
  921. if flags.Pretty {
  922. js.SetIndent("", " ")
  923. }
  924. return js.Encode(data)
  925. } else {
  926. return w.WriteLine(fmt.Sprintf("name=%v cidr=%v", data.Name, data.Cidr))
  927. }
  928. }
  929. // sshReload reloads the config and writes the result to the client. It reloads even if the client has gone away
  930. func sshReload(l *slog.Logger, c *config.C, w sshd.StringWriter) error {
  931. writeErr := w.WriteLine("Reloading config")
  932. res := c.ReloadConfig()
  933. res.Log(l)
  934. if err := w.WriteLine(res.String()); writeErr == nil {
  935. writeErr = err
  936. }
  937. return writeErr
  938. }