Local listeners bind 0.0.0.0 with no access control #3

Closed
opened 2026-08-31 18:53:10 -05:00 by Ghost · 1 comment

Severity: Medium

Client (and DNS, and typical server examples) listen on all interfaces with no access control on the local forwarded ports. Anyone who can reach that host can use the tunnel.

What happens

  • Client TCP: net.Listen("tcp", fmt.Sprintf(":%d", rule.LocalPort))0.0.0.0 (internal/client/client.go).
  • Client UDP: same with :%d.
  • Built-in DNS: Addr: fmt.Sprintf(":%d", cfg.DNSServer.ListenPort) (pkg/dns/dns.go).
  • README / generated server config uses listen_address: :9000 / :8080 (all interfaces). Docs talk about connecting to localhost:8080 as if it were loopback-only.

So if you forward SSH as tcp://22:2222 on a homelab box, every machine on the LAN (or the internet, if the port is forwarded) can hit remote SSH through your key and your tunnel. Same for databases. There is no allowlist, no local auth, no bind-host field.

The server side of the tunnel only checks that decryption of the handshake succeeds (shared key). Combined with all-interfaces bind, a leaked or guessed key plus an exposed listen port is a full proxy.

Suggested fix

  • Default client local listeners to 127.0.0.1:<port>, not :port.
  • Add an explicit bind/host in config, e.g. bind_address: 127.0.0.1 (or per-rule tcp://22:127.0.0.1:2222).
  • Default listen_address examples to 127.0.0.1:9000 unless the operator is intentionally publishing the server.
  • If DNS is enabled, bind loopback by default; do not act as an open recursive forwarder on 0.0.0.0.
## Severity: Medium Client (and DNS, and typical server examples) **listen on all interfaces** with **no access control** on the local forwarded ports. Anyone who can reach that host can use the tunnel. ### What happens - Client TCP: `net.Listen("tcp", fmt.Sprintf(":%d", rule.LocalPort))` → `0.0.0.0` (`internal/client/client.go`). - Client UDP: same with `:%d`. - Built-in DNS: `Addr: fmt.Sprintf(":%d", cfg.DNSServer.ListenPort)` (`pkg/dns/dns.go`). - README / generated server config uses `listen_address: :9000` / `:8080` (all interfaces). Docs talk about connecting to `localhost:8080` as if it were loopback-only. So if you forward SSH as `tcp://22:2222` on a homelab box, every machine on the LAN (or the internet, if the port is forwarded) can hit remote SSH through *your* key and *your* tunnel. Same for databases. There is no allowlist, no local auth, no bind-host field. The server side of the tunnel only checks that decryption of the handshake succeeds (shared key). Combined with all-interfaces bind, a leaked or guessed key plus an exposed listen port is a full proxy. ### Suggested fix - Default client local listeners to `127.0.0.1:<port>`, not `:port`. - Add an explicit bind/host in config, e.g. `bind_address: 127.0.0.1` (or per-rule `tcp://22:127.0.0.1:2222`). - Default `listen_address` examples to `127.0.0.1:9000` unless the operator is intentionally publishing the server. - If DNS is enabled, bind loopback by default; do not act as an open recursive forwarder on `0.0.0.0`.

Triaged as medium. Default listeners to 127.0.0.1, add bind host, fix README. Same stream as #2, not PR #1.

Triaged as medium. Default listeners to 127.0.0.1, add bind host, fix README. Same stream as #2, not PR #1.
Ghost closed this issue 2026-08-31 23:13:43 -05:00
Sign in to join this conversation.