mirror of
https://github.com/ScuroNeko/mtg.git
synced 2026-08-31 16:24:03 +03:00
Improve TCP keepalive and idle timeout for mobile clients
TCP keepalive was configured (SetKeepAlivePeriod) but never actually enabled (SO_KEEPALIVE) on accepted client connections. Go 1.26's SetKeepAlivePeriod only sets TCP_KEEPIDLE — it does not call setsockopt(SO_KEEPALIVE, 1). Without SO_KEEPALIVE the kernel never sends probe packets, so dead connections from sleeping mobile clients linger until the idle timeout fires. Replace SetKeepAlive + SetKeepAlivePeriod with net.KeepAliveConfig (available since Go 1.24) for explicit per-socket control: Idle: 30s (time before first probe) Interval: 10s (between probes) Count: 3 (failed probes to declare dead) This detects dead connections in ~60s instead of relying on system defaults (tcp_keepalive_intvl=75s, probes=9 → up to 11 minutes). Increase the default idle timeout from 1 minute to 5 minutes. MTProto clients send ping_delay_disconnect every ~60s, which resets the idle timer. The previous 1-minute default created a race: if a ping arrived even 1–2 seconds late the relay was killed. A 5-minute window also survives typical mobile sleep periods (phone idle 2–5 min) where the NAT mapping is still alive and the connection can resume without reconnection. Ref: #132
This commit is contained in:
+1
-1
@@ -211,7 +211,7 @@ proxies = [
|
|||||||
[network.timeout]
|
[network.timeout]
|
||||||
tcp = "5s"
|
tcp = "5s"
|
||||||
http = "10s"
|
http = "10s"
|
||||||
idle = "1m"
|
idle = "5m"
|
||||||
|
|
||||||
# mtg has to mimic real websites. It does not mean domain fronting, it also
|
# mtg has to mimic real websites. It does not mean domain fronting, it also
|
||||||
# means that traffic characteristics should be similar to real world traffic.
|
# means that traffic characteristics should be similar to real world traffic.
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/9seconds/mtg/v2/antireplay"
|
"github.com/9seconds/mtg/v2/antireplay"
|
||||||
"github.com/9seconds/mtg/v2/events"
|
"github.com/9seconds/mtg/v2/events"
|
||||||
@@ -263,7 +262,7 @@ func runProxy(conf *config.Config, version string) error { //nolint: funlen
|
|||||||
|
|
||||||
AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
|
AllowFallbackOnUnknownDC: conf.AllowFallbackOnUnknownDC.Get(false),
|
||||||
TolerateTimeSkewness: conf.TolerateTimeSkewness.Value,
|
TolerateTimeSkewness: conf.TolerateTimeSkewness.Value,
|
||||||
IdleTimeout: conf.Network.Timeout.Idle.Get(time.Minute),
|
IdleTimeout: conf.Network.Timeout.Idle.Get(mtglib.DefaultIdleTimeout),
|
||||||
|
|
||||||
DoppelGangerURLs: doppelGangerURLs,
|
DoppelGangerURLs: doppelGangerURLs,
|
||||||
DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),
|
DoppelGangerPerRaid: conf.Defense.Doppelganger.Repeats.Get(mtglib.DoppelGangerPerRaid),
|
||||||
|
|||||||
+3
-2
@@ -77,8 +77,9 @@ const (
|
|||||||
// DefaultIdleTimeout is a default timeout for closing a connection in case of
|
// DefaultIdleTimeout is a default timeout for closing a connection in case of
|
||||||
// idling.
|
// idling.
|
||||||
//
|
//
|
||||||
// Deprecated: no longer in use because of changed TCP relay algorithm.
|
// Set to 5 minutes to survive typical mobile sleep periods (2-5 min) and
|
||||||
DefaultIdleTimeout = time.Minute
|
// avoid racing with MTProto ping_delay_disconnect (~60s interval).
|
||||||
|
DefaultIdleTimeout = 5 * time.Minute
|
||||||
|
|
||||||
// DefaultTolerateTimeSkewness is a default timeout for time skewness on a
|
// DefaultTolerateTimeSkewness is a default timeout for time skewness on a
|
||||||
// faketls timeout verification.
|
// faketls timeout verification.
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ func (p ProxyOpts) getPreferIP() string {
|
|||||||
|
|
||||||
func (p ProxyOpts) getIdleTimeout() time.Duration {
|
func (p ProxyOpts) getIdleTimeout() time.Duration {
|
||||||
if p.IdleTimeout == 0 {
|
if p.IdleTimeout == 0 {
|
||||||
return time.Minute
|
return DefaultIdleTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.IdleTimeout
|
return p.IdleTimeout
|
||||||
|
|||||||
@@ -36,8 +36,22 @@ const (
|
|||||||
|
|
||||||
// DefaultTCPKeepAlivePeriod defines a time period between 2 consequitive
|
// DefaultTCPKeepAlivePeriod defines a time period between 2 consequitive
|
||||||
// probes.
|
// probes.
|
||||||
|
//
|
||||||
|
// Deprecated: use DefaultKeepAliveIdle and DefaultKeepAliveInterval instead.
|
||||||
DefaultTCPKeepAlivePeriod = 10 * time.Second
|
DefaultTCPKeepAlivePeriod = 10 * time.Second
|
||||||
|
|
||||||
|
// DefaultKeepAliveIdle is the time a connection must be idle before
|
||||||
|
// the first keepalive probe is sent.
|
||||||
|
DefaultKeepAliveIdle = 30 * time.Second
|
||||||
|
|
||||||
|
// DefaultKeepAliveInterval is the time between consecutive keepalive
|
||||||
|
// probes.
|
||||||
|
DefaultKeepAliveInterval = 10 * time.Second
|
||||||
|
|
||||||
|
// DefaultKeepAliveCount is the number of unacknowledged probes before
|
||||||
|
// the connection is considered dead.
|
||||||
|
DefaultKeepAliveCount = 3
|
||||||
|
|
||||||
// ProxyDialerOpenThreshold is used for load balancing SOCKS5 dialer only.
|
// ProxyDialerOpenThreshold is used for load balancing SOCKS5 dialer only.
|
||||||
//
|
//
|
||||||
// This dialer uses circuit breaker with of 3 stages: OPEN, HALF_OPEN and
|
// This dialer uses circuit breaker with of 3 stages: OPEN, HALF_OPEN and
|
||||||
|
|||||||
+7
-2
@@ -20,8 +20,13 @@ func SetServerSocketOptions(conn net.Conn, bufferSize int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setCommonSocketOptions(conn *net.TCPConn) error {
|
func setCommonSocketOptions(conn *net.TCPConn) error {
|
||||||
if err := conn.SetKeepAlivePeriod(DefaultTCPKeepAlivePeriod); err != nil {
|
if err := conn.SetKeepAliveConfig(net.KeepAliveConfig{
|
||||||
return fmt.Errorf("cannot set time period of TCP keepalive probes: %w", err)
|
Enable: true,
|
||||||
|
Idle: DefaultKeepAliveIdle,
|
||||||
|
Interval: DefaultKeepAliveInterval,
|
||||||
|
Count: DefaultKeepAliveCount,
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("cannot configure TCP keepalive: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := conn.SetLinger(tcpLingerTimeout); err != nil {
|
if err := conn.SetLinger(tcpLingerTimeout); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
//go:build linux || darwin
|
||||||
|
// +build linux darwin
|
||||||
|
|
||||||
|
package network_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/9seconds/mtg/v2/network"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func tcpKeepIdleOption() int {
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
return 0x10 // TCP_KEEPALIVE on macOS
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0x4 // TCP_KEEPIDLE on Linux
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetClientSocketOptionsKeepAlive(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() {
|
||||||
|
err := listener.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
type dialResult struct {
|
||||||
|
conn net.Conn
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
dialDone := make(chan dialResult, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
c, err := net.Dial("tcp", listener.Addr().String())
|
||||||
|
dialDone <- dialResult{conn: c, err: err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
tcpListener, ok := listener.(*net.TCPListener)
|
||||||
|
require.True(t, ok, "listener must be a *net.TCPListener")
|
||||||
|
|
||||||
|
require.NoError(t, tcpListener.SetDeadline(time.Now().Add(5*time.Second)))
|
||||||
|
|
||||||
|
accepted, err := listener.Accept()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() {
|
||||||
|
err := accepted.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
dr := <-dialDone
|
||||||
|
require.NoError(t, dr.err)
|
||||||
|
defer func() {
|
||||||
|
err := dr.conn.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = network.SetClientSocketOptions(accepted, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tcpConn := accepted.(*net.TCPConn)
|
||||||
|
|
||||||
|
rawConn, err := tcpConn.SyscallConn()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = rawConn.Control(func(fd uintptr) {
|
||||||
|
val, err := unix.GetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, 0, val, "SO_KEEPALIVE should be enabled")
|
||||||
|
|
||||||
|
idle, err := unix.GetsockoptInt(int(fd), syscall.IPPROTO_TCP, tcpKeepIdleOption())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int(network.DefaultKeepAliveIdle.Seconds()), idle)
|
||||||
|
|
||||||
|
interval, err := unix.GetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_KEEPINTVL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int(network.DefaultKeepAliveInterval.Seconds()), interval)
|
||||||
|
|
||||||
|
count, err := unix.GetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_KEEPCNT)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, network.DefaultKeepAliveCount, count)
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
@@ -26,8 +26,22 @@ const (
|
|||||||
|
|
||||||
// DefaultTCPKeepAlivePeriod defines a time period between 2 consecuitive
|
// DefaultTCPKeepAlivePeriod defines a time period between 2 consecuitive
|
||||||
// probes.
|
// probes.
|
||||||
|
//
|
||||||
|
// Deprecated: use DefaultKeepAliveIdle and DefaultKeepAliveInterval instead.
|
||||||
DefaultTCPKeepAlivePeriod = 10 * time.Second
|
DefaultTCPKeepAlivePeriod = 10 * time.Second
|
||||||
|
|
||||||
|
// DefaultKeepAliveIdle is the time a connection must be idle before
|
||||||
|
// the first keepalive probe is sent.
|
||||||
|
DefaultKeepAliveIdle = 30 * time.Second
|
||||||
|
|
||||||
|
// DefaultKeepAliveInterval is the time between consecutive keepalive
|
||||||
|
// probes.
|
||||||
|
DefaultKeepAliveInterval = 10 * time.Second
|
||||||
|
|
||||||
|
// DefaultKeepAliveCount is the number of unacknowledged probes before
|
||||||
|
// the connection is considered dead.
|
||||||
|
DefaultKeepAliveCount = 3
|
||||||
|
|
||||||
// User Agent to use in HTTP client.
|
// User Agent to use in HTTP client.
|
||||||
UserAgent = "curl/8.5.0"
|
UserAgent = "curl/8.5.0"
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func setCommonSocketOptions(conn *net.TCPConn) error {
|
func setCommonSocketOptions(conn *net.TCPConn) error {
|
||||||
if err := conn.SetKeepAlivePeriod(DefaultTCPKeepAlivePeriod); err != nil {
|
if err := conn.SetKeepAliveConfig(net.KeepAliveConfig{
|
||||||
return fmt.Errorf("cannot set time period of TCP keepalive probes: %w", err)
|
Enable: true,
|
||||||
|
Idle: DefaultKeepAliveIdle,
|
||||||
|
Interval: DefaultKeepAliveInterval,
|
||||||
|
Count: DefaultKeepAliveCount,
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("cannot configure TCP keepalive: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := conn.SetLinger(tcpLingerTimeout); err != nil {
|
if err := conn.SetLinger(tcpLingerTimeout); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
//go:build linux || darwin
|
||||||
|
// +build linux darwin
|
||||||
|
|
||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func tcpKeepIdleOption() int {
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
return 0x10 // TCP_KEEPALIVE on macOS
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0x4 // TCP_KEEPIDLE on Linux
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetCommonSocketOptionsKeepAlive(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() {
|
||||||
|
err := listener.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
type dialResult struct {
|
||||||
|
conn net.Conn
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
dialDone := make(chan dialResult, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
c, err := net.Dial("tcp", listener.Addr().String())
|
||||||
|
dialDone <- dialResult{conn: c, err: err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
tcpListener, ok := listener.(*net.TCPListener)
|
||||||
|
require.True(t, ok, "listener must be a *net.TCPListener")
|
||||||
|
|
||||||
|
require.NoError(t, tcpListener.SetDeadline(time.Now().Add(5*time.Second)))
|
||||||
|
|
||||||
|
accepted, err := listener.Accept()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() {
|
||||||
|
err := accepted.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
dr := <-dialDone
|
||||||
|
require.NoError(t, dr.err)
|
||||||
|
defer func() {
|
||||||
|
err := dr.conn.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
tcpConn := accepted.(*net.TCPConn)
|
||||||
|
|
||||||
|
err = setCommonSocketOptions(tcpConn)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rawConn, err := tcpConn.SyscallConn()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = rawConn.Control(func(fd uintptr) {
|
||||||
|
val, err := unix.GetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, 0, val, "SO_KEEPALIVE should be enabled")
|
||||||
|
|
||||||
|
idle, err := unix.GetsockoptInt(int(fd), syscall.IPPROTO_TCP, tcpKeepIdleOption())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int(DefaultKeepAliveIdle.Seconds()), idle, "keepalive idle should match DefaultKeepAliveIdle")
|
||||||
|
|
||||||
|
interval, err := unix.GetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_KEEPINTVL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int(DefaultKeepAliveInterval.Seconds()), interval, "keepalive interval should match DefaultKeepAliveInterval")
|
||||||
|
|
||||||
|
count, err := unix.GetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_KEEPCNT)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, DefaultKeepAliveCount, count, "keepalive count should match DefaultKeepAliveCount")
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user