diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 261083783..130fe9cd4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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/`): diff --git a/client/cmd/root.go b/client/cmd/root.go index f5d417547..94c768220 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -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() diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index e9a82c36b..93a69d29e 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -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 { diff --git a/client/cmd/service_json_gateway.go b/client/cmd/service_json_gateway.go index 1473da688..405ff17fd 100644 --- a/client/cmd/service_json_gateway.go +++ b/client/cmd/service_json_gateway.go @@ -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) diff --git a/client/cmd/service_pipe_windows.go b/client/cmd/service_pipe_windows.go index 76bbd7f47..46fae45d0 100644 --- a/client/cmd/service_pipe_windows.go +++ b/client/cmd/service_pipe_windows.go @@ -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(), diff --git a/client/cmd/service_socket.go b/client/cmd/service_socket.go index 219cdee2c..7112bb7a8 100644 --- a/client/cmd/service_socket.go +++ b/client/cmd/service_socket.go @@ -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" ) diff --git a/client/internal/ipcauth/authorize.go b/client/internal/ipcauth/authorize.go index 3673b157d..f05a09eed 100644 --- a/client/internal/ipcauth/authorize.go +++ b/client/internal/ipcauth/authorize.go @@ -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 diff --git a/client/internal/ipcauth/creds_stub.go b/client/internal/ipcauth/creds_stub.go index d4d5893ce..be232e0df 100644 --- a/client/internal/ipcauth/creds_stub.go +++ b/client/internal/ipcauth/creds_stub.go @@ -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 } diff --git a/client/internal/ipcauth/creds_unix.go b/client/internal/ipcauth/creds_unix.go index 3cddc531d..deda17a42 100644 --- a/client/internal/ipcauth/creds_unix.go +++ b/client/internal/ipcauth/creds_unix.go @@ -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{} } diff --git a/client/internal/ipcauth/creds_windows.go b/client/internal/ipcauth/creds_windows.go index 6b10ac5ac..a578a6561 100644 --- a/client/internal/ipcauth/creds_windows.go +++ b/client/internal/ipcauth/creds_windows.go @@ -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{} } diff --git a/client/internal/ipcauth/forward.go b/client/internal/ipcauth/forward.go index 6aec00650..c52cb9a3c 100644 --- a/client/internal/ipcauth/forward.go +++ b/client/internal/ipcauth/forward.go @@ -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 { diff --git a/client/internal/ipcauth/forward_test.go b/client/internal/ipcauth/forward_test.go index ce9596f1d..de79a4e1a 100644 --- a/client/internal/ipcauth/forward_test.go +++ b/client/internal/ipcauth/forward_test.go @@ -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) diff --git a/client/internal/ipcauth/identity.go b/client/internal/ipcauth/identity.go index fb7de1513..afb6816a9 100644 --- a/client/internal/ipcauth/identity.go +++ b/client/internal/ipcauth/identity.go @@ -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 { diff --git a/client/internal/ipcauth/interceptor.go b/client/internal/ipcauth/interceptor.go index f475eadc5..bb1b8aebf 100644 --- a/client/internal/ipcauth/interceptor.go +++ b/client/internal/ipcauth/interceptor.go @@ -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 diff --git a/client/internal/ipcauth/interceptor_test.go b/client/internal/ipcauth/interceptor_test.go index 666f613cc..c3d92eb12 100644 --- a/client/internal/ipcauth/interceptor_test.go +++ b/client/internal/ipcauth/interceptor_test.go @@ -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)) } diff --git a/client/internal/ipcauth/policy.go b/client/internal/ipcauth/policy.go index ac5940fbf..4b0449de1 100644 --- a/client/internal/ipcauth/policy.go +++ b/client/internal/ipcauth/policy.go @@ -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 diff --git a/client/internal/ipcauth/principal.go b/client/internal/ipcauth/principal.go index 785168ba0..056b6ede8 100644 --- a/client/internal/ipcauth/principal.go +++ b/client/internal/ipcauth/principal.go @@ -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) diff --git a/client/internal/ipcauth/resolver_unix.go b/client/internal/ipcauth/resolver_unix.go index e480c55d5..c55a5b35a 100644 --- a/client/internal/ipcauth/resolver_unix.go +++ b/client/internal/ipcauth/resolver_unix.go @@ -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)} diff --git a/client/internal/ipcauth/resolver_windows.go b/client/internal/ipcauth/resolver_windows.go index 7517ad943..14d8d25cc 100644 --- a/client/internal/ipcauth/resolver_windows.go +++ b/client/internal/ipcauth/resolver_windows.go @@ -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 } diff --git a/client/internal/profilemanager/config.go b/client/internal/profilemanager/config.go index cec6edf44..ba3fc9498 100644 --- a/client/internal/profilemanager/config.go +++ b/client/internal/profilemanager/config.go @@ -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 diff --git a/client/internal/shell/getent_windows.go b/client/internal/shell/getent_windows.go index 28300b941..584d72211 100644 --- a/client/internal/shell/getent_windows.go +++ b/client/internal/shell/getent_windows.go @@ -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 "" } diff --git a/client/server/ownership.go b/client/server/ownership.go index 3d94dff4a..7791f1f4d 100644 --- a/client/server/ownership.go +++ b/client/server/ownership.go @@ -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() diff --git a/client/ui/main.go b/client/ui/main.go index 4889bad79..4ee0e7e01 100644 --- a/client/ui/main.go +++ b/client/ui/main.go @@ -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.")