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
+56
View File
@@ -10,6 +10,7 @@ import (
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
"github.com/mhsanaei/3x-ui/v3/internal/util/random"
wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
)
//go:embed default.json
@@ -217,6 +218,12 @@ func (s *SubJsonService) getConfig(subReq *SubService, inbound *model.Inbound, c
newOutbounds = append(newOutbounds, s.genServer(subReq, inbound, streamSettings, client, jsonMux(mux, hostMux)))
case "hysteria":
newOutbounds = append(newOutbounds, s.genHy(inbound, newStream, client, jsonMux(mux, hostMux)))
case "wireguard":
wgOutbound := s.genWireguard(inbound, client)
if wgOutbound == nil {
continue
}
newOutbounds = append(newOutbounds, wgOutbound)
}
newOutbounds = append(newOutbounds, s.defaultOutbounds...)
@@ -519,6 +526,55 @@ func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any,
return result
}
// genWireguard builds an Xray wireguard outbound for a native WireGuard inbound,
// mirroring genWireguardLink: the peer public key is derived from the inbound
// secretKey, the client owns the private key / tunnel address / pre-shared key,
// and the peer routes the full tunnel. Returns nil when the client has no key.
func (s *SubJsonService) genWireguard(inbound *model.Inbound, client model.Client) json_util.RawMessage {
if client.PrivateKey == "" {
return nil
}
var inboundSettings map[string]any
_ = json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
secretKey, _ := inboundSettings["secretKey"].(string)
peer := map[string]any{
"endpoint": joinHostPort(inbound.Listen, inbound.Port),
"allowedIPs": []string{"0.0.0.0/0", "::/0"},
}
if secretKey != "" {
if pub, err := wgutil.PublicKeyFromPrivate(secretKey); err == nil {
peer["publicKey"] = pub
}
}
if client.PreSharedKey != "" {
peer["preSharedKey"] = client.PreSharedKey
}
if client.KeepAlive > 0 {
peer["keepAlive"] = client.KeepAlive
}
settings := map[string]any{
"secretKey": client.PrivateKey,
"peers": []any{peer},
}
if len(client.AllowedIPs) > 0 {
settings["address"] = client.AllowedIPs
}
if mtu, ok := inboundSettings["mtu"].(float64); ok && mtu > 0 {
settings["mtu"] = int(mtu)
}
outbound := map[string]any{
"protocol": string(inbound.Protocol),
"tag": "proxy",
"settings": settings,
}
result, _ := json.MarshalIndent(outbound, "", " ")
return result
}
func mergeFinalMask(base any, extra map[string]any) map[string]any {
merged := map[string]any{}
if baseMap, ok := base.(map[string]any); ok {