Skip to content

About

Authenticated loopback control endpoints for Lisp image daemons, wooo

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

20 Commits

Folders and files

Repository files navigation

image-daemon

Authenticated loopback control endpoints for Lisp image daemons.

The library defines the wire protocol shared by a detached Lisp image and the terminals controlling it: bounded length-prefixed readable packet frames with reader evaluation disabled, capability tokens, timestamp-bearing session identifiers with legacy compatibility, and deadline-bounded loopback connection and call primitives. Load the optional runtime for private endpoint discovery, persistent request services, terminal relay ownership, bounded replay and attachment client loops.

Systems and package

  • ASDF system: image-daemon
  • Optional SBCL runtime system: image-daemon/runtime
  • Optional SBCL evaluation endpoint system: image-daemon/eval
  • Test system: image-daemon/tests
  • Package: IMAGE-DAEMON

Install the locked dependencies and run all tests:

./script/bootstrap
./script/check

The control protocol

(image-daemon:daemon-call port token ':status)

(multiple-value-bind (socket stream)
    (image-daemon:daemon-connect port)
  (image-daemon:daemon-write-packet stream packet)
  (image-daemon:daemon-read-response stream ':attach))

Packets are proper lists written readably and framed by one decimal character count. Reading rejects reader evaluation, unbalanced payloads, and frames above *DAEMON-PACKET-CHARACTER-LIMIT*. Every required response is bounded by *DAEMON-CONNECT-TIMEOUT-SECONDS*. Request frames keep the historical :LOCALGROUP-REQUEST tag for compatibility with deployed endpoints.

Persistent services and attachments

Create an endpoint with DAEMON-RUNTIME-CREATE and a request callback. The default mode publishes a discovery record and requires a registry directory and identifier. Pass :PUBLISH-P NIL for an unpublished ephemeral runtime; it needs neither and keeps no registry pathname. After your own startup transaction, call DAEMON-RUNTIME-START. Stop and unpublish a published runtime with DAEMON-RUNTIME-STOP. The callback receives (runtime request &key socket stream) after protocol and capability validation; write the response with the packet API. You may subclass DAEMON-RUNTIME and specialize DAEMON-RUNTIME-REQUEST-VALID-P and DAEMON-RUNTIME-ERROR-RESPONSE for an application protocol.

Use DAEMON-REGISTRY-DISCOVER to inspect complete discovery records. Registry writers serialize with a directory file lock; publication retains live or uncertain ownership unless the capability matches. Reconciliation deletes only definitely dead records that still equal the inspected value. Shutdown likewise compares the complete record, preserving an intervening replacement.

Create a RELAY with RELAY-CREATE. Implement the TRANSPORT- protocol for your foreground transport, or specialize it on a relay subclass to synchronize your UI’s state. RELAY-ATTACH queues the handshake and replay before live output, allows multiple observers and one controller, and revokes the former controller on takeover. Read semantic packets with RELAY-READ-ATTACHMENT. Resizes run under the relay lock, so a host resize method should publish to its UI thread without reacquiring that lock. Hosts decide when input readers may pause and whether releasing a foreground terminal requires process handoff.

ATTACHMENT-CREATE owns an asynchronous output writer. Pending output and replay have independent character limits. On overflow, shut down socket I/O before abortive stream close and join the writer with a bounded wait. The client-side DAEMON-ATTACH-CLIENT-RUN accepts input-readiness, event-read and resize callbacks; supply its socket or a shutdown callback that unblocks concurrent reads. Pass :packet-function to handle host-specific control packets in the receiver thread. Return true from that callback to end the attachment. Attachment handshakes include :history-start and :history-position character positions for replaying output produced during a reconnect.

When the hosted application is about to exit with a status its launcher acts on, call RELAY-FINISH with that status and an optional message. Every client receives it after the output already queued for it, and DAEMON-ATTACH-CLIENT-RUN returns it as (:STATUS STATUS :MESSAGE MESSAGE), so a relaying front process can exit exactly as a foreground application would have. Any other end returns NIL.

Process handoff

A process that hands its work to a detached replacement does it through a private ticket. handoff-ticket-pathname names one beneath a directory and handoff-ticket-write publishes the host’s record, which carries :state and :token. The replacement claims the ticket with handoff-ticket-claim, a rename that fails once the launcher has cancelled, and keeps checking handoff-ticket-owned-record while it starts. The launcher cancels with handoff-ticket-cancel, a rename the replacement then sees. Every state lives in a sibling named after the ticket (handoff-ticket-sibling), so handoff-ticket-delete removes the whole family.

handoff-launch-supervised starts the replacement behind a Bash supervisor that creates its process group, records the group identifier beside the ticket (handoff-ticket-launcher-pid), and only then lets the launcher run, so a failed handoff can always find what to stop. handoff-ticket-replacement-pid names the claimant. daemon-endpoint-status accepts a registry record only when a different process serves it with the expected token and answers an authenticated :status request, and daemon-wait-until polls such a check up to a deadline.

Evaluation endpoints

image-daemon/eval lets trusted local programs evaluate Common Lisp in the running image. eval-endpoint-create validates the settings: a private Unix socket or an IPv4 loopback TCP address, a token file, the package sources are read and evaluated in, and bounds on frames, sources, output, queued requests, clients and time. eval-endpoint-start listens and starts one accept thread and one serial evaluator; eval-endpoint-stop closes everything within a deadline and signals :quiesce naming any thread that stayed alive.

Every frame is a four-octet big-endian length followed by that many UTF-8 octets holding one S-expression. The length is checked before the payload is read, and the payload is parsed by a closed sexp-config grammar, so no frame, authenticated or not, interns a symbol or nests without bound. A connection starts with (:challenge :version 1 :algorithm :hmac-sha-256 :nonce HEX) and must answer (:authenticate :proof HEX), the HMAC-SHA-256 of the nonce keyed by the token file’s content. The endpoint reads the token only while checking a proof, refuses a token file that is not a private regular file, wipes every secret octet, and retains only the pathname. Authenticated clients then send (:evaluate :source TEXT) one at a time and receive an :evaluation-result with bounded values, captured output, and a :condition or :timeout status for failures, including debugger entry.

A Unix endpoint refuses an active socket and any object it does not own; a stale socket is renamed aside and removed only while it is still the socket that was inspected. eval-connect and eval-call are a complete client, and eval-proof computes a proof for clients written elsewhere.

(let ((endpoint (image-daemon:eval-endpoint-create
                 :transport ':unix
                 :unix-pathname #p"/run/user/1000/app/eval.sock"
                 :token-pathname #p"/home/me/.config/app/eval.token"
                 :package "MY-APP")))
  (image-daemon:eval-endpoint-start endpoint))

Failures

Failures signal DAEMON-ERROR carrying a message, the failing operation, and the session identifier when known. Hosts may substitute a subclass through *DAEMON-ERROR-CLASS* to join their own condition hierarchy. Evaluation endpoints signal EVAL-ENDPOINT-ERROR, a DAEMON-ERROR that adds a non-secret reason keyword and, for :quiesce, the names of the threads that did not stop.

Thread stops use one deadline across graceful exit and forced termination. If unwind cleanup cannot finish, handle DAEMON-ERROR with operation :STOP-THREAD. Recover the target with SB-THREAD:THREAD-ERROR-THREAD on DAEMON-ERROR-CAUSE, release the blocked cleanup, and retry DAEMON-STOP-THREAD. Attachment and runtime owners retain their thread references when a stop fails.

License

COLL-Attribution. See LICENSE.lisp.

About

Authenticated loopback control endpoints for Lisp image daemons, wooo

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages