| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- //go:build windows
- package localapi
- import (
- "errors"
- "fmt"
- "log/slog"
- "net"
- "strings"
- "time"
- "github.com/Microsoft/go-winio"
- "golang.org/x/sys/windows"
- )
- // pipePrefix is where windows named pipes live. The pipe's own security descriptor
- // carries the access control that the socket's mode carries on unix.
- const pipePrefix = `\\.\pipe\`
- // protectedPrefix is the pipe namespace that only administrators and SYSTEM may create
- // names under; the kernel refuses everyone else. The default name lives there, so the
- // name itself says who could have created the pipe answering it: no ordinary user can
- // have put anything under it ahead of the daemon. Tailscale, WireGuard and NetBird keep
- // their daemon pipes there for the same reason. nebula on windows is already an
- // administrator, since creating the tun adapter requires it.
- const protectedPrefix = pipePrefix + `ProtectedPrefix\Administrators\`
- const network = "pipe"
- // currentUID is the identity the unix owner checks are made for. There is no uid here
- // and no per-connection check that would read one, so the value is never consulted; it
- // exists so a Server is built the same way on every platform.
- var currentUID = func() uint32 { return 0 }
- // logListening is the line written when the pipe is up. It says which layer is doing
- // the access control, so nobody assumes a per-connection check that is not running:
- // there is none here, the pipe's security descriptor admits only the accounts listed in
- // ownerOnlySDDL, and the kernel enforces that at connect time.
- func logListening(l *slog.Logger, path string, acc Access) {
- l.Info("Local control socket listening, access restricted by the pipe's security descriptor",
- "path", path,
- "access", acc.String(),
- "reason", "the security descriptor built at listen time is the access check; the connecting process is not identified separately",
- )
- }
- // infoSupported is false here. A pipe has no mode to open to other accounts, and the
- // security descriptor listen builds admits the daemon's account alone, which is the
- // opposite of what the info API is for. Serving it on windows means designing which
- // accounts a pipe admits, and that has not been done.
- const infoSupported = false
- // listen creates the named pipe at path with a security descriptor that admits the user
- // nebula runs as, SYSTEM and Administrators, and the group acc names, if any, for
- // reading and writing, which is what connecting to a pipe takes. acc's mode means
- // nothing to a pipe.
- //
- // The first instance is created, never opened, so a name some other process already
- // holds is refused rather than joined: that process would otherwise answer clients in
- // nebula's place. Under protectedPrefix only an administrator could hold it; anywhere
- // else any account could. Either way the refusal is reported as errEndpointHeld for the
- // caller to log as the event it is, except when the refusal is the namespace's own: a
- // daemon that is not an administrator cannot create under protectedPrefix at all, and
- // that is told apart so the operator is sent to elevate or to name a pipe elsewhere
- // rather than to look for a process that is not there.
- func listen(path string, acc Access) (net.Listener, error) {
- sddl, err := sddlFor(acc)
- if err != nil {
- return nil, fmt.Errorf("could not build a security descriptor for %s: %w", path, err)
- }
- ln, err := winio.ListenPipe(path, &winio.PipeConfig{
- SecurityDescriptor: sddl,
- InputBufferSize: int32(MaxRequestBytes),
- OutputBufferSize: 64 * 1024,
- })
- if err != nil {
- return nil, listenRefused(path, isAdministrator(), err)
- }
- return ln, nil
- }
- // listenRefused turns a failure to create the pipe at path into the error the caller
- // logs. admin says whether this process may create under protectedPrefix.
- //
- // ERROR_ACCESS_DENIED is ambiguous: it is what the kernel says to a non-administrator
- // creating under protectedPrefix, and also what an existing instance whose DACL does
- // not admit us says to a second create. A process that is an administrator cannot be
- // getting the first, so for it, and for every name outside the prefix, access denied
- // means held.
- func listenRefused(path string, admin bool, err error) error {
- if errors.Is(err, windows.ERROR_ACCESS_DENIED) && isProtectedPath(path) && !admin {
- return fmt.Errorf("creating %s requires an administrator, as only administrators may create pipes under %s; run nebula elevated, or set %s to a pipe name outside it: %w", path, protectedPrefix, CtlSocketKey, err)
- }
- if errors.Is(err, windows.ERROR_ALREADY_EXISTS) || errors.Is(err, windows.ERROR_ACCESS_DENIED) {
- return fmt.Errorf("%w: %s: %w", errEndpointHeld, path, err)
- }
- return err
- }
- // isProtectedPath says whether path is a name under protectedPrefix. Pipe names are
- // case insensitive, so the comparison is too.
- func isProtectedPath(path string) bool {
- return len(path) > len(protectedPrefix) && strings.EqualFold(path[:len(protectedPrefix)], protectedPrefix)
- }
- // isAdministrator says whether this process holds Administrators as an enabled group,
- // which is what creating under protectedPrefix takes. SYSTEM holds it; an elevated
- // administrator holds it; an administrator's unelevated token carries it deny-only,
- // which the membership check correctly reads as no. The membership is checked against
- // the thread's own token: CheckTokenMembership wants an impersonation token and makes
- // one from the primary token when handed none. A check that cannot be made is read as
- // not an administrator, which only changes the wording of a failure that has already
- // happened.
- func isAdministrator() bool {
- admins, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
- if err != nil {
- return false
- }
- member, err := windows.Token(0).IsMember(admins)
- return err == nil && member
- }
- // sddlFor grants full access to the account nebula is running as, to SYSTEM and to
- // Administrators, and read and write, which is what a client's connect takes, to the
- // group acc names, if any; nobody else. P makes the DACL protected, so it does not
- // inherit entries that would widen it. It is dockerd's descriptor for its pipe, with
- // its -G group in the same place.
- //
- // Administrators is the counterpart of the uid 0 allowance in the unix peer check. The
- // service normally runs as LocalSystem, so without it an operator in an elevated shell
- // could not reach the daemon at all; and an administrator can already stop the service,
- // so this grants no authority they did not have.
- func sddlFor(acc Access) (string, error) {
- token := windows.GetCurrentProcessToken()
- user, err := token.GetTokenUser()
- if err != nil {
- return "", err
- }
- sddl := fmt.Sprintf("D:P(A;;GA;;;%s)(A;;GA;;;SY)(A;;GA;;;BA)", user.User.Sid.String())
- if acc.Group != nil {
- sddl += fmt.Sprintf("(A;;GRGW;;;%s)", acc.Group.ID)
- }
- return sddl, nil
- }
- // removeSocket is a no-op: a named pipe disappears when its last handle closes, so
- // there is nothing to unlink and no stale instance to clean up.
- func removeSocket(path string, id socketID) {}
- // identifySocket has no node to identify: a pipe is its handle, and it cannot be
- // removed from under the daemon, so an endpoint on its path is always still there.
- func identifySocket(path string) (socketID, bool) { return socketID{}, true }
- // applyAccess cannot change a pipe's access in place: its security descriptor is fixed
- // when it is created, so a change means creating it again, which the caller does.
- func applyAccess(path string, acc Access) error { return errReopenForAccess }
- // dial connects to the pipe and, before the connection carries anything, checks who
- // serves it. The request names a command and its arguments, so it must not reach a
- // pipe that some other account put under nebula's name. For the default name the
- // namespace already promises that, since only an administrator could have created it;
- // the check is what covers a CtlSocketKey override outside protectedPrefix, and costs
- // nothing to make on every name.
- func dial(path string, timeout time.Duration) (net.Conn, error) {
- conn, err := winio.DialPipe(path, &timeout)
- if err != nil {
- return nil, err
- }
- if err := checkPipeOwner(conn, path); err != nil {
- conn.Close()
- return nil, err
- }
- return conn, nil
- }
- // checkPipeOwner refuses a pipe whose owner is not an account that could be running
- // nebula. It is the unix owner check made from the other side: the handle winio dialed
- // exposes the pipe object, and the object's owner is whoever created it.
- //
- // The owner is what makes this sound. An account can only create an object owned by
- // itself, or by a SID it holds with the owner flag, unless it has SeRestorePrivilege,
- // which ordinary users do not; so a pipe owned by SYSTEM, Administrators or a service
- // account was put there by one of them, and never by a user squatting the name. winio
- // dials with SECURITY_ANONYMOUS, so the impostor could not have impersonated the client
- // either, but it could have read the command and answered it with whatever it liked.
- func checkPipeOwner(conn net.Conn, path string) error {
- h, ok := conn.(interface{ Fd() uintptr })
- if !ok {
- return fmt.Errorf("%s: cannot identify who serves the pipe", path)
- }
- sd, err := windows.GetSecurityInfo(windows.Handle(h.Fd()), windows.SE_KERNEL_OBJECT, windows.OWNER_SECURITY_INFORMATION)
- if err != nil {
- return fmt.Errorf("%s: could not read the pipe's owner: %w", path, err)
- }
- owner, _, err := sd.Owner()
- if err != nil {
- return fmt.Errorf("%s: could not read the pipe's owner: %w", path, err)
- }
- if owner == nil {
- return fmt.Errorf("%s has no owner; refusing to send to it", path)
- }
- me, err := windows.GetCurrentProcessToken().GetTokenUser()
- if err != nil {
- return fmt.Errorf("%s: could not read own identity: %w", path, err)
- }
- if ownerAccepted(owner, me.User.Sid) {
- return nil
- }
- return fmt.Errorf("%s is served by %s, not by nebula; run nebula ctl as that user, or pass -config or -socket to name the daemon's pipe", path, describeSID(owner))
- }
- // ownerAccepted says whether a pipe owned by owner may be sent a request by a client
- // running as me. Accepted are the client's own account and the accounts a daemon runs
- // under that no ordinary user can create an object as: SYSTEM, Administrators, the
- // LocalService and NetworkService accounts, and the per-service virtual accounts under
- // NT SERVICE (S-1-5-80-...). Another user's account is refused, whoever the client is;
- // the daemon's DACL decides who may connect, and this decides who may answer.
- func ownerAccepted(owner, me *windows.SID) bool {
- if owner.Equals(me) {
- return true
- }
- if owner.IsWellKnown(windows.WinLocalSystemSid) ||
- owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) ||
- owner.IsWellKnown(windows.WinLocalServiceSid) ||
- owner.IsWellKnown(windows.WinNetworkServiceSid) {
- return true
- }
- // NT SERVICE\<name>: S-1-5-80 followed by the five sub-authorities of the service's
- // name hash. The well-known SID table has no entry for them, as each is distinct.
- const serviceBaseRID = 80
- return owner.IdentifierAuthority() == windows.SECURITY_NT_AUTHORITY &&
- owner.SubAuthorityCount() == 6 &&
- owner.SubAuthority(0) == serviceBaseRID
- }
- // describeSID renders a SID with its account name when the host can say what it is, so
- // a message about somebody else's pipe tells the operator who that is.
- func describeSID(sid *windows.SID) string {
- account, domain, _, err := sid.LookupAccount("")
- if err != nil || account == "" {
- return sid.String()
- }
- if domain == "" {
- return account
- }
- return domain + `\` + account
- }
- // defaultSocketPath is the control pipe when the config names none. It is under
- // protectedPrefix, so whatever answers it was created by an administrator or SYSTEM; see
- // there.
- const defaultSocketPath = protectedPrefix + "nebula-ctl"
- // checkSocketPath refuses a configured name that is not a full pipe name. A pipe lives
- // under pipePrefix and nowhere else, so a bare name has no meaning here, and one under
- // pipePrefix is used as given. An override outside protectedPrefix is a name any
- // account could have created first; the owner check in dial is what protects a client
- // of one.
- func checkSocketPath(key, value string) error {
- if len(value) <= len(pipePrefix) || !strings.EqualFold(value[:len(pipePrefix)], pipePrefix) {
- return fmt.Errorf(`%s must be a full pipe name under %s, got %q`, key, pipePrefix, value)
- }
- return nil
- }
|