Browse Source

Add TCP MultiPath support

世界 2 years ago
parent
commit
1019ecfdcf

+ 8 - 2
common/dialer/default.go

@@ -26,7 +26,7 @@ type DefaultDialer struct {
 	udpAddr6    string
 	udpAddr6    string
 }
 }
 
 
-func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDialer {
+func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDialer, error) {
 	var dialer net.Dialer
 	var dialer net.Dialer
 	var listener net.ListenConfig
 	var listener net.ListenConfig
 	if options.BindInterface != "" {
 	if options.BindInterface != "" {
@@ -93,6 +93,12 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
 		udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
 		udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
 		udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
 		udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
 	}
 	}
+	if options.TCPMultiPath {
+		if !multipathTCPAvailable {
+			return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
+		}
+		setMultiPathTCP(&dialer4)
+	}
 	return &DefaultDialer{
 	return &DefaultDialer{
 		tfo.Dialer{Dialer: dialer4, DisableTFO: !options.TCPFastOpen},
 		tfo.Dialer{Dialer: dialer4, DisableTFO: !options.TCPFastOpen},
 		tfo.Dialer{Dialer: dialer6, DisableTFO: !options.TCPFastOpen},
 		tfo.Dialer{Dialer: dialer6, DisableTFO: !options.TCPFastOpen},
@@ -101,7 +107,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
 		listener,
 		listener,
 		udpAddr4,
 		udpAddr4,
 		udpAddr6,
 		udpAddr6,
-	}
+	}, nil
 }
 }
 
 
 func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
 func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {

+ 11 - 0
common/dialer/default_go1.21.go

@@ -0,0 +1,11 @@
+//go:build go1.21
+
+package dialer
+
+import "net"
+
+const multipathTCPAvailable = true
+
+func setMultiPathTCP(dialer *net.Dialer) {
+	dialer.SetMultipathTCP(true)
+}

+ 12 - 0
common/dialer/default_nongo1.21.go

@@ -0,0 +1,12 @@
+//go:build !go1.21
+
+package dialer
+
+import (
+	"net"
+)
+
+const multipathTCPAvailable = false
+
+func setMultiPathTCP(dialer *net.Dialer) {
+}

+ 15 - 4
common/dialer/dialer.go

@@ -6,13 +6,24 @@ import (
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing-dns"
+	"github.com/sagernet/sing/common"
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
 )
 )
 
 
-func New(router adapter.Router, options option.DialerOptions) N.Dialer {
-	var dialer N.Dialer
+func MustNew(router adapter.Router, options option.DialerOptions) N.Dialer {
+	return common.Must1(New(router, options))
+}
+
+func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) {
+	var (
+		dialer N.Dialer
+		err    error
+	)
 	if options.Detour == "" {
 	if options.Detour == "" {
-		dialer = NewDefault(router, options)
+		dialer, err = NewDefault(router, options)
+		if err != nil {
+			return nil, err
+		}
 	} else {
 	} else {
 		dialer = NewDetour(router, options.Detour)
 		dialer = NewDetour(router, options.Detour)
 	}
 	}
@@ -20,5 +31,5 @@ func New(router adapter.Router, options option.DialerOptions) N.Dialer {
 	if domainStrategy != dns.DomainStrategyAsIS || options.Detour == "" {
 	if domainStrategy != dns.DomainStrategyAsIS || options.Detour == "" {
 		dialer = NewResolveDialer(router, dialer, domainStrategy, time.Duration(options.FallbackDelay))
 		dialer = NewResolveDialer(router, dialer, domainStrategy, time.Duration(options.FallbackDelay))
 	}
 	}
-	return dialer
+	return dialer, nil
 }
 }

+ 4 - 1
common/tls/reality_server.go

@@ -101,7 +101,10 @@ func NewRealityServer(ctx context.Context, router adapter.Router, logger log.Log
 		tlsConfig.ShortIds[shortID] = true
 		tlsConfig.ShortIds[shortID] = true
 	}
 	}
 
 
-	handshakeDialer := dialer.New(router, options.Reality.Handshake.DialerOptions)
+	handshakeDialer, err := dialer.New(router, options.Reality.Handshake.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
 	tlsConfig.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
 	tlsConfig.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
 		return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
 		return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
 	}
 	}

+ 12 - 3
docs/configuration/shared/dial.md

@@ -10,6 +10,7 @@
   "reuse_addr": false,
   "reuse_addr": false,
   "connect_timeout": "5s",
   "connect_timeout": "5s",
   "tcp_fast_open": false,
   "tcp_fast_open": false,
+  "tcp_multi_path": false,
   "udp_fragment": false,
   "udp_fragment": false,
   "domain_strategy": "prefer_ipv6",
   "domain_strategy": "prefer_ipv6",
   "fallback_delay": "300ms"
   "fallback_delay": "300ms"
