Skip to content

Support multiple bind addresses + sync upstream (OpenBSD keepalive fix)#22

Merged
dolonet merged 5 commits intomasterfrom
feature/multi-bind
Apr 10, 2026
Merged

Support multiple bind addresses + sync upstream (OpenBSD keepalive fix)#22
dolonet merged 5 commits intomasterfrom
feature/multi-bind

Conversation

@dolonet
Copy link
Copy Markdown
Owner

@dolonet dolonet commented Apr 10, 2026

Summary

  • Upstream sync: merges upstream/master — brings in the OpenBSD keepalive fix from Fix TCP keepalive setup on OpenBSD 9seconds/mtg#459 (build-tag split: SetKeepAlive only on OpenBSD, full SetKeepAliveConfig elsewhere)
  • Multi-bind: bind-to now accepts an array of addresses in addition to a single string
# single address (backwards compatible)
bind-to = "0.0.0.0:443"

# multiple addresses
bind-to = ["127.0.0.1:443", "[::1]:443"]

A MultiListener fans-in Accept calls from all underlying listeners. When only one address is given, no extra goroutines are spawned — zero overhead for existing configs.

Motivated by #21: on OpenBSD, rc.d is simple and doesn't expect multiple instances of the same service, so binding to both 127.0.0.1 and ::1 previously required running separate processes with separate configs.

Changes

  • internal/config/parse.gobind-to accepts both string and []string in TOML, normalizes to []string before JSON decode
  • internal/config/config.goBindTo is now []TypeHostPort, added GetBindAddrs() and GetFirstBindPort() helpers
  • internal/utils/net_listener.go — new MultiListener type
  • internal/cli/run_proxy.go — creates listeners for each bind address
  • internal/cli/access.go, simple_run.go — updated callers
  • example.config.toml — documents the array syntax
  • Tests for multi-bind config parsing and MultiListener

Test plan

  • go build ./... passes
  • go vet ./... passes
  • All existing tests pass (go test ./...)
  • New tests: TestParseMultiBind, TestMultiBindGetAddrs, TestMultiBindGetFirstPort, TestMultiListener (accept, close, addr)
  • Manual: run with bind-to = ["127.0.0.1:3128", "[::1]:3128"], connect to both

Closes #21

dolonet and others added 4 commits April 9, 2026 16:06
Fixes 9seconds#457.

OpenBSD has no user-settable per-socket TCP keepalive options:
TCP_KEEPIDLE, TCP_KEEPINTVL and TCP_KEEPCNT do not exist on OpenBSD,
keepalive timing is controlled system-wide via the sysctls
net.inet.tcp.keepidle and net.inet.tcp.keepintvl. Go reflects this in
src/net/tcpsockopt_openbsd.go: setKeepAliveIdle / Interval / Count
return ENOPROTOOPT for any non-negative value, and only short-circuit
to nil for negative values that explicitly mean "leave alone".

mtg builds a net.KeepAliveConfig with zero-valued Idle / Interval /
Count whenever the user does not override them in the config (which
is the default and the documented expectation). It then hands that
config to (*TCPConn).SetKeepAliveConfig in two places:

  - network/sockopts.go: applied to every connection accepted by
    internal/utils.Listener.Accept and to every server-side dial that
    goes through the v1 default network.
  - network/v2/sockopts.go: applied to every connection produced by
    the v2 network's DialContext.

On OpenBSD both calls fail with "set tcp ...: protocol not available".
The user-visible effect is that:

  - `mtg doctor` reports the error for every Telegram DC.
  - `mtg run` accepts incoming TCP connections at the kernel level but
    Listener.Accept then closes each one before the proxy server ever
    sees it, so the client appears to hang on a half-open socket and
    nothing is logged.
  - There is no configuration workaround. Setting [network]
    keep-alive.disabled = true only zeroes Enable; Go still calls
    setKeepAliveIdle / Interval / Count, which still fail.

This change extracts the keepalive setup behind an applyKeepAlive
helper that has a per-platform implementation, following the same
build-tag pattern already used for sockopts_lowat, sockopts_congestion,
sockopts_reuseaddr and sockopts_usertimeout. On every supported
platform except OpenBSD it still calls SetKeepAliveConfig and the
behaviour is unchanged. On OpenBSD it calls SetKeepAlive(cfg.Enable)
instead, which only flips SO_KEEPALIVE on or off and never touches
the missing per-socket options. OpenBSD users get the system-wide
sysctl-controlled keepalive timing, which is the only thing the
kernel exposes anyway.

Verified by cross-building (`GOOS=openbsd GOARCH=amd64 go build ./...`
and `GOARCH=arm64`) and by running `go test ./network/...` on linux.
bind-to now accepts either a single string or an array of addresses:

  bind-to = "0.0.0.0:443"
  bind-to = ["127.0.0.1:443", "[::1]:443"]

This avoids running multiple instances with separate configs just to
listen on different addresses — useful on OpenBSD where rc.d does not
expect multiple instances of the same service.

A MultiListener fans-in Accept calls from all underlying listeners.
When a single address is given, no extra goroutines are spawned.

Closes #21
@dolonet dolonet mentioned this pull request Apr 10, 2026
- Use buffered channel (cap = number of listeners) so all acceptLoop
  goroutines can exit cleanly on Close instead of blocking forever on
  the unbuffered send.

- Validate duplicate bind-to addresses in Config.Validate().

- Add tests: empty bind array, invalid address, non-string element,
  duplicate addresses, single-listener MultiListener, concurrent
  accept under load (3 listeners x 10 connections).
@dolonet dolonet merged commit f01e3d8 into master Apr 10, 2026
7 checks passed
@dolonet dolonet deleted the feature/multi-bind branch April 10, 2026 13:16
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