From 5d9ef0123fa41774fd8da3f6a966c7fbc279d8bd Mon Sep 17 00:00:00 2001 From: "Theodor S. Midtlien" Date: Thu, 23 Jul 2026 16:00:45 +0200 Subject: [PATCH] Add impersonateNamedPipeClient --- client/cmd/service_pipe_windows.go | 17 +++++++++------ client/internal/ipcauth/creds_windows.go | 27 ++++++++++++------------ client/ui/.gitignore | 1 + 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/client/cmd/service_pipe_windows.go b/client/cmd/service_pipe_windows.go index 51eff1cae..18ca0fbd5 100644 --- a/client/cmd/service_pipe_windows.go +++ b/client/cmd/service_pipe_windows.go @@ -5,9 +5,9 @@ package cmd import ( "context" "net" - "time" "github.com/Microsoft/go-winio" + "golang.org/x/sys/windows" "github.com/netbirdio/netbird/client/internal/ipcauth" ) @@ -22,11 +22,14 @@ func listenNamedPipe(path string) (net.Listener, error) { }) } -// dialNamedPipe connects to the daemon control named pipe. +// dialNamedPipe connects to the daemon control named pipe at SECURITY_IDENTIFICATION. +// +// winio's plain DialPipe connects at SECURITY_ANONYMOUS, under which the daemon +// cannot read the caller's token (ImpersonateNamedPipeClient fails / yields an +// anonymous token and the handshake is dropped). Identification lets the daemon +// *identify* the caller (read its SID/groups) without granting it the ability to +// act as the caller — the least privilege the daemon needs for authorization. func dialNamedPipe(ctx context.Context, path string) (net.Conn, error) { - if deadline, ok := ctx.Deadline(); ok { - timeout := time.Until(deadline) - return winio.DialPipe(path, &timeout) - } - return winio.DialPipeContext(ctx, path) + access := uint32(windows.GENERIC_READ | windows.GENERIC_WRITE) + return winio.DialPipeAccessImpLevel(ctx, path, access, winio.PipeImpLevelIdentification) } diff --git a/client/internal/ipcauth/creds_windows.go b/client/internal/ipcauth/creds_windows.go index f09cb27f9..490116aee 100644 --- a/client/internal/ipcauth/creds_windows.go +++ b/client/internal/ipcauth/creds_windows.go @@ -35,10 +35,14 @@ func DefaultPipeSDDL() string { } // NewTransportCredentials returns gRPC transport credentials that derive the -// caller's identity from the named-pipe client token, following Microsoft's -// "Verifying Client Access with ACLs" pattern: ImpersonateNamedPipeClient -> -// OpenThreadToken -> RevertToSelf. Per threat-model M-NOIMP, impersonation is -// used only to read the client token for identity, never to perform privileged work. +// caller's identity from the named-pipe client token. +// +// It uses ImpersonateNamedPipeClient, which binds the client's security context +// to the *connection* in the kernel — there is no PID lookup, so it is immune to +// the PID-reuse race that OpenProcess-by-PID would have. Per threat-model +// M-NOIMP, impersonation is used only to read the client token, then reverted +// immediately; the daemon never performs privileged work while impersonating. +// This requires the client to dial at SECURITY_IDENTIFICATION (see dialNamedPipe). func NewTransportCredentials() credentials.TransportCredentials { return winpipeCreds{} } @@ -49,8 +53,8 @@ func (winpipeCreds) ClientHandshake(_ context.Context, _ string, conn net.Conn) return conn, AuthInfo{}, nil } -// ServerHandshake extracts the connecting client's identity from the pipe token. -// Fails closed if the handle or token cannot be read. +// ServerHandshake extracts the connecting client's identity from the pipe. Fails +// closed if the handle or token cannot be read. func (winpipeCreds) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) { // go-winio's pipe connection embeds *win32File, which exposes Fd(). fdConn, ok := conn.(interface{ Fd() uintptr }) @@ -78,12 +82,10 @@ func (winpipeCreds) Clone() credentials.TransportCredentials { return winpipeCre func (winpipeCreds) OverrideServerName(string) error { return nil } // pipeClientIdentity reads the connecting client's user SID, enabled group SIDs, -// and elevation from the named-pipe handle. The impersonation window is kept as -// small as possible and pinned to the OS thread (impersonation is thread-local). +// and elevation by impersonating the pipe client on this thread and reading the +// impersonation token. Impersonation is connection-bound (no PID race) and the +// window is kept minimal and pinned to the OS thread (impersonation is thread-local). func pipeClientIdentity(handle windows.Handle) (Identity, error) { - var pid uint32 - hasPID := windows.GetNamedPipeClientProcessId(handle, &pid) == nil - runtime.LockOSThread() defer runtime.UnlockOSThread() @@ -121,8 +123,7 @@ func pipeClientIdentity(handle windows.Handle) (Identity, error) { SID: tu.User.Sid.String(), Groups: groups, Elevated: token.IsElevated(), - PID: int32(pid), - HasPID: hasPID, + HasPID: false, }, nil } diff --git a/client/ui/.gitignore b/client/ui/.gitignore index 9f233d8b6..4ef83ebb6 100644 --- a/client/ui/.gitignore +++ b/client/ui/.gitignore @@ -6,3 +6,4 @@ frontend/bindings frontend/.vite build/linux/appimage/build build/windows/nsis/MicrosoftEdgeWebview2Setup.exe +build/windows/frontend