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>
146 lines
3.6 KiB
Go
146 lines
3.6 KiB
Go
package amneziawgnet
|
|
|
|
import (
|
|
"net"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
|
|
)
|
|
|
|
// egressPortBound reports whether 127.0.0.1:<EgressBasePort> accepts TCP.
|
|
func egressPortBound(t *testing.T) bool {
|
|
t.Helper()
|
|
conn, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", itoa(int(EgressBasePort))), 500*time.Millisecond)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
conn.Close()
|
|
return true
|
|
}
|
|
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
var b [8]byte
|
|
i := len(b)
|
|
for n > 0 {
|
|
i--
|
|
b[i] = byte('0' + n%10)
|
|
n /= 10
|
|
}
|
|
return string(b[i:])
|
|
}
|
|
|
|
// newTestOutboundDesired builds one runnable outbound desired state.
|
|
func newTestOutboundDesired(t *testing.T, tag string) OutboundDesired {
|
|
t.Helper()
|
|
priv, _, err := wireguard.GenerateWireguardKeypair()
|
|
if err != nil {
|
|
t.Fatalf("generate keypair: %v", err)
|
|
}
|
|
return OutboundDesired{
|
|
Instance: amneziawg.OutboundInstance{
|
|
Tag: tag,
|
|
Address: []string{"10.204.0.1/24"},
|
|
MTU: 1420,
|
|
PrivateKey: priv,
|
|
ListenPort: 0,
|
|
},
|
|
}
|
|
}
|
|
|
|
// TestOutboundManagerReconcileEmptyDesiredClosesEgress verifies that an empty
|
|
// desired set tears down interfaces and releases 127.0.0.1:64900.
|
|
func TestOutboundManagerReconcileEmptyDesiredClosesEgress(t *testing.T) {
|
|
m := &OutboundManager{iface: map[string]*managedOutbound{}}
|
|
defer m.Reconcile(nil)
|
|
|
|
// Other tests in this package may leave the process-wide egress
|
|
// singleton bound; converge to a known-free state before pinning.
|
|
GetEgressServer().Close()
|
|
if egressPortBound(t) {
|
|
t.Fatal("egress port still bound after Close; Close() failed to release it")
|
|
}
|
|
|
|
// Non-empty: listener must come up.
|
|
d := newTestOutboundDesired(t, "t1")
|
|
m.Reconcile([]OutboundDesired{d})
|
|
if !egressPortBound(t) {
|
|
t.Fatal("egress port not bound after Reconcile with a desired outbound")
|
|
}
|
|
|
|
// Empty: listener must be released so other listeners can take the port.
|
|
m.Reconcile(nil)
|
|
if egressPortBound(t) {
|
|
t.Fatal("egress port still bound after Reconcile(nil)")
|
|
}
|
|
ln, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", itoa(int(EgressBasePort))))
|
|
if err != nil {
|
|
t.Fatalf("egress port must be free after Reconcile(nil): %v", err)
|
|
}
|
|
ln.Close()
|
|
|
|
// Back to non-empty and empty again: Close/Listen must be repeatable.
|
|
m.Reconcile([]OutboundDesired{d})
|
|
if !egressPortBound(t) {
|
|
t.Fatal("egress port not re-bound after a second non-empty Reconcile")
|
|
}
|
|
m.Reconcile(nil)
|
|
if egressPortBound(t) {
|
|
t.Fatal("egress port still bound after a second Reconcile(nil)")
|
|
}
|
|
}
|
|
|
|
// TestEgressServerCloseDuringConcurrentAccepts ensures Close during
|
|
// concurrent accepts shuts down cleanly without hanging wg.Wait().
|
|
func TestEgressServerCloseDuringConcurrentAccepts(t *testing.T) {
|
|
srv := GetEgressServer()
|
|
if err := srv.Listen(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stop := make(chan struct{})
|
|
done := make(chan struct{})
|
|
var clientWg sync.WaitGroup
|
|
go func() {
|
|
defer close(done)
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
default:
|
|
c, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", itoa(int(EgressBasePort))), 50*time.Millisecond)
|
|
if err == nil {
|
|
clientWg.Add(1)
|
|
go func(conn net.Conn) {
|
|
defer clientWg.Done()
|
|
time.Sleep(20 * time.Millisecond)
|
|
conn.Close()
|
|
}(c)
|
|
}
|
|
time.Sleep(2 * time.Millisecond)
|
|
}
|
|
}
|
|
}()
|
|
|
|
time.Sleep(20 * time.Millisecond)
|
|
closeChan := make(chan struct{})
|
|
go func() {
|
|
srv.Close()
|
|
close(closeChan)
|
|
}()
|
|
|
|
select {
|
|
case <-closeChan:
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("srv.Close() hung waiting for connection handlers to exit")
|
|
}
|
|
close(stop)
|
|
<-done
|
|
clientWg.Wait()
|
|
}
|