Skip to content

fix: bind Unix socket listener in Rust for --listen-socket - #27

Merged
abienkowski merged 2 commits into
mainfrom
fix/rust-unix-socket-listener
Sep 11, 2026
Merged

abienkowski merged 2 commits into
mainfrom
fix/rust-unix-socket-listener

Conversation

@abienkowski

Copy link
Copy Markdown
Collaborator

Summary

Fixes #25 — the Rust implementation parsed --listen-socket but never bound a UnixListener, so it silently ran TCP-only. This bypassed the socket-ownership security boundary that flag exists for (SocketMode=0660/SocketGroup deployments), and made fd://3 systemd socket activation unimplementable.

Changes (rs/src/main.rs only)

  • Bind a real tokio::net::UnixListener for --listen-socket, served concurrently with the TCP listener via two independent listener tasks coordinated by a broadcast shutdown channel (replacing the single-listener mpsc shutdown).
  • Remove a stale socket file left by a prior run before binding — matches Go's os.Remove behavior.
  • Support fd://3 systemd socket activation by adopting the fd (unix_listener_from_raw_fd, split out so the fd-adoption mechanics are unit-testable without touching the process's real fd 3).
  • Extracted serve_connection<S> so both listeners share the hyper HTTP/1 serving path.
  • Each listener now fails independently (log + return) instead of crashing the whole process on bad config — matches Go's startListener behavior.
  • Added 3 unit tests (stale-file removal, fresh bind, raw-fd wrapping).

Out of scope (noted, not fixed here)

  • Removing #![allow(dead_code)] (suggested in the issue) surfaces 7 pre-existing, unrelated dead-code warnings in policy.rs/proxy.rs/middleware.rs (fields kept for YAML deserialization completeness, test helpers). Restored the attribute with a comment explaining why; left as a separate cleanup.
  • README already correctly documented --listen-socket as "Go/Rust only" — no doc changes needed.
  • Cross-language integration test for the listen socket (issue's suggestion Quint spec review: verify invariants map to implementation behavior #5) — deferred per scope decision; covered here at the unit level only.

Verification

  • cargo build --release: clean, no warnings
  • cargo test (make test-rs): 115/115 passing (112 existing + 3 new)
  • make lint-rs: clean
  • Manual smoke test: ran the real binary; confirmed the Unix socket file is created, accepts a real HTTP request end-to-end through the full proxy handler, TCP listener works concurrently, and SIGTERM cleanly shuts down both listeners.

The Rust implementation parsed --listen-socket but never bound a
UnixListener, silently running TCP-only despite advertising Unix-socket
support. This bypassed the socket-ownership security boundary the flag
exists for, and made fd://3 systemd socket activation unimplementable.

- Bind a real tokio::net::UnixListener for --listen-socket, served
  concurrently with the TCP listener via independent listener tasks
  coordinated by a broadcast shutdown channel.
- Remove a stale socket file left by a prior run before binding,
  matching the Go implementation.
- Support fd://3 systemd socket activation by adopting the fd (split
  into a small helper so the fd-adoption logic is unit-testable
  without touching the process's real fd 3).
- Extract serve_connection<S> so both listeners share the hyper
  HTTP/1 serving path; each listener now fails independently (log +
  return) instead of crashing the whole process on bad config,
  matching Go's startListener behavior.
- Add unit tests for stale-file cleanup, fresh bind, and raw-fd
  wrapping.

Fixes #25
@abienkowski abienkowski self-assigned this Sep 11, 2026
- Create both broadcast receivers before spawning the signal-handler
  tasks: a broadcast send with zero receivers is dropped, so a signal
  arriving before the listeners subscribed was silently lost.
- Back off 100ms after a failed accept so persistent errors (EMFILE,
  non-listening fd under socket activation) don't busy-loop at 100% CPU.
- Reword the from_raw_fd SAFETY comment: the fd comes from user input
  and may be invalid; misuse surfaces as io::Error, not UB.
@abienkowski

Copy link
Copy Markdown
Collaborator Author

Self-review (dispatched independent code review against b446087..448af95) found two issues, both fixed in 4be5425:

  1. Shutdown subscribe race — the signal-handler tasks were spawned before any broadcast receiver existed; a send with zero receivers is dropped, so a signal landing in that window would be lost and the process would never shut down. Both receivers are now created before the signal tasks spawn.
  2. Accept-error busy loop — a persistent accept() failure (EMFILE, or a non-listening fd under fd://3 socket activation) retried instantly at 100% CPU. Both accept loops now back off 100ms after an error.

Also reworded the from_raw_fd SAFETY comment: the fd comes from user input and may be invalid; misuse surfaces as io::Error, never UB.

Noted for follow-up issues (pre-existing, not regressions from this PR):

  • Graceful connection draining on shutdown: Go drains in-flight requests via server.Shutdown (30s timeout); Rust drops in-flight connections when the runtime exits. Same behavior as before this PR.
  • fd://3 validation: Go's net.FileListener verifies the fd is a socket; Rust adopts it blindly (errors surface on first accept, now with backoff).
  • Exit code when all listeners fail to bind: process exits 0 with nothing serving; Restart=on-failure won't restart it. (Go blocks forever with zero listeners — differently wrong.)

Verification after fixes: cargo build --release clean, 115/115 tests pass, manual smoke test confirms unix+TCP serving and clean SIGTERM shutdown.

@abienkowski

Copy link
Copy Markdown
Collaborator Author

Follow-ups from the review are now tracked: #28 (graceful drain), #29 (fd://3 validation), #30 (all-listeners-failed exit code).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rust implementation ignores --listen-socket and is TCP-only (no UnixListener, no fd:// socket activation)

1 participant