Skip to content

feat(cli): serve --bind to choose the listen address (default unchanged: 0.0.0.0) - #537

Merged
arun-pathiban-ddog merged 2 commits into
nerdsane:mainfrom
shlomobenyaminov:serve-bind-address
Oct 19, 2026
Merged

arun-pathiban-ddog merged 2 commits into
nerdsane:mainfrom
shlomobenyaminov:serve-bind-address

Conversation

@shlomobenyaminov

@shlomobenyaminov shlomobenyaminov commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

temper serve always bound 0.0.0.0, so a single-user server reached through an SSH tunnel or a local proxy had to be fenced off with firewall rules. This adds an opt-in loopback-only mode:

  • temper serve --bind <ip>: the address to bind. Default 0.0.0.0, so nothing changes for anyone who does not pass it.
  • TEMPER_BIND_ADDR: selects the address when the flag is not given, in the same precedence the storage and actor-runtime options use (flag wins over env; a set-but-invalid value aborts startup).
  • Only 0.0.0.0 and 127.0.0.1 are accepted. Temper's own components — the Observe UI proxy, WASM host self-calls, the blob endpoint, the Discord transport — reach the API at http://127.0.0.1:{port}, so any other address (::1, an interface IP, a host name) would bind a server those callers cannot reach. They are refused with a clear error; the list can widen later if those callers ever follow the bind address.
  • --bind 127.0.0.1 leaves no listener on 0.0.0.0: the Observe UI port probe and dev server (next dev -H <bind>) and the auto-started local sandbox (local_sandbox.py --host <bind>, an unauthenticated shell-exec helper that previously always bound all interfaces) follow the same address. Both are passed only when the bind is non-default, so existing setups and older sandbox scripts are untouched.
  • The startup line names the address that was bound.

A preparatory refactor commit moves the serve option resolvers to serve/options.rs and the Observe UI spawn to serve/observe_ui.rs: main.rs (990 lines) and serve/mod.rs (997) sit just under the 1000-line readability ratchet (PROD_FILES_GT1000 baseline is exactly the current count), and the feature would have pushed both files over it.

Verification

Unit tests (cargo test -p temper-cli --bin temper, 101 passed): the resolver (default, env, trimming, blank env, explicit flag, flag-beats-env, rejected values including localhost, ::1, other IPs) and the clap parse of --bind and its default. cargo fmt --check, cargo clippy -p temper-cli --all-targets -- -D warnings, and scripts/readability-ratchet.sh check all clean.

On the built binary (listeners read with lsof; "LAN IP" is the same machine's non-loopback address):

Case API listener Sandbox (port+10) curl 127.0.0.1 curl LAN IP
--bind 127.0.0.1 127.0.0.1:3399 127.0.0.1:3409 serves refused
no flag, no env *:3399 *:3409 serves serves (as before)
TEMPER_BIND_ADDR=127.0.0.1 127.0.0.1:3399 127.0.0.1:3409 serves refused
--bind 0.0.0.0 + env 127.0.0.1 *:3399 *:3409 serves serves (flag wins)

With observe enabled and --bind 127.0.0.1, the spawned dev server runs next dev -H 127.0.0.1 and listens on 127.0.0.1:3400 only; without the flag it still binds all interfaces, as before.

Rejected values exit 1 before binding anything:

$ temper serve --bind localhost
Error: --bind must be 0.0.0.0 (all interfaces) or 127.0.0.1 (loopback only); got "localhost"
$ TEMPER_BIND_ADDR=nope temper serve
Error: TEMPER_BIND_ADDR must be 0.0.0.0 (all interfaces) or 127.0.0.1 (loopback only); got "nope"

Context: we run Temper per person on a workspace behind an SSH tunnel; a security review asked for the listener to be loopback-only rather than relying on an iptables rule.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 3/5

Fix sandbox restart behavior before merging so loopback-only mode does not silently leave the shell helper exposed.

Fix All in Claude CodeFindings

  1. P1 Security Old sandbox stays exposed ▶
Fix with agent prompt
### Issue 1
crates/temper-cli/src/serve/mod.rs:207-214
If a sandbox from a previous run is still listening on `0.0.0.0`, restarting with `--bind 127.0.0.1` does not close that network access. The CLI drops `_child` without stopping it. The replacement sandbox then fails to bind the same port, but its error is discarded and the CLI still prints “auto-started.” Requests keep reaching the old sandbox, whose shell endpoint remains reachable from other machines.

Keep and stop the owned child, and confirm that its replacement binds before reporting success. If an existing listener prevents the requested restriction, fail clearly rather than silently using it.

**How this was verified:** The startup path discards the sandbox child and its errors, while the sandbox accepts network requests that run shell commands without an authentication check.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds --bind and TEMPER_BIND_ADDR while preserving the all-interface default.

  • Temper can keep its API and local helpers on loopback.

Reviews (1) · Last reviewed commit: "feat(cli): serve --bind to choose the li..." · Reviewed by Greptile

shlomobenyaminov and others added 2 commits October 19, 2026 15:12
…into modules

main.rs and serve/mod.rs sit just under the 1000-line readability ratchet;
the --bind option would push both over it. Move the flag/env resolvers to
serve/options.rs and the Observe UI spawn to serve/observe_ui.rs. No
behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed: 0.0.0.0)

