mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-18 03:45:02 +03:00
805f94a00c
Windows Firewall prompted on every run of amneziawgnet.test.exe, since the tests opened AmneziaWG, outbound-client and port-forward sockets on all interfaces and go test rebuilds the binary under a fresh temp path, so a granted exception never sticks. Route every "all interfaces" bind through wildcardBindHost, which is empty in production (behaviour unchanged) and pinned to 127.0.0.1 by the package TestMain.
200 lines
5.3 KiB
Go
200 lines
5.3 KiB
Go
package amneziawgnet
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
awgconn "github.com/amnezia-vpn/amneziawg-go/v3/conn"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
)
|
|
|
|
// pinnedBind opens its UDP socket on exactly one host address (#6367).
|
|
// Empty/wildcard listen still uses StdNetBind via newListenBind.
|
|
type pinnedBind struct {
|
|
mu sync.Mutex
|
|
addr netip.Addr
|
|
conn *net.UDPConn
|
|
}
|
|
|
|
func newPinnedBind(addr netip.Addr) *pinnedBind {
|
|
return &pinnedBind{addr: addr.Unmap()}
|
|
}
|
|
|
|
func (b *pinnedBind) Open(uport uint16) ([]awgconn.ReceiveFunc, uint16, error) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
if b.conn != nil {
|
|
return nil, 0, awgconn.ErrBindAlreadyOpen
|
|
}
|
|
|
|
network := "udp4"
|
|
if b.addr.Is6() {
|
|
network = "udp6"
|
|
}
|
|
pc, err := (&net.ListenConfig{}).ListenPacket(context.Background(), network, net.JoinHostPort(b.addr.String(), strconv.Itoa(int(uport))))
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
uc, ok := pc.(*net.UDPConn)
|
|
if !ok {
|
|
pc.Close()
|
|
return nil, 0, fmt.Errorf("amneziawgnet: listen %s returned %T, want *net.UDPConn", network, pc)
|
|
}
|
|
laddr, ok := uc.LocalAddr().(*net.UDPAddr)
|
|
if !ok {
|
|
uc.Close()
|
|
return nil, 0, fmt.Errorf("amneziawgnet: unexpected local addr %T", uc.LocalAddr())
|
|
}
|
|
b.conn = uc
|
|
return []awgconn.ReceiveFunc{b.makeReceiveFunc(uc)}, uint16(laddr.Port), nil
|
|
}
|
|
|
|
func (b *pinnedBind) makeReceiveFunc(uc *net.UDPConn) awgconn.ReceiveFunc {
|
|
return func(bufs [][]byte, sizes []int, eps []awgconn.Endpoint) (int, error) {
|
|
n, addr, err := uc.ReadFromUDPAddrPort(bufs[0])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
sizes[0] = n
|
|
eps[0] = &awgconn.StdNetEndpoint{AddrPort: netip.AddrPortFrom(addr.Addr().Unmap(), addr.Port())}
|
|
return 1, nil
|
|
}
|
|
}
|
|
|
|
func (b *pinnedBind) Close() error {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
if b.conn == nil {
|
|
return nil
|
|
}
|
|
err := b.conn.Close()
|
|
b.conn = nil
|
|
return err
|
|
}
|
|
|
|
// SetMark is a no-op: the panel never configures a WireGuard fwmark here.
|
|
func (b *pinnedBind) SetMark(uint32) error { return nil }
|
|
|
|
func (b *pinnedBind) Send(bufs [][]byte, ep awgconn.Endpoint) error {
|
|
std, ok := ep.(*awgconn.StdNetEndpoint)
|
|
if !ok {
|
|
return awgconn.ErrWrongEndpointType
|
|
}
|
|
b.mu.Lock()
|
|
uc := b.conn
|
|
b.mu.Unlock()
|
|
if uc == nil {
|
|
return net.ErrClosed
|
|
}
|
|
for _, buf := range bufs {
|
|
if _, err := uc.WriteToUDPAddrPort(buf, std.AddrPort); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *pinnedBind) ParseEndpoint(s string) (awgconn.Endpoint, error) {
|
|
ap, err := netip.ParseAddrPort(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &awgconn.StdNetEndpoint{AddrPort: netip.AddrPortFrom(ap.Addr().Unmap(), ap.Port())}, nil
|
|
}
|
|
|
|
func (b *pinnedBind) BatchSize() int { return 1 }
|
|
|
|
// isWildcardListen reports empty / dual-stack wildcard listen values.
|
|
// Includes ::0 (isAnyListen) and [::] so AmneziaWG keeps dual-stack StdNetBind.
|
|
func isWildcardListen(listen string) bool {
|
|
switch strings.TrimSpace(listen) {
|
|
case "", "0.0.0.0", "::", "::0", "[::]", "[::0]":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// parseListenAddr returns a concrete host address to pin. ok is false for
|
|
// wildcards and for values that are not a bare IP (previously inert for AWG).
|
|
func parseListenAddr(listen string) (addr netip.Addr, ok bool) {
|
|
listen = strings.TrimSpace(listen)
|
|
if isWildcardListen(listen) {
|
|
return netip.Addr{}, false
|
|
}
|
|
// Bracketed IPv6 literal e.g. [::1] — strip for ParseAddr.
|
|
if strings.HasPrefix(listen, "[") && strings.HasSuffix(listen, "]") {
|
|
listen = listen[1 : len(listen)-1]
|
|
}
|
|
addr, err := netip.ParseAddr(listen)
|
|
if err != nil {
|
|
return netip.Addr{}, false
|
|
}
|
|
return addr.Unmap(), true
|
|
}
|
|
|
|
// listenBindable probes whether addr can be used as a UDP local address.
|
|
func listenBindable(addr netip.Addr) bool {
|
|
network := "udp4"
|
|
if addr.Is6() {
|
|
network = "udp6"
|
|
}
|
|
pc, err := (&net.ListenConfig{}).ListenPacket(context.Background(), network, net.JoinHostPort(addr.String(), "0"))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_ = pc.Close()
|
|
return true
|
|
}
|
|
|
|
// newListenBind returns StdNetBind for wildcards / unusable listen values, or
|
|
// a pinnedBind for a real local address. Never fails the inbound on bad listen.
|
|
func newListenBind(listen string) awgconn.Bind {
|
|
raw := strings.TrimSpace(listen)
|
|
addr, pinned := parseListenAddr(raw)
|
|
if !pinned {
|
|
if raw != "" && !isWildcardListen(raw) {
|
|
logger.Warningf("amneziawgnet: listen %q is not a bindable IP; using dual-stack wildcard", raw)
|
|
}
|
|
return wildcardBind()
|
|
}
|
|
if !listenBindable(addr) {
|
|
logger.Warningf("amneziawgnet: listen %q is not usable on this host; using dual-stack wildcard", raw)
|
|
return wildcardBind()
|
|
}
|
|
return newPinnedBind(addr)
|
|
}
|
|
|
|
// wildcardBindHost replaces "all interfaces" for every host socket this package
|
|
// opens; TestMain pins it to loopback so Windows Firewall never prompts.
|
|
var wildcardBindHost = ""
|
|
|
|
func wildcardBind() awgconn.Bind {
|
|
if wildcardBindHost != "" {
|
|
return newPinnedBind(netip.MustParseAddr(wildcardBindHost))
|
|
}
|
|
return awgconn.NewDefaultBind()
|
|
}
|
|
|
|
// normalizedListenFP collapses wildcard spellings so fingerprint rebuilds
|
|
// only when the effective Bind actually changes.
|
|
func normalizedListenFP(listen string) string {
|
|
if isWildcardListen(listen) {
|
|
return ""
|
|
}
|
|
addr, ok := parseListenAddr(listen)
|
|
if !ok {
|
|
return "" // unusable → same Bind as wildcard fallback
|
|
}
|
|
if !listenBindable(addr) {
|
|
return ""
|
|
}
|
|
return addr.String()
|
|
}
|