v6alias.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. // Phase 3.5: restoring each opted-in peer's distinct public IPv6 source
  2. // identity for peer-initiated outbound connections. The retired
  3. // kernel-module architecture used NDP-proxying (ip -6 neigh add proxy) to
  4. // hand inbound traffic off to a real awg<N> kernel interface — this path has
  5. // no such interface at all (the tunnel lives entirely inside an in-process
  6. // gVisor netstack), so there is nothing for NDP-proxying to forward into.
  7. // Scoped to what this path actually needs — a peer's own outbound
  8. // connections carrying a distinct source address, not unsolicited inbound
  9. // connections toward the peer (that's the separate, not-yet-built Phase
  10. // 3.6 port-forwarding) — a host-owned address alias is sufficient and
  11. // simpler: once the kernel genuinely owns the address, Xray's freedom
  12. // outbound can bind an egress socket to it, and return traffic lands on a
  13. // normal, locally-owned address with no forwarding or NDP-proxy involved.
  14. package amneziawgnet
  15. import (
  16. "bytes"
  17. "context"
  18. "os/exec"
  19. "strings"
  20. "time"
  21. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  22. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  23. )
  24. // v6Alias is one host-owned IPv6 address alias this package manages, always
  25. // applied as a /128 regardless of whatever prefix width the peer's own
  26. // AllowedIPs entry happens to use.
  27. type v6Alias struct {
  28. Addr string
  29. Iface string
  30. }
  31. // effectiveIPv6ExternalInterface returns IPv6ExternalInterface if the admin
  32. // set one, falling back to ExternalInterface — matches the frontend's own
  33. // ipv6ExternalInterfaceHint copy ("Leave empty to reuse External
  34. // Interface") and the retired kernel-module PostUp's identical fallback.
  35. func effectiveIPv6ExternalInterface(inst amneziawg.Instance) string {
  36. if inst.IPv6ExternalInterface != "" {
  37. return inst.IPv6ExternalInterface
  38. }
  39. return inst.ExternalInterface
  40. }
  41. // V6AliasesActive reports whether inst is fully configured for per-peer IPv6
  42. // identity. The Xray-side v6 egress injector must use this exact gate too —
  43. // see xray.go's injectAmneziawgV6Egress — so the two halves can't diverge.
  44. func V6AliasesActive(inst amneziawg.Instance) bool {
  45. return inst.IPv6Enabled && effectiveIPv6ExternalInterface(inst) != ""
  46. }
  47. // desiredV6Aliases returns the aliases inst wants right now, keyed by peer
  48. // email. Empty whenever inst isn't fully configured for this feature
  49. // (IPv6Enabled false, or no usable interface either way) — deliberately
  50. // what makes "IPv6 toggled off" fall out of diffV6Aliases for free, rather
  51. // than a separate branch anywhere else.
  52. func desiredV6Aliases(inst amneziawg.Instance) map[string]v6Alias {
  53. out := map[string]v6Alias{}
  54. if !V6AliasesActive(inst) {
  55. return out
  56. }
  57. iface := effectiveIPv6ExternalInterface(inst)
  58. for _, p := range inst.Peers {
  59. if p.Email == "" {
  60. continue
  61. }
  62. if addr := amneziawg.FirstIPv6(p.AllowedIPs); addr != "" {
  63. out[p.Email] = v6Alias{Addr: addr, Iface: iface}
  64. }
  65. }
  66. return out
  67. }
  68. // diffV6Aliases returns the ip -6 addr add/del calls needed to move the
  69. // host from oldInst's alias set to newInst's. Pass amneziawg.Instance{} as
  70. // oldInst for "nothing was aliased before" (a brand new instance) and as
  71. // newInst for "tear down entirely" (Remove/StopAll/Reconcile's stop-loop).
  72. // A peer whose alias is unchanged appears in neither slice — the common
  73. // case on every steady-state reconcile tick, so a healthy system issues no
  74. // exec calls at all most of the time.
  75. func diffV6Aliases(oldInst, newInst amneziawg.Instance) (add, remove []v6Alias) {
  76. oldSet, newSet := desiredV6Aliases(oldInst), desiredV6Aliases(newInst)
  77. for email, oldAlias := range oldSet {
  78. if newAlias, ok := newSet[email]; ok && newAlias == oldAlias {
  79. continue
  80. }
  81. remove = append(remove, oldAlias)
  82. }
  83. for email, newAlias := range newSet {
  84. if oldAlias, ok := oldSet[email]; ok && oldAlias == newAlias {
  85. continue
  86. }
  87. add = append(add, newAlias)
  88. }
  89. return add, remove
  90. }
  91. // runIP is the seam tests swap to assert exact invocations without a real
  92. // ip binary — this package has no internal/database dependency, so
  93. // everything except this var's real invocation builds and unit-tests fine
  94. // even on a non-Linux dev machine; the real command is verified manually
  95. // against a Linux VPS, matching this project's established verification
  96. // pattern for other OS-effecting AmneziaWG changes.
  97. var runIP = func(ctx context.Context, args ...string) (stderr string, err error) {
  98. cmd := exec.CommandContext(ctx, "ip", args...)
  99. var buf bytes.Buffer
  100. cmd.Stderr = &buf
  101. err = cmd.Run()
  102. return buf.String(), err
  103. }
  104. const ipCommandTimeout = 3 * time.Second
  105. // applyV6Aliases runs every add before any remove, so a peer whose address
  106. // changed is never briefly unaliased (briefly having both old and new
  107. // aliased at once is harmless). Never surfaces an error — an alias failing
  108. // only narrows that one peer's own outbound-source-identity feature, never
  109. // a reason to fail the tunnel or its SOCKS5 relay.
  110. func applyV6Aliases(add, remove []v6Alias) {
  111. for _, a := range add {
  112. addV6Alias(a)
  113. }
  114. for _, a := range remove {
  115. removeV6Alias(a)
  116. }
  117. }
  118. func addV6Alias(a v6Alias) {
  119. ctx, cancel := context.WithTimeout(context.Background(), ipCommandTimeout)
  120. defer cancel()
  121. // nodad: this address is a specific peer's own admin-assigned identity,
  122. // nothing else on the link should ever claim it, so the ~1s Duplicate
  123. // Address Detection window before the kernel would otherwise mark it
  124. // usable is pure latency with no real collision to detect.
  125. stderr, err := runIP(ctx, "-6", "addr", "add", a.Addr+"/128", "dev", a.Iface, "nodad")
  126. if err == nil {
  127. logger.Infof("amneziawgnet: aliased IPv6 address %s onto %s", a.Addr, a.Iface)
  128. return
  129. }
  130. if strings.Contains(stderr, "File exists") {
  131. // Already the desired end state -- most commonly hit once, harmlessly,
  132. // right after an ungraceful panel restart (the OS-level alias from
  133. // before the crash outlives the process; the in-memory managed map
  134. // doesn't).
  135. return
  136. }
  137. logger.Warningf("amneziawgnet: alias IPv6 address %s onto %s: %v (%s)", a.Addr, a.Iface, err, strings.TrimSpace(stderr))
  138. }
  139. func removeV6Alias(a v6Alias) {
  140. ctx, cancel := context.WithTimeout(context.Background(), ipCommandTimeout)
  141. defer cancel()
  142. stderr, err := runIP(ctx, "-6", "addr", "del", a.Addr+"/128", "dev", a.Iface)
  143. if err == nil {
  144. logger.Infof("amneziawgnet: removed IPv6 alias %s from %s", a.Addr, a.Iface)
  145. return
  146. }
  147. if strings.Contains(stderr, "Cannot assign requested address") || strings.Contains(stderr, "Cannot find device") {
  148. // Already gone (the address itself, or the whole interface) -- for a
  149. // delete, the desired end state ("not aliased here") already holds.
  150. return
  151. }
  152. logger.Warningf("amneziawgnet: remove IPv6 alias %s from %s: %v (%s)", a.Addr, a.Iface, err, strings.TrimSpace(stderr))
  153. }