fix(sub): include native WireGuard clients in Clash and JSON subscriptions (#5676)

The Clash (buildProxy) and JSON (getConfig) subscription generators had no
WireGuard branch, so a native WireGuard inbound's clients were silently
dropped: buildProxy hit its default nil case, and getConfig emitted a config
with no proxy outbound. Only the raw subscription (genWireguardLink) and
external-link Clash path handled WireGuard.

Add a WireGuard case to both generators, mirroring genWireguardLink: the peer
public key is derived from the inbound secretKey, while the private key, tunnel
address (mihomo ip/ipv6, Xray settings.address), pre-shared key and keep-alive
come from the client. The peer routes the full tunnel (0.0.0.0/0, ::/0), which
both mihomo and Xray also default to.

Field names verified against the mihomo WireGuardOption source (private-key,
public-key, pre-shared-key, persistent-keepalive, ip, ipv6, mtu, dns) and the
Xray wireguard outbound schema (secretKey, address, peers[].publicKey/endpoint/
preSharedKey/keepAlive/allowedIPs, mtu).
This commit is contained in:
Grigoriy
2026-07-09 01:52:46 +03:00
committed by GitHub
parent cb5b3a803a
commit d2efe9b022
4 changed files with 275 additions and 0 deletions
+65
View File
@@ -9,6 +9,7 @@ import (
yaml "github.com/goccy/go-yaml"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
)
type SubClashService struct {
@@ -214,6 +215,9 @@ func (s *SubClashService) buildProxy(subReq *SubService, inbound *model.Inbound,
if inbound.Protocol == model.Hysteria {
return s.buildHysteriaProxy(subReq, inbound, client, ep)
}
if inbound.Protocol == model.WireGuard {
return s.buildWireguardProxy(subReq, inbound, client, ep)
}
network, _ := stream["network"].(string)
@@ -361,6 +365,67 @@ func (s *SubClashService) buildHysteriaProxy(subReq *SubService, inbound *model.
return proxy
}
// buildWireguardProxy produces a mihomo-compatible Clash entry for a native
// WireGuard inbound, mirroring genWireguardLink: the peer public key is derived
// from the inbound secretKey, while the private key, tunnel address, and
// pre-shared key come from the client. Returns nil when the client has no key.
func (s *SubClashService) buildWireguardProxy(subReq *SubService, inbound *model.Inbound, client model.Client, ep map[string]any) map[string]any {
if client.PrivateKey == "" {
return nil
}
var inboundSettings map[string]any
_ = json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
secretKey, _ := inboundSettings["secretKey"].(string)
proxy := map[string]any{
"name": subReq.endpointRemark(inbound, client.Email, ep, ""),
"type": "wireguard",
"server": inbound.Listen,
"port": inbound.Port,
"udp": true,
"private-key": client.PrivateKey,
}
if secretKey != "" {
if pub, err := wgutil.PublicKeyFromPrivate(secretKey); err == nil {
proxy["public-key"] = pub
}
}
if client.PreSharedKey != "" {
proxy["pre-shared-key"] = client.PreSharedKey
}
if client.KeepAlive > 0 {
proxy["persistent-keepalive"] = client.KeepAlive
}
for _, addr := range client.AllowedIPs {
ip := stripCIDR(addr)
if ip == "" {
continue
}
if strings.Contains(ip, ":") {
proxy["ipv6"] = ip
} else {
proxy["ip"] = ip
}
}
if mtu, ok := inboundSettings["mtu"].(float64); ok && mtu > 0 {
proxy["mtu"] = int(mtu)
}
if dns, _ := inboundSettings["dns"].(string); dns != "" {
servers := make([]string, 0)
for _, server := range strings.Split(dns, ",") {
if server = strings.TrimSpace(server); server != "" {
servers = append(servers, server)
}
}
if len(servers) > 0 {
proxy["dns"] = servers
}
}
return proxy
}
// buildXhttpClashOpts converts xhttpSettings from 3x-ui's camelCase JSON
// storage into the kebab-case map that Mihomo expects under xhttp-opts.
//