From 2fa0e5ed94f0bc58851b998f9d41cd96f6fd2b9a Mon Sep 17 00:00:00 2001 From: dolonet Date: Thu, 9 Apr 2026 16:06:46 +0000 Subject: [PATCH] Fix TCP keepalive setup on OpenBSD Fixes #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. --- network/sockopts.go | 2 +- network/sockopts_keepalive.go | 11 +++++++++++ network/sockopts_keepalive_openbsd.go | 20 ++++++++++++++++++++ network/v2/sockopts.go | 2 +- network/v2/sockopts_keepalive.go | 11 +++++++++++ network/v2/sockopts_keepalive_openbsd.go | 20 ++++++++++++++++++++ 6 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 network/sockopts_keepalive.go create mode 100644 network/sockopts_keepalive_openbsd.go create mode 100644 network/v2/sockopts_keepalive.go create mode 100644 network/v2/sockopts_keepalive_openbsd.go diff --git a/network/sockopts.go b/network/sockopts.go index 7d537e4..1b6c32c 100644 --- a/network/sockopts.go +++ b/network/sockopts.go @@ -20,7 +20,7 @@ func SetServerSocketOptions(conn net.Conn, bufferSize int) error { } func setCommonSocketOptions(conn *net.TCPConn) error { - if err := conn.SetKeepAliveConfig(net.KeepAliveConfig{ + if err := applyKeepAlive(conn, net.KeepAliveConfig{ Enable: true, Idle: DefaultKeepAliveIdle, Interval: DefaultKeepAliveInterval, diff --git a/network/sockopts_keepalive.go b/network/sockopts_keepalive.go new file mode 100644 index 0000000..0f8cb42 --- /dev/null +++ b/network/sockopts_keepalive.go @@ -0,0 +1,11 @@ +//go:build !openbsd + +package network + +import "net" + +// applyKeepAlive enables TCP keepalive on conn and applies the per-socket +// idle/interval/count tuning from cfg. +func applyKeepAlive(conn *net.TCPConn, cfg net.KeepAliveConfig) error { + return conn.SetKeepAliveConfig(cfg) //nolint: wrapcheck +} diff --git a/network/sockopts_keepalive_openbsd.go b/network/sockopts_keepalive_openbsd.go new file mode 100644 index 0000000..6c12f7d --- /dev/null +++ b/network/sockopts_keepalive_openbsd.go @@ -0,0 +1,20 @@ +package network + +import "net" + +// applyKeepAlive enables (or disables) TCP keepalive on conn. +// +// OpenBSD has no user-settable per-socket TCP keepalive options: TCP_KEEPIDLE, +// TCP_KEEPINTVL and TCP_KEEPCNT do not exist on OpenBSD, and Go's +// (*TCPConn).SetKeepAliveConfig therefore returns ENOPROTOOPT ("protocol not +// available") for any non-negative Idle/Interval/Count value (see +// src/net/tcpsockopt_openbsd.go in the Go source tree). Calling +// SetKeepAliveConfig with mtg's defaults (zero values) breaks every accepted +// listener connection and every outbound dial on OpenBSD. +// +// On OpenBSD we only flip SO_KEEPALIVE on or off; the keepalive timing is +// controlled system-wide via the sysctl knobs net.inet.tcp.keepidle and +// net.inet.tcp.keepintvl. +func applyKeepAlive(conn *net.TCPConn, cfg net.KeepAliveConfig) error { + return conn.SetKeepAlive(cfg.Enable) //nolint: wrapcheck +} diff --git a/network/v2/sockopts.go b/network/v2/sockopts.go index 933c76f..edbc47a 100644 --- a/network/v2/sockopts.go +++ b/network/v2/sockopts.go @@ -6,7 +6,7 @@ import ( ) func setCommonSocketOptions(conn *net.TCPConn, keepAliveConfig net.KeepAliveConfig) error { - if err := conn.SetKeepAliveConfig(keepAliveConfig); err != nil { + if err := applyKeepAlive(conn, keepAliveConfig); err != nil { return fmt.Errorf("cannot configure TCP keepalive: %w", err) } diff --git a/network/v2/sockopts_keepalive.go b/network/v2/sockopts_keepalive.go new file mode 100644 index 0000000..0f8cb42 --- /dev/null +++ b/network/v2/sockopts_keepalive.go @@ -0,0 +1,11 @@ +//go:build !openbsd + +package network + +import "net" + +// applyKeepAlive enables TCP keepalive on conn and applies the per-socket +// idle/interval/count tuning from cfg. +func applyKeepAlive(conn *net.TCPConn, cfg net.KeepAliveConfig) error { + return conn.SetKeepAliveConfig(cfg) //nolint: wrapcheck +} diff --git a/network/v2/sockopts_keepalive_openbsd.go b/network/v2/sockopts_keepalive_openbsd.go new file mode 100644 index 0000000..6c12f7d --- /dev/null +++ b/network/v2/sockopts_keepalive_openbsd.go @@ -0,0 +1,20 @@ +package network + +import "net" + +// applyKeepAlive enables (or disables) TCP keepalive on conn. +// +// OpenBSD has no user-settable per-socket TCP keepalive options: TCP_KEEPIDLE, +// TCP_KEEPINTVL and TCP_KEEPCNT do not exist on OpenBSD, and Go's +// (*TCPConn).SetKeepAliveConfig therefore returns ENOPROTOOPT ("protocol not +// available") for any non-negative Idle/Interval/Count value (see +// src/net/tcpsockopt_openbsd.go in the Go source tree). Calling +// SetKeepAliveConfig with mtg's defaults (zero values) breaks every accepted +// listener connection and every outbound dial on OpenBSD. +// +// On OpenBSD we only flip SO_KEEPALIVE on or off; the keepalive timing is +// controlled system-wide via the sysctl knobs net.inet.tcp.keepidle and +// net.inet.tcp.keepintvl. +func applyKeepAlive(conn *net.TCPConn, cfg net.KeepAliveConfig) error { + return conn.SetKeepAlive(cfg.Enable) //nolint: wrapcheck +}