mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-04-26 01:15:57 +00:00
* Fix Hysteria External Proxy + include Hysteria in Clash subscription (#4053) Two related gaps on the Hysteria side of the subscription layer: 1) `genHysteriaLink` ignored `externalProxy` entirely, so an admin who pointed a Hysteria inbound at an alternate endpoint (e.g. a CDN hostname forwarding UDP back to the node) still got a link with the original server address. Mirror what `genVlessLink` / `genTrojanLink` already do: fan out one link per entry, substituting `dest` / `port` and picking up the entry's remark suffix. As a bonus, the salamander obfs password is now copied into the URL too — the panel-side link generator already did this, so the subscription output was lagging behind it. 2) `buildProxy` in `subClashService.go` had a protocol switch with cases for VMESS / VLESS / Trojan / Shadowsocks and a `default: return nil`. Hysteria inbounds fell into the default branch and silently vanished from the Clash YAML. Route Hysteria to a dedicated `buildHysteriaProxy` helper before the transport/security helpers run (applyTransport / applySecurity model xray streams, which Hysteria doesn't use). `buildHysteriaProxy` reads `inbound.StreamSettings` directly instead of going through `streamData` / `tlsData`, because those prune fields (`allowInsecure`, the salamander `finalmask.udp` block) that the mihomo Hysteria proxy wants preserved. Output shape matches mihomo's expectations: type: hysteria2 # or "hysteria" for v1 password / auth-str: <client auth> sni, alpn, skip-cert-verify, client-fingerprint obfs: salamander obfs-password: <finalmask.udp[salamander].settings.password> The existing `getProxies` fanout over `externalProxy` already plugs in for Clash, so with Hysteria now recognised, External Proxy entries also flow through to the Clash output for Hysteria inbounds. Closes #4053 * gofmt: align map keys in buildHysteriaProxy --------- Co-authored-by: pwnnex <eternxles@gmail.com>
This commit is contained in:
parent
292eb992f4
commit
9611c9def6
2 changed files with 135 additions and 3 deletions
|
|
@ -159,6 +159,16 @@ func (s *SubClashService) getProxies(inbound *model.Inbound, client model.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubClashService) buildProxy(inbound *model.Inbound, client model.Client, stream map[string]any, extraRemark string) map[string]any {
|
func (s *SubClashService) buildProxy(inbound *model.Inbound, client model.Client, stream map[string]any, extraRemark string) map[string]any {
|
||||||
|
// Hysteria (v1 / v2) doesn't ride an xray `streamSettings.network`
|
||||||
|
// transport and the TLS story is handled inside hysteria itself, so
|
||||||
|
// applyTransport / applySecurity below don't model it. Build the
|
||||||
|
// proxy directly. Without this, hysteria inbounds fell into the
|
||||||
|
// `default: return nil` branch and silently vanished from the
|
||||||
|
// generated Clash config.
|
||||||
|
if inbound.Protocol == model.Hysteria {
|
||||||
|
return s.buildHysteriaProxy(inbound, client, extraRemark)
|
||||||
|
}
|
||||||
|
|
||||||
proxy := map[string]any{
|
proxy := map[string]any{
|
||||||
"name": s.SubService.genRemark(inbound, client.Email, extraRemark),
|
"name": s.SubService.genRemark(inbound, client.Email, extraRemark),
|
||||||
"server": inbound.Listen,
|
"server": inbound.Listen,
|
||||||
|
|
@ -222,6 +232,82 @@ func (s *SubClashService) buildProxy(inbound *model.Inbound, client model.Client
|
||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildHysteriaProxy produces a mihomo-compatible Clash entry for a
|
||||||
|
// Hysteria (v1) or Hysteria2 inbound. It reads `inbound.StreamSettings`
|
||||||
|
// directly instead of going through streamData/tlsData, because those
|
||||||
|
// helpers prune fields (like `allowInsecure` / the salamander obfs
|
||||||
|
// block) that the hysteria proxy wants preserved.
|
||||||
|
func (s *SubClashService) buildHysteriaProxy(inbound *model.Inbound, client model.Client, extraRemark string) map[string]any {
|
||||||
|
var inboundSettings map[string]any
|
||||||
|
_ = json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
|
||||||
|
|
||||||
|
proxyType := "hysteria2"
|
||||||
|
authKey := "password"
|
||||||
|
if v, ok := inboundSettings["version"].(float64); ok && int(v) == 1 {
|
||||||
|
proxyType = "hysteria"
|
||||||
|
authKey = "auth-str"
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := map[string]any{
|
||||||
|
"name": s.SubService.genRemark(inbound, client.Email, extraRemark),
|
||||||
|
"type": proxyType,
|
||||||
|
"server": inbound.Listen,
|
||||||
|
"port": inbound.Port,
|
||||||
|
"udp": true,
|
||||||
|
authKey: client.Auth,
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawStream map[string]any
|
||||||
|
_ = json.Unmarshal([]byte(inbound.StreamSettings), &rawStream)
|
||||||
|
|
||||||
|
// TLS details — hysteria always uses TLS.
|
||||||
|
if tlsSettings, ok := rawStream["tlsSettings"].(map[string]any); ok {
|
||||||
|
if serverName, ok := tlsSettings["serverName"].(string); ok && serverName != "" {
|
||||||
|
proxy["sni"] = serverName
|
||||||
|
}
|
||||||
|
if alpnList, ok := tlsSettings["alpn"].([]any); ok && len(alpnList) > 0 {
|
||||||
|
out := make([]string, 0, len(alpnList))
|
||||||
|
for _, a := range alpnList {
|
||||||
|
if s, ok := a.(string); ok && s != "" {
|
||||||
|
out = append(out, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(out) > 0 {
|
||||||
|
proxy["alpn"] = out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inner, ok := tlsSettings["settings"].(map[string]any); ok {
|
||||||
|
if insecure, ok := inner["allowInsecure"].(bool); ok && insecure {
|
||||||
|
proxy["skip-cert-verify"] = true
|
||||||
|
}
|
||||||
|
if fp, ok := inner["fingerprint"].(string); ok && fp != "" {
|
||||||
|
proxy["client-fingerprint"] = fp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salamander obfs (Hysteria2). Read the same finalmask.udp[salamander]
|
||||||
|
// block the subscription link generator uses.
|
||||||
|
if finalmask, ok := rawStream["finalmask"].(map[string]any); ok {
|
||||||
|
if udpMasks, ok := finalmask["udp"].([]any); ok {
|
||||||
|
for _, m := range udpMasks {
|
||||||
|
mask, _ := m.(map[string]any)
|
||||||
|
if mask == nil || mask["type"] != "salamander" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
settings, _ := mask["settings"].(map[string]any)
|
||||||
|
if pw, ok := settings["password"].(string); ok && pw != "" {
|
||||||
|
proxy["obfs"] = "salamander"
|
||||||
|
proxy["obfs-password"] = pw
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SubClashService) applyTransport(proxy map[string]any, network string, stream map[string]any) bool {
|
func (s *SubClashService) applyTransport(proxy map[string]any, network string, stream map[string]any) bool {
|
||||||
switch network {
|
switch network {
|
||||||
case "", "tcp":
|
case "", "tcp":
|
||||||
|
|
|
||||||
|
|
@ -906,7 +906,6 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) string {
|
||||||
address := s.address
|
|
||||||
if inbound.Protocol != model.Hysteria {
|
if inbound.Protocol != model.Hysteria {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -921,7 +920,6 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
auth := clients[clientIndex].Auth
|
auth := clients[clientIndex].Auth
|
||||||
port := inbound.Port
|
|
||||||
params := make(map[string]string)
|
params := make(map[string]string)
|
||||||
|
|
||||||
params["security"] = "tls"
|
params["security"] = "tls"
|
||||||
|
|
@ -950,6 +948,26 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// salamander obfs (Hysteria2). The panel-side link generator already
|
||||||
|
// emits these; keep the subscription output in sync so a client has
|
||||||
|
// the obfs password to match the server.
|
||||||
|
if finalmask, ok := stream["finalmask"].(map[string]interface{}); ok {
|
||||||
|
if udpMasks, ok := finalmask["udp"].([]interface{}); ok {
|
||||||
|
for _, m := range udpMasks {
|
||||||
|
mask, _ := m.(map[string]interface{})
|
||||||
|
if mask == nil || mask["type"] != "salamander" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
settings, _ := mask["settings"].(map[string]interface{})
|
||||||
|
if pw, ok := settings["password"].(string); ok && pw != "" {
|
||||||
|
params["obfs"] = "salamander"
|
||||||
|
params["obfs-password"] = pw
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var settings map[string]interface{}
|
var settings map[string]interface{}
|
||||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||||
version, _ := settings["version"].(float64)
|
version, _ := settings["version"].(float64)
|
||||||
|
|
@ -958,7 +976,35 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin
|
||||||
protocol = "hysteria"
|
protocol = "hysteria"
|
||||||
}
|
}
|
||||||
|
|
||||||
link := fmt.Sprintf("%s://%s@%s:%d", protocol, auth, address, port)
|
// Fan out one link per External Proxy entry if any. Previously this
|
||||||
|
// generator ignored `externalProxy` entirely, so the link kept the
|
||||||
|
// server's own IP/port even when the admin configured an alternate
|
||||||
|
// endpoint (e.g. a CDN hostname + port that forwards to the node).
|
||||||
|
// Matches the behaviour of genVlessLink / genTrojanLink / ….
|
||||||
|
externalProxies, _ := stream["externalProxy"].([]interface{})
|
||||||
|
if len(externalProxies) > 0 {
|
||||||
|
links := make([]string, 0, len(externalProxies))
|
||||||
|
for _, externalProxy := range externalProxies {
|
||||||
|
ep, _ := externalProxy.(map[string]interface{})
|
||||||
|
dest, _ := ep["dest"].(string)
|
||||||
|
epPort := int(ep["port"].(float64))
|
||||||
|
epRemark, _ := ep["remark"].(string)
|
||||||
|
|
||||||
|
link := fmt.Sprintf("%s://%s@%s:%d", protocol, auth, dest, epPort)
|
||||||
|
u, _ := url.Parse(link)
|
||||||
|
q := u.Query()
|
||||||
|
for k, v := range params {
|
||||||
|
q.Add(k, v)
|
||||||
|
}
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
u.Fragment = s.genRemark(inbound, email, epRemark)
|
||||||
|
links = append(links, u.String())
|
||||||
|
}
|
||||||
|
return strings.Join(links, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// No external proxy configured — fall back to the request host.
|
||||||
|
link := fmt.Sprintf("%s://%s@%s:%d", protocol, auth, s.address, inbound.Port)
|
||||||
url, _ := url.Parse(link)
|
url, _ := url.Parse(link)
|
||||||
q := url.Query()
|
q := url.Query()
|
||||||
for k, v := range params {
|
for k, v := range params {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue