platform_windows_test.go 6.6 KB

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