feat: support realm-opts for hysteria2 outbound and listener

This commit is contained in:
wwqgtxx committed 2026-05-11 19:20:30 +08:00
1 parent 8b8eae56d7
commit 98aa7e63c2
7 files changed
+190 -3

No files matched your search

+64
View File
@@ -5,19 +5,23 @@ import (
"errors"
"fmt"
"net"
"net/netip"
"strconv"
"time"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/transport/tuic/common"
"github.com/metacubex/http"
"github.com/metacubex/quic-go"
qtls "github.com/metacubex/sing-quic"
"github.com/metacubex/sing-quic/hysteria2"
"github.com/metacubex/sing-quic/hysteria2/realm"
M "github.com/metacubex/sing/common/metadata"
"github.com/metacubex/tls"
)
@@ -55,6 +59,8 @@ type Hysteria2Option struct {
BBRProfile string `proxy:"bbr-profile,omitempty"`
UdpMTU int `proxy:"udp-mtu,omitempty"`
RealmOpts Hysteria2RealmOption `proxy:"realm-opts,omitempty"`
// quic-go special config
InitialStreamReceiveWindow uint64 `proxy:"initial-stream-receive-window,omitempty"`
MaxStreamReceiveWindow uint64 `proxy:"max-stream-receive-window,omitempty"`
@@ -62,6 +68,22 @@ type Hysteria2Option struct {
MaxConnectionReceiveWindow uint64 `proxy:"max-connection-receive-window,omitempty"`
}
type Hysteria2RealmOption struct {
Enable bool `proxy:"enable,omitempty"`
ServerURL string `proxy:"server-url,omitempty"`
Token string `proxy:"token,omitempty"`
RealmID string `proxy:"realm-id,omitempty"`
STUNServers []string `proxy:"stun-servers,omitempty"`
// for ServerURL
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
Certificate string `proxy:"certificate,omitempty"`
PrivateKey string `proxy:"private-key,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
}
func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := h.client.DialConn(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
if err != nil {
@@ -229,6 +251,48 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
return nil, errors.New("invalid port")
}
if option.RealmOpts.Enable {
httpTLSClientConfig, err := ca.GetTLSConfig(ca.Option{
TLSConfig: &tls.Config{
ServerName: option.RealmOpts.SNI,
InsecureSkipVerify: option.RealmOpts.SkipCertVerify,
},
Fingerprint: option.RealmOpts.Fingerprint,
Certificate: option.RealmOpts.Certificate,
PrivateKey: option.RealmOpts.PrivateKey,
})
if err != nil {
return nil, err
}
clientOptions.RealmOptions = &realm.Options{
ServerURL: option.RealmOpts.ServerURL,
Token: option.RealmOpts.Token,
RealmID: option.RealmOpts.RealmID,
STUNServers: option.RealmOpts.STUNServers,
HTTPClient: &http.Client{
Transport: &http.Transport{
DialContext: outbound.dialer.DialContext,
TLSClientConfig: httpTLSClientConfig,
// from http.DefaultTransport
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
},
Resolver: func(ctx context.Context, host string, ipv4, ipv6 bool) ([]netip.Addr, error) {
if ipv4 && !ipv6 {
return resolver.LookupIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver)
} else if ipv6 && !ipv4 {
return resolver.LookupIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver)
}
return resolver.LookupIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
},
Logger: log.SingLogger,
}
}
client, err := hysteria2.NewClient(clientOptions)
if err != nil {
return nil, err
+24
View File
@@ -1014,6 +1014,18 @@ proxies: # socks5
# private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径
# alpn:
# - h3
# realm-opts:
# enable: true # 必须手动开启
# server-url: https://realm.hy2.io
# token: public
# realm-id: my-cabin-1f3a8c2e9b
# stun-servers:
# - stun.nextcloud.com:3478
# - stun.sip.us:3478
# - global.stun.twilio.com:3478
# # 下面支持填写针对server-url的TLS配置(sni, skip-cert-verify, fingerprint, certificate, private-key, alpn)
# # skip-cert-verify: false
# # ......
###quic-go特殊配置项,不要随意修改除非你知道你在干什么###
# initial-stream-receive-window: 8388608
# max-stream-receive-window: 8388608
@@ -1903,6 +1915,18 @@ listeners:
# masquerade: file:///var/www # 作为文件服务器
# masquerade: http://127.0.0.1:8080 #作为反向代理
# masquerade: https://127.0.0.1:8080 #作为反向代理
# realm-opts:
# enable: true # 必须手动开启
# server-url: https://realm.hy2.io
# token: public
# realm-id: my-cabin-1f3a8c2e9b
# stun-servers:
# - stun.nextcloud.com:3478
# - stun.sip.us:3478
# - global.stun.twilio.com:3478
# # 下面支持填写针对server-url的TLS配置(sni, skip-cert-verify, fingerprint, certificate, private-key, alpn)
# # skip-cert-verify: false
# # ......
- name: trusttunnel-in-1
type: trusttunnel
+1 -1
View File
@@ -30,7 +30,7 @@ require (
github.com/metacubex/restls-client-go v0.1.7
github.com/metacubex/sing v0.5.7
github.com/metacubex/sing-mux v0.3.9
github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a
github.com/metacubex/sing-quic v0.0.0-20260511111944-ed400da99ad4
github.com/metacubex/sing-shadowsocks v0.2.12
github.com/metacubex/sing-shadowsocks2 v0.2.7
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
+2 -2
View File
@@ -127,8 +127,8 @@ github.com/metacubex/sing v0.5.7 h1:8OC+fhKFSv/l9ehEhJRaZZAOuthfZo68SteBVLe8QqM=
github.com/metacubex/sing v0.5.7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.9 h1:/aoBD2+sK2qsXDlNDe3hkR0GZuFDtwIZhOeGUx9W0Yk=
github.com/metacubex/sing-mux v0.3.9/go.mod h1:8bT7ZKT3clRrJjYc/x5CRYibC1TX/bK73a3r3+2E+Fc=
github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a h1:977o0ZYYbiQAGuOxql7Q6UN3rEy59OyAE0tELq4gZfI=
github.com/metacubex/sing-quic v0.0.0-20260414034501-3ea3410d197a/go.mod h1:6ayFGfzzBE85csgQkM3gf4neFq6s0losHlPRSxY+nuk=
github.com/metacubex/sing-quic v0.0.0-20260511111944-ed400da99ad4 h1:WwMH5gADSmQ2RgudpoAym4nSk5U70QwfovgCqXBX34M=
github.com/metacubex/sing-quic v0.0.0-20260511111944-ed400da99ad4/go.mod h1:6ayFGfzzBE85csgQkM3gf4neFq6s0losHlPRSxY+nuk=
github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE=
github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU=
github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A=
+18
View File
@@ -28,6 +28,8 @@ type Hysteria2Server struct {
UdpMTU int `yaml:"udp-mtu" json:"udp-mtu,omitempty"`
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
RealmOpts Hysteria2RealmOption `yaml:"realm-opts" json:"realm-opts,omitempty"`
// quic-go special config
InitialStreamReceiveWindow uint64 `yaml:"initial-stream-receive-window" json:"initial-stream-receive-window,omitempty"`
MaxStreamReceiveWindow uint64 `yaml:"max-stream-receive-window" json:"max-stream-receive-window,omitempty"`
@@ -35,6 +37,22 @@ type Hysteria2Server struct {
MaxConnectionReceiveWindow uint64 `yaml:"max-connection-receive-window" json:"max-connection-receive-window,omitempty"`
}
type Hysteria2RealmOption struct {
Enable bool `yaml:"enable" json:"enable,omitempty"`
ServerURL string `yaml:"server-url" json:"server-url,omitempty"`
Token string `yaml:"token" json:"token,omitempty"`
RealmID string `yaml:"realm-id" json:"realm-id,omitempty"`
STUNServers []string `yaml:"stun-servers" json:"stun-servers,omitempty"`
// for ServerURL
SNI string `yaml:"sni" json:"sni,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify" json:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint" json:"fingerprint,omitempty"`
Certificate string `yaml:"certificate" json:"certificate,omitempty"`
PrivateKey string `yaml:"private-key" json:"private-key,omitempty"`
ALPN []string `yaml:"alpn" json:"alpn,omitempty"`
}
func (h Hysteria2Server) String() string {
b, _ := json.Marshal(h)
return string(b)
+35
View File
@@ -30,6 +30,8 @@ type Hysteria2Option struct {
UdpMTU int `inbound:"udp-mtu,omitempty"`
MuxOption MuxOption `inbound:"mux-option,omitempty"`
RealmOpts Hysteria2RealmOption `inbound:"realm-opts,omitempty"`
// quic-go special config
InitialStreamReceiveWindow uint64 `inbound:"initial-stream-receive-window,omitempty"`
MaxStreamReceiveWindow uint64 `inbound:"max-stream-receive-window,omitempty"`
@@ -37,6 +39,38 @@ type Hysteria2Option struct {
MaxConnectionReceiveWindow uint64 `inbound:"max-connection-receive-window,omitempty"`
}
type Hysteria2RealmOption struct {
Enable bool `inbound:"enable,omitempty"`
ServerURL string `inbound:"server-url,omitempty"`
Token string `inbound:"token,omitempty"`
RealmID string `inbound:"realm-id,omitempty"`
STUNServers []string `inbound:"stun-servers,omitempty"`
// for ServerURL
SNI string `inbound:"sni,omitempty"`
SkipCertVerify bool `inbound:"skip-cert-verify,omitempty"`
Fingerprint string `inbound:"fingerprint,omitempty"`
Certificate string `inbound:"certificate,omitempty"`
PrivateKey string `inbound:"private-key,omitempty"`
ALPN []string `inbound:"alpn,omitempty"`
}
func (o Hysteria2RealmOption) Build() LC.Hysteria2RealmOption {
return LC.Hysteria2RealmOption{
Enable: o.Enable,
ServerURL: o.ServerURL,
Token: o.Token,
RealmID: o.RealmID,
STUNServers: o.STUNServers,
SNI: o.SNI,
SkipCertVerify: o.SkipCertVerify,
Fingerprint: o.Fingerprint,
Certificate: o.Certificate,
PrivateKey: o.PrivateKey,
ALPN: o.ALPN,
}
}
func (o Hysteria2Option) Equal(config C.InboundConfig) bool {
return optionToString(o) == optionToString(config)
}
@@ -77,6 +111,7 @@ func NewHysteria2(options *Hysteria2Option) (*Hysteria2, error) {
BBRProfile: options.BBRProfile,
UdpMTU: options.UdpMTU,
MuxOption: options.MuxOption.Build(),
RealmOpts: options.RealmOpts.Build(),
// quic-go special config
InitialStreamReceiveWindow: options.InitialStreamReceiveWindow,
MaxStreamReceiveWindow: options.MaxStreamReceiveWindow,
+46
View File
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net"
"net/netip"
"net/url"
"strings"
"time"
@@ -14,6 +15,7 @@ import (
"github.com/metacubex/mihomo/common/sockopt"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/inner"
@@ -26,6 +28,7 @@ import (
"github.com/metacubex/http/httputil"
"github.com/metacubex/quic-go"
"github.com/metacubex/sing-quic/hysteria2"
"github.com/metacubex/sing-quic/hysteria2/realm"
E "github.com/metacubex/sing/common/exceptions"
"github.com/metacubex/tls"
)
@@ -146,6 +149,48 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi
return nil, E.New("unknown masquerade URL scheme: ", masqueradeURL.Scheme)
}
}
var realmOptions *realm.Options
if config.RealmOpts.Enable {
httpTLSClientConfig, err := ca.GetTLSConfig(ca.Option{
TLSConfig: &tls.Config{
ServerName: config.RealmOpts.SNI,
InsecureSkipVerify: config.RealmOpts.SkipCertVerify,
},
Fingerprint: config.RealmOpts.Fingerprint,
Certificate: config.RealmOpts.Certificate,
PrivateKey: config.RealmOpts.PrivateKey,
})
if err != nil {
return nil, err
}
realmOptions = &realm.Options{
ServerURL: config.RealmOpts.ServerURL,
Token: config.RealmOpts.Token,
RealmID: config.RealmOpts.RealmID,
STUNServers: config.RealmOpts.STUNServers,
HTTPClient: &http.Client{Transport: &http.Transport{
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return inner.HandleTcp(tunnel, address, "")
},
TLSClientConfig: httpTLSClientConfig,
// from http.DefaultTransport
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}},
Resolver: func(ctx context.Context, host string, ipv4, ipv6 bool) ([]netip.Addr, error) {
if ipv4 && !ipv6 {
return resolver.LookupIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver)
} else if ipv6 && !ipv4 {
return resolver.LookupIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver)
}
return resolver.LookupIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
},
Logger: log.SingLogger,
}
}
if config.UdpMTU == 0 {
// "1200" from quic-go's MaxDatagramSize
@@ -173,6 +218,7 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi
Handler: h,
MasqueradeHandler: masqueradeHandler,
UdpMTU: config.UdpMTU,
RealmOptions: realmOptions,
SetBBRCongestion: func(quicConn *quic.Conn) {
common.SetCongestionController(quicConn, "bbr", config.CWND, config.BBRProfile)
},