Skip to content

Commit 53475c7

Browse files
committed
Add hysteria2 protocol
1 parent 5d8af15 commit 53475c7

31 files changed

+2564
-17
lines changed

constant/proxy.go

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const (
2222
TypeShadowsocksR = "shadowsocksr"
2323
TypeVLESS = "vless"
2424
TypeTUIC = "tuic"
25+
TypeHysteria2 = "hysteria2"
2526
)
2627

2728
const (
@@ -65,6 +66,8 @@ func ProxyDisplayName(proxyType string) string {
6566
return "VLESS"
6667
case TypeTUIC:
6768
return "TUIC"
69+
case TypeHysteria2:
70+
return "Hysteria2"
6871
case TypeSelector:
6972
return "Selector"
7073
case TypeURLTest:

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ require (
2626
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2
2727
github.com/sagernet/quic-go v0.0.0-20230911082307-390b7c274032
2828
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
29-
github.com/sagernet/sing v0.2.10-0.20230912051334-9163f4486b0d
29+
github.com/sagernet/sing v0.2.10-0.20230912050851-1453c7c8c20d
3030
github.com/sagernet/sing-dns v0.1.9-0.20230911082806-425022bdc92b
31-
github.com/sagernet/sing-mux v0.1.3-0.20230907005326-7befbadbf314
31+
github.com/sagernet/sing-mux v0.1.3-0.20230908032617-759a1886a400
3232
github.com/sagernet/sing-shadowsocks v0.2.5-0.20230907005610-126234728ca0
3333
github.com/sagernet/sing-shadowsocks2 v0.1.4-0.20230907005906-5d2917b29248
3434
github.com/sagernet/sing-shadowtls v0.1.4

go.sum

+4-4
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,12 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byL
112112
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
113113
github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
114114
github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
115-
github.com/sagernet/sing v0.2.10-0.20230912051334-9163f4486b0d h1:VUBu98Mrq1B0LMo4TXd4/wSFxxLn32ygDaDtoPxoyvM=
116-
github.com/sagernet/sing v0.2.10-0.20230912051334-9163f4486b0d/go.mod h1:9uOZwWkhT2Z2WldolLxX34s+1svAX4i4vvz5hy8u1MA=
115+
github.com/sagernet/sing v0.2.10-0.20230912050851-1453c7c8c20d h1:dWUNsHDX8EMeGj1XCnrVtydy5C+5D+Orc5JjZP7myHg=
116+
github.com/sagernet/sing v0.2.10-0.20230912050851-1453c7c8c20d/go.mod h1:9uOZwWkhT2Z2WldolLxX34s+1svAX4i4vvz5hy8u1MA=
117117
github.com/sagernet/sing-dns v0.1.9-0.20230911082806-425022bdc92b h1:m/UWg2voyb94YCWAMInMXIQ91qQGZ6utPAwKCYrlciI=
118118
github.com/sagernet/sing-dns v0.1.9-0.20230911082806-425022bdc92b/go.mod h1:Kg98PBJEg/08jsNFtmZWmPomhskn9Ausn50ecNm4M+8=
119-
github.com/sagernet/sing-mux v0.1.3-0.20230907005326-7befbadbf314 h1:P5+NZGMH8KSI3L8lKw1znxdRi0tIpWbGYjmv8GrFHrQ=
120-
github.com/sagernet/sing-mux v0.1.3-0.20230907005326-7befbadbf314/go.mod h1:TKxqIvfQQgd36jp2tzsPavGjYTVZilV+atip1cssjIY=
119+
github.com/sagernet/sing-mux v0.1.3-0.20230908032617-759a1886a400 h1:LtpYd5c5AJtUSxjyH4KjUS8HT+2XgilyozjbCq/x3EM=
120+
github.com/sagernet/sing-mux v0.1.3-0.20230908032617-759a1886a400/go.mod h1:TKxqIvfQQgd36jp2tzsPavGjYTVZilV+atip1cssjIY=
121121
github.com/sagernet/sing-shadowsocks v0.2.5-0.20230907005610-126234728ca0 h1:9wHYWxH+fcs01PM2+DylA8LNNY3ElnZykQo9rysng8U=
122122
github.com/sagernet/sing-shadowsocks v0.2.5-0.20230907005610-126234728ca0/go.mod h1:80fNKP0wnqlu85GZXV1H1vDPC/2t+dQbFggOw4XuFUM=
123123
github.com/sagernet/sing-shadowsocks2 v0.1.4-0.20230907005906-5d2917b29248 h1:JTFfy/LDmVFEK4KZJEujmC1iO8+aoF4unYhhZZRzRq4=

inbound/builder.go

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
4646
return NewVLESS(ctx, router, logger, options.Tag, options.VLESSOptions)
4747
case C.TypeTUIC:
4848
return NewTUIC(ctx, router, logger, options.Tag, options.TUICOptions)
49+
case C.TypeHysteria2:
50+
return NewHysteria2(ctx, router, logger, options.Tag, options.Hysteria2Options)
4951
default:
5052
return nil, E.New("unknown inbound type: ", options.Type)
5153
}

inbound/hysteria2.go

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
//go:build with_quic
2+
3+
package inbound
4+
5+
import (
6+
"context"
7+
"net"
8+
"net/http"
9+
"net/http/httputil"
10+
"net/url"
11+
12+
"github.com/sagernet/sing-box/adapter"
13+
"github.com/sagernet/sing-box/common/tls"
14+
C "github.com/sagernet/sing-box/constant"
15+
"github.com/sagernet/sing-box/log"
16+
"github.com/sagernet/sing-box/option"
17+
"github.com/sagernet/sing-box/transport/hysteria2"
18+
"github.com/sagernet/sing/common"
19+
"github.com/sagernet/sing/common/auth"
20+
E "github.com/sagernet/sing/common/exceptions"
21+
N "github.com/sagernet/sing/common/network"
22+
)
23+
24+
var _ adapter.Inbound = (*Hysteria2)(nil)
25+
26+
type Hysteria2 struct {
27+
myInboundAdapter
28+
tlsConfig tls.ServerConfig
29+
server *hysteria2.Server
30+
}
31+
32+
func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (*Hysteria2, error) {
33+
if options.TLS == nil || !options.TLS.Enabled {
34+
return nil, C.ErrTLSRequired
35+
}
36+
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
37+
if err != nil {
38+
return nil, err
39+
}
40+
var salamanderPassword string
41+
if options.Obfs != nil {
42+
if options.Obfs.Password == "" {
43+
return nil, E.New("missing obfs password")
44+
}
45+
switch options.Obfs.Type {
46+
case hysteria2.ObfsTypeSalamander:
47+
salamanderPassword = options.Obfs.Password
48+
default:
49+
return nil, E.New("unknown obfs type: ", options.Obfs.Type)
50+
}
51+
}
52+
var masqueradeHandler http.Handler
53+
if options.Masquerade != "" {
54+
masqueradeURL, err := url.Parse(options.Masquerade)
55+
if err != nil {
56+
return nil, E.Cause(err, "parse masquerade URL")
57+
}
58+
switch masqueradeURL.Scheme {
59+
case "file":
60+
masqueradeHandler = http.FileServer(http.Dir(masqueradeURL.Path))
61+
case "http", "https":
62+
masqueradeHandler = &httputil.ReverseProxy{
63+
Rewrite: func(r *httputil.ProxyRequest) {
64+
r.SetURL(masqueradeURL)
65+
r.Out.Host = r.In.Host
66+
},
67+
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
68+
w.WriteHeader(http.StatusBadGateway)
69+
},
70+
}
71+
default:
72+
return nil, E.New("unknown masquerade URL scheme: ", masqueradeURL.Scheme)
73+
}
74+
}
75+
inbound := &Hysteria2{
76+
myInboundAdapter: myInboundAdapter{
77+
protocol: C.TypeHysteria2,
78+
network: []string{N.NetworkUDP},
79+
ctx: ctx,
80+
router: router,
81+
logger: logger,
82+
tag: tag,
83+
listenOptions: options.ListenOptions,
84+
},
85+
tlsConfig: tlsConfig,
86+
}
87+
server, err := hysteria2.NewServer(hysteria2.ServerOptions{
88+
Context: ctx,
89+
Logger: logger,
90+
SendBPS: uint64(options.UpMbps * 1024 * 1024),
91+
ReceiveBPS: uint64(options.DownMbps * 1024 * 1024),
92+
SalamanderPassword: salamanderPassword,
93+
TLSConfig: tlsConfig,
94+
Users: common.Map(options.Users, func(it option.Hysteria2User) hysteria2.User {
95+
return hysteria2.User(it)
96+
}),
97+
IgnoreClientBandwidth: options.IgnoreClientBandwidth,
98+
Handler: adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, nil),
99+
MasqueradeHandler: masqueradeHandler,
100+
})
101+
if err != nil {
102+
return nil, err
103+
}
104+
inbound.server = server
105+
return inbound, nil
106+
}
107+
108+
func (h *Hysteria2) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
109+
ctx = log.ContextWithNewID(ctx)
110+
h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
111+
metadata = h.createMetadata(conn, metadata)
112+
metadata.User, _ = auth.UserFromContext[string](ctx)
113+
return h.router.RouteConnection(ctx, conn, metadata)
114+
}
115+
116+
func (h *Hysteria2) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
117+
ctx = log.ContextWithNewID(ctx)
118+
metadata = h.createPacketMetadata(conn, metadata)
119+
metadata.User, _ = auth.UserFromContext[string](ctx)
120+
h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
121+
return h.router.RoutePacketConnection(ctx, conn, metadata)
122+
}
123+
124+
func (h *Hysteria2) Start() error {
125+
if h.tlsConfig != nil {
126+
err := h.tlsConfig.Start()
127+
if err != nil {
128+
return err
129+
}
130+
}
131+
packetConn, err := h.myInboundAdapter.ListenUDP()
132+
if err != nil {
133+
return err
134+
}
135+
return h.server.Start(packetConn)
136+
}
137+
138+
func (h *Hysteria2) Close() error {
139+
return common.Close(
140+
&h.myInboundAdapter,
141+
h.tlsConfig,
142+
common.PtrOrNil(h.server),
143+
)
144+
}

