portfwd.go 13 KB

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