From c80e5e276b8e393616ebcec8c6dbb606dcadcc16 Mon Sep 17 00:00:00 2001 From: H-TTTTT <36735327+H-TTTTT@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:50:16 +0800 Subject: [PATCH] fix(wireguard): preserve all Allowed IPs in share link, .conf, and subscription (#6051) The WireGuard client address is stored end-to-end as a string slice (model.AllowedIPs []string, comma-split/joined in the DB), and the UI hint documents comma-separated multi-value input. Three export paths only read index [0], silently dropping every address after the first (e.g. a dual-stack IPv4+IPv6 client never receives its second address): - genWireguardLink share link address param (inbound-link.ts) - genWireguardConfig .conf Address line (inbound-link.ts) - SubService.genWireguardLink raw subscription address (service.go) The sibling JSON and Clash subscriptions already emit the full slice, and WireguardPeerFromClient passes the whole slice into the real Xray peer config, so only the exported text was wrong. Join all entries, matching the model's strings.Join(..., ",") convention for the link params and the ', '-joined AllowedIPs line already used a few lines below in genWireguardConfig. Fixes #6031 --- frontend/src/lib/xray/inbound-link.ts | 6 ++--- frontend/src/test/inbound-link.test.ts | 37 ++++++++++++++++++++++++++ internal/sub/service.go | 4 +-- internal/sub/service_wireguard_test.go | 30 +++++++++++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/xray/inbound-link.ts b/frontend/src/lib/xray/inbound-link.ts index c7509f4b4..005edd075 100644 --- a/frontend/src/lib/xray/inbound-link.ts +++ b/frontend/src/lib/xray/inbound-link.ts @@ -822,8 +822,8 @@ export function genWireguardLink(input: GenWireguardLinkInput): string { ? Wireguard.generateKeypair(settings.secretKey).publicKey : ''; if (pubKey.length > 0) url.searchParams.set('publickey', pubKey); - if (peer.allowedIPs.length > 0 && peer.allowedIPs[0]) { - url.searchParams.set('address', peer.allowedIPs[0]); + if (peer.allowedIPs.length > 0) { + url.searchParams.set('address', peer.allowedIPs.join(',')); } if (typeof settings.mtu === 'number' && settings.mtu > 0) { url.searchParams.set('mtu', String(settings.mtu)); @@ -850,7 +850,7 @@ export function genWireguardConfig(input: GenWireguardLinkInput): string { let txt = `[Interface]\n`; txt += `PrivateKey = ${peer.privateKey ?? ''}\n`; - txt += `Address = ${peer.allowedIPs[0] ?? ''}\n`; + txt += `Address = ${peer.allowedIPs.join(', ')}\n`; txt += `DNS = ${settings.dns || '1.1.1.1, 1.0.0.1'}\n`; if (typeof settings.mtu === 'number' && settings.mtu > 0) { txt += `MTU = ${settings.mtu}\n`; diff --git a/frontend/src/test/inbound-link.test.ts b/frontend/src/test/inbound-link.test.ts index 4935b2b77..97f56f046 100644 --- a/frontend/src/test/inbound-link.test.ts +++ b/frontend/src/test/inbound-link.test.ts @@ -308,6 +308,43 @@ describe('genWireguardLink + genWireguardConfig', () => { } }); +describe('genWireguardLink + genWireguardConfig multi allowedIPs', () => { + const settings = { + secretKey: '', + mtu: 1280, + dns: '', + peers: [ + { + privateKey: 'cLI', + allowedIPs: ['10.0.0.2/32', 'fd00::2/128'], + }, + ], + } as unknown as WireguardInboundSettings; + + it('joins every allowed IP into the share-link address param', () => { + const link = genWireguardLink({ + settings, + address: 'wg.example.test', + port: 51820, + remark: 'dual-stack', + peerIndex: 0, + }); + const u = new URL(link); + expect(u.searchParams.get('address')).toBe('10.0.0.2/32,fd00::2/128'); + }); + + it('joins every allowed IP into the .conf Address line', () => { + const config = genWireguardConfig({ + settings, + address: 'wg.example.test', + port: 51820, + remark: 'dual-stack', + peerIndex: 0, + }); + expect(config).toContain('Address = 10.0.0.2/32, fd00::2/128\n'); + }); +}); + describe('resolveAddr precedence', () => { const baseInbound = { listen: '', diff --git a/internal/sub/service.go b/internal/sub/service.go index 7a8712b8d..6bc436601 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -644,8 +644,8 @@ func (s *SubService) genWireguardLink(inbound *model.Inbound, email string) stri params["publickey"] = pub } } - if len(client.AllowedIPs) > 0 && client.AllowedIPs[0] != "" { - params["address"] = client.AllowedIPs[0] + if joined := strings.Join(client.AllowedIPs, ","); joined != "" { + params["address"] = joined } if mtu, ok := settings["mtu"].(float64); ok && mtu > 0 { params["mtu"] = strconv.Itoa(int(mtu)) diff --git a/internal/sub/service_wireguard_test.go b/internal/sub/service_wireguard_test.go index 582f356a3..a071e3ec9 100644 --- a/internal/sub/service_wireguard_test.go +++ b/internal/sub/service_wireguard_test.go @@ -55,6 +55,36 @@ func TestGenWireguardLinkFields(t *testing.T) { } } +func TestGenWireguardLinkMultiAllowedIPs(t *testing.T) { + serverPriv, _, err := wgutil.GenerateWireguardKeypair() + if err != nil { + t.Fatalf("keypair: %v", err) + } + clientPriv, _, err := wgutil.GenerateWireguardKeypair() + if err != nil { + t.Fatalf("client keypair: %v", err) + } + + inbound := &model.Inbound{ + Listen: "203.0.113.7", + Port: 51820, + Protocol: model.WireGuard, + Remark: "wg-sub", + Settings: `{"secretKey":"` + serverPriv + `","clients":[{"email":"user","privateKey":"` + clientPriv + `","allowedIPs":["10.0.0.2/32","fd00::2/128"]}]}`, + } + + s := &SubService{} + link := s.genWireguardLink(inbound, "user") + + u, err := url.Parse(link) + if err != nil { + t.Fatalf("link does not parse: %v\n got: %s", err, link) + } + if got, want := u.Query().Get("address"), "10.0.0.2/32,fd00::2/128"; got != want { + t.Fatalf("address = %q, want %q (all allowed IPs joined, not just the first)", got, want) + } +} + func TestGenWireguardLinkWrongProtocol(t *testing.T) { s := &SubService{} vless := &model.Inbound{Protocol: model.VLESS, Settings: `{"clients":[{"email":"user"}]}`}