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/client.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

145 lines
3.7 KiB
Go

package daemon
import (
"encoding/json"
"fmt"
"net"
"os"
"time"
)
const (
// clientDialTimeout is the maximum time to wait when connecting to the daemon.
clientDialTimeout = 5 * time.Second
// clientReadTimeout is the maximum time to wait for a response from the daemon.
clientReadTimeout = 30 * time.Second
)
// Client communicates with the greywall daemon over a Unix socket using
// newline-delimited JSON.
type Client struct {
socketPath string
debug bool
}
// NewClient creates a new daemon client that connects to the given Unix socket path.
func NewClient(socketPath string, debug bool) *Client {
return &Client{
socketPath: socketPath,
debug: debug,
}
}
// CreateSession asks the daemon to create a new sandbox session with the given
// proxy URL and optional DNS address. Returns the session info on success.
func (c *Client) CreateSession(proxyURL, dnsAddr string) (*Response, error) {
req := Request{
Action: "create_session",
ProxyURL: proxyURL,
DNSAddr: dnsAddr,
}
resp, err := c.sendRequest(req)
if err != nil {
return nil, fmt.Errorf("create session request failed: %w", err)
}
if !resp.OK {
return resp, fmt.Errorf("create session failed: %s", resp.Error)
}
return resp, nil
}
// DestroySession asks the daemon to tear down the session with the given ID.
func (c *Client) DestroySession(sessionID string) error {
req := Request{
Action: "destroy_session",
SessionID: sessionID,
}
resp, err := c.sendRequest(req)
if err != nil {
return fmt.Errorf("destroy session request failed: %w", err)
}
if !resp.OK {
return fmt.Errorf("destroy session failed: %s", resp.Error)
}
return nil
}
// Status queries the daemon for its current status.
func (c *Client) Status() (*Response, error) {
req := Request{
Action: "status",
}
resp, err := c.sendRequest(req)
if err != nil {
return nil, fmt.Errorf("status request failed: %w", err)
}
if !resp.OK {
return resp, fmt.Errorf("status request failed: %s", resp.Error)
}
return resp, nil
}
// IsRunning checks whether the daemon is reachable by attempting to connect
// to the Unix socket. Returns true if the connection succeeds.
func (c *Client) IsRunning() bool {
conn, err := net.DialTimeout("unix", c.socketPath, clientDialTimeout)
if err != nil {
return false
}
_ = conn.Close()
return true
}
// sendRequest connects to the daemon Unix socket, sends a JSON-encoded request,
// and reads back a JSON-encoded response.
func (c *Client) sendRequest(req Request) (*Response, error) {
c.logDebug("Connecting to daemon at %s", c.socketPath)
conn, err := net.DialTimeout("unix", c.socketPath, clientDialTimeout)
if err != nil {
return nil, fmt.Errorf("failed to connect to daemon at %s: %w", c.socketPath, err)
}
defer conn.Close() //nolint:errcheck // best-effort close on request completion
// Set a read deadline for the response.
if err := conn.SetReadDeadline(time.Now().Add(clientReadTimeout)); err != nil {
return nil, fmt.Errorf("failed to set read deadline: %w", err)
}
// Send the request as newline-delimited JSON.
encoder := json.NewEncoder(conn)
if err := encoder.Encode(req); err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
c.logDebug("Sent request: action=%s", req.Action)
// Read the response.
decoder := json.NewDecoder(conn)
var resp Response
if err := decoder.Decode(&resp); err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
c.logDebug("Received response: ok=%v", resp.OK)
return &resp, nil
}
// logDebug writes a debug message to stderr with the [greywall:daemon] prefix.
func (c *Client) logDebug(format string, args ...interface{}) {
if c.debug {
fmt.Fprintf(os.Stderr, "[greywall:daemon] "+format+"\n", args...)
}
}