Clean up comments

This commit is contained in:
Theodor S. Midtlien
2026-07-24 14:57:21 +02:00
parent 57f9cbe5ff
commit 0137876618
23 changed files with 55 additions and 103 deletions

View File

@@ -237,7 +237,7 @@ task dev
Pass daemon flags after `--`:
```
task dev -- --daemon-addr=tcp://127.0.0.1:41731
task dev -- --daemon-addr=npipe://netbird
```
Production build (frontend assets embedded into the binary, output in `client/ui/bin/`):

View File

@@ -268,16 +268,9 @@ func FlagNameToEnvVar(cmdFlag string, prefix string) string {
return prefix + upper
}
// DialClientGRPCServer returns client connection to the daemon server.
// daemonDialTarget returns the gRPC dial target and base options for the daemon
// address, handling the npipe scheme (Windows named pipe, via a context dialer)
// and unix/tcp. It sets insecure transport credentials but NOT WithBlock, so it
// serves both the blocking CLI dial and the JSON gateway's lazy client.
//
// The daemon reads the caller's kernel identity from the transport (SO_PEERCRED
// on a Unix socket, the client token on a Windows named pipe), so the client
// stays insecure. gRPC's resolver does not understand Windows named pipes, hence
// the context dialer.
// and unix/tcp.
func daemonDialTarget(addr string) (string, []grpc.DialOption) {
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
target := strings.TrimPrefix(addr, "tcp://")
@@ -291,6 +284,7 @@ func daemonDialTarget(addr string) (string, []grpc.DialOption) {
return target, opts
}
// DialClientGRPCServer returns client connection to the daemon server.
func DialClientGRPCServer(ctx context.Context, addr string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()

View File

@@ -22,11 +22,7 @@ import (
)
// daemonServerOptions installs peer-identity transport credentials and the
// authorization interceptor on the daemon ipc. Identity is only available
// over a Unix socket (SO_PEERCRED) or a Windows named pipe (client token).
// Over TCP, or on platforms without a peer-credential primitive, the daemon
// runs without per-caller authorization and warns (no interceptor, so it does
// not deny everyone).
// authorization interceptor on the daemon ipc if supported.
func daemonServerOptions(network string, interceptor *ipcauth.Interceptor) []grpc.ServerOption {
creds := ipcauth.NewTransportCredentials()
if creds == nil {

View File

@@ -23,8 +23,8 @@ import (
type jsonPeerCtxKey struct{}
// jsonConnContext reads the connecting HTTP client's identity from the JSON
// socket (peercred) and stashes it so it can be forwarded to the daemon. The
// gateway re-dials the daemon as the daemon's own identity, so without this the
// socket and stashes it so it can be forwarded to the daemon. The gateway
// re-dials the daemon as the daemon's own identity, so without this the
// daemon would see every JSON request as privileged.
func jsonConnContext(ctx context.Context, c net.Conn) context.Context {
id, err := ipcauth.ConnIdentity(c)

View File

@@ -13,10 +13,7 @@ import (
)
// listenNamedPipe creates the daemon control named pipe with a permissive,
// local-only SDDL. Any local caller may connect, on par with the Unix
// socket's 0666, and the per-RPC interceptor authorizes. ListenPipe fails
// if the pipe already exists (first-instance semantics), which prevents a
// squatting process from pre-creating it.
// local-only SDDL. Any local caller may connect, like Unix socket with 0666.
func listenNamedPipe(path string) (net.Listener, error) {
return winio.ListenPipe(path, &winio.PipeConfig{
SecurityDescriptor: ipcauth.DefaultPipeSDDL(),

View File

@@ -19,8 +19,7 @@ const (
windowsPipeDaemonAddr = "npipe://netbird"
// legacyWindowsDaemonAddr is the loopback-TCP address the Windows daemon used
// before named-pipe support. TCP exposes no peer-identity primitive, so the
// authorization interceptor cannot run over it.
// before named-pipe support.
legacyWindowsDaemonAddr = "tcp://127.0.0.1:41731"
)

View File

@@ -12,10 +12,8 @@ type Ownership struct {
Shared bool
}
// GroupResolver resolves a Unix caller's effective group IDs (primary +
// supplementary, NSS-aware) and owner group names to GIDs. It is only consulted
// for Unix `gid:`/`group:` owners; Windows uses the SIDs carried in the Identity.
// A nil resolver disables group matching.
// GroupResolver resolves a Unix caller's effective group IDs and owner group
// names to GIDs. A nil resolver disables group matching.
type GroupResolver interface {
// CallerGIDs returns the set of group IDs the caller belongs to.
CallerGIDs(id Identity) map[uint32]struct{}
@@ -24,9 +22,7 @@ type GroupResolver interface {
}
// Authorize reports whether the identity may control a profile with the given
// ownership. Privileged callers (root / elevated-admin / LocalSystem) and shared
// profiles are always allowed; otherwise the identity must match one of the
// owner principals.
// ownership. Privileged callers and shared profiles are always allowed.
func Authorize(o Ownership, id Identity, r GroupResolver) bool {
if id.IsPrivileged() {
return true
@@ -76,8 +72,6 @@ func principalMatches(p Principal, id Identity, r GroupResolver) bool {
}
}
// callerHasGID reports whether gid is the caller's primary GID (from peercred,
// no lookup) or one of their supplementary groups (NSS-resolved via r).
func callerHasGID(gid uint32, id Identity, r GroupResolver) bool {
if id.GID == gid {
return true

View File

@@ -11,8 +11,7 @@ import (
)
// NewTransportCredentials returns nil on platforms without a peer-identity
// primitive. The daemon falls back to insecure credentials and skips per-RPC
// authorization (logging a warning), preserving pre-hardening behavior.
// primitive.
func NewTransportCredentials() credentials.TransportCredentials {
return nil
}

View File

@@ -17,10 +17,6 @@ func NewTransportCredentials() credentials.TransportCredentials {
return unixCreds{}
}
// unixCreds implements credentials.TransportCredentials over a Unix socket.
// The server side reads SO_PEERCRED/LOCAL_PEERCRED during the handshake; the
// client side is a no-op (the kernel supplies the peer identity to the server
// without any client cooperation), so an ordinary insecure client still works.
type unixCreds struct{}
func (unixCreds) ClientHandshake(_ context.Context, _ string, conn net.Conn) (net.Conn, credentials.AuthInfo, error) {
@@ -28,8 +24,8 @@ func (unixCreds) ClientHandshake(_ context.Context, _ string, conn net.Conn) (ne
}
// ConnIdentity extracts the caller's identity from an accepted local IPC
// connection. On Unix it reads peer credentials from the socket. It is shared by
// the gRPC transport credentials and the JSON gateway (which forwards it).
// connection. On Unix it reads peer credentials from the socket. It is shared
// by the gRPC transport credentials and the JSON gateway (which forwards it).
func ConnIdentity(conn net.Conn) (Identity, error) {
return PeerIdentity(conn)
}
@@ -47,7 +43,7 @@ func (unixCreds) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo,
}
func (unixCreds) Info() credentials.ProtocolInfo {
return credentials.ProtocolInfo{SecurityProtocol: "netbird-ipc-peercred"}
return credentials.ProtocolInfo{SecurityProtocol: AuthInfo{}.AuthType()}
}
func (unixCreds) Clone() credentials.TransportCredentials { return unixCreds{} }

View File

@@ -17,8 +17,8 @@ var (
procImpersonateNamedPipeClient = modadvapi32.NewProc("ImpersonateNamedPipeClient")
)
// DefaultPipeSDDL keeps the daemon control pipe open to any LOCAL caller, on par
// with the Unix socket's 0666 mode.
// DefaultPipeSDDL keeps the daemon control pipe open to any LOCAL caller,
// like Unix socket with 0666 permissions.
//
// D:P protected DACL, no inheritance
// (D;;GA;;;NU) deny GENERIC_ALL to NETWORK (remote/SMB)
@@ -69,7 +69,7 @@ func (winpipeCreds) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthIn
}
func (winpipeCreds) Info() credentials.ProtocolInfo {
return credentials.ProtocolInfo{SecurityProtocol: "netbird-ipc-peercred"}
return credentials.ProtocolInfo{SecurityProtocol: AuthInfo{}.AuthType()}
}
func (winpipeCreds) Clone() credentials.TransportCredentials { return winpipeCreds{} }

View File

@@ -8,9 +8,7 @@ import (
)
// Metadata keys used by the local JSON gateway to forward the HTTP client's
// identity to the daemon. Trusted by the interceptor ONLY when the gRPC peer is
// itself the daemon (self/privileged) — i.e. the loopback gateway — so a direct
// gRPC caller cannot forge them.
// identity to the daemon.
const (
mdFwdUID = "x-netbird-fwd-uid" // Unix
mdFwdGID = "x-netbird-fwd-gid" // Unix
@@ -20,9 +18,7 @@ const (
)
// ForwardIdentityMetadata encodes an identity for the gateway to forward to the
// daemon — Unix uid/gid, or the Windows user SID + enabled group SIDs +
// elevation. Both are supported so the gateway works whether the JSON socket is
// a Unix socket or a named pipe.
// daemon.
func ForwardIdentityMetadata(id Identity) metadata.MD {
if id.IsWindows() {
md := metadata.MD{}
@@ -41,8 +37,7 @@ func ForwardIdentityMetadata(id Identity) metadata.MD {
)
}
// forwardedIdentity extracts a forwarded identity from incoming gRPC metadata,
// if present and well-formed. Windows (SID) takes precedence over Unix (uid).
// forwardedIdentity extracts a forwarded identity from incoming gRPC metadata
func forwardedIdentity(ctx context.Context) (Identity, bool) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {

View File

@@ -33,9 +33,8 @@ func TestForwardIdentityRoundTrip(t *testing.T) {
func TestForwardedIdentity_None(t *testing.T) {
_, ok := forwardedIdentity(context.Background())
assert.False(t, ok, "no metadata no forwarded identity")
assert.False(t, ok, "no metadata, no forwarded identity")
// Empty metadata (no forwarding keys) → none.
ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("other", "x"))
_, ok = forwardedIdentity(ctx)
assert.False(t, ok)

View File

@@ -5,8 +5,7 @@
// On Unix the identity is read from the kernel via SO_PEERCRED (Linux) or
// LOCAL_PEERCRED (Darwin/FreeBSD). On Windows it is derived from the named-pipe
// client token. Platforms without a peer-identity primitive get no credentials
// and therefore no enforcement (the daemon logs a warning and stays open,
// preserving today's behavior until the transport gains an identity primitive).
// and therefore no enforcement.
package ipcauth
import (
@@ -21,8 +20,7 @@ import (
const sidLocalSystem = "S-1-5-18"
// Identity is the kernel-authenticated identity of a local IPC caller. The zero
// value is not a valid identity; obtain one via IdentityFromContext (which
// reports presence) or PeerIdentity.
// value is not a valid identity.
type Identity struct {
// UID and GID are the caller's Unix user ID and primary group ID.
// Zero on Windows, where SID is authoritative instead.
@@ -49,10 +47,7 @@ func (i Identity) IsWindows() bool {
}
// IsPrivileged reports whether the caller is the platform's administrative
// principal — Unix root (uid 0), or on Windows an elevated token or LocalSystem.
// It deliberately requires actual elevation on Windows (a non-elevated member of
// Administrators has a filtered token and is NOT privileged), mirroring "must
// really be root" on Unix.
// principal.
func (i Identity) IsPrivileged() bool {
if i.IsWindows() {
return i.Elevated || i.SID == sidLocalSystem
@@ -80,8 +75,7 @@ func (AuthInfo) AuthType() string { return "netbird-ipc-peercred" }
// IdentityFromContext extracts the caller's kernel-authenticated identity from
// the gRPC peer context. The second return value is false when no IPC transport
// credentials were negotiated (e.g. an unsupported platform, or a caller that
// did not come through the daemon socket) — callers MUST fail closed in that case.
// credentials were negotiated, callers MUST fail closed in that case.
func IdentityFromContext(ctx context.Context) (Identity, bool) {
p, ok := peer.FromContext(ctx)
if !ok {

View File

@@ -10,16 +10,14 @@ import (
"google.golang.org/grpc/status"
)
// Interceptor enforces per-RPC authorization on the daemon control channel,
// keyed to the caller's kernel-authenticated identity. It is safe-by-default:
// Interceptor enforces per-RPC authorization on the daemon IPC, keyed to
// the caller's kernel-authenticated identity. It is safe-by-default:
// any RPC without a matching bypass is gated by the active profile's ownership,
// and a caller without a readable identity is denied.
type Interceptor struct {
policy ProfilePolicy
resolver GroupResolver
// selfUID is the daemon's own effective UID. A caller whose UID matches it
// (rootless container / foreground daemon running as the invoking user) is
// allowed: it already has full control of the daemon process. -1 on Windows.
// selfUID is the daemon's own effective UID. -1 on Windows.
selfUID int
}
@@ -51,22 +49,22 @@ func (i *Interceptor) StreamServerInterceptor() grpc.StreamServerInterceptor {
func (i *Interceptor) authorize(ctx context.Context, fullMethod string) error {
id, ok := IdentityFromContext(ctx)
if !ok {
log.Warnf("ipc authz: DENY %s caller identity unavailable", fullMethod)
log.Warnf("ipc authz: DENY %s. caller identity unavailable", fullMethod)
return status.Error(codes.PermissionDenied, "caller identity could not be verified on the daemon control channel")
}
if i.isSelfOrPrivileged(id) {
// The local JSON gateway connects as the daemon itself (self/privileged)
// and forwards the real HTTP client's identity. Trust it here — and only
// here, where the transport peer is already the daemon then authorize
// as the forwarded client. A direct non-privileged caller never reaches
// this branch, so it cannot forge the forwarding metadata.
// and forwards the real HTTP client's identity. Trust it here, where
// the transport peer is already the daemon, then authorize as the
// forwarded client. A direct non-privileged caller never reaches this
// branch, so it cannot forge the forwarding metadata.
fwd, hasFwd := forwardedIdentity(ctx)
if !hasFwd {
i.auditAllow(id, fullMethod)
return nil
}
log.Infof("ipc authz: honoring gateway-forwarded identity %s", fwd)
log.Infof("ipc authz: gateway-forwarded identity %s", fwd)
id = fwd
if i.isSelfOrPrivileged(id) {
i.auditAllow(id, fullMethod)
@@ -82,7 +80,7 @@ func (i *Interceptor) authorize(ctx context.Context, fullMethod string) error {
o := i.policy.ActiveProfileOwnership()
// Trust-on-first-use: an unowned, non-shared profile is claimed by the first
// caller. The claim is atomic; if we lose the race we re-read and authorize.
// caller. The claim is atomic, if we lose the race we re-read and authorize.
if len(o.Owners) == 0 && !o.Shared {
claimed, err := i.policy.ClaimActiveProfileOwnerIfUnowned(id)
if err != nil {
@@ -102,9 +100,9 @@ func (i *Interceptor) authorize(ctx context.Context, fullMethod string) error {
return nil
}
log.Warnf("ipc authz: DENY %s for %s active profile owned by another principal", fullMethod, id)
log.Warnf("ipc authz: DENY %s for %s. active profile owned by another principal", fullMethod, id)
return status.Errorf(codes.PermissionDenied,
"not authorized to control the active profile (caller %s); ask an owner or run as root/administrator", id)
"not authorized to control the active profile (caller %s). ask an owner or run as root/administrator", id)
}
// isSelfOrPrivileged reports whether the caller is the platform administrator

View File

@@ -104,15 +104,14 @@ func TestInterceptorForwardedIdentity(t *testing.T) {
return metadata.NewIncomingContext(ctx, metadata.Pairs(mdFwdUID, itoa(fwdUID)))
}
// Gateway (peer == daemon-self) forwards a non-owner client denied as that client.
// Gateway forwards a non-owner client: denied as that client.
i := &Interceptor{policy: &mockPolicy{o: owners}, selfUID: selfUID}
assert.Error(t, i.authorize(withFwd(selfUID, 2000), up))
// Gateway forwards the owner allowed.
// Gateway forwards the owner: allowed.
assert.NoError(t, i.authorize(withFwd(selfUID, 1000), up))
// A non-privileged direct caller's forwarded metadata is IGNORED (can't forge):
// caller uid 2000 forwarding uid:1000 is still treated as 2000 → denied.
// A non-privileged direct caller's forwarded metadata: denied
assert.Error(t, i.authorize(withFwd(2000, 1000), up))
}

View File

@@ -5,7 +5,7 @@ import "sync"
const servicePath = "/daemon.DaemonService/"
// ProfilePolicy exposes the active profile's ownership to the interceptor. The
// daemon server implements it; ConfigAdapter bridges the gap because the gRPC
// daemon server implements it. ConfigAdapter bridges the gap because the gRPC
// server (and its interceptor) is constructed before the server instance exists.
type ProfilePolicy interface {
// ActiveProfileOwnership returns the active profile's ownership policy.
@@ -14,8 +14,7 @@ type ProfilePolicy interface {
// ClaimActiveProfileOwnerIfUnowned atomically claims the active profile for
// id when it has no owners and is not shared (trust-on-first-use), and
// reports whether id is now an owner. A false return means the profile was
// already owned/shared or another caller won the claim — the caller must
// re-read ownership and authorize normally.
// already owned/shared or another caller won the claim.
ClaimActiveProfileOwnerIfUnowned(id Identity) (bool, error)
}
@@ -30,8 +29,7 @@ var handlerAuthorizedMethods = map[string]bool{
servicePath + "RenameProfile": true,
}
// auditMethods are the Tier-C/H RPCs (threat model §3) whose successful
// authorization is worth an audit log line. Denials are always logged.
// auditMethods are worth an audit log line. Denials are always logged.
var auditMethods = map[string]bool{
servicePath + "GetConfig": true,
servicePath + "SetConfig": true,
@@ -56,7 +54,7 @@ var auditMethods = map[string]bool{
// ConfigAdapter is a ProfilePolicy whose backend is set lazily, once the daemon
// server instance is created. Until then it reports an unowned profile
// (Ownership zero value), so non-privileged callers are denied — fail closed.
// (Ownership zero value), so non-privileged callers are denied.
type ConfigAdapter struct {
mu sync.RWMutex
backend ProfilePolicy

View File

@@ -45,8 +45,7 @@ func UIDPrincipal(uid uint32) string {
func SIDPrincipal(sid string) string { return string(KindSID) + ":" + sid }
// OwnerPrincipalForIdentity returns the self-ownership principal for an identity:
// the user's UID on Unix, or the user's SID on Windows. Used to auto-isolate a
// new profile to its creator.
// the user's UID on Unix, or the user's SID on Windows.
func OwnerPrincipalForIdentity(id Identity) string {
if id.IsWindows() {
return SIDPrincipal(id.SID)

View File

@@ -12,8 +12,7 @@ import (
const groupCacheTTL = 30 * time.Second
// NewDefaultGroupResolver returns an NSS-aware group resolver backed by
// getent/`id -G` (via client/internal/shell), so `gid:`/`group:` owners resolve
// NewDefaultGroupResolver returns an NSS-aware group resolver, owners resolve
// correctly for LDAP/AD users under CGO_ENABLED=0. Results are cached briefly.
func NewDefaultGroupResolver() GroupResolver {
return &nssResolver{byUID: make(map[uint32]gidCacheEntry)}

View File

@@ -4,7 +4,7 @@ package ipcauth
// NewDefaultGroupResolver returns nil on Windows: group authorization uses the
// group SIDs carried in the client token (see the Windows transport
// credentials), not NSS/getent.
// credentials).
func NewDefaultGroupResolver() GroupResolver {
return nil
}

View File

@@ -192,8 +192,7 @@ type Config struct {
// Owners lists the principals allowed to control this profile over the local
// IPC, as typed strings: "uid:1000", "gid:1000", "group:netbird-admins"
// (Unix, NSS-resolved) or "sid:S-1-5-..." (Windows user or group SID). Empty
// with Shared=false means the profile is owned by nobody yet (privileged +
// daemon-self only, until claimed). Interpreted by client/internal/ipcauth.
// with Shared=false means the profile is owned by nobody yet, until claimed
Owners []string `json:"Owners,omitempty"`
// Shared, when true, lets any authenticated local caller control this profile

View File

@@ -5,7 +5,6 @@ package shell
import "os/user"
// LookupWithGetent on Windows just delegates to os/user.Lookup.
// Windows does not use NSS/getent; its user lookup works without CGO.
func LookupWithGetent(username string) (*user.User, error) {
return user.Lookup(username)
}
@@ -20,7 +19,7 @@ func LookupGroupWithGetent(name string) (*user.Group, error) {
return user.LookupGroup(name)
}
// GetShellFromGetent is a no-op on Windows; shell resolution uses PowerShell detection.
// GetShellFromGetent is a no-op on Windows.
func GetShellFromGetent(_ string) string {
return ""
}

View File

@@ -15,8 +15,7 @@ import (
"github.com/netbirdio/netbird/util"
)
// The daemon Server implements ipcauth.ProfilePolicy so the gRPC interceptor can
// authorize each RPC against the active profile's ownership.
// Verify that the daemon Server implements ipcauth.ProfilePolicy.
var _ ipcauth.ProfilePolicy = (*Server)(nil)
// ActiveProfileOwnership returns the active profile's ownership policy. Reads
@@ -40,8 +39,7 @@ func (s *Server) ActiveProfileOwnership() ipcauth.Ownership {
// ClaimActiveProfileOwnerIfUnowned atomically claims the active profile for id
// when it has no owners and is not shared (trust-on-first-use). Returns whether
// id is now an owner. Concurrent first-callers are serialized by s.mutex, so
// exactly one wins the claim; the others get false and are authorized normally.
// id is now an owner. Concurrent first-callers are serialized by s.mutex.
func (s *Server) ClaimActiveProfileOwnerIfUnowned(id ipcauth.Identity) (bool, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
@@ -56,7 +54,7 @@ func (s *Server) ClaimActiveProfileOwnerIfUnowned(id ipcauth.Identity) (bool, er
}
if len(cfg.Owners) > 0 || cfg.Shared {
return false, nil // already owned or shared — someone won the race
return false, nil // already owned or shared
}
cfg.Owners = []string{ipcauth.OwnerPrincipalForIdentity(id)}
@@ -130,7 +128,7 @@ func (s *Server) claimForCallerLocked(id ipcauth.Identity, cfg *profilemanager.C
}
// AddOwner adds a principal to the active profile's owner list. The interceptor
// has already confirmed the caller is an owner or privileged; the handler just
// has already confirmed the caller is an owner or privileged, the handler just
// validates and persists.
func (s *Server) AddOwner(_ context.Context, msg *proto.AddOwnerRequest) (*proto.AddOwnerResponse, error) {
principal := msg.GetPrincipal()

View File

@@ -231,7 +231,7 @@ func requestNotificationAuthorization(notifier *notifications.NotificationServic
// "no flag" and an explicit "--log-file console" stay distinguishable; empty
// falls back to console for InitLog.
func parseFlagsAndInitLog() (string, bool) {
daemonAddr := flag.String("daemon-addr", DaemonAddr(), "Daemon gRPC address: unix:///path or tcp://host:port")
daemonAddr := flag.String("daemon-addr", DaemonAddr(), "Daemon gRPC address: unix:///path, npipe://name, tcp://host:port")
logFiles := &stringList{}
flag.Var(logFiles, "log-file", "Log destination. Repeat to log to multiple targets at once, e.g. `--log-file console --log-file Y:/netbird-ui.log`. Each value is one of: console, syslog, or a file path. File destinations are rotated by lumberjack (same as the daemon). Defaults to console. Passing any value disables the daemon-debug-driven gui-client.log.")
logLevel := flag.String("log-level", "info", "Log level: trace|debug|info|warn|error.")