REPOSITORY / ScuroNeko/mtg

Compare commits

DIFF REPOSITORY

Compare commits

..
19 Commits
Author SHA1 Message Date
9seconds 95db895136 Merge remote-tracking branch 'origin/master' into stable 2026-04-07 18:10:26 +02:00
9seconds 2544c521ed Merge remote-tracking branch 'origin/master' into stable 2026-04-01 17:16:55 +02:00
9seconds 4b8da719ae Merge remote-tracking branch 'origin/master' into stable 2026-03-30 13:08:51 +02:00
9seconds 9d43c2d759 Merge remote-tracking branch 'origin/master' into stable 2026-03-29 23:33:06 +02:00
9seconds 7b333ed833 Merge remote-tracking branch 'origin/master' into stable 2026-03-24 09:58:48 +01:00
9seconds de89de2ad6 Merge remote-tracking branch 'origin/master' into stable 2026-03-24 09:57:43 +01:00
9seconds 2eb0828f72 Merge remote-tracking branch 'origin/master' into stable 2026-03-20 11:29:24 +01:00
9seconds cb627f2a66 Merge remote-tracking branch 'origin/master' into stable 2026-03-19 18:52:11 +01:00
9seconds 9d5fd989e5 Merge remote-tracking branch 'origin/master' into stable 2026-03-16 23:55:56 +01:00
9seconds 30aa9d3a44 Merge remote-tracking branch 'origin/master' into stable 2026-03-15 22:04:09 +01:00
9seconds 9f12620dba Merge remote-tracking branch 'origin/master' into stable 2026-02-28 14:51:26 +01:00
9seconds 9946cdfeb5 Merge remote-tracking branch 'origin/master' into stable 2026-02-28 14:47:25 +01:00
9seconds f61a63a59b Merge remote-tracking branch 'origin/master' into stable 2026-02-26 13:53:17 +01:00
9seconds e6fa5906c9 Merge remote-tracking branch 'origin/master' into stable 2026-02-24 18:45:46 +01:00
9seconds f4b296d1e0 Merge branch 'master' into stable 2026-02-19 14:37:36 +01:00
9seconds e7fdb02a29 Merge branch 'v2' into stable 2026-02-16 19:23:45 +01:00
9seconds 7a6ba6d8c6 Merge branch 'master' into v2 2026-02-16 19:23:28 +01:00
9seconds 25bca76da5 Merge remote-tracking branch 'origin/stable' into v2 2026-02-11 14:26:31 +01:00
9seconds 76109f6204 Merge remote-tracking branch 'origin/master' into stable 2026-02-11 14:25:54 +01:00
20 changed files with 3 additions and 504 deletions
-2
View File
@@ -8,5 +8,3 @@
mtg mtg
coverage.txt coverage.txt
dist/ dist/
.idea/
.vscode/
-44
View File
@@ -514,50 +514,6 @@ This is not very necessary. Keep in mind these rules:
you can enable `drs` setting. you can enable `drs` setting.
9. **If you are not sure, touch nothing!** 9. **If you are not sure, touch nothing!**
## Troubleshooting
### `ip was blacklisted` for clients on the same LAN
If you run mtg at home and a client on the same LAN (for example, your
phone on the home Wi-Fi) cannot connect, check the proxy logs for a
message like:
```json
{"level":"info","ip":"10.0.1.1","logger":"proxy","message":"ip was blacklisted"}
```
The reason is that the default blocklist (`firehol_level1.netset`)
includes bogon networks, which covers all RFC1918 ranges
(`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`). Any client
connecting from such an address is rejected by the blocklist —
the TCP connection is closed immediately with no response, so
from the client's point of view nothing loads at all.
There are three ways to resolve it:
1. Disable the blocklist entirely in `config.toml`:
```toml
[defense.blocklist]
enabled = false
```
Simplest option if the proxy is used only by you and people you trust.
2. Keep the blocklist but swap `firehol_level1` for a narrower list that
does not include bogons, for example `firehol_abusers_1d`:
```toml
[defense.blocklist]
enabled = true
urls = ["https://iplists.firehol.org/files/firehol_abusers_1d.netset"]
```
3. Connect to the proxy through a public IP or domain name with hairpin
NAT (`MASQUERADE`) on your router. mtg will then see the client with
its public address and the blocklist will not match. This is more
work to set up but preserves full blocklist protection.
## Metrics ## Metrics
Out of the box, mtg works with Out of the box, mtg works with
-30
View File
@@ -1,30 +0,0 @@
{
# Caddy sits behind HAProxy which passes raw TLS through on :8443.
# ACME HTTP-01 challenges arrive on :80 via HAProxy's acl passthrough.
http_port 80
https_port 8443
# HAProxy forwards connections to :8443 with a PROXY protocol v2
# header (see haproxy.cfg `send-proxy-v2`). The proxy_protocol
# listener wrapper strips the header and exposes the real client IP
# to Caddy's access log. The `tls` wrapper must follow so that TLS
# is terminated on the unwrapped connection.
#
# `allow` lists the networks permitted to send PROXY headers. These
# ranges cover docker compose's default bridge networks; tighten
# them if you pin a specific subnet in docker-compose.yml.
servers :8443 {
listener_wrappers {
proxy_protocol {
timeout 5s
allow 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
}
tls
}
}
}
{$DOMAIN} {
root * /srv
file_server
}
-89
View File
@@ -1,89 +0,0 @@
# SNI-routing deployment for mtg
A turnkey `docker compose` setup that puts an SNI-aware TCP router
(HAProxy) in front of mtg **and** a real web server (Caddy with
automatic HTTPS).
## Why
Modern DPI systems actively probe suspected proxies. If the server
closes the connection or returns something unexpected, the IP gets
flagged. With this setup:
- **Telegram clients** connect to port 443, HAProxy sees the configured
SNI and routes them to mtg (FakeTLS).
- **Everything else** (browsers, DPI probes, scanners) is routed to
Caddy, which responds with a real Let's Encrypt certificate and serves
genuine web content.
Because your domain's DNS points to this server, the SNI/IP match is
natural and passive DPI has nothing to flag.
## Quick start
```bash
# 1. Point your domain's DNS A/AAAA record to this server's IP.
# 2. Generate an mtg secret:
docker run --rm nineseconds/mtg:2 generate-secret --hex YOUR_DOMAIN
# 3. Edit the config files:
# - mtg-config.toml → paste the secret
# - haproxy.cfg → replace "example.com" in the SNI ACL
# - .env or export → DOMAIN=your.domain
# 4. (Optional) put your site content into www/
# 5. Start:
docker compose up -d
# 6. Verify:
# - Open https://YOUR_DOMAIN in a browser → you should see the web page
# - Configure Telegram with the proxy link from:
docker compose exec mtg mtg access /config/config.toml
```
## Real client IPs (PROXY protocol)
HAProxy forwards TCP connections to mtg and Caddy with a PROXY protocol
v2 header so both backends see the real client IP instead of HAProxy's
container address. The three pieces must stay in sync:
- `haproxy.cfg``send-proxy-v2` on the `mtg` and `web` backend `server` lines
- `mtg-config.toml``proxy-protocol-listener = true`
- `Caddyfile``listener_wrappers { proxy_protocol { ... } tls }` on `:8443`
If you disable one, disable all three, otherwise the backend will fail
to parse the connection.
## ACME (Let's Encrypt) notes
HAProxy passes `/.well-known/acme-challenge/` requests on `:80` to
Caddy so that HTTP-01 validation works out of the box. Make sure your
domain's DNS A/AAAA record points to this server before starting.
## Architecture
```
┌──────────────────┐
:443 ──────>│ HAProxy │
│ (TCP, SNI peek) │
└──┬───────────┬───┘
SNI match │ │ default
v v
┌─────────┐ ┌─────────┐
│ mtg │ │ Caddy │
│ :3128 │ │ :8443 │
│ FakeTLS │ │ real TLS│
└─────────┘ └─────────┘
```
## Files
| File | Purpose |
|---|---|
| `docker-compose.yml` | Service definitions |
| `haproxy.cfg` | SNI routing rules — **edit the domain** |
| `mtg-config.toml` | mtg proxy config — **paste your secret** |
| `Caddyfile` | Web server config (auto-HTTPS) |
| `www/` | Static site content served by Caddy |
-54
View File
@@ -1,54 +0,0 @@
# SNI-routing deployment: HAProxy (443) -> mtg + real web backend
#
# This setup puts an SNI-aware TCP router in front of mtg so that:
# - Telegram clients (FakeTLS with the correct SNI) are routed to mtg
# - All other TLS traffic (including DPI probes) reaches the real web
# server, which responds with a genuine certificate
#
# The result: active probes see a real website; passive DPI sees matching
# SNI/IP because the domain resolves to this server's IP.
#
# Quick start:
# 1. Set YOUR_DOMAIN below (and in mtg-config.toml)
# 2. docker compose up -d
# 3. mtg generate-secret YOUR_DOMAIN -> put it in mtg-config.toml
# 4. docker compose restart mtg
#
# See BEST_PRACTICES.md and the project wiki for background.
services:
haproxy:
image: haproxy:lts-alpine
ports:
- "443:443"
- "80:80"
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
depends_on:
- mtg
- web
restart: unless-stopped
mtg:
image: nineseconds/mtg:2
volumes:
- ./mtg-config.toml:/config/config.toml:ro
expose:
- "3128"
restart: unless-stopped
web:
image: caddy:alpine
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- ./www:/srv:ro
expose:
- "80"
- "8443"
environment:
DOMAIN: ${DOMAIN:-example.com}
restart: unless-stopped
volumes:
caddy_data:
-62
View File
@@ -1,62 +0,0 @@
# HAProxy SNI router — Layer 4 (TCP mode)
#
# Inspects the SNI in the TLS ClientHello and routes traffic:
# - SNI matching the mtg secret domain -> mtg (FakeTLS / MTProto)
# - Everything else -> real web backend (Caddy)
#
# Because routing happens before TLS termination, each backend sees the
# raw ClientHello and handles TLS itself. The real web backend therefore
# presents a genuine certificate to any probe or browser.
global
log stdout format raw local0 info
maxconn 4096
defaults
log global
mode tcp
option tcplog
timeout connect 5s
timeout client 60s
timeout server 60s
# --- HTTP :80 — ACME challenges + redirect -----------------------------------
frontend http
bind *:80
mode http
# Let Caddy answer ACME HTTP-01 challenges for Let's Encrypt.
acl is_acme path_beg /.well-known/acme-challenge/
use_backend web_acme if is_acme
http-request redirect scheme https code 301
# --- TLS :443 — SNI-based routing -------------------------------------------
frontend tls
bind *:443
tcp-request inspect-delay 5s
tcp-request content accept if { req_ssl_hello_type 1 }
# Route Telegram clients to mtg.
# Replace "example.com" with the domain from your mtg secret.
use_backend mtg if { req_ssl_sni -i example.com }
default_backend web
backend mtg
# send-proxy-v2 prepends a PROXY protocol v2 header so mtg sees the
# real client IP instead of HAProxy's. mtg must have
# `proxy-protocol-listener = true` in its config.
server mtg mtg:3128 send-proxy-v2
backend web
# send-proxy-v2 prepends a PROXY protocol v2 header so Caddy logs the
# real client IP instead of HAProxy's. Caddy must enable the
# proxy_protocol listener wrapper on :8443 (see Caddyfile).
server web web:8443 send-proxy-v2
backend web_acme
mode http
server web web:80
-17
View File
@@ -1,17 +0,0 @@
# Minimal mtg configuration for the SNI-router setup.
#
# 1. Generate a secret: mtg generate-secret --hex example.com
# 2. Paste it below.
# 3. Replace example.com with your actual domain everywhere.
secret = "PASTE_YOUR_SECRET_HERE"
bind-to = "0.0.0.0:3128"
# HAProxy in front sends PROXY protocol v2 headers so mtg can see the
# real client IP. Keep this in sync with haproxy.cfg (`send-proxy-v2`).
proxy-protocol-listener = true
[defense.anti-replay]
enabled = true
max-size = "1mib"
error-rate = 0.001
-5
View File
@@ -1,5 +0,0 @@
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Welcome</title></head>
<body><h1>It works!</h1><p>Replace this with your own content.</p></body>
</html>
+1 -13
View File
@@ -25,18 +25,6 @@ var TelegramCoreAddresses = map[int][]string{
}, },
203: { 203: {
"91.105.192.100:443", "91.105.192.100:443",
"[2a0a:f280:0203:a:5000::100]:443", "[2a0a:f280:0203:000a:5000:0000:0000:0100]:443",
},
10001: {
"149.154.175.10:443",
"[2001:b28:f23d:f001::e]:443",
},
10002: {
"149.154.167.40:443",
"[2001:67c:4e8:f002::e]:443",
},
10003: {
"149.154.175.117:443",
"[2001:b28:f23d:f003::e]:443",
}, },
} }
-19
View File
@@ -316,17 +316,6 @@ download-concurrency = 2
# A list of URLs in FireHOL format (https://iplists.firehol.org/) # A list of URLs in FireHOL format (https://iplists.firehol.org/)
# You can provider links here (starts with https:// or http://) or # You can provider links here (starts with https:// or http://) or
# path to a local file, but in this case it should be absolute. # path to a local file, but in this case it should be absolute.
#
# NOTE: the default list below (firehol_level1.netset) includes bogon
# networks, and therefore RFC1918 ranges as well (10.0.0.0/8,
# 172.16.0.0/12, 192.168.0.0/16). If you run mtg on a home/LAN network
# and connect from a client on the same LAN, that client will be
# rejected with "ip was blacklisted" and the connection dropped (TCP
# close, no response). If you see this, you can either disable this section
# (enabled = false), replace firehol_level1 with a narrower list that
# does not include bogons (e.g. firehol_abusers_1d), or connect via
# a public IP/domain with hairpin NAT on your router. See README for
# details.
urls = [ urls = [
"https://iplists.firehol.org/files/firehol_level1.netset", "https://iplists.firehol.org/files/firehol_level1.netset",
# "/local.file" # "/local.file"
@@ -378,11 +367,3 @@ bind-to = "127.0.0.1:3129"
http-path = "/" http-path = "/"
# prefix for metrics for prometheus # prefix for metrics for prometheus
metric-prefix = "mtg" metric-prefix = "mtg"
[dc]
dc1 = [
"149.154.175.50:443",
"149.154.175.54:443",
"149.154.175.59:443",
"[2001:b28:f23d:f001::a]:443"
]
-13
View File
@@ -50,12 +50,6 @@ var (
tplEDCConnect = template.Must( tplEDCConnect = template.Must(
template.New("").Parse(" ❌ DC {{ .dc }}: {{ .error }}\n"), template.New("").Parse(" ❌ DC {{ .dc }}: {{ .error }}\n"),
) )
tplODCIPConnect = template.Must(
template.New("").Parse(" ✅ {{ .ip }}\n"),
)
tplEDCIPConnect = template.Must(
template.New("").Parse(" ❌ {{ .ip }}: {{ .error }}\n"),
)
tplODNSSNIMatch = template.Must( tplODNSSNIMatch = template.Must(
template.New("").Parse(" ✅ IP address {{ .ip }} matches secret hostname {{ .hostname }}\n"), template.New("").Parse(" ✅ IP address {{ .ip }} matches secret hostname {{ .hostname }}\n"),
@@ -291,15 +285,8 @@ func (d *Doctor) checkNetworkAddresses(ntw mtglib.Network, addresses []string) e
for _, addr := range checkAddresses { for _, addr := range checkAddresses {
conn, err = ntw.DialContext(ctx, "tcp", addr) conn, err = ntw.DialContext(ctx, "tcp", addr)
if err != nil { if err != nil {
tplEDCIPConnect.Execute(os.Stdout, map[string]any{
"error": err,
"ip": addr,
})
continue continue
} }
tplODCIPConnect.Execute(os.Stdout, map[string]any{
"ip": addr,
})
conn.Close() //nolint: errcheck conn.Close() //nolint: errcheck
-11
View File
@@ -91,17 +91,6 @@ type Config struct {
MetricPrefix TypeMetricPrefix `json:"metricPrefix"` MetricPrefix TypeMetricPrefix `json:"metricPrefix"`
} `json:"prometheus"` } `json:"prometheus"`
} `json:"stats"` } `json:"stats"`
DCs struct{
DC1 []TypeHostPort `json:"dc1"`
DC2 []TypeHostPort `json:"dc2"`
DC3 []TypeHostPort `json:"dc3"`
DC4 []TypeHostPort `json:"dc4"`
DC5 []TypeHostPort `json:"dc5"`
DC203 []TypeHostPort `json:"dc203"`
DC10001 []TypeHostPort `json:"dc10001"`
DC10002[]TypeHostPort `json:"dc10002"`
DC10003 []TypeHostPort `json:"dc10003"`
} `json:"dc"`
} }
func (c *Config) GetConcurrency(defaultValue uint) uint { func (c *Config) GetConcurrency(defaultValue uint) uint {
-11
View File
@@ -84,17 +84,6 @@ type tomlConfig struct {
MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"` MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"`
} `toml:"prometheus" json:"prometheus,omitempty"` } `toml:"prometheus" json:"prometheus,omitempty"`
} `toml:"stats" json:"stats,omitempty"` } `toml:"stats" json:"stats,omitempty"`
DCs struct{
DC1 []string `toml:"dc1" json:"dc1,omitempty"`
DC2 []string `toml:"dc2" json:"dc2,omitempty"`
DC3 []string `toml:"dc3" json:"dc3,omitempty"`
DC4 []string `toml:"dc4" json:"dc4,omitempty"`
DC5 []string `toml:"dc5" json:"dc5,omitempty"`
DC203 []string `toml:"dc203" json:"dc203,omitempty"`
DC10001 []string `toml:"dc10001" json:"dc10001,omitempty"`
DC10002 []string `toml:"dc10002" json:"dc10002,omitempty"`
DC10003 []string `toml:"dc10003" json:"dc10003,omitempty"`
} `toml:"dc" json:"dc,omitempty"`
} }
func Parse(rawData []byte) (*Config, error) { func Parse(rawData []byte) (*Config, error) {
-70
View File
@@ -4,77 +4,9 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/9seconds/mtg/v2/essentials"
"github.com/9seconds/mtg/v2/internal/config" "github.com/9seconds/mtg/v2/internal/config"
) )
func readCustomDC(cfg *config.Config) {
if len(cfg.DCs.DC1) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC1 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[1] = ips
}
if len(cfg.DCs.DC2) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC2 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[2] = ips
}
if len(cfg.DCs.DC3) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC3 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[3] = ips
}
if len(cfg.DCs.DC4) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC4 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[4] = ips
}
if len(cfg.DCs.DC5) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC5 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[5] = ips
}
if len(cfg.DCs.DC203) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC203 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[203] = ips
}
if len(cfg.DCs.DC10001) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC10001 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[10001] = ips
}
if len(cfg.DCs.DC10002) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC10002 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[10002] = ips
}
if len(cfg.DCs.DC10003) > 0 {
var ips []string
for _, addr := range cfg.DCs.DC10003 {
ips = append(ips, addr.Value)
}
essentials.TelegramCoreAddresses[10003] = ips
}
}
func ReadConfig(path string) (*config.Config, error) { func ReadConfig(path string) (*config.Config, error) {
content, err := os.ReadFile(path) content, err := os.ReadFile(path)
if err != nil { if err != nil {
@@ -90,7 +22,5 @@ func ReadConfig(path string) (*config.Config, error) {
return nil, fmt.Errorf("invalid config: %w", err) return nil, fmt.Errorf("invalid config: %w", err)
} }
readCustomDC(conf)
return conf, nil return conf, nil
} }
+1 -1
View File
@@ -20,7 +20,7 @@ func SetServerSocketOptions(conn net.Conn, bufferSize int) error {
} }
func setCommonSocketOptions(conn *net.TCPConn) error { func setCommonSocketOptions(conn *net.TCPConn) error {
if err := applyKeepAlive(conn, net.KeepAliveConfig{ if err := conn.SetKeepAliveConfig(net.KeepAliveConfig{
Enable: true, Enable: true,
Idle: DefaultKeepAliveIdle, Idle: DefaultKeepAliveIdle,
Interval: DefaultKeepAliveInterval, Interval: DefaultKeepAliveInterval,
-11
View File
@@ -1,11 +0,0 @@
//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
}
-20
View File
@@ -1,20 +0,0 @@
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
}
+1 -1
View File
@@ -6,7 +6,7 @@ import (
) )
func setCommonSocketOptions(conn *net.TCPConn, keepAliveConfig net.KeepAliveConfig) error { func setCommonSocketOptions(conn *net.TCPConn, keepAliveConfig net.KeepAliveConfig) error {
if err := applyKeepAlive(conn, keepAliveConfig); err != nil { if err := conn.SetKeepAliveConfig(keepAliveConfig); err != nil {
return fmt.Errorf("cannot configure TCP keepalive: %w", err) return fmt.Errorf("cannot configure TCP keepalive: %w", err)
} }
-11
View File
@@ -1,11 +0,0 @@
//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
}
-20
View File
@@ -1,20 +0,0 @@
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
}