feat: switch macOS daemon from user-based to group-based pf routing
Sandboxed commands previously ran as `sudo -u _greywall`, breaking user identity (home dir, SSH keys, git config). Now uses `sudo -u #<uid> -g _greywall` so the process keeps the real user's identity while pf matches on EGID for traffic routing. Key changes: - pf rules use `group <GID>` instead of `user _greywall` - GID resolved dynamically at daemon startup (not hardcoded, since macOS system groups like com.apple.access_ssh may claim preferred IDs) - Sudoers rule installed at /etc/sudoers.d/greywall (validated with visudo) - Invoking user added to _greywall group via dscl (not dseditgroup, which clobbers group attributes) - tun2socks device discovery scans both stdout and stderr (fixes 10s timeout caused by STACK message going to stdout) - Always-on daemon logging for session create/destroy events
This commit is contained in:
@@ -5,9 +5,20 @@ import (
|
||||
"os"
|
||||
|
||||
"gitea.app.monadical.io/monadical/greywall/internal/config"
|
||||
"gitea.app.monadical.io/monadical/greywall/internal/daemon"
|
||||
"gitea.app.monadical.io/monadical/greywall/internal/platform"
|
||||
)
|
||||
|
||||
// DaemonSession holds the state from an active daemon session on macOS.
|
||||
// When a daemon session is active, traffic is routed through pf + utun
|
||||
// instead of using env-var proxy settings.
|
||||
type DaemonSession struct {
|
||||
SessionID string
|
||||
TunDevice string
|
||||
SandboxUser string
|
||||
SandboxGroup string
|
||||
}
|
||||
|
||||
// Manager handles sandbox initialization and command wrapping.
|
||||
type Manager struct {
|
||||
config *config.Config
|
||||
@@ -22,6 +33,9 @@ type Manager struct {
|
||||
learning bool // learning mode: permissive sandbox with strace
|
||||
straceLogPath string // host-side temp file for strace output
|
||||
commandName string // name of the command being learned
|
||||
// macOS daemon session fields
|
||||
daemonClient *daemon.Client
|
||||
daemonSession *DaemonSession
|
||||
}
|
||||
|
||||
// NewManager creates a new sandbox manager.
|
||||
@@ -63,11 +77,36 @@ func (m *Manager) Initialize() error {
|
||||
return fmt.Errorf("sandbox is not supported on platform: %s", platform.Detect())
|
||||
}
|
||||
|
||||
// On macOS, the daemon is required for transparent proxying.
|
||||
// Without it, env-var proxying is unreliable (only works for tools that
|
||||
// honor HTTP_PROXY) and gives users a false sense of security.
|
||||
if platform.Detect() == platform.MacOS && m.config.Network.ProxyURL != "" {
|
||||
client := daemon.NewClient(daemon.DefaultSocketPath, m.debug)
|
||||
if !client.IsRunning() {
|
||||
return fmt.Errorf("greywall daemon is not running (required for macOS network sandboxing)\n\n" +
|
||||
" Install and start: sudo greywall daemon install\n" +
|
||||
" Check status: greywall daemon status")
|
||||
}
|
||||
m.logDebug("Daemon is running, requesting session")
|
||||
resp, err := client.CreateSession(m.config.Network.ProxyURL, m.config.Network.DnsAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create daemon session: %w", err)
|
||||
}
|
||||
m.daemonClient = client
|
||||
m.daemonSession = &DaemonSession{
|
||||
SessionID: resp.SessionID,
|
||||
TunDevice: resp.TunDevice,
|
||||
SandboxUser: resp.SandboxUser,
|
||||
SandboxGroup: resp.SandboxGroup,
|
||||
}
|
||||
m.logDebug("Daemon session created: id=%s device=%s user=%s group=%s", resp.SessionID, resp.TunDevice, resp.SandboxUser, resp.SandboxGroup)
|
||||
}
|
||||
|
||||
// On Linux, set up proxy bridge and tun2socks if proxy is configured
|
||||
if platform.Detect() == platform.Linux {
|
||||
if m.config.Network.ProxyURL != "" {
|
||||
// Extract embedded tun2socks binary
|
||||
tun2socksPath, err := extractTun2Socks()
|
||||
tun2socksPath, err := ExtractTun2Socks()
|
||||
if err != nil {
|
||||
m.logDebug("Failed to extract tun2socks: %v (will fall back to env-var proxying)", err)
|
||||
} else {
|
||||
@@ -148,7 +187,7 @@ func (m *Manager) WrapCommand(command string) (string, error) {
|
||||
plat := platform.Detect()
|
||||
switch plat {
|
||||
case platform.MacOS:
|
||||
return WrapCommandMacOS(m.config, command, m.exposedPorts, m.debug)
|
||||
return WrapCommandMacOS(m.config, command, m.exposedPorts, m.daemonSession, m.debug)
|
||||
case platform.Linux:
|
||||
if m.learning {
|
||||
return m.wrapCommandLearning(command)
|
||||
@@ -201,6 +240,16 @@ func (m *Manager) GenerateLearnedTemplate(cmdName string) (string, error) {
|
||||
|
||||
// Cleanup stops the proxies and cleans up resources.
|
||||
func (m *Manager) Cleanup() {
|
||||
// Destroy macOS daemon session if active.
|
||||
if m.daemonClient != nil && m.daemonSession != nil {
|
||||
m.logDebug("Destroying daemon session %s", m.daemonSession.SessionID)
|
||||
if err := m.daemonClient.DestroySession(m.daemonSession.SessionID); err != nil {
|
||||
m.logDebug("Warning: failed to destroy daemon session: %v", err)
|
||||
}
|
||||
m.daemonSession = nil
|
||||
m.daemonClient = nil
|
||||
}
|
||||
|
||||
if m.reverseBridge != nil {
|
||||
m.reverseBridge.Cleanup()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user