package service import ( "context" "crypto/ed25519" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "fmt" "net" "net/url" "os" "strconv" "strings" "time" "github.com/gofrs/uuid/v5" "go.uber.org/zap" "wails-app/internal/dao" "wails-app/internal/global" "wails-app/internal/modules/deviceprovisioning/model/request" responseModel "wails-app/internal/modules/deviceprovisioning/model/response" ) const ( discoveryPort = 31001 discoveryPacketMax = 4 * 1024 discoveryWindow = 3 * time.Second ) type discoveryRequestPacket struct { Schema string `json:"schema"` RequestID string `json:"request_id"` IssuedAt int64 `json:"issued_at"` ExpiresAt int64 `json:"expires_at"` SystemID string `json:"system_id"` Nonce string `json:"nonce"` } // discoveryResponsePacket intentionally remains an untrusted transport envelope. // HTTPS certificate pinning and administrator confirmation establish the device trust. type discoveryResponsePacket struct { Schema string `json:"schema"` RequestID string `json:"request_id"` DeviceID string `json:"device_id"` DeviceCode string `json:"device_code"` DeviceType string `json:"device_type"` DeviceModel string `json:"device_model"` FirmwareVersion string `json:"firmware_version"` IP string `json:"ip"` ConfigURL string `json:"config_url"` PublicKeyFingerprint string `json:"public_key_fingerprint"` SigningPublicKey string `json:"signing_public_key"` Nonce string `json:"nonce"` Signature string `json:"signature"` } // Discover broadcasts discovery.request.v1 and stores deduplicated candidates for ten minutes. func (s *Service) Discover(req request.DiscoverRequest) ([]responseModel.DiscoveryResponse, error) { targets, err := discoveryBroadcastTargets(req.BroadcastAddress) if err != nil { return nil, err } requestID := uuid.Must(uuid.NewV4()).String() nonceBytes := make([]byte, 32) if _, err := rand.Read(nonceBytes); err != nil { return nil, fmt.Errorf("生成发现随机数失败: %w", err) } now := time.Now() packet := discoveryRequestPacket{ Schema: "discovery.request.v1", RequestID: requestID, IssuedAt: now.Unix(), ExpiresAt: now.Add(30 * time.Second).Unix(), SystemID: discoverySystemID(), Nonce: base64.StdEncoding.EncodeToString(nonceBytes), } body, err := json.Marshal(packet) if err != nil { return nil, err } packetConn, err := (&net.ListenConfig{Control: enableBroadcastSocketOption}).ListenPacket(context.Background(), "udp4", ":0") if err != nil { return nil, fmt.Errorf("创建 UDP 发现监听失败: %w", err) } conn, ok := packetConn.(*net.UDPConn) if !ok { _ = packetConn.Close() return nil, errors.New("创建 UDP 发现监听失败: 套接字类型异常") } defer conn.Close() send := func() error { for _, target := range targets { if _, err := conn.WriteToUDP(body, &net.UDPAddr{IP: target, Port: discoveryPort}); err != nil { return fmt.Errorf("发送 UDP 发现请求失败: %w", err) } } return nil } if err := send(); err != nil { return nil, err } deadline := now.Add(discoveryWindow) nextSend := now.Add(500 * time.Millisecond) sendCount := 1 results := make(map[string]responseModel.DiscoveryResponse) buffer := make([]byte, discoveryPacketMax+1) for time.Now().Before(deadline) { readUntil := deadline if sendCount < 3 && nextSend.Before(readUntil) { readUntil = nextSend } _ = conn.SetReadDeadline(readUntil) n, source, readErr := conn.ReadFromUDP(buffer) if readErr != nil { if netErr, ok := readErr.(net.Error); !ok || !netErr.Timeout() { return nil, fmt.Errorf("读取 UDP 发现响应失败: %w", readErr) } if sendCount < 3 && !time.Now().Before(nextSend) { if err := send(); err != nil { return nil, err } nextSend = nextSend.Add(500 * time.Millisecond) sendCount++ } continue } if n > discoveryPacketMax { discoveryLogWarn("丢弃过大的 UDP 发现响应", zap.String("source_ip", source.IP.String()), zap.Int("packet_bytes", n)) continue } discoveryLogInfo("收到 UDP 发现响应", zap.String("request_id", requestID), zap.String("source_ip", source.IP.String()), zap.Int("packet_bytes", n)) candidate, err := validateDiscoveryResponse(buffer[:n], source.IP, packet) if err != nil { discoveryLogWarn("丢弃 UDP 发现响应", zap.String("request_id", requestID), zap.String("source_ip", source.IP.String()), zap.String("reason", err.Error())) continue } expiresAt := time.Now().Add(10 * time.Minute) payload := string(buffer[:n]) if _, exists := results[candidate.DeviceID]; exists { discoveryLogInfo("忽略重复 UDP 发现响应", zap.String("request_id", requestID), zap.String("source_ip", source.IP.String()), zap.String("device_id", candidate.DeviceID)) continue } record := &dao.DeviceDiscovery{ RequestID: requestID, DeviceID: candidate.DeviceID, DeviceCode: candidate.DeviceCode, DeviceType: candidate.DeviceType, DeviceModel: candidate.DeviceModel, FirmwareVersion: candidate.FirmwareVersion, SourceIP: candidate.IP, ConfigPort: candidate.ConfigPort, Payload: payload, PublicKeyFingerprint: candidate.PublicKeyFingerprint, Signature: candidateSignature(buffer[:n]), SignatureVerified: candidate.SignatureVerified, ProvisionMethod: "auto_udp", ProvisionStatus: "unprovisioned", ExpiredAt: expiresAt, } if err := s.repo.CreateDiscovery(record); err != nil { discoveryLogWarn("保存 UDP 发现结果失败", zap.String("request_id", requestID), zap.String("source_ip", source.IP.String()), zap.String("device_id", candidate.DeviceID), zap.Error(err)) return nil, err } 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)) candidate.RequestID = requestID candidate.ExpiredAt = expiresAt results[candidate.DeviceID] = candidate } output := make([]responseModel.DiscoveryResponse, 0, len(results)) for _, result := range results { output = append(output, result) } return output, nil } func (s *Service) Discoveries() ([]responseModel.DiscoveryResponse, error) { records, err := s.repo.ActiveDiscoveries() if err != nil { return nil, err } items := make([]responseModel.DiscoveryResponse, 0, len(records)) seen := map[string]bool{} for _, record := range records { if seen[record.DeviceID] { continue } seen[record.DeviceID] = true items = append(items, responseModel.DiscoveryResponse{ RequestID: record.RequestID, DeviceID: record.DeviceID, DeviceCode: record.DeviceCode, DeviceType: record.DeviceType, DeviceModel: record.DeviceModel, FirmwareVersion: record.FirmwareVersion, IP: record.SourceIP, ConfigPort: record.ConfigPort, PublicKeyFingerprint: record.PublicKeyFingerprint, SignatureVerified: record.SignatureVerified, ProvisionMethod: record.ProvisionMethod, ProvisionStatus: record.ProvisionStatus, ExpiredAt: record.ExpiredAt, }) } return items, nil } // ReadDiscoveredIdentity establishes the HTTPS certificate identity for a UDP candidate. func (s *Service) ReadDiscoveredIdentity(deviceID string) (*responseModel.IdentityResponse, error) { record, err := s.repo.LatestDiscoveryByMethod(deviceID, "auto_udp") if err != nil { return nil, err } if record.ProvisionMethod != "auto_udp" { return nil, errors.New("该设备不是自动发现记录") } return s.ReadIdentity(request.ManualIdentityRequest{IP: record.SourceIP, Port: record.ConfigPort, ProvisionMethod: "auto_udp"}) } func validateDiscoveryResponse(raw []byte, sourceIP net.IP, request discoveryRequestPacket) (responseModel.DiscoveryResponse, error) { var packet discoveryResponsePacket if err := json.Unmarshal(raw, &packet); err != nil { return responseModel.DiscoveryResponse{}, err } if packet.Schema != "discovery.response.v1" || packet.RequestID != request.RequestID || packet.Nonce != request.Nonce || strings.TrimSpace(packet.DeviceID) == "" { return responseModel.DiscoveryResponse{}, errors.New("UDP 响应关联信息无效") } if strings.TrimSpace(packet.Signature) == "" || strings.TrimSpace(packet.SigningPublicKey) == "" { return responseModel.DiscoveryResponse{}, errors.New("UDP 响应缺少签名") } publicKey, err := base64.StdEncoding.DecodeString(packet.SigningPublicKey) if err != nil || len(publicKey) != ed25519.PublicKeySize { return responseModel.DiscoveryResponse{}, errors.New("UDP 响应签名公钥无效") } publicKeyHash := sha256.Sum256(publicKey) expectedFingerprint := fmt.Sprintf("%x", publicKeyHash) if normalizeFingerprint(packet.PublicKeyFingerprint) != expectedFingerprint { return responseModel.DiscoveryResponse{}, fmt.Errorf("UDP 响应公钥指纹不匹配: received=%q expected=SHA256:%s", packet.PublicKeyFingerprint, expectedFingerprint) } signature, err := base64.StdEncoding.DecodeString(packet.Signature) if err != nil || !ed25519.Verify(ed25519.PublicKey(publicKey), discoverySigningPayload(packet), signature) { return responseModel.DiscoveryResponse{}, errors.New("UDP 响应签名校验失败") } claimedIP := net.ParseIP(packet.IP) if claimedIP == nil || !claimedIP.Equal(sourceIP) { return responseModel.DiscoveryResponse{}, fmt.Errorf("UDP 响应 IP 与报文来源不一致: claimed_ip=%q source_ip=%q", packet.IP, sourceIP.String()) } configURL, err := url.Parse(packet.ConfigURL) if err != nil || configURL.Scheme != "https" || configURL.Hostname() != sourceIP.String() { return responseModel.DiscoveryResponse{}, errors.New("UDP 响应配置地址无效") } port := defaultConfigPort if configURL.Port() != "" { port, err = strconv.Atoi(configURL.Port()) if err != nil { return responseModel.DiscoveryResponse{}, errors.New("UDP 响应配置端口无效") } } if _, err := ValidateAddress(sourceIP.String(), port); err != nil { return responseModel.DiscoveryResponse{}, err } return responseModel.DiscoveryResponse{ DeviceID: packet.DeviceID, DeviceCode: packet.DeviceCode, DeviceType: packet.DeviceType, DeviceModel: packet.DeviceModel, FirmwareVersion: packet.FirmwareVersion, IP: sourceIP.String(), ConfigPort: port, PublicKeyFingerprint: packet.PublicKeyFingerprint, SignatureVerified: true, ProvisionMethod: "auto_udp", ProvisionStatus: "unprovisioned", }, nil } // Discovery operates without the full backend bootstrap in unit tests, so logging is optional there. func discoveryLogInfo(message string, fields ...zap.Field) { if global.GVA_LOG != nil { global.GVA_LOG.Info(message, fields...) } } func discoveryLogWarn(message string, fields ...zap.Field) { if global.GVA_LOG != nil { global.GVA_LOG.Warn(message, fields...) } } func discoverySigningPayload(packet discoveryResponsePacket) []byte { return []byte(strings.Join([]string{ packet.Schema, packet.RequestID, packet.DeviceID, packet.IP, packet.ConfigURL, packet.Nonce, }, "\n")) } func candidateSignature(raw []byte) string { var packet discoveryResponsePacket if json.Unmarshal(raw, &packet) != nil { return "" } return packet.Signature } func discoveryBroadcastTargets(rawAddress string) ([]net.IP, error) { if value := strings.TrimSpace(rawAddress); value != "" { ip := net.ParseIP(value) if ip == nil || ip.To4() == nil || ip.IsLoopback() || ip.IsUnspecified() || ip.IsMulticast() { return nil, errors.New("广播地址必须是合法 IPv4 定向广播地址") } return []net.IP{ip.To4()}, nil } interfaces, err := net.Interfaces() if err != nil { return nil, fmt.Errorf("枚举网卡失败: %w", err) } seen := map[string]bool{} var targets []net.IP for _, iface := range interfaces { if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { continue } addrs, _ := iface.Addrs() for _, address := range addrs { ipNet, ok := address.(*net.IPNet) if !ok || ipNet.IP.To4() == nil || len(ipNet.Mask) != net.IPv4len { continue } broadcast := make(net.IP, net.IPv4len) ip := ipNet.IP.To4() for i := range broadcast { broadcast[i] = ip[i] | ^ipNet.Mask[i] } if broadcast.Equal(ip) || seen[broadcast.String()] { continue } seen[broadcast.String()] = true targets = append(targets, broadcast) } } if len(targets) == 0 { return nil, errors.New("未找到可用 IPv4 网卡,请指定广播地址或使用手工 IP 接入") } return targets, nil } func discoverySystemID() string { name, err := os.Hostname() if err != nil || strings.TrimSpace(name) == "" { return "smart-parking" } return "smart-parking-" + name }