From 246d9207a5ca62127a40eb571d3eceaa38f1c4f8 Mon Sep 17 00:00:00 2001 From: Sanaei Date: Wed, 9 Sep 2026 00:17:45 +0200 Subject: [PATCH] fix(sub): emit a bare host in Clash proxies A Clash "server" is a bare host, not a URI authority, but the custom share address strategy stores an IPv6 literal with brackets so the address normalizer can hand it to the raw link generators. The Clash renderer copied that value into every proxy verbatim, so mihomo received server: "[2001:db8::1]" and failed to parse the node. Raw links were unaffected because joinHostPort strips the brackets and re-adds exactly one. Strip them once where the renderer takes the resolved dest, which is the single place all three proxy builders read the address from. Closes #6373 --- internal/sub/clash_service.go | 5 ++++- internal/sub/clash_service_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/internal/sub/clash_service.go b/internal/sub/clash_service.go index 410dd2691..df52ab9bf 100644 --- a/internal/sub/clash_service.go +++ b/internal/sub/clash_service.go @@ -234,7 +234,10 @@ func (s *SubClashService) getProxies(subReq *SubService, inbound *model.Inbound, // the synthetic/legacy entry) before it becomes the proxy name. subReq.renderHostRemark(inbound, client, extPrxy, network) workingInbound := *inbound - workingInbound.Listen, _ = extPrxy["dest"].(string) + // A Clash "server" is a bare host, not a URI authority, and the custom + // share address stores IPv6 literals bracketed. + dest, _ := extPrxy["dest"].(string) + workingInbound.Listen = strings.Trim(dest, "[]") if port, ok := extPrxy["port"].(float64); ok { workingInbound.Port = int(port) } diff --git a/internal/sub/clash_service_test.go b/internal/sub/clash_service_test.go index 1147fea4c..e785e7164 100644 --- a/internal/sub/clash_service_test.go +++ b/internal/sub/clash_service_test.go @@ -883,3 +883,28 @@ func TestBuildWireguardProxyForClashNoKey(t *testing.T) { t.Fatalf("buildProxy = %v, want nil for a keyless wireguard client", proxy) } } + +// TestGetProxies_CustomIPv6ShareAddrIsUnbracketed pins that a Clash "server" is a +// bare host: the custom share address stores IPv6 literals bracketed, and mihomo +// rejects "[2001:db8::1]" there. +func TestGetProxies_CustomIPv6ShareAddrIsUnbracketed(t *testing.T) { + svc := &SubClashService{SubService: &SubService{}} + inbound := &model.Inbound{ + Protocol: model.VLESS, + Port: 443, + Remark: "r", + Settings: `{"encryption":"none"}`, + StreamSettings: `{"network":"tcp","security":"none"}`, + ShareAddrStrategy: "custom", + ShareAddr: "[2001:db8::1]", + } + client := model.Client{ID: "11111111-2222-4333-8444-555555555555", Email: "a@example.com"} + + proxies := svc.getProxies(svc.SubService, inbound, client, "panel.example.com") + if len(proxies) != 1 { + t.Fatalf("getProxies returned %d proxies, want 1", len(proxies)) + } + if got := proxies[0]["server"]; got != "2001:db8::1" { + t.Fatalf("server = %v, want 2001:db8::1", got) + } +}