@@ -18,9 +19,9 @@
 
 
 ### Fields
 ### Fields
 
 
-| Field                                                                                                                | Available Context |
-|----------------------------------------------------------------------------------------------------------------------|-------------------|
-| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open`/ `udp_fragment` /`connect_timeout` | `detour` not set  |
+| Field                                                                                                                                    | Available Context |
+|------------------------------------------------------------------------------------------------------------------------------------------|-------------------|
+| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open` / `tcp_multi_path` / `udp_fragment` /`connect_timeout` | `detour` not set  |
 
 
 #### detour
 #### detour
 
 
@@ -54,6 +55,14 @@ Reuse listener address.
 
 
 Enable TCP Fast Open.
 Enable TCP Fast Open.
 
 
+#### tcp_multi_path
+
+!!! warning ""
+
+    Go 1.21 required.
+
+Enable TCP Multi Path.
+
 #### udp_fragment
 #### udp_fragment
 
 
 Enable UDP fragmentation.
 Enable UDP fragmentation.

+ 12 - 3
docs/configuration/shared/dial.zh.md

@@ -10,6 +10,7 @@
   "reuse_addr": false,
   "reuse_addr": false,
   "connect_timeout": "5s",
   "connect_timeout": "5s",
   "tcp_fast_open": false,
   "tcp_fast_open": false,
+  "tcp_multi_path": false,
   "udp_fragment": false,
   "udp_fragment": false,
   "domain_strategy": "prefer_ipv6",
   "domain_strategy": "prefer_ipv6",
   "fallback_delay": "300ms"
   "fallback_delay": "300ms"
@@ -18,9 +19,9 @@
 
 
 ### 字段
 ### 字段
 
 
-| 字段                                                                                                                   | 可用上下文        |
-|----------------------------------------------------------------------------------------------------------------------|--------------|
-| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open`/ `udp_fragment` /`connect_timeout` | `detour` 未设置 |
+| 字段                                                                                                                                       | 可用上下文        |
+|------------------------------------------------------------------------------------------------------------------------------------------|--------------|
+| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open` / `tcp_mutli_path` / `udp_fragment` /`connect_timeout` | `detour` 未设置 |
 
 
 
 
 #### detour
 #### detour
@@ -57,6 +58,14 @@
 
 
 启用 TCP Fast Open。
 启用 TCP Fast Open。
 
 
+#### tcp_multi_path
+
+!!! warning ""
+
+    需要 Go 1.21。
+
+启用 TCP Multi Path。
+
 #### udp_fragment
 #### udp_fragment
 
 
 启用 UDP 分段。
 启用 UDP 分段。

+ 10 - 0
docs/configuration/shared/listen.md

@@ -5,6 +5,7 @@
   "listen": "::",
   "listen": "::",
   "listen_port": 5353,
   "listen_port": 5353,
   "tcp_fast_open": false,
   "tcp_fast_open": false,
+  "tcp_multi_path": false,
   "udp_fragment": false,
   "udp_fragment": false,
   "sniff": false,
   "sniff": false,
   "sniff_override_destination": false,
   "sniff_override_destination": false,
@@ -24,6 +25,7 @@
 | `listen`                          | Needs to listen on TCP or UDP.                                    |
 | `listen`                          | Needs to listen on TCP or UDP.                                    |
 | `listen_port`                     | Needs to listen on TCP or UDP.                                    |
 | `listen_port`                     | Needs to listen on TCP or UDP.                                    |
 | `tcp_fast_open`                   | Needs to listen on TCP.                                           |
 | `tcp_fast_open`                   | Needs to listen on TCP.                                           |
+| `tcp_multi_path`                  | Needs to listen on TCP.                                           |
 | `udp_timeout`                     | Needs to assemble UDP connections, currently Tun and Shadowsocks. |
 | `udp_timeout`                     | Needs to assemble UDP connections, currently Tun and Shadowsocks. |
 | `proxy_protocol`                  | Needs to listen on TCP.                                           |
 | `proxy_protocol`                  | Needs to listen on TCP.                                           |
 | `proxy_protocol_accept_no_header` | When `proxy_protocol` enabled                                     |
 | `proxy_protocol_accept_no_header` | When `proxy_protocol` enabled                                     |
@@ -42,6 +44,14 @@ Listen port.
 
 
 Enable TCP Fast Open.
 Enable TCP Fast Open.
 
 
+#### tcp_multi_path
+
+!!! warning ""
+
+    Go 1.21 required.
+
+Enable TCP Multi Path.
+
 #### udp_fragment
 #### udp_fragment
 
 
 Enable UDP fragmentation.
 Enable UDP fragmentation.

+ 10 - 0
docs/configuration/shared/listen.zh.md

@@ -5,6 +5,7 @@
   "listen": "::",
   "listen": "::",
   "listen_port": 5353,
   "listen_port": 5353,
   "tcp_fast_open": false,
   "tcp_fast_open": false,
