| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- // Phase 3.6: per-client port-forwarding. A real Go listener bound to each
- // forwarded external port relays into the peer's own tunnel-internal
- // address via a direct gonet dial -- the mirror image of
- // AttachTCPForwarder/AttachUDPHandler (which relay FROM the tunnel TO the
- // real world), and this path's replacement for the retired kernel-module
- // architecture's PostUp/PostDown iptables DNAT rules: there's no real OS
- // network interface here for DNAT to rewrite packets on, the same root
- // reason Phase 3.5's IPv6 alias mechanism couldn't reuse NDP-proxy either.
- //
- // Deliberately dials straight into the gVisor stack rather than relaying
- // through Xray's own SOCKS5 inbound the way the outbound direction does
- // (relay.go): Xray runs as a genuinely separate OS process
- // (internal/xray/process.go), so it has no visibility into this process's
- // private, in-memory netstack at all -- a tunnel-internal address like
- // 10.8.1.5:8080 has no route from Xray's own freedom outbound; only code
- // holding the actual *stack.Stack can reach it. Accepted consequence:
- // forwarded-port bytes don't appear in Xray's per-email stats/quota
- // counters. This undercounts, it doesn't bypass enforcement -- a
- // depleted/disabled client's peer is dropped from the interface's peer list
- // entirely by DesiredAmneziaWGInstances, which tears its forwards down too
- // as a side effect of Reconcile's own diff below.
- package amneziawgnet
- import (
- "context"
- "fmt"
- "io"
- "net"
- "net/netip"
- "sync"
- "time"
- "gvisor.dev/gvisor/pkg/tcpip"
- "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
- "gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
- "gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
- "gvisor.dev/gvisor/pkg/tcpip/stack"
- "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
- "github.com/mhsanaei/3x-ui/v3/internal/logger"
- )
- // portForwardProto distinguishes the two sockets a single forwarded port
- // needs -- ForwardedPorts has no per-port protocol selector (matches the
- // retired DNAT implementation's own unconditional-TCP+UDP contract), so
- // every port gets both.
- type portForwardProto uint8
- const (
- tcpForward portForwardProto = iota
- udpForward
- )
- // portForwardKey identifies one listener: a specific peer's specific port on
- // a specific protocol. Two different peers (even on the same inbound)
- // forwarding the same port number get two independent listeners under two
- // independent keys -- a same-port collision surfaces as an ordinary bind
- // failure on whichever one opens second, not something actively prevented
- // here (see the migration plan's Phase 3.6 notes).
- type portForwardKey struct {
- email string
- port int
- proto portForwardProto
- }
- // portForwardTargetFunc resolves a peer's current tunnel-internal target
- // address by email, re-checked on every new connection/session rather than
- // captured once at listen time -- so a peer re-IP takes effect for the next
- // connection with zero listener churn (see Reconcile's own comment on
- // this). false means the peer has no resolvable target right now (removed,
- // or its AllowedIPs/ForwardedPorts changed): the caller drops the
- // connection/packet, and Reconcile will close the now-undesired listener
- // shortly after, if it hasn't already.
- type portForwardTargetFunc func(email string) (netip.Addr, bool)
- // portForwardListener is the common handle both listenPortForwardTCP and
- // listenPortForwardUDP return, so PortForwardSet can hold either behind one
- // map value type without a type switch.
- type portForwardListener interface {
- Close()
- }
- // PortForwardSet owns every open port-forward listener for one embedded
- // AmneziaWG interface (one per amneziawgnet managed entry -- see
- // manager.go). Unlike v6alias.go's stateless desired/diff/apply functions,
- // this holds live Go resources (net.Listener/net.PacketConn) that must be
- // explicitly closed -- there's no OS-level idempotent recreate the way
- // `ip addr add` has -- so Reconcile diffs against its own live listeners
- // map directly instead of a remembered prior Instance.
- type PortForwardSet struct {
- gstack *stack.Stack
- inboundID int
- mu sync.Mutex
- peerTargets map[string]netip.Addr
- listeners map[portForwardKey]portForwardListener
- }
- // NewPortForwardSet creates an empty supervisor for one embedded interface's
- // stack. Call Reconcile to actually open any listeners.
- func NewPortForwardSet(gstack *stack.Stack, inboundID int) *PortForwardSet {
- return &PortForwardSet{
- gstack: gstack,
- inboundID: inboundID,
- peerTargets: map[string]netip.Addr{},
- listeners: map[portForwardKey]portForwardListener{},
- }
- }
- // desiredPeerTargets resolves each peer's tunnel-internal target address:
- // the first IPv4 AllowedIPs entry, falling back to the first IPv6 entry only
- // when no v4 entry exists and the instance has IPv6 enabled (mirrors
- // desiredV6Aliases' own gating in v6alias.go -- no v6 route exists on the
- // stack otherwise). A peer with no resolvable address at all (neither
- // family, or an unparseable entry) is simply absent from the result.
- func desiredPeerTargets(inst amneziawg.Instance) map[string]netip.Addr {
- out := map[string]netip.Addr{}
- for _, p := range inst.Peers {
- if p.Email == "" {
- continue
- }
- raw := amneziawg.FirstIPv4(p.AllowedIPs)
- if raw == "" && inst.IPv6Enabled {
- raw = amneziawg.FirstIPv6(p.AllowedIPs)
- }
- if raw == "" {
- continue
- }
- addr, err := netip.ParseAddr(raw)
- if err != nil {
- continue
- }
- out[p.Email] = addr
- }
- return out
- }
- // desiredPortForwardKeys returns the full set of listener keys inst wants
- // right now: one tcpForward and one udpForward key per port in every peer's
- // ForwardedPorts spec, for every peer that also has a resolvable target
- // (see desiredPeerTargets) -- a key never exists without a target, so
- // Reconcile can always resolve one for any key it opens.
- func desiredPortForwardKeys(inst amneziawg.Instance) map[portForwardKey]struct{} {
- out := map[portForwardKey]struct{}{}
- targets := desiredPeerTargets(inst)
- for _, p := range inst.Peers {
- if p.Email == "" || p.ForwardedPorts == "" {
- continue
- }
- if _, ok := targets[p.Email]; !ok {
- continue
- }
- for _, port := range amneziawg.ExpandForwardedPorts(p.ForwardedPorts) {
- out[portForwardKey{email: p.Email, port: port, proto: tcpForward}] = struct{}{}
- out[portForwardKey{email: p.Email, port: port, proto: udpForward}] = struct{}{}
- }
- }
- return out
- }
- // Reconcile brings the supervisor's open listeners in line with what inst
- // currently wants: closes anything no longer desired, opens anything newly
- // desired, leaves everything else untouched. Never returns an error --
- // matches applyV6Aliases' contract exactly: one listener failing to bind
- // only narrows that specific forward, never a reason to fail the whole
- // reconcile.
- func (s *PortForwardSet) Reconcile(inst amneziawg.Instance) {
- wantTargets := desiredPeerTargets(inst)
- wantKeys := desiredPortForwardKeys(inst)
- s.mu.Lock()
- s.peerTargets = wantTargets
- var toClose []portForwardListener
- for key, ln := range s.listeners {
- if _, ok := wantKeys[key]; ok {
- continue
- }
- toClose = append(toClose, ln)
- delete(s.listeners, key)
- }
- var toOpen []portForwardKey
- for key := range wantKeys {
- if _, ok := s.listeners[key]; ok {
- continue
- }
- toOpen = append(toOpen, key)
- }
- s.mu.Unlock()
- // Outside the lock: closing/opening real sockets shouldn't block a
- // concurrent targetFor lookup from an in-flight connection on some
- // other, unaffected listener.
- for _, ln := range toClose {
- ln.Close()
- }
- for _, key := range toOpen {
- ln := openPortForwardListener(s.gstack, s.inboundID, key, s.targetFor)
- if ln == nil {
- continue
- }
- s.mu.Lock()
- s.listeners[key] = ln
- s.mu.Unlock()
- }
- }
- // targetFor implements portForwardTargetFunc against the supervisor's
- // current peerTargets snapshot.
- func (s *PortForwardSet) targetFor(email string) (netip.Addr, bool) {
- s.mu.Lock()
- defer s.mu.Unlock()
- addr, ok := s.peerTargets[email]
- return addr, ok
- }
- // Close tears down every open listener. Call when the owning Device is
- // closed (or rebuilt -- see manager.go's ensureLocked, which always
- // constructs a fresh PortForwardSet alongside a fresh Device.Stack, the
- // same reason it also rebuilds udpRelay from scratch rather than reusing
- // one bound to a discarded stack).
- func (s *PortForwardSet) Close() {
- s.mu.Lock()
- listeners := s.listeners
- s.listeners = map[portForwardKey]portForwardListener{}
- s.mu.Unlock()
- for _, ln := range listeners {
- ln.Close()
- }
- }
- // openPortForwardListener dispatches to the protocol-specific opener and
- // normalizes its result to a real nil interface value on failure -- a
- // (*tcpForwardListener)(nil) (or *udpForwardListener(nil)) wrapped directly
- // into the portForwardListener interface would be a non-nil interface
- // holding a nil pointer, Go's classic trap, so the concrete pointer is
- // checked before it's ever assigned into the interface-typed return.
- func openPortForwardListener(gstack *stack.Stack, inboundID int, key portForwardKey, target portForwardTargetFunc) portForwardListener {
- switch key.proto {
- case tcpForward:
- if ln := listenPortForwardTCP(gstack, inboundID, key, target); ln != nil {
- return ln
- }
- case udpForward:
- if ln := listenPortForwardUDP(gstack, inboundID, key, target); ln != nil {
- return ln
- }
- }
- return nil
- }
- const portForwardDialTimeout = 10 * time.Second
- // tunnelNetwork returns the gVisor network protocol number matching addr's
- // address family, for dialing toward it inside the embedded stack.
- func tunnelNetwork(addr netip.Addr) tcpip.NetworkProtocolNumber {
- if addr.Is4() {
- return ipv4.ProtocolNumber
- }
- return ipv6.ProtocolNumber
- }
- // tunnelFullAddress builds the tcpip.FullAddress a gonet dial needs to
- // reach addr:port inside the embedded stack -- NIC 1, matching
- // createNetTUNWithStack's own CreateNIC(1, ...) (this package's stack only
- // ever registers one NIC, and WriteUDPReply's WriteRawPacket already
- // addresses it explicitly the same way elsewhere in this package, rather
- // than relying on NIC 0's route-table auto-selection).
- func tunnelFullAddress(addr netip.Addr, port int) tcpip.FullAddress {
- return tcpip.FullAddress{NIC: 1, Addr: tcpip.AddrFromSlice(addr.AsSlice()), Port: uint16(port)}
- }
- // tcpForwardListener is one open host-facing TCP listener for a single
- // portForwardKey.
- type tcpForwardListener struct {
- ln net.Listener
- closing chan struct{}
- }
- // listenPortForwardTCP opens a host-facing TCP listener on key.port and
- // starts relaying accepted connections into the tunnel toward
- // target(key.email). A bind failure (most commonly EADDRINUSE, whether from
- // an unrelated process or another AmneziaWG peer/inbound that already
- // claimed the same port) is logged and returns nil; Reconcile treats a nil
- // result as "not open this round" and retries on every future Reconcile
- // call for as long as the key stays desired.
- func listenPortForwardTCP(gstack *stack.Stack, inboundID int, key portForwardKey, target portForwardTargetFunc) *tcpForwardListener {
- ln, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", fmt.Sprintf(":%d", key.port))
- if err != nil {
- logger.Warningf("amneziawgnet: port-forward: inbound %d peer %q: listen tcp :%d: %v", inboundID, key.email, key.port, err)
- return nil
- }
- l := &tcpForwardListener{ln: ln, closing: make(chan struct{})}
- logger.Infof("amneziawgnet: port-forward: inbound %d peer %q: listening tcp :%d", inboundID, key.email, key.port)
- go l.acceptLoop(gstack, inboundID, key, target)
- return l
- }
- func (l *tcpForwardListener) acceptLoop(gstack *stack.Stack, inboundID int, key portForwardKey, target portForwardTargetFunc) {
- for {
- conn, err := l.ln.Accept()
- if err != nil {
- select {
- case <-l.closing:
- return // intentional shutdown, not a real accept error
- default:
- }
- logger.Warningf("amneziawgnet: port-forward: inbound %d peer %q: accept tcp :%d: %v", inboundID, key.email, key.port, err)
- return
- }
- go relayTCPForward(gstack, conn, inboundID, key, target)
- }
- }
- func relayTCPForward(gstack *stack.Stack, conn net.Conn, inboundID int, key portForwardKey, target portForwardTargetFunc) {
- defer conn.Close()
- addr, ok := target(key.email)
- if !ok {
- return
- }
- ctx, cancel := context.WithTimeout(context.Background(), portForwardDialTimeout)
- defer cancel()
- tunnelConn, err := gonet.DialContextTCP(ctx, gstack, tunnelFullAddress(addr, key.port), 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)
- return
- }
- defer tunnelConn.Close()
- done := make(chan struct{}, 2)
- go func() { _, _ = io.Copy(tunnelConn, conn); done <- struct{}{} }()
- go func() { _, _ = io.Copy(conn, tunnelConn); done <- struct{}{} }()
- <-done
- }
- // Close stops accepting new connections. Already-relaying connections are
- // left to finish on their own -- there's no shared state to tear down early
- // for, and an abrupt cut would just look like a network error to whichever
- // external client was mid-transfer.
- func (l *tcpForwardListener) Close() {
- close(l.closing)
- l.ln.Close()
- }
|