Files
netbird/client/internal/ipcauth/consoleuser_linux.go
T
Theodor Midtlien 90052cbefb [client] Profile ownership console user tofu (#7529)
* Add consoleuser and stamp default profile on known username in migration

* Refactor consoleuser to verify Id, fix seats on linux and default stamp

* Add default profile claim

* Add disable auto-claim of default profile and always fail close

* Add disable auto-claim flag to migration

* Adding timeout to console user on Linux and close library load on darwin

* Fixed failed close test

* Close both Dlopen for darwin

* Replace RegisterFunc with purego.Dlsym to avoid possible panic

* Fix freebsd tty enumeration

* Fix active profile migration logic and add test

* Log defaultClaimDisabled error once

* Guard against panicking console user lookup.

* Fix merge conflict

* Fix broken tests
2026-09-17 11:20:13 +02:00

174 lines
5.5 KiB
Go

package ipcauth
import (
"context"
"fmt"
"strings"
"time"
"github.com/godbus/dbus/v5"
log "github.com/sirupsen/logrus"
)
const (
loginDest = "org.freedesktop.login1"
loginPath = dbus.ObjectPath("/org/freedesktop/login1")
loginInterface = "org.freedesktop.login1.Manager"
listSeats = loginInterface + ".ListSeats"
seatInterface = "org.freedesktop.login1.Seat"
seatActiveSession = seatInterface + ".ActiveSession"
sessionInterface = "org.freedesktop.login1.Session"
sessionActive = sessionInterface + ".Active"
sessionClass = sessionInterface + ".Class"
sessionRemote = sessionInterface + ".Remote"
sessionUser = sessionInterface + ".User"
// propertiesGet is the standard property reader. godbus offers no
// context-aware GetProperty, and GetProperty is only this call underneath,
// so a bounded read has to make it directly.
propertiesGet = "org.freedesktop.DBus.Properties.Get"
// nullObjectPath is what logind puts in an object path field that refers to
// nothing, a seat with no session in the foreground being the one that
// matters here.
nullObjectPath = dbus.ObjectPath("/")
// consoleLookupTimeout bounds the whole seat walk, not each call in it, so
// a machine with several seats cannot multiply what an unanswering bus
// costs. A healthy lookup is well under a millisecond.
consoleLookupTimeout = 1 * time.Second
)
// isConsoleUser reports whether id holds the foreground session of one of this
// machine's seats.
//
// No seats means no console, which is the honest answer for a headless machine
// and the one that keeps its profiles to the privileged caller.
func isConsoleUser(id Identity) bool {
// A SID belongs to a Windows principal and has no uid to compare. Nothing
// on Linux produces one, and comparing anyway would match uid 0.
if id.IsWindows() {
return false
}
conn, err := dbus.SystemBus()
if err != nil {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), consoleLookupTimeout)
defer cancel()
// ListSeats returns a(so): seat id and object path.
var seats []struct {
ID string
Path dbus.ObjectPath
}
if err := conn.Object(loginDest, loginPath).CallWithContext(ctx, listSeats, 0).Store(&seats); err != nil {
log.Debugf("cannot list seats, treating the caller as not at the console: %v", err)
return false
}
for _, seat := range seats {
uid, ok := consoleSessionUID(ctx, conn, seat.Path)
if ok && uid == id.UID {
return true
}
}
return false
}
// consoleSessionUID returns who holds a seat's foreground session, and false
// unless that session is a person logged in locally.
func consoleSessionUID(ctx context.Context, conn *dbus.Conn, seatPath dbus.ObjectPath) (uint32, bool) {
// ActiveSession is (so): session id and object path. It names nothing on a
// seat whose VT has been switched away from, and on one whose display
// manager has not started a session yet.
prop, err := property(ctx, conn.Object(loginDest, seatPath), seatActiveSession)
if err != nil {
return 0, false
}
var active struct {
ID string
Path dbus.ObjectPath
}
if err := prop.Store(&active); err != nil {
return 0, false
}
if active.ID == "" || active.Path == "" || active.Path == nullObjectPath {
return 0, false
}
session := conn.Object(loginDest, active.Path)
// Only "user" sessions count: a greeter or a lock screen is the display
// manager sitting at the seat, not somebody to hand a profile to.
if class, ok := stringProperty(ctx, session, sessionClass); !ok || class != "user" {
return 0, false
}
// A remote session can hold a seat, and unlike the session's class and
// type, which pam_systemd takes from the environment of whoever opened the
// session, remoteness is set by the thing that accepted the connection. So
// it is worth asking even once the seat is established.
if remote, ok := boolProperty(ctx, session, sessionRemote); !ok || remote {
return 0, false
}
// Implied by the seat having named this session, and kept as a cross-check
// against a foreground that moved between the two calls.
if isActive, ok := boolProperty(ctx, session, sessionActive); !ok || !isActive {
return 0, false
}
// User is (uo): uid and the user object's path. Read from the session
// object rather than carried over from a listing, so the uid returned and
// the checks above are known to describe the same session.
prop, err = property(ctx, session, sessionUser)
if err != nil {
return 0, false
}
var owner struct {
UID uint32
Path dbus.ObjectPath
}
if err := prop.Store(&owner); err != nil {
return 0, false
}
return owner.UID, true
}
// property reads one property under ctx, name being the interface.member form
// godbus takes. It is what GetProperty does, with a deadline the caller owns.
func property(ctx context.Context, obj dbus.BusObject, name string) (dbus.Variant, error) {
idx := strings.LastIndex(name, ".")
if idx < 0 || idx+1 == len(name) {
return dbus.Variant{}, fmt.Errorf("invalid property name %q", name)
}
var v dbus.Variant
err := obj.CallWithContext(ctx, propertiesGet, 0, name[:idx], name[idx+1:]).Store(&v)
return v, err
}
func stringProperty(ctx context.Context, obj dbus.BusObject, name string) (string, bool) {
prop, err := property(ctx, obj, name)
if err != nil {
return "", false
}
s, ok := prop.Value().(string)
return s, ok
}
func boolProperty(ctx context.Context, obj dbus.BusObject, name string) (bool, bool) {
prop, err := property(ctx, obj, name)
if err != nil {
return false, false
}
b, ok := prop.Value().(bool)
return b, ok
}