Wire up json socket as a named pipe with metdata exchange to daemon

This commit is contained in:
Theodor S. Midtlien
2026-07-23 20:27:06 +02:00
parent 2f84aa3d20
commit f60ac9e746
10 changed files with 139 additions and 41 deletions
+12 -1
View File
@@ -2,7 +2,13 @@
package ipcauth
import "google.golang.org/grpc/credentials"
import (
"fmt"
"net"
"runtime"
"google.golang.org/grpc/credentials"
)
// NewTransportCredentials returns nil on platforms without a peer-identity
// primitive. The daemon falls back to insecure credentials and skips per-RPC
@@ -10,3 +16,8 @@ import "google.golang.org/grpc/credentials"
func NewTransportCredentials() credentials.TransportCredentials {
return nil
}
// ConnIdentity is unsupported on platforms without a peer-identity primitive.
func ConnIdentity(net.Conn) (Identity, error) {
return Identity{}, fmt.Errorf("peer identity not supported on %s", runtime.GOOS)
}
+8 -1
View File
@@ -27,9 +27,16 @@ func (unixCreds) ClientHandshake(_ context.Context, _ string, conn net.Conn) (ne
return conn, AuthInfo{}, nil
}
// 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).
func ConnIdentity(conn net.Conn) (Identity, error) {
return PeerIdentity(conn)
}
// ServerHandshake extracts the peer identity and fails closed if it cannot be read.
func (unixCreds) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) {
id, err := PeerIdentity(conn)
id, err := ConnIdentity(conn)
if err != nil {
return nil, nil, err
}
+12 -6
View File
@@ -42,17 +42,23 @@ func (winpipeCreds) ClientHandshake(_ context.Context, _ string, conn net.Conn)
return conn, AuthInfo{}, nil
}
// 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) {
// ConnIdentity extracts the caller's identity from an accepted named-pipe
// connection by impersonating the pipe client and reading its token. It is
// shared by the gRPC transport credentials and the JSON gateway (which forwards
// it). Requires the client to have connected at SECURITY_IDENTIFICATION.
func ConnIdentity(conn net.Conn) (Identity, error) {
// go-winio's pipe connection embeds *win32File, which exposes Fd().
fdConn, ok := conn.(interface{ Fd() uintptr })
if !ok {
return nil, nil, fmt.Errorf("connection %T does not expose a pipe handle", conn)
return Identity{}, fmt.Errorf("connection %T does not expose a pipe handle", conn)
}
handle := windows.Handle(fdConn.Fd())
return pipeClientIdentity(windows.Handle(fdConn.Fd()))
}
id, err := pipeClientIdentity(handle)
// 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) {
id, err := ConnIdentity(conn)
if err != nil {
return nil, nil, err
}
+29 -8
View File
@@ -12,16 +12,28 @@ import (
// itself the daemon (self/privileged) — i.e. the loopback gateway — so a direct
// gRPC caller cannot forge them.
const (
mdFwdUID = "x-netbird-fwd-uid"
mdFwdGID = "x-netbird-fwd-gid"
mdFwdUID = "x-netbird-fwd-uid" // Unix
mdFwdGID = "x-netbird-fwd-gid" // Unix
mdFwdSID = "x-netbird-fwd-sid" // Windows user SID
mdFwdGroup = "x-netbird-fwd-group" // Windows group SID (repeated)
mdFwdElevated = "x-netbird-fwd-elevated" // Windows, "1" if elevated
)
// ForwardIdentityMetadata encodes a Unix identity for the gateway to forward to
// the daemon. Windows identities are not forwarded (the gateway cannot read a
// pipe token for an HTTP client); nil is returned in that case.
// 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.
func ForwardIdentityMetadata(id Identity) metadata.MD {
if id.IsWindows() {
return nil
md := metadata.MD{}
md.Set(mdFwdSID, id.SID)
if len(id.Groups) > 0 {
md.Set(mdFwdGroup, id.Groups...)
}
if id.Elevated {
md.Set(mdFwdElevated, "1")
}
return md
}
return metadata.Pairs(
mdFwdUID, strconv.FormatUint(uint64(id.UID), 10),
@@ -29,13 +41,22 @@ func ForwardIdentityMetadata(id Identity) metadata.MD {
)
}
// forwardedIdentity extracts a forwarded Unix identity from incoming gRPC
// metadata, if present and well-formed.
// forwardedIdentity extracts a forwarded identity from incoming gRPC metadata,
// if present and well-formed. Windows (SID) takes precedence over Unix (uid).
func forwardedIdentity(ctx context.Context) (Identity, bool) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return Identity{}, false
}
if sid := mdFirst(md, mdFwdSID); sid != "" {
return Identity{
SID: sid,
Groups: md.Get(mdFwdGroup),
Elevated: mdFirst(md, mdFwdElevated) == "1",
}, true
}
uidStr := mdFirst(md, mdFwdUID)
if uidStr == "" {
return Identity{}, false
+42
View File
@@ -0,0 +1,42 @@
package ipcauth
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc/metadata"
)
func TestForwardIdentityRoundTrip(t *testing.T) {
cases := []struct {
name string
id Identity
}{
{"unix uid/gid", Identity{UID: 1000, GID: 1000}},
{"windows sid+groups+elevated", Identity{
SID: "S-1-5-21-1-2-3-1001",
Groups: []string{"S-1-5-32-544", "S-1-1-0"},
Elevated: true,
}},
{"windows sid only", Identity{SID: "S-1-5-21-9"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ctx := metadata.NewIncomingContext(context.Background(), ForwardIdentityMetadata(tc.id))
got, ok := forwardedIdentity(ctx)
assert.True(t, ok)
assert.Equal(t, tc.id, got)
})
}
}
func TestForwardedIdentity_None(t *testing.T) {
_, ok := forwardedIdentity(context.Background())
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)
}