diagnostics.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. package amneziawgnet
  2. import (
  3. "bufio"
  4. "strconv"
  5. "strings"
  6. "time"
  7. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  8. "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
  9. )
  10. // ClientDiagnostic is one configured peer's live state, cross-referenced
  11. // from the running Device's own UAPI dump against the peer list the caller
  12. // supplies. A peer that has never handshaked still appears here (with a
  13. // zero LastHandshake) rather than being silently absent, so an admin can
  14. // tell "misconfigured client" apart from "client just hasn't connected".
  15. type ClientDiagnostic struct {
  16. Email string
  17. LastHandshake time.Time
  18. RxBytes uint64
  19. TxBytes uint64
  20. Endpoint string
  21. // AllowedIPs is comma-joined, from the running Device's own UAPI dump
  22. // when it has ever handshaked (so a re-IP is reflected immediately);
  23. // falls back to the peer's configured AllowedIPs otherwise.
  24. AllowedIPs string
  25. }
  26. // Connected reports whether this client has ever completed a handshake.
  27. func (c ClientDiagnostic) Connected() bool {
  28. return !c.LastHandshake.IsZero()
  29. }
  30. // Diagnostics is a read-only snapshot of one embedded AmneziaWG inbound's
  31. // live state. Gathering it can never itself change anything -- it only
  32. // reads the already-running Device's own UAPI dump, never writes to it.
  33. type Diagnostics struct {
  34. Running bool
  35. ListenPort int
  36. Clients []ClientDiagnostic
  37. }
  38. // Diagnose builds a live snapshot for inbound id, cross-referenced against
  39. // peers (the inbound's currently configured client list -- the caller
  40. // supplies this since amneziawgnet has no DB access of its own). Running
  41. // stays false (with an empty Clients list) when there's no managed
  42. // instance for this id right now -- disabled, not yet reconciled, or never
  43. // started -- which is itself useful information for an admin, not an error.
  44. func Diagnose(id int, peers []amneziawg.Peer) Diagnostics {
  45. dev, _, ok := GetManager().Lookup(id)
  46. if !ok {
  47. return Diagnostics{}
  48. }
  49. return diagnoseDevice(dev, peers)
  50. }
  51. func diagnoseDevice(dev *Device, peers []amneziawg.Peer) Diagnostics {
  52. diag := Diagnostics{Running: true}
  53. dump, err := dev.IpcGet()
  54. if err != nil {
  55. return diag
  56. }
  57. listenPort, states := parseUAPIDump(dump)
  58. diag.ListenPort = listenPort
  59. diag.Clients = make([]ClientDiagnostic, 0, len(peers))
  60. for _, p := range peers {
  61. hexKey, err := wireguard.KeyToHex(p.PublicKey)
  62. if err != nil {
  63. continue
  64. }
  65. st := states[hexKey]
  66. cd := ClientDiagnostic{Email: p.Email, RxBytes: st.rxBytes, TxBytes: st.txBytes, Endpoint: st.endpoint}
  67. if st.lastHandshakeSec > 0 {
  68. cd.LastHandshake = time.Unix(st.lastHandshakeSec, 0)
  69. }
  70. if len(st.allowedIPs) > 0 {
  71. cd.AllowedIPs = strings.Join(st.allowedIPs, ", ")
  72. } else {
  73. cd.AllowedIPs = strings.Join(p.AllowedIPs, ", ")
  74. }
  75. diag.Clients = append(diag.Clients, cd)
  76. }
  77. return diag
  78. }
  79. type peerUAPIState struct {
  80. lastHandshakeSec int64
  81. rxBytes, txBytes uint64
  82. endpoint string
  83. allowedIPs []string
  84. }
  85. // parseUAPIDump reads a Device.IpcGet() text dump: device-level keys first
  86. // (only listen_port matters here), then one block per peer starting with
  87. // its own public_key= line. Keyed by lowercase-hex public key, matching
  88. // the hex form IpcGet itself emits (see wireguard.KeyToHex for the same
  89. // base64-to-hex conversion applied to our own stored keys before lookup).
  90. func parseUAPIDump(dump string) (listenPort int, states map[string]peerUAPIState) {
  91. states = make(map[string]peerUAPIState)
  92. var current string
  93. scanner := bufio.NewScanner(strings.NewReader(dump))
  94. for scanner.Scan() {
  95. key, value, ok := strings.Cut(scanner.Text(), "=")
  96. if !ok {
  97. continue
  98. }
  99. switch key {
  100. case "listen_port":
  101. listenPort, _ = strconv.Atoi(value)
  102. case "public_key":
  103. current = value
  104. states[current] = peerUAPIState{}
  105. case "last_handshake_time_sec":
  106. st := states[current]
  107. st.lastHandshakeSec, _ = strconv.ParseInt(value, 10, 64)
  108. states[current] = st
  109. case "rx_bytes":
  110. st := states[current]
  111. st.rxBytes, _ = strconv.ParseUint(value, 10, 64)
  112. states[current] = st
  113. case "tx_bytes":
  114. st := states[current]
  115. st.txBytes, _ = strconv.ParseUint(value, 10, 64)
  116. states[current] = st
  117. case "endpoint":
  118. st := states[current]
  119. st.endpoint = value
  120. states[current] = st
  121. case "allowed_ip":
  122. st := states[current]
  123. st.allowedIPs = append(st.allowedIPs, value)
  124. states[current] = st
  125. }
  126. }
  127. return listenPort, states
  128. }