Early-stage software. Shurli is experimental and built with AI assistance. It will have bugs. Not recommended for production or safety-critical use. Read the disclaimer.

Batch F - Daemon Mode

Unix socket IPC, cookie authentication, RuntimeInfo interface, and hot-reload authorized_keys.


ADR-F01: Unix Socket (Not TCP)

Context: The daemon needs a control API for CLI subcommands (shurli daemon status, shurli daemon ping, etc.). Need an IPC mechanism.

Alternatives considered:

  • TCP on localhost - Universal, works on all platforms. Rejected because (a) any local process can connect (no filesystem permissions), (b) port conflicts with other services, (c) potentially exposed if firewall misconfigured.
  • Named pipes - Windows-friendly. Rejected because they don’t support HTTP natively and complicate the implementation.
  • gRPC - Type-safe, bi-directional streaming. Rejected because it adds protobuf dependency, code generation, and binary size. HTTP+JSON is simpler and sufficient.

Decision: Unix domain socket at ~/.config/shurli/shurli.sock with HTTP/1.1 over it. Socket created with umask(0077) to ensure 0700 permissions atomically (no TOCTOU race between Listen() and Chmod()). Stale socket detection: try connecting first, only remove if connection fails.

Consequences: Unix-only (no Windows support for now). Accepted because Shurli’s target users are Linux/macOS. Socket permissions enforce that only the owning user can connect. The HTTP layer means standard tools (curl --unix-socket) work for debugging.

Reference: https://github.com/shurlinet/shurli/blob/main/internal/daemon/server.go:86-138


ADR-F02: Cookie Auth (Not mTLS)

Context: Even with socket permissions, the API needs authentication to prevent attacks via symlink races or debugger attachment.

Alternatives considered:

  • mTLS - Strong mutual authentication. Rejected because it requires certificate management, key generation, and trust store configuration - too complex for a local IPC mechanism.
  • Token in socket filename - Embed the token in the path. Rejected because path-based auth is fragile and leaks the token in ps output and logs.
  • No auth (rely on socket permissions) - Rejected because defense-in-depth requires authentication even when filesystem permissions are correct.

Decision: 32-byte random hex cookie written to ~/.config/shurli/.daemon-cookie with 0600 permissions. CLI reads the cookie and sends it as Authorization: Bearer <token>. Cookie is rotated every daemon restart. Written AFTER socket is secured (ordering prevents clients from reading cookie before socket is ready).

Consequences: Simple, fast, no crypto libraries needed. The cookie file is the single secret - protect it like an SSH private key. If compromised, restart the daemon to rotate.

Reference: https://github.com/shurlinet/shurli/blob/main/internal/daemon/server.go:88-116, https://github.com/shurlinet/shurli/blob/main/internal/daemon/client.go


ADR-F03: RuntimeInfo Interface

Context: The daemon server needs access to the P2P network, config paths, version info, and connection methods. But the daemon package shouldn’t import cmd/shurli.

Alternatives considered:

  • Pass individual fields - NewServer(network, configPath, authKeys, version, ...). Rejected because the parameter list would grow with every new feature.
  • Share a struct directly - Import the runtime struct from cmd. Rejected because it creates a circular dependency between https://github.com/shurlinet/shurli/blob/main/internal/daemon and cmd/shurli.

Decision: daemon.RuntimeInfo interface with methods: Network(), ConfigFile(), AuthKeysPath(), GaterForHotReload(), Version(), StartTime(), PingProtocolID(), ConnectToPeer(). The serveRuntime struct in https://github.com/shurlinet/shurli/blob/main/cmd/shurli/cmd_daemon.go implements it.

Consequences: Clean dependency direction (daemon depends on interface, not concrete type). Easy to mock in tests (mockRuntime). Adding new runtime capabilities means adding methods to the interface - intentionally explicit.

Reference: https://github.com/shurlinet/shurli/blob/main/internal/daemon/server.go:23-32, https://github.com/shurlinet/shurli/blob/main/cmd/shurli/cmd_daemon.go:23-28


ADR-F04: Hot-Reload authorized_keys

Context: Adding or removing peers via shurli daemon auth add/remove should take effect immediately without restarting the daemon.

Alternatives considered:

  • File watcher (fsnotify) - Watch the file for changes. Rejected because it adds a dependency and doesn’t help with API-triggered changes (where we already know when to reload).
  • Restart required - Simpler but terrible UX. Rejected.

Decision: GaterReloader interface with ReloadFromFile() method. When the daemon API adds/removes a peer from the authorized_keys file, it immediately calls ReloadFromFile(), which re-reads the file and calls gater.UpdateAuthorizedPeers() with the new map. The gater uses sync.RWMutex for concurrent safety.

Consequences: Changes are atomic (read file, swap map under lock). No file watching needed. The gater’s authorizedPeers map is replaced entirely - no incremental updates. This is fine because the authorized_keys file is small (typically <100 entries).

Reference: https://github.com/shurlinet/shurli/blob/main/cmd/shurli/cmd_daemon.go:37-51, https://github.com/shurlinet/shurli/blob/main/internal/auth/gater.go:74-79