inbound/hysteria_stub.go

+4
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,7 @@ import (
1414
func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (adapter.Inbound, error) {
1515
return nil, C.ErrQUICNotIncluded
1616
}
17+
18+
func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) {
19+
return nil, C.ErrQUICNotIncluded
20+
}

option/hysteria2.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package option
2+
3+
type Hysteria2InboundOptions struct {
4+
ListenOptions
5+
UpMbps int `json:"up_mbps,omitempty"`
6+
DownMbps int `json:"down_mbps,omitempty"`
7+
Obfs *Hysteria2Obfs `json:"obfs,omitempty"`
8+
Users []Hysteria2User `json:"users,omitempty"`
9+
IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"`
10+
TLS *InboundTLSOptions `json:"tls,omitempty"`
11+
Masquerade string `json:"masquerade,omitempty"`
12+
}
13+
14+
type Hysteria2Obfs struct {
15+
Type string `json:"type,omitempty"`
16+
Password string `json:"password,omitempty"`
17+
}
18+
19+
type Hysteria2User struct {
20+
Name string `json:"name,omitempty"`
21+
Password string `json:"password,omitempty"`
22+
}
23+
24+
type Hysteria2OutboundOptions struct {
25+
DialerOptions
26+
ServerOptions
27+
UpMbps int `json:"up_mbps,omitempty"`
28+
DownMbps int `json:"down_mbps,omitempty"`
29+
Obfs *Hysteria2Obfs `json:"obfs,omitempty"`
30+
Password string `json:"password,omitempty"`
31+
Network NetworkList `json:"network,omitempty"`
32+
TLS *OutboundTLSOptions `json:"tls,omitempty"`
33+
}

