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 }