From f64453041a6d0950dee684745fcaced104a0879a Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Wed, 2 Sep 2026 11:45:11 -0700 Subject: [PATCH] fix: preserve per-inbound WireGuard peer addresses (#6344) Clients are stored once per email in the client table, so when the same email exists on more than one WireGuard inbound the shared record's AllowedIPs and PreSharedKey win for every inbound. A client present on both a WG and an AWG tunnel was emitted with one tunnel's address on both, so the second tunnel's peer got the wrong allowedIPs. Read the per-inbound client settings for WireGuard inbounds and, when the inbound carries its own entry for that email, use its AllowedIPs and PreSharedKey when building the peer. --- internal/web/service/xray.go | 14 +++ .../web/service/xray_wireguard_config_test.go | 86 +++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/internal/web/service/xray.go b/internal/web/service/xray.go index ecb1d9060..3a94b2b53 100644 --- a/internal/web/service/xray.go +++ b/internal/web/service/xray.go @@ -179,6 +179,16 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) { } settings := map[string]any{} _ = json.Unmarshal([]byte(inbound.Settings), &settings) + var wireguardClientsByEmail map[string]model.Client + if inbound.Protocol == model.WireGuard { + inboundClients, _ := ParseInboundSettingsClients(inbound.Settings) + if len(inboundClients) > 0 { + wireguardClientsByEmail = make(map[string]model.Client, len(inboundClients)) + for _, client := range inboundClients { + wireguardClientsByEmail[strings.ToLower(strings.TrimSpace(client.Email))] = client + } + } + } dbClients, listErr := s.inboundService.clientService.ListForInbound(nil, inbound.Id) if listErr != nil { @@ -244,6 +254,10 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) { entry["auth"] = c.Auth } case model.WireGuard: + if inboundClient, ok := wireguardClientsByEmail[strings.ToLower(strings.TrimSpace(c.Email))]; ok { + c.AllowedIPs = inboundClient.AllowedIPs + c.PreSharedKey = inboundClient.PreSharedKey + } wgPeers = append(wgPeers, model.WireguardPeerFromClient(c)) continue } diff --git a/internal/web/service/xray_wireguard_config_test.go b/internal/web/service/xray_wireguard_config_test.go index 1cc5922ff..dc19504a3 100644 --- a/internal/web/service/xray_wireguard_config_test.go +++ b/internal/web/service/xray_wireguard_config_test.go @@ -55,6 +55,59 @@ func seedWGInbound(t *testing.T, tag string, port int, clients []model.Client) { } } +func seedDualTunnelClient(t *testing.T, enabled bool) string { + t.Helper() + setupSettingTestDB(t) + db := database.GetDB() + + const email = "dual@wg.test" + wgClient := model.Client{ + Email: email, + Enable: true, + PublicKey: "pub-dual", + AllowedIPs: []string{"10.0.0.5/32"}, + PreSharedKey: "wg-psk", + } + awgClient := wgClient + awgClient.AllowedIPs = []string{"10.8.1.5/32"} + awgClient.PreSharedKey = "awg-psk" + + wgSettings, err := json.Marshal(map[string]any{ + "secretKey": wgTestSecretKey(), + "mtu": 1420, + "clients": []model.Client{wgClient}, + }) + if err != nil { + t.Fatalf("marshal wg settings: %v", err) + } + awgSettings, err := json.Marshal(map[string]any{ + "server": map[string]any{"subnetIp": "10.8.1.0", "subnetCidr": 24}, + "clients": []model.Client{awgClient}, + }) + if err != nil { + t.Fatalf("marshal awg settings: %v", err) + } + + wgInbound := &model.Inbound{Tag: "wg-dual", Enable: true, Port: 51823, Protocol: model.WireGuard, Settings: string(wgSettings)} + awgInbound := &model.Inbound{Tag: "awg-dual", Enable: true, Port: 51824, Protocol: model.AmneziaWG, Settings: string(awgSettings)} + if err := db.Create(wgInbound).Error; err != nil { + t.Fatalf("create wg inbound: %v", err) + } + if err := db.Create(awgInbound).Error; err != nil { + t.Fatalf("create awg inbound: %v", err) + } + + svc := ClientService{} + if err := svc.SyncInbound(nil, wgInbound.Id, []model.Client{wgClient}); err != nil { + t.Fatalf("SyncInbound(wg): %v", err) + } + awgClient.Enable = enabled + if err := svc.SyncInbound(nil, awgInbound.Id, []model.Client{awgClient}); err != nil { + t.Fatalf("SyncInbound(awg): %v", err) + } + return email +} + func wgPeerList(t *testing.T, settings map[string]any) []map[string]any { t.Helper() if _, ok := settings["clients"]; ok { @@ -137,6 +190,39 @@ func TestGetXrayConfigWireGuardDisabledClientExcluded(t *testing.T) { } } +func TestGetXrayConfigWireGuardUsesInboundLocalTunnelFields(t *testing.T) { + email := seedDualTunnelClient(t, true) + + var shared model.ClientRecord + if err := database.GetDB().Where("email = ?", email).First(&shared).Error; err != nil { + t.Fatalf("read shared client: %v", err) + } + if shared.AllowedIPs != "10.8.1.5/32" || shared.PreSharedKey != "awg-psk" { + t.Fatalf("test setup did not persist AmneziaWG last: allowedIPs=%q preSharedKey=%q", shared.AllowedIPs, shared.PreSharedKey) + } + + peers := wgPeerList(t, wgInboundEmittedSettings(t, "wg-dual")) + if len(peers) != 1 { + t.Fatalf("expected 1 peer, got %d: %v", len(peers), peers) + } + allowed, ok := peers[0]["allowedIPs"].([]any) + if !ok || len(allowed) != 1 || allowed[0] != "10.0.0.5/32" { + t.Fatalf("WireGuard peer allowedIPs = %v, want [10.0.0.5/32]", peers[0]["allowedIPs"]) + } + if peers[0]["preSharedKey"] != "wg-psk" { + t.Fatalf("WireGuard peer preSharedKey = %v, want wg-psk", peers[0]["preSharedKey"]) + } +} + +func TestGetXrayConfigWireGuardDisabledDualProtocolClientExcluded(t *testing.T) { + seedDualTunnelClient(t, false) + + peers := wgPeerList(t, wgInboundEmittedSettings(t, "wg-dual")) + if len(peers) != 0 { + t.Fatalf("expected disabled dual-protocol client to be excluded, got %v", peers) + } +} + func TestGetXrayConfigWireGuardNoClientsEmitsEmptyPeers(t *testing.T) { seedWGInbound(t, "wg-empty", 51822, nil)