1
0

outbound_manager.go 5.0 KB

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