Files
3x-ui-web-src/internal/amneziawgnet/portfwd_udp.go
T
Sanaei f727d04f65
CI / go-test (push) Canceled after 0s
CI / postgres-durable-first (push) Canceled after 0s
CI / codegen (push) Canceled after 0s
CI / govulncheck (push) Canceled after 0s
CI / race (push) Canceled after 0s
CI / fuzz-smoke (push) Canceled after 0s
CI / golangci (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CodeQL Advanced / Analyze (go) (push) Canceled after 0s
CodeQL Advanced / Analyze (actions) (push) Canceled after 0s
CodeQL Advanced / Analyze (javascript-typescript) (push) Canceled after 0s
Docs CI / build (push) Canceled after 0s
Docs Deploy (GitHub Pages) / build (push) Canceled after 0s
Release 3X-UI / build (386) (push) Canceled after 0s
Release 3X-UI / build (amd64) (push) Canceled after 0s
Release 3X-UI / build (arm64) (push) Canceled after 0s
Release 3X-UI / build (armv5) (push) Canceled after 0s
Release 3X-UI / build (armv6) (push) Canceled after 0s
Release 3X-UI / build (armv7) (push) Canceled after 0s
Release 3X-UI / build (s390x) (push) Canceled after 0s
Release 3X-UI / Build for Windows (amd64) (push) Canceled after 0s
Deploy Smoke Tests / noninteractive-install (ubuntu-24.04-arm) (push) Canceled after 0s
Deploy Smoke Tests / noninteractive-install (ubuntu-latest) (push) Canceled after 0s
Deploy Smoke Tests / release-tag-install (ubuntu-24.04-arm) (push) Canceled after 0s
Deploy Smoke Tests / release-tag-install (ubuntu-latest) (push) Canceled after 0s
Docs Deploy (GitHub Pages) / deploy (push) Canceled after 0s
Release 3X-UI / Publish rolling dev release (push) Canceled after 0s
v3.7.0
2026-08-24 15:07:15 +02:00

148 lines
4.8 KiB
Go

package amneziawgnet
import (
"context"
"fmt"
"net"
"net/netip"
"sync"
"time"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
)
// portForwardUDPIdleTimeout matches UDPRelay.pump's own idle window
// (relay.go) -- both are "how long to keep a per-flow session alive with no
// traffic before tearing it down," so there's no reason for the two
// directions to disagree.
const portForwardUDPIdleTimeout = 2 * time.Minute
// udpForwardSession is one established flow from a single external source
// address into the tunnel toward a peer -- conn is a connected gonet UDP
// endpoint (DialUDP with a non-nil raddr), so plain Read/Write, not
// ReadFrom/WriteTo, address it correctly.
type udpForwardSession struct {
conn *gonet.UDPConn
}
// udpForwardListener is one open host-facing UDP socket for a single
// portForwardKey, demultiplexing by external source address -- the mirror
// image of AttachUDPHandler/UDPRelay, which demultiplex by tunnel-internal
// source for the opposite direction. net.ListenPacket has no accept/session
// model of its own, so this package tracks sessions itself here, the same
// way UDPRelay already does in relay.go.
type udpForwardListener struct {
pc net.PacketConn
mu sync.Mutex
sessions map[netip.AddrPort]*udpForwardSession
}
// listenPortForwardUDP opens a host-facing UDP socket on key.port and
// starts demultiplexing datagrams into per-source-address tunnel sessions
// toward target(key.email). Bind-failure contract matches
// listenPortForwardTCP exactly: log, return nil, Reconcile retries later.
func listenPortForwardUDP(gstack *stack.Stack, inboundID int, key portForwardKey, target portForwardTargetFunc) *udpForwardListener {
pc, err := (&net.ListenConfig{}).ListenPacket(context.Background(), "udp", fmt.Sprintf(":%d", key.port))
if err != nil {
logger.Warningf("amneziawgnet: port-forward: inbound %d peer %q: listen udp :%d: %v", inboundID, key.email, key.port, err)
return nil
}
l := &udpForwardListener{pc: pc, sessions: map[netip.AddrPort]*udpForwardSession{}}
logger.Infof("amneziawgnet: port-forward: inbound %d peer %q: listening udp :%d", inboundID, key.email, key.port)
go l.readLoop(gstack, inboundID, key, target)
return l
}
func (l *udpForwardListener) readLoop(gstack *stack.Stack, inboundID int, key portForwardKey, target portForwardTargetFunc) {
buf := make([]byte, 65536)
for {
n, from, err := l.pc.ReadFrom(buf)
if err != nil {
return // closed
}
src, ok := udpAddrPort(from)
if !ok {
continue
}
l.mu.Lock()
sess, exists := l.sessions[src]
l.mu.Unlock()
if !exists {
addr, ok := target(key.email)
if !ok {
continue
}
raddr := tunnelFullAddress(addr, key.port)
conn, err := gonet.DialUDP(gstack, nil, &raddr, tunnelNetwork(addr))
if err != nil {
logger.Warningf("amneziawgnet: port-forward: inbound %d peer %q: dial tunnel %s:%d: %v", inboundID, key.email, addr, key.port, err)
continue
}
sess = &udpForwardSession{conn: conn}
l.mu.Lock()
l.sessions[src] = sess
l.mu.Unlock()
go l.pump(src, sess)
}
// buf is reused by the next ReadFrom the instant this loop continues,
// so the session's own goroutine can't be handed a slice into it --
// Write copies synchronously here, on this goroutine, before that
// can happen, so no copy of the payload is needed.
if _, err := sess.conn.Write(buf[:n]); err != nil {
logger.Warningf("amneziawgnet: port-forward: inbound %d peer %q: write tunnel: %v", inboundID, key.email, err)
}
}
}
// pump reads replies from sess and writes them back to the external source
// src until the session errors out or goes idle, mirroring UDPRelay.pump's
// exact structure (relay.go) for the opposite direction.
func (l *udpForwardListener) pump(src netip.AddrPort, sess *udpForwardSession) {
defer func() {
l.mu.Lock()
delete(l.sessions, src)
l.mu.Unlock()
sess.conn.Close()
}()
buf := make([]byte, 65536)
for {
_ = sess.conn.SetReadDeadline(time.Now().Add(portForwardUDPIdleTimeout))
n, err := sess.conn.Read(buf)
if err != nil {
return
}
if _, err := l.pc.WriteTo(buf[:n], net.UDPAddrFromAddrPort(src)); err != nil {
return
}
}
}
// Close tears down every open session and the underlying socket.
func (l *udpForwardListener) Close() {
l.mu.Lock()
sessions := l.sessions
l.sessions = map[netip.AddrPort]*udpForwardSession{}
l.mu.Unlock()
for _, sess := range sessions {
sess.conn.Close()
}
l.pc.Close()
}
// udpAddrPort extracts a netip.AddrPort from a net.Addr returned by
// net.ListenPacket's ReadFrom -- always a *net.UDPAddr in practice for a
// "udp" network listener, but handled defensively rather than assumed.
func udpAddrPort(addr net.Addr) (netip.AddrPort, bool) {
udpAddr, ok := addr.(*net.UDPAddr)
if !ok {
return netip.AddrPort{}, false
}
return udpAddr.AddrPort(), true
}