package ipcauth import ( "os" "runtime" ) // Fields of the ErrorInfo detail the daemon attaches to a PermissionDenied it // raises for an operation that requires root/administrator. Clients match on // Reason and Domain rather than on the message text, and render the summary and // command themselves so the user gets guidance instead of a gRPC error dump. const ( // ErrorReasonPrivilegeRequired identifies the detail. ErrorReasonPrivilegeRequired = "PRIVILEGE_REQUIRED" // ErrorDomain scopes the reason to the NetBird daemon. ErrorDomain = "daemon.netbird.io" // ErrorMetaSummary is the one-sentence explanation of what was refused. ErrorMetaSummary = "summary" // ErrorMetaCommand is the command that performs the same operation with the // privileges it needs, ready to copy and run. ErrorMetaCommand = "command" ) // The identity of the process evaluating callers, captured once because it cannot // change. selfKnown is false when it could not be read, in which case nothing is // ever treated as this process. selfMayDelegate additionally requires this // process to be unprivileged: see IsPrivilegedCaller. var ( selfIdentity Identity selfKnown bool selfMayDelegate bool // selfPID is this process's PID, used to recognise the daemon dialling itself. selfPID = os.Getpid() ) func init() { id, err := CurrentProcessIdentity() if err != nil { return } selfIdentity, selfKnown = id, true // Only an unprivileged daemon delegates its authority to its own identity. // When it is root or LocalSystem, sharing its identity does not mean sharing // its power: on Windows a filtered and a full token carry the same SID, so // matching there would let a non-elevated shell of an administrator account // act as an administrator, which is the boundary the token check exists to // keep. selfMayDelegate = !id.IsPrivileged() } // IsDaemonSelf reports whether an identity is this very process. The JSON gateway // runs inside the daemon and re-dials it locally, so this is what distinguishes // the gateway from any other caller, whatever user the daemon runs as. func IsDaemonSelf(id Identity) bool { if !selfKnown || id.IsWindows() != selfIdentity.IsWindows() { return false } if id.IsWindows() { return id.SID != "" && id.SID == selfIdentity.SID } return id.UID == selfIdentity.UID } // IsPrivilegedCaller reports whether an identity may make the changes the daemon // restricts to the platform administrator. This is the daemon's own rule and // cannot be evaluated by a client, which does not know what the daemon runs as. // // Beyond root/administrator it accepts a caller running as the daemon's own // identity when the daemon is itself unprivileged. That keeps a rootless container // working, where there is no uid 0 at all, and a Windows daemon in netstack mode, // which needs no administrator rights. In those setups a caller sharing the // daemon's identity can already rewrite the config files it reads and replace the // binary it runs, so refusing it a config change would protect nothing; and an // unprivileged daemon cannot hand out a root shell in the first place. func IsPrivilegedCaller(id Identity) bool { if id.IsPrivileged() { return true } return selfMayDelegate && IsDaemonSelf(id) } // SelfDelegatesTo returns the identity this process delegates its authority to, // and whether it delegates at all. Only an unprivileged daemon does: see // IsPrivilegedCaller. It exists so a refusal can name who may actually perform the // operation, because on such a host root is neither required nor necessarily // available. func SelfDelegatesTo() (Identity, bool) { if !selfKnown || !selfMayDelegate { return Identity{}, false } return selfIdentity, true } // PrivilegedActor names the principal a privileged operation requires, for use // in messages shown to the user. func PrivilegedActor() string { if runtime.GOOS == "windows" { return "administrator privileges" } return "root" } // ElevatedCommand renders a command so that running it grants the privileges the // operation needs. Windows has no in-line equivalent of sudo, so the command is // returned unchanged and the user is expected to run it from an elevated // terminal. func ElevatedCommand(command string) string { if runtime.GOOS == "windows" { return command } return "sudo " + command } // UpCommand renders an elevated `netbird up` with the given flags, preceded by a // `down`. The down is what makes the command work on a connected client: `netbird // up` prints "Already connected" and returns without applying any config flag, so // on its own the command would appear to do nothing. It is a no-op, exit 0, when // the client is not connected. // // ";" rather than "&&" so the line can be pasted into any of the shells a user // might have: PowerShell 5.1, still the default on Windows Server, rejects "&&" // as a syntax error. func UpCommand(flags string) string { return ElevatedCommand("netbird down") + "; " + ElevatedCommand("netbird up "+flags) }