mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-27 01:32:08 +03:00
feat(amneziawg): add frontend support and fix a Go->Zod generator gap
Wires the amneziawg protocol through the panel UI the same way every
other protocol is registered: a Zod settings schema (nested
{server, clients}, matching the Go JSON exactly), the protocol enum,
the inbound-form's per-protocol fields component and its
tab-visibility allowlist, the default-settings factory, the client
schema dispatcher, and the sniffing-capability exclusion (no Xray
inbound exists for amneziawg, same as mtproto).
Client key/allowedIPs fields are reused rather than duplicated: since
AmneziaWG clients are wire-identical to WireGuard clients (same
model.Client fields), ClientFormModal renders one shared field block
for both, switching only the visible label by which protocol is
active. The private-key input also gets a live public-key sync via a
new useEffect, because unlike WireGuard's Xray-native inbound (which
re-derives its public key at runtime and never stores one),
AmneziaWG's server.publicKey is a real persisted field the Go backend
reads directly — free-typing a new private key without this would
silently save a mismatched keypair.
Adds a downloadable per-client .conf (amneziawgConfig.ts, mirroring
wireguardConfig.ts) with the obfuscation lines, and an
InboundOption.AwgServer field on the Go side so the config builder
gets the full server block in one round trip.
Along the way, running tools/openapigen surfaced a real bug: it
doesn't flatten anonymously-embedded Go structs the way encoding/json
does, so ServerSettings embedding Obfuscation20 produced a Zod schema
with a nested `obfuscation20` key that never matches the real wire
JSON. Fixed by un-embedding (flat fields + an accessor method) and
registering internal/amneziawg in the generator's own package list,
which had been silently emitting a dangling schema reference.
English and Russian translations are complete; the other 10 locale
files still fall back to English for the new keys.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,7 +70,7 @@ func InstanceFromInbound(ib *model.Inbound) (Instance, bool) {
|
||||
PublicKey: server.PublicKey,
|
||||
Address: []string{serverAddress(server.SubnetIP, server.SubnetCIDR)},
|
||||
MTU: server.MTU,
|
||||
Obfuscation: server.Obfuscation20,
|
||||
Obfuscation: server.Obfuscation(),
|
||||
Peers: peers,
|
||||
ExternalInterface: server.ExternalInterface,
|
||||
}, true
|
||||
|
||||
@@ -79,10 +79,36 @@ type ServerSettings struct {
|
||||
// Empty means auto-detect.
|
||||
ExternalInterface string `json:"externalInterface,omitempty"`
|
||||
|
||||
// Obfuscation20 is embedded (not nested) so its fields (jc, jmin, s1...)
|
||||
// sit flat in the JSON alongside the rest of the server block, matching
|
||||
// the upstream AmneziaWG PR's schema.
|
||||
Obfuscation20
|
||||
// Obfuscation20's fields, repeated flat (not embedded) rather than
|
||||
// nested under their own key: encoding/json would happily inline an
|
||||
// embedded Obfuscation20 the same way, but the frontend's Go->Zod/TS
|
||||
// generator (tools/openapigen) does not — it emits a genuinely nested
|
||||
// `obfuscation20` object, which would silently diverge from the real
|
||||
// wire JSON. See Obfuscation() below for the manager-facing conversion.
|
||||
Jc int `json:"jc"`
|
||||
Jmin int `json:"jmin"`
|
||||
Jmax int `json:"jmax"`
|
||||
S1 int `json:"s1"`
|
||||
S2 int `json:"s2"`
|
||||
S3 int `json:"s3"`
|
||||
S4 int `json:"s4"`
|
||||
H1 string `json:"h1"`
|
||||
H2 string `json:"h2"`
|
||||
H3 string `json:"h3"`
|
||||
H4 string `json:"h4"`
|
||||
I1 string `json:"i1,omitempty"`
|
||||
}
|
||||
|
||||
// Obfuscation extracts the Obfuscation20 parameter set from a ServerSettings
|
||||
// block, for callers (the Manager, ValidateObfuscation) that want the
|
||||
// grouped type rather than the flat wire fields.
|
||||
func (s ServerSettings) Obfuscation() Obfuscation20 {
|
||||
return Obfuscation20{
|
||||
Jc: s.Jc, Jmin: s.Jmin, Jmax: s.Jmax,
|
||||
S1: s.S1, S2: s.S2, S3: s.S3, S4: s.S4,
|
||||
H1: s.H1, H2: s.H2, H3: s.H3, H4: s.H4,
|
||||
I1: s.I1,
|
||||
}
|
||||
}
|
||||
|
||||
// InboundSettings is the full Settings JSON shape stored on an AmneziaWG
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
@@ -304,6 +305,10 @@ type InboundOption struct {
|
||||
WgMtu int `json:"wgMtu,omitempty"`
|
||||
WgDns string `json:"wgDns,omitempty"`
|
||||
MtprotoDomain string `json:"mtprotoDomain,omitempty"`
|
||||
// AwgServer carries the full AmneziaWG server block (keys, subnet,
|
||||
// obfuscation params) so the clients page can render a downloadable
|
||||
// per-client .conf without a second round trip.
|
||||
AwgServer *amneziawg.ServerSettings `json:"awgServer,omitempty"`
|
||||
// Hosting node; nil for this panel's own inbounds. Lets the clients
|
||||
// page map a node filter onto inbound IDs (#4997).
|
||||
NodeId *int `json:"nodeId,omitempty"`
|
||||
@@ -365,6 +370,7 @@ func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error)
|
||||
WgMtu: wgMtu,
|
||||
WgDns: wgDns,
|
||||
MtprotoDomain: inboundMtprotoDomain(r.Protocol, r.Settings),
|
||||
AwgServer: inboundAmneziaWGServer(r.Protocol, r.Settings),
|
||||
NodeId: r.NodeId,
|
||||
NodeAddress: r.NodeAddress,
|
||||
Listen: r.Listen,
|
||||
@@ -401,6 +407,20 @@ func inboundWireguardHints(protocol string, settings string) (string, int, strin
|
||||
return publicKey, parsed.MTU, parsed.DNS
|
||||
}
|
||||
|
||||
// inboundAmneziaWGServer returns the AmneziaWG server block for the clients
|
||||
// page's config-download builder, or nil when the inbound isn't AmneziaWG or
|
||||
// its settings don't parse.
|
||||
func inboundAmneziaWGServer(protocol string, settings string) *amneziawg.ServerSettings {
|
||||
if protocol != string(model.AmneziaWG) || strings.TrimSpace(settings) == "" {
|
||||
return nil
|
||||
}
|
||||
var parsed amneziawg.InboundSettings
|
||||
if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
|
||||
return nil
|
||||
}
|
||||
return parsed.Server
|
||||
}
|
||||
|
||||
// inboundMtprotoDomain returns the inbound-level FakeTLS default domain, used by
|
||||
// the clients UI to seed a new mtproto client's secret with the right fronting
|
||||
// hostname.
|
||||
|
||||
@@ -109,12 +109,24 @@ func (s *InboundService) applyLocalAmneziaWG(inboundId int) {
|
||||
// obfuscation set, the default tunnel subnet/DNS, and a freshly generated
|
||||
// keypair.
|
||||
func defaultAmneziaWGServer() (*amneziawg.ServerSettings, error) {
|
||||
obf := amneziawg.GenerateObfuscation20("default")
|
||||
server := &amneziawg.ServerSettings{
|
||||
SubnetIP: "10.8.1.0",
|
||||
SubnetCIDR: 24,
|
||||
PrimaryDNS: "8.8.8.8",
|
||||
SecondaryDNS: "8.8.4.4",
|
||||
Obfuscation20: amneziawg.GenerateObfuscation20("default"),
|
||||
SubnetIP: "10.8.1.0",
|
||||
SubnetCIDR: 24,
|
||||
PrimaryDNS: "8.8.8.8",
|
||||
SecondaryDNS: "8.8.4.4",
|
||||
Jc: obf.Jc,
|
||||
Jmin: obf.Jmin,
|
||||
Jmax: obf.Jmax,
|
||||
S1: obf.S1,
|
||||
S2: obf.S2,
|
||||
S3: obf.S3,
|
||||
S4: obf.S4,
|
||||
H1: obf.H1,
|
||||
H2: obf.H2,
|
||||
H3: obf.H3,
|
||||
H4: obf.H4,
|
||||
I1: obf.I1,
|
||||
}
|
||||
if err := fillAmneziaWGServerKeys(server); err != nil {
|
||||
return nil, err
|
||||
@@ -173,7 +185,7 @@ func (s *InboundService) normalizeAmneziaWGSettings(inbound *model.Inbound) erro
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := amneziawg.ValidateObfuscation(parsed.Server.Obfuscation20); err != nil {
|
||||
if err := amneziawg.ValidateObfuscation(parsed.Server.Obfuscation()); err != nil {
|
||||
return fmt.Errorf("amneziawg: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -915,6 +915,12 @@
|
||||
"wireguardPreSharedKey": "WireGuard Pre-Shared Key",
|
||||
"wireguardAllowedIPs": "WireGuard Allowed IPs",
|
||||
"wireguardAllowedIPsHint": "Leave empty to auto-assign; separate entries with commas",
|
||||
"amneziaWgPrivateKey": "AmneziaWG Private Key",
|
||||
"amneziaWgPublicKey": "AmneziaWG Public Key",
|
||||
"amneziaWgPreSharedKey": "AmneziaWG Pre-Shared Key",
|
||||
"amneziaWgAllowedIPs": "AmneziaWG Allowed IPs",
|
||||
"amneziaWgAllowedIPsHint": "Leave empty to auto-assign; separate entries with commas",
|
||||
"amneziaWgConfig": "AmneziaWG config",
|
||||
"mtprotoSecret": "MTProto secret",
|
||||
"mtprotoSecretHint": "The client's FakeTLS secret. Regenerate to rotate it.",
|
||||
"mtprotoAdTag": "Ad-tag (sponsored channel)",
|
||||
@@ -1917,6 +1923,31 @@
|
||||
"psk": "PreShared Key",
|
||||
"domainStrategy": "Domain Strategy"
|
||||
},
|
||||
"amneziawg": {
|
||||
"privateKey": "Private Key",
|
||||
"publicKey": "Public Key",
|
||||
"subnetIp": "Subnet",
|
||||
"subnetCidr": "Subnet CIDR",
|
||||
"mtu": "MTU",
|
||||
"primaryDns": "Primary DNS",
|
||||
"secondaryDns": "Secondary DNS",
|
||||
"externalInterface": "External Interface",
|
||||
"externalInterfaceHint": "Host NIC for NAT (PostUp/PostDown). Leave empty to auto-detect.",
|
||||
"jc": "Jc (junk packet count)",
|
||||
"jmin": "Jmin (junk packet min size)",
|
||||
"jmax": "Jmax (junk packet max size)",
|
||||
"s1": "S1 (init packet junk size)",
|
||||
"s2": "S2 (response packet junk size)",
|
||||
"s3": "S3 (cookie reply padding, 2.0)",
|
||||
"s4": "S4 (transport packet padding, 2.0)",
|
||||
"h1": "H1 (magic header)",
|
||||
"h2": "H2 (magic header)",
|
||||
"h3": "H3 (magic header)",
|
||||
"h4": "H4 (magic header)",
|
||||
"hHint": "A single integer or a low-high range. Leave empty for the classic 1/2/3/4 default.",
|
||||
"i1": "I1 (signature packet, 2.0)",
|
||||
"i1Hint": "AmneziaWG 2.0 only. Leave empty for a 1.x-compatible config."
|
||||
},
|
||||
"tun": {
|
||||
"nameDesc": "The name of the TUN interface. Default is 'xray0'",
|
||||
"mtuDesc": "Maximum Transmission Unit. The maximum size of data packets. Default is 1500",
|
||||
|
||||
@@ -915,6 +915,12 @@
|
||||
"wireguardPreSharedKey": "Общий ключ WireGuard",
|
||||
"wireguardAllowedIPs": "Разрешённые IP WireGuard",
|
||||
"wireguardAllowedIPsHint": "Оставьте пустым для автоназначения; разделяйте записи запятыми",
|
||||
"amneziaWgPrivateKey": "Приватный ключ AmneziaWG",
|
||||
"amneziaWgPublicKey": "Публичный ключ AmneziaWG",
|
||||
"amneziaWgPreSharedKey": "Общий ключ AmneziaWG",
|
||||
"amneziaWgAllowedIPs": "Разрешённые IP AmneziaWG",
|
||||
"amneziaWgAllowedIPsHint": "Оставьте пустым для автоназначения; разделяйте записи запятыми",
|
||||
"amneziaWgConfig": "Конфиг AmneziaWG",
|
||||
"mtprotoSecret": "Секрет MTProto",
|
||||
"mtprotoSecretHint": "Секрет FakeTLS клиента. Перегенерируйте, чтобы сменить.",
|
||||
"mtprotoAdTag": "Рекламный тег (спонсорский канал)",
|
||||
@@ -1800,6 +1806,31 @@
|
||||
"psk": "Общий ключ",
|
||||
"domainStrategy": "Стратегия домена"
|
||||
},
|
||||
"amneziawg": {
|
||||
"privateKey": "Приватный ключ",
|
||||
"publicKey": "Публичный ключ",
|
||||
"subnetIp": "Подсеть",
|
||||
"subnetCidr": "Маска подсети (CIDR)",
|
||||
"mtu": "MTU",
|
||||
"primaryDns": "Основной DNS",
|
||||
"secondaryDns": "Резервный DNS",
|
||||
"externalInterface": "Внешний интерфейс",
|
||||
"externalInterfaceHint": "Сетевой интерфейс хоста для NAT (PostUp/PostDown). Оставьте пустым для автоопределения.",
|
||||
"jc": "Jc (кол-во мусорных пакетов)",
|
||||
"jmin": "Jmin (мин. размер мусорного пакета)",
|
||||
"jmax": "Jmax (макс. размер мусорного пакета)",
|
||||
"s1": "S1 (мусор init-пакета)",
|
||||
"s2": "S2 (мусор response-пакета)",
|
||||
"s3": "S3 (паддинг cookie reply, 2.0)",
|
||||
"s4": "S4 (паддинг transport-пакета, 2.0)",
|
||||
"h1": "H1 (магический заголовок)",
|
||||
"h2": "H2 (магический заголовок)",
|
||||
"h3": "H3 (магический заголовок)",
|
||||
"h4": "H4 (магический заголовок)",
|
||||
"hHint": "Целое число или диапазон low-high. Оставьте пустым для классических значений 1/2/3/4.",
|
||||
"i1": "I1 (сигнатурный пакет, 2.0)",
|
||||
"i1Hint": "Только для AmneziaWG 2.0. Оставьте пустым для совместимости с 1.x."
|
||||
},
|
||||
"tun": {
|
||||
"nameDesc": "Имя интерфейса TUN. Значение по умолчанию - 'xray0'",
|
||||
"mtuDesc": "Максимальная единица передачи. Максимальный размер пакетов данных. Значение по умолчанию - 1500",
|
||||
|
||||
Reference in New Issue
Block a user