mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 01:26:52 +01:00
Add support for accessing the previously ssh-only debug commands via `nebula ctl <command>.` - unix socket (named pipe on windows) for communication - sockets scoped to the user + an optional group - in cmd/nebula and cmd/nebula-service, but not nebula.Main Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
187 lines
6.4 KiB
Go
187 lines
6.4 KiB
Go
//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),
|
|
}
|
|
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"),
|
|
"a service's own account": parse("S-1-5-80-1-2-3-4-5"),
|
|
"LocalService": wellKnown(windows.WinLocalServiceSid),
|
|
}
|
|
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, ownerOnly())
|
|
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, ownerOnly())
|
|
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, ownerOnly())
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { first.Close() })
|
|
|
|
var second net.Listener
|
|
second, err = listen(path, ownerOnly())
|
|
if second != nil {
|
|
second.Close()
|
|
}
|
|
require.ErrorIs(t, err, errEndpointHeld)
|
|
}
|