mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
7a41c59494
* fix(amneziawg): honor inbound listen when binding UDP socket AmneziaWG inbounds ignored the listen field and always opened awgconn.NewDefaultBind(), so multi-IP hosts replied from the primary address and handshakes to a secondary IP never completed (#6367). Carry Inbound.Listen on amneziawg.Instance, open a Bind pinned to that address (wildcard when empty/0.0.0.0/::), and include listen in addressFingerprint so edits rebuild the Device. Fixes #6367 * fix(amneziawg): fall back to wildcard when listen is unusable Invalid or non-local listen values no longer hard-fail inbound startup; treat ::0/[::0] as wildcards and normalize listen in the bind fingerprint. * fix(amneziawg): use ListenConfig.ListenPacket for noctx --------- Co-authored-by: mrchatam <mrchatam@users.noreply.github.com> Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com>
189 lines
4.9 KiB
Go
189 lines
4.9 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 awgconn.NewDefaultBind()
|
|
}
|
|
if !listenBindable(addr) {
|
|
logger.Warningf("amneziawgnet: listen %q is not usable on this host; using dual-stack wildcard", raw)
|
|
return awgconn.NewDefaultBind()
|
|
}
|
|
return newPinnedBind(addr)
|
|
}
|
|
|
|
// 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()
|
|
}
|