diff --git a/examples/trusttunnel/client.json b/examples/trusttunnel/client.json index 4ecf2e6d..2f0a201b 100644 --- a/examples/trusttunnel/client.json +++ b/examples/trusttunnel/client.json @@ -52,6 +52,7 @@ "quic": true, "congestion_controller": "bbr", // bbr, bbr_standard, bbr2, bbr2_variant, cubic, reno "cwnd": 32, + "disable_path_mtu_discovery": false, // QUIC only "multiplex": { "enabled": true, "max_connections": 8, diff --git a/examples/trusttunnel/server.json b/examples/trusttunnel/server.json index 6f7bdfef..8c6ebdea 100644 --- a/examples/trusttunnel/server.json +++ b/examples/trusttunnel/server.json @@ -14,6 +14,7 @@ ], "congestion_controller": "bbr", // bbr, bbr_standard, bbr2, bbr2_variant, cubic, reno "cwnd": 32, + "disable_path_mtu_discovery": false, // QUIC only "tls": { "enabled": true, "alpn": ["h2", "h3"], diff --git a/option/trusttunnel.go b/option/trusttunnel.go index 046f823c..af4c193b 100644 --- a/option/trusttunnel.go +++ b/option/trusttunnel.go @@ -3,10 +3,11 @@ package option type TrustTunnelInboundOptions struct { ListenOptions InboundTLSOptionsContainer - Users []TrustTunnelUser `json:"users,omitempty"` - Network NetworkList `json:"network,omitempty"` - CongestionController string `json:"congestion_controller,omitempty"` - CWND int `json:"cwnd,omitempty"` + Users []TrustTunnelUser `json:"users,omitempty"` + Network NetworkList `json:"network,omitempty"` + CongestionController string `json:"congestion_controller,omitempty"` + CWND int `json:"cwnd,omitempty"` + DisablePathMTUDiscovery bool `json:"disable_path_mtu_discovery,omitempty"` } type TrustTunnelUser struct { @@ -25,12 +26,13 @@ type TrustTunnelOutboundOptions struct { DialerOptions ServerOptions OutboundTLSOptionsContainer - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - Network NetworkList `json:"network,omitempty"` - HealthCheck bool `json:"health_check,omitempty"` - QUIC bool `json:"quic,omitempty"` - CongestionController string `json:"congestion_controller,omitempty"` - CWND int `json:"cwnd,omitempty"` - Multiplex *TrustTunnelMultiplexOptions `json:"multiplex,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Network NetworkList `json:"network,omitempty"` + HealthCheck bool `json:"health_check,omitempty"` + QUIC bool `json:"quic,omitempty"` + CongestionController string `json:"congestion_controller,omitempty"` + CWND int `json:"cwnd,omitempty"` + DisablePathMTUDiscovery bool `json:"disable_path_mtu_discovery,omitempty"` + Multiplex *TrustTunnelMultiplexOptions `json:"multiplex,omitempty"` } diff --git a/protocol/trusttunnel/inbound.go b/protocol/trusttunnel/inbound.go index f5e1f977..2c2e1b4e 100644 --- a/protocol/trusttunnel/inbound.go +++ b/protocol/trusttunnel/inbound.go @@ -157,9 +157,10 @@ func (h *Inbound) Start(stage adapter.StartStage, scope *adapter.Scope) error { }, } quicListener, err := qtls.ListenEarly(udpConn, h.http3TLSConfig, &quic.Config{ - MaxIdleTimeout: trusttunnel.DefaultSessionTimeout * 2, - MaxIncomingStreams: 1 << 60, - Allow0RTT: true, + MaxIdleTimeout: trusttunnel.DefaultSessionTimeout * 2, + MaxIncomingStreams: 1 << 60, + Allow0RTT: true, + DisablePathMTUDiscovery: h.options.DisablePathMTUDiscovery, }) if err != nil { return err diff --git a/protocol/trusttunnel/outbound.go b/protocol/trusttunnel/outbound.go index 079c12bd..7cc57d6f 100644 --- a/protocol/trusttunnel/outbound.go +++ b/protocol/trusttunnel/outbound.go @@ -45,16 +45,17 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } clientOpts := trusttunnel.ClientOptions{ - Dialer: outboundDialer, - TLSConfig: tlsConfig, - Server: serverAddr, - Username: options.Username, - Password: options.Password, - QUIC: options.QUIC, - CongestionControl: options.CongestionController, - CWND: options.CWND, - Logger: logger, - HealthCheck: options.HealthCheck, + Dialer: outboundDialer, + TLSConfig: tlsConfig, + Server: serverAddr, + Username: options.Username, + Password: options.Password, + QUIC: options.QUIC, + DisablePathMTUDiscovery: options.DisablePathMTUDiscovery, + CongestionControl: options.CongestionController, + CWND: options.CWND, + Logger: logger, + HealthCheck: options.HealthCheck, } var client trusttunnel.Dialer if options.Multiplex != nil && options.Multiplex.Enabled { diff --git a/transport/trusttunnel/client.go b/transport/trusttunnel/client.go index 9b57675b..6b8fb545 100644 --- a/transport/trusttunnel/client.go +++ b/transport/trusttunnel/client.go @@ -8,6 +8,7 @@ import ( "io" "net" "net/http" + "net/http/httptrace" "net/url" "runtime" "sync" @@ -17,6 +18,7 @@ import ( "github.com/sagernet/sing-box/common/congestion" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -43,19 +45,20 @@ type Dialer interface { } type ClientOptions struct { - Dialer N.Dialer - TLSConfig tls.Config - Server M.Socksaddr - Username string - Password string - QUIC bool - CongestionControl string - CWND int - Logger logger.Logger - HealthCheck bool - MaxConnections int - MinStreams int - MaxStreams int + Dialer N.Dialer + TLSConfig tls.Config + Server M.Socksaddr + Username string + Password string + QUIC bool + DisablePathMTUDiscovery bool + CongestionControl string + CWND int + Logger logger.Logger + HealthCheck bool + MaxConnections int + MinStreams int + MaxStreams int } type Client struct { @@ -92,8 +95,9 @@ func NewClient(ctx context.Context, options ClientOptions) (*Client, error) { } client.roundTripper = &http3.Transport{ QUICConfig: &quic.Config{ - MaxIdleTimeout: DefaultSessionTimeout * 2, - KeepAlivePeriod: DefaultHealthCheckTimeout, + MaxIdleTimeout: DefaultSessionTimeout * 2, + KeepAlivePeriod: DefaultHealthCheckTimeout, + DisablePathMTUDiscovery: options.DisablePathMTUDiscovery, }, Dial: func(ctx context.Context, addr string, tlsCfg *stdtls.Config, cfg *quic.Config) (*quic.Conn, error) { udpConn, err := options.Dialer.DialContext(ctx, N.NetworkUDP, client.server) @@ -151,27 +155,38 @@ func (c *Client) resetHealthCheckTimer() { c.healthCheckTimer.Reset(DefaultHealthCheckTimeout) } -func (c *Client) roundTrip(request *http.Request, conn *httpConn) { +func (c *Client) roundTrip(ctx context.Context, request *http.Request, conn *httpConn) error { c.startOnce.Do(c.start) pipeReader, pipeWriter := io.Pipe() request.Body = pipeReader *conn = httpConn{writer: pipeWriter, created: make(chan struct{})} c.count.Add(1) conn.closeFn = sync.OnceFunc(func() { c.count.Add(-1) }) - ctx, cancel := context.WithCancel(c.ctx) + requestCtx, cancel := context.WithCancel(c.ctx) conn.cancelFn = cancel + gotConn := make(chan bool, 1) + streamCtx := httptrace.WithClientTrace(requestCtx, &httptrace.ClientTrace{ + GotConn: func(info httptrace.GotConnInfo) { + select { + case gotConn <- true: + default: + } + }, + }) + var requestErr common.TypedValue[error] go func() { - timeout := time.AfterFunc(C.TCPTimeout, cancel) - defer timeout.Stop() - request = request.WithContext(ctx) + request = request.WithContext(streamCtx) response, err := c.roundTripper.RoundTrip(request) if err != nil { + requestErr.Store(err) + close(gotConn) _ = pipeWriter.CloseWithError(err) _ = pipeReader.CloseWithError(err) conn.setup(nil, err) } else if response.StatusCode != http.StatusOK { _ = response.Body.Close() err = fmt.Errorf("unexpected status code: %d", response.StatusCode) + requestErr.Store(err) _ = pipeWriter.CloseWithError(err) _ = pipeReader.CloseWithError(err) conn.setup(nil, err) @@ -180,6 +195,12 @@ func (c *Client) roundTrip(request *http.Request, conn *httpConn) { conn.setup(response.Body, nil) } }() + select { + case <-ctx.Done(): + return ctx.Err() + case <-gotConn: + return requestErr.Load() + } } func (c *Client) newConnectRequest(host, userAgent string) *http.Request { @@ -196,13 +217,21 @@ func (c *Client) newConnectRequest(host, userAgent string) *http.Request { func (c *Client) Dial(ctx context.Context, host string) (net.Conn, error) { conn := &tcpConn{} - c.roundTrip(c.newConnectRequest(host, tcpUserAgent), &conn.httpConn) + err := c.roundTrip(ctx, c.newConnectRequest(host, tcpUserAgent), &conn.httpConn) + if err != nil { + _ = conn.Close() + return nil, err + } return conn, nil } func (c *Client) ListenPacket(ctx context.Context) (net.PacketConn, error) { conn := &clientPacketConn{} - c.roundTrip(c.newConnectRequest(UDPMagicAddress, udpUserAgent), &conn.httpConn) + err := c.roundTrip(ctx, c.newConnectRequest(UDPMagicAddress, udpUserAgent), &conn.httpConn) + if err != nil { + _ = conn.Close() + return nil, err + } return conn, nil }