From f20f371a612a6cb62fb1045fc63732b503e30071 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Wed, 14 Aug 2024 13:01:06 +0800 Subject: [PATCH] chore: better keepalive handle --- common/net/tcp_keepalive.go | 23 ++++++++ common/net/tcp_keepalive_go122.go | 8 +-- common/net/tcp_keepalive_go123.go | 18 +++--- common/net/tcp_keepalive_go123_unix.go | 15 +++++ common/net/tcp_keepalive_go123_windows.go | 63 ++++++++++++++++++++ common/net/tcpip.go | 6 -- component/dialer/dialer.go | 70 +++++++++++------------ component/dialer/patch_android.go | 38 ------------ component/dialer/patch_common.go | 21 ------- component/dialer/socket_hook.go | 27 +++++++++ config/config.go | 2 + docs/config.yaml | 5 +- listener/http/server.go | 8 +-- 13 files changed, 184 insertions(+), 120 deletions(-) create mode 100644 common/net/tcp_keepalive.go create mode 100644 common/net/tcp_keepalive_go123_unix.go create mode 100644 common/net/tcp_keepalive_go123_windows.go delete mode 100644 component/dialer/patch_android.go delete mode 100644 component/dialer/patch_common.go create mode 100644 component/dialer/socket_hook.go diff --git a/common/net/tcp_keepalive.go b/common/net/tcp_keepalive.go new file mode 100644 index 000000000..047a1c05e --- /dev/null +++ b/common/net/tcp_keepalive.go @@ -0,0 +1,23 @@ +package net + +import ( + "net" + "runtime" + "time" +) + +var ( + KeepAliveIdle = 0 * time.Second + KeepAliveInterval = 0 * time.Second + DisableKeepAlive = false +) + +func TCPKeepAlive(c net.Conn) { + if tcp, ok := c.(*net.TCPConn); ok { + if runtime.GOOS == "android" || DisableKeepAlive { + _ = tcp.SetKeepAlive(false) + } else { + tcpKeepAlive(tcp) + } + } +} diff --git a/common/net/tcp_keepalive_go122.go b/common/net/tcp_keepalive_go122.go index 099701578..128731686 100644 --- a/common/net/tcp_keepalive_go122.go +++ b/common/net/tcp_keepalive_go122.go @@ -4,9 +4,7 @@ package net import "net" -func TCPKeepAlive(c net.Conn) { - if tcp, ok := c.(*net.TCPConn); ok { - _ = tcp.SetKeepAlive(true) - _ = tcp.SetKeepAlivePeriod(KeepAliveInterval) - } +func tcpKeepAlive(tcp *net.TCPConn) { + _ = tcp.SetKeepAlive(true) + _ = tcp.SetKeepAlivePeriod(KeepAliveInterval) } diff --git a/common/net/tcp_keepalive_go123.go b/common/net/tcp_keepalive_go123.go index f5bca964d..2dd4754bb 100644 --- a/common/net/tcp_keepalive_go123.go +++ b/common/net/tcp_keepalive_go123.go @@ -4,12 +4,16 @@ package net import "net" -func TCPKeepAlive(c net.Conn) { - if tcp, ok := c.(*net.TCPConn); ok { - _ = tcp.SetKeepAliveConfig(net.KeepAliveConfig{ - Enable: true, - Idle: KeepAliveIdle, - Interval: KeepAliveInterval, - }) +func tcpKeepAlive(tcp *net.TCPConn) { + config := net.KeepAliveConfig{ + Enable: true, + Idle: KeepAliveIdle, + Interval: KeepAliveInterval, } + if !SupportTCPKeepAliveCount() { + // it's recommended to set both Idle and Interval to non-negative values in conjunction with a -1 + // for Count on those old Windows if you intend to customize the TCP keep-alive settings. + config.Count = -1 + } + _ = tcp.SetKeepAliveConfig(config) } diff --git a/common/net/tcp_keepalive_go123_unix.go b/common/net/tcp_keepalive_go123_unix.go new file mode 100644 index 000000000..0ead7ca47 --- /dev/null +++ b/common/net/tcp_keepalive_go123_unix.go @@ -0,0 +1,15 @@ +//go:build go1.23 && unix + +package net + +func SupportTCPKeepAliveIdle() bool { + return true +} + +func SupportTCPKeepAliveInterval() bool { + return true +} + +func SupportTCPKeepAliveCount() bool { + return true +} diff --git a/common/net/tcp_keepalive_go123_windows.go b/common/net/tcp_keepalive_go123_windows.go new file mode 100644 index 000000000..8f1e61f95 --- /dev/null +++ b/common/net/tcp_keepalive_go123_windows.go @@ -0,0 +1,63 @@ +//go:build go1.23 && windows + +// copy and modify from golang1.23's internal/syscall/windows/version_windows.go + +package net + +import ( + "errors" + "sync" + "syscall" + + "github.com/metacubex/mihomo/constant/features" + + "golang.org/x/sys/windows" +) + +var ( + supportTCPKeepAliveIdle bool + supportTCPKeepAliveInterval bool + supportTCPKeepAliveCount bool +) + +var initTCPKeepAlive = sync.OnceFunc(func() { + s, err := windows.WSASocket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP, nil, 0, windows.WSA_FLAG_NO_HANDLE_INHERIT) + if err != nil { + // Fallback to checking the Windows version. + major, build := features.WindowsMajorVersion, features.WindowsBuildNumber + supportTCPKeepAliveIdle = major >= 10 && build >= 16299 + supportTCPKeepAliveInterval = major >= 10 && build >= 16299 + supportTCPKeepAliveCount = major >= 10 && build >= 15063 + return + } + defer windows.Closesocket(s) + var optSupported = func(opt int) bool { + err := windows.SetsockoptInt(s, syscall.IPPROTO_TCP, opt, 1) + return !errors.Is(err, syscall.WSAENOPROTOOPT) + } + supportTCPKeepAliveIdle = optSupported(windows.TCP_KEEPIDLE) + supportTCPKeepAliveInterval = optSupported(windows.TCP_KEEPINTVL) + supportTCPKeepAliveCount = optSupported(windows.TCP_KEEPCNT) +}) + +// SupportTCPKeepAliveIdle indicates whether TCP_KEEPIDLE is supported. +// The minimal requirement is Windows 10.0.16299. +func SupportTCPKeepAliveIdle() bool { + initTCPKeepAlive() + return supportTCPKeepAliveIdle +} + +// SupportTCPKeepAliveInterval indicates whether TCP_KEEPINTVL is supported. +// The minimal requirement is Windows 10.0.16299. +func SupportTCPKeepAliveInterval() bool { + initTCPKeepAlive() + return supportTCPKeepAliveInterval +} + +// SupportTCPKeepAliveCount indicates whether TCP_KEEPCNT is supported. +// supports TCP_KEEPCNT. +// The minimal requirement is Windows 10.0.15063. +func SupportTCPKeepAliveCount() bool { + initTCPKeepAlive() + return supportTCPKeepAliveCount +} diff --git a/common/net/tcpip.go b/common/net/tcpip.go index 4f4fa6611..a84e7e4c4 100644 --- a/common/net/tcpip.go +++ b/common/net/tcpip.go @@ -4,12 +4,6 @@ import ( "fmt" "net" "strings" - "time" -) - -var ( - KeepAliveIdle = 0 * time.Second - KeepAliveInterval = 0 * time.Second ) func SplitNetworkType(s string) (string, string, error) { diff --git a/component/dialer/dialer.go b/component/dialer/dialer.go index ba95c31b8..2a39508f3 100644 --- a/component/dialer/dialer.go +++ b/component/dialer/dialer.go @@ -13,7 +13,6 @@ import ( "time" "github.com/metacubex/mihomo/component/resolver" - "github.com/metacubex/mihomo/constant/features" "github.com/metacubex/mihomo/log" ) @@ -79,29 +78,29 @@ func DialContext(ctx context.Context, network, address string, options ...Option } func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort, options ...Option) (net.PacketConn, error) { - if features.CMFA && DefaultSocketHook != nil { - return listenPacketHooked(ctx, network, address) - } - cfg := applyOptions(options...) lc := &net.ListenConfig{} - if cfg.interfaceName != "" { - bind := bindIfaceToListenConfig - if cfg.fallbackBind { - bind = fallbackBindIfaceToListenConfig - } - addr, err := bind(cfg.interfaceName, lc, network, address, rAddrPort) - if err != nil { - return nil, err - } - address = addr - } if cfg.addrReuse { addrReuseToListenConfig(lc) } - if cfg.routingMark != 0 { - bindMarkToListenConfig(cfg.routingMark, lc, network, address) + if DefaultSocketHook != nil { // ignore interfaceName, routingMark when DefaultSocketHook not null (in CFMA) + socketHookToListenConfig(lc) + } else { + if cfg.interfaceName != "" { + bind := bindIfaceToListenConfig + if cfg.fallbackBind { + bind = fallbackBindIfaceToListenConfig + } + addr, err := bind(cfg.interfaceName, lc, network, address, rAddrPort) + if err != nil { + return nil, err + } + address = addr + } + if cfg.routingMark != 0 { + bindMarkToListenConfig(cfg.routingMark, lc, network, address) + } } return lc.ListenPacket(ctx, network, address) @@ -149,25 +148,26 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po setMultiPathTCP(dialer) } - if features.CMFA && DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo in CMFA - return dialContextHooked(ctx, dialer, network, address) + if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CFMA) + socketHookToToDialer(dialer) + } else { + if opt.interfaceName != "" { + bind := bindIfaceToDialer + if opt.fallbackBind { + bind = fallbackBindIfaceToDialer + } + if err := bind(opt.interfaceName, dialer, network, destination); err != nil { + return nil, err + } + } + if opt.routingMark != 0 { + bindMarkToDialer(opt.routingMark, dialer, network, destination) + } + if opt.tfo && !DisableTFO { + return dialTFO(ctx, *dialer, network, address) + } } - if opt.interfaceName != "" { - bind := bindIfaceToDialer - if opt.fallbackBind { - bind = fallbackBindIfaceToDialer - } - if err := bind(opt.interfaceName, dialer, network, destination); err != nil { - return nil, err - } - } - if opt.routingMark != 0 { - bindMarkToDialer(opt.routingMark, dialer, network, destination) - } - if opt.tfo && !DisableTFO { - return dialTFO(ctx, *dialer, network, address) - } return dialer.DialContext(ctx, network, address) } diff --git a/component/dialer/patch_android.go b/component/dialer/patch_android.go deleted file mode 100644 index 079b9772a..000000000 --- a/component/dialer/patch_android.go +++ /dev/null @@ -1,38 +0,0 @@ -//go:build android && cmfa - -package dialer - -import ( - "context" - "net" - "syscall" -) - -type SocketControl func(network, address string, conn syscall.RawConn) error - -var DefaultSocketHook SocketControl - -func dialContextHooked(ctx context.Context, dialer *net.Dialer, network string, address string) (net.Conn, error) { - addControlToDialer(dialer, func(ctx context.Context, network, address string, c syscall.RawConn) error { - return DefaultSocketHook(network, address, c) - }) - - conn, err := dialer.DialContext(ctx, network, address) - if err != nil { - return nil, err - } - - if t, ok := conn.(*net.TCPConn); ok { - t.SetKeepAlive(false) - } - - return conn, nil -} - -func listenPacketHooked(ctx context.Context, network, address string) (net.PacketConn, error) { - lc := &net.ListenConfig{ - Control: DefaultSocketHook, - } - - return lc.ListenPacket(ctx, network, address) -} diff --git a/component/dialer/patch_common.go b/component/dialer/patch_common.go deleted file mode 100644 index 2c96fe60b..000000000 --- a/component/dialer/patch_common.go +++ /dev/null @@ -1,21 +0,0 @@ -//go:build !(android && cmfa) - -package dialer - -import ( - "context" - "net" - "syscall" -) - -type SocketControl func(network, address string, conn syscall.RawConn) error - -var DefaultSocketHook SocketControl - -func dialContextHooked(ctx context.Context, dialer *net.Dialer, network string, address string) (net.Conn, error) { - return nil, nil -} - -func listenPacketHooked(ctx context.Context, network, address string) (net.PacketConn, error) { - return nil, nil -} diff --git a/component/dialer/socket_hook.go b/component/dialer/socket_hook.go new file mode 100644 index 000000000..7a2ea4321 --- /dev/null +++ b/component/dialer/socket_hook.go @@ -0,0 +1,27 @@ +package dialer + +import ( + "context" + "net" + "syscall" +) + +// SocketControl +// never change type traits because it's used in CFMA +type SocketControl func(network, address string, conn syscall.RawConn) error + +// DefaultSocketHook +// never change type traits because it's used in CFMA +var DefaultSocketHook SocketControl + +func socketHookToToDialer(dialer *net.Dialer) { + addControlToDialer(dialer, func(ctx context.Context, network, address string, c syscall.RawConn) error { + return DefaultSocketHook(network, address, c) + }) +} + +func socketHookToListenConfig(lc *net.ListenConfig) { + addControlToListenConfig(lc, func(ctx context.Context, network, address string, c syscall.RawConn) error { + return DefaultSocketHook(network, address, c) + }) +} diff --git a/config/config.go b/config/config.go index 3b13146df..235d71ae5 100644 --- a/config/config.go +++ b/config/config.go @@ -340,6 +340,7 @@ type RawConfig struct { GlobalUA string `yaml:"global-ua"` KeepAliveIdle int `yaml:"keep-alive-idle"` KeepAliveInterval int `yaml:"keep-alive-interval"` + DisableKeepAlive bool `yaml:"disable-keep-alive"` Sniffer RawSniffer `yaml:"sniffer" json:"sniffer"` ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` @@ -657,6 +658,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) { if cfg.KeepAliveInterval != 0 { N.KeepAliveInterval = time.Duration(cfg.KeepAliveInterval) * time.Second } + N.DisableKeepAlive = cfg.DisableKeepAlive updater.ExternalUIPath = cfg.ExternalUI // checkout externalUI exist diff --git a/docs/config.yaml b/docs/config.yaml index 6feca27d8..c4644550a 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -82,8 +82,9 @@ external-doh-server: /dns-query global-client-fingerprint: chrome # TCP keep alive interval -# keep-alive-idle: 7200 -# keep-alive-interval: 75 +# disable-keep-alive: false #目前在android端强制为true +# keep-alive-idle: 15 +# keep-alive-interval: 15 # routing-mark:6666 # 配置 fwmark 仅用于 Linux experimental: diff --git a/listener/http/server.go b/listener/http/server.go index 77e10f084..f61dd0360 100644 --- a/listener/http/server.go +++ b/listener/http/server.go @@ -4,9 +4,9 @@ import ( "net" "github.com/metacubex/mihomo/adapter/inbound" + N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/auth" C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/features" authStore "github.com/metacubex/mihomo/listener/auth" ) @@ -74,11 +74,7 @@ func NewWithAuthenticator(addr string, tunnel C.Tunnel, authenticator auth.Authe } continue } - if features.CMFA { - if t, ok := conn.(*net.TCPConn); ok { - t.SetKeepAlive(false) - } - } + N.TCPKeepAlive(conn) if isDefault { // only apply on default listener if !inbound.IsRemoteAddrDisAllowed(conn.RemoteAddr()) { _ = conn.Close()