platform_windows_test.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. //go:build windows
  2. package localapi
  3. import (
  4. "errors"
  5. "net"
  6. "testing"
  7. "time"
  8. "github.com/stretchr/testify/assert"
  9. "github.com/stretchr/testify/require"
  10. "golang.org/x/sys/windows"
  11. )
  12. // TestOwnerAccepted pins which accounts may answer a nebula ctl request: the client's
  13. // own, and the accounts a daemon runs as that an ordinary user cannot create an object
  14. // owned by. Any other user is refused, since any user can create any pipe name.
  15. func TestOwnerAccepted(t *testing.T) {
  16. me, err := windows.GetCurrentProcessToken().GetTokenUser()
  17. require.NoError(t, err)
  18. wellKnown := func(typ windows.WELL_KNOWN_SID_TYPE) *windows.SID {
  19. sid, err := windows.CreateWellKnownSid(typ)
  20. require.NoError(t, err)
  21. return sid
  22. }
  23. parse := func(s string) *windows.SID {
  24. sid, err := windows.StringToSid(s)
  25. require.NoError(t, err)
  26. return sid
  27. }
  28. accepted := map[string]*windows.SID{
  29. "self": me.User.Sid,
  30. "SYSTEM": wellKnown(windows.WinLocalSystemSid),
  31. "Administrators": wellKnown(windows.WinBuiltinAdministratorsSid),
  32. }
  33. for name, sid := range accepted {
  34. assert.True(t, ownerAccepted(sid, me.User.Sid), name)
  35. }
  36. refused := map[string]*windows.SID{
  37. "Everyone": wellKnown(windows.WinWorldSid),
  38. "Users": wellKnown(windows.WinBuiltinUsersSid),
  39. "another user": parse("S-1-5-21-1-2-3-1001"),
  40. "a service's own account": parse("S-1-5-80-1-2-3-4-5"),
  41. "LocalService": wellKnown(windows.WinLocalServiceSid),
  42. }
  43. for name, sid := range refused {
  44. assert.False(t, ownerAccepted(sid, me.User.Sid), name)
  45. }
  46. }
  47. // TestDefaultPathIsProtected pins the default name to the namespace only administrators
  48. // and SYSTEM can create under, which is what lets nebula ctl trust the default name
  49. // from its spelling alone.
  50. func TestDefaultPathIsProtected(t *testing.T) {
  51. p := DefaultSocketPath()
  52. assert.True(t, isProtectedPath(p), p)
  53. assert.Equal(t, `\\.\pipe\ProtectedPrefix\Administrators\nebula-ctl`, p)
  54. }
  55. // TestConfiguredPipeNameMustBeFull: a configured name is used as given, and only a full
  56. // pipe name means anything here.
  57. func TestConfiguredPipeNameMustBeFull(t *testing.T) {
  58. p, err := SocketPathFromConfig(fakeWinConfig{CtlSocketKey: `\\.\pipe\nebula-ctl`})
  59. require.NoError(t, err)
  60. assert.Equal(t, `\\.\pipe\nebula-ctl`, p)
  61. for _, bad := range []string{"nebula-ctl", `C:\nebula\ctl.sock`, `\\.\pipe\`} {
  62. _, err := SocketPathFromConfig(fakeWinConfig{CtlSocketKey: bad})
  63. require.Error(t, err, bad)
  64. assert.ErrorContains(t, err, "full pipe name", bad)
  65. }
  66. }
  67. type fakeWinConfig map[string]string
  68. func (f fakeWinConfig) GetString(k, d string) string {
  69. if v, ok := f[k]; ok {
  70. return v
  71. }
  72. return d
  73. }
  74. func TestIsProtectedPath(t *testing.T) {
  75. assert.True(t, isProtectedPath(`\\.\pipe\ProtectedPrefix\Administrators\nebula-1`))
  76. assert.True(t, isProtectedPath(`\\.\pipe\protectedprefix\administrators\nebula-1`), "pipe names are case insensitive")
  77. assert.False(t, isProtectedPath(`\\.\pipe\ProtectedPrefix\Administrators\`), "the prefix alone names nothing")
  78. assert.False(t, isProtectedPath(`\\.\pipe\nebula-1`))
  79. assert.False(t, isProtectedPath(`\\.\pipe\ProtectedPrefix\Users\nebula-1`))
  80. }
  81. // TestListenRefused pins how a failed create is reported. Access denied under the
  82. // protected prefix is the namespace refusing a non-administrator, and says so; the same
  83. // error for an administrator, or for any name outside the prefix, is a name some other
  84. // process holds, and is errEndpointHeld so the daemon logs it as an error.
  85. func TestListenRefused(t *testing.T) {
  86. protected := `\\.\pipe\ProtectedPrefix\Administrators\nebula-1`
  87. plain := `\\.\pipe\nebula-1`
  88. err := listenRefused(protected, false, windows.ERROR_ACCESS_DENIED)
  89. assert.NotErrorIs(t, err, errEndpointHeld)
  90. assert.ErrorIs(t, err, windows.ERROR_ACCESS_DENIED)
  91. assert.Contains(t, err.Error(), "requires an administrator")
  92. assert.Contains(t, err.Error(), CtlSocketKey)
  93. assert.ErrorIs(t, listenRefused(protected, true, windows.ERROR_ACCESS_DENIED), errEndpointHeld)
  94. assert.ErrorIs(t, listenRefused(plain, false, windows.ERROR_ACCESS_DENIED), errEndpointHeld)
  95. assert.ErrorIs(t, listenRefused(protected, false, windows.ERROR_ALREADY_EXISTS), errEndpointHeld)
  96. assert.ErrorIs(t, listenRefused(plain, true, windows.ERROR_ALREADY_EXISTS), errEndpointHeld)
  97. other := errors.New("something else")
  98. assert.Same(t, other, listenRefused(protected, false, other))
  99. }
  100. // TestListenProtectedName creates and dials a name under the protected prefix. It needs
  101. // an administrator, and otherwise checks only that the refusal is the one listen
  102. // promises a non-administrator.
  103. func TestListenProtectedName(t *testing.T) {
  104. path := protectedPrefix + "nebula-test-" + t.Name()
  105. ln, err := listen(path, ownerOnly())
  106. if !isAdministrator() {
  107. require.Error(t, err)
  108. assert.Contains(t, err.Error(), "requires an administrator")
  109. t.Skip("not an administrator; the protected namespace refused the create as it should")
  110. }
  111. require.NoError(t, err)
  112. t.Cleanup(func() { ln.Close() })
  113. accepted := make(chan error, 1)
  114. go func() {
  115. conn, err := ln.Accept()
  116. if err == nil {
  117. conn.Close()
  118. }
  119. accepted <- err
  120. }()
  121. conn, err := dial(path, time.Second)
  122. require.NoError(t, err)
  123. conn.Close()
  124. require.NoError(t, <-accepted)
  125. }
  126. // TestDialAcceptsOwnPipe dials a pipe this process created outside the protected
  127. // prefix, as an override might name, which the owner check must pass: the pipe's owner
  128. // is this account, or Administrators when the test is elevated.
  129. func TestDialAcceptsOwnPipe(t *testing.T) {
  130. path := pipePrefix + "nebula-test-" + t.Name()
  131. ln, err := listen(path, ownerOnly())
  132. require.NoError(t, err)
  133. t.Cleanup(func() { ln.Close() })
  134. accepted := make(chan error, 1)
  135. go func() {
  136. conn, err := ln.Accept()
  137. if err == nil {
  138. conn.Close()
  139. }
  140. accepted <- err
  141. }()
  142. conn, err := dial(path, time.Second)
  143. require.NoError(t, err)
  144. conn.Close()
  145. require.NoError(t, <-accepted)
  146. }
  147. // TestListenRefusesHeldName pins that a name another listener already holds is refused
  148. // with errEndpointHeld rather than joined, which is what keeps an impostor from being
  149. // served alongside, or in place of, the daemon.
  150. func TestListenRefusesHeldName(t *testing.T) {
  151. path := pipePrefix + "nebula-test-" + t.Name()
  152. first, err := listen(path, ownerOnly())
  153. require.NoError(t, err)
  154. t.Cleanup(func() { first.Close() })
  155. var second net.Listener
  156. second, err = listen(path, ownerOnly())
  157. if second != nil {
  158. second.Close()
  159. }
  160. require.ErrorIs(t, err, errEndpointHeld)
  161. }