portfwd.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. // Phase 3.6: per-client port-forwarding. A real Go listener bound to each
  2. // forwarded external port relays into the peer's own tunnel-internal
  3. // address via a direct gonet dial -- the mirror image of
  4. // AttachTCPForwarder/AttachUDPHandler (which relay FROM the tunnel TO the
  5. // real world), and this path's replacement for the retired kernel-module
  6. // architecture's PostUp/PostDown iptables DNAT rules: there's no real OS
  7. // network interface here for DNAT to rewrite packets on, the same root
  8. // reason Phase 3.5's IPv6 alias mechanism couldn't reuse NDP-proxy either.
  9. //
  10. // Deliberately dials straight into the gVisor stack rather than relaying
  11. // through Xray's own SOCKS5 inbound the way the outbound direction does
  12. // (relay.go): Xray runs as a genuinely separate OS process
  13. // (internal/xray/process.go), so it has no visibility into this process's
  14. // private, in-memory netstack at all -- a tunnel-internal address like
  15. // 10.8.1.5:8080 has no route from Xray's own freedom outbound; only code
  16. // holding the actual *stack.Stack can reach it. Accepted consequence:
  17. // forwarded-port bytes don't appear in Xray's per-email stats/quota
  18. // counters. This undercounts, it doesn't bypass enforcement -- a
  19. // depleted/disabled client's peer is dropped from the interface's peer list
  20. // entirely by DesiredAmneziaWGInstances, which tears its forwards down too
  21. // as a side effect of Reconcile's own diff below.
  22. package amneziawgnet
  23. import (
  24. "context"
  25. "fmt"
  26. "io"
  27. "net"
  28. "net/netip"
  29. "sync"
  30. "time"
  31. "gvisor.dev/gvisor/pkg/tcpip"
  32. "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
  33. "gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
  34. "gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
  35. "gvisor.dev/gvisor/pkg/tcpip/stack"
  36. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  37. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  38. )
  39. // portForwardProto distinguishes the two sockets a single forwarded port
  40. // needs -- ForwardedPorts has no per-port protocol selector (matches the
  41. // retired DNAT implementation's own unconditional-TCP+UDP contract), so
  42. // every port gets both.
  43. type portForwardProto uint8
  44. const (
  45. tcpForward portForwardProto = iota
  46. udpForward
  47. )
  48. // portForwardKey identifies one listener: a specific peer's specific port on
  49. // a specific protocol. Two different peers (even on the same inbound)
  50. // forwarding the same port number get two independent listeners under two
  51. // independent keys -- a same-port collision surfaces as an ordinary bind
  52. // failure on whichever one opens second, not something actively prevented
  53. // here (see the migration plan's Phase 3.6 notes).
  54. type portForwardKey struct {
  55. email string
  56. port int
  57. proto portForwardProto
  58. }
  59. // portForwardTargetFunc resolves a peer's current tunnel-internal target
  60. // address by email, re-checked on every new connection/session rather than
  61. // captured once at listen time -- so a peer re-IP takes effect for the next
  62. // connection with zero listener churn (see Reconcile's own comment on
  63. // this). false means the peer has no resolvable target right now (removed,
  64. // or its AllowedIPs/ForwardedPorts changed): the caller drops the
  65. // connection/packet, and Reconcile will close the now-undesired listener
  66. // shortly after, if it hasn't already.
  67. type portForwardTargetFunc func(email string) (netip.Addr, bool)
  68. // portForwardListener is the common handle both listenPortForwardTCP and
  69. // listenPortForwardUDP return, so PortForwardSet can hold either behind one
  70. // map value type without a type switch.
  71. type portForwardListener interface {
  72. Close()
  73. }
  74. // PortForwardSet owns every open port-forward listener for one embedded
  75. // AmneziaWG interface (one per amneziawgnet managed entry -- see
  76. // manager.go). Unlike v6alias.go's stateless desired/diff/apply functions,
  77. // this holds live Go resources (net.Listener/net.PacketConn) that must be
  78. // explicitly closed -- there's no OS-level idempotent recreate the way
  79. // `ip addr add` has -- so Reconcile diffs against its own live listeners
  80. // map directly instead of a remembered prior Instance.
  81. type PortForwardSet struct {
  82. gstack *stack.Stack
  83. inboundID int
  84. mu sync.Mutex
  85. peerTargets map[string]netip.Addr
  86. listeners map[portForwardKey]portForwardListener
  87. }
  88. // NewPortForwardSet creates an empty supervisor for one embedded interface's
  89. // stack. Call Reconcile to actually open any listeners.
  90. func NewPortForwardSet(gstack *stack.Stack, inboundID int) *PortForwardSet {
  91. return &PortForwardSet{
  92. gstack: gstack,
  93. inboundID: inboundID,
  94. peerTargets: map[string]netip.Addr{},
  95. listeners: map[portForwardKey]portForwardListener{},
  96. }
  97. }
  98. // desiredPeerTargets resolves each peer's tunnel-internal target address:
  99. // the first IPv4 AllowedIPs entry, falling back to the first IPv6 entry only
  100. // when no v4 entry exists and the instance has IPv6 enabled (mirrors
  101. // desiredV6Aliases' own gating in v6alias.go -- no v6 route exists on the
  102. // stack otherwise). A peer with no resolvable address at all (neither
  103. // family, or an unparseable entry) is simply absent from the result.
  104. func desiredPeerTargets(inst amneziawg.Instance) map[string]netip.Addr {
  105. out := map[string]netip.Addr{}
  106. for _, p := range inst.Peers {
  107. if p.Email == "" {
  108. continue
  109. }
  110. raw := amneziawg.FirstIPv4(p.AllowedIPs)
  111. if raw == "" && inst.IPv6Enabled {
  112. raw = amneziawg.FirstIPv6(p.AllowedIPs)
  113. }
  114. if raw == "" {
  115. continue
  116. }
  117. addr, err := netip.ParseAddr(raw)
  118. if err != nil {
  119. continue
  120. }
  121. out[p.Email] = addr
  122. }
  123. return out
  124. }
  125. // desiredPortForwardKeys returns the full set of listener keys inst wants
  126. // right now: one tcpForward and one udpForward key per port in every peer's
  127. // ForwardedPorts spec, for every peer that also has a resolvable target
  128. // (see desiredPeerTargets) -- a key never exists without a target, so
  129. // Reconcile can always resolve one for any key it opens.
  130. func desiredPortForwardKeys(inst amneziawg.Instance) map[portForwardKey]struct{} {
  131. out := map[portForwardKey]struct{}{}
  132. targets := desiredPeerTargets(inst)
  133. for _, p := range inst.Peers {
  134. if p.Email == "" || p.ForwardedPorts == "" {
  135. continue
  136. }
  137. if _, ok := targets[p.Email]; !ok {
  138. continue
  139. }
  140. for _, port := range amneziawg.ExpandForwardedPorts(p.ForwardedPorts) {
  141. out[portForwardKey{email: p.Email, port: port, proto: tcpForward}] = struct{}{}
  142. out[portForwardKey{email: p.Email, port: port, proto: udpForward}] = struct{}{}
  143. }
  144. }
  145. return out
  146. }
  147. // Reconcile brings the supervisor's open listeners in line with what inst
  148. // currently wants: closes anything no longer desired, opens anything newly
  149. // desired, leaves everything else untouched. Never returns an error --
  150. // matches applyV6Aliases' contract exactly: one listener failing to bind
  151. // only narrows that specific forward, never a reason to fail the whole
  152. // reconcile.
  153. func (s *PortForwardSet) Reconcile(inst amneziawg.Instance) {
  154. wantTargets := desiredPeerTargets(inst)
  155. wantKeys := desiredPortForwardKeys(inst)
  156. s.mu.Lock()
  157. s.peerTargets = wantTargets
  158. var toClose []portForwardListener
  159. for key, ln := range s.listeners {
  160. if _, ok := wantKeys[key]; ok {
  161. continue
  162. }
  163. toClose = append(toClose, ln)
  164. delete(s.listeners, key)
  165. }
  166. var toOpen []portForwardKey
  167. for key := range wantKeys {
  168. if _, ok := s.listeners[key]; ok {
  169. continue
  170. }
  171. toOpen = append(toOpen, key)
  172. }
  173. s.mu.Unlock()
  174. // Outside the lock: closing/opening real sockets shouldn't block a
  175. // concurrent targetFor lookup from an in-flight connection on some
  176. // other, unaffected listener.
  177. for _, ln := range toClose {
  178. ln.Close()
  179. }
  180. for _, key := range toOpen {
  181. ln := openPortForwardListener(s.gstack, s.inboundID, key, s.targetFor)
  182. if ln == nil {
  183. continue
  184. }
  185. s.mu.Lock()
  186. s.listeners[key] = ln
  187. s.mu.Unlock()
  188. }
  189. }
  190. // targetFor implements portForwardTargetFunc against the supervisor's
  191. // current peerTargets snapshot.
  192. func (s *PortForwardSet) targetFor(email string) (netip.Addr, bool) {
  193. s.mu.Lock()
  194. defer s.mu.Unlock()
  195. addr, ok := s.peerTargets[email]
  196. return addr, ok
  197. }
  198. // Close tears down every open listener. Call when the owning Device is
  199. // closed (or rebuilt -- see manager.go's ensureLocked, which always
  200. // constructs a fresh PortForwardSet alongside a fresh Device.Stack, the
  201. // same reason it also rebuilds udpRelay from scratch rather than reusing
  202. // one bound to a discarded stack).
  203. func (s *PortForwardSet) Close() {
  204. s.mu.Lock()
  205. listeners := s.listeners
  206. s.listeners = map[portForwardKey]portForwardListener{}
  207. s.mu.Unlock()
  208. for _, ln := range listeners {
  209. ln.Close()
  210. }
  211. }
  212. // openPortForwardListener dispatches to the protocol-specific opener and
  213. // normalizes its result to a real nil interface value on failure -- a
  214. // (*tcpForwardListener)(nil) (or *udpForwardListener(nil)) wrapped directly
  215. // into the portForwardListener interface would be a non-nil interface
  216. // holding a nil pointer, Go's classic trap, so the concrete pointer is
  217. // checked before it's ever assigned into the interface-typed return.
  218. func openPortForwardListener(gstack *stack.Stack, inboundID int, key portForwardKey, target portForwardTargetFunc) portForwardListener {
  219. switch key.proto {
  220. case tcpForward:
  221. if ln := listenPortForwardTCP(gstack, inboundID, key, target); ln != nil {
  222. return ln
  223. }
  224. case udpForward:
  225. if ln := listenPortForwardUDP(gstack, inboundID, key, target); ln != nil {
  226. return ln
  227. }
  228. }
  229. return nil
  230. }
  231. const portForwardDialTimeout = 10 * time.Second
  232. // tunnelNetwork returns the gVisor network protocol number matching addr's
  233. // address family, for dialing toward it inside the embedded stack.
  234. func tunnelNetwork(addr netip.Addr) tcpip.NetworkProtocolNumber {
  235. if addr.Is4() {
  236. return ipv4.ProtocolNumber
  237. }
  238. return ipv6.ProtocolNumber
  239. }
  240. // tunnelFullAddress builds the tcpip.FullAddress a gonet dial needs to
  241. // reach addr:port inside the embedded stack -- NIC 1, matching
  242. // createNetTUNWithStack's own CreateNIC(1, ...) (this package's stack only
  243. // ever registers one NIC, and WriteUDPReply's WriteRawPacket already
  244. // addresses it explicitly the same way elsewhere in this package, rather
  245. // than relying on NIC 0's route-table auto-selection).
  246. func tunnelFullAddress(addr netip.Addr, port int) tcpip.FullAddress {
  247. return tcpip.FullAddress{NIC: 1, Addr: tcpip.AddrFromSlice(addr.AsSlice()), Port: uint16(port)}
  248. }
  249. // tcpForwardListener is one open host-facing TCP listener for a single
  250. // portForwardKey.
  251. type tcpForwardListener struct {
  252. ln net.Listener
  253. closing chan struct{}
  254. }
  255. // listenPortForwardTCP opens a host-facing TCP listener on key.port and
  256. // starts relaying accepted connections into the tunnel toward
  257. // target(key.email). A bind failure (most commonly EADDRINUSE, whether from
  258. // an unrelated process or another AmneziaWG peer/inbound that already
  259. // claimed the same port) is logged and returns nil; Reconcile treats a nil
  260. // result as "not open this round" and retries on every future Reconcile
  261. // call for as long as the key stays desired.
  262. func listenPortForwardTCP(gstack *stack.Stack, inboundID int, key portForwardKey, target portForwardTargetFunc) *tcpForwardListener {
  263. ln, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", fmt.Sprintf(":%d", key.port))
  264. if err != nil {
  265. logger.Warningf("amneziawgnet: port-forward: inbound %d peer %q: listen tcp :%d: %v", inboundID, key.email, key.port, err)
  266. return nil
  267. }
  268. l := &tcpForwardListener{ln: ln, closing: make(chan struct{})}
  269. logger.Infof("amneziawgnet: port-forward: inbound %d peer %q: listening tcp :%d", inboundID, key.email, key.port)
  270. go l.acceptLoop(gstack, inboundID, key, target)
  271. return l
  272. }
  273. func (l *tcpForwardListener) acceptLoop(gstack *stack.Stack, inboundID int, key portForwardKey, target portForwardTargetFunc) {
  274. for {
  275. conn, err := l.ln.Accept()
  276. if err != nil {
  277. select {
  278. case <-l.closing:
  279. return // intentional shutdown, not a real accept error
  280. default:
  281. }
  282. logger.Warningf("amneziawgnet: port-forward: inbound %d peer %q: accept tcp :%d: %v", inboundID, key.email, key.port, err)
  283. return
  284. }
  285. go relayTCPForward(gstack, conn, inboundID, key, target)
  286. }
  287. }
  288. func relayTCPForward(gstack *stack.Stack, conn net.Conn, inboundID int, key portForwardKey, target portForwardTargetFunc) {
  289. defer conn.Close()
  290. addr, ok := target(key.email)
  291. if !ok {
  292. return
  293. }
  294. ctx, cancel := context.WithTimeout(context.Background(), portForwardDialTimeout)
  295. defer cancel()
  296. tunnelConn, err := gonet.DialContextTCP(ctx, gstack, tunnelFullAddress(addr, key.port), tunnelNetwork(addr))
  297. if err != nil {
  298. logger.Warningf("amneziawgnet: port-forward: inbound %d peer %q: dial tunnel %s:%d: %v", inboundID, key.email, addr, key.port, err)
  299. return
  300. }
  301. defer tunnelConn.Close()
  302. done := make(chan struct{}, 2)
  303. go func() { _, _ = io.Copy(tunnelConn, conn); done <- struct{}{} }()
  304. go func() { _, _ = io.Copy(conn, tunnelConn); done <- struct{}{} }()
  305. <-done
  306. }
  307. // Close stops accepting new connections. Already-relaying connections are
  308. // left to finish on their own -- there's no shared state to tear down early
  309. // for, and an abrupt cut would just look like a network error to whichever
  310. // external client was mid-transfer.
  311. func (l *tcpForwardListener) Close() {
  312. close(l.closing)
  313. l.ln.Close()
  314. }