option/inbound.go

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type _Inbound struct {
2424
ShadowTLSOptions ShadowTLSInboundOptions `json:"-"`
2525
VLESSOptions VLESSInboundOptions `json:"-"`
2626
TUICOptions TUICInboundOptions `json:"-"`
27+
Hysteria2Options Hysteria2InboundOptions `json:"-"`
2728
}
2829

2930
type Inbound _Inbound
@@ -61,6 +62,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) {
6162
v = h.VLESSOptions
6263
case C.TypeTUIC:
6364
v = h.TUICOptions
65+
case C.TypeHysteria2:
66+
v = h.Hysteria2Options
6467
default:
6568
return nil, E.New("unknown inbound type: ", h.Type)
6669
}
@@ -104,6 +107,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
104107
v = &h.VLESSOptions
105108
case C.TypeTUIC:
106109
v = &h.TUICOptions
110+
case C.TypeHysteria2:
111+
v = &h.Hysteria2Options
107112
default:
108113
return E.New("unknown inbound type: ", h.Type)
109114
}

option/outbound.go

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type _Outbound struct {
2424
ShadowsocksROptions ShadowsocksROutboundOptions `json:"-"`
2525
VLESSOptions VLESSOutboundOptions `json:"-"`
2626
TUICOptions TUICOutboundOptions `json:"-"`
27+
Hysteria2Options Hysteria2OutboundOptions `json:"-"`
2728
SelectorOptions SelectorOutboundOptions `json:"-"`
2829
URLTestOptions URLTestOutboundOptions `json:"-"`
2930
}
@@ -63,6 +64,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) {
6364
v = h.VLESSOptions
6465
case C.TypeTUIC:
6566
v = h.TUICOptions
67+
case C.TypeHysteria2:
68+
v = h.Hysteria2Options
6669
case C.TypeSelector:
6770
v = h.SelectorOptions
6871
case C.TypeURLTest:
@@ -110,6 +113,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
110113
v = &h.VLESSOptions
111114
case C.TypeTUIC:
112115
v = &h.TUICOptions
116+
case C.TypeHysteria2:
117+
v = &h.Hysteria2Options
113118
case C.TypeSelector:
114119
v = &h.SelectorOptions
115120
case C.TypeURLTest:

outbound/builder.go

+2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t
5353
return NewVLESS(ctx, router, logger, tag, options.VLESSOptions)
5454
case C.TypeTUIC:
5555
return NewTUIC(ctx, router, logger, tag, options.TUICOptions)
56+
case C.TypeHysteria2:
57+
return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options)
5658
case C.TypeSelector:
5759
return NewSelector(router, logger, tag, options.SelectorOptions)
5860
case C.TypeURLTest:

