mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-29 17:25:01 +03:00
56ac8d0fc7
* feat(amneziawg): add opt-in Linux kernel datapath Add the kernel backend as an explicit per-inbound option while keeping the embedded backend as the default. Include host preflight, UI/API support, the bundled installer CLI, lifecycle and accounting integration, regression tests, and upstream-facing documentation. Kernel mode is NAT-only and rejects unsupported routing, IPv6, forwarding, and pinned-listen configurations. Local saves check the local host; remote nodes check their own host during apply. Preserve adopted interface accounting across panel restarts, retain manager state after failed teardown so the job can retry, prevent embedded startup while a kernel interface remains live, and prune collector state for removed peers. Tested the restart accounting path on Debian 13 with XanMod 7.1.11 on x86_64; Docker and cross-kernel coverage are not claimed. * fix(amneziawg): align readiness and endpoint reporting Expire endpoint sightings using the latest handshake and sweep stale addresses for the polled interface. Keep the readiness API schema, generated client types, and ready-host empty blocker list consistent; watch the nested form field and defer local readiness checks for remote targets. Document safe rollback before using a build without kernel support. Regression evidence: the idle endpoint test failed before the collector change because the old address remained visible after 31 minutes; endpoint tests pass after the fix. The form component suite passes 3 tests; restoring the invalid array watch made its off-mode severity test fail, while the corrected dot-path watch passes. Targeted Go regressions across amneziawgkern, job, and service pass. * style(amneziawg): format readiness test * test: reproduce kernel AWG client validation gaps * test(service): cover kernel AWG shared client fanout * fix(amneziawg): validate kernel client fanout and config values * test(amneziawgkern): cover owned interface lifecycle recovery * fix(amneziawgkern): recover owned interfaces on restart * test(amneziawgkern): retain visibility after recovered cleanup failure * fix(amneziawgkern): report recovered interfaces while cleanup retries * style(amneziawgkern): satisfy static analysis * test(amneziawgkern): cover long rendered peer comments * fix(amneziawgkern): accept long generated config comments * test(amneziawgkern): refuse foreign occupied interfaces Capture the current Ensure ordering failure with real temporary config paths and manager side-effect boundaries. * test(amneziawgkern): cover stale interface collisions Extend the Ensure regression across absent links, mismatched live interfaces, cached manager state, and Remove teardown. * fix(amneziawgkern): verify interface ownership before mutation Refuse to replace or tear down links unless the canonical config is a valid panel-owned file and its key matches the live interface. Preserve normal adoption and key rotation, and document migration of legacy unmarked configs. Validation: go test ./internal/amneziawgkern -count=1; go test -race ./internal/amneziawgkern -count=1; golangci-lint run ./internal/amneziawgkern/...; git diff --check. * build: update Go toolchain to 1.27.3 * test(amneziawg): reproduce unsafe key rotation recovery Keep the previous owned config authoritative until teardown succeeds, and require fresh managers to rebuild structural key changes. These regressions expose the stuck live-key/disk-key mismatch after teardown failure. * fix(amneziawg): recover interrupted key rotations Keep the old owned config on disk until a structural rebuild has confirmed the previous interface is down. Fresh managers only adopt when the live config and desired structural identity match, so retries and removals retain the key needed to prove ownership. * fix(scanner): align Go image with module version * fix(docs): pin pnpm release with bootstrap fix pnpm 12.9.1 restores a Node launcher before its postinstall replaces the target with ELF; pnpm 11 then runs that binary as JavaScript. Pin 12.10.0, which includes the shim fix, so the docs CI bootstrap can switch versions safely. * fix(xray): build core from pinned module The configured Xray release asset tag is unpublished, and the latest published binary lacks GetUsersStats used by the panel. Build the packaged core from the exact Xray module version already selected in go.mod for Docker and release targets. * fix(tests): stabilize local network fixtures Keep the TUIC stream fragmentation check on a working UDP loopback path, allocate the AmneziaWG fixture ports dynamically, and wait for a real Xray stats response before starting API mutation assertions. Recreate failed readiness channels so an early connection cannot stall the probe. Verified host and offline parallel Go suites, repeated occupied-port and UDP tests, race/shuffle runs, and golangci-lint. Production behavior and dependency versions are unchanged. * fix(i18n): add the kernel-mode AmneziaWG keys to sk-SK main gained the sk-SK locale after this branch added its kernel-mode keys to the other 13, so i18n-dead-keys.test.ts failed on the merged tree. * refactor(amneziawgkern): apply kernel interfaces with direct commands, no awg-quick awg-quick evaluates PostUp/PostDown through a root shell, so every value the panel rendered into its config was one validation slip away from root command execution. The interface is now built with direct ip/awg/nft execs from a setconf-only file: link add, awg setconf, address, mtu, an atomically replaced nftables table per inbound, and the IPv4 forwarding sysctl written directly. - Panel-owned names and paths: xui-awg<id> interfaces, xui_awg<id> NAT tables and configs under the panel's data dir, so nothing collides with an operator's own awg0/awg1 awg-quick tunnels in /etc/amnezia/amneziawg. - FORWARD accepts (comment 3x-ui:xui-awg<id>) when iptables is present: Docker and ufw set the FORWARD policy to DROP, which silently black-holed the tunnel. - A full panel stop records the final counters and tears the interfaces down, like the embedded datapath; a stopped panel enforces no quota or expiry. Crash survivors are still adopted without a rebuild. - The job reads counters before reconciling, and from every managed interface, so a client disabled for quota, or a removed inbound, is charged for its last interval instead of losing it with the dropped peer. - An unchanged reconcile tick forks no process (it probed the live key with `awg show` every 10s); readiness no longer loads the module from a GET. Verified on the real module (WSL2 6.6 kernel, v3.1.20260906): create, peer and egress changes in place, SIGKILL adoption with the same ifindex, SIGTERM teardown leaving no link, table, FORWARD rule or config behind. * fix(amneziawg): gate kernel host readiness only when a save starts the datapath Every save of a kernel-mode inbound re-ran the host preflight, so a host that lost its module (a kernel upgrade without a DKMS rebuild) refused even disabling the inbound or switching it back to embedded mode. Host blockers now apply only when the save starts the kernel datapath: a new inbound, a switch from embedded, or enabling a disabled one. Feature blockers (IPv6, forwarded ports, pinned listen) still apply to every save. * feat(nodes): refuse kernel-mode AmneziaWG pushes to nodes that predate it A node built before kernel mode drops the unknown kernelMode field and serves the inbound on the embedded datapath while the master's form shows kernel mode on. Nodes now advertise awg-kernel in X-3x-Node-Caps, and Remote refuses to push a kernel-mode inbound to a node that has not advertised it, learning the caps with an inbound list first when this Remote has not heard from it. * fix(amneziawg): pin the kernel module installer to verified commits The installer ran as root and built whatever upstream tag sorted newest at install time. It now clones fixed tags (module v3.1.20260906, tools v3.1.20260812) and refuses unless each resolves to its recorded commit. The udp_tunnel perl patch is gone: those releases carry the kernel 7.1.5 compat upstream, so the patch never matched. * fix(i18n): translate the kernel-mode AmneziaWG strings All thirteen non-English locales carried the English text verbatim. * fix(amneziawg): skip the kernel traffic flush when no kernel interface runs Shutdown listed the desired AmneziaWG inbounds from the database even on a panel with no kernel interface, an unneeded query on every stop. A server stopped before its database was opened panicked on it, which turned TestServerStopWhenXrayNotRunningSucceeds red whenever it ran first. --------- Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
230 lines
12 KiB
Plaintext
230 lines
12 KiB
Plaintext
---
|
|
title: AmneziaWG
|
|
description: Configure embedded or optional Linux kernel AmneziaWG inbounds in 3x-ui.
|
|
icon: Lock
|
|
---
|
|
|
|
**AmneziaWG** is a WireGuard fork that adds traffic obfuscation (junk packets,
|
|
randomized padding, and rewritten protocol magic values) so the tunnel doesn't
|
|
look like WireGuard to deep-packet inspection. It's a popular choice where
|
|
plain WireGuard is blocked but a WireGuard-shaped tunnel with a different
|
|
fingerprint gets through.
|
|
|
|
<Callout type="info">
|
|
AmneziaWG runs **embedded in the panel process** — `amneziawg-go` over a
|
|
userspace (gVisor) network stack, not a kernel module. There is no DKMS
|
|
build, no Secure Boot conflict, and no host network/kernel access
|
|
requirement, so it works the same way inside a container as on bare
|
|
metal. Each peer's decapsulated traffic relays into its own loopback Xray
|
|
SOCKS5 inbound, so a peer's routing, sniffing, and per-client stats all
|
|
come from Xray's own machinery — the same as any other protocol's
|
|
inbound, not a separate code path.
|
|
</Callout>
|
|
|
|
## Key settings
|
|
|
|
### Server / interface
|
|
|
|
| Field | What it is |
|
|
| ------------------------ | ------------------------------------------------------------------------ |
|
|
| **Subnet** | The tunnel's IPv4 subnet (e.g. `10.8.1.0/24`); each client gets an address from it. |
|
|
| **MTU** | Interface MTU. Leave at the default unless you have a reason to change it. |
|
|
| **DNS (primary/secondary)** | Seeded into downloadable client configs; the server's own interface doesn't need one. |
|
|
| **External interface** | The host NIC a peer's IPv6 address gets aliased onto when IPv6 is enabled (see below). Leave blank to auto-detect. |
|
|
|
|
### Obfuscation (AmneziaWG 3.1)
|
|
|
|
The same values must match on both ends of the tunnel, so the server stores
|
|
them once and every client config inherits them. The panel generates a
|
|
randomized set for you (with a **regenerate** button) — a static, reused
|
|
value defeats the point, since DPI can fingerprint it over time.
|
|
|
|
| Field | What it is |
|
|
| ------------ | ---------------------------------------------------------------------------- |
|
|
| **Jc** | Number of junk packets sent before the handshake. |
|
|
| **Jmin/Jmax** | Size range (bytes) for those junk packets. `Jmin` must not exceed `Jmax`. |
|
|
| **S1/S2** | Padding added to the handshake init/response packets, `0`-`1552` / `0`-`1608`: the packets are `148 + S1` and `92 + S2` bytes and must fit the 1700-byte receive buffer amneziawg-go uses on iOS. The panel also rejects `S1 + 56 = S2`, which would give both packets the same size on the wire (amneziawg-go itself accepts it). An AmneziaWG outbound takes the remote server's values as they are, up to `65535`. |
|
|
| **S3** | Cookie-reply padding, `0`-`1636`: the reply is `64 + S3` bytes and must fit the 1700-byte receive buffer amneziawg-go uses on iOS. An outbound, as with S1/S2, takes the remote server's value up to `65535`. |
|
|
| **S4** | Transport (data) packet padding, `0`-`32`. |
|
|
| **H1-H4** | Header values that replace WireGuard's message-type field. Each is a single integer or a `low-high` range, and the four must not overlap — amneziawg-go and the kernel module refuse the whole device otherwise. `1`-`4` are WireGuard's own types and the engine default for a blank field: valid, but without a HeaderProtectionKey the type field then reads like plain WireGuard. |
|
|
| **I1-I5** | Optional signature packets — random bytes prepended before the handshake, e.g. `<r 148>`. Generated sets fill `I1` only, matching Amnezia's own generator. |
|
|
| **HeaderProtectionKey** | A base64 32-byte key for the 3.0 header-protection mechanism. Must match on every client config; blank disables it. |
|
|
| **ContentPaddingAddition** | A single integer or `low-high` byte range of extra padding on content packets. Kept `<= 64` by the generator so a 1420-MTU tunnel doesn't fragment. |
|
|
| **RekeyAfterTime / RekeyTimeout / RejectAfterTime / KeepaliveTimeout / MaxHandshakeAttempts** | Handshake-timing randomization: each is a `low-high` range (seconds; attempts for the last one) the peer samples from, so session timing stops being a WireGuard fingerprint. Every `RekeyAfterTime` value must stay below every `RejectAfterTime` value. Blank keeps the WireGuard default. |
|
|
| **RandomTrailers** | Appends a random number of bytes to the end of every packet. |
|
|
| **DisableCookies** | Never send cookie replies — removes a DPI-visible WireGuard message type, at the cost of WireGuard's handshake-flood mitigation. |
|
|
|
|
<Callout type="info">
|
|
If you enter obfuscation values by hand instead of using the generated
|
|
defaults, keep `H1`-`H4` **non-overlapping** and above `4`, and double-check
|
|
`S1 + 56 != S2` — a bad value here keeps the embedded interface from
|
|
coming up at all.
|
|
</Callout>
|
|
|
|
<Callout type="warn">
|
|
The 3.1 parameters need a **3.1-capable client**. Clients must run a
|
|
3.1-capable Amnezia app; blanking the 3.1 fields renders a config older
|
|
clients still understand. There is no host-side version requirement —
|
|
the panel ships its own pinned `amneziawg-go`, not whatever happens to be
|
|
installed on the system.
|
|
</Callout>
|
|
|
|
## Set it up in the panel
|
|
|
|
<Steps>
|
|
|
|
<Step>
|
|
### Add an inbound
|
|
|
|
Add a new inbound, choose protocol **AmneziaWG**, and set the port and tunnel
|
|
subnet.
|
|
</Step>
|
|
|
|
<Step>
|
|
### Leave obfuscation on defaults (or regenerate)
|
|
|
|
The panel fills in a randomized, kernel-valid obfuscation set automatically.
|
|
Use **Regenerate** if you want a fresh one; there's no need to hand-edit these
|
|
unless you have a specific reason to.
|
|
</Step>
|
|
|
|
<Step>
|
|
### Add a client
|
|
|
|
Each client gets its own keypair and tunnel address. Download the client's
|
|
`.conf` or copy its share link (`vpn://…`, importable by the official
|
|
AmneziaWG/AmneziaVPN apps) from the client list.
|
|
</Step>
|
|
|
|
<Step>
|
|
### Optional: enable IPv6
|
|
|
|
Turning on IPv6 allocates an IPv6 address alongside each client's IPv4 one
|
|
from the configured IPv6 subnet. The panel aliases that address onto the
|
|
external interface's host NIC so outbound connections carry the peer's own
|
|
distinct public IPv6 identity — no NAT66 needed.
|
|
</Step>
|
|
|
|
<Step>
|
|
### Optional: forward ports to a client
|
|
|
|
Set a client's forwarded ports (e.g. `80, 443, 8000-8100`) to open a real
|
|
listener on the host that relays that traffic straight to the client's
|
|
tunnel address — useful for a client that needs to expose a service through
|
|
the server.
|
|
</Step>
|
|
|
|
</Steps>
|
|
|
|
In the **embedded** backend, each peer relays into its own loopback SOCKS5
|
|
inbound, tagged with the AmneziaWG inbound's tag. Its traffic therefore uses
|
|
Xray routing and appears on the [Routing](/docs/operations/outbounds-routing)
|
|
page, like other Xray inbounds.
|
|
|
|
## Optional Linux kernel backend
|
|
|
|
The embedded backend remains the default. On a supported Linux host, an
|
|
administrator can opt an individual inbound into the kernel backend with
|
|
**Kernel mode**. The panel then creates an `xui-awg<id>` interface on the
|
|
AmneziaWG kernel module, configures it with `awg setconf`, and masquerades
|
|
peer traffic out of the host NIC with an nftables table of its own
|
|
(`xui_awg<id>`). Peer traffic bypasses Xray. Nothing runs through a shell:
|
|
every step is a direct `ip`, `awg` or `nft` command, and the interface's
|
|
config lives in a panel-private directory (`/etc/x-ui/amneziawg` on a standard
|
|
install), never next to your own `awg-quick` configs.
|
|
|
|
Kernel mode requires a Linux host running the panel as root, with the
|
|
`amneziawg` module installed and `awg`, `ip` and `nft` available. The
|
|
readiness panel reports missing host requirements and provides the install
|
|
command. On a standard install, run
|
|
`sudo /usr/local/x-ui/x-ui install-awg-kernel`. The bundled command builds a
|
|
pinned, commit-verified release of the module and tools on Debian and Ubuntu;
|
|
kernel headers and, when Secure Boot is enabled, module signing must be
|
|
available. Review the script and host changes before running it.
|
|
|
|
The panel enables IPv4 forwarding and, when `iptables` is installed, adds two
|
|
`FORWARD` accept rules per interface (commented `3x-ui:xui-awg<id>`), so a host
|
|
whose `FORWARD` policy is `DROP` — as Docker and ufw set it — still forwards
|
|
the tunnel. A firewall that drops forwarded traffic in its own nftables table,
|
|
such as firewalld, still needs a rule from you.
|
|
|
|
Kernel mode is not supported in Docker. Embedded mode continues to work in
|
|
containers. The kernel backend does not support Xray routing or sniffing,
|
|
per-client IPv6, client port forwarding, or a pinned listen address; the panel
|
|
rejects saves that request those features. On a multi-node setup the master
|
|
checks that the target node supports kernel mode before pushing such an
|
|
inbound, and the node checks its own host when it applies it.
|
|
|
|
Stopping the panel records the interfaces' final traffic and then tears them
|
|
down, exactly as the embedded backend stops: a stopped panel enforces no quota
|
|
or expiry, so nothing keeps forwarding. A panel-only restart (`SIGHUP`) leaves
|
|
them running. If the panel process dies instead, the next start adopts the
|
|
surviving interfaces; traffic between the crash and the first counter read
|
|
after it is not charged. Interfaces and configs the panel cannot prove are its
|
|
own — a same-named link with a different key, or an unmarked, non-regular or
|
|
symlinked config file — are never changed or removed.
|
|
|
|
## Keepalive ranges
|
|
|
|
AmneziaWG clients accept `keepAlive` as a number or an ordered range from 0 to 65535, such as `15-25`. Omitted values keep the existing setting; explicit `0` disables keepalive. Native `.conf` files preserve ranges, while Clash/Mihomo scalar-only output uses the lower bound (`0-25` therefore disables keepalive). Plain WireGuard clients support only a scalar value. Upgrade sub-nodes before sending them range values because older nodes parse keepalive as an integer.
|
|
|
|
Parser support is present in the native tools and the panel embedded engine: [the native `awg-tools` v3.1.20260812 config parser](https://github.com/amnezia-vpn/amneziawg-tools/blob/v3.1.20260812/src/config.c#L596-L599) sends `PersistentKeepalive` to its [ordered-range parser](https://github.com/amnezia-vpn/amneziawg-tools/blob/v3.1.20260812/src/type.c#L40-L59), and this panel pins [amneziawg-go v3.1.20260828](https://github.com/amnezia-vpn/amneziawg-go/tree/v3.1.20260828) in `go.mod`; its [UAPI parser](https://github.com/amnezia-vpn/amneziawg-go/blob/v3.1.20260828/device/uapi.go#L663-L674) reads the interval as a `UintRange`. This confirms parser support for these code paths, not interoperability with every third-party client. The panel enforces the documented 0-65535 limit on new values; the native parser's syntax support is not a substitute for that validation.
|
|
|
|

|
|
|
|
## What the configuration looks like
|
|
|
|
A client's downloadable `.conf` (also what the `vpn://` share link encodes,
|
|
base64url'd) looks like this:
|
|
|
|
```ini title="client .conf"
|
|
[Interface]
|
|
PrivateKey = <client private key>
|
|
Address = 10.8.1.2/32
|
|
DNS = 8.8.8.8, 8.8.4.4
|
|
Jc = 4
|
|
Jmin = 65
|
|
Jmax = 220
|
|
S1 = 87
|
|
S2 = 44
|
|
S3 = 21
|
|
S4 = 9
|
|
H1 = 463065432
|
|
H2 = 912345678
|
|
H3 = 1345678901
|
|
H4 = 1987654321
|
|
I1 = <r 148>
|
|
HeaderProtectionKey = 8Iu83eHDA3fMKKSGaEsVW9Ycd2lYYzc0MYlk1jJTvE4=
|
|
ContentPaddingAddition = 17-49
|
|
RekeyAfterTime = 111-139
|
|
RekeyTimeout = 4-7
|
|
RejectAfterTime = 187-251
|
|
KeepaliveTimeout = 9-14
|
|
MaxHandshakeAttempts = 19-36
|
|
RandomTrailers = on
|
|
DisableCookies = on
|
|
|
|
# my-client
|
|
[Peer]
|
|
PublicKey = <server public key>
|
|
AllowedIPs = 0.0.0.0/0, ::/0
|
|
Endpoint = your-server:443
|
|
PersistentKeepalive = 25
|
|
```
|
|
|
|
## Multi-node (sub-nodes)
|
|
|
|
An AmneziaWG inbound can be created on, or cloned to, a sub-node. The node's
|
|
own panel runs the interface, so the node must run panel **v3.7.0** or newer;
|
|
the master refuses an older node, or one that has not reported its version yet.
|
|
A client's `forwardedPorts` are checked against the ports in use on that node.
|
|
|
|
## Not yet covered
|
|
|
|
<Callout type="info">
|
|
|
|
- **Telegram bot** — AmneziaWG inbounds haven't been exercised through the
|
|
bot yet. Treat it as unverified until someone reports back.
|
|
|
|
</Callout>
|