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
130 lines
4.0 KiB
Go
130 lines
4.0 KiB
Go
//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)
|
|
}
|
|
})
|
|
}
|
|
}
|