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:
Kuzz007
2026-07-25 17:25:58 +03:00
parent a76f0ab233
commit ef13e8567e
15 changed files with 227 additions and 33 deletions
+3
View File
@@ -654,6 +654,9 @@ export const EXAMPLES: Record<string, unknown> = {
"h3": "",
"h4": "",
"i1": "",
"ipv6Enabled": false,
"ipv6ExternalInterface": "",
"ipv6Subnet": "",
"jc": 0,
"jmax": 0,
"jmin": 0,
+10
View File
@@ -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"
+3
View File
@@ -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;
+3
View File
@@ -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),