diff --git a/docs/configuration/outbound/tuic.md b/docs/configuration/outbound/tuic.md index d46d4cb4..bbcc199f 100644 --- a/docs/configuration/outbound/tuic.md +++ b/docs/configuration/outbound/tuic.md @@ -11,6 +11,7 @@ "password": "hello", "congestion_control": "cubic", "udp_relay_mode": "native", + "udp_over_stream": false, "zero_rtt_handshake": false, "heartbeat": "10s", "network": "tcp", @@ -67,6 +68,19 @@ UDP packet relay mode `native` is used by default. +Conflict with `udp_over_stream`. + +#### udp_over_stream + +This is the TUIC port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp), designed to provide a QUIC +stream based UDP relay mode that TUIC does not provide. Since it is an add-on protocol, you will need to use sing-box or +another program compatible with the protocol as a server. + +This mode has no positive effect in a proper UDP proxy scenario and should only be applied to relay streaming UDP +traffic (basically QUIC streams). + +Conflict with `udp_relay_mode`. + #### network Enabled network diff --git a/docs/configuration/outbound/tuic.zh.md b/docs/configuration/outbound/tuic.zh.md index 9d19078a..ce5a1899 100644 --- a/docs/configuration/outbound/tuic.zh.md +++ b/docs/configuration/outbound/tuic.zh.md @@ -11,6 +11,7 @@ "password": "hello", "congestion_control": "cubic", "udp_relay_mode": "native", + "udp_over_stream": false, "zero_rtt_handshake": false, "heartbeat": "10s", "network": "tcp", @@ -65,6 +66,15 @@ UDP 包中继模式 | native | 原生 UDP | | quic | 使用 QUIC 流的无损 UDP 中继,引入了额外的开销 | +与 `udp_over_stream` 冲突。 + +#### udp_over_stream + +这是 TUIC 的 [UDP over TCP 协议](/configuration/shared/udp-over-tcp) 移植, 旨在提供 TUIC 不提供的 基于 QUIC 流的 UDP 中继模式。 由于它是一个附加协议,因此您需要使用 sing-box 或其他兼容的程序作为服务器。 + +此模式在正确的 UDP 代理场景中没有任何积极作用,仅适用于中继流式 UDP 流量(基本上是 QUIC 流)。 + +与 `udp_relay_mode` 冲突。 #### zero_rtt_handshake diff --git a/option/tuic.go b/option/tuic.go index 98d48be2..2720509d 100644 --- a/option/tuic.go +++ b/option/tuic.go @@ -23,6 +23,7 @@ type TUICOutboundOptions struct { Password string `json:"password,omitempty"` CongestionControl string `json:"congestion_control,omitempty"` UDPRelayMode string `json:"udp_relay_mode,omitempty"` + UDPOverStream bool `json:"udp_over_stream,omitempty"` ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` Heartbeat Duration `json:"heartbeat,omitempty"` Network NetworkList `json:"network,omitempty"` diff --git a/outbound/tuic.go b/outbound/tuic.go index 3b0ff157..71148aca 100644 --- a/outbound/tuic.go +++ b/outbound/tuic.go @@ -20,6 +20,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/uot" "github.com/gofrs/uuid/v5" ) @@ -31,7 +32,8 @@ var ( type TUIC struct { myOutboundAdapter - client *tuic.Client + client *tuic.Client + udpStream bool } func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (*TUIC, error) { @@ -51,11 +53,14 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge if err != nil { return nil, E.Cause(err, "invalid uuid") } - var udpStream bool + var tuicUDPStream bool + if options.UDPOverStream && options.UDPRelayMode != "" { + return nil, E.New("udp_over_stream is conflict with udp_relay_mode") + } switch options.UDPRelayMode { case "native": case "quic": - udpStream = true + tuicUDPStream = true } outboundDialer, err := dialer.New(router, options.DialerOptions) if err != nil { @@ -69,7 +74,7 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge UUID: userUUID, Password: options.Password, CongestionControl: options.CongestionControl, - UDPStream: udpStream, + UDPStream: tuicUDPStream, ZeroRTTHandshake: options.ZeroRTTHandshake, Heartbeat: time.Duration(options.Heartbeat), }) @@ -85,7 +90,8 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge tag: tag, dependencies: withDialerDependency(options.DialerOptions), }, - client: client, + client: client, + udpStream: options.UDPOverStream, }, nil } @@ -95,19 +101,43 @@ func (h *TUIC) DialContext(ctx context.Context, network string, destination M.So h.logger.InfoContext(ctx, "outbound connection to ", destination) return h.client.DialConn(ctx, destination) case N.NetworkUDP: - conn, err := h.ListenPacket(ctx, destination) - if err != nil { - return nil, err + if h.udpStream { + h.logger.InfoContext(ctx, "outbound stream packet connection to ", destination) + streamConn, err := h.client.DialConn(ctx, uot.RequestDestination(uot.Version)) + if err != nil { + return nil, err + } + return uot.NewLazyConn(streamConn, uot.Request{ + IsConnect: true, + Destination: destination, + }), nil + } else { + conn, err := h.ListenPacket(ctx, destination) + if err != nil { + return nil, err + } + return bufio.NewBindPacketConn(conn, destination), nil } - return bufio.NewBindPacketConn(conn, destination), nil default: return nil, E.New("unsupported network: ", network) } } func (h *TUIC) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - h.logger.InfoContext(ctx, "outbound packet connection to ", destination) - return h.client.ListenPacket(ctx) + if h.udpStream { + h.logger.InfoContext(ctx, "outbound stream packet connection to ", destination) + streamConn, err := h.client.DialConn(ctx, uot.RequestDestination(uot.Version)) + if err != nil { + return nil, err + } + return uot.NewLazyConn(streamConn, uot.Request{ + IsConnect: false, + Destination: destination, + }), nil + } else { + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + return h.client.ListenPacket(ctx) + } } func (h *TUIC) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {