Skip to content

Commit 98aa7e6

Browse files
committed
feat: support realm-opts for hysteria2 outbound and listener
1 parent 8b8eae5 commit 98aa7e6

7 files changed

Lines changed: 190 additions & 3 deletions

File tree

adapter/outbound/hysteria2.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,23 @@ import (
55
"errors"
66
"fmt"
77
"net"
8+
"net/netip"
89
"strconv"
910
"time"
1011

1112
N "github.com/metacubex/mihomo/common/net"
1213
"github.com/metacubex/mihomo/common/utils"
1314
"github.com/metacubex/mihomo/component/ca"
15+
"github.com/metacubex/mihomo/component/resolver"
1416
C "github.com/metacubex/mihomo/constant"
1517
"github.com/metacubex/mihomo/log"
1618
"github.com/metacubex/mihomo/transport/tuic/common"
1719

20+
"github.com/metacubex/http"
1821
"github.com/metacubex/quic-go"
1922
qtls "github.com/metacubex/sing-quic"
2023
"github.com/metacubex/sing-quic/hysteria2"
24+
"github.com/metacubex/sing-quic/hysteria2/realm"
2125
M "github.com/metacubex/sing/common/metadata"
2226
"github.com/metacubex/tls"
2327
)
@@ -55,13 +59,31 @@ type Hysteria2Option struct {
5559
BBRProfile string `proxy:"bbr-profile,omitempty"`
5660
UdpMTU int `proxy:"udp-mtu,omitempty"`
5761

62+
RealmOpts Hysteria2RealmOption `proxy:"realm-opts,omitempty"`
63+
5864
// quic-go special config
5965
InitialStreamReceiveWindow uint64 `proxy:"initial-stream-receive-window,omitempty"`
6066
MaxStreamReceiveWindow uint64 `proxy:"max-stream-receive-window,omitempty"`
6167
InitialConnectionReceiveWindow uint64 `proxy:"initial-connection-receive-window,omitempty"`
6268
MaxConnectionReceiveWindow uint64 `proxy:"max-connection-receive-window,omitempty"`
6369
}
6470

