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 9d5d852860 feat: switch macOS learning mode from fs_usage to eslogger
Replace fs_usage (reports Mach thread IDs, requiring process name matching
with false positives) with eslogger (Endpoint Security framework, reports
real Unix PIDs via audit_token.pid plus fork events for process tree tracking).

Key changes:
- Daemon starts eslogger instead of fs_usage, with early-exit detection
  and clear Full Disk Access error messaging
- New two-pass eslogger JSON parser: pass 1 builds PID tree from fork
  events, pass 2 filters filesystem events by PID set
- Remove runtime PID polling (StartPIDTracking, pollDescendantPIDs) —
  process tree is now built post-hoc from the eslogger log
- Platform-specific generateLearnedTemplatePlatform() for darwin/linux/stub
- Refactor TraceResult and GenerateLearnedTemplate to be platform-agnostic
2026-02-26 17:23:43 -06:00

182 lines
4.5 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
}
// StartLearning asks the daemon to start an fs_usage trace for learning mode.
func (c *Client) StartLearning() (*Response, error) {
req := Request{
Action: "start_learning",
}
resp, err := c.sendRequest(req)
if err != nil {
return nil, fmt.Errorf("start learning request failed: %w", err)
}
if !resp.OK {
return resp, fmt.Errorf("start learning failed: %s", resp.Error)
}
return resp, nil
}
// StopLearning asks the daemon to stop the fs_usage trace for the given learning session.
func (c *Client) StopLearning(learningID string) error {
req := Request{
Action: "stop_learning",
LearningID: learningID,
}
resp, err := c.sendRequest(req)
if err != nil {
return fmt.Errorf("stop learning request failed: %w", err)
}
if !resp.OK {
return fmt.Errorf("stop learning 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...)
}
}