Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpwnnex <pwnnex@proton.me>2026-04-22 11:01:21 +0300
committerGitHub <noreply@github.com>2026-04-22 11:01:21 +0300
commit9611c9def67ca8daa81f4b220ae7b9cec729ed21 (patch)
tree4321b4497e891544c7a9ffb92094424b0650e4e0 /sub/subClashService.go
parent292eb992f46830ef03c54d280b2031f03fe2eb4f (diff)
Fix Hysteria External Proxy + include Hysteria in Clash subscription (#4053) (#4073)
* 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>
Diffstat (limited to 'sub/subClashService.go')
-rw-r--r--sub/subClashService.go86
1 files changed, 86 insertions, 0 deletions
diff --git a/sub/subClashService.go b/sub/subClashService.go
index f0a8ca8a..d0445aa4 100644
--- a/sub/subClashService.go
+++ b/sub/subClashService.go
@@ -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 {
+ // 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{
"name": s.SubService.genRemark(inbound, client.Email, extraRemark),
"server": inbound.Listen,
@@ -222,6 +232,82 @@ func (s *SubClashService) buildProxy(inbound *model.Inbound, client model.Client
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 {
switch network {
case "", "tcp":