discovery.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. package service
  2. import (
  3. "context"
  4. "crypto/ed25519"
  5. "crypto/rand"
  6. "crypto/sha256"
  7. "encoding/base64"
  8. "encoding/json"
  9. "errors"
  10. "fmt"
  11. "net"
  12. "net/url"
  13. "os"
  14. "strconv"
  15. "strings"
  16. "time"
  17. "github.com/gofrs/uuid/v5"
  18. "go.uber.org/zap"
  19. "wails-app/internal/dao"
  20. "wails-app/internal/global"
  21. "wails-app/internal/modules/deviceprovisioning/model/request"
  22. responseModel "wails-app/internal/modules/deviceprovisioning/model/response"
  23. )
  24. const (
  25. discoveryPort = 31001
  26. discoveryPacketMax = 4 * 1024
  27. discoveryWindow = 3 * time.Second
  28. )
  29. type discoveryRequestPacket struct {
  30. Schema string `json:"schema"`
  31. RequestID string `json:"request_id"`
  32. IssuedAt int64 `json:"issued_at"`
  33. ExpiresAt int64 `json:"expires_at"`
  34. SystemID string `json:"system_id"`
  35. Nonce string `json:"nonce"`
  36. }
  37. // discoveryResponsePacket intentionally remains an untrusted transport envelope.
  38. // HTTPS certificate pinning and administrator confirmation establish the device trust.
  39. type discoveryResponsePacket struct {
  40. Schema string `json:"schema"`
  41. RequestID string `json:"request_id"`
  42. DeviceID string `json:"device_id"`
  43. DeviceCode string `json:"device_code"`
  44. DeviceType string `json:"device_type"`
  45. DeviceModel string `json:"device_model"`
  46. FirmwareVersion string `json:"firmware_version"`
  47. IP string `json:"ip"`
  48. ConfigURL string `json:"config_url"`
  49. PublicKeyFingerprint string `json:"public_key_fingerprint"`
  50. SigningPublicKey string `json:"signing_public_key"`
  51. Nonce string `json:"nonce"`
  52. Signature string `json:"signature"`
  53. }
  54. // Discover broadcasts discovery.request.v1 and stores deduplicated candidates for ten minutes.
  55. func (s *Service) Discover(req request.DiscoverRequest) ([]responseModel.DiscoveryResponse, error) {
  56. targets, err := discoveryBroadcastTargets(req.BroadcastAddress)
  57. if err != nil {
  58. return nil, err
  59. }
  60. requestID := uuid.Must(uuid.NewV4()).String()
  61. nonceBytes := make([]byte, 32)
  62. if _, err := rand.Read(nonceBytes); err != nil {
  63. return nil, fmt.Errorf("生成发现随机数失败: %w", err)
  64. }
  65. now := time.Now()
  66. packet := discoveryRequestPacket{
  67. Schema: "discovery.request.v1", RequestID: requestID, IssuedAt: now.Unix(),
  68. ExpiresAt: now.Add(30 * time.Second).Unix(), SystemID: discoverySystemID(),
  69. Nonce: base64.StdEncoding.EncodeToString(nonceBytes),
  70. }
  71. body, err := json.Marshal(packet)
  72. if err != nil {
  73. return nil, err
  74. }
  75. packetConn, err := (&net.ListenConfig{Control: enableBroadcastSocketOption}).ListenPacket(context.Background(), "udp4", ":0")
  76. if err != nil {
  77. return nil, fmt.Errorf("创建 UDP 发现监听失败: %w", err)
  78. }
  79. conn, ok := packetConn.(*net.UDPConn)
  80. if !ok {
  81. _ = packetConn.Close()
  82. return nil, errors.New("创建 UDP 发现监听失败: 套接字类型异常")
  83. }
  84. defer conn.Close()
  85. send := func() error {
  86. for _, target := range targets {
  87. if _, err := conn.WriteToUDP(body, &net.UDPAddr{IP: target, Port: discoveryPort}); err != nil {
  88. return fmt.Errorf("发送 UDP 发现请求失败: %w", err)
  89. }
  90. }
  91. return nil
  92. }
  93. if err := send(); err != nil {
  94. return nil, err
  95. }
  96. deadline := now.Add(discoveryWindow)
  97. nextSend := now.Add(500 * time.Millisecond)
  98. sendCount := 1
  99. results := make(map[string]responseModel.DiscoveryResponse)
  100. buffer := make([]byte, discoveryPacketMax+1)
  101. for time.Now().Before(deadline) {
  102. readUntil := deadline
  103. if sendCount < 3 && nextSend.Before(readUntil) {
  104. readUntil = nextSend
  105. }
  106. _ = conn.SetReadDeadline(readUntil)
  107. n, source, readErr := conn.ReadFromUDP(buffer)
  108. if readErr != nil {
  109. if netErr, ok := readErr.(net.Error); !ok || !netErr.Timeout() {
  110. return nil, fmt.Errorf("读取 UDP 发现响应失败: %w", readErr)
  111. }
  112. if sendCount < 3 && !time.Now().Before(nextSend) {
  113. if err := send(); err != nil {
  114. return nil, err
  115. }
  116. nextSend = nextSend.Add(500 * time.Millisecond)
  117. sendCount++
  118. }
  119. continue
  120. }
  121. if n > discoveryPacketMax {
  122. discoveryLogWarn("丢弃过大的 UDP 发现响应", zap.String("source_ip", source.IP.String()), zap.Int("packet_bytes", n))
  123. continue
  124. }
  125. discoveryLogInfo("收到 UDP 发现响应", zap.String("request_id", requestID), zap.String("source_ip", source.IP.String()), zap.Int("packet_bytes", n))
  126. candidate, err := validateDiscoveryResponse(buffer[:n], source.IP, packet)
  127. if err != nil {
  128. discoveryLogWarn("丢弃 UDP 发现响应", zap.String("request_id", requestID), zap.String("source_ip", source.IP.String()), zap.String("reason", err.Error()))
  129. continue
  130. }
  131. expiresAt := time.Now().Add(10 * time.Minute)
  132. payload := string(buffer[:n])
  133. if _, exists := results[candidate.DeviceID]; exists {
  134. discoveryLogInfo("忽略重复 UDP 发现响应", zap.String("request_id", requestID), zap.String("source_ip", source.IP.String()), zap.String("device_id", candidate.DeviceID))
  135. continue
  136. }
  137. record := &dao.DeviceDiscovery{
  138. RequestID: requestID,
  139. DeviceID: candidate.DeviceID, DeviceCode: candidate.DeviceCode, DeviceType: candidate.DeviceType,
  140. DeviceModel: candidate.DeviceModel, FirmwareVersion: candidate.FirmwareVersion,
  141. SourceIP: candidate.IP, ConfigPort: candidate.ConfigPort, Payload: payload,
  142. PublicKeyFingerprint: candidate.PublicKeyFingerprint, Signature: candidateSignature(buffer[:n]),
  143. SignatureVerified: candidate.SignatureVerified,
  144. ProvisionMethod: "auto_udp", ProvisionStatus: "unprovisioned", ExpiredAt: expiresAt,
  145. }
  146. if err := s.repo.CreateDiscovery(record); err != nil {
  147. discoveryLogWarn("保存 UDP 发现结果失败", zap.String("request_id", requestID), zap.String("source_ip", source.IP.String()), zap.String("device_id", candidate.DeviceID), zap.Error(err))
  148. return nil, err
  149. }
  150. discoveryLogInfo("UDP 发现响应校验并保存成功", zap.String("request_id", requestID), zap.String("source_ip", source.IP.String()), zap.String("device_id", candidate.DeviceID), zap.String("device_code", candidate.DeviceCode), zap.Int("config_port", candidate.ConfigPort))
  151. candidate.RequestID = requestID
  152. candidate.ExpiredAt = expiresAt
  153. results[candidate.DeviceID] = candidate
  154. }
  155. output := make([]responseModel.DiscoveryResponse, 0, len(results))
  156. for _, result := range results {
  157. output = append(output, result)
  158. }
  159. return output, nil
  160. }
  161. func (s *Service) Discoveries() ([]responseModel.DiscoveryResponse, error) {
  162. records, err := s.repo.ActiveDiscoveries()
  163. if err != nil {
  164. return nil, err
  165. }
  166. items := make([]responseModel.DiscoveryResponse, 0, len(records))
  167. seen := map[string]bool{}
  168. for _, record := range records {
  169. if seen[record.DeviceID] {
  170. continue
  171. }
  172. seen[record.DeviceID] = true
  173. items = append(items, responseModel.DiscoveryResponse{
  174. RequestID: record.RequestID, DeviceID: record.DeviceID, DeviceCode: record.DeviceCode,
  175. DeviceType: record.DeviceType, DeviceModel: record.DeviceModel, FirmwareVersion: record.FirmwareVersion,
  176. IP: record.SourceIP, ConfigPort: record.ConfigPort, PublicKeyFingerprint: record.PublicKeyFingerprint,
  177. SignatureVerified: record.SignatureVerified, ProvisionMethod: record.ProvisionMethod,
  178. ProvisionStatus: record.ProvisionStatus, ExpiredAt: record.ExpiredAt,
  179. })
  180. }
  181. return items, nil
  182. }
  183. // ReadDiscoveredIdentity establishes the HTTPS certificate identity for a UDP candidate.
  184. func (s *Service) ReadDiscoveredIdentity(deviceID string) (*responseModel.IdentityResponse, error) {
  185. record, err := s.repo.LatestDiscoveryByMethod(deviceID, "auto_udp")
  186. if err != nil {
  187. return nil, err
  188. }
  189. if record.ProvisionMethod != "auto_udp" {
  190. return nil, errors.New("该设备不是自动发现记录")
  191. }
  192. return s.ReadIdentity(request.ManualIdentityRequest{IP: record.SourceIP, Port: record.ConfigPort, ProvisionMethod: "auto_udp"})
  193. }
  194. func validateDiscoveryResponse(raw []byte, sourceIP net.IP, request discoveryRequestPacket) (responseModel.DiscoveryResponse, error) {
  195. var packet discoveryResponsePacket
  196. if err := json.Unmarshal(raw, &packet); err != nil {
  197. return responseModel.DiscoveryResponse{}, err
  198. }
  199. if packet.Schema != "discovery.response.v1" || packet.RequestID != request.RequestID || packet.Nonce != request.Nonce || strings.TrimSpace(packet.DeviceID) == "" {
  200. return responseModel.DiscoveryResponse{}, errors.New("UDP 响应关联信息无效")
  201. }
  202. if strings.TrimSpace(packet.Signature) == "" || strings.TrimSpace(packet.SigningPublicKey) == "" {
  203. return responseModel.DiscoveryResponse{}, errors.New("UDP 响应缺少签名")
  204. }
  205. publicKey, err := base64.StdEncoding.DecodeString(packet.SigningPublicKey)
  206. if err != nil || len(publicKey) != ed25519.PublicKeySize {
  207. return responseModel.DiscoveryResponse{}, errors.New("UDP 响应签名公钥无效")
  208. }
  209. publicKeyHash := sha256.Sum256(publicKey)
  210. expectedFingerprint := fmt.Sprintf("%x", publicKeyHash)
  211. if normalizeFingerprint(packet.PublicKeyFingerprint) != expectedFingerprint {
  212. return responseModel.DiscoveryResponse{}, fmt.Errorf("UDP 响应公钥指纹不匹配: received=%q expected=SHA256:%s", packet.PublicKeyFingerprint, expectedFingerprint)
  213. }
  214. signature, err := base64.StdEncoding.DecodeString(packet.Signature)
  215. if err != nil || !ed25519.Verify(ed25519.PublicKey(publicKey), discoverySigningPayload(packet), signature) {
  216. return responseModel.DiscoveryResponse{}, errors.New("UDP 响应签名校验失败")
  217. }
  218. claimedIP := net.ParseIP(packet.IP)
  219. if claimedIP == nil || !claimedIP.Equal(sourceIP) {
  220. return responseModel.DiscoveryResponse{}, fmt.Errorf("UDP 响应 IP 与报文来源不一致: claimed_ip=%q source_ip=%q", packet.IP, sourceIP.String())
  221. }
  222. configURL, err := url.Parse(packet.ConfigURL)
  223. if err != nil || configURL.Scheme != "https" || configURL.Hostname() != sourceIP.String() {
  224. return responseModel.DiscoveryResponse{}, errors.New("UDP 响应配置地址无效")
  225. }
  226. port := defaultConfigPort
  227. if configURL.Port() != "" {
  228. port, err = strconv.Atoi(configURL.Port())
  229. if err != nil {
  230. return responseModel.DiscoveryResponse{}, errors.New("UDP 响应配置端口无效")
  231. }
  232. }
  233. if _, err := ValidateAddress(sourceIP.String(), port); err != nil {
  234. return responseModel.DiscoveryResponse{}, err
  235. }
  236. return responseModel.DiscoveryResponse{
  237. DeviceID: packet.DeviceID, DeviceCode: packet.DeviceCode, DeviceType: packet.DeviceType,
  238. DeviceModel: packet.DeviceModel, FirmwareVersion: packet.FirmwareVersion, IP: sourceIP.String(),
  239. ConfigPort: port, PublicKeyFingerprint: packet.PublicKeyFingerprint, SignatureVerified: true, ProvisionMethod: "auto_udp",
  240. ProvisionStatus: "unprovisioned",
  241. }, nil
  242. }
  243. // Discovery operates without the full backend bootstrap in unit tests, so logging is optional there.
  244. func discoveryLogInfo(message string, fields ...zap.Field) {
  245. if global.GVA_LOG != nil {
  246. global.GVA_LOG.Info(message, fields...)
  247. }
  248. }
  249. func discoveryLogWarn(message string, fields ...zap.Field) {
  250. if global.GVA_LOG != nil {
  251. global.GVA_LOG.Warn(message, fields...)
  252. }
  253. }
  254. func discoverySigningPayload(packet discoveryResponsePacket) []byte {
  255. return []byte(strings.Join([]string{
  256. packet.Schema, packet.RequestID, packet.DeviceID, packet.IP, packet.ConfigURL, packet.Nonce,
  257. }, "\n"))
  258. }
  259. func candidateSignature(raw []byte) string {
  260. var packet discoveryResponsePacket
  261. if json.Unmarshal(raw, &packet) != nil {
  262. return ""
  263. }
  264. return packet.Signature
  265. }
  266. func discoveryBroadcastTargets(rawAddress string) ([]net.IP, error) {
  267. if value := strings.TrimSpace(rawAddress); value != "" {
  268. ip := net.ParseIP(value)
  269. if ip == nil || ip.To4() == nil || ip.IsLoopback() || ip.IsUnspecified() || ip.IsMulticast() {
  270. return nil, errors.New("广播地址必须是合法 IPv4 定向广播地址")
  271. }
  272. return []net.IP{ip.To4()}, nil
  273. }
  274. interfaces, err := net.Interfaces()
  275. if err != nil {
  276. return nil, fmt.Errorf("枚举网卡失败: %w", err)
  277. }
  278. seen := map[string]bool{}
  279. var targets []net.IP
  280. for _, iface := range interfaces {
  281. if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
  282. continue
  283. }
  284. addrs, _ := iface.Addrs()
  285. for _, address := range addrs {
  286. ipNet, ok := address.(*net.IPNet)
  287. if !ok || ipNet.IP.To4() == nil || len(ipNet.Mask) != net.IPv4len {
  288. continue
  289. }
  290. broadcast := make(net.IP, net.IPv4len)
  291. ip := ipNet.IP.To4()
  292. for i := range broadcast {
  293. broadcast[i] = ip[i] | ^ipNet.Mask[i]
  294. }
  295. if broadcast.Equal(ip) || seen[broadcast.String()] {
  296. continue
  297. }
  298. seen[broadcast.String()] = true
  299. targets = append(targets, broadcast)
  300. }
  301. }
  302. if len(targets) == 0 {
  303. return nil, errors.New("未找到可用 IPv4 网卡,请指定广播地址或使用手工 IP 接入")
  304. }
  305. return targets, nil
  306. }
  307. func discoverySystemID() string {
  308. name, err := os.Hostname()
  309. if err != nil || strings.TrimSpace(name) == "" {
  310. return "smart-parking"
  311. }
  312. return "smart-parking-" + name
  313. }