diff --git a/docs/configuration/outbound/hysteria2.md b/docs/configuration/outbound/hysteria2.md index 26d5b72800..e7c3932cef 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/inbound/hysteria2.go b/inbound/hysteria2.go index 5db881c3d4..b591385352 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" @@ -80,7 +81,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 feab475b3a..ddf250652d 100644 --- a/option/hysteria2.go +++ b/option/hysteria2.go @@ -25,11 +25,12 @@ type Hysteria2User struct { type Hysteria2OutboundOptions struct { DialerOptions ServerOptions - UpMbps int `json:"up_mbps,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs *Hysteria2Obfs `json:"obfs,omitempty"` - Password string `json:"password,omitempty"` - Network NetworkList `json:"network,omitempty"` - TLS *OutboundTLSOptions `json:"tls,omitempty"` - BrutalDebug bool `json:"brutal_debug,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs *Hysteria2Obfs `json:"obfs,omitempty"` + Password string `json:"password,omitempty"` + Network NetworkList `json:"network,omitempty"` + TLS *OutboundTLSOptions `json:"tls,omitempty"` + UDPOverStream bool `json:"udp_over_stream,omitempty"` + BrutalDebug bool `json:"brutal_debug,omitempty"` } diff --git a/outbound/hysteria2.go b/outbound/hysteria2.go index f2ffe2fd1b..b45905c811 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 {