| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- 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
- }
|