inbound_amneziawg.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "strings"
  7. "gorm.io/gorm"
  8. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  9. "github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
  10. "github.com/mhsanaei/3x-ui/v3/internal/database"
  11. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  12. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  13. wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
  14. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  15. )
  16. // DesiredAmneziaWGInstances derives the AmneziaWG interfaces this panel
  17. // should be running: one instance per enabled local AmneziaWG inbound,
  18. // serving only the peers of clients that are both enabled in the inbound
  19. // settings and not depletion-disabled in client_traffics. That is the same
  20. // effective peer set buildInboundForLocalRuntime pushes on interactive edits,
  21. // so the reconcile job and the push path agree on one fingerprint — see
  22. // DesiredMtprotoInstances, which this mirrors exactly.
  23. func (s *InboundService) DesiredAmneziaWGInstances() ([]amneziawg.Instance, error) {
  24. db := database.GetDB()
  25. var inbounds []*model.Inbound
  26. err := db.Model(model.Inbound{}).
  27. Where("protocol = ? AND enable = ? AND node_id IS NULL", model.AmneziaWG, true).
  28. Find(&inbounds).Error
  29. if err != nil {
  30. return nil, err
  31. }
  32. if len(inbounds) == 0 {
  33. return nil, nil
  34. }
  35. ids := make([]int, 0, len(inbounds))
  36. for _, ib := range inbounds {
  37. ids = append(ids, ib.Id)
  38. }
  39. var disabledRows []xray.ClientTraffic
  40. err = db.Model(xray.ClientTraffic{}).
  41. Where("inbound_id IN ? AND enable = ?", ids, false).
  42. Select("inbound_id", "email").
  43. Find(&disabledRows).Error
  44. if err != nil {
  45. return nil, err
  46. }
  47. disabled := make(map[int]map[string]struct{}, len(disabledRows))
  48. for _, row := range disabledRows {
  49. if disabled[row.InboundId] == nil {
  50. disabled[row.InboundId] = map[string]struct{}{}
  51. }
  52. disabled[row.InboundId][row.Email] = struct{}{}
  53. }
  54. instances := make([]amneziawg.Instance, 0, len(inbounds))
  55. for _, ib := range inbounds {
  56. inst, ok := amneziawg.InstanceFromInbound(ib)
  57. if !ok {
  58. continue
  59. }
  60. if off := disabled[ib.Id]; len(off) > 0 {
  61. kept := make([]amneziawg.Peer, 0, len(inst.Peers))
  62. for _, p := range inst.Peers {
  63. if _, skip := off[p.Email]; !skip {
  64. kept = append(kept, p)
  65. }
  66. }
  67. inst.Peers = kept
  68. }
  69. if len(inst.Peers) == 0 {
  70. continue
  71. }
  72. instances = append(instances, inst)
  73. }
  74. return instances, nil
  75. }
  76. // applyLocalAmneziaWG pushes a single local AmneziaWG inbound's current peer
  77. // set to its interface right after a client edit commits, so an add,
  78. // removal, re-key or enable-toggle takes effect immediately instead of
  79. // waiting up to 10s for the reconcile job. It re-reads the inbound so it sees
  80. // the committed settings, filters depleted clients exactly like the
  81. // reconcile job, and is a no-op for node-owned or non-AmneziaWG inbounds.
  82. // Failures are logged and swallowed: the reconcile job is the backstop.
  83. // Mirrors applyLocalMtproto.
  84. func (s *InboundService) applyLocalAmneziaWG(inboundId int) {
  85. inbound, err := s.GetInbound(inboundId)
  86. if err != nil || inbound == nil || inbound.Protocol != model.AmneziaWG || inbound.NodeID != nil {
  87. return
  88. }
  89. rt, err := s.runtimeFor(inbound)
  90. if err != nil {
  91. return
  92. }
  93. payload := inbound
  94. if inbound.Enable {
  95. if built, bErr := s.buildInboundForLocalRuntime(database.GetDB(), inbound); bErr == nil {
  96. payload = built
  97. }
  98. }
  99. if err := rt.UpdateInbound(context.Background(), inbound, payload); err != nil {
  100. logger.Debugf("amneziawg: immediate apply failed for inbound %d: %v", inboundId, err)
  101. }
  102. }
  103. // defaultAmneziaWGServer builds a fresh server block: a random AmneziaWG 3.1
  104. // obfuscation set, the default tunnel subnet/DNS, and a freshly generated
  105. // keypair.
  106. func defaultAmneziaWGServer() (*amneziawg.ServerSettings, error) {
  107. obf := amneziawg.GenerateObfuscation31()
  108. server := &amneziawg.ServerSettings{
  109. SubnetIP: "10.8.1.0",
  110. SubnetCIDR: 24,
  111. PrimaryDNS: "8.8.8.8",
  112. SecondaryDNS: "8.8.4.4",
  113. Jc: obf.Jc,
  114. Jmin: obf.Jmin,
  115. Jmax: obf.Jmax,
  116. S1: obf.S1,
  117. S2: obf.S2,
  118. S3: obf.S3,
  119. S4: obf.S4,
  120. H1: obf.H1,
  121. H2: obf.H2,
  122. H3: obf.H3,
  123. H4: obf.H4,
  124. I1: obf.I1,
  125. HeaderProtectionKey: obf.HeaderProtectionKey,
  126. ContentPaddingAddition: obf.ContentPaddingAddition,
  127. RekeyAfterTime: obf.RekeyAfterTime,
  128. RekeyTimeout: obf.RekeyTimeout,
  129. RejectAfterTime: obf.RejectAfterTime,
  130. KeepaliveTimeout: obf.KeepaliveTimeout,
  131. MaxHandshakeAttempts: obf.MaxHandshakeAttempts,
  132. RandomTrailers: obf.RandomTrailers,
  133. DisableCookies: obf.DisableCookies,
  134. }
  135. if err := fillAmneziaWGServerKeys(server); err != nil {
  136. return nil, err
  137. }
  138. return server, nil
  139. }
  140. // fillAmneziaWGServerKeys generates a real WireGuard-compatible keypair for
  141. // the server block when one is missing.
  142. func fillAmneziaWGServerKeys(server *amneziawg.ServerSettings) error {
  143. priv, pub, err := wgutil.GenerateWireguardKeypair()
  144. if err != nil {
  145. return fmt.Errorf("amneziawg: generate server keypair: %w", err)
  146. }
  147. server.PrivateKey = priv
  148. server.PublicKey = pub
  149. return nil
  150. }
  151. // resolveAmneziaWGServerKeys settles the server keypair for a save. An omitted
  152. // key means "unchanged", never "mint a new one": rotating it silently
  153. // invalidates every client config already handed out.
  154. func resolveAmneziaWGServerKeys(server *amneziawg.ServerSettings, oldSettings string) error {
  155. if server.PrivateKey == "" {
  156. storedPriv, storedPub := storedAmneziaWGServerKeys(oldSettings)
  157. if storedPriv == "" {
  158. return fillAmneziaWGServerKeys(server)
  159. }
  160. server.PrivateKey, server.PublicKey = storedPriv, storedPub
  161. }
  162. if server.PublicKey == "" {
  163. pub, err := wgutil.PublicKeyFromPrivate(server.PrivateKey)
  164. if err != nil {
  165. return fmt.Errorf("amneziawg: derive server public key: %w", err)
  166. }
  167. server.PublicKey = pub
  168. }
  169. return nil
  170. }
  171. // storedAmneziaWGServerKeys returns the keypair already saved for this inbound.
  172. // oldSettings is empty on a first save, and need not be valid AmneziaWG JSON.
  173. func storedAmneziaWGServerKeys(oldSettings string) (priv, pub string) {
  174. if strings.TrimSpace(oldSettings) == "" {
  175. return "", ""
  176. }
  177. var prev amneziawg.InboundSettings
  178. if err := json.Unmarshal([]byte(oldSettings), &prev); err != nil || prev.Server == nil {
  179. return "", ""
  180. }
  181. return prev.Server.PrivateKey, prev.Server.PublicKey
  182. }
  183. // normalizeAmneziaWGSettings ensures an AmneziaWG inbound's settings have a
  184. // valid server block, generating one (fresh obfuscation params + keypair) on
  185. // first save and validating a manually-edited one so a bad entry can't bring
  186. // the interface down on the next apply. A no-op for every other protocol.
  187. func (s *InboundService) normalizeAmneziaWGSettings(inbound *model.Inbound, oldSettings string) error {
  188. if inbound.Protocol != model.AmneziaWG {
  189. return nil
  190. }
  191. trimmed := strings.TrimSpace(inbound.Settings)
  192. if trimmed == "" || trimmed == "null" || trimmed == "{}" {
  193. server, err := defaultAmneziaWGServer()
  194. if err != nil {
  195. return err
  196. }
  197. settings := amneziawg.InboundSettings{Server: server, Clients: []model.Client{}}
  198. bs, err := json.MarshalIndent(settings, "", " ")
  199. if err != nil {
  200. return err
  201. }
  202. inbound.Settings = string(bs)
  203. return nil
  204. }
  205. var parsed amneziawg.InboundSettings
  206. if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil {
  207. return fmt.Errorf("amneziawg: invalid settings: %w", err)
  208. }
  209. if parsed.Server == nil {
  210. server, err := defaultAmneziaWGServer()
  211. if err != nil {
  212. return err
  213. }
  214. parsed.Server = server
  215. } else if err := resolveAmneziaWGServerKeys(parsed.Server, oldSettings); err != nil {
  216. return err
  217. }
  218. parsed.Server.HeaderProtectionKey = strings.TrimSpace(parsed.Server.HeaderProtectionKey)
  219. for _, f := range []*string{
  220. &parsed.Server.ContentPaddingAddition, &parsed.Server.RekeyAfterTime,
  221. &parsed.Server.RekeyTimeout, &parsed.Server.RejectAfterTime,
  222. &parsed.Server.KeepaliveTimeout, &parsed.Server.MaxHandshakeAttempts,
  223. } {
  224. *f = amneziawg.CanonicalizeUintRange(*f)
  225. }
  226. if err := amneziawg.ValidateObfuscation(parsed.Server.Obfuscation()); err != nil {
  227. return fmt.Errorf("amneziawg: %w", err)
  228. }
  229. if err := amneziawg.ValidateIPv6Subnet(parsed.Server.IPv6Enabled, parsed.Server.IPv6Subnet); err != nil {
  230. return fmt.Errorf("amneziawg: %w", err)
  231. }
  232. if err := amneziawg.ValidateSubnetIPv4(parsed.Server.SubnetIP, parsed.Server.SubnetCIDR); err != nil {
  233. return fmt.Errorf("amneziawg: %w", err)
  234. }
  235. if err := amneziawg.ValidateInterfaceName(parsed.Server.ExternalInterface); err != nil {
  236. return fmt.Errorf("amneziawg: externalInterface: %w", err)
  237. }
  238. if err := amneziawg.ValidateInterfaceName(parsed.Server.IPv6ExternalInterface); err != nil {
  239. return fmt.Errorf("amneziawg: ipv6ExternalInterface: %w", err)
  240. }
  241. if err := amneziawg.ValidateConfigValue("privateKey", parsed.Server.PrivateKey); err != nil {
  242. return fmt.Errorf("amneziawg: %w", err)
  243. }
  244. if err := amneziawg.ValidateConfigValue("publicKey", parsed.Server.PublicKey); err != nil {
  245. return fmt.Errorf("amneziawg: %w", err)
  246. }
  247. signaturePackets := []struct{ field, v string }{
  248. {"i1", parsed.Server.I1},
  249. {"i2", parsed.Server.I2},
  250. {"i3", parsed.Server.I3},
  251. {"i4", parsed.Server.I4},
  252. {"i5", parsed.Server.I5},
  253. }
  254. for _, sp := range signaturePackets {
  255. if err := amneziawg.ValidateConfigValue(sp.field, sp.v); err != nil {
  256. return fmt.Errorf("amneziawg: %w", err)
  257. }
  258. }
  259. portCtx, err := s.loadPortConflictContext(database.GetDB())
  260. if err != nil {
  261. return err
  262. }
  263. for i := range parsed.Clients {
  264. c := &parsed.Clients[i]
  265. if hit := s.checkForwardedPortsConflict(portCtx, c.ForwardedPorts); hit != "" {
  266. return fmt.Errorf("amneziawg: client %q forwardedPorts collides with %s", c.Email, hit)
  267. }
  268. if err := amneziawg.ValidateConfigValue("email", c.Email); err != nil {
  269. return fmt.Errorf("amneziawg: %w", err)
  270. }
  271. if err := amneziawg.ValidateConfigValue("publicKey", c.PublicKey); err != nil {
  272. return fmt.Errorf("amneziawg: client %q: %w", c.Email, err)
  273. }
  274. if err := amneziawg.ValidateConfigValue("preSharedKey", c.PreSharedKey); err != nil {
  275. return fmt.Errorf("amneziawg: client %q: %w", c.Email, err)
  276. }
  277. // AllowedIPs lands verbatim in a rendered [Peer] block, so a newline here
  278. // re-opens an [Interface] section whose PostUp runs as root once the
  279. // downloaded config is applied (client app, or awg-quick directly).
  280. normalized, err := normalizeWireguardAllowedIPs(c.AllowedIPs)
  281. if err != nil {
  282. return fmt.Errorf("amneziawg: client %q: %w", c.Email, err)
  283. }
  284. // An enabled peer with no address is skipped by InstanceFromInbound, and
  285. // if it was the only one the whole inbound never starts, silently.
  286. if c.Enable && len(normalized) == 0 {
  287. return fmt.Errorf("amneziawg: client %q: allowedIPs is required", c.Email)
  288. }
  289. c.AllowedIPs = normalized
  290. }
  291. bs, err := json.MarshalIndent(parsed, "", " ")
  292. if err != nil {
  293. return err
  294. }
  295. inbound.Settings = string(bs)
  296. return nil
  297. }
  298. // portConflictContext caches the state checkForwardedPortsConflict needs —
  299. // the panel's own port and this host's enabled inbound ports — so validating
  300. // N clients in one save (normalizeAmneziaWGSettings, or a bulk client add)
  301. // costs one query total instead of N. Load it once with
  302. // loadPortConflictContext and pass it to every checkForwardedPortsConflict
  303. // call in that batch.
  304. type portConflictContext struct {
  305. webPort int
  306. inbounds []*model.Inbound
  307. }
  308. // loadPortConflictContext loads the panel's own port and every enabled
  309. // inbound hosted on THIS panel (node_id IS NULL) — an inbound hosted on a
  310. // different node listens on that node's own host, never this one, so it can
  311. // never collide with a DNAT rule this process installs.
  312. func (s *InboundService) loadPortConflictContext(db *gorm.DB) (portConflictContext, error) {
  313. var ctx portConflictContext
  314. if webPort, err := (&SettingService{}).GetPort(); err == nil {
  315. ctx.webPort = webPort
  316. }
  317. err := db.Model(model.Inbound{}).
  318. Where("enable = ? AND node_id IS NULL", true).
  319. Find(&ctx.inbounds).Error
  320. return ctx, err
  321. }
  322. // checkForwardedPortsConflict reports whether a client's ForwardedPorts spec
  323. // exceeds the cap, covers the panel's own web port, one of this host's own
  324. // enabled inbound listen ports, or an AmneziaWG inbound's own phantom SOCKS5
  325. // relay port (SOCKSPortForInbound -- never a real inbounds row, so the loop
  326. // below can't see it any other way). A collision on the SOCKS5 port would
  327. // let a port-forward listener race Xray's own relay for the bind and, if it
  328. // wins, take down that inbound's entire relay rather than just one forward.
  329. // Returns a human-readable description of the first collision found, or ""
  330. // when there is none.
  331. func (s *InboundService) checkForwardedPortsConflict(ctx portConflictContext, forwardedPorts string) string {
  332. if forwardedPorts == "" {
  333. return ""
  334. }
  335. if amneziawg.ExceedsForwardedPortsCap(forwardedPorts) {
  336. return fmt.Sprintf("more than %d forwarded ports", amneziawg.MaxForwardedPorts)
  337. }
  338. if ctx.webPort > 0 && amneziawg.ForwardedPortsInclude(forwardedPorts, ctx.webPort) {
  339. return fmt.Sprintf("the panel's own port (%d)", ctx.webPort)
  340. }
  341. for _, ib := range ctx.inbounds {
  342. if amneziawg.ForwardedPortsInclude(forwardedPorts, ib.Port) {
  343. name := ib.Remark
  344. if name == "" {
  345. name = ib.Tag
  346. }
  347. return fmt.Sprintf("inbound '%s' (#%d, port %d)", name, ib.Id, ib.Port)
  348. }
  349. if ib.Protocol != model.AmneziaWG {
  350. continue
  351. }
  352. socksPort := amneziawgnet.SOCKSPortForInbound(ib.Id)
  353. if amneziawg.ForwardedPortsInclude(forwardedPorts, socksPort) {
  354. name := ib.Remark
  355. if name == "" {
  356. name = ib.Tag
  357. }
  358. return fmt.Sprintf("inbound '%s' (#%d)'s own SOCKS5 relay port (%d)", name, ib.Id, socksPort)
  359. }
  360. }
  361. return ""
  362. }
  363. // GetAmneziaWGDiagnostics returns a live diagnostics snapshot for inbound
  364. // id: interface up/down, listen port, and per-client handshake/traffic
  365. // state, read entirely from data amneziawgnet.Manager already tracks --
  366. // gathering it can never itself change anything. Returns an error only
  367. // when id doesn't name an AmneziaWG inbound at all; an inbound that simply
  368. // isn't running right now (disabled, no enabled clients, or reconcile
  369. // hasn't caught up yet) comes back as amneziawgnet.Diagnostics{}
  370. // (Running=false), not an error, since that's a normal state an admin
  371. // might specifically be checking for.
  372. func (s *InboundService) GetAmneziaWGDiagnostics(id int) (amneziawgnet.Diagnostics, error) {
  373. inbound, err := s.GetInbound(id)
  374. if err != nil {
  375. return amneziawgnet.Diagnostics{}, err
  376. }
  377. if inbound.Protocol != model.AmneziaWG {
  378. return amneziawgnet.Diagnostics{}, fmt.Errorf("inbound %d is not an AmneziaWG inbound", id)
  379. }
  380. inst, ok := amneziawg.InstanceFromInbound(inbound)
  381. if !ok {
  382. return amneziawgnet.Diagnostics{}, nil
  383. }
  384. return amneziawgnet.Diagnose(inst.Id, inst.Peers), nil
  385. }