Files
3x-ui/internal/amneziawgnet/pinned_bind.go
T
MHSanaei 805f94a00c chore(amneziawgnet): bind test sockets to loopback
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.
2026-10-03 00:00:36 +02:00

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()
}