| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- //go:build windows
- package localapi
- import (
- "errors"
- "net"
- "testing"
- "time"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "golang.org/x/sys/windows"
- )
- // TestOwnerAccepted pins which accounts may answer a nebula ctl request: the client's
- // own, and the accounts a daemon runs as that an ordinary user cannot create an object
- // owned by. Any other user is refused, since any user can create any pipe name.
- func TestOwnerAccepted(t *testing.T) {
- me, err := windows.GetCurrentProcessToken().GetTokenUser()
- require.NoError(t, err)
- wellKnown := func(typ windows.WELL_KNOWN_SID_TYPE) *windows.SID {
- sid, err := windows.CreateWellKnownSid(typ)
- require.NoError(t, err)
- return sid
- }
- parse := func(s string) *windows.SID {
- sid, err := windows.StringToSid(s)
- require.NoError(t, err)
- return sid
- }
- accepted := map[string]*windows.SID{
- "self": me.User.Sid,
- "SYSTEM": wellKnown(windows.WinLocalSystemSid),
- "Administrators": wellKnown(windows.WinBuiltinAdministratorsSid),
- "LocalService": wellKnown(windows.WinLocalServiceSid),
- "NetworkService": wellKnown(windows.WinNetworkServiceSid),
- "virtual account": parse("S-1-5-80-1-2-3-4-5"),
- }
- for name, sid := range accepted {
- assert.True(t, ownerAccepted(sid, me.User.Sid), name)
- }
- refused := map[string]*windows.SID{
- "Everyone": wellKnown(windows.WinWorldSid),
- "Users": wellKnown(windows.WinBuiltinUsersSid),
- "another user": parse("S-1-5-21-1-2-3-1001"),
- "service RID, wrong shape": parse("S-1-5-80"),
- "service RID elsewhere": parse("S-1-5-21-80-1-2-3-4"),
- }
- for name, sid := range refused {
- assert.False(t, ownerAccepted(sid, me.User.Sid), name)
- }
- }
- // TestDefaultPathIsProtected pins the default name to the namespace only administrators
- // and SYSTEM can create under, which is what lets nebula ctl trust the default name
- // from its spelling alone.
- func TestDefaultPathIsProtected(t *testing.T) {
- p := DefaultSocketPath()
- assert.True(t, isProtectedPath(p), p)
- assert.Equal(t, `\\.\pipe\ProtectedPrefix\Administrators\nebula-ctl`, p)
- }
- // TestConfiguredPipeNameMustBeFull: a configured name is used as given, and only a full
- // pipe name means anything here.
- func TestConfiguredPipeNameMustBeFull(t *testing.T) {
- p, err := SocketPathFromConfig(fakeWinConfig{CtlSocketKey: `\\.\pipe\nebula-ctl`})
- require.NoError(t, err)
- assert.Equal(t, `\\.\pipe\nebula-ctl`, p)
- for _, bad := range []string{"nebula-ctl", `C:\nebula\ctl.sock`, `\\.\pipe\`} {
- _, err := SocketPathFromConfig(fakeWinConfig{CtlSocketKey: bad})
- require.Error(t, err, bad)
- assert.ErrorContains(t, err, "full pipe name", bad)
- }
- }
- type fakeWinConfig map[string]string
- func (f fakeWinConfig) GetString(k, d string) string {
- if v, ok := f[k]; ok {
- return v
- }
- return d
- }
- func TestIsProtectedPath(t *testing.T) {
- assert.True(t, isProtectedPath(`\\.\pipe\ProtectedPrefix\Administrators\nebula-1`))
- assert.True(t, isProtectedPath(`\\.\pipe\protectedprefix\administrators\nebula-1`), "pipe names are case insensitive")
- assert.False(t, isProtectedPath(`\\.\pipe\ProtectedPrefix\Administrators\`), "the prefix alone names nothing")
- assert.False(t, isProtectedPath(`\\.\pipe\nebula-1`))
- assert.False(t, isProtectedPath(`\\.\pipe\ProtectedPrefix\Users\nebula-1`))
- }
- // TestListenRefused pins how a failed create is reported. Access denied under the
- // protected prefix is the namespace refusing a non-administrator, and says so; the same
- // error for an administrator, or for any name outside the prefix, is a name some other
- // process holds, and is errEndpointHeld so the daemon logs it as an error.
- func TestListenRefused(t *testing.T) {
- protected := `\\.\pipe\ProtectedPrefix\Administrators\nebula-1`
- plain := `\\.\pipe\nebula-1`
- err := listenRefused(protected, false, windows.ERROR_ACCESS_DENIED)
- assert.NotErrorIs(t, err, errEndpointHeld)
- assert.ErrorIs(t, err, windows.ERROR_ACCESS_DENIED)
- assert.Contains(t, err.Error(), "requires an administrator")
- assert.Contains(t, err.Error(), CtlSocketKey)
- assert.ErrorIs(t, listenRefused(protected, true, windows.ERROR_ACCESS_DENIED), errEndpointHeld)
- assert.ErrorIs(t, listenRefused(plain, false, windows.ERROR_ACCESS_DENIED), errEndpointHeld)
- assert.ErrorIs(t, listenRefused(protected, false, windows.ERROR_ALREADY_EXISTS), errEndpointHeld)
- assert.ErrorIs(t, listenRefused(plain, true, windows.ERROR_ALREADY_EXISTS), errEndpointHeld)
- other := errors.New("something else")
- assert.Same(t, other, listenRefused(protected, false, other))
- }
- // TestListenProtectedName creates and dials a name under the protected prefix. It needs
- // an administrator, and otherwise checks only that the refusal is the one listen
- // promises a non-administrator.
- func TestListenProtectedName(t *testing.T) {
- path := protectedPrefix + "nebula-test-" + t.Name()
- ln, err := listen(path, Access{Mode: ctlSocketMode})
- if !isAdministrator() {
- require.Error(t, err)
- assert.Contains(t, err.Error(), "requires an administrator")
- t.Skip("not an administrator; the protected namespace refused the create as it should")
- }
- require.NoError(t, err)
- t.Cleanup(func() { ln.Close() })
- accepted := make(chan error, 1)
- go func() {
- conn, err := ln.Accept()
- if err == nil {
- conn.Close()
- }
- accepted <- err
- }()
- conn, err := dial(path, time.Second)
- require.NoError(t, err)
- conn.Close()
- require.NoError(t, <-accepted)
- }
- // TestDialAcceptsOwnPipe dials a pipe this process created outside the protected
- // prefix, as an override might name, which the owner check must pass: the pipe's owner
- // is this account, or Administrators when the test is elevated.
- func TestDialAcceptsOwnPipe(t *testing.T) {
- path := pipePrefix + "nebula-test-" + t.Name()
- ln, err := listen(path, Access{Mode: ctlSocketMode})
- require.NoError(t, err)
- t.Cleanup(func() { ln.Close() })
- accepted := make(chan error, 1)
- go func() {
- conn, err := ln.Accept()
- if err == nil {
- conn.Close()
- }
- accepted <- err
- }()
- conn, err := dial(path, time.Second)
- require.NoError(t, err)
- conn.Close()
- require.NoError(t, <-accepted)
- }
- // TestListenRefusesHeldName pins that a name another listener already holds is refused
- // with errEndpointHeld rather than joined, which is what keeps an impostor from being
- // served alongside, or in place of, the daemon.
- func TestListenRefusesHeldName(t *testing.T) {
- path := pipePrefix + "nebula-test-" + t.Name()
- first, err := listen(path, Access{Mode: ctlSocketMode})
- require.NoError(t, err)
- t.Cleanup(func() { first.Close() })
- var second net.Listener
- second, err = listen(path, Access{Mode: ctlSocketMode})
- if second != nil {
- second.Close()
- }
- require.ErrorIs(t, err, errEndpointHeld)
- }
|