+  "tcp_multi_path": false,
   "udp_fragment": false,
   "udp_fragment": false,
   "sniff": false,
   "sniff": false,
   "sniff_override_destination": false,
   "sniff_override_destination": false,
@@ -23,6 +24,7 @@
 | `listen`                          | 需要监听 TCP 或 UDP。                     |
 | `listen`                          | 需要监听 TCP 或 UDP。                     |
 | `listen_port`                     | 需要监听 TCP 或 UDP。                     |
 | `listen_port`                     | 需要监听 TCP 或 UDP。                     |
 | `tcp_fast_open`                   | 需要监听 TCP。                           |
 | `tcp_fast_open`                   | 需要监听 TCP。                           |
+| `tcp_multi_path`                  | 需要监听 TCP。                           |
 | `udp_timeout`                     | 需要组装 UDP 连接, 当前为 Tun 和 Shadowsocks。 |
 | `udp_timeout`                     | 需要组装 UDP 连接, 当前为 Tun 和 Shadowsocks。 |
 | `proxy_protocol`                  | 需要监听 TCP。                           |
 | `proxy_protocol`                  | 需要监听 TCP。                           |
 | `proxy_protocol_accept_no_header` | `proxy_protocol` 启用时                |
 | `proxy_protocol_accept_no_header` | `proxy_protocol` 启用时                |
@@ -43,6 +45,14 @@
 
 
 启用 TCP Fast Open。
 启用 TCP Fast Open。
 
 
+#### tcp_multi_path
+
+!!! warning ""
+
+    需要 Go 1.21。
+
+启用 TCP Multi Path。
+
 #### udp_fragment
 #### udp_fragment
 
 
 启用 UDP 分段。
 启用 UDP 分段。

+ 8 - 1
inbound/default_tcp.go

