server_test.go 31 KB

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