Skip to content

Fix TCP keepalive setup on OpenBSD#459

Merged
9seconds merged 1 commit into9seconds:masterfrom
dolonet:fix/openbsd-keepalive
Apr 10, 2026
Merged

Fix TCP keepalive setup on OpenBSD#459
9seconds merged 1 commit into9seconds:masterfrom
dolonet:fix/openbsd-keepalive

Conversation

@dolonet
Copy link
Copy Markdown
Contributor

@dolonet dolonet commented Apr 9, 2026

Fixes #457.

Problem

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. The Go runtime reflects this in src/net/tcpsockopt_openbsd.go: setKeepAliveIdle / setKeepAliveInterval / setKeepAliveCount return ENOPROTOOPT for any non-negative value, and only short-circuit to nil for negative values (the explicit "leave alone" sentinel).

mtg builds a net.KeepAliveConfig with zero-valued Idle / Interval / Count whenever the user does not override them, 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.

Both calls fail on OpenBSD with set tcp ...: protocol not available. The user-visible effects:

  • mtg doctor reports the error for every Telegram DC.
  • mtg run accepts incoming TCP connections at the kernel level but Listener.Accept closes each one before the proxy server ever sees it, so the client appears to hang on a half-open socket and nothing is written to the log. This is exactly what @babut85 reports in openbsd #457.
  • There is no configuration workaround. Setting [network] keep-alive.disabled = true only zeroes Enable; Go's SetKeepAliveConfig still calls all four setters, and three of them still fail.

Fix

Extract the keepalive setup behind an applyKeepAlive(conn, cfg) 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.

  • All platforms except OpenBSD (//go:build !openbsd): applyKeepAlive calls conn.SetKeepAliveConfig(cfg). Behaviour is unchanged.
  • OpenBSD (//go:build openbsd): applyKeepAlive calls conn.SetKeepAlive(cfg.Enable), 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.

The same fix is applied symmetrically in network/ (v1) and network/v2/.

Verified

  • GOOS=openbsd GOARCH=amd64 go build ./... — clean.
  • GOOS=openbsd GOARCH=arm64 go build ./... — clean.
  • go vet ./... clean for both linux and openbsd.
  • go test -count=1 ./network/... passes on linux.

Cannot run the test suite under OpenBSD locally; happy to iterate if anything turns up on a real OpenBSD host.

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.
@dolonet dolonet mentioned this pull request Apr 9, 2026
@9seconds
Copy link
Copy Markdown
Owner

Понял, спасибо большое!

@9seconds 9seconds merged commit d724975 into 9seconds:master Apr 10, 2026
6 checks passed
@dolonet dolonet deleted the fix/openbsd-keepalive branch April 10, 2026 11:07
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.

openbsd

2 participants