You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Listening socket: dockerd parity, Unix path only (drop fd:// and --listen-socket-mode, add single-instance lock) #45
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.
--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.
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.
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.
Is your feature request related to a problem?
The listening socket should be as simple to operate as
docker.sock: always0660, owned by a well-known group, with access granted or revoked by group membership alone. Today it isn't, in three ways.--listen-tcpwas removed in Listen on Unix socket only; remove TCP listener and bring TypeScript to parity #35, but--listen-socket=fd://3adopts whatever systemd passes, andListenStream=127.0.0.1:2375passes 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 thatfd://would need.--listen-socket-modeis a setting operators shouldn't have. Every mode except0660is either useless (connect(2)needs write permission) or dangerous (world-writable). dockerd has no such flag.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.ymlalso shows--listen-socket-groupis redundant when the proxy runs asuser: uid:gid, becausebind(2)already gives the socket the process's group.Describe the solution
Full design:
spec/listener-design.md. In summary: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.0660.--listen-socket-modeis removed. The existingumask(0177)→ bind → chown → chmod order stays.moby/daemon/listeners/listeners_linux.go), with a default group nameddocker-socket-policy:--listen-socket-group=""→ the proxy's own group, with no warning.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,SIGKILLincluded.connect(2)s to it. If anything answers, it refuses with "in use" and exits 1, leaving the live socket alone.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 testruns one test per design-table row, with same-named tests in Go, Rust and TS.make test-specis added to CI.This is a breaking CLI change:
--listen-socket-modeandfd://3are removed.Describe alternatives
fd://instead (checkSO_ACCEPTCONN, validateLISTEN_PIDandLISTEN_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.--listen-socket-mode: rejected. No valid mode exists besides0660.--listen-socket-grouptoo, always using the proxy's own group: rejected in favour of matching dockerd, so operators can use the familiargroupadd/usermod -aGsteps.flockneeds no cleanup.flock(checked on Node 22). We considered a native addon and a lock-freelink/renameprotocol. 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?
Additional context
Supersedes #29 and fixes #44; both will be closed by the PR. Independent of #43.