mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
d5ab84e8d5
* feat(amneziawg): add AmneziaWG as an outbound protocol - AmneziaWG outbound protocol end-to-end: config schema, socks bridge, netstack, panel UI - Route amneziawg outbounds to HTTP probe in TCP mode (backend + frontend classifiers) with pinning test - Add 2-minute idle read deadline to pumpUDPEgress to reap idle egress sessions - Require SOCKS5 username/password auth on the egress server (reject NO-AUTH with 0xFF) with test - Bound the egress TCP tunnel dial with portForwardDialTimeout (10s), matching portfwd.go - Resolve UDP domain targets off the association's reader loop via deliverUDPDatagram; race-safe getOrDial starts the reply pump at session creation; client passed by value into resolver goroutines (pinned by TestEgressUDPDatagramDomainInterleavedClients) - Reconcile early-returns on an empty desired set and closes the egress listener; EgressBasePort (64900) is reserved against local inbound port conflicts like the internal API port, with pinning tests for both the port reservation (TestCheckPortConflict_EgressPortBlockedLocal) and the Reconcile empty-desired Close/Listen lifecycle (TestOutboundManagerReconcileEmptyDesiredClosesEgress) - Eliminate acceptLoop shutdown race by validating listener != nil and registering to tracked under s.mu before wg.Add; bound pre-auth handshake with deadline (pinned by TestEgressServerCloseDuringConcurrentAccepts) - Support AAAA and dual-stack domain resolution in tunnel DNS resolver with v6 default fallback (DefaultTunnelDNSServerV6); add DNS field to frontend protocol form; avoid unneeded cache flushes on unchanged SetStack ticks * fix(amneziawg): resolve IPv6-only DNS default fallback and validate required keys - Default to IPv6 tunnel DNS on IPv6-only outbounds with blank dns - Require non-empty secretKey and peer publicKey in ValidateAmneziaWGOutbound - Add end-to-end IPv6 tunnel domain resolution test and test empty key rejection - Trim comment blocks exceeding 2 lines across modified files - Fix Storybook test execution on environments with POSIX locale Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
118 lines
3.5 KiB
Go
118 lines
3.5 KiB
Go
package amneziawgnet
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
|
|
|
wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
|
|
)
|
|
|
|
// clientDeviceTestInstance builds a minimal valid client instance with one
|
|
// peer and a non-zero keepalive -- the exact shape the outbound form seeds.
|
|
func clientDeviceTestInstance(t *testing.T) amneziawg.OutboundInstance {
|
|
t.Helper()
|
|
priv, pub, err := wgutil.GenerateWireguardKeypair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return amneziawg.OutboundInstance{
|
|
Tag: "awg-out-test",
|
|
Address: []string{"10.8.0.2/32"},
|
|
MTU: 1420,
|
|
PrivateKey: priv,
|
|
Peers: []amneziawg.OutboundPeer{{
|
|
PublicKey: pub,
|
|
AllowedIPs: []string{"0.0.0.0/0", "::/0"},
|
|
Endpoint: "203.0.113.7:51820",
|
|
KeepAlive: 25,
|
|
}},
|
|
}
|
|
}
|
|
|
|
func TestBuildClientUAPIConfig_KeepAliveKeyIsValidUAPIPeerKey(t *testing.T) {
|
|
inst := clientDeviceTestInstance(t)
|
|
conf, err := buildClientUAPIConfig(inst, DeviceOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := "persistent_keepalive_interval=25\n"
|
|
if !strings.Contains(conf, want) {
|
|
t.Fatalf("UAPI config missing %q:\n%s", want, conf)
|
|
}
|
|
if strings.Contains(conf, "persistent_keepalive_seconds") {
|
|
t.Fatalf("UAPI config contains invalid peer key persistent_keepalive_seconds:\n%s", conf)
|
|
}
|
|
}
|
|
|
|
func TestBuildClientUAPIConfig_ZeroKeepAliveOmitsLine(t *testing.T) {
|
|
inst := clientDeviceTestInstance(t)
|
|
inst.Peers[0].KeepAlive = 0
|
|
conf, err := buildClientUAPIConfig(inst, DeviceOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(conf, "persistent_keepalive") {
|
|
t.Fatalf("zero KeepAlive must not emit a keepalive line:\n%s", conf)
|
|
}
|
|
}
|
|
|
|
// amneziawg-go reads an absent UAPI line as "keep the current value", and
|
|
// ensureLocked reconfigures in place, so a cleared key must be sent as zero.
|
|
func TestBuildClientUAPIConfig_ClearedHeaderProtectionKeyIsSentAsZero(t *testing.T) {
|
|
inst := clientDeviceTestInstance(t)
|
|
inst.Obfuscation = amneziawg.Obfuscation31{S1: 20, S2: 20, S3: 20, S4: 20}
|
|
|
|
key, err := wgutil.GenerateWireguardPSK()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
withKey, err := buildClientUAPIConfig(inst, DeviceOptions{HeaderProtectionKey: key})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
keyHex, err := wgutil.KeyToHex(key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(withKey, "header_protection_key="+keyHex+"\n") {
|
|
t.Fatalf("a set key must be emitted verbatim, got:\n%s", withKey)
|
|
}
|
|
|
|
cleared, err := buildClientUAPIConfig(inst, DeviceOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
zero := "header_protection_key=" + strings.Repeat("0", 64) + "\n"
|
|
if !strings.Contains(cleared, zero) {
|
|
t.Fatalf("an unset key must be emitted as the all-zero key, got:\n%s", cleared)
|
|
}
|
|
}
|
|
|
|
// With no explicit MTU the netstack is built from S4, so an S4-only edit must
|
|
// move the fingerprint or ensureLocked reconfigures in place and keeps the old.
|
|
func TestOutboundFingerprintTracksTheS4DerivedMTU(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mtu int
|
|
wantChange bool
|
|
}{
|
|
{"derived MTU", 0, true},
|
|
{"explicit MTU", 1420, false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
inst := clientDeviceTestInstance(t)
|
|
inst.MTU = tt.mtu
|
|
inst.Obfuscation.S4 = 12
|
|
before := outboundFingerprint(inst)
|
|
inst.Obfuscation.S4 = 28
|
|
after := outboundFingerprint(inst)
|
|
if changed := before != after; changed != tt.wantChange {
|
|
t.Fatalf("fingerprint changed = %v, want %v (%q -> %q)", changed, tt.wantChange, before, after)
|
|
}
|
|
})
|
|
}
|
|
}
|