Skip to content

fix: check the debug bind is loopback before claiming the port (#64 item 5) - #88

Merged
jason-shen merged 1 commit into
streamcoreai:mainfrom
vsolano9:fix/64-bind-check-before-listen
Nov 7, 2026
Merged

jason-shen merged 1 commit into
streamcoreai:mainfrom
vsolano9:fix/64-bind-check-before-listen

Conversation

@vsolano9

@vsolano9 vsolano9 commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Addresses item 5 of #64: the loopback check now runs before the debug port is claimed.

Cause

startDebugServer called net.Listen and only then checked whether the bound address was loopback. So debug.bind = "0.0.0.0:6060" without allow_public really bound the public port, accepting into the backlog, for the moment before the check closed it. That isn't an exposure, since Serve never started. But a public port shouldn't be claimed at all, and ":6060" got the vague "resolves to a non-loopback address".

Change

checkLoopbackBind runs first when allow_public is false:

  • net.SplitHostPort failure → debug.bind "6060": missing port in address.
  • Empty host (":6060") → "listens on every interface; use 127.0.0.1 or set debug.allow_public = true …".
  • IP literal → it must be loopback.
  • Hostname → every address it resolves to must be loopback (net.Listen may pick any of them).

The post-Listen check stays as a backstop, since a hostname can resolve differently between the check and the bind. allow_public = true skips both, as before. No config, docs or runtime-behaviour change for valid loopback binds.

Item 5 only.

Tests

  • TestDebugServerRejectsPublicBindBeforeListening holds a public port itself, then asks the debug server to bind it. A server that binds first fails with "address already in use". One that checks first returns the allow_public guidance without touching the socket.
  • TestDebugServerNamesAnAllInterfacesBind: ":0" gets the "every interface" message.
  • TestDebugServerRejectsAMalformedBind: "6060" gets a debug.bind parse error.
  • TestDebugServerAcceptsLoopbackBinds: 127.0.0.1:0, [::1]:0 (skipped if the host has no IPv6 loopback) and localhost:0 still start.

Red proof, the same tests against main (f16e44f):

main_test.go:268: error = "listen on \"[::]:46409\": listen tcp 0.0.0.0:46409: bind: address already in use"; want the allow_public guidance, not a listen error
--- FAIL: TestDebugServerRejectsPublicBindBeforeListening
main_test.go:278: error = "bind \":0\" resolves to a non-loopback address; ...", want it to say the bind covers every interface
--- FAIL: TestDebugServerNamesAnAllInterfacesBind
main_test.go:285: error = listen on "6060": listen tcp: address 6060: missing port in address, want a debug.bind parse error
--- FAIL: TestDebugServerRejectsAMalformedBind
--- PASS: TestDebugServerAcceptsLoopbackBinds (all three subtests)

The first failure is the defect itself: main reached bind() on the public port.

Validation

The four CI commands (Linux, Go 1.27.3): gofmt -l . clean, go build ./... clean, go vet ./... clean, go test -race ./... ok in all 15 packages with tests (internal/turn has none).

Prepared with AI assistance (Claude Code). I ran the checks above myself and the outputs are quoted as observed.

The loopback check ran after net.Listen, so a public debug.bind without
allow_public really bound the port (and accepted into the backlog) until
the check closed it. Parse and check the host first: an empty host is
named as "every interface", an IP must be loopback, and a hostname must
resolve to loopback addresses only. The post-Listen check stays as a
backstop for hostnames that resolve differently at bind time.

Addresses item 5 of streamcoreai#64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jason-shen
jason-shen merged commit 456921f into streamcoreai:main Nov 7, 2026
1 check passed
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