mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-27 08:15:01 +03:00
feat(amneziawg): Phase 2a — IPv6 support + NDP proxy
Adds native dual-stack IPv6 to AmneziaWG inbounds, ported from coinman-dev/3ax-ui's approach: - ServerSettings gets ipv6Enabled/ipv6Subnet/ipv6ExternalInterface; Instance carries the server's own IPv6 address (first host of the subnet) alongside its IPv4 one. - defaultAmneziaWGClients allocates an IPv6 host address per client (second AllowedIPs entry) when the server has IPv6 enabled, reusing allocateWireguardAddress — which needed a real fix along the way: it always suffixed "/32" regardless of address family, which is wrong for an IPv6 host address (needs /128). Now family-aware. - generateServerConfig's PostUp/PostDown gains IPv6 forward-accept rules, proxy_ndp sysctl, and one `ip -6 neigh add/del proxy` entry per enabled peer with an IPv6 address — the lightweight per-client method, not the ndppd-daemon whole-subnet method (not worth the config-file-management complexity at this scale; ndppd itself is still installed by install.sh in case that changes later). - ValidateIPv6Subnet rejects a malformed subnet before save. - Frontend: ipv6Enabled/ipv6Subnet/ipv6ExternalInterface fields on the AmneziaWG inbound form, EN+RU translations, openapi.json/generated/* regenerated (the latter via `go run ./tools/openapigen`, pure Go). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,22 +10,27 @@ import (
|
||||
wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
|
||||
)
|
||||
|
||||
// defaultAmneziaWGSubnetBase resolves the /CIDR base new peer addresses are
|
||||
// allocated from, out of the inbound's own configured server subnet — unlike
|
||||
// WireGuard, which always falls back to a fixed 10.0.0.0/24.
|
||||
func defaultAmneziaWGSubnetBase(settingsJSON string) (string, error) {
|
||||
// defaultAmneziaWGSubnetBases resolves the /CIDR bases new peer addresses are
|
||||
// allocated from, out of the inbound's own configured server subnet(s) —
|
||||
// unlike WireGuard, which always falls back to a fixed 10.0.0.0/24. v6Base is
|
||||
// "" when the server doesn't have IPv6 enabled.
|
||||
func defaultAmneziaWGSubnetBases(settingsJSON string) (v4Base, v6Base string, err error) {
|
||||
var parsed amneziawg.InboundSettings
|
||||
if err := json.Unmarshal([]byte(settingsJSON), &parsed); err != nil {
|
||||
return "", fmt.Errorf("amneziawg: invalid settings: %w", err)
|
||||
return "", "", fmt.Errorf("amneziawg: invalid settings: %w", err)
|
||||
}
|
||||
if parsed.Server == nil {
|
||||
return "", fmt.Errorf("amneziawg: settings missing server block")
|
||||
return "", "", fmt.Errorf("amneziawg: settings missing server block")
|
||||
}
|
||||
cidr := parsed.Server.SubnetCIDR
|
||||
if cidr <= 0 {
|
||||
cidr = 24
|
||||
}
|
||||
return fmt.Sprintf("%s/%d", parsed.Server.SubnetIP, cidr), nil
|
||||
v4Base = fmt.Sprintf("%s/%d", parsed.Server.SubnetIP, cidr)
|
||||
if parsed.Server.IPv6Enabled && parsed.Server.IPv6Subnet != "" {
|
||||
v6Base = parsed.Server.IPv6Subnet
|
||||
}
|
||||
return v4Base, v6Base, nil
|
||||
}
|
||||
|
||||
// defaultAmneziaWGClients fills in blank AmneziaWG credentials for newly
|
||||
@@ -38,7 +43,7 @@ func defaultAmneziaWGSubnetBase(settingsJSON string) (string, error) {
|
||||
// its IP allocation and validation helpers — the only real difference is
|
||||
// where the allocation base comes from.
|
||||
func defaultAmneziaWGClients(settingsJSON string, existing, clients []model.Client, interfaceClients []any) error {
|
||||
base, err := defaultAmneziaWGSubnetBase(settingsJSON)
|
||||
v4Base, v6Base, err := defaultAmneziaWGSubnetBases(settingsJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -64,11 +69,19 @@ func defaultAmneziaWGClients(settingsJSON string, existing, clients []model.Clie
|
||||
c.PublicKey = pub
|
||||
}
|
||||
if len(c.AllowedIPs) == 0 {
|
||||
addr, err := allocateWireguardAddress(used, base)
|
||||
addr, err := allocateWireguardAddress(used, v4Base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.AllowedIPs = []string{addr}
|
||||
allowed := []string{addr}
|
||||
if v6Base != "" {
|
||||
addr6, err := allocateWireguardAddress(used, v6Base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allowed = append(allowed, addr6)
|
||||
}
|
||||
c.AllowedIPs = allowed
|
||||
} else {
|
||||
normalized, err := normalizeWireguardAllowedIPs(c.AllowedIPs)
|
||||
if err != nil {
|
||||
|
||||
@@ -46,9 +46,10 @@ func wireguardAllocationBase(used []string, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
// allocateWireguardAddress returns the first free /32 host address in base that
|
||||
// is not already present in used. The server holds the first host (.1), so
|
||||
// allocation starts at the second host (.2).
|
||||
// allocateWireguardAddress returns the first free single-host address in base
|
||||
// (suffixed /32 for an IPv4 base, /128 for IPv6) that is not already present
|
||||
// in used. The server holds the first host (.1 / ::1), so allocation starts
|
||||
// at the second host (.2 / ::2).
|
||||
func allocateWireguardAddress(used []string, base string) (string, error) {
|
||||
if base == "" {
|
||||
base = defaultWireguardBase
|
||||
@@ -57,6 +58,10 @@ func allocateWireguardAddress(used []string, base string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
hostBits := "32"
|
||||
if prefix.Addr().Is6() {
|
||||
hostBits = "128"
|
||||
}
|
||||
taken := make(map[netip.Addr]struct{}, len(used))
|
||||
for _, u := range used {
|
||||
if a := wireguardHostAddr(u); a.IsValid() {
|
||||
@@ -66,7 +71,7 @@ func allocateWireguardAddress(used []string, base string) (string, error) {
|
||||
addr := prefix.Masked().Addr().Next().Next()
|
||||
for prefix.Contains(addr) {
|
||||
if _, ok := taken[addr]; !ok {
|
||||
return addr.String() + "/32", nil
|
||||
return addr.String() + "/" + hostBits, nil
|
||||
}
|
||||
addr = addr.Next()
|
||||
}
|
||||
|
||||
@@ -188,6 +188,9 @@ func (s *InboundService) normalizeAmneziaWGSettings(inbound *model.Inbound) erro
|
||||
if err := amneziawg.ValidateObfuscation(parsed.Server.Obfuscation()); err != nil {
|
||||
return fmt.Errorf("amneziawg: %w", err)
|
||||
}
|
||||
if err := amneziawg.ValidateIPv6Subnet(parsed.Server.IPv6Enabled, parsed.Server.IPv6Subnet); err != nil {
|
||||
return fmt.Errorf("amneziawg: %w", err)
|
||||
}
|
||||
|
||||
bs, err := json.MarshalIndent(parsed, "", " ")
|
||||
if err != nil {
|
||||
|
||||
@@ -1933,6 +1933,11 @@
|
||||
"secondaryDns": "Secondary DNS",
|
||||
"externalInterface": "External Interface",
|
||||
"externalInterfaceHint": "Host NIC for NAT (PostUp/PostDown). Leave empty to auto-detect.",
|
||||
"ipv6Enabled": "Enable IPv6",
|
||||
"ipv6Subnet": "IPv6 Subnet",
|
||||
"ipv6SubnetHint": "e.g. fd86:ea04:1115::/64. Required when IPv6 is enabled.",
|
||||
"ipv6ExternalInterface": "IPv6 External Interface",
|
||||
"ipv6ExternalInterfaceHint": "Host NIC for the NDP proxy entries. Leave empty to reuse External Interface.",
|
||||
"obfuscation": "Obfuscation parameters",
|
||||
"regenerateObfuscation": "Regenerate",
|
||||
"jc": "Jc (junk packet count)",
|
||||
|
||||
@@ -1816,6 +1816,11 @@
|
||||
"secondaryDns": "Резервный DNS",
|
||||
"externalInterface": "Внешний интерфейс",
|
||||
"externalInterfaceHint": "Сетевой интерфейс хоста для NAT (PostUp/PostDown). Оставьте пустым для автоопределения.",
|
||||
"ipv6Enabled": "Включить IPv6",
|
||||
"ipv6Subnet": "Подсеть IPv6",
|
||||
"ipv6SubnetHint": "Например, fd86:ea04:1115::/64. Обязательно при включённом IPv6.",
|
||||
"ipv6ExternalInterface": "Внешний интерфейс для IPv6",
|
||||
"ipv6ExternalInterfaceHint": "Сетевой интерфейс хоста для записей NDP-прокси. Оставьте пустым, чтобы использовать «Внешний интерфейс».",
|
||||
"obfuscation": "Параметры обфускации",
|
||||
"regenerateObfuscation": "Сгенерировать заново",
|
||||
"jc": "Jc (кол-во мусорных пакетов)",
|
||||
|
||||
Reference in New Issue
Block a user