diff --git a/docs/configuration/outbound/hysteria2.md b/docs/configuration/outbound/hysteria2.md index ae0b96ed..fa844bd7 100644 --- a/docs/configuration/outbound/hysteria2.md +++ b/docs/configuration/outbound/hysteria2.md @@ -15,6 +15,7 @@ }, "password": "goofy_ahh_password", "network": "tcp", + "udp_over_stream": false, "tls": {}, "brutal_debug": false, @@ -72,6 +73,15 @@ One of `tcp` `udp`. Both is enabled by default. +#### udp_over_stream + +This is the Hysteria2 port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp), designed to provide a QUIC +stream based UDP relay mode that Hysteria2 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 can improve reliability in proxying UDP traffic in lossy networks, as it supports retransmitting lost packets using +QUIC's loss detection mechanisms. + #### tls ==Required== diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md index 7176b9a6..02295834 100644 --- a/docs/configuration/outbound/hysteria2.zh.md +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -15,6 +15,7 @@ }, "password": "goofy_ahh_password", "network": "tcp", + "udp_over_stream": false, "tls": {}, "brutal_debug": false, @@ -70,6 +71,15 @@ QUIC 流量混淆器密码. 默认所有。 +#### udp_over_stream + +这是 Hysteria2 版本的 [UDP over TCP protocol](/configuration/shared/udp-over-tcp), 设计为提供一个Hysteria2没有的, +基于QUIC stream的UDP中继模式。由于这是一个附加的协议,你需要使用 +sing-box 或者与这个协议兼容的其他软件作为服务器。 + +这个协议可以增加在恶劣环境代理UDP的可靠性,因为它支持使用 +QUIC 的丢失检测机制重传丢失的数据包。 + #### tls ==必填== diff --git a/inbound/hysteria2.go b/inbound/hysteria2.go index 0e49cca2..4a32c946 100644 --- a/inbound/hysteria2.go +++ b/inbound/hysteria2.go @@ -4,6 +4,7 @@ package inbound import ( "context" + "github.com/sagernet/sing-box/common/uot" "net" "net/http" "net/http/httputil" @@ -81,7 +82,7 @@ func NewHysteria2(ctx context.Context, router adapter.Router, logger log.Context protocol: C.TypeHysteria2, network: []string{N.NetworkUDP}, ctx: ctx, - router: router, + router: uot.NewRouter(router, logger), logger: logger, tag: tag, listenOptions: options.ListenOptions, diff --git a/option/hysteria2.go b/option/hysteria2.go index 5032c734..3e6c4d2a 100644 --- a/option/hysteria2.go +++ b/option/hysteria2.go @@ -31,5 +31,6 @@ type Hysteria2OutboundOptions struct { Password string `json:"password,omitempty"` Network NetworkList `json:"network,omitempty"` OutboundTLSOptionsContainer + UDPOverStream bool `json:"udp_over_stream,omitempty"` BrutalDebug bool `json:"brutal_debug,omitempty"` } diff --git a/outbound/hysteria2.go b/outbound/hysteria2.go index f2ffe2fd..b45905c8 100644 --- a/outbound/hysteria2.go +++ b/outbound/hysteria2.go @@ -4,6 +4,7 @@ package outbound import ( "context" + "github.com/sagernet/sing/common/uot" "net" "os" @@ -29,7 +30,8 @@ var ( type Hysteria2 struct { myOutboundAdapter - client *hysteria2.Client + client *hysteria2.Client + udpStream bool } func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2OutboundOptions) (*Hysteria2, error) { @@ -83,7 +85,8 @@ func NewHysteria2(ctx context.Context, router adapter.Router, logger log.Context tag: tag, dependencies: withDialerDependency(options.DialerOptions), }, - client: client, + client: client, + udpStream: options.UDPOverStream, }, nil } @@ -93,19 +96,43 @@ func (h *Hysteria2) DialContext(ctx context.Context, network string, destination 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 *Hysteria2) 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 *Hysteria2) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {