Files
3x-ui/internal/sub/service_amneziawg_test.go
T
Kuzz007 70d80fbe8d fix(amneziawg): use the real vpn:// share-link scheme
The AmneziaWG share link (both the panel's per-client copy-link/QR and
the subscription endpoint) used an invented amneziawg://user@host:port
URI the real AmneziaVPN app can't parse -- it only recognizes its own
vpn:// scheme. Reverse-engineered the real app's import path (reading
amnezia-vpn/amnezia-client's own source) and confirmed it just needs
base64url(no padding) of a plain AmneziaWG .conf text -- no JSON schema
or qCompress framing to replicate, since qUncompress falls back to the
raw bytes for plain text and the parser reads a flat "Key = Value" bag
regardless of section. Both link generators now wrap the same .conf
text their own "download config" feature already produces correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 00:16:00 +03:00

114 lines
3.7 KiB
Go

package sub
import (
"encoding/base64"
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
)
// TestGenAmneziaWGLinkFields covers the real AmneziaVPN app's vpn:// scheme:
// base64url (no padding) of a plain AmneziaWG .conf text, parsed by the real
// app as a flat "Key = Value" bag (confirmed by reading its own source).
func TestGenAmneziaWGLinkFields(t *testing.T) {
serverPriv, serverPub, 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.AmneziaWG,
Remark: "awg-sub",
Settings: `{"server":{"privateKey":"` + serverPriv + `","publicKey":"` + serverPub + `","mtu":1420,"primaryDns":"8.8.8.8"},` +
`"clients":[{"email":"user","privateKey":"` + clientPriv + `","allowedIPs":["10.8.1.2/32"],"keepAlive":25}]}`,
}
s := &SubService{}
link := s.genAmneziaWGLink(inbound, "user")
if !strings.HasPrefix(link, "vpn://") {
t.Fatalf("link = %q, want vpn:// prefix", link)
}
raw, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(link, "vpn://"))
if err != nil {
t.Fatalf("link body does not decode as base64url: %v\n got: %s", err, link)
}
text := string(raw)
for _, want := range []string{
"[Interface]",
"PrivateKey = " + clientPriv,
"Address = 10.8.1.2/32",
"MTU = 1420",
"DNS = 8.8.8.8",
"[Peer]",
"PublicKey = " + serverPub,
"Endpoint = 203.0.113.7:51820",
"PersistentKeepalive = 25",
} {
if !strings.Contains(text, want) {
t.Fatalf("decoded config missing %q\n got: %s", want, text)
}
}
}
func TestGenAmneziaWGLinkWrongProtocol(t *testing.T) {
s := &SubService{}
vless := &model.Inbound{Protocol: model.VLESS, Settings: `{"clients":[{"email":"user"}]}`}
if got := s.genAmneziaWGLink(vless, "user"); got != "" {
t.Fatalf("wrong protocol should yield empty link, got %q", got)
}
}
func TestGenAmneziaWGLinkNoKey(t *testing.T) {
s := &SubService{}
inbound := &model.Inbound{
Protocol: model.AmneziaWG,
Port: 51820,
Settings: `{"server":{"privateKey":"x","publicKey":"y"},"clients":[{"email":"user"}]}`,
}
if got := s.genAmneziaWGLink(inbound, "user"); got != "" {
t.Fatalf("client without private key should yield empty link, got %q", got)
}
}
// Regression test for the bug where getInboundsBySubId's SQL allowlist was
// missing 'amneziawg', silently excluding every AmneziaWG client from
// subscriptions (plain/individual links, JSON, Clash) even though
// genAmneziaWGLink itself was already fully implemented and wired into
// GetLink's dispatch switch.
func TestGetInboundsBySubIdIncludesAmneziaWG(t *testing.T) {
initSubDB(t)
db := database.GetDB()
in := &model.Inbound{Port: 51820, Protocol: model.AmneziaWG, Enable: true, Tag: "awg-sub", Settings: `{"server":{"privateKey":"x","publicKey":"y"},"clients":[]}`}
if err := db.Create(in).Error; err != nil {
t.Fatalf("create inbound: %v", err)
}
rec := &model.ClientRecord{Email: "u@awg", SubID: "subawg", Enable: true}
if err := db.Create(rec).Error; err != nil {
t.Fatalf("create client: %v", err)
}
if err := db.Create(&model.ClientInbound{ClientId: rec.Id, InboundId: in.Id}).Error; err != nil {
t.Fatalf("create link: %v", err)
}
s := &SubService{}
inbounds, err := s.getInboundsBySubId("subawg")
if err != nil {
t.Fatalf("getInboundsBySubId: %v", err)
}
if len(inbounds) != 1 || inbounds[0].Id != in.Id {
t.Fatalf("amneziawg inbound not returned for subId: %+v", inbounds)
}
}