fix(wireguard): check every line of generated client configs

The WireGuard and AmneziaWG .conf builders (subscription service, clients
page, inbound modal and subscription page) checked only a few of the values
they write. Each builder now assembles the file line by line and renders
nothing when a line holds a control character, so every value is covered
whatever stored it. A decoded vpn:// payload is refused when it carries a
wg-quick hook directive, which no panel builder writes, and the subscription
page hides a config block that rendered nothing.

The inbound form now validates a WireGuard/AmneziaWG client's allowedIPs on
create and on update, including the stored clients a protocol switch keeps,
as the client endpoints already did.
This commit is contained in:
MHSanaei
2026-10-24 01:10:09 +02:00
parent 57fd61790c
commit 0faad4f4f3
12 changed files with 285 additions and 222 deletions
@@ -2,6 +2,7 @@ package service
import (
"encoding/json"
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
@@ -149,6 +150,29 @@ func TestAddInbound_AcceptsWireguardClientWithKey(t *testing.T) {
}
}
// The client endpoints already refuse a non-prefix allowedIPs entry; the inbound
// form stored it verbatim, and a line break in it reaches every rendered .conf.
func TestAddInbound_RejectsWireguardClientAllowedIPsThatIsNotAPrefix(t *testing.T) {
setupConflictDB(t)
svc := &InboundService{}
settings := `{"secretKey":"` + wgTestSecretKey() + `","mtu":1420,"clients":[{"email":"wgbad@x","enable":true,"privateKey":"keep-priv","publicKey":"keep-pub",` +
`"allowedIPs":["10.0.0.51/32\n[Interface]\nPostUp = touch /tmp/pwned"]}]}`
in := &model.Inbound{Tag: "in-45201-wg", Enable: true, Listen: "0.0.0.0", Port: 45201, Protocol: model.WireGuard, Settings: settings}
_, _, err := svc.AddInbound(in)
if err == nil || !strings.Contains(err.Error(), "invalid allowedIPs entry") {
t.Fatalf("AddInbound err = %v, want an invalid allowedIPs entry error", err)
}
var count int64
if err := database.GetDB().Model(&model.Inbound{}).Where("tag = ?", "in-45201-wg").Count(&count).Error; err != nil {
t.Fatalf("count: %v", err)
}
if count != 0 {
t.Fatalf("rejected WireGuard inbound was persisted anyway, row count = %d", count)
}
}
// end-to-end: same guard on the update path, on a row that was valid before
// the edit — the rejected StreamSettings must not overwrite the stored row.
func TestUpdateInbound_RejectsFinalMaskRealityCombo(t *testing.T) {