71+
type Hysteria2RealmOption struct {
72+
Enable bool `proxy:"enable,omitempty"`
73+
ServerURL string `proxy:"server-url,omitempty"`
74+
Token string `proxy:"token,omitempty"`
75+
RealmID string `proxy:"realm-id,omitempty"`
76+
STUNServers []string `proxy:"stun-servers,omitempty"`
77+
78+
// for ServerURL
79+
SNI string `proxy:"sni,omitempty"`
80+
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
81+
Fingerprint string `proxy:"fingerprint,omitempty"`
82+
Certificate string `proxy:"certificate,omitempty"`
83+
PrivateKey string `proxy:"private-key,omitempty"`
84+
ALPN []string `proxy:"alpn,omitempty"`
85+
}
86+
6587
func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
6688
c, err := h.client.DialConn(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
6789
if err != nil {
@@ -229,6 +251,48 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
229251
return nil, errors.New("invalid port")
230252
}
231253

254+
if option.RealmOpts.Enable {
255+
httpTLSClientConfig, err := ca.GetTLSConfig(ca.Option{
256+
TLSConfig: &tls.Config{
257+
ServerName: option.RealmOpts.SNI,
258+
InsecureSkipVerify: option.RealmOpts.SkipCertVerify,
259+
},
260+
Fingerprint: option.RealmOpts.Fingerprint,
261+
Certificate: option.RealmOpts.Certificate,
262+
PrivateKey: option.RealmOpts.PrivateKey,
263+
})
264+
if err != nil {
265+
return nil, err
266+
}
267+
clientOptions.RealmOptions = &realm.Options{
268+
ServerURL: option.RealmOpts.ServerURL,
269+
Token: option.RealmOpts.Token,
270+
RealmID: option.RealmOpts.RealmID,
271+
STUNServers: option.RealmOpts.STUNServers,
272+
HTTPClient: &http.Client{
273+
Transport: &http.Transport{
274+
DialContext: outbound.dialer.DialContext,
275+
TLSClientConfig: httpTLSClientConfig,
276+
// from http.DefaultTransport
277+
ForceAttemptHTTP2: true,
278+
MaxIdleConns: 100,
279+
IdleConnTimeout: 90 * time.Second,
280+
TLSHandshakeTimeout: 10 * time.Second,
281+
ExpectContinueTimeout: 1 * time.Second,
282+
},
283+
},
284+
Resolver: func(ctx context.Context, host string, ipv4, ipv6 bool) ([]netip.Addr, error) {
285+
if ipv4 && !ipv6 {
286+
return resolver.LookupIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver)
287+
} else if ipv6 && !ipv4 {
288+
return resolver.LookupIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver)
289+
}
290+
return resolver.LookupIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
291+
},
292+
Logger: log.SingLogger,
293+
}
294+
}
295+
232296
client, err := hysteria2.NewClient(clientOptions)
233297
if err != nil {
234298
return nil, err

docs/config.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,18 @@ proxies: # socks5
10141014
# private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径
10151015
# alpn:
10161016
# - h3
1017+
# realm-opts:
1018+
# enable: true # 必须手动开启
1019+
# server-url: https://realm.hy2.io
1020+
# token: public
1021+
# realm-id: my-cabin-1f3a8c2e9b
1022+
# stun-servers:
1023+
# - stun.nextcloud.com:3478
1024+
# - stun.sip.us:3478
1025+
# - global.stun.twilio.com:3478
1026+
# # 下面支持填写针对server-url的TLS配置(sni, skip-cert-verify, fingerprint, certificate, private-key, alpn)
1027+
# # skip-cert-verify: false
1028+
# # ......
10171029
###quic-go特殊配置项,不要随意修改除非你知道你在干什么###
10181030
# initial-stream-receive-window: 8388608
10191031
# max-stream-receive-window: 8388608
@@ -1903,6 +1915,18 @@ listeners:
19031915
# masquerade: file:///var/www # 作为文件服务器
19041916
# masquerade: http://127.0.0.1:8080 #作为反向代理
19051917
# masquerade: https://127.0.0.1:8080 #作为反向代理
1918+
# realm-opts:
1919+
# enable: true # 必须手动开启
1920+
# server-url: https://realm.hy2.io
1921+
# token: public
1922+
# realm-id: my-cabin-1f3a8c2e9b
1923+
# stun-servers:
1924+
# - stun.nextcloud.com:3478
1925+
# - stun.sip.us:3478
1926+
# - global.stun.twilio.com:3478
1927+
# # 下面支持填写针对server-url的TLS配置(sni, skip-cert-verify, fingerprint, certificate, private-key, alpn)
1928+
# # skip-cert-verify: false
1929+
# # ......
19061930

19071931
- name: trusttunnel-in-1
19081932
type: trusttunnel

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ require (
3030
github.com/metacubex/restls-client-go v0.1.7
3131
github.com/metacubex/sing v0.5.7
3232
github.com/metacubex/sing-mux v0.3.9
33-
github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a
33+
github.com/metacubex/sing-quic v0.0.0-20260511111944-ed400da99ad4
3434
github.com/metacubex/sing-shadowsocks v0.2.12
3535
github.com/metacubex/sing-shadowsocks2 v0.2.7
3636
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ github.com/metacubex/sing v0.5.7 h1:8OC+fhKFSv/l9ehEhJRaZZAOuthfZo68SteBVLe8QqM=
127127
github.com/metacubex/sing v0.5.7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
128128
github.com/metacubex/sing-mux v0.3.9 h1:/aoBD2+sK2qsXDlNDe3hkR0GZuFDtwIZhOeGUx9W0Yk=
129129
github.com/metacubex/sing-mux v0.3.9/go.mod h1:8bT7ZKT3clRrJjYc/x5CRYibC1TX/bK73a3r3+2E+Fc=
130-
github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a h1:977o0ZYYbiQAGuOxql7Q6UN3rEy59OyAE0tELq4gZfI=
131-
github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a/go.mod h1:6ayFGfzzBE85csgQkM3gf4neFq6s0losHlPRSxY+nuk=
130+
github.com/metacubex/sing-quic v0.0.0-20260511111944-ed400da99ad4 h1:WwMH5gADSmQ2RgudpoAym4nSk5U70QwfovgCqXBX34M=
131+
github.com/metacubex/sing-quic v0.0.0-20260511111944-ed400da99ad4/go.mod h1:6ayFGfzzBE85csgQkM3gf4neFq6s0losHlPRSxY+nuk=
132132
github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE=
133133
github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU=
134134
github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A=

listener/config/hysteria2.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,31 @@ type Hysteria2Server struct {
2828
UdpMTU int `yaml:"udp-mtu" json:"udp-mtu,omitempty"`
2929
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
3030

31+
RealmOpts Hysteria2RealmOption `yaml:"realm-opts" json:"realm-opts,omitempty"`
32+
3133
// quic-go special config
3234
InitialStreamReceiveWindow uint64 `yaml:"initial-stream-receive-window" json:"initial-stream-receive-window,omitempty"`
3335
MaxStreamReceiveWindow uint64 `yaml:"max-stream-receive-window" json:"max-stream-receive-window,omitempty"`
3436
InitialConnectionReceiveWindow uint64 `yaml:"initial-connection-receive-window" json:"initial-connection-receive-window,omitempty"`
3537
MaxConnectionReceiveWindow uint64 `yaml:"max-connection-receive-window" json:"max-connection-receive-window,omitempty"`
3638
}
3739

40+
type Hysteria2RealmOption struct {
41+
Enable bool `yaml:"enable" json:"enable,omitempty"`
42+
ServerURL string `yaml:"server-url" json:"server-url,omitempty"`
43+
Token string `yaml:"token" json:"token,omitempty"`
44+
RealmID string `yaml:"realm-id" json:"realm-id,omitempty"`
45+
STUNServers []string `yaml:"stun-servers" json:"stun-servers,omitempty"`
46+
47+
// for ServerURL
48+
SNI string `yaml:"sni" json:"sni,omitempty"`
49+
SkipCertVerify bool `yaml:"skip-cert-verify" json:"skip-cert-verify,omitempty"`
50+
Fingerprint string `yaml:"fingerprint" json:"fingerprint,omitempty"`
51+
Certificate string `yaml:"certificate" json:"certificate,omitempty"`
52+
PrivateKey string `yaml:"private-key" json:"private-key,omitempty"`
53+
ALPN []string `yaml:"alpn" json:"alpn,omitempty"`
54+
}
55+
3856
func (h Hysteria2Server) String() string {
3957
b, _ := json.Marshal(h)
4058
return string(b)

listener/inbound/hysteria2.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,47 @@ type Hysteria2Option struct {
3030
UdpMTU int `inbound:"udp-mtu,omitempty"`
3131
MuxOption MuxOption `inbound:"mux-option,omitempty"`
3232

33+
RealmOpts Hysteria2RealmOption `inbound:"realm-opts,omitempty"`
34+
3335
// quic-go special config
3436
InitialStreamReceiveWindow uint64 `inbound:"initial-stream-receive-window,omitempty"`
3537
MaxStreamReceiveWindow uint64 `inbound:"max-stream-receive-window,omitempty"`
3638
InitialConnectionReceiveWindow uint64 `inbound:"initial-connection-receive-window,omitempty"`
3739
MaxConnectionReceiveWindow uint64 `inbound:"max-connection-receive-window,omitempty"`
3840
}
3941

42+
type Hysteria2RealmOption struct {
43+
Enable bool `inbound:"enable,omitempty"`
44+
ServerURL string `inbound:"server-url,omitempty"`
45+
Token string `inbound:"token,omitempty"`
46+
RealmID string `inbound:"realm-id,omitempty"`
47+
STUNServers []string `inbound:"stun-servers,omitempty"`
48+
49+
// for ServerURL
50+
SNI string `inbound:"sni,omitempty"`
51+
SkipCertVerify bool `inbound:"skip-cert-verify,omitempty"`
52+
Fingerprint string `inbound:"fingerprint,omitempty"`
53+
Certificate string `inbound:"certificate,omitempty"`
54+
PrivateKey string `inbound:"private-key,omitempty"`
55+
ALPN []string `inbound:"alpn,omitempty"`
56+
}
57+
58+
func (o Hysteria2RealmOption) Build() LC.Hysteria2RealmOption {
59+
return LC.Hysteria2RealmOption{
60+
Enable: o.Enable,
61+
ServerURL: o.ServerURL,
62+
Token: o.Token,
63+
RealmID: o.RealmID,
64+
STUNServers: o.STUNServers,
65+
SNI: o.SNI,
66+
SkipCertVerify: o.SkipCertVerify,
67+
Fingerprint: o.Fingerprint,
68+
Certificate: o.Certificate,
69+
PrivateKey: o.PrivateKey,
70+
ALPN: o.ALPN,
71+
}
72+
}
73+
4074
func (o Hysteria2Option) Equal(config C.InboundConfig) bool {
4175
return optionToString(o) == optionToString(config)
4276
}
@@ -77,6 +111,7 @@ func NewHysteria2(options *Hysteria2Option) (*Hysteria2, error) {
77111
BBRProfile: options.BBRProfile,
78112
UdpMTU: options.UdpMTU,
79113
MuxOption: options.MuxOption.Build(),
114+
RealmOpts: options.RealmOpts.Build(),
80115
// quic-go special config
81116
InitialStreamReceiveWindow: options.InitialStreamReceiveWindow,
82117
MaxStreamReceiveWindow: options.MaxStreamReceiveWindow,

listener/sing_hysteria2/server.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"net"
8+
"net/netip"
89
"net/url"
910
"strings"
1011
"time"
@@ -14,6 +15,7 @@ import (
1415
"github.com/metacubex/mihomo/common/sockopt"
1516
"github.com/metacubex/mihomo/component/ca"
1617
"github.com/metacubex/mihomo/component/ech"
18+
"github.com/metacubex/mihomo/component/resolver"
1719
C "github.com/metacubex/mihomo/constant"
1820
LC "github.com/metacubex/mihomo/listener/config"
1921
"github.com/metacubex/mihomo/listener/inner"
@@ -26,6 +28,7 @@ import (
2628
"github.com/metacubex/http/httputil"
2729
"github.com/metacubex/quic-go"
2830
"github.com/metacubex/sing-quic/hysteria2"
31+
"github.com/metacubex/sing-quic/hysteria2/realm"
2932
E "github.com/metacubex/sing/common/exceptions"
3033
"github.com/metacubex/tls"
3134
)
@@ -146,6 +149,48 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi
146149
return nil, E.New("unknown masquerade URL scheme: ", masqueradeURL.Scheme)
147150
}
148151
}
152+
var realmOptions *realm.Options
153+
if config.RealmOpts.Enable {
154+
httpTLSClientConfig, err := ca.GetTLSConfig(ca.Option{
155+
TLSConfig: &tls.Config{
156+
ServerName: config.RealmOpts.SNI,
157+
InsecureSkipVerify: config.RealmOpts.SkipCertVerify,
158+
},
159+
Fingerprint: config.RealmOpts.Fingerprint,
160+
Certificate: config.RealmOpts.Certificate,
161+
PrivateKey: config.RealmOpts.PrivateKey,
162+
})
163+
if err != nil {
164+
return nil, err
165+
}
166+
realmOptions = &realm.Options{
167+
ServerURL: config.RealmOpts.ServerURL,
168+
Token: config.RealmOpts.Token,
169+
RealmID: config.RealmOpts.RealmID,
170+
STUNServers: config.RealmOpts.STUNServers,
171+
HTTPClient: &http.Client{Transport: &http.Transport{
172+
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
173+
return inner.HandleTcp(tunnel, address, "")
174+
},
175+
TLSClientConfig: httpTLSClientConfig,
176+
// from http.DefaultTransport
177+
ForceAttemptHTTP2: true,
178+
MaxIdleConns: 100,
179+
IdleConnTimeout: 90 * time.Second,
180+
TLSHandshakeTimeout: 10 * time.Second,
181+
ExpectContinueTimeout: 1 * time.Second,
182+
}},
183+
Resolver: func(ctx context.Context, host string, ipv4, ipv6 bool) ([]netip.Addr, error) {
184+
if ipv4 && !ipv6 {
185+
return resolver.LookupIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver)
186+
} else if ipv6 && !ipv4 {
187+
return resolver.LookupIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver)
188+
}
189+
return resolver.LookupIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
190+
},
191+
Logger: log.SingLogger,
192+
}
193+
}
149194

150195
if config.UdpMTU == 0 {
151196
// "1200" from quic-go's MaxDatagramSize
@@ -173,6 +218,7 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi
173218
Handler: h,
174219
MasqueradeHandler: masqueradeHandler,
175220
UdpMTU: config.UdpMTU,
221+
RealmOptions: realmOptions,
176222
SetBBRCongestion: func(quicConn *quic.Conn) {
177223
common.SetCongestionController(quicConn, "bbr", config.CWND, config.BBRProfile)
178224
},

0 commit comments

Comments
 (0)