@@ -18,7 +18,14 @@ func (a *myInboundAdapter) ListenTCP() (net.Listener, error) {
 	bindAddr := M.SocksaddrFrom(a.listenOptions.Listen.Build(), a.listenOptions.ListenPort)
 	bindAddr := M.SocksaddrFrom(a.listenOptions.Listen.Build(), a.listenOptions.ListenPort)
 	var tcpListener net.Listener
 	var tcpListener net.Listener
 	if !a.listenOptions.TCPFastOpen {
 	if !a.listenOptions.TCPFastOpen {
-		tcpListener, err = net.ListenTCP(M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.TCPAddr())
+		var listenConfig net.ListenConfig
+		if a.listenOptions.TCPMultiPath {
+			if !multipathTCPAvailable {
+				return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
+			}
+			setMultiPathTCP(&listenConfig)
+		}
+		tcpListener, err = listenConfig.Listen(a.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
 	} else {
 	} else {
 		tcpListener, err = tfo.ListenTCP(M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.TCPAddr())
 		tcpListener, err = tfo.ListenTCP(M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.TCPAddr())
 	}
 	}

+ 11 - 0
inbound/default_tcp_go1.21.go

@@ -0,0 +1,11 @@
+//go:build go1.21
+
+package inbound
+
+import "net"
+
+const multipathTCPAvailable = true
+
+func setMultiPathTCP(listenConfig *net.ListenConfig) {
+	listenConfig.SetMultipathTCP(true)
+}

+ 10 - 0
inbound/default_tcp_nongo1.21.go

@@ -0,0 +1,10 @@
+//go:build !go1.21
+
+package inbound
+
+import "net"
+
+const multipathTCPAvailable = false
+
+func setMultiPathTCP(listenConfig *net.ListenConfig) {
+}

+ 10 - 2
inbound/shadowtls.go

@@ -40,12 +40,20 @@ func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.Context
 	if options.Version > 1 {
 	if options.Version > 1 {
 		handshakeForServerName = make(map[string]shadowtls.HandshakeConfig)
 		handshakeForServerName = make(map[string]shadowtls.HandshakeConfig)
 		for serverName, serverOptions := range options.HandshakeForServerName {
 		for serverName, serverOptions := range options.HandshakeForServerName {
+			handshakeDialer, err := dialer.New(router, serverOptions.DialerOptions)
+			if err != nil {
+				return nil, err
+			}
 			handshakeForServerName[serverName] = shadowtls.HandshakeConfig{
 			handshakeForServerName[serverName] = shadowtls.HandshakeConfig{
 				Server: serverOptions.ServerOptions.Build(),
 				Server: serverOptions.ServerOptions.Build(),
-				Dialer: dialer.New(router, serverOptions.DialerOptions),
+				Dialer: handshakeDialer,
 			}
 			}
 		}
 		}
 	}
 	}
+	handshakeDialer, err := dialer.New(router, options.Handshake.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
 	service, err := shadowtls.NewService(shadowtls.ServiceConfig{
 	service, err := shadowtls.NewService(shadowtls.ServiceConfig{
 		Version:  options.Version,
 		Version:  options.Version,
 		Password: options.Password,
 		Password: options.Password,
@@ -54,7 +62,7 @@ func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.Context
 		}),
 		}),
 		Handshake: shadowtls.HandshakeConfig{
 		Handshake: shadowtls.HandshakeConfig{
 			Server: options.Handshake.ServerOptions.Build(),
 			Server: options.Handshake.ServerOptions.Build(),
-			Dialer: dialer.New(router, options.Handshake.DialerOptions),
+			Dialer: handshakeDialer,
 		},
 		},
 		HandshakeForServerName: handshakeForServerName,
 		HandshakeForServerName: handshakeForServerName,
 		StrictMode:             options.StrictMode,
 		StrictMode:             options.StrictMode,

+ 7 - 3
ntp/service.go

@@ -31,7 +31,7 @@ type Service struct {
 	clockOffset   time.Duration
 	clockOffset   time.Duration
 }
 }
 
 
-func NewService(ctx context.Context, router adapter.Router, logger logger.Logger, options option.NTPOptions) *Service {
+func NewService(ctx context.Context, router adapter.Router, logger logger.Logger, options option.NTPOptions) (*Service, error) {
 	ctx, cancel := common.ContextWithCancelCause(ctx)
 	ctx, cancel := common.ContextWithCancelCause(ctx)
 	server := options.ServerOptions.Build()
 	server := options.ServerOptions.Build()
 	if server.Port == 0 {
 	if server.Port == 0 {
@@ -43,15 +43,19 @@ func NewService(ctx context.Context, router adapter.Router, logger logger.Logger
 	} else {
 	} else {
 		interval = 30 * time.Minute
 		interval = 30 * time.Minute
 	}
 	}
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
 	return &Service{
 	return &Service{
 		ctx:           ctx,
 		ctx:           ctx,
 		cancel:        cancel,
 		cancel:        cancel,
 		server:        server,
 		server:        server,
 		writeToSystem: options.WriteToSystem,
 		writeToSystem: options.WriteToSystem,
-		dialer:        dialer.New(router, options.DialerOptions),
+		dialer:        outboundDialer,
 		logger:        logger,
 		logger:        logger,
 		ticker:        time.NewTicker(interval),
 		ticker:        time.NewTicker(interval),
-	}
+	}, nil
 }
 }
 
 
 func (s *Service) Start() error {
 func (s *Service) Start() error {

+ 1 - 0
option/inbound.go

@@ -125,6 +125,7 @@ type ListenOptions struct {
 	Listen                      *ListenAddress `json:"listen,omitempty"`
 	Listen                      *ListenAddress `json:"listen,omitempty"`
 	ListenPort                  uint16         `json:"listen_port,omitempty"`
 	ListenPort                  uint16         `json:"listen_port,omitempty"`
 	TCPFastOpen                 bool           `json:"tcp_fast_open,omitempty"`
 	TCPFastOpen                 bool           `json:"tcp_fast_open,omitempty"`
+	TCPMultiPath                bool           `json:"tcp_multi_path,omitempty"`
 	UDPFragment                 *bool          `json:"udp_fragment,omitempty"`
 	UDPFragment                 *bool          `json:"udp_fragment,omitempty"`
 	UDPFragmentDefault          bool           `json:"-"`
 	UDPFragmentDefault          bool           `json:"-"`
 	UDPTimeout                  int64          `json:"udp_timeout,omitempty"`
 	UDPTimeout                  int64          `json:"udp_timeout,omitempty"`

+ 1 - 0
option/outbound.go

@@ -134,6 +134,7 @@ type DialerOptions struct {
 	ReuseAddr          bool           `json:"reuse_addr,omitempty"`
 	ReuseAddr          bool           `json:"reuse_addr,omitempty"`
 	ConnectTimeout     Duration       `json:"connect_timeout,omitempty"`
 	ConnectTimeout     Duration       `json:"connect_timeout,omitempty"`
 	TCPFastOpen        bool           `json:"tcp_fast_open,omitempty"`
 	TCPFastOpen        bool           `json:"tcp_fast_open,omitempty"`
+	TCPMultiPath       bool           `json:"tcp_multi_path,omitempty"`
 	UDPFragment        *bool          `json:"udp_fragment,omitempty"`
 	UDPFragment        *bool          `json:"udp_fragment,omitempty"`
 	UDPFragmentDefault bool           `json:"-"`
 	UDPFragmentDefault bool           `json:"-"`
 	DomainStrategy     DomainStrategy `json:"domain_strategy,omitempty"`
 	DomainStrategy     DomainStrategy `json:"domain_strategy,omitempty"`

+ 5 - 1
outbound/direct.go

@@ -38,6 +38,10 @@ type Direct struct {
 
 
 func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, options option.DirectOutboundOptions) (*Direct, error) {
 func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, options option.DirectOutboundOptions) (*Direct, error) {
 	options.UDPFragmentDefault = true
 	options.UDPFragmentDefault = true
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
 	outbound := &Direct{
 	outbound := &Direct{
 		myOutboundAdapter: myOutboundAdapter{
 		myOutboundAdapter: myOutboundAdapter{
 			protocol:     C.TypeDirect,
 			protocol:     C.TypeDirect,
@@ -49,7 +53,7 @@ func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, opti
 		},
 		},
 		domainStrategy: dns.DomainStrategy(options.DomainStrategy),
 		domainStrategy: dns.DomainStrategy(options.DomainStrategy),
 		fallbackDelay:  time.Duration(options.FallbackDelay),
 		fallbackDelay:  time.Duration(options.FallbackDelay),
-		dialer:         dialer.New(router, options.DialerOptions),
+		dialer:         outboundDialer,
 		proxyProto:     options.ProxyProtocol,
 		proxyProto:     options.ProxyProtocol,
 	}
 	}
 	if options.ProxyProtocol > 2 {
 	if options.ProxyProtocol > 2 {

+ 5 - 1
outbound/http.go

@@ -26,7 +26,11 @@ type HTTP struct {
 }
 }
 
 
 func NewHTTP(router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPOutboundOptions) (*HTTP, error) {
 func NewHTTP(router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPOutboundOptions) (*HTTP, error) {
-	detour, err := tls.NewDialerFromOptions(router, dialer.New(router, options.DialerOptions), options.Server, common.PtrValueOrDefault(options.TLS))
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
+	detour, err := tls.NewDialerFromOptions(router, outboundDialer, options.Server, common.PtrValueOrDefault(options.TLS))
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}

+ 5 - 1
outbound/hysteria.go

@@ -117,6 +117,10 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
 	if down < hysteria.MinSpeedBPS {
 	if down < hysteria.MinSpeedBPS {
 		return nil, E.New("invalid down speed")
 		return nil, E.New("invalid down speed")
 	}
 	}
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
 	return &Hysteria{
 	return &Hysteria{
 		myOutboundAdapter: myOutboundAdapter{
 		myOutboundAdapter: myOutboundAdapter{
 			protocol:     C.TypeHysteria,
 			protocol:     C.TypeHysteria,
@@ -127,7 +131,7 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
 			dependencies: withDialerDependency(options.DialerOptions),
 			dependencies: withDialerDependency(options.DialerOptions),
 		},
 		},
 		ctx:        ctx,
 		ctx:        ctx,
-		dialer:     dialer.New(router, options.DialerOptions),
+		dialer:     outboundDialer,
 		serverAddr: options.ServerOptions.Build(),
 		serverAddr: options.ServerOptions.Build(),
 		tlsConfig:  tlsConfig,
 		tlsConfig:  tlsConfig,
 		quicConfig: quicConfig,
 		quicConfig: quicConfig,

+ 5 - 1
outbound/shadowsocks.go

@@ -39,6 +39,10 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
 	outbound := &Shadowsocks{
 	outbound := &Shadowsocks{
 		myOutboundAdapter: myOutboundAdapter{
 		myOutboundAdapter: myOutboundAdapter{
 			protocol:     C.TypeShadowsocks,
 			protocol:     C.TypeShadowsocks,
@@ -48,7 +52,7 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
 			tag:          tag,
 			tag:          tag,
 			dependencies: withDialerDependency(options.DialerOptions),
 			dependencies: withDialerDependency(options.DialerOptions),
 		},
 		},
-		dialer:     dialer.New(router, options.DialerOptions),
+		dialer:     outboundDialer,
 		method:     method,
 		method:     method,
 		serverAddr: options.ServerOptions.Build(),
 		serverAddr: options.ServerOptions.Build(),
 	}
 	}

+ 5 - 2
outbound/shadowsocksr.go

@@ -37,6 +37,10 @@ type ShadowsocksR struct {
 }
 }
 
 
 func NewShadowsocksR(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksROutboundOptions) (*ShadowsocksR, error) {
 func NewShadowsocksR(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksROutboundOptions) (*ShadowsocksR, error) {
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
 	outbound := &ShadowsocksR{
 	outbound := &ShadowsocksR{
 		myOutboundAdapter: myOutboundAdapter{
 		myOutboundAdapter: myOutboundAdapter{
 			protocol:     C.TypeShadowsocksR,
 			protocol:     C.TypeShadowsocksR,
@@ -46,11 +50,10 @@ func NewShadowsocksR(ctx context.Context, router adapter.Router, logger log.Cont
 			tag:          tag,
 			tag:          tag,
 			dependencies: withDialerDependency(options.DialerOptions),
 			dependencies: withDialerDependency(options.DialerOptions),
 		},
 		},
-		dialer:     dialer.New(router, options.DialerOptions),
+		dialer:     outboundDialer,
 		serverAddr: options.ServerOptions.Build(),
 		serverAddr: options.ServerOptions.Build(),
 	}
 	}
 	var cipher string
 	var cipher string
-	var err error
 	switch options.Method {
 	switch options.Method {
 	case "none":
 	case "none":
 		cipher = "dummy"
 		cipher = "dummy"

+ 5 - 1
outbound/shadowtls.go

@@ -72,11 +72,15 @@ func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.Context
 			tlsHandshakeFunc = shadowtls.DefaultTLSHandshakeFunc(options.Password, stdTLSConfig)
 			tlsHandshakeFunc = shadowtls.DefaultTLSHandshakeFunc(options.Password, stdTLSConfig)
 		}
 		}
 	}
 	}
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
 	client, err := shadowtls.NewClient(shadowtls.ClientConfig{
 	client, err := shadowtls.NewClient(shadowtls.ClientConfig{
 		Version:      options.Version,
 		Version:      options.Version,
 		Password:     options.Password,
 		Password:     options.Password,
 		Server:       options.ServerOptions.Build(),
 		Server:       options.ServerOptions.Build(),
-		Dialer:       dialer.New(router, options.DialerOptions),
+		Dialer:       outboundDialer,
 		TLSHandshake: tlsHandshakeFunc,
 		TLSHandshake: tlsHandshakeFunc,
 		Logger:       logger,
 		Logger:       logger,
 	})
 	})

+ 5 - 1
outbound/socks.go

@@ -37,6 +37,10 @@ func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, optio
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
 	outbound := &Socks{
 	outbound := &Socks{
 		myOutboundAdapter: myOutboundAdapter{
 		myOutboundAdapter: myOutboundAdapter{
 			protocol:     C.TypeSOCKS,
 			protocol:     C.TypeSOCKS,
@@ -46,7 +50,7 @@ func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, optio
 			tag:          tag,
 			tag:          tag,
 			dependencies: withDialerDependency(options.DialerOptions),
 			dependencies: withDialerDependency(options.DialerOptions),
 		},
 		},
-		client:  socks.NewClient(dialer.New(router, options.DialerOptions), options.ServerOptions.Build(), version, options.Username, options.Password),
+		client:  socks.NewClient(outboundDialer, options.ServerOptions.Build(), version, options.Username, options.Password),
 		resolve: version == socks.Version4,
 		resolve: version == socks.Version4,
 	}
 	}
 	uotOptions := common.PtrValueOrDefault(options.UDPOverTCPOptions)
 	uotOptions := common.PtrValueOrDefault(options.UDPOverTCPOptions)

+ 5 - 1
outbound/ssh.go

@@ -44,6 +44,10 @@ type SSH struct {
 }
 }
 
 
 func NewSSH(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SSHOutboundOptions) (*SSH, error) {
 func NewSSH(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SSHOutboundOptions) (*SSH, error) {
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
 	outbound := &SSH{
 	outbound := &SSH{
 		myOutboundAdapter: myOutboundAdapter{
 		myOutboundAdapter: myOutboundAdapter{
 			protocol:     C.TypeSSH,
 			protocol:     C.TypeSSH,
@@ -54,7 +58,7 @@ func NewSSH(ctx context.Context, router adapter.Router, logger log.ContextLogger
 			dependencies: withDialerDependency(options.DialerOptions),
 			dependencies: withDialerDependency(options.DialerOptions),
 		},
 		},
 		ctx:               ctx,
 		ctx:               ctx,
-		dialer:            dialer.New(router, options.DialerOptions),
+		dialer:            outboundDialer,
 		serverAddr:        options.ServerOptions.Build(),
 		serverAddr:        options.ServerOptions.Build(),
 		user:              options.User,
 		user:              options.User,
 		hostKeyAlgorithms: options.HostKeyAlgorithms,
 		hostKeyAlgorithms: options.HostKeyAlgorithms,

+ 5 - 1
outbound/tor.go

@@ -66,6 +66,10 @@ func NewTor(ctx context.Context, router adapter.Router, logger log.ContextLogger
 		}
 		}
 		startConf.TorrcFile = torrcFile
 		startConf.TorrcFile = torrcFile
 	}
 	}
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
 	return &Tor{
 	return &Tor{
 		myOutboundAdapter: myOutboundAdapter{
 		myOutboundAdapter: myOutboundAdapter{
 			protocol:     C.TypeTor,
 			protocol:     C.TypeTor,
@@ -76,7 +80,7 @@ func NewTor(ctx context.Context, router adapter.Router, logger log.ContextLogger
 			dependencies: withDialerDependency(options.DialerOptions),
 			dependencies: withDialerDependency(options.DialerOptions),
 		},
 		},
 		ctx:       ctx,
 		ctx:       ctx,
-		proxy:     NewProxyListener(ctx, logger, dialer.New(router, options.DialerOptions)),
+		proxy:     NewProxyListener(ctx, logger, outboundDialer),
 		startConf: &startConf,
 		startConf: &startConf,
 		options:   options.Options,
 		options:   options.Options,
 	}, nil
 	}, nil

+ 5 - 2
outbound/trojan.go

@@ -33,6 +33,10 @@ type Trojan struct {
 }
 }
 
 
 func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanOutboundOptions) (*Trojan, error) {
 func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanOutboundOptions) (*Trojan, error) {
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
 	outbound := &Trojan{
 	outbound := &Trojan{
 		myOutboundAdapter: myOutboundAdapter{
 		myOutboundAdapter: myOutboundAdapter{
 			protocol:     C.TypeTrojan,
 			protocol:     C.TypeTrojan,
@@ -42,11 +46,10 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog
 			tag:          tag,
 			tag:          tag,
 			dependencies: withDialerDependency(options.DialerOptions),
 			dependencies: withDialerDependency(options.DialerOptions),
 		},
 		},
-		dialer:     dialer.New(router, options.DialerOptions),
+		dialer:     outboundDialer,
 		serverAddr: options.ServerOptions.Build(),
 		serverAddr: options.ServerOptions.Build(),
 		key:        trojan.Key(options.Password),
 		key:        trojan.Key(options.Password),
 	}
 	}
-	var err error
 	if options.TLS != nil {
 	if options.TLS != nil {
 		outbound.tlsConfig, err = tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
 		outbound.tlsConfig, err = tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
 		if err != nil {
 		if err != nil {

+ 5 - 1
outbound/tuic.go

@@ -57,9 +57,13 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge
 	case "quic":
 	case "quic":
 		udpStream = true
 		udpStream = true
 	}
 	}
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
 	client, err := tuic.NewClient(tuic.ClientOptions{
 	client, err := tuic.NewClient(tuic.ClientOptions{
 		Context:           ctx,
 		Context:           ctx,
-		Dialer:            dialer.New(router, options.DialerOptions),
+		Dialer:            outboundDialer,
 		ServerAddress:     options.ServerOptions.Build(),
 		ServerAddress:     options.ServerOptions.Build(),
 		TLSConfig:         tlsConfig,
 		TLSConfig:         tlsConfig,
 		UUID:              userUUID,
 		UUID:              userUUID,

+ 5 - 2
outbound/vless.go

@@ -36,6 +36,10 @@ type VLESS struct {
 }
 }
 
 
 func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSOutboundOptions) (*VLESS, error) {
 func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSOutboundOptions) (*VLESS, error) {
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
 	outbound := &VLESS{
 	outbound := &VLESS{
 		myOutboundAdapter: myOutboundAdapter{
 		myOutboundAdapter: myOutboundAdapter{
 			protocol:     C.TypeVLESS,
 			protocol:     C.TypeVLESS,
@@ -45,10 +49,9 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg
 			tag:          tag,
 			tag:          tag,
 			dependencies: withDialerDependency(options.DialerOptions),
 			dependencies: withDialerDependency(options.DialerOptions),
 		},
 		},
-		dialer:     dialer.New(router, options.DialerOptions),
+		dialer:     outboundDialer,
 		serverAddr: options.ServerOptions.Build(),
 		serverAddr: options.ServerOptions.Build(),
 	}
 	}
-	var err error
 	if options.TLS != nil {
 	if options.TLS != nil {
 		outbound.tlsConfig, err = tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
 		outbound.tlsConfig, err = tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
 		if err != nil {
 		if err != nil {

+ 5 - 2
outbound/vmess.go

@@ -35,6 +35,10 @@ type VMess struct {
 }
 }
 
 
 func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessOutboundOptions) (*VMess, error) {
 func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessOutboundOptions) (*VMess, error) {
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
 	outbound := &VMess{
 	outbound := &VMess{
 		myOutboundAdapter: myOutboundAdapter{
 		myOutboundAdapter: myOutboundAdapter{
 			protocol:     C.TypeVMess,
 			protocol:     C.TypeVMess,
@@ -44,10 +48,9 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
 			tag:          tag,
 			tag:          tag,
 			dependencies: withDialerDependency(options.DialerOptions),
 			dependencies: withDialerDependency(options.DialerOptions),
 		},
 		},
-		dialer:     dialer.New(router, options.DialerOptions),
+		dialer:     outboundDialer,
 		serverAddr: options.ServerOptions.Build(),
 		serverAddr: options.ServerOptions.Build(),
 	}
 	}
-	var err error
 	if options.TLS != nil {
 	if options.TLS != nil {
 		outbound.tlsConfig, err = tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
 		outbound.tlsConfig, err = tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
 		if err != nil {
 		if err != nil {

+ 5 - 2
outbound/wireguard.go

@@ -65,7 +65,11 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
 			connectAddr = options.ServerOptions.Build()
 			connectAddr = options.ServerOptions.Build()
 		}
 		}
 	}
 	}
-	outbound.bind = wireguard.NewClientBind(ctx, outbound, dialer.New(router, options.DialerOptions), isConnect, connectAddr, reserved)
+	outboundDialer, err := dialer.New(router, options.DialerOptions)
+	if err != nil {
+		return nil, err
+	}
+	outbound.bind = wireguard.NewClientBind(ctx, outbound, outboundDialer, isConnect, connectAddr, reserved)
 	localPrefixes := common.Map(options.LocalAddress, option.ListenPrefix.Build)
 	localPrefixes := common.Map(options.LocalAddress, option.ListenPrefix.Build)
 	if len(localPrefixes) == 0 {
 	if len(localPrefixes) == 0 {
 		return nil, E.New("missing local address")
 		return nil, E.New("missing local address")
@@ -157,7 +161,6 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
 		mtu = 1408
 		mtu = 1408
 	}
 	}
 	var wireTunDevice wireguard.Device
 	var wireTunDevice wireguard.Device
-	var err error
 	if !options.SystemInterface && tun.WithGVisor {
 	if !options.SystemInterface && tun.WithGVisor {
 		wireTunDevice, err = wireguard.NewStackDevice(localPrefixes, mtu)
 		wireTunDevice, err = wireguard.NewStackDevice(localPrefixes, mtu)
 	} else {
 	} else {

+ 8 - 1
route/router.go

@@ -38,7 +38,9 @@ import (
 	F "github.com/sagernet/sing/common/format"
 	F "github.com/sagernet/sing/common/format"
 	M "github.com/sagernet/sing/common/metadata"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
+	serviceNTP "github.com/sagernet/sing/common/ntp"
 	"github.com/sagernet/sing/common/uot"
 	"github.com/sagernet/sing/common/uot"
+	"github.com/sagernet/sing/service"
 	"github.com/sagernet/sing/service/pause"
 	"github.com/sagernet/sing/service/pause"
 )
 )
 
 
@@ -319,7 +321,12 @@ func NewRouter(
 		}
 		}
 	}
 	}
 	if ntpOptions.Enabled {
 	if ntpOptions.Enabled {
-		router.timeService = ntp.NewService(ctx, router, logFactory.NewLogger("ntp"), ntpOptions)
+		timeService, err := ntp.NewService(ctx, router, logFactory.NewLogger("ntp"), ntpOptions)
+		if err != nil {
+			return nil, err
+		}
+		service.ContextWith[serviceNTP.TimeService](ctx, timeService)
+		router.timeService = timeService
 	}
 	}
 	return router, nil
 	return router, nil
 }
 }

+ 2 - 2
transport/dhcp/server.go

@@ -248,10 +248,10 @@ func (t *Transport) recreateServers(iface *net.Interface, serverAddrs []netip.Ad
 		}), ","), "]")
 		}), ","), "]")
 	}
 	}
 
 
-	serverDialer := dialer.NewDefault(t.router, option.DialerOptions{
+	serverDialer := common.Must1(dialer.NewDefault(t.router, option.DialerOptions{
 		BindInterface:      iface.Name,
 		BindInterface:      iface.Name,
 		UDPFragmentDefault: true,
 		UDPFragmentDefault: true,
-	})
+	}))
 	var transports []dns.Transport
 	var transports []dns.Transport
 	for _, serverAddr := range serverAddrs {
 	for _, serverAddr := range serverAddrs {
 		serverTransport, err := dns.NewUDPTransport(t.name, t.ctx, serverDialer, M.Socksaddr{Addr: serverAddr, Port: 53})
 		serverTransport, err := dns.NewUDPTransport(t.name, t.ctx, serverDialer, M.Socksaddr{Addr: serverAddr, Port: 53})

+ 3 - 2
transport/wireguard/device_system.go

@@ -10,6 +10,7 @@ import (
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing-tun"
+	"github.com/sagernet/sing/common"
 	M "github.com/sagernet/sing/common/metadata"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
 	wgTun "github.com/sagernet/wireguard-go/tun"
 	wgTun "github.com/sagernet/wireguard-go/tun"
@@ -58,9 +59,9 @@ func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes
 		inet6Address = inet6Addresses[0].Addr()
 		inet6Address = inet6Addresses[0].Addr()
 	}
 	}
 	return &SystemDevice{
 	return &SystemDevice{
-		dialer: dialer.NewDefault(router, option.DialerOptions{
+		dialer: common.Must1(dialer.NewDefault(router, option.DialerOptions{
 			BindInterface: interfaceName,
 			BindInterface: interfaceName,
-		}),
+		})),
 		device: tunInterface,
 		device: tunInterface,
 		name:   interfaceName,
 		name:   interfaceName,
 		mtu:    int(mtu),
 		mtu:    int(mtu),