`temper serve` always bound 0.0.0.0, so a single-user server reached
through an SSH tunnel or a local proxy had to be fenced off with firewall
rules. --bind <ip> picks the address; TEMPER_BIND_ADDR selects it when
the flag is not given, with the same precedence the storage and
actor-runtime options use.

Only 0.0.0.0 and 127.0.0.1 are accepted: Temper's own components (the
Observe UI proxy, WASM host self-calls, the blob endpoint, the Discord
transport) reach the API at http://127.0.0.1:{port}, so any other address
would bind a server those callers cannot reach; a host name is refused
rather than resolved. The Observe UI port probe and dev server and the
auto-started local sandbox follow the chosen address, so --bind 127.0.0.1
leaves no listener on 0.0.0.0.

Opt-in and backward compatible: without the flag or the variable nothing
changes, and the startup line still names the address that was bound.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread crates/temper-cli/src/serve/mod.rs
@arun-pathiban-ddog
arun-pathiban-ddog merged commit 118bb6f into nerdsane:main Oct 19, 2026
12 checks passed
@shlomobenyaminov

Copy link
Copy Markdown
Contributor Author

Valid finding — fixed in 101840e.

The mechanics (detached child, discarded errors, unconditional "auto-started") predate this PR, but --bind 127.0.0.1's promise made them a real hole: a lingering wildcard sandbox would silently keep serving, network-exposed, while startup claimed loopback-only.

The fix probes the sandbox port with a connect before spawning — a bind probe misses the case on macOS, where SO_REUSEADDR lets a specific bind succeed over an existing wildcard listener (verified: the first version of this fix failed its own reproduction that way). If something already accepts:

  • --bind 127.0.0.1: startup fails with the port, the likely cause, and the remedy (lsof -ti :PORT | xargs kill, or SANDBOX_URL) — the requested restriction cannot be met, so refuse rather than silently use the existing listener.
  • default bind: behavior-compatible, but an explicit warning says requests will reach the existing listener, and nothing claims "auto-started".

After a clean spawn the child is checked for an immediate exit before "auto-started" is printed, the handle is kept, and the child is killed when the server returns. (Ctrl-C already tears the sandbox down via the foreground process group; a SIGKILLed temper still orphans it, which is now detected loudly at the next start instead of being masked.)

Reproduction: SIGKILL a default serve → sandbox lingers on *:3409; restart with --bind 127.0.0.1 → exit 1 naming port 3409, old listener never impersonated; default restart → warning + zero auto-started claims; clean loopback start → sandbox on 127.0.0.1:3409.

rita-aga added a commit to arni-labs/temper that referenced this pull request Oct 21, 2026
feat(cli): serve --bind to choose the listen address (port of nerdsane#537)
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.

2 participants