mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-08 03:07:15 +00:00
ef13e8567e
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>
114 lines
3.6 KiB
Go
114 lines
3.6 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
|
wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
if parsed.Server == nil {
|
|
return "", "", fmt.Errorf("amneziawg: settings missing server block")
|
|
}
|
|
cidr := parsed.Server.SubnetCIDR
|
|
if cidr <= 0 {
|
|
cidr = 24
|
|
}
|
|
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
|
|
// added clients: a generated keypair when none was provided, a derived
|
|
// public key when only a private key was given, and a unique tunnel address
|
|
// allocated from the inbound's own configured subnet. It mutates both the
|
|
// typed clients and the parallel raw client maps that get persisted into the
|
|
// inbound settings. Existing values are never overwritten, so editing a
|
|
// client never rotates its keys. Mirrors defaultWireguardClients, reusing
|
|
// 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 {
|
|
v4Base, v6Base, err := defaultAmneziaWGSubnetBases(settingsJSON)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
used := make([]string, 0)
|
|
for i := range existing {
|
|
used = append(used, existing[i].AllowedIPs...)
|
|
}
|
|
for i := range clients {
|
|
c := &clients[i]
|
|
if c.PrivateKey == "" && c.PublicKey == "" {
|
|
priv, pub, err := wgutil.GenerateWireguardKeypair()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.PrivateKey = priv
|
|
c.PublicKey = pub
|
|
} else if c.PublicKey == "" && c.PrivateKey != "" {
|
|
pub, err := wgutil.PublicKeyFromPrivate(c.PrivateKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.PublicKey = pub
|
|
}
|
|
if len(c.AllowedIPs) == 0 {
|
|
addr, err := allocateWireguardAddress(used, v4Base)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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 {
|
|
return err
|
|
}
|
|
if len(normalized) == 0 {
|
|
return common.NewError("amneziawg: allowedIPs has no usable entry")
|
|
}
|
|
if hit := wireguardAllowedIPsCollision(normalized, used); hit != "" {
|
|
return common.NewError("amneziawg: allowedIPs entry already used by another client:", hit)
|
|
}
|
|
c.AllowedIPs = normalized
|
|
}
|
|
used = append(used, c.AllowedIPs...)
|
|
|
|
if i < len(interfaceClients) {
|
|
if m, ok := interfaceClients[i].(map[string]any); ok {
|
|
m["privateKey"] = c.PrivateKey
|
|
m["publicKey"] = c.PublicKey
|
|
m["allowedIPs"] = c.AllowedIPs
|
|
if c.PreSharedKey != "" {
|
|
m["preSharedKey"] = c.PreSharedKey
|
|
}
|
|
interfaceClients[i] = m
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|