Skip to content

feat: bind to localhost by default, --host for opt-in#53

Merged
tito merged 4 commits into
mainfrom
feature/prevent-binding-on-a-f6q
May 21, 2026
Merged

feat: bind to localhost by default, --host for opt-in#53
tito merged 4 commits into
mainfrom
feature/prevent-binding-on-a-f6q

Conversation

@tito
Copy link
Copy Markdown
Contributor

@tito tito commented May 21, 2026

Closes #52.

Summary

  • Default every listener to 127.0.0.1. Today greyproxy binds the dashboard, HTTP/SOCKS5/DNS proxies, metrics, and profiling all to 0.0.0.0 because the addresses in the embedded config are bare :PORT strings.
  • New --host <ip> flag on greyproxy serve and a top-level host: YAML field, IP literal only (hostnames rejected with a clear error).
  • Precedence: CLI flag > YAML field > built-in 127.0.0.1. Addresses with an explicit host (0.0.0.0:43080, 192.168.1.10:43080, [::1]:43080) are left alone so operator intent wins.
  • Single WARN line at startup when the resolved host is unspecified (0.0.0.0 / ::), so the choice is visible in logs.
  • SECURITY.md replaced (it was the stock GitHub template) with a Default Security Posture section listing the four ports + the opt-in path.

What changed where

Layer Files
Helpers + tests internal/gostx/config/parsing/parser/host.go, host_test.go
Parser wiring + YAML schema internal/gostx/config/parsing/parser/parser.go, internal/gostx/config/config.go
CLI flag + dashboard / profiling normalize + WARN cmd/greyproxy/main.go, cmd/greyproxy/program.go
Embedded default greyproxy.yml
Docs + policy docs/configuration.md, docs/cli-reference.md, SECURITY.md

Greywall companion compatibility

Read the greywall source to verify nothing breaks:

  • Linux: bwrap --unshare-net plus a socat bridge that targets TCP:127.0.0.1:<port> on the host (internal/sandbox/linux.go:354-357). Exactly what greyproxy will be listening on after this PR. ✅
  • macOS: sandbox-exec rules use localhost:<port>; getaddrinfo resolves to 127.0.0.1 (and ::1). TCP paths are fine because Happy Eyeballs falls back from ::1 to 127.0.0.1 on connection refused. UDP DNS is the one edge case — if macOS getaddrinfo hands back ::1 first and socat doesn't iterate, queries vanish. In practice macOS default gai.conf prefers IPv4 for loopback, so this rarely fires.

Recommended follow-up (separate, in the greywall repo, not this PR): change greywall's defaults from localhost:<port> to 127.0.0.1:<port> to eliminate the IPv6 ambiguity entirely.

Test plan

  • go test ./... — all green (the new internal/gostx/config/parsing/parser tests cover the helper, parser roundtrip, real net.Listen behaviour, and --host flag validation).
  • go vet ./... — clean.
  • Local smoke: built binary with go build ./cmd/greyproxy, ran with a custom config, lsof -nP -iTCP -sTCP:LISTEN confirms listeners on 127.0.0.1:<port> (not *:<port>).
  • greyproxy serve --host 0.0.0.0 — WARN line binding listeners to all interfaces (host=0.0.0.0)... fires, listeners bind everywhere.
  • greyproxy serve --host bogus — exits 2 with --host: host requires a literal IP address, got "bogus".
  • greyproxy serve -O yaml — resolved config shows all four service addrs prefixed with 127.0.0.1:.
  • Reviewer: please run greywall regression on Linux (HTTP / SOCKS5 / DNS paths through the sandbox) before merge.
  • Reviewer: please run greywall regression on macOS — particularly greywall run -- dig example.com — to catch the IPv6 DNS edge case if it ever surfaces in practice.

Breaking change notes

This is a behaviour change for anyone who was reaching the dashboard or proxy ports from another host with the default config. To restore the old behaviour, either:

  • pass --host 0.0.0.0 to greyproxy serve, or
  • add host: 0.0.0.0 at the top of the config file.

Release notes should call this out.

tito added 4 commits May 20, 2026 19:40
…lpers

Prepare for binding listeners to localhost by default. The helpers added
here are the building blocks; the parser, CLI flag, and embedded default
config are wired up in follow-up commits.

- applyDefaultHost rewrites bare ":PORT" / "PORT" addrs by prepending a
  default host; explicit hosts are preserved so operator overrides win.
- walkAddresses applies the helper across services / metrics / profiling.
- ParseHostFlag validates the --host flag, accepting IP literals only.

Tests cover IPv4, IPv6, unix sockets, empty input, and bracket handling.
Plumbs the resolved bind host (CLI flag > YAML host: > 127.0.0.1) through
parser.Parse so service / metrics / profiling addresses written as a bare
port get rewritten before listeners are constructed. Explicit hosts are
left alone so operator overrides keep winning.

The expanded parser_test.go runs a full YAML roundtrip and confirms
net.Listen with the normalized address actually produces a loopback (or
unspecified, with --host 0.0.0.0) socket on the test platform.
…RN on unspecified bind

- main.go registers --host as a top-level serve flag; rejects hostnames
  (IP literal only) so operators don't have to wonder which resolved
  address the listener picked.
- program.go applies the resolved host to the dashboard address (read
  through viper) and to the profiling fallback (":6060"), since those
  paths sit outside the parser's cfg.Services walk.
- warnIfUnspecifiedBind logs a single warning at startup whenever the
  resolved host is 0.0.0.0 / ::, so the choice is visible in logs.
- greyproxy.yml (the embedded default) now sets host: 127.0.0.1 explicitly.
- docs/configuration.md gains a Bind Interface section explaining the
  precedence order (flag > YAML > built-in default).
- docs/cli-reference.md documents --host and notes installed services
  bind to loopback (operators edit the unit/plist for LAN exposure).
- SECURITY.md replaces the stock GitHub template with a Default Security
  Posture section listing the four ports + how to opt into a wider bind.
@tito tito marked this pull request as ready for review May 21, 2026 14:59
@tito tito merged commit f7964ec into main May 21, 2026
3 checks passed
@tito tito deleted the feature/prevent-binding-on-a-f6q branch May 21, 2026 15:00
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.

Default config binds to all interfaces; should default to localhost

1 participant