server_test.go 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997
  1. //go:build !windows
  2. package localapi
  3. import (
  4. "bytes"
  5. "context"
  6. "encoding/json"
  7. "errors"
  8. "flag"
  9. "fmt"
  10. "io"
  11. "math"
  12. "net"
  13. "os"
  14. "path/filepath"
  15. "strings"
  16. "sync"
  17. "testing"
  18. "time"
  19. "github.com/slackhq/nebula/commands"
  20. "github.com/slackhq/nebula/test"
  21. "github.com/stretchr/testify/assert"
  22. "github.com/stretchr/testify/require"
  23. "golang.org/x/sys/unix"
  24. )
  25. // testSocket returns a short socket path whose directory does not exist yet and whose
  26. // parent is private: one level below the test's own directory, so that listen has a
  27. // directory of its own to create.
  28. func testSocket(t *testing.T) string {
  29. t.Helper()
  30. return filepath.Join(test.ShortTempDir(t), "n", "s.sock")
  31. }
  32. func serve(t *testing.T, reg *commands.Registry) (*Server, string) {
  33. t.Helper()
  34. path := testSocket(t)
  35. s := NewServer(test.NewLogger(), reg)
  36. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  37. t.Cleanup(s.Stop)
  38. return s, path
  39. }
  40. // serveAt starts a server on an exact path, for tests that care where the socket is.
  41. func serveAt(t *testing.T, path string) (*Server, func()) {
  42. t.Helper()
  43. s := NewServer(test.NewLogger(), basicRegistry())
  44. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  45. return s, s.Stop
  46. }
  47. // echoCommand reports its operands, for tests on either surface.
  48. func echoCommand() *commands.Command {
  49. return &commands.Command{
  50. Name: "echo",
  51. ShortDescription: "echoes its arguments",
  52. Run: func(fs any, a []string) (commands.Result, error) {
  53. return commands.Value(fmt.Sprintf("%v", a)), nil
  54. },
  55. }
  56. }
  57. func basicRegistry() *commands.Registry {
  58. reg := commands.New()
  59. reg.Register(echoCommand())
  60. reg.Register(&commands.Command{
  61. Name: "withheld",
  62. ShortDescription: "a command this surface withholds",
  63. ForProfiling: true,
  64. Run: func(fs any, a []string) (commands.Result, error) {
  65. return commands.Value("mutated"), nil
  66. },
  67. })
  68. reg.Register(&commands.Command{
  69. Name: "boom",
  70. ShortDescription: "panics",
  71. Run: func(fs any, a []string) (commands.Result, error) {
  72. panic("this command is broken")
  73. },
  74. })
  75. return reg
  76. }
  77. // run invokes one command and returns its output and its error text. An empty error is
  78. // success: the protocol has no separate status field.
  79. func run(t *testing.T, path, command string, args ...string) (string, string) {
  80. t.Helper()
  81. res, err := (&Client{Path: path}).Invoke(command, nil, args, "")
  82. require.NoError(t, err)
  83. return textOf(res), res.Error
  84. }
  85. // textOf is what a test reads as a command's output: the text when text was asked for,
  86. // else the JSON result, unquoted when it is a JSON string so a test can compare words.
  87. func textOf(res *Response) string {
  88. if len(res.Text) > 0 {
  89. return string(res.Text)
  90. }
  91. if len(res.Result) > 0 && res.Result[0] == '"' {
  92. var s string
  93. if err := json.Unmarshal(res.Result, &s); err == nil {
  94. return s
  95. }
  96. }
  97. return string(res.Result)
  98. }
  99. func TestServeAllowedCommand(t *testing.T) {
  100. _, path := serve(t, basicRegistry())
  101. out, errOut := run(t, path, "echo", "a", "b")
  102. assert.Equal(t, "[a b]", out)
  103. assert.Empty(t, errOut, "a command that worked reports no error")
  104. }
  105. // TestWithheldCommandIsRefused checks the filter at the wire: the daemon refuses even
  106. // though the client named the command directly.
  107. func TestWithheldCommandIsRefused(t *testing.T) {
  108. _, path := serve(t, basicRegistry())
  109. out, errOut := run(t, path, "withheld")
  110. assert.Empty(t, out)
  111. assert.Contains(t, errOut, "withheld is not available on this surface")
  112. }
  113. // TestPanicInCommandDoesNotKillTheDaemon is ADR 003's requirement: a broken command
  114. // costs a connection, not the overlay.
  115. func TestPanicInCommandDoesNotKillTheDaemon(t *testing.T) {
  116. _, path := serve(t, basicRegistry())
  117. out, errOut := run(t, path, "boom")
  118. assert.Empty(t, out)
  119. assert.Contains(t, errOut, "command failed")
  120. // The server is still serving, which is the point of the test.
  121. out, errOut = run(t, path, "echo", "still", "here")
  122. assert.Equal(t, "[still here]", out)
  123. assert.Empty(t, errOut)
  124. }
  125. func TestSocketPermissions(t *testing.T) {
  126. old := unix.Umask(0)
  127. defer unix.Umask(old)
  128. _, path := serve(t, basicRegistry())
  129. fi, err := os.Stat(path)
  130. require.NoError(t, err)
  131. assert.Equal(t, os.FileMode(0600), fi.Mode().Perm(), "socket must not be reachable by other users")
  132. di, err := os.Stat(filepath.Dir(path))
  133. require.NoError(t, err)
  134. assert.Equal(t, os.FileMode(0755), di.Mode().Perm(), "a directory we create ourselves is searchable, so ServedByRoot can see into it")
  135. }
  136. // TestSocketPermissionsUnderTightUmask: the modes are pinned, not left to the umask. A
  137. // 077 umask would make the directory 0700 and hide a root daemon's socket from an
  138. // unprivileged nebula ctl.
  139. func TestSocketPermissionsUnderTightUmask(t *testing.T) {
  140. old := unix.Umask(0077)
  141. defer unix.Umask(old)
  142. _, path := serve(t, basicRegistry())
  143. fi, err := os.Stat(path)
  144. require.NoError(t, err)
  145. assert.Equal(t, os.FileMode(0600), fi.Mode().Perm())
  146. di, err := os.Stat(filepath.Dir(path))
  147. require.NoError(t, err)
  148. assert.Equal(t, os.FileMode(0755), di.Mode().Perm(), "the umask must not take the directory's searchability back")
  149. }
  150. func TestStaleSocketIsReplaced(t *testing.T) {
  151. path := testSocket(t)
  152. require.NoError(t, os.Mkdir(filepath.Dir(path), 0700))
  153. // A hard kill leaves a socket file with nothing behind it.
  154. ln, err := net.Listen("unix", path)
  155. require.NoError(t, err)
  156. require.NoError(t, ln.Close())
  157. require.NoError(t, os.WriteFile(path, nil, 0600))
  158. _ = os.Remove(path)
  159. ln, err = net.Listen("unix", path)
  160. require.NoError(t, err)
  161. fl, ok := ln.(*net.UnixListener)
  162. require.True(t, ok)
  163. fl.SetUnlinkOnClose(false)
  164. require.NoError(t, ln.Close())
  165. _, err = os.Stat(path)
  166. require.NoError(t, err, "test setup should leave a stale socket behind")
  167. s := NewServer(test.NewLogger(), basicRegistry())
  168. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()), "a stale socket must not block startup")
  169. defer s.Stop()
  170. out, _ := run(t, path, "echo", "ok")
  171. assert.Equal(t, "[ok]", out)
  172. }
  173. func TestLiveSocketIsNotStolen(t *testing.T) {
  174. first, path := serve(t, basicRegistry())
  175. require.NotNil(t, first)
  176. second := NewServer(test.NewLogger(), basicRegistry())
  177. err := second.Rebind(context.Background(), path, ownerOnly())
  178. assert.ErrorContains(t, err, "already in use", "a live endpoint must not be unlinked by a second instance")
  179. }
  180. // TestLiveSocketIsNotStolenBeforeItsChmod: between an instance's bind and its chmod the
  181. // socket is at whatever mode the umask left. A second instance starting in that window
  182. // must still see it as live, not stale: only a refused connect proves nothing is behind
  183. // the file.
  184. func TestLiveSocketIsNotStolenBeforeItsChmod(t *testing.T) {
  185. first, path := serve(t, basicRegistry())
  186. require.NotNil(t, first)
  187. require.NoError(t, os.Chmod(path, 0666))
  188. second := NewServer(test.NewLogger(), basicRegistry())
  189. err := second.Rebind(context.Background(), path, ownerOnly())
  190. require.ErrorContains(t, err, "already in use", "a live socket at any mode is not thereby dead")
  191. _, err = os.Stat(path)
  192. require.NoError(t, err, "the first instance's socket is still there")
  193. require.NoError(t, os.Chmod(path, 0600))
  194. out, _ := run(t, path, "echo", "ok")
  195. assert.Equal(t, "[ok]", out, "and still served")
  196. }
  197. func TestRebindMovesTheSocket(t *testing.T) {
  198. s, path := serve(t, basicRegistry())
  199. next := filepath.Join(filepath.Dir(path), "n.sock")
  200. require.NoError(t, s.Rebind(context.Background(), next, ownerOnly()))
  201. assert.Equal(t, next, s.Path())
  202. out, _ := run(t, next, "echo", "moved")
  203. assert.Equal(t, "[moved]", out)
  204. _, err := os.Stat(path)
  205. assert.True(t, os.IsNotExist(err), "the old socket should be unlinked")
  206. }
  207. func TestRebindFailureKeepsTheOldSocket(t *testing.T) {
  208. s, path := serve(t, basicRegistry())
  209. err := s.Rebind(context.Background(), "/nonexistent-dir-for-tests/x.sock", ownerOnly())
  210. require.Error(t, err)
  211. assert.Equal(t, path, s.Path(), "a bad path must leave the working endpoint alone")
  212. out, _ := run(t, path, "echo", "ok")
  213. assert.Equal(t, "[ok]", out)
  214. }
  215. // TestSemaphoreSurvivesRestart pins the rule that the connection limiter is made once. A
  216. // handler releases into the channel it acquired from, so if Rebind replaced it after a
  217. // Stop, a command still running across the restart would block forever on the new,
  218. // empty channel and leak its goroutine and connection.
  219. func TestSemaphoreSurvivesRestart(t *testing.T) {
  220. s := NewServer(test.NewLogger(), basicRegistry())
  221. path := testSocket(t)
  222. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  223. first := s.sem
  224. require.NotNil(t, first)
  225. s.Stop()
  226. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  227. defer s.Stop()
  228. assert.Equal(t, first, s.sem, "Rebind after Stop must keep the same semaphore")
  229. require.NoError(t, s.Rebind(context.Background(), testSocket(t), ownerOnly()))
  230. assert.Equal(t, first, s.sem, "Rebind must keep the same semaphore")
  231. }
  232. // TestRestartIsWatchedOnItsOwnContext: the ctx watcher is installed by the first bind of
  233. // a run and cleared by Stop, so a server stopped by one context's cancellation and
  234. // started again under another is stopped when that second context is cancelled, not
  235. // left serving until the process exits.
  236. func TestRestartIsWatchedOnItsOwnContext(t *testing.T) {
  237. s := NewServer(test.NewLogger(), basicRegistry())
  238. path := testSocket(t)
  239. ctxA, cancelA := context.WithCancel(context.Background())
  240. require.NoError(t, s.Rebind(ctxA, path, ownerOnly()))
  241. cancelA()
  242. require.Eventually(t, func() bool { return s.Path() == "" }, time.Second, 10*time.Millisecond, "the first context must stop the server")
  243. ctxB, cancelB := context.WithCancel(context.Background())
  244. require.NoError(t, s.Rebind(ctxB, path, ownerOnly()))
  245. defer s.Stop()
  246. assert.Equal(t, path, s.Path())
  247. cancelB()
  248. require.Eventually(t, func() bool { return s.Path() == "" }, time.Second, 10*time.Millisecond, "the second context must stop the server too")
  249. _, err := os.Stat(path)
  250. assert.True(t, os.IsNotExist(err), "the socket must be removed when the second context stops the server")
  251. }
  252. // TestStopDetachesTheWatcher: a server stopped by Stop and started again under another
  253. // context belongs to that context alone. The one that started the earlier run, ending
  254. // later, must not take the new run down with it.
  255. func TestStopDetachesTheWatcher(t *testing.T) {
  256. s := NewServer(test.NewLogger(), basicRegistry())
  257. path := testSocket(t)
  258. ctxA, cancelA := context.WithCancel(context.Background())
  259. t.Cleanup(cancelA)
  260. require.NoError(t, s.Rebind(ctxA, path, ownerOnly()))
  261. s.Stop()
  262. ctxB, cancelB := context.WithCancel(context.Background())
  263. t.Cleanup(cancelB)
  264. require.NoError(t, s.Rebind(ctxB, path, ownerOnly()))
  265. t.Cleanup(s.Stop)
  266. cancelA()
  267. time.Sleep(50 * time.Millisecond)
  268. assert.Equal(t, path, s.Path(), "the first run's context has nothing to do with the second run")
  269. _, err := (&Client{Path: path}).Describe()
  270. require.NoError(t, err)
  271. cancelB()
  272. require.Eventually(t, func() bool { return s.Path() == "" }, time.Second, 10*time.Millisecond, "the second run's context stops it")
  273. }
  274. func TestVersionMismatchIsExplained(t *testing.T) {
  275. _, path := serve(t, basicRegistry())
  276. conn, err := net.Dial("unix", path)
  277. require.NoError(t, err)
  278. defer conn.Close()
  279. body, _ := json.Marshal(Request{Version: ProtocolVersion + 1, Command: "echo"})
  280. _, err = conn.Write(append(body, '\n'))
  281. require.NoError(t, err)
  282. res, err := readResponse(conn)
  283. require.NoError(t, err)
  284. assert.Contains(t, res.Error, "different versions")
  285. }
  286. func TestOversizedRequestIsRejected(t *testing.T) {
  287. _, path := serve(t, basicRegistry())
  288. conn, err := net.Dial("unix", path)
  289. require.NoError(t, err)
  290. defer conn.Close()
  291. _, err = conn.Write(append(bytes.Repeat([]byte("a"), MaxRequestBytes+10), '\n'))
  292. if err == nil {
  293. res, rerr := readResponse(conn)
  294. require.NoError(t, rerr)
  295. assert.NotEmpty(t, res.Error)
  296. }
  297. }
  298. // TestConcurrentClientsAreBounded checks that going over the connection cap is answered
  299. // rather than queued, so a burst of clients cannot pile up against the packet path.
  300. func TestConcurrentClientsAreBounded(t *testing.T) {
  301. reg := commands.New()
  302. release := make(chan struct{})
  303. reg.Register(&commands.Command{
  304. Name: "block",
  305. ShortDescription: "blocks until released",
  306. Run: func(fs any, a []string) (commands.Result, error) {
  307. <-release
  308. return commands.Value("done"), nil
  309. },
  310. })
  311. s := NewServer(test.NewLogger(), reg)
  312. s.maxConns = 2
  313. path := testSocket(t)
  314. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  315. defer s.Stop()
  316. var wg sync.WaitGroup
  317. for i := 0; i < 2; i++ {
  318. wg.Add(1)
  319. go func() {
  320. defer wg.Done()
  321. _, _ = (&Client{Path: path}).Invoke("block", nil, nil, "")
  322. }()
  323. }
  324. // Give the two blockers time to occupy the slots.
  325. time.Sleep(200 * time.Millisecond)
  326. res, err := (&Client{Path: path}).Invoke("block", nil, nil, "")
  327. require.NoError(t, err)
  328. assert.Contains(t, res.Error, "busy")
  329. close(release)
  330. wg.Wait()
  331. }
  332. // TestSlowCommandIsNamedInTheLog: a command that does not return holds its slot for
  333. // good, and enough of them leave the socket answering only "busy". Both are logged, the
  334. // command by name once it has run past slowAfter and each refused connection as it
  335. // happens, so a busy socket can be traced to what wedged it.
  336. func TestSlowCommandIsNamedInTheLog(t *testing.T) {
  337. reg := commands.New()
  338. release := make(chan struct{})
  339. reg.Register(&commands.Command{
  340. Name: "block",
  341. ShortDescription: "blocks until released",
  342. Run: func(fs any, a []string) (commands.Result, error) {
  343. <-release
  344. return commands.Value("done"), nil
  345. },
  346. })
  347. var logs lockedBuffer
  348. s := NewServer(test.NewLoggerWithOutput(&logs), reg)
  349. s.maxConns = 1
  350. s.slowAfter = 20 * time.Millisecond
  351. path := testSocket(t)
  352. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  353. defer s.Stop()
  354. var wg sync.WaitGroup
  355. wg.Add(1)
  356. go func() {
  357. defer wg.Done()
  358. _, _ = (&Client{Path: path}).Invoke("block", nil, []string{"x"}, "")
  359. }()
  360. require.Eventually(t, func() bool {
  361. return strings.Contains(logs.String(), "still running")
  362. }, 2*time.Second, 10*time.Millisecond)
  363. assert.Contains(t, logs.String(), "command=block")
  364. assert.Contains(t, logs.String(), "args=[x]")
  365. res, err := (&Client{Path: path}).Invoke("block", nil, nil, "")
  366. require.NoError(t, err)
  367. assert.Contains(t, res.Error, "busy")
  368. assert.Contains(t, logs.String(), "at capacity")
  369. close(release)
  370. wg.Wait()
  371. }
  372. // TestPanickedCommandIsNotReportedAsStillRunning: the slow command line is about a slot
  373. // that is still held. A command that panicked has given its slot back, and the client
  374. // its answer, so no line must claim otherwise after the fact.
  375. func TestPanickedCommandIsNotReportedAsStillRunning(t *testing.T) {
  376. reg := commands.New()
  377. reg.Register(&commands.Command{
  378. Name: "boom",
  379. ShortDescription: "panics",
  380. Run: func(fs any, a []string) (commands.Result, error) {
  381. panic("this command is broken")
  382. },
  383. })
  384. var logs lockedBuffer
  385. s := NewServer(test.NewLoggerWithOutput(&logs), reg)
  386. s.slowAfter = 20 * time.Millisecond
  387. path := testSocket(t)
  388. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  389. defer s.Stop()
  390. res, err := (&Client{Path: path}).Invoke("boom", nil, nil, "")
  391. require.NoError(t, err)
  392. assert.Equal(t, "command failed", res.Error)
  393. time.Sleep(100 * time.Millisecond)
  394. assert.Contains(t, logs.String(), "Recovered from panic")
  395. assert.NotContains(t, logs.String(), "still running")
  396. }
  397. // lockedBuffer is a strings.Builder the server's goroutines and the test can share.
  398. type lockedBuffer struct {
  399. mu sync.Mutex
  400. b strings.Builder
  401. }
  402. func (l *lockedBuffer) Write(p []byte) (int, error) {
  403. l.mu.Lock()
  404. defer l.mu.Unlock()
  405. return l.b.Write(p)
  406. }
  407. func (l *lockedBuffer) String() string {
  408. l.mu.Lock()
  409. defer l.mu.Unlock()
  410. return l.b.String()
  411. }
  412. // flakyListener fails its first few Accepts the way a process that is out of file
  413. // descriptors does, then behaves.
  414. type flakyListener struct {
  415. net.Listener
  416. failures int
  417. }
  418. func (f *flakyListener) Accept() (net.Conn, error) {
  419. if f.failures > 0 {
  420. f.failures--
  421. return nil, &net.OpError{Op: "accept", Net: "unix", Err: errors.New("too many open files")}
  422. }
  423. return f.Listener.Accept()
  424. }
  425. // TestAcceptSurvivesTransientErrors: an Accept that fails for a reason other than the
  426. // listener being closed is retried, so a moment at the descriptor limit does not take
  427. // the control socket down for the rest of the process. Closing the listener is still
  428. // what ends the loop.
  429. func TestAcceptSurvivesTransientErrors(t *testing.T) {
  430. path := testSocket(t)
  431. require.NoError(t, os.Mkdir(filepath.Dir(path), 0700))
  432. ln, err := listen(path, Access{Mode: ctlSocketMode})
  433. require.NoError(t, err)
  434. s := NewServer(test.NewLogger(), basicRegistry())
  435. done := make(chan struct{})
  436. go func() {
  437. defer close(done)
  438. s.accept(&flakyListener{Listener: ln, failures: 3}, make(chan struct{}, 1))
  439. }()
  440. out, errText := run(t, path, "echo", "still", "here")
  441. assert.Empty(t, errText)
  442. assert.Equal(t, "[still here]", out)
  443. require.NoError(t, ln.Close())
  444. select {
  445. case <-done:
  446. case <-time.After(5 * time.Second):
  447. t.Fatal("accept loop did not exit when the listener was closed")
  448. }
  449. }
  450. func TestStopRemovesTheSocket(t *testing.T) {
  451. s, path := serve(t, basicRegistry())
  452. s.Stop()
  453. _, err := os.Stat(path)
  454. assert.True(t, os.IsNotExist(err))
  455. }
  456. func TestContextCancellationStops(t *testing.T) {
  457. ctx, cancel := context.WithCancel(context.Background())
  458. path := testSocket(t)
  459. s := NewServer(test.NewLogger(), basicRegistry())
  460. require.NoError(t, s.Rebind(ctx, path, ownerOnly()))
  461. cancel()
  462. assert.Eventually(t, func() bool {
  463. _, err := os.Stat(path)
  464. return os.IsNotExist(err)
  465. }, 2*time.Second, 20*time.Millisecond)
  466. }
  467. // TestNamedParametersDriveFlags is the point of the protocol: a caller passes options by
  468. // name, with JSON's own types, and never assembles a command line.
  469. // TestStringParamsNumbers: JSON numbers arrive as float64. Whole ones render as integers
  470. // so int flags accept them, and a whole one outside int64 is refused rather than
  471. // converted, since Go's float-to-int conversion of an out-of-range value is unspecified
  472. // and would set a number the caller never sent.
  473. func TestStringParamsNumbers(t *testing.T) {
  474. cases := map[string]struct {
  475. in float64
  476. want string
  477. wantErr bool
  478. }{
  479. "whole": {in: 3, want: "3"},
  480. "negative whole": {in: -1, want: "-1"},
  481. "fraction": {in: 1.5, want: "1.5"},
  482. "min int64": {in: math.MinInt64, want: "-9223372036854775808"},
  483. "largest in range": {in: 9223372036854774784, want: "9223372036854774784"},
  484. "2^63": {in: 9223372036854775808, wantErr: true},
  485. "1e19": {in: 1e19, wantErr: true},
  486. "below min int64": {in: -9.3e18, wantErr: true},
  487. }
  488. for name, tc := range cases {
  489. t.Run(name, func(t *testing.T) {
  490. out, err := stringParams(map[string]any{"n": tc.in})
  491. if tc.wantErr {
  492. require.Error(t, err)
  493. require.ErrorContains(t, err, `parameter "n" is `)
  494. assert.ErrorContains(t, err, "outside the range of a 64-bit integer")
  495. return
  496. }
  497. require.NoError(t, err)
  498. assert.Equal(t, tc.want, out["n"])
  499. })
  500. }
  501. }
  502. func TestNamedParametersDriveFlags(t *testing.T) {
  503. reg := commands.New()
  504. reg.Register(&commands.Command{
  505. Name: "report",
  506. ShortDescription: "reports its flags",
  507. Flags: func() (*flag.FlagSet, any) {
  508. fl := flag.NewFlagSet("", flag.ContinueOnError)
  509. s := struct {
  510. Verbose bool
  511. Label string
  512. }{}
  513. fl.BoolVar(&s.Verbose, "verbose", false, "say more")
  514. fl.StringVar(&s.Label, "label", "none", "a label")
  515. return fl, &s
  516. },
  517. Run: func(fs any, a []string) (commands.Result, error) {
  518. s := fs.(*struct {
  519. Verbose bool
  520. Label string
  521. })
  522. return commands.Value(fmt.Sprintf("verbose=%v label=%s args=%v", s.Verbose, s.Label, a)), nil
  523. },
  524. })
  525. _, path := serve(t, reg)
  526. res, err := (&Client{Path: path}).Invoke("report",
  527. map[string]string{"verbose": "true", "label": "here"},
  528. []string{"an-operand"},
  529. "",
  530. )
  531. require.NoError(t, err)
  532. assert.Empty(t, res.Error)
  533. assert.Equal(t, "verbose=true label=here args=[an-operand]", textOf(res))
  534. }
  535. // TestUnknownParameterIsReported: a misspelled parameter is an error rather than a
  536. // silently applied default, which is the difference between an API and a guess.
  537. func TestUnknownParameterIsReported(t *testing.T) {
  538. reg := commands.New()
  539. reg.Register(&commands.Command{
  540. Name: "report",
  541. ShortDescription: "takes a flag",
  542. Flags: func() (*flag.FlagSet, any) {
  543. fl := flag.NewFlagSet("", flag.ContinueOnError)
  544. fl.Bool("verbose", false, "say more")
  545. return fl, nil
  546. },
  547. Run: func(fs any, a []string) (commands.Result, error) { return commands.Nothing(), nil },
  548. })
  549. _, path := serve(t, reg)
  550. res, err := (&Client{Path: path}).Invoke("report", map[string]string{"verbos": "true"}, nil, "")
  551. require.NoError(t, err)
  552. assert.Contains(t, res.Error, `report has no parameter "verbos"`)
  553. }
  554. func TestParametersOnACommandThatTakesNoneAreReported(t *testing.T) {
  555. _, path := serve(t, basicRegistry())
  556. res, err := (&Client{Path: path}).Invoke("echo", map[string]string{"nope": "true"}, nil, "")
  557. require.NoError(t, err)
  558. assert.Contains(t, res.Error, "echo takes no parameters")
  559. }
  560. // TestJSONOutputIsAJSONValue: a command that emits JSON lands in the response as JSON,
  561. // not as a string a caller has to parse a second time.
  562. func TestJSONOutputIsAJSONValue(t *testing.T) {
  563. reg := commands.New()
  564. reg.Register(&commands.Command{
  565. Name: "emit",
  566. ShortDescription: "emits json",
  567. Run: func(fs any, a []string) (commands.Result, error) {
  568. return commands.Value(map[string]any{"hosts": []int{1, 2, 3}}), nil
  569. },
  570. })
  571. _, path := serve(t, reg)
  572. res, err := (&Client{Path: path}).Invoke("emit", nil, nil, "")
  573. require.NoError(t, err)
  574. var decoded struct {
  575. Hosts []int `json:"hosts"`
  576. }
  577. require.NoError(t, json.Unmarshal(res.Result, &decoded))
  578. assert.Equal(t, []int{1, 2, 3}, decoded.Hosts)
  579. }
  580. // TestTextOutputIsAJSONString: everything else round trips as a string, unchanged.
  581. func TestTextOutputIsAJSONString(t *testing.T) {
  582. _, path := serve(t, basicRegistry())
  583. res, err := (&Client{Path: path}).Invoke("echo", nil, []string{"hi"}, "")
  584. require.NoError(t, err)
  585. var s string
  586. require.NoError(t, json.Unmarshal(res.Result, &s))
  587. assert.Equal(t, "[hi]", s)
  588. }
  589. func TestDescribeReportsFlagTypes(t *testing.T) {
  590. _, path := serve(t, basicRegistry())
  591. schema, err := (&Client{Path: path}).Describe()
  592. require.NoError(t, err)
  593. names := make([]string, 0, len(schema))
  594. for _, c := range schema {
  595. names = append(names, string(c.Name))
  596. }
  597. assert.Contains(t, names, "echo")
  598. assert.NotContains(t, names, "withheld", "the schema advertises only what this surface serves")
  599. }
  600. // TestPresentationParametersAreRefused: the schema lists json and pretty on every
  601. // command, because the command lines take them, but a request has one way to choose its
  602. // rendering, the format field. A client that sends them as parameters is pointed there
  603. // rather than given a second, weaker way to say the same thing.
  604. func TestPresentationParametersAreRefused(t *testing.T) {
  605. reg := commands.New()
  606. reg.Register(&commands.Command{
  607. Name: "pair",
  608. ShortDescription: "returns a pair",
  609. Run: func(fs any, a []string) (commands.Result, error) {
  610. return commands.Value(map[string]int{"a": 1}), nil
  611. },
  612. Text: func(v any, w io.Writer) error {
  613. _, err := fmt.Fprintln(w, "a is one")
  614. return err
  615. },
  616. })
  617. _, path := serve(t, reg)
  618. c := &Client{Path: path}
  619. for _, name := range []string{"json", "pretty"} {
  620. res, err := c.Invoke("pair", map[string]string{name: "true"}, nil, "text")
  621. require.NoError(t, err)
  622. assert.Contains(t, res.Error, name+" is a command line flag, not a parameter")
  623. assert.Contains(t, res.Error, "format")
  624. assert.Empty(t, res.Result, "a refused request runs nothing")
  625. }
  626. // The format field is the one that counts.
  627. res, err := c.Invoke("pair", nil, nil, "text")
  628. require.NoError(t, err)
  629. require.Empty(t, res.Error)
  630. assert.Equal(t, "a is one\n", textOf(res))
  631. res, err = c.Invoke("pair", nil, nil, "")
  632. require.NoError(t, err)
  633. require.Empty(t, res.Error)
  634. assert.JSONEq(t, `{"a":1}`, string(res.Result), "the API default is JSON")
  635. }
  636. // TestTextRequestToATextlessCommand: a command with no human rendering still answers a
  637. // text request when the answer is a refusal, since a refusal always has one. Only a
  638. // successful result has nothing to say in text, and that is reported then, by the daemon.
  639. func TestTextRequestToATextlessCommand(t *testing.T) {
  640. reg := commands.New()
  641. reg.Register(&commands.Command{
  642. Name: "tunnel",
  643. ShortDescription: "data on success, a refusal without an operand",
  644. Run: func(fs any, a []string) (commands.Result, error) {
  645. if len(a) == 0 {
  646. return commands.Failed("No vpn address was provided"), nil
  647. }
  648. return commands.Value(map[string]int{"index": 7}), nil
  649. },
  650. JSONOnly: true,
  651. })
  652. _, path := serve(t, reg)
  653. c := &Client{Path: path}
  654. res, err := c.Invoke("tunnel", nil, nil, "text")
  655. require.NoError(t, err)
  656. assert.Equal(t, "No vpn address was provided\n", res.Error, "the refusal is rendered, not the missing text form")
  657. res, err = c.Invoke("tunnel", nil, []string{"10.0.0.1"}, "text")
  658. require.NoError(t, err)
  659. assert.Contains(t, res.Error, "tunnel has no human readable output yet; pass -json or -pretty")
  660. assert.Empty(t, res.Result)
  661. res, err = c.Invoke("tunnel", nil, []string{"10.0.0.1"}, "json")
  662. require.NoError(t, err)
  663. require.Empty(t, res.Error)
  664. assert.JSONEq(t, `{"index":7}`, string(res.Result))
  665. // A refusal asked for as JSON carries the outcome for a program and the reason in
  666. // Error, so a client exits non-zero on the envelope alone, never by sniffing the
  667. // payload for an ok field some unrelated result might also have.
  668. res, err = c.Invoke("tunnel", nil, nil, "json")
  669. require.NoError(t, err)
  670. assert.Equal(t, "No vpn address was provided", res.Error)
  671. assert.JSONEq(t, `{"ok":false,"reason":"No vpn address was provided"}`, string(res.Result))
  672. }
  673. // TestRebindAfterFailedBindStillStopsOnCancel covers a daemon whose initial listen
  674. // failed and which later Rebinds onto a good path from a reloaded config. The context
  675. // watcher has to be installed by that Rebind, or the socket outlives the daemon.
  676. func TestRebindAfterFailedBindStillStopsOnCancel(t *testing.T) {
  677. ctx, cancel := context.WithCancel(context.Background())
  678. defer cancel()
  679. base := test.ShortTempDir(t)
  680. // listen creates one level only, so a socket directory under a parent that is not
  681. // there yet fails to bind, the shape of a runtime directory that does not exist.
  682. s := NewServer(test.NewLogger(), basicRegistry())
  683. require.Error(t, s.Rebind(ctx, filepath.Join(base, "missing", "n", "s.sock"), ownerOnly()))
  684. good := filepath.Join(base, "n", "s.sock")
  685. require.NoError(t, s.Rebind(ctx, good, ownerOnly()))
  686. t.Cleanup(s.Stop)
  687. _, err := (&Client{Path: good}).Describe()
  688. require.NoError(t, err)
  689. cancel()
  690. assert.Eventually(t, func() bool {
  691. _, err := os.Stat(good)
  692. return os.IsNotExist(err)
  693. }, 2*time.Second, 20*time.Millisecond)
  694. }
  695. // TestCommandRegisteredAfterStartIsServed: the server filters the registry per request,
  696. // so a command an embedder registers after wiring the socket is served, and described,
  697. // just as it would be over ssh.
  698. func TestCommandRegisteredAfterStartIsServed(t *testing.T) {
  699. reg := basicRegistry()
  700. _, path := serve(t, reg)
  701. reg.Register(&commands.Command{
  702. Name: "late",
  703. ShortDescription: "registered after the server started",
  704. Run: func(fs any, a []string) (commands.Result, error) {
  705. return commands.Value("late"), nil
  706. },
  707. })
  708. c := &Client{Path: path}
  709. schema, err := c.Describe()
  710. require.NoError(t, err)
  711. names := make([]string, 0, len(schema))
  712. for _, cs := range schema {
  713. names = append(names, string(cs.Name))
  714. }
  715. assert.Contains(t, names, "late")
  716. res, err := c.Invoke("late", nil, nil, "")
  717. require.NoError(t, err)
  718. require.Empty(t, res.Error)
  719. assert.JSONEq(t, `"late"`, string(res.Result))
  720. }
  721. // TestAfterReplyRunsOnceTheResponseIsOut: the hook blocks until the test releases it, so
  722. // the only way a response can arrive is if it was written before the hook began.
  723. func TestAfterReplyRunsOnceTheResponseIsOut(t *testing.T) {
  724. release := make(chan struct{})
  725. acted := make(chan struct{})
  726. reg := commands.New()
  727. reg.Register(&commands.Command{
  728. Name: "act",
  729. ShortDescription: "replies, then acts",
  730. Run: func(fs any, a []string) (commands.Result, error) {
  731. return commands.Did("about to act"), nil
  732. },
  733. AfterReply: func() {
  734. <-release
  735. close(acted)
  736. },
  737. })
  738. _, path := serve(t, reg)
  739. res, err := (&Client{Path: path}).Invoke("act", nil, nil, "")
  740. require.NoError(t, err)
  741. require.Empty(t, res.Error)
  742. select {
  743. case <-acted:
  744. t.Fatal("AfterReply ran before the response was delivered")
  745. default:
  746. }
  747. close(release)
  748. select {
  749. case <-acted:
  750. case <-time.After(2 * time.Second):
  751. t.Fatal("AfterReply never ran")
  752. }
  753. }
  754. // TestRebindRefusesDuringShutdown: a reload that lands after the daemon's context is
  755. // cancelled must not bring the endpoint back. Stop is not terminal, so Rebind has to
  756. // take the answer from the context rather than from the running flag.
  757. func TestRebindRefusesDuringShutdown(t *testing.T) {
  758. ctx, cancel := context.WithCancel(context.Background())
  759. defer cancel()
  760. // Built by hand rather than through serve: the context watcher is installed once,
  761. // by the first bind, and this test needs it on a context it can cancel.
  762. s := NewServer(test.NewLogger(), basicRegistry())
  763. require.NoError(t, s.Rebind(ctx, testSocket(t), ownerOnly()))
  764. t.Cleanup(s.Stop)
  765. cancel()
  766. assert.Eventually(t, func() bool { return s.Path() == "" }, 2*time.Second, 20*time.Millisecond, "the watcher stops the server")
  767. next := testSocket(t)
  768. err := s.Rebind(ctx, next, ownerOnly())
  769. require.Error(t, err)
  770. require.ErrorContains(t, err, "shutting down")
  771. assert.Empty(t, s.Path(), "the server stays stopped")
  772. _, statErr := os.Stat(next)
  773. assert.True(t, os.IsNotExist(statErr), "and the refused socket is not left on disk")
  774. }
  775. // TestRebindAndStopSerialize hammers Rebind against Stop. Whichever runs last decides
  776. // the final state, so after the dust settles a Stop must leave nothing listening and no
  777. // socket file behind, on either path.
  778. func TestRebindAndStopSerialize(t *testing.T) {
  779. a := testSocket(t)
  780. b := testSocket(t)
  781. for i := 0; i < 50; i++ {
  782. s := NewServer(test.NewLogger(), basicRegistry())
  783. require.NoError(t, s.Rebind(context.Background(), a, ownerOnly()))
  784. var wg sync.WaitGroup
  785. wg.Add(2)
  786. go func() {
  787. defer wg.Done()
  788. _ = s.Rebind(context.Background(), b, ownerOnly())
  789. }()
  790. go func() {
  791. defer wg.Done()
  792. s.Stop()
  793. }()
  794. wg.Wait()
  795. // The Rebind may have run after the Stop, which restarts the server by design.
  796. // A final Stop then has to take everything down.
  797. s.Stop()
  798. assert.Empty(t, s.Path())
  799. for _, p := range []string{a, b} {
  800. _, err := os.Stat(p)
  801. assert.True(t, os.IsNotExist(err), "iteration %d: %s left behind", i, p)
  802. }
  803. }
  804. }
  805. // TestUnknownRequestFieldIsRefused: a request with a field the protocol does not define
  806. // is answered with the field named, the way an unknown parameter is, and not run.
  807. func TestUnknownRequestFieldIsRefused(t *testing.T) {
  808. _, path := serve(t, basicRegistry())
  809. conn, err := dial(path, time.Second)
  810. require.NoError(t, err)
  811. defer conn.Close()
  812. require.NoError(t, writeFrame(conn, map[string]any{"version": ProtocolVersion, "command": "echo", "arguments": []string{"x"}}))
  813. var res Response
  814. require.NoError(t, readFrame(conn, MaxResponseBytes, "response", &res, false))
  815. assert.Contains(t, res.Error, `unknown field "arguments"`)
  816. assert.Empty(t, res.Result)
  817. }
  818. // TestTextOutputKeepsTheBytes: a text rendering travels as the bytes the command wrote.
  819. // A JSON string would have had to be UTF-8, and a byte that is not would have come out
  820. // as U+FFFD, where the ssh server passes it through.
  821. func TestTextOutputKeepsTheBytes(t *testing.T) {
  822. reg := commands.New()
  823. raw := []byte{'n', 'a', 'm', 'e', ' ', 0xff, 0xfe, '\n'}
  824. reg.Register(&commands.Command{
  825. Name: "raw",
  826. ShortDescription: "writes what a certificate held",
  827. Run: func(fs any, a []string) (commands.Result, error) { return commands.Value("raw"), nil },
  828. Text: func(v any, w io.Writer) error {
  829. _, err := w.Write(raw)
  830. return err
  831. },
  832. })
  833. _, path := serve(t, reg)
  834. res, err := (&Client{Path: path}).Invoke("raw", nil, nil, "text")
  835. require.NoError(t, err)
  836. assert.Empty(t, res.Error)
  837. assert.Equal(t, raw, res.Text)
  838. assert.Empty(t, res.Result, "text is not also carried as JSON")
  839. }