outbound/default.go

+23-7
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata a
6565
outConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination)
6666
}
6767
if err != nil {
68-
return N.HandshakeFailure(conn, err)
68+
return N.ReportHandshakeFailure(conn, err)
69+
}
70+
err = N.ReportHandshakeSuccess(conn)
71+
if err != nil {
72+
return err
6973
}
7074
return CopyEarlyConn(ctx, conn, outConn)
7175
}
@@ -80,14 +84,18 @@ func NewDirectConnection(ctx context.Context, router adapter.Router, this N.Dial
8084
var destinationAddresses []netip.Addr
8185
destinationAddresses, err = router.LookupDefault(ctx, metadata.Destination.Fqdn)
8286
if err != nil {
83-
return N.HandshakeFailure(conn, err)
87+
return N.ReportHandshakeFailure(conn, err)
8488
}
8589
outConn, err = N.DialSerial(ctx, this, N.NetworkTCP, metadata.Destination, destinationAddresses)
8690
} else {
8791
outConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination)
8892
}
8993
if err != nil {
90-
return N.HandshakeFailure(conn, err)
94+
return N.ReportHandshakeFailure(conn, err)
95+
}
96+
err = N.ReportHandshakeSuccess(conn)
97+
if err != nil {
98+
return err
9199
}
92100
return CopyEarlyConn(ctx, conn, outConn)
93101
}
@@ -103,7 +111,11 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn,
103111
outConn, err = this.ListenPacket(ctx, metadata.Destination)
104112
}
105113
if err != nil {
106-
return N.HandshakeFailure(conn, err)
114+
return N.ReportHandshakeFailure(conn, err)
115+
}
116+
err = N.ReportHandshakeSuccess(conn)
117+
if err != nil {
118+
return err
107119
}
108120
if destinationAddress.IsValid() {
109121
if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded {
@@ -132,14 +144,18 @@ func NewDirectPacketConnection(ctx context.Context, router adapter.Router, this
132144
var destinationAddresses []netip.Addr
133145
destinationAddresses, err = router.LookupDefault(ctx, metadata.Destination.Fqdn)
134146
if err != nil {
135-
return N.HandshakeFailure(conn, err)
147+
return N.ReportHandshakeFailure(conn, err)
136148
}
137149
outConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, destinationAddresses)
138150
} else {
139151
outConn, err = this.ListenPacket(ctx, metadata.Destination)
140152
}
141153
if err != nil {
142-
return N.HandshakeFailure(conn, err)
154+
return N.ReportHandshakeFailure(conn, err)
155+
}
156+
err = N.ReportHandshakeSuccess(conn)
157+
if err != nil {
158+
return err
143159
}
144160
if destinationAddress.IsValid() {
145161
if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded {
@@ -187,7 +203,7 @@ func CopyEarlyConn(ctx context.Context, conn net.Conn, serverConn net.Conn) erro
187203
}
188204
_, err = serverConn.Write(payload.Bytes())
189205
if err != nil {
190-
return N.HandshakeFailure(conn, err)
206+
return N.ReportHandshakeFailure(conn, err)
191207
}
192208
payload.Release()
193209
}

0 commit comments

Comments
 (0)