platform_unix_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. //go:build !windows
  2. package localapi
  3. import (
  4. "bufio"
  5. "context"
  6. "net"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "syscall"
  11. "testing"
  12. "time"
  13. "github.com/slackhq/nebula/test"
  14. "github.com/stretchr/testify/assert"
  15. "github.com/stretchr/testify/require"
  16. )
  17. // privateBase returns a short directory of our own, 0700, for tests to lay sockets out
  18. // under, short enough that a path under it fits sun_path.
  19. func privateBase(t *testing.T) string {
  20. t.Helper()
  21. return test.ShortTempDir(t)
  22. }
  23. // openDir returns a world writable directory under base, the shape of /tmp.
  24. func openDir(t *testing.T, base string) string {
  25. t.Helper()
  26. open := filepath.Join(base, "open")
  27. require.NoError(t, os.Mkdir(open, 0777))
  28. require.NoError(t, os.Chmod(open, 0777|os.ModeSticky))
  29. return open
  30. }
  31. // TestListenLeavesAServiceManagersDirectoryAlone covers the directory a service manager
  32. // pre-creates: systemd's RuntimeDirectory is 0755 by default. The socket is 0600, which
  33. // is what refuses other users, so the directory is served from as it came and nothing
  34. // is chmoded that the operator did not set.
  35. func TestListenLeavesAServiceManagersDirectoryAlone(t *testing.T) {
  36. base := privateBase(t)
  37. dir := filepath.Join(base, DirName)
  38. require.NoError(t, os.Mkdir(dir, 0755))
  39. s := NewServer(test.NewLogger(), basicRegistry())
  40. require.NoError(t, s.Rebind(context.Background(), filepath.Join(dir, "s.sock"), ownerOnly()))
  41. t.Cleanup(s.Stop)
  42. fi, err := os.Stat(dir)
  43. require.NoError(t, err)
  44. assert.Equal(t, os.FileMode(0755), fi.Mode().Perm(), "the directory is left exactly as it was")
  45. fi, err = os.Stat(s.Path())
  46. require.NoError(t, err)
  47. assert.Equal(t, os.FileMode(0600), fi.Mode().Perm(), "the socket is what is private")
  48. _, err = (&Client{Path: s.Path()}).Describe()
  49. require.NoError(t, err)
  50. }
  51. // TestListenCreatesOneLevelOnly: the socket directory is created when it is missing,
  52. // but nothing above it is. A runtime directory that does not exist is the operator's to
  53. // provide, and the failure says which directory could not be created.
  54. func TestListenCreatesOneLevelOnly(t *testing.T) {
  55. base := privateBase(t)
  56. s := NewServer(test.NewLogger(), basicRegistry())
  57. require.NoError(t, s.Rebind(context.Background(), filepath.Join(base, DirName, "s.sock"), ownerOnly()))
  58. s.Stop()
  59. dir := filepath.Join(base, "missing", DirName)
  60. err := s.Rebind(context.Background(), filepath.Join(dir, "s.sock"), ownerOnly())
  61. require.Error(t, err)
  62. require.ErrorContains(t, err, "could not create "+dir)
  63. assert.NoDirExists(t, filepath.Join(base, "missing"))
  64. }
  65. // TestReleaseRemovesTheDirectoryItMade: a directory listen created goes away with the
  66. // last socket in it, and one that was already there is left alone, whatever it holds.
  67. func TestReleaseRemovesTheDirectoryItMade(t *testing.T) {
  68. base := privateBase(t)
  69. made := filepath.Join(base, DirName)
  70. first, err := listen(filepath.Join(made, "a.sock"), ownerOnly())
  71. require.NoError(t, err)
  72. second, err := listen(filepath.Join(made, "b.sock"), ownerOnly())
  73. require.NoError(t, err)
  74. release(first, filepath.Join(made, "a.sock"))
  75. assert.DirExists(t, made, "b.sock is still in it")
  76. release(second, filepath.Join(made, "b.sock"))
  77. assert.NoDirExists(t, made)
  78. given := filepath.Join(base, "given")
  79. require.NoError(t, os.Mkdir(given, 0755))
  80. ln, err := listen(filepath.Join(given, "s.sock"), ownerOnly())
  81. require.NoError(t, err)
  82. release(ln, filepath.Join(given, "s.sock"))
  83. assert.DirExists(t, given)
  84. assert.NoFileExists(t, filepath.Join(given, "s.sock"))
  85. }
  86. // TestListenRestoresTheUmask: listen tightens the process umask around bind so the socket
  87. // node is never created looser than 0600. That is process wide state, so it must come
  88. // back exactly as it was, on success and on failure alike.
  89. func TestListenRestoresTheUmask(t *testing.T) {
  90. old := syscall.Umask(0022)
  91. t.Cleanup(func() { syscall.Umask(old) })
  92. base := privateBase(t)
  93. ln, err := listen(filepath.Join(base, DirName, "s.sock"), ownerOnly())
  94. require.NoError(t, err)
  95. t.Cleanup(func() { ln.Close() })
  96. assert.Equal(t, 0022, syscall.Umask(0022), "umask after a successful listen")
  97. _, err = listen(filepath.Join(base, DirName, "s.sock"), ownerOnly())
  98. require.Error(t, err, "the path is already bound")
  99. assert.Equal(t, 0022, syscall.Umask(0022), "umask after a failed listen")
  100. }
  101. // TestListenServesFromASharedDirectory: an operator who points local_api.control.socket
  102. // into a directory holding other files, /etc/nebula say, gets a socket there and keeps
  103. // the config and certs readable by everyone who could read them before.
  104. func TestListenServesFromASharedDirectory(t *testing.T) {
  105. base := privateBase(t)
  106. dir := filepath.Join(base, "etc-nebula")
  107. require.NoError(t, os.Mkdir(dir, 0755))
  108. cfg := filepath.Join(dir, "config.yml")
  109. require.NoError(t, os.WriteFile(cfg, []byte("{}\n"), 0644))
  110. s := NewServer(test.NewLogger(), basicRegistry())
  111. require.NoError(t, s.Rebind(context.Background(), filepath.Join(dir, "ctl.sock"), ownerOnly()))
  112. t.Cleanup(s.Stop)
  113. fi, err := os.Stat(dir)
  114. require.NoError(t, err)
  115. assert.Equal(t, os.FileMode(0755), fi.Mode().Perm())
  116. fi, err = os.Stat(cfg)
  117. require.NoError(t, err)
  118. assert.Equal(t, os.FileMode(0644), fi.Mode().Perm())
  119. }
  120. func TestOverlongPathIsDiagnosed(t *testing.T) {
  121. long := filepath.Join(os.TempDir(), string(make([]byte, 0)))
  122. for len(long) <= sunPathMax {
  123. long = filepath.Join(long, "abcdefghij")
  124. }
  125. s := NewServer(test.NewLogger(), basicRegistry())
  126. err := s.Rebind(context.Background(), filepath.Join(long, "s.sock"), ownerOnly())
  127. require.Error(t, err)
  128. assert.ErrorContains(t, err, "byte limit for a unix socket")
  129. }
  130. // TestPathLimitIsExact pins the boundary: sun_path has room for the NUL as well, and Go
  131. // refuses a path that fills the whole buffer, so the last usable length is one short.
  132. func TestPathLimitIsExact(t *testing.T) {
  133. assert.False(t, pathTooLong(strings.Repeat("a", maxSocketPath)))
  134. assert.True(t, pathTooLong(strings.Repeat("a", maxSocketPath+1)))
  135. assert.True(t, pathTooLong(strings.Repeat("a", sunPathMax)))
  136. }
  137. // TestNamedPathIsUsedAsGiven: a path the operator named is served from, and dialed,
  138. // wherever it is, a directory other users can write to included; the socket's own mode
  139. // is what keeps them out.
  140. func TestNamedPathIsUsedAsGiven(t *testing.T) {
  141. base := privateBase(t)
  142. path := filepath.Join(openDir(t, base), DirName, "s.sock")
  143. s := NewServer(test.NewLogger(), basicRegistry())
  144. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  145. t.Cleanup(s.Stop)
  146. _, err := (&Client{Path: path}).Describe()
  147. require.NoError(t, err)
  148. }
  149. // TestStaleSocketIsCleared: a dead socket in a good directory is the routine leftover of
  150. // a hard kill, and the daemon's stale check must see it as free to replace.
  151. func TestStaleSocketIsCleared(t *testing.T) {
  152. path := testSocket(t)
  153. require.NoError(t, os.Mkdir(filepath.Dir(path), 0700))
  154. ln, err := net.Listen(network, path)
  155. require.NoError(t, err)
  156. require.NoError(t, os.Chmod(path, 0600))
  157. // Close the listener but keep the file, the way a hard kill does.
  158. ln.(*net.UnixListener).SetUnlinkOnClose(false)
  159. require.NoError(t, ln.Close())
  160. _, statErr := os.Stat(path)
  161. require.NoError(t, statErr, "the socket file is still there")
  162. require.NoError(t, clearStale(path), "a stale socket is cleared, not reported as in use")
  163. _, statErr = os.Stat(path)
  164. assert.True(t, os.IsNotExist(statErr))
  165. }
  166. // TestClientSaysWhenTheReplyCannotBeRead: something answered on the socket, but not
  167. // with a response. That is reported as a reply that could not be read, which is what
  168. // it is, and not as a failure to reach the daemon, whether the other side read the
  169. // request first or hung up on it: a daemon that is not nebula may do either.
  170. func TestClientSaysWhenTheReplyCannotBeRead(t *testing.T) {
  171. for _, readsFirst := range []bool{true, false} {
  172. path := testSocket(t)
  173. require.NoError(t, os.Mkdir(filepath.Dir(path), 0700))
  174. ln, err := net.Listen(network, path)
  175. require.NoError(t, err)
  176. require.NoError(t, os.Chmod(path, 0600))
  177. go func() {
  178. c, err := ln.Accept()
  179. if err != nil {
  180. return
  181. }
  182. if readsFirst {
  183. _, _ = bufio.NewReader(c).ReadBytes('\n')
  184. }
  185. _, _ = c.Write([]byte("this is not a response\n"))
  186. c.Close()
  187. }()
  188. _, err = (&Client{Path: path}).Describe()
  189. var unread *ReplyError
  190. require.ErrorAs(t, err, &unread, "reads first: %v", readsFirst)
  191. assert.Contains(t, err.Error(), "the daemon's reply could not be read")
  192. ln.Close()
  193. }
  194. }
  195. // TestClientGivesUpOnASilentDaemon: a daemon that accepts and then never answers must
  196. // not hang the client for good. A script wrapping nebula ctl has no other way out.
  197. func TestClientGivesUpOnASilentDaemon(t *testing.T) {
  198. path := testSocket(t)
  199. require.NoError(t, os.Mkdir(filepath.Dir(path), 0700))
  200. ln, err := net.Listen(network, path)
  201. require.NoError(t, err)
  202. defer ln.Close()
  203. require.NoError(t, os.Chmod(path, 0600))
  204. accepted := make(chan net.Conn, 1)
  205. go func() {
  206. c, err := ln.Accept()
  207. if err == nil {
  208. accepted <- c // held open, never written to
  209. }
  210. }()
  211. start := time.Now()
  212. _, err = (&Client{Path: path, Timeout: 200 * time.Millisecond}).Describe()
  213. require.Error(t, err)
  214. require.ErrorContains(t, err, "did not answer within")
  215. assert.Less(t, time.Since(start), 5*time.Second)
  216. select {
  217. case c := <-accepted:
  218. _ = c.Close()
  219. default:
  220. }
  221. }
  222. // asUID makes the owner checks run as uid for the rest of the test. The socket the test
  223. // serves is still owned by the real user, so from uid's point of view it belongs to
  224. // somebody else: the shape of a service account's daemon, and of an impostor's.
  225. func asUID(t *testing.T, uid uint32) {
  226. t.Helper()
  227. real := currentUID
  228. currentUID = func() uint32 { return uid }
  229. t.Cleanup(func() { currentUID = real })
  230. }
  231. // TestPeerCheckRefusesAnotherUser: a server built as one user and serving as another
  232. // refuses the real user at the peer check, and the refusal reaches the client as the
  233. // daemon's answer rather than as a failure to reach it.
  234. func TestPeerCheckRefusesAnotherUser(t *testing.T) {
  235. if !peerCheckSupported {
  236. t.Skip("no peer credentials on this platform")
  237. }
  238. me := currentUID()
  239. asUID(t, me+1)
  240. s := NewServer(test.NewLogger(), basicRegistry())
  241. asUID(t, me)
  242. path := testSocket(t)
  243. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  244. defer s.Stop()
  245. _, err := (&Client{Path: path}).Describe()
  246. require.Error(t, err)
  247. require.ErrorContains(t, err, "not permitted")
  248. var refused *DaemonError
  249. assert.ErrorAs(t, err, &refused, "the daemon answered; this is not a reachability failure")
  250. }
  251. // TestListenerDoesNotUnlinkOnClose: the server unlinks its socket node itself, before
  252. // the listener closes, and the listener must not unlink again afterwards. By the time a
  253. // close happens the path may belong to a newer daemon that found the node stale and
  254. // bound its own, and a second unlink would take that daemon's socket from under it.
  255. func TestListenerDoesNotUnlinkOnClose(t *testing.T) {
  256. path := filepath.Join(privateBase(t), DirName, "s.sock")
  257. old, err := listen(path, ownerOnly())
  258. require.NoError(t, err)
  259. // A newer daemon's start, with the old node gone and the path bound afresh.
  260. require.NoError(t, os.Remove(path))
  261. next, err := net.Listen(network, path)
  262. require.NoError(t, err)
  263. t.Cleanup(func() { next.Close() })
  264. require.NoError(t, old.Close())
  265. _, err = os.Stat(path)
  266. assert.NoError(t, err, "closing the old listener left the newer daemon's socket in place")
  267. }
  268. // TestMadeDirStaysRememberedUntilItIsGone: a directory listen created that cannot be
  269. // removed right now is still ours to remove, so the record survives the failure and the
  270. // next release tries again. Only a directory that is gone is forgotten.
  271. func TestMadeDirStaysRememberedUntilItIsGone(t *testing.T) {
  272. if os.Geteuid() == 0 {
  273. t.Skip("root is not refused by directory permissions")
  274. }
  275. parent := t.TempDir()
  276. dir := filepath.Join(parent, "nebula")
  277. require.NoError(t, os.Mkdir(dir, 0o755))
  278. d := dirSet{made: map[string]bool{}}
  279. d.add(dir)
  280. // A parent without write permission refuses the rmdir.
  281. require.NoError(t, os.Chmod(parent, 0o500))
  282. t.Cleanup(func() { _ = os.Chmod(parent, 0o700) })
  283. d.remove(dir)
  284. assert.True(t, d.made[dir], "a directory that is still there must stay remembered")
  285. assert.DirExists(t, dir)
  286. require.NoError(t, os.Chmod(parent, 0o700))
  287. d.remove(dir)
  288. assert.False(t, d.made[dir])
  289. assert.NoDirExists(t, dir)
  290. // One that something else already removed is simply forgotten.
  291. gone := filepath.Join(parent, "gone")
  292. d.add(gone)
  293. d.remove(gone)
  294. assert.False(t, d.made[gone])
  295. }
  296. // TestListenRefusesADirectoryOthersCanWrite: whoever can write the socket's directory
  297. // can put a socket of their own at the path and answer the clients, who trust the path;
  298. // so a directory writable by its group or by everyone is refused, and nothing is bound.
  299. func TestListenRefusesADirectoryOthersCanWrite(t *testing.T) {
  300. for _, mode := range []os.FileMode{0775, 0757, 0777, 01777} {
  301. dir := filepath.Join(privateBase(t), "loose")
  302. require.NoError(t, os.Mkdir(dir, mode))
  303. require.NoError(t, os.Chmod(dir, mode))
  304. _, err := listen(filepath.Join(dir, "s.sock"), ownerOnly())
  305. require.ErrorContains(t, err, "writable by", "mode %04o", mode)
  306. assert.NoFileExists(t, filepath.Join(dir, "s.sock"))
  307. }
  308. }
  309. // TestListenRefusesADirectoryOwnedBySomebodyElse: a directory that is neither ours nor
  310. // root's is refused, since its owner could replace the socket. The test's own directory
  311. // stands in for somebody else's by the process pretending to be another user.
  312. func TestListenRefusesADirectoryOwnedBySomebodyElse(t *testing.T) {
  313. me := currentUID()
  314. if me == 0 {
  315. t.Skip("root's directories are accepted whoever is asking")
  316. }
  317. dir := filepath.Join(privateBase(t), "theirs")
  318. require.NoError(t, os.Mkdir(dir, 0755))
  319. asUID(t, me+1)
  320. _, err := listen(filepath.Join(dir, "s.sock"), ownerOnly())
  321. require.ErrorContains(t, err, "is owned by")
  322. assert.NoFileExists(t, filepath.Join(dir, "s.sock"))
  323. asUID(t, me)
  324. ln, err := listen(filepath.Join(dir, "s.sock"), ownerOnly())
  325. require.NoError(t, err, "the same directory, seen as our own, serves")
  326. release(ln, filepath.Join(dir, "s.sock"))
  327. }
  328. // TestListenThroughASymlinkedDirectory: a link where the directory should be is judged
  329. // by who owns it, since they decide where the path leads, and then by the directory it
  330. // resolves to. Our own link to a proper directory serves, as /var/run on linux is root's
  331. // link to /run; somebody else's link is refused, and so is a link of ours to a directory
  332. // others can write.
  333. func TestListenThroughASymlinkedDirectory(t *testing.T) {
  334. me := currentUID()
  335. if me == 0 {
  336. t.Skip("root's links are accepted whoever is asking")
  337. }
  338. base := privateBase(t)
  339. real := filepath.Join(base, "real")
  340. require.NoError(t, os.Mkdir(real, 0755))
  341. link := filepath.Join(base, "link")
  342. require.NoError(t, os.Symlink(real, link))
  343. path := filepath.Join(link, "s.sock")
  344. ln, err := listen(path, ownerOnly())
  345. require.NoError(t, err, "our link to our directory")
  346. release(ln, path)
  347. assert.NoFileExists(t, filepath.Join(real, "s.sock"))
  348. asUID(t, me+1)
  349. _, err = listen(path, ownerOnly())
  350. require.ErrorContains(t, err, "is a symlink owned by")
  351. asUID(t, me)
  352. require.NoError(t, os.Chmod(real, 0777))
  353. _, err = listen(path, ownerOnly())
  354. require.ErrorContains(t, err, "writable by everyone")
  355. }