Skip to content

Listening socket: dockerd parity, Unix path only (drop fd:// and --listen-socket-mode, add single-instance lock) #45

Description

@abienkowski

Is your feature request related to a problem?

The listening socket should be as simple to operate as docker.sock: always 0660, owned by a well-known group, with access granted or revoked by group membership alone. Today it isn't, in three ways.

  1. TCP can still reach the listener. --listen-tcp was removed in Listen on Unix socket only; remove TCP listener and bring TypeScript to parity #35, but --listen-socket=fd://3 adopts whatever systemd passes, and ListenStream=127.0.0.1:2375 passes a TCP socket. Keeping TCP out depends on a separate guard in each language, and TypeScript fd://3 socket activation is broken: every valid socket is rejected at startup #44 shows one of them is wrong: TypeScript rejects every fd, valid ones included. fd://3 socket activation: not verified to be listening, and sd_listen_fds env contract unchecked in all three #29 lists further hardening that fd:// would need.
  2. --listen-socket-mode is a setting operators shouldn't have. Every mode except 0660 is either useless (connect(2) needs write permission) or dangerous (world-writable). dockerd has no such flag.
  3. A second instance takes over a live socket. Measured on main (15b8630) in all three languages: starting a second proxy on the same path unlinks the first proxy's live socket and binds its own. The first keeps running, but nothing can reach it any more. dockerd relies on its pidfile to prevent this; we have neither a pidfile nor any other check.

deploy/docker-compose.sock.yml also shows --listen-socket-group is redundant when the proxy runs as user: uid:gid, because bind(2) already gives the socket the process's group.

Describe the solution

Full design: spec/listener-design.md. In summary:

  • Unix socket path only. fd:// is removed and rejected like any other non-path value. This fixes TypeScript fd://3 socket activation is broken: every valid socket is rejected at startup #44 and makes fd://3 socket activation: not verified to be listening, and sd_listen_fds env contract unchecked in all three #29 moot.
  • The mode is always 0660. --listen-socket-mode is removed. The existing umask(0177) → bind → chown → chmod order stays.
  • Group selection works like dockerd's (moby/daemon/listeners/listeners_linux.go), with a default group named docker-socket-policy:
    • flag not passed and the group exists → use it;
    • flag not passed and the group is missing → warn and use the proxy's own group;
    • an explicit group that doesn't exist → exit 2;
    • --listen-socket-group="" → the proxy's own group, with no warning.
  • Single-instance lock (Go and Rust). Each takes flock(LOCK_EX|LOCK_NB) on <path>.lock (0600, O_NOFOLLOW, never unlinked) and holds it for as long as it runs. The kernel releases it on any exit, SIGKILL included.
  • "In use" check (all three). Before deleting an existing socket, the proxy connect(2)s to it. If anything answers, it refuses with "in use" and exits 1, leaving the live socket alone.
  • New Quint module spec/listener.qnt. It models three instances going through the socket setup one step at a time, interleaving and crashing at any point. It checks six invariants: no TCP, group set before mode, never world-writable, never deletes a non-socket, no live takeover, and group selection matching the table. quint test runs one test per design-table row, with same-named tests in Go, Rust and TS. make test-spec is added to CI.

This is a breaking CLI change: --listen-socket-mode and fd://3 are removed.

Describe alternatives

  • Harden fd:// instead (check SO_ACCEPTCONN, validate LISTEN_PID and LISTEN_FDS, fix the TypeScript guard): rejected. It keeps a way for TCP to reach the listener behind per-language guards, and one of those guards already shipped broken (TypeScript fd://3 socket activation is broken: every valid socket is rejected at startup #44). Removing the feature removes the whole class of bug.
  • Keep --listen-socket-mode: rejected. No valid mode exists besides 0660.
  • Drop --listen-socket-group too, always using the proxy's own group: rejected in favour of matching dockerd, so operators can use the familiar groupadd / usermod -aG steps.
  • A pidfile for single-instance protection: rejected. Checking a PID doesn't work across PID namespaces (two containers sharing a socket volume), and a crash leaves a stale pidfile behind. An flock needs no cleanup.
  • The "in use" check alone, with no lock: rejected for Go and Rust. It still has a race between checking and deleting: two instances starting together can both see a stale socket and both delete it. The Quint model is required to show that race when the lock is off.
  • TypeScript lock: Node has no flock (checked on Node 22). We considered a native addon and a lock-free link/rename protocol. For now TypeScript keeps only the "in use" check, and the race is documented. What closing the gap needs is tracked in a follow-up issue.

Which implementation(s) would this affect?

  • Go
  • Rust
  • TypeScript
  • Quint specification
  • All

Additional context

Supersedes #29 and fixes #44; both will be closed by the PR. Independent of #43.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Status: Break ChangeAdded to a PR or issue that would cause a breaking changeType: EnhancementAdded to issues and PRs when a change includes improvements or optimizations.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions