feat!: dockerd-parity listening socket, Unix path only - #47
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
With this change the proxy's listening socket works like
docker.sock. The socket is always0660and owned by a well-known group (docker-socket-policy), and access is granted or revoked by group membership alone. No code path can listen on anything except a Unix socket file the proxy created itself.Design:
spec/listener-design.md.Closes #45
Closes #29
Closes #44
Follow-up: #46 (TypeScript single-instance lock)
What changes
Unix socket path only.
fd://socket activation has been removed.fd://3is now rejected like any other address that isn't a path (only supports Unix socket paths, exit 2). It was the last remaining way for a TCP listener to get in:ListenStream=127.0.0.1:2375would hand one over. Removing it fixes TypeScript fd://3 socket activation is broken: every valid socket is rejected at startup #44 (TypeScript rejected every fd, valid ones included) and makes fd://3 socket activation: not verified to be listening, and sd_listen_fds env contract unchecked in all three #29's hardening moot.The mode is always
0660.--listen-socket-modehas been removed. The socket is still created at0600underumask(0177), then chowned, then chmodded.Group selection works like dockerd's (
moby/daemon/listeners/listeners_linux.go):--listen-socket-groupdocker-socket-policy=name=name=gid0-4294967294, otherwise exit 2=""If the proxy isn't a member of the selected group, chown fails with
EPERM. The proxy then exits 1, and the error says it must be a member of that group.Single-instance lock (Go and Rust). Each takes
flock(LOCK_EX|LOCK_NB)on<path>.lock, opened withO_NOFOLLOWand mode0600. The lock is held until exit and the file is never deleted. The kernel releases it on any exit, includingSIGKILL. A second instance exits 1 with<path> is in use by another instance (lock <path>.lock held).Live-socket check (all three). Before deleting an existing socket, the proxy tries to connect to it. If something answers, or the attempt times out, it exits 1 with
in use by another processand leaves the socket alone. "Connection refused" means the socket is stale, so it is replaced. Any other connect error, or something at the path that isn't a socket, is refused and left untouched.TypeScript exception. Node has no
flock, so TypeScript does only the live-socket check. That leaves a small check-then-delete race when two TypeScript instances start at the same moment. The race is documented in the README and in the spec, and TypeScript: single-instance lock for the listening socket #46 tracks closing it.On main before this PR
fd://3socket activation rejected every fd. Fixed by removing the feature.--listen-socket-group=4294967296(or+4294967296) was cut to 32 bits by chown and became gid 0. Rust and TypeScript differed on these inputs as well. All three now accept only a digit string in0-4294967294.Type of change
Implementation(s) changed
Formal specification
New module
spec/listener.qnt. It models three instances going through lock → check → delete → bind → chown → chmod, one step at a time, so their steps can interleave and any instance can crash at any point. It has two variants:listener_locked(Go and Rust) andlistener_unlocked(TypeScript).It checks six invariants:
neverListensOnTcp,groupBeforeMode,neverWorldWritable,neverUnlinksNonSocket,noLiveTakeoverandgroupSelectionMatchesTable.noLiveTakeoverdoes catch the race it's there for. With the lock turned off, the simulator finds the takeover:With the lock on, all six invariants hold (
[ok] No violation found). Each invariant was also checked by removing its protection in a scratch copy of the model and confirming it then fails.quint testhas onerunper row of the design tables. Each has a same-named unit test in Go, Rust and TypeScript, so a row can be traced across all four. The newmake test-spectarget runs these tests in the CI quint job and inrelease-verify.Testing
make test-all)make test-integration)make verify)runtestsEach suite passed in Go, Rust and TypeScript:
make test-integration-sock{,-rs,-ts}(15/15) andmake test-integration{,-rs,-ts}(27/27).New tests were run against the old code first. They failed as expected:
nonexistentmakesdefault.sock owned by docker-socket-policy (2001)fail with gid 65532.Checked by running the binaries, on macOS and in a Linux container:
0777socket is recreated at660.fd://3exits 2.Concurrency tests (Go, Rust): 8 instances started together, 50 times over. Each time exactly one serves, and the other seven get the lock message.
Breaking changes
--listen-socket-modehas been removed; passing it is an unknown-flag error (exit 2).--listen-socket=fd://3(systemd socket activation) has been removed. Use a plain.serviceinstead; see the README's "systemd service" section.--listen-socket-groupisn't passed, the socket now belongs todocker-socket-policyif that group exists. Before, it used the proxy's own group.The squash commit should keep this footer:
Checklist