mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-05 18:07:14 +00: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:
@@ -2821,6 +2821,15 @@
|
||||
"i1": {
|
||||
"type": "string"
|
||||
},
|
||||
"ipv6Enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ipv6ExternalInterface": {
|
||||
"type": "string"
|
||||
},
|
||||
"ipv6Subnet": {
|
||||
"type": "string"
|
||||
},
|
||||
"jc": {
|
||||
"description": "Obfuscation20's fields, repeated flat (not embedded) rather than\nnested under their own key: encoding/json would happily inline an\nembedded Obfuscation20 the same way, but the frontend's Go->Zod/TS\ngenerator (tools/openapigen) does not — it emits a genuinely nested\n`obfuscation20` object, which would silently diverge from the real\nwire JSON. See Obfuscation() below for the manager-facing conversion.",
|
||||
"type": "integer"
|
||||
|
||||
@@ -654,6 +654,9 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"h3": "",
|
||||
"h4": "",
|
||||
"i1": "",
|
||||
"ipv6Enabled": false,
|
||||
"ipv6ExternalInterface": "",
|
||||
"ipv6Subnet": "",
|
||||
"jc": 0,
|
||||
"jmax": 0,
|
||||
"jmin": 0,
|
||||
|
||||
@@ -2795,6 +2795,16 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"i1": {
|
||||
"type": "string"
|
||||
},
|
||||
"ipv6Enabled": {
|
||||
"description": "IPv6Enabled turns on native IPv6 for clients: an IPv6 host address is\nallocated from IPv6Subnet alongside each client's IPv4 one, and the\nserver proxies NDP for each enabled client's address so upstream\nrouters see it as directly reachable (no NAT66). IPv6ExternalInterface\noverrides ExternalInterface for the NDP-proxy PostUp/PostDown entries\nspecifically; empty reuses ExternalInterface.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ipv6ExternalInterface": {
|
||||
"type": "string"
|
||||
},
|
||||
"ipv6Subnet": {
|
||||
"type": "string"
|
||||
},
|
||||
"jc": {
|
||||
"description": "Obfuscation20's fields, repeated flat (not embedded) rather than\nnested under their own key: encoding/json would happily inline an\nembedded Obfuscation20 the same way, but the frontend's Go-\u003eZod/TS\ngenerator (tools/openapigen) does not — it emits a genuinely nested\n`obfuscation20` object, which would silently diverge from the real\nwire JSON. See Obfuscation() below for the manager-facing conversion.",
|
||||
"type": "integer"
|
||||
|
||||
@@ -637,6 +637,9 @@ export interface ServerSettings {
|
||||
h3: string;
|
||||
h4: string;
|
||||
i1?: string;
|
||||
ipv6Enabled?: boolean;
|
||||
ipv6ExternalInterface?: string;
|
||||
ipv6Subnet?: string;
|
||||
jc: number;
|
||||
jmax: number;
|
||||
jmin: number;
|
||||
|
||||
@@ -676,6 +676,9 @@ export const ServerSettingsSchema = z.object({
|
||||
h3: z.string(),
|
||||
h4: z.string(),
|
||||
i1: z.string().optional(),
|
||||
ipv6Enabled: z.boolean().optional(),
|
||||
ipv6ExternalInterface: z.string().optional(),
|
||||
ipv6Subnet: z.string().optional(),
|
||||
jc: z.number().int(),
|
||||
jmax: z.number().int(),
|
||||
jmin: z.number().int(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Input, InputNumber, Space } from 'antd';
|
||||
import { Button, Form, Input, InputNumber, Space, Switch } from 'antd';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
|
||||
import { FormField } from '@/components/form/rhf';
|
||||
@@ -47,6 +47,27 @@ export default function AmneziawgFields({ awgPubKey, regenInboundAwg, regenInbou
|
||||
>
|
||||
<Input placeholder="eth0" />
|
||||
</FormField>
|
||||
<FormField
|
||||
name={['settings', 'server', 'ipv6Enabled']}
|
||||
label={t('pages.xray.amneziawg.ipv6Enabled')}
|
||||
valueProp="checked"
|
||||
>
|
||||
<Switch />
|
||||
</FormField>
|
||||
<FormField
|
||||
name={['settings', 'server', 'ipv6Subnet']}
|
||||
label={t('pages.xray.amneziawg.ipv6Subnet')}
|
||||
extra={t('pages.xray.amneziawg.ipv6SubnetHint')}
|
||||
>
|
||||
<Input placeholder="fd86:ea04:1115::/64" />
|
||||
</FormField>
|
||||
<FormField
|
||||
name={['settings', 'server', 'ipv6ExternalInterface']}
|
||||
label={t('pages.xray.amneziawg.ipv6ExternalInterface')}
|
||||
extra={t('pages.xray.amneziawg.ipv6ExternalInterfaceHint')}
|
||||
>
|
||||
<Input placeholder="eth0" />
|
||||
</FormField>
|
||||
<Form.Item label={t('pages.xray.amneziawg.obfuscation')}>
|
||||
<Button icon={<ReloadOutlined />} onClick={regenInboundAwgObfuscation}>
|
||||
{t('pages.xray.amneziawg.regenerateObfuscation')}
|
||||
|
||||
@@ -46,6 +46,9 @@ export const AmneziawgServerSchema = z.object({
|
||||
primaryDns: z.string().default('8.8.8.8'),
|
||||
secondaryDns: z.string().default('8.8.4.4'),
|
||||
externalInterface: z.string().default(''),
|
||||
ipv6Enabled: z.boolean().default(false),
|
||||
ipv6Subnet: z.string().default(''),
|
||||
ipv6ExternalInterface: z.string().default(''),
|
||||
jc: z.number().int().min(0).default(5),
|
||||
jmin: z.number().int().min(0).default(10),
|
||||
jmax: z.number().int().min(0).default(50),
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -62,18 +63,27 @@ func InstanceFromInbound(ib *model.Inbound) (Instance, bool) {
|
||||
return Instance{}, false
|
||||
}
|
||||
|
||||
addresses := []string{serverAddress(server.SubnetIP, server.SubnetCIDR)}
|
||||
if server.IPv6Enabled {
|
||||
if v6, ok := serverAddressV6(server.IPv6Subnet); ok {
|
||||
addresses = append(addresses, v6)
|
||||
}
|
||||
}
|
||||
|
||||
return Instance{
|
||||
Id: ib.Id,
|
||||
Tag: ib.Tag,
|
||||
InterfaceName: interfaceNameForID(ib.Id),
|
||||
ListenPort: ib.Port,
|
||||
PrivateKey: server.PrivateKey,
|
||||
PublicKey: server.PublicKey,
|
||||
Address: []string{serverAddress(server.SubnetIP, server.SubnetCIDR)},
|
||||
MTU: server.MTU,
|
||||
Obfuscation: server.Obfuscation(),
|
||||
Peers: peers,
|
||||
ExternalInterface: server.ExternalInterface,
|
||||
Id: ib.Id,
|
||||
Tag: ib.Tag,
|
||||
InterfaceName: interfaceNameForID(ib.Id),
|
||||
ListenPort: ib.Port,
|
||||
PrivateKey: server.PrivateKey,
|
||||
PublicKey: server.PublicKey,
|
||||
Address: addresses,
|
||||
MTU: server.MTU,
|
||||
Obfuscation: server.Obfuscation(),
|
||||
Peers: peers,
|
||||
ExternalInterface: server.ExternalInterface,
|
||||
IPv6Enabled: server.IPv6Enabled,
|
||||
IPv6ExternalInterface: server.IPv6ExternalInterface,
|
||||
}, true
|
||||
}
|
||||
|
||||
@@ -96,6 +106,19 @@ func serverAddress(subnetIP string, cidr int) string {
|
||||
return fmt.Sprintf("%s/%d", subnetIP, cidr)
|
||||
}
|
||||
|
||||
// serverAddressV6 returns the server's own IPv6 tunnel address for a subnet
|
||||
// CIDR (e.g. "fd86:ea04:1115::1/64" for "fd86:ea04:1115::/64"), the first
|
||||
// usable host in the prefix. ok is false when subnetCIDR is empty or not a
|
||||
// valid IPv6 prefix.
|
||||
func serverAddressV6(subnetCIDR string) (addr string, ok bool) {
|
||||
prefix, err := netip.ParsePrefix(subnetCIDR)
|
||||
if err != nil || !prefix.Addr().Is6() {
|
||||
return "", false
|
||||
}
|
||||
host := prefix.Masked().Addr().Next()
|
||||
return fmt.Sprintf("%s/%d", host, prefix.Bits()), true
|
||||
}
|
||||
|
||||
// structuralFingerprint changes whenever a value that requires a full
|
||||
// interface bounce (awg-quick down + up) changes.
|
||||
func (inst Instance) structuralFingerprint() string {
|
||||
@@ -397,7 +420,7 @@ func generateServerConfig(inst Instance) string {
|
||||
if ext == "" {
|
||||
ext = detectDefaultInterface()
|
||||
}
|
||||
postUp, postDown := defaultPostUpDown(inst.InterfaceName, ext, inst.Address)
|
||||
postUp, postDown := defaultPostUpDown(inst, ext)
|
||||
fmt.Fprintf(&b, "PostUp = %s\n", postUp)
|
||||
fmt.Fprintf(&b, "PostDown = %s\n", postDown)
|
||||
|
||||
@@ -451,11 +474,15 @@ func hOrDefault(v, def string) string {
|
||||
return v
|
||||
}
|
||||
|
||||
// defaultPostUpDown returns basic NAT + forwarding rules: MASQUERADE the
|
||||
// tunnel subnet out the external interface and accept forwarded traffic in
|
||||
// both directions. Per-peer port-forwarding, IPv6/NDP and RouteViaXray are a
|
||||
// later phase (see project TODO).
|
||||
func defaultPostUpDown(iface, ext string, addresses []string) (postUp, postDown string) {
|
||||
// defaultPostUpDown returns NAT + forwarding rules: MASQUERADE the tunnel
|
||||
// subnet out the external interface, accept forwarded traffic in both
|
||||
// directions, and — when the instance has IPv6 enabled — the IPv6-forward
|
||||
// rules, proxy_ndp sysctl, and one `ip -6 neigh add proxy` entry per enabled
|
||||
// peer with an IPv6 address, so upstream routers see each client's IPv6 as
|
||||
// directly reachable on the LAN without NAT66. Per-client port-forwarding and
|
||||
// RouteViaXray are a later phase (see project TODO).
|
||||
func defaultPostUpDown(inst Instance, ext string) (postUp, postDown string) {
|
||||
iface := inst.InterfaceName
|
||||
up := []string{
|
||||
fmt.Sprintf("iptables -A FORWARD -i %s -j ACCEPT", iface),
|
||||
fmt.Sprintf("iptables -A FORWARD -o %s -j ACCEPT", iface),
|
||||
@@ -464,10 +491,38 @@ func defaultPostUpDown(iface, ext string, addresses []string) (postUp, postDown
|
||||
fmt.Sprintf("iptables -D FORWARD -i %s -j ACCEPT", iface),
|
||||
fmt.Sprintf("iptables -D FORWARD -o %s -j ACCEPT", iface),
|
||||
}
|
||||
if subnet := firstAddress(addresses); subnet != "" && ext != "" {
|
||||
if subnet := firstAddress(inst.Address); subnet != "" && ext != "" {
|
||||
up = append([]string{fmt.Sprintf("iptables -t nat -A POSTROUTING -s %s -o %s -j MASQUERADE", subnet, ext)}, up...)
|
||||
down = append([]string{fmt.Sprintf("iptables -t nat -D POSTROUTING -s %s -o %s -j MASQUERADE", subnet, ext)}, down...)
|
||||
}
|
||||
|
||||
if inst.IPv6Enabled {
|
||||
ext6 := inst.IPv6ExternalInterface
|
||||
if ext6 == "" {
|
||||
ext6 = ext
|
||||
}
|
||||
up = append(up,
|
||||
fmt.Sprintf("ip6tables -A FORWARD -i %s -j ACCEPT", iface),
|
||||
fmt.Sprintf("ip6tables -A FORWARD -o %s -j ACCEPT", iface),
|
||||
fmt.Sprintf("ip6tables -A FORWARD -i %s -o %s -j ACCEPT", ext6, iface),
|
||||
"sysctl -w net.ipv6.conf.all.forwarding=1",
|
||||
fmt.Sprintf("sysctl -w net.ipv6.conf.%s.proxy_ndp=1", ext6),
|
||||
)
|
||||
down = append(down,
|
||||
fmt.Sprintf("ip6tables -D FORWARD -i %s -j ACCEPT", iface),
|
||||
fmt.Sprintf("ip6tables -D FORWARD -o %s -j ACCEPT", iface),
|
||||
fmt.Sprintf("ip6tables -D FORWARD -i %s -o %s -j ACCEPT", ext6, iface),
|
||||
)
|
||||
for _, p := range inst.Peers {
|
||||
ip6 := firstIPv6(p.AllowedIPs)
|
||||
if ip6 == "" {
|
||||
continue
|
||||
}
|
||||
up = append(up, fmt.Sprintf("ip -6 neigh add proxy %s dev %s", ip6, ext6))
|
||||
down = append(down, fmt.Sprintf("ip -6 neigh del proxy %s dev %s", ip6, ext6))
|
||||
}
|
||||
}
|
||||
|
||||
up = append(up, "sysctl -w net.ipv4.ip_forward=1")
|
||||
return strings.Join(up, "; "), strings.Join(down, "; ")
|
||||
}
|
||||
@@ -481,6 +536,23 @@ func firstAddress(addresses []string) string {
|
||||
return addresses[0]
|
||||
}
|
||||
|
||||
// firstIPv6 returns the first IPv6 address (mask stripped) among allowedIPs,
|
||||
// or "" if none — used to build one NDP proxy PostUp/PostDown entry per peer.
|
||||
func firstIPv6(allowedIPs []string) string {
|
||||
for _, a := range allowedIPs {
|
||||
if prefix, err := netip.ParsePrefix(a); err == nil {
|
||||
if prefix.Addr().Is6() {
|
||||
return prefix.Addr().String()
|
||||
}
|
||||
continue
|
||||
}
|
||||
if addr, err := netip.ParseAddr(a); err == nil && addr.Is6() {
|
||||
return addr.String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// detectDefaultInterface returns the first non-loopback, non-tunnel, UP
|
||||
// interface that has a routable IPv4 address. Falls back to "eth0" only if
|
||||
// nothing is found.
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -119,6 +120,26 @@ func ValidateObfuscation(o Obfuscation20) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateIPv6Subnet rejects a malformed IPv6 subnet before it's saved, so a
|
||||
// bad manual entry can't bring the interface down on `awg-quick up`. A blank
|
||||
// subnet is only valid when IPv6 itself is disabled.
|
||||
func ValidateIPv6Subnet(enabled bool, subnet string) error {
|
||||
if !enabled {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(subnet) == "" {
|
||||
return fmt.Errorf("ipv6Subnet is required when IPv6 is enabled")
|
||||
}
|
||||
prefix, err := netip.ParsePrefix(subnet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid ipv6Subnet %q: %w", subnet, err)
|
||||
}
|
||||
if !prefix.Addr().Is6() {
|
||||
return fmt.Errorf("invalid ipv6Subnet %q: not an IPv6 prefix", subnet)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateHValue checks one H parameter: empty, a single uint32, or
|
||||
// "low-high" with 0 <= low <= high <= uint32 max.
|
||||
func validateHValue(v string) error {
|
||||
|
||||
@@ -47,6 +47,7 @@ type Instance struct {
|
||||
PrivateKey string
|
||||
PublicKey string
|
||||
// Address holds the interface's own tunnel address(es), e.g. "10.8.1.1/24".
|
||||
// Carries both the IPv4 and (when enabled) IPv6 server address.
|
||||
Address []string
|
||||
MTU int
|
||||
|
||||
@@ -56,6 +57,13 @@ type Instance struct {
|
||||
// ExternalInterface is the host NIC PostUp/PostDown NAT rules attach to.
|
||||
// Empty means auto-detect at config-generation time.
|
||||
ExternalInterface string
|
||||
|
||||
// IPv6Enabled turns on the per-peer NDP proxy PostUp/PostDown entries
|
||||
// (ip -6 neigh add/del proxy) for peers that have an IPv6 AllowedIPs
|
||||
// entry. IPv6ExternalInterface overrides ExternalInterface for those
|
||||
// entries specifically; empty means reuse ExternalInterface.
|
||||
IPv6Enabled bool
|
||||
IPv6ExternalInterface string
|
||||
}
|
||||
|
||||
// ServerSettings is the "server" block of an AmneziaWG inbound's Settings
|
||||
@@ -79,6 +87,16 @@ type ServerSettings struct {
|
||||
// Empty means auto-detect.
|
||||
ExternalInterface string `json:"externalInterface,omitempty"`
|
||||
|
||||
// IPv6Enabled turns on native IPv6 for clients: an IPv6 host address is
|
||||
// allocated from IPv6Subnet alongside each client's IPv4 one, and the
|
||||
// server proxies NDP for each enabled client's address so upstream
|
||||
// routers see it as directly reachable (no NAT66). IPv6ExternalInterface
|
||||
// overrides ExternalInterface for the NDP-proxy PostUp/PostDown entries
|
||||
// specifically; empty reuses ExternalInterface.
|
||||
IPv6Enabled bool `json:"ipv6Enabled,omitempty"`
|
||||
IPv6Subnet string `json:"ipv6Subnet,omitempty"`
|
||||
IPv6ExternalInterface string `json:"ipv6ExternalInterface,omitempty"`
|
||||
|
||||
// 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
|
||||
|
||||
@@ -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