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
182 lines
4.5 KiB
Go
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...)
|
|
}
|
|
}
|