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:
129
internal/daemon/launchd_test.go
Normal file
129
internal/daemon/launchd_test.go
Normal file
@@ -0,0 +1,129 @@
|
||||
//go:build darwin
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGeneratePlist(t *testing.T) {
|
||||
plist := generatePlist()
|
||||
|
||||
// Verify it is valid-looking XML with the expected plist header.
|
||||
if !strings.HasPrefix(plist, `<?xml version="1.0" encoding="UTF-8"?>`) {
|
||||
t.Error("plist should start with XML declaration")
|
||||
}
|
||||
|
||||
if !strings.Contains(plist, `<!DOCTYPE plist PUBLIC`) {
|
||||
t.Error("plist should contain DOCTYPE declaration")
|
||||
}
|
||||
|
||||
if !strings.Contains(plist, `<plist version="1.0">`) {
|
||||
t.Error("plist should contain plist version tag")
|
||||
}
|
||||
|
||||
// Verify the label matches the constant.
|
||||
expectedLabel := "<string>" + LaunchDaemonLabel + "</string>"
|
||||
if !strings.Contains(plist, expectedLabel) {
|
||||
t.Errorf("plist should contain label %q", LaunchDaemonLabel)
|
||||
}
|
||||
|
||||
// Verify program arguments.
|
||||
if !strings.Contains(plist, "<string>"+InstallBinaryPath+"</string>") {
|
||||
t.Errorf("plist should reference binary path %q", InstallBinaryPath)
|
||||
}
|
||||
|
||||
if !strings.Contains(plist, "<string>daemon</string>") {
|
||||
t.Error("plist should contain 'daemon' argument")
|
||||
}
|
||||
|
||||
if !strings.Contains(plist, "<string>run</string>") {
|
||||
t.Error("plist should contain 'run' argument")
|
||||
}
|
||||
|
||||
// Verify RunAtLoad and KeepAlive.
|
||||
if !strings.Contains(plist, "<key>RunAtLoad</key><true/>") {
|
||||
t.Error("plist should have RunAtLoad set to true")
|
||||
}
|
||||
|
||||
if !strings.Contains(plist, "<key>KeepAlive</key><true/>") {
|
||||
t.Error("plist should have KeepAlive set to true")
|
||||
}
|
||||
|
||||
// Verify log paths.
|
||||
if !strings.Contains(plist, "/var/log/greywall.log") {
|
||||
t.Error("plist should reference /var/log/greywall.log for stdout/stderr")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePlistProgramArguments(t *testing.T) {
|
||||
plist := generatePlist()
|
||||
|
||||
// Verify the ProgramArguments array contains exactly 3 entries in order.
|
||||
// The array should be: /usr/local/bin/greywall, daemon, run
|
||||
argStart := strings.Index(plist, "<key>ProgramArguments</key>")
|
||||
if argStart == -1 {
|
||||
t.Fatal("plist should contain ProgramArguments key")
|
||||
}
|
||||
|
||||
// Extract the array section.
|
||||
arrayStart := strings.Index(plist[argStart:], "<array>")
|
||||
arrayEnd := strings.Index(plist[argStart:], "</array>")
|
||||
if arrayStart == -1 || arrayEnd == -1 {
|
||||
t.Fatal("ProgramArguments should contain an array")
|
||||
}
|
||||
|
||||
arrayContent := plist[argStart+arrayStart : argStart+arrayEnd]
|
||||
|
||||
expectedArgs := []string{InstallBinaryPath, "daemon", "run"}
|
||||
for _, arg := range expectedArgs {
|
||||
if !strings.Contains(arrayContent, "<string>"+arg+"</string>") {
|
||||
t.Errorf("ProgramArguments array should contain %q", arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsInstalledReturnsFalse(t *testing.T) {
|
||||
// On a test machine without the daemon installed, this should return false.
|
||||
// We cannot guarantee the daemon is not installed, but on most dev machines
|
||||
// it will not be. This test verifies the function runs without panicking.
|
||||
result := IsInstalled()
|
||||
|
||||
// We only validate the function returns a bool without error.
|
||||
// On CI/dev machines the plist should not exist.
|
||||
_ = result
|
||||
}
|
||||
|
||||
func TestIsRunningReturnsFalse(t *testing.T) {
|
||||
// On a test machine without the daemon running, this should return false.
|
||||
// Similar to TestIsInstalledReturnsFalse, we verify it runs cleanly.
|
||||
result := IsRunning()
|
||||
_ = result
|
||||
}
|
||||
|
||||
func TestConstants(t *testing.T) {
|
||||
// Verify constants have expected values.
|
||||
tests := []struct {
|
||||
name string
|
||||
got string
|
||||
expected string
|
||||
}{
|
||||
{"LaunchDaemonLabel", LaunchDaemonLabel, "co.greyhaven.greywall"},
|
||||
{"LaunchDaemonPlistPath", LaunchDaemonPlistPath, "/Library/LaunchDaemons/co.greyhaven.greywall.plist"},
|
||||
{"InstallBinaryPath", InstallBinaryPath, "/usr/local/bin/greywall"},
|
||||
{"InstallLibDir", InstallLibDir, "/usr/local/lib/greywall"},
|
||||
{"SandboxUserName", SandboxUserName, "_greywall"},
|
||||
{"SandboxUserUID", SandboxUserUID, "399"},
|
||||
{"SandboxGroupName", SandboxGroupName, "_greywall"},
|
||||
{"SudoersFilePath", SudoersFilePath, "/etc/sudoers.d/greywall"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.got != tt.expected {
|
||||
t.Errorf("%s = %q, want %q", tt.name, tt.got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user