Real production bug, found via a live test connection that reset every
~10 seconds: ensureLocked's reconfigure-in-place branch called IpcSet
unconditionally on every Ensure, including AmneziaWGJob's routine 10s
reconcile tick even when nothing in the DB had changed. amneziawg-go's
IpcSet always includes replace_peers=true (buildUAPIConfig), and its own
handling of that op is device.RemoveAllPeers() -- unconditional, even
when the new peer list is byte-identical to the old one. So every tick
tore down and recreated every peer's live handshake/session state, and
no connection could ever survive past one reconcile cycle.
Root-caused with AMNEZIAWGNET_DEBUG (previous commit) showing "UAPI:
Removing all peers" + peer Stopping/Starting exactly ~10s after a real
handshake completed, matching AmneziaWGJob's own cadence precisely.
Fixed by comparing the freshly rendered UAPI config string against what
was last actually applied and skipping IpcSet entirely when identical --
reusing buildUAPIConfig's own exhaustive field coverage instead of a
hand-maintained fingerprint that could drift out of sync with it.
TestEnsureUnchangedInstanceDoesNotResetLivePeers verifies via
device.LookupPeer pointer identity (confirmed to fail without this fix,
not just pass trivially with it).
The embedded amneziawg-go Device is silent by design (DeviceOptions'
Logger defaults to LogLevelSilent) -- real protocol-level diagnostics
(handshake progress, decrypt/MAC errors, keepalive state) were completely
unavailable while debugging a live "handshake happens, then goes silent"
report on a real test box, with nothing useful in the panel's own logs.
Setting AMNEZIAWGNET_DEBUG on the host now switches every embedded
interface to LogLevelVerbose. Deliberately env-var-gated, not a permanent
level bump: this logging has no per-peer filtering, so it's meant for
targeted investigation, not routine operation.
Adds internal/amneziawg.FirstIPv6 and a new internal/amneziawgnet/v6alias.go
that aliases each IPv6-enabled peer's own address onto the host NIC
(ip -6 addr add), wired into the Manager's Ensure/Remove/Reconcile/StopAll
lifecycle. internal/web/service/xray.go's new injectAmneziawgV6Egress gives
each such peer a dedicated freedom outbound (sendThrough) plus a routing
rule matching its own email, so its outbound connections carry a distinct
public source address again -- restoring what the embedded-architecture
hard cutover temporarily dropped. Scoped to outbound source identity only
(not unsolicited inbound/port-forwarding, which stays the separate Phase
3.6); no frontend changes needed since IPv6Enabled/IPv6ExternalInterface
were already in the UI and per-peer opt-in is just an IPv6 AllowedIPs entry,
same as today.
Hard cutover, part 2: every real call site that used to drive
internal/amneziawg's kernel-module Manager now drives
internal/amneziawgnet's instead --
- internal/web/job/amneziawg_job.go: the reconcile cron job. Traffic/
online-status accounting is dropped entirely (not ported) -- once a
peer's traffic is relayed through Xray's own SOCKS5 inbound, it's an
ordinary Xray user and XrayTrafficJob's existing generic stats polling
already handles it, with zero AmneziaWG-specific code.
- internal/web/runtime/local.go: the immediate-apply CRUD path
(AddInbound/DelInbound/updateAmneziaWGInbound).
- internal/web/web.go: panel shutdown's StopAll.
internal/amneziawgnet.Manager gains Remove(id) to match the kernel-module
Manager's shape at these call sites (Reconcile alone doesn't cover a
single-inbound removal outside a full reconcile pass).
internal/web/service/inbound_amneziawg.go's applyLocalAmneziaWG needed no
change: it already goes through runtime.Runtime.UpdateInbound, which now
resolves to the updated local.go path.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Manager.ensureLocked now attaches AttachTCPForwarder/AttachUDPHandler to
every Device it builds, relaying into that instance's own loopback SOCKS5
inbound automatically -- no caller needs to know relay.go exists at all.
Peer identity is re-looked-up via Manager.Lookup on every connection rather
than captured once at attach time, so a reconfigure-in-place (peers added/
removed without a full rebuild) doesn't leave the forwarder working off a
stale peer index.
Added TestManagerEnsureAutomaticallyWiresRelay: drives this through the
real Manager.Ensure entry point (not manual wiring like the existing
relay_e2e_test.go) against a real xray-core process, confirming the
automatic attachment and the port/password Manager derives internally
actually agree with what a real SOCKS5 inbound expects.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New internal/amneziawgnet package: builds a real amneziawg-go Device over a
gVisor netstack from an existing amneziawg.Instance, with a TCP/UDP
forwarder that recovers each tunnel connection's real destination and a
peer-identity index keyed by AllowedIPs. This is the foundation for
migrating AmneziaWG off the kernel-module+TPROXY path (see the AmneziaWG-go
vs kernel-module decision) -- nothing wires into live traffic yet, that's
Phase 2 (relay into Xray's own SOCKS5 inbound).
Covered by three real end-to-end tests: a genuine handshake + TCP forwarder
+ identity resolution, the same for UDP (including a reply routed back
through the tunnel), and the manager's reconfigure-in-place vs. rebuild
lifecycle.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>