This repository has been archived on 2026-03-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
greywall/internal/daemon/server_test.go
Mathieu Virbel cfe29d2c0b 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
2026-02-26 09:56:15 -06:00

528 lines
14 KiB
Go

package daemon
import (
"encoding/json"
"net"
"os"
"path/filepath"
"testing"
"time"
)
// testSocketPath returns a temporary Unix socket path for testing.
// macOS limits Unix socket paths to 104 bytes, so we use a short temp directory
// under /tmp rather than the longer t.TempDir() paths.
func testSocketPath(t *testing.T) string {
t.Helper()
dir, err := os.MkdirTemp("/tmp", "gw-")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
sockPath := filepath.Join(dir, "d.sock")
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
return sockPath
}
func TestServerStartStop(t *testing.T) {
sockPath := testSocketPath(t)
srv := NewServer(sockPath, "/nonexistent/tun2socks", true)
if err := srv.Start(); err != nil {
t.Fatalf("Start failed: %v", err)
}
// Verify socket file exists.
info, err := os.Stat(sockPath)
if err != nil {
t.Fatalf("Socket file not found: %v", err)
}
// Verify socket permissions (0666 — any local user can connect).
perm := info.Mode().Perm()
if perm != 0o666 {
t.Errorf("Expected socket permissions 0666, got %o", perm)
}
// Verify no active sessions at start.
if n := srv.ActiveSessions(); n != 0 {
t.Errorf("Expected 0 active sessions, got %d", n)
}
if err := srv.Stop(); err != nil {
t.Fatalf("Stop failed: %v", err)
}
// Verify socket file is removed after stop.
if _, err := os.Stat(sockPath); !os.IsNotExist(err) {
t.Error("Socket file should be removed after stop")
}
}
func TestServerStartRemovesStaleSocket(t *testing.T) {
sockPath := testSocketPath(t)
// Create a stale socket file.
if err := os.WriteFile(sockPath, []byte("stale"), 0o600); err != nil {
t.Fatalf("Failed to create stale file: %v", err)
}
srv := NewServer(sockPath, "/nonexistent/tun2socks", true)
if err := srv.Start(); err != nil {
t.Fatalf("Start failed with stale socket: %v", err)
}
defer srv.Stop() //nolint:errcheck // test cleanup
// Verify the server is listening by connecting.
conn, err := net.DialTimeout("unix", sockPath, 2*time.Second)
if err != nil {
t.Fatalf("Failed to connect to server: %v", err)
}
_ = conn.Close()
}
func TestServerDoubleStop(t *testing.T) {
sockPath := testSocketPath(t)
srv := NewServer(sockPath, "/nonexistent/tun2socks", false)
if err := srv.Start(); err != nil {
t.Fatalf("Start failed: %v", err)
}
// First stop should succeed.
if err := srv.Stop(); err != nil {
t.Fatalf("First stop failed: %v", err)
}
// Second stop should not panic (socket already removed).
_ = srv.Stop()
}
func TestProtocolStatus(t *testing.T) {
sockPath := testSocketPath(t)
srv := NewServer(sockPath, "/nonexistent/tun2socks", true)
if err := srv.Start(); err != nil {
t.Fatalf("Start failed: %v", err)
}
defer srv.Stop() //nolint:errcheck // test cleanup
// Send a status request.
resp := sendTestRequest(t, sockPath, Request{Action: "status"})
if !resp.OK {
t.Fatalf("Expected OK=true, got error: %s", resp.Error)
}
if !resp.Running {
t.Error("Expected Running=true")
}
if resp.ActiveSessions != 0 {
t.Errorf("Expected 0 active sessions, got %d", resp.ActiveSessions)
}
}
func TestProtocolUnknownAction(t *testing.T) {
sockPath := testSocketPath(t)
srv := NewServer(sockPath, "/nonexistent/tun2socks", true)
if err := srv.Start(); err != nil {
t.Fatalf("Start failed: %v", err)
}
defer srv.Stop() //nolint:errcheck // test cleanup
resp := sendTestRequest(t, sockPath, Request{Action: "unknown_action"})
if resp.OK {
t.Fatal("Expected OK=false for unknown action")
}
if resp.Error == "" {
t.Error("Expected error message for unknown action")
}
}
func TestProtocolCreateSessionMissingProxy(t *testing.T) {
sockPath := testSocketPath(t)
srv := NewServer(sockPath, "/nonexistent/tun2socks", true)
if err := srv.Start(); err != nil {
t.Fatalf("Start failed: %v", err)
}
defer srv.Stop() //nolint:errcheck // test cleanup
// Create session without proxy_url should fail.
resp := sendTestRequest(t, sockPath, Request{
Action: "create_session",
})
if resp.OK {
t.Fatal("Expected OK=false for missing proxy URL")
}
if resp.Error == "" {
t.Error("Expected error message for missing proxy URL")
}
}
func TestProtocolCreateSessionTunFailure(t *testing.T) {
sockPath := testSocketPath(t)
// Use a nonexistent tun2socks path so TunManager.Start() will fail.
srv := NewServer(sockPath, "/nonexistent/tun2socks", true)
if err := srv.Start(); err != nil {
t.Fatalf("Start failed: %v", err)
}
defer srv.Stop() //nolint:errcheck // test cleanup
// Create session should fail because tun2socks binary does not exist.
resp := sendTestRequest(t, sockPath, Request{
Action: "create_session",
ProxyURL: "socks5://127.0.0.1:1080",
})
if resp.OK {
t.Fatal("Expected OK=false when tun2socks is not available")
}
if resp.Error == "" {
t.Error("Expected error message when tun2socks fails")
}
// Verify no session was created.
if srv.ActiveSessions() != 0 {
t.Error("Expected 0 active sessions after failed create")
}
}
func TestProtocolDestroySessionMissingID(t *testing.T) {
sockPath := testSocketPath(t)
srv := NewServer(sockPath, "/nonexistent/tun2socks", true)
if err := srv.Start(); err != nil {
t.Fatalf("Start failed: %v", err)
}
defer srv.Stop() //nolint:errcheck // test cleanup
resp := sendTestRequest(t, sockPath, Request{
Action: "destroy_session",
})
if resp.OK {
t.Fatal("Expected OK=false for missing session ID")
}
if resp.Error == "" {
t.Error("Expected error message for missing session ID")
}
}
func TestProtocolDestroySessionNotFound(t *testing.T) {
sockPath := testSocketPath(t)
srv := NewServer(sockPath, "/nonexistent/tun2socks", true)
if err := srv.Start(); err != nil {
t.Fatalf("Start failed: %v", err)
}
defer srv.Stop() //nolint:errcheck // test cleanup
resp := sendTestRequest(t, sockPath, Request{
Action: "destroy_session",
SessionID: "nonexistent-session-id",
})
if resp.OK {
t.Fatal("Expected OK=false for nonexistent session")
}
if resp.Error == "" {
t.Error("Expected error message for nonexistent session")
}
}
func TestProtocolInvalidJSON(t *testing.T) {
sockPath := testSocketPath(t)
srv := NewServer(sockPath, "/nonexistent/tun2socks", true)
if err := srv.Start(); err != nil {
t.Fatalf("Start failed: %v", err)
}
defer srv.Stop() //nolint:errcheck // test cleanup
// Send invalid JSON to the server.
conn, err := net.DialTimeout("unix", sockPath, 2*time.Second)
if err != nil {
t.Fatalf("Failed to connect: %v", err)
}
defer conn.Close() //nolint:errcheck // test cleanup
if _, err := conn.Write([]byte("not valid json\n")); err != nil {
t.Fatalf("Failed to write: %v", err)
}
// Read error response.
_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
decoder := json.NewDecoder(conn)
var resp Response
if err := decoder.Decode(&resp); err != nil {
t.Fatalf("Failed to decode error response: %v", err)
}
if resp.OK {
t.Fatal("Expected OK=false for invalid JSON")
}
if resp.Error == "" {
t.Error("Expected error message for invalid JSON")
}
}
func TestClientIsRunning(t *testing.T) {
sockPath := testSocketPath(t)
client := NewClient(sockPath, true)
// Server not started yet.
if client.IsRunning() {
t.Error("Expected IsRunning=false when server is not started")
}
// Start the server.
srv := NewServer(sockPath, "/nonexistent/tun2socks", true)
if err := srv.Start(); err != nil {
t.Fatalf("Start failed: %v", err)
}
defer srv.Stop() //nolint:errcheck // test cleanup
// Now the client should detect the server.
if !client.IsRunning() {
t.Error("Expected IsRunning=true when server is running")
}
}
func TestClientStatus(t *testing.T) {
sockPath := testSocketPath(t)
srv := NewServer(sockPath, "/nonexistent/tun2socks", true)
if err := srv.Start(); err != nil {
t.Fatalf("Start failed: %v", err)
}
defer srv.Stop() //nolint:errcheck // test cleanup
client := NewClient(sockPath, true)
resp, err := client.Status()
if err != nil {
t.Fatalf("Status failed: %v", err)
}
if !resp.OK {
t.Fatalf("Expected OK=true, got error: %s", resp.Error)
}
if !resp.Running {
t.Error("Expected Running=true")
}
if resp.ActiveSessions != 0 {
t.Errorf("Expected 0 active sessions, got %d", resp.ActiveSessions)
}
}
func TestClientDestroySessionNotFound(t *testing.T) {
sockPath := testSocketPath(t)
srv := NewServer(sockPath, "/nonexistent/tun2socks", true)
if err := srv.Start(); err != nil {
t.Fatalf("Start failed: %v", err)
}
defer srv.Stop() //nolint:errcheck // test cleanup
client := NewClient(sockPath, true)
err := client.DestroySession("nonexistent-id")
if err == nil {
t.Fatal("Expected error for nonexistent session")
}
}
func TestClientConnectionRefused(t *testing.T) {
sockPath := testSocketPath(t)
// No server running.
client := NewClient(sockPath, true)
_, err := client.Status()
if err == nil {
t.Fatal("Expected error when server is not running")
}
_, err = client.CreateSession("socks5://127.0.0.1:1080", "")
if err == nil {
t.Fatal("Expected error when server is not running")
}
err = client.DestroySession("some-id")
if err == nil {
t.Fatal("Expected error when server is not running")
}
}
func TestProtocolMultipleStatusRequests(t *testing.T) {
sockPath := testSocketPath(t)
srv := NewServer(sockPath, "/nonexistent/tun2socks", true)
if err := srv.Start(); err != nil {
t.Fatalf("Start failed: %v", err)
}
defer srv.Stop() //nolint:errcheck // test cleanup
// Send multiple status requests sequentially (each on a new connection).
for i := 0; i < 5; i++ {
resp := sendTestRequest(t, sockPath, Request{Action: "status"})
if !resp.OK {
t.Fatalf("Request %d: expected OK=true, got error: %s", i, resp.Error)
}
}
}
func TestProtocolRequestResponseJSON(t *testing.T) {
// Test that protocol types serialize/deserialize correctly.
req := Request{
Action: "create_session",
ProxyURL: "socks5://127.0.0.1:1080",
DNSAddr: "1.1.1.1:53",
SessionID: "test-session",
}
data, err := json.Marshal(req)
if err != nil {
t.Fatalf("Failed to marshal request: %v", err)
}
var decoded Request
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Failed to unmarshal request: %v", err)
}
if decoded.Action != req.Action {
t.Errorf("Action: got %q, want %q", decoded.Action, req.Action)
}
if decoded.ProxyURL != req.ProxyURL {
t.Errorf("ProxyURL: got %q, want %q", decoded.ProxyURL, req.ProxyURL)
}
if decoded.DNSAddr != req.DNSAddr {
t.Errorf("DNSAddr: got %q, want %q", decoded.DNSAddr, req.DNSAddr)
}
if decoded.SessionID != req.SessionID {
t.Errorf("SessionID: got %q, want %q", decoded.SessionID, req.SessionID)
}
resp := Response{
OK: true,
SessionID: "abc123",
TunDevice: "utun7",
SandboxUser: "_greywall",
SandboxGroup: "_greywall",
Running: true,
ActiveSessions: 1,
}
data, err = json.Marshal(resp)
if err != nil {
t.Fatalf("Failed to marshal response: %v", err)
}
var decodedResp Response
if err := json.Unmarshal(data, &decodedResp); err != nil {
t.Fatalf("Failed to unmarshal response: %v", err)
}
if decodedResp.OK != resp.OK {
t.Errorf("OK: got %v, want %v", decodedResp.OK, resp.OK)
}
if decodedResp.SessionID != resp.SessionID {
t.Errorf("SessionID: got %q, want %q", decodedResp.SessionID, resp.SessionID)
}
if decodedResp.TunDevice != resp.TunDevice {
t.Errorf("TunDevice: got %q, want %q", decodedResp.TunDevice, resp.TunDevice)
}
if decodedResp.SandboxUser != resp.SandboxUser {
t.Errorf("SandboxUser: got %q, want %q", decodedResp.SandboxUser, resp.SandboxUser)
}
if decodedResp.SandboxGroup != resp.SandboxGroup {
t.Errorf("SandboxGroup: got %q, want %q", decodedResp.SandboxGroup, resp.SandboxGroup)
}
if decodedResp.Running != resp.Running {
t.Errorf("Running: got %v, want %v", decodedResp.Running, resp.Running)
}
if decodedResp.ActiveSessions != resp.ActiveSessions {
t.Errorf("ActiveSessions: got %d, want %d", decodedResp.ActiveSessions, resp.ActiveSessions)
}
}
func TestProtocolResponseOmitEmpty(t *testing.T) {
// Verify omitempty works: error-only response should not include session fields.
resp := Response{OK: false, Error: "something went wrong"}
data, err := json.Marshal(resp)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
var raw map[string]interface{}
if err := json.Unmarshal(data, &raw); err != nil {
t.Fatalf("Failed to unmarshal to map: %v", err)
}
// These fields should be omitted due to omitempty.
for _, key := range []string{"session_id", "tun_device", "sandbox_user", "sandbox_group"} {
if _, exists := raw[key]; exists {
t.Errorf("Expected %q to be omitted from JSON, but it was present", key)
}
}
// Error should be present.
if _, exists := raw["error"]; !exists {
t.Error("Expected 'error' field in JSON")
}
}
func TestGenerateSessionID(t *testing.T) {
// Verify session IDs are unique and properly formatted.
seen := make(map[string]bool)
for i := 0; i < 100; i++ {
id, err := generateSessionID()
if err != nil {
t.Fatalf("generateSessionID failed: %v", err)
}
if len(id) != 32 { // 16 bytes = 32 hex chars
t.Errorf("Expected 32-char hex ID, got %d chars: %q", len(id), id)
}
if seen[id] {
t.Errorf("Duplicate session ID: %s", id)
}
seen[id] = true
}
}
// sendTestRequest connects to the server, sends a JSON request, and returns
// the JSON response. This is a low-level helper that bypasses the Client
// to test the raw protocol.
func sendTestRequest(t *testing.T, sockPath string, req Request) Response {
t.Helper()
conn, err := net.DialTimeout("unix", sockPath, 2*time.Second)
if err != nil {
t.Fatalf("Failed to connect to server: %v", err)
}
defer conn.Close() //nolint:errcheck // test cleanup
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
encoder := json.NewEncoder(conn)
if err := encoder.Encode(req); err != nil {
t.Fatalf("Failed to encode request: %v", err)
}
decoder := json.NewDecoder(conn)
var resp Response
if err := decoder.Decode(&resp); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}
return resp
}