platform_unix_test.go 17 KB

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