wifi_linux_wpa.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. package settings
  2. import (
  3. "bufio"
  4. "context"
  5. "fmt"
  6. "net"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "sync"
  11. "sync/atomic"
  12. "time"
  13. "github.com/sagernet/sing-box/adapter"
  14. )
  15. var wpaSocketCounter atomic.Uint64
  16. type wpaSupplicantMonitor struct {
  17. socketPath string
  18. callback func(adapter.WIFIState)
  19. cancel context.CancelFunc
  20. monitorConn *net.UnixConn
  21. connMutex sync.Mutex
  22. }
  23. func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
  24. socketDirs := []string{"/var/run/wpa_supplicant", "/run/wpa_supplicant"}
  25. for _, socketDir := range socketDirs {
  26. entries, err := os.ReadDir(socketDir)
  27. if err != nil {
  28. continue
  29. }
  30. for _, entry := range entries {
  31. if entry.IsDir() || entry.Name() == "." || entry.Name() == ".." {
  32. continue
  33. }
  34. socketPath := filepath.Join(socketDir, entry.Name())
  35. id := wpaSocketCounter.Add(1)
  36. localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d-%d", os.Getpid(), id), Net: "unixgram"}
  37. remoteAddr := &net.UnixAddr{Name: socketPath, Net: "unixgram"}
  38. conn, err := net.DialUnix("unixgram", localAddr, remoteAddr)
  39. if err != nil {
  40. continue
  41. }
  42. conn.Close()
  43. return &wpaSupplicantMonitor{socketPath: socketPath, callback: callback}, nil
  44. }
  45. }
  46. return nil, os.ErrNotExist
  47. }
  48. func (m *wpaSupplicantMonitor) ReadWIFIState() adapter.WIFIState {
  49. id := wpaSocketCounter.Add(1)
  50. localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d-%d", os.Getpid(), id), Net: "unixgram"}
  51. remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"}
  52. conn, err := net.DialUnix("unixgram", localAddr, remoteAddr)
  53. if err != nil {
  54. return adapter.WIFIState{}
  55. }
  56. defer conn.Close()
  57. conn.SetDeadline(time.Now().Add(3 * time.Second))
  58. status, err := m.sendCommand(conn, "STATUS")
  59. if err != nil {
  60. return adapter.WIFIState{}
  61. }
  62. var ssid, bssid string
  63. var connected bool
  64. scanner := bufio.NewScanner(strings.NewReader(status))
  65. for scanner.Scan() {
  66. line := scanner.Text()
  67. if strings.HasPrefix(line, "wpa_state=") {
  68. state := strings.TrimPrefix(line, "wpa_state=")
  69. connected = state == "COMPLETED"
  70. } else if strings.HasPrefix(line, "ssid=") {
  71. ssid = strings.TrimPrefix(line, "ssid=")
  72. } else if strings.HasPrefix(line, "bssid=") {
  73. bssid = strings.TrimPrefix(line, "bssid=")
  74. }
  75. }
  76. if !connected || ssid == "" {
  77. return adapter.WIFIState{}
  78. }
  79. return adapter.WIFIState{
  80. SSID: ssid,
  81. BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")),
  82. }
  83. }
  84. // sendCommand sends a command to wpa_supplicant and returns the response.
  85. // Commands are sent without trailing newlines per the wpa_supplicant control
  86. // interface protocol - the official wpa_ctrl.c sends raw command strings.
  87. func (m *wpaSupplicantMonitor) sendCommand(conn *net.UnixConn, command string) (string, error) {
  88. _, err := conn.Write([]byte(command))
  89. if err != nil {
  90. return "", err
  91. }
  92. buf := make([]byte, 4096)
  93. n, err := conn.Read(buf)
  94. if err != nil {
  95. return "", err
  96. }
  97. response := string(buf[:n])
  98. if strings.HasPrefix(response, "FAIL") {
  99. return "", os.ErrInvalid
  100. }
  101. return strings.TrimSpace(response), nil
  102. }
  103. func (m *wpaSupplicantMonitor) Start() error {
  104. if m.callback == nil {
  105. return nil
  106. }
  107. ctx, cancel := context.WithCancel(context.Background())
  108. m.cancel = cancel
  109. state := m.ReadWIFIState()
  110. go m.monitorEvents(ctx, state)
  111. m.callback(state)
  112. return nil
  113. }
  114. func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adapter.WIFIState) {
  115. var consecutiveErrors int
  116. var debounceTimer *time.Timer
  117. var debounceMutex sync.Mutex
  118. localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-mon-%d", os.Getpid()), Net: "unixgram"}
  119. remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"}
  120. conn, err := net.DialUnix("unixgram", localAddr, remoteAddr)
  121. if err != nil {
  122. return
  123. }
  124. defer conn.Close()
  125. m.connMutex.Lock()
  126. m.monitorConn = conn
  127. m.connMutex.Unlock()
  128. // ATTACH/DETACH commands use os_strcmp() for exact matching in wpa_supplicant,
  129. // so they must be sent without trailing newlines.
  130. // See: https://w1.fi/cgit/hostap/tree/wpa_supplicant/ctrl_iface_unix.c
  131. _, err = conn.Write([]byte("ATTACH"))
  132. if err != nil {
  133. return
  134. }
  135. buf := make([]byte, 4096)
  136. n, err := conn.Read(buf)
  137. if err != nil || !strings.HasPrefix(string(buf[:n]), "OK") {
  138. return
  139. }
  140. for {
  141. select {
  142. case <-ctx.Done():
  143. debounceMutex.Lock()
  144. if debounceTimer != nil {
  145. debounceTimer.Stop()
  146. }
  147. debounceMutex.Unlock()
  148. conn.Write([]byte("DETACH"))
  149. return
  150. default:
  151. }
  152. conn.SetReadDeadline(time.Now().Add(30 * time.Second))
  153. n, err := conn.Read(buf)
  154. if err != nil {
  155. if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
  156. continue
  157. }
  158. select {
  159. case <-ctx.Done():
  160. return
  161. default:
  162. }
  163. consecutiveErrors++
  164. if consecutiveErrors > 10 {
  165. return
  166. }
  167. time.Sleep(time.Second)
  168. continue
  169. }
  170. consecutiveErrors = 0
  171. msg := string(buf[:n])
  172. if strings.Contains(msg, "CTRL-EVENT-CONNECTED") || strings.Contains(msg, "CTRL-EVENT-DISCONNECTED") {
  173. debounceMutex.Lock()
  174. if debounceTimer != nil {
  175. debounceTimer.Stop()
  176. }
  177. debounceTimer = time.AfterFunc(500*time.Millisecond, func() {
  178. state := m.ReadWIFIState()
  179. if state != lastState {
  180. lastState = state
  181. m.callback(state)
  182. }
  183. })
  184. debounceMutex.Unlock()
  185. }
  186. }
  187. }
  188. func (m *wpaSupplicantMonitor) Close() error {
  189. if m.cancel != nil {
  190. m.cancel()
  191. }
  192. m.connMutex.Lock()
  193. if m.monitorConn != nil {
  194. m.monitorConn.Close()
  195. }
  196. m.connMutex.Unlock()
  197. return nil
  198. }