outbound_manager.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. package amneziawgnet
  2. import (
  3. "fmt"
  4. "net/netip"
  5. "strings"
  6. "sync"
  7. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  8. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  9. )
  10. // OutboundDesired pairs an instance with inbound-path DeviceOptions; AWG
  11. // parameters must be identical on both ends of a tunnel.
  12. type OutboundDesired struct {
  13. Instance amneziawg.OutboundInstance
  14. Options DeviceOptions
  15. }
  16. // managedOutbound is one running interface plus its rendered UAPI config
  17. // (no-op/reconfigure decision) and an address/MTU fingerprint.
  18. type managedOutbound struct {
  19. dev *Device
  20. uapiConfig string
  21. structFP string
  22. }
  23. // OutboundManager owns the running AmneziaWG client interfaces keyed by tag,
  24. // keeping the egress registry and listener current; callers just Reconcile.
  25. type OutboundManager struct {
  26. mu sync.Mutex
  27. iface map[string]*managedOutbound
  28. }
  29. var (
  30. outboundManagerOnce sync.Once
  31. outboundManager *OutboundManager
  32. )
  33. // GetOutboundManager returns the process-wide outbound manager singleton.
  34. func GetOutboundManager() *OutboundManager {
  35. outboundManagerOnce.Do(func() {
  36. outboundManager = &OutboundManager{iface: map[string]*managedOutbound{}}
  37. })
  38. return outboundManager
  39. }
  40. // outboundFingerprint captures what IpcSet can't change on a running Device,
  41. // fixed when the netstack is built: address, and the S4-derived effective MTU.
  42. func outboundFingerprint(inst amneziawg.OutboundInstance) string {
  43. return fmt.Sprintf("%d|%s",
  44. amneziawg.EffectiveMTU(inst.MTU, inst.Obfuscation.S4),
  45. strings.Join(inst.Address, ","))
  46. }
  47. // normalizeDNSServer normalizes a configured DNS server to host:port.
  48. func normalizeDNSServer(s string) string {
  49. s = strings.TrimSpace(s)
  50. if s == "" {
  51. return ""
  52. }
  53. if addr, err := netip.ParseAddr(s); err == nil {
  54. return netip.AddrPortFrom(addr, 53).String()
  55. }
  56. if ap, err := netip.ParseAddrPort(s); err == nil {
  57. return ap.String()
  58. }
  59. return s
  60. }
  61. // Reconcile converges devices to desired and stops removed tags; per-tick
  62. // contract of Manager.Reconcile -- errors log, never abort the batch.
  63. func (m *OutboundManager) Reconcile(desired []OutboundDesired) {
  64. m.mu.Lock()
  65. defer m.mu.Unlock()
  66. // Empty desired converges to "no tunnels": close egress listener so
  67. // 127.0.0.1:64900 stays free on installs without AWG outbounds.
  68. if len(desired) == 0 {
  69. for tag, cur := range m.iface {
  70. cur.dev.Close()
  71. GetEgressServer().DeleteStack(tag)
  72. delete(m.iface, tag)
  73. logger.Infof("amneziawgnet: stopped embedded outbound %q", tag)
  74. }
  75. GetEgressServer().Close()
  76. return
  77. }
  78. if err := GetEgressServer().Listen(); err != nil {
  79. logger.Warningf("amneziawgnet: egress listener unavailable: %v", err)
  80. }
  81. want := make(map[string]struct{}, len(desired))
  82. for _, d := range desired {
  83. want[d.Instance.Tag] = struct{}{}
  84. }
  85. for tag, cur := range m.iface {
  86. if _, ok := want[tag]; ok {
  87. continue
  88. }
  89. cur.dev.Close()
  90. GetEgressServer().DeleteStack(tag)
  91. delete(m.iface, tag)
  92. logger.Infof("amneziawgnet: stopped embedded outbound %q", tag)
  93. }
  94. for _, d := range desired {
  95. if err := m.ensureLocked(d); err != nil {
  96. logger.Warningf("amneziawgnet: reconcile failed for outbound %q: %v", d.Instance.Tag, err)
  97. }
  98. }
  99. }
  100. // ensureLocked picks no-op / reconfigure-in-place / rebuild for one desired
  101. // outbound (address/MTU are fixed at netstack build time).
  102. func (m *OutboundManager) ensureLocked(d OutboundDesired) error {
  103. inst, opts := d.Instance, d.Options
  104. if opts.Logger == nil {
  105. opts.Logger = verboseLoggerIfEnabled(0)
  106. }
  107. fp := outboundFingerprint(inst)
  108. conf, err := buildClientUAPIConfig(inst, opts)
  109. if err != nil {
  110. return fmt.Errorf("render UAPI config: %w", err)
  111. }
  112. cur, exists := m.iface[inst.Tag]
  113. if exists && cur.structFP == fp {
  114. if conf == cur.uapiConfig {
  115. GetEgressServer().SetStack(inst.Tag, cur.dev, inst.DNS)
  116. return nil
  117. }
  118. if err := cur.dev.IpcSet(conf); err != nil {
  119. return fmt.Errorf("reconfigure outbound %q: %w", inst.Tag, err)
  120. }
  121. cur.uapiConfig = conf
  122. GetEgressServer().SetStack(inst.Tag, cur.dev, inst.DNS)
  123. return nil
  124. }
  125. if exists {
  126. cur.dev.Close()
  127. // A failed rebuild must not leave stackFor handing out a closed device.
  128. GetEgressServer().DeleteStack(inst.Tag)
  129. delete(m.iface, inst.Tag)
  130. }
  131. dev, err := newUnconfiguredClientDevice(inst, opts)
  132. if err != nil {
  133. return err
  134. }
  135. if err := dev.ConfigureClient(inst, opts); err != nil {
  136. return err
  137. }
  138. m.iface[inst.Tag] = &managedOutbound{dev: dev, uapiConfig: conf, structFP: fp}
  139. GetEgressServer().SetStack(inst.Tag, dev, inst.DNS)
  140. logger.Infof("amneziawgnet: started embedded outbound %s (%d peers)", inst.Tag, len(inst.Peers))
  141. return nil
  142. }
  143. // Remove tears down one outbound's device by tag.
  144. func (m *OutboundManager) Remove(tag string) {
  145. m.mu.Lock()
  146. defer m.mu.Unlock()
  147. cur, exists := m.iface[tag]
  148. if !exists {
  149. return
  150. }
  151. cur.dev.Close()
  152. GetEgressServer().DeleteStack(tag)
  153. delete(m.iface, tag)
  154. logger.Infof("amneziawgnet: stopped embedded outbound %q", tag)
  155. }
  156. // StopAll tears down every managed outbound device and the egress listener;
  157. // m.mu stays held across Close so a cron tick cannot re-bind mid-teardown.
  158. func (m *OutboundManager) StopAll() {
  159. m.mu.Lock()
  160. defer m.mu.Unlock()
  161. for tag, cur := range m.iface {
  162. cur.dev.Close()
  163. GetEgressServer().DeleteStack(tag)
  164. delete(m.iface, tag)
  165. }
  166. GetEgressServer().Close()
  167. }
  168. // HasRunning reports whether any outbound device is currently managed.
  169. func (m *OutboundManager) HasRunning() bool {
  170. m.mu.Lock()
  171. defer m.mu.Unlock()
  172. return len(m.iface) > 0
  173. }