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...) } }