feat: trust peer

This commit is contained in:
2026-02-07 03:17:37 +08:00
parent d8ffc5eea5
commit f3adb56bd0
19 changed files with 438 additions and 155 deletions

View File

@@ -1,6 +1,9 @@
package discovery
import "time"
import (
"fmt"
"time"
)
// Peer 代表一个可达的网络端点 (Network Endpoint)。
// 注意:一个物理设备 (Device) 可能通过多个网络接口广播,因此会对应多个 Peer 结构体。
@@ -20,6 +23,12 @@ type Peer struct {
Port int `json:"port"`
OS OS `json:"os"`
PublicKey string `json:"pk"`
// TrustMismatch 指示该节点的公钥与本地信任列表中的公钥不匹配
// 如果为 true说明可能存在 ID 欺骗或密钥轮换
TrustMismatch bool `json:"trust_mismatch"`
}
// RouteState 记录单条路径的状态
@@ -38,8 +47,17 @@ const (
// PresencePacket 是 UDP 广播的载荷
type PresencePacket struct {
ID string `json:"id"`
Name string `json:"name"`
Port int `json:"port"`
OS OS `json:"os"`
ID string `json:"id"`
Name string `json:"name"`
Port int `json:"port"`
OS OS `json:"os"`
PublicKey string `json:"pk"`
Signature string `json:"sig"`
}
// SignPayload 生成用于签名的确定性数据
func (p *PresencePacket) SignPayload() []byte {
// 使用固定格式拼接字段,避免 JSON 序列化的不确定性
// 格式: id|name|port|os|pk
return fmt.Appendf(nil, "%s|%s|%d|%s|%s", p.ID, p.Name, p.Port, p.OS, p.PublicKey)
}

View File

@@ -5,8 +5,10 @@ import (
"fmt"
"log/slog"
"mesh-drop/internal/config"
"mesh-drop/internal/security"
"net"
"runtime"
"sort"
"sync"
"time"
@@ -15,8 +17,8 @@ import (
const (
DiscoveryPort = 9988
HeartbeatRate = 3 * time.Second
PeerTimeout = 10 * time.Second
HeartbeatRate = 1 * time.Second
PeerTimeout = 2 * time.Second
)
type Service struct {
@@ -26,9 +28,11 @@ type Service struct {
config *config.Config
FileServerPort int
// key 使用 peer.id 和 peer.ip 组合而成的 hash
// Key: peer.ID
peers map[string]*Peer
peersMutex sync.RWMutex
self Peer
}
func NewService(config *config.Config, app *application.App, port int) *Service {
@@ -38,10 +42,17 @@ func NewService(config *config.Config, app *application.App, port int) *Service
config: config,
FileServerPort: port,
peers: make(map[string]*Peer),
self: Peer{
ID: config.GetID(),
Name: config.GetHostName(),
Port: port,
OS: OS(runtime.GOOS),
PublicKey: config.PublicKey,
},
}
}
func (s *Service) GetLocalIPs() ([]string, bool) {
func GetLocalIPs() ([]string, bool) {
interfaces, err := net.Interfaces()
if err != nil {
slog.Error("Failed to get network interfaces", "error", err, "component", "discovery")
@@ -114,11 +125,22 @@ func (s *Service) startBroadcasting() {
continue
}
packet := PresencePacket{
ID: s.ID,
Name: s.config.GetHostName(),
Port: s.FileServerPort,
OS: OS(runtime.GOOS),
ID: s.ID,
Name: s.config.GetHostName(),
Port: s.FileServerPort,
OS: OS(runtime.GOOS),
PublicKey: s.config.PublicKey,
}
// 签名
sigData := packet.SignPayload()
sig, err := security.Sign(s.config.PrivateKey, sigData)
if err != nil {
slog.Error("Failed to sign discovery packet", "error", err)
continue
}
packet.Signature = sig
data, _ := json.Marshal(packet)
for _, iface := range interfaces {
// 过滤掉 Down 的接口和 Loopback 接口
@@ -195,12 +217,33 @@ func (s *Service) startListening() {
continue
}
s.handleHeartbeat(packet, remoteAddr.IP.String())
// 验证签名
sig := packet.Signature
sigData := packet.SignPayload()
valid, err := security.Verify(packet.PublicKey, sigData, sig)
if err != nil || !valid {
slog.Warn("Received invalid discovery packet signature", "id", packet.ID, "ip", remoteAddr.IP.String())
continue
}
// 验证身份一致性 (防止 ID 欺骗)
trustMismatch := false
trustedKeys := s.config.GetTrustedPeer()
if knownKey, ok := trustedKeys[packet.ID]; ok {
if knownKey != packet.PublicKey {
slog.Warn("SECURITY ALERT: Peer ID mismatch with known public key (Spoofing attempt?)", "id", packet.ID, "known_key", knownKey, "received_key", packet.PublicKey)
trustMismatch = true
// 当发现 ID 欺骗时,不更新 peer而是标记为 trustMismatch
// 用户可以手动重新添加信任
}
}
s.handleHeartbeat(packet, remoteAddr.IP.String(), trustMismatch)
}
}
// handleHeartbeat 处理心跳包
func (s *Service) handleHeartbeat(pkt PresencePacket, ip string) {
func (s *Service) handleHeartbeat(pkt PresencePacket, ip string, trustMismatch bool) {
s.peersMutex.Lock()
peer, exists := s.peers[pkt.ID]
@@ -215,19 +258,27 @@ func (s *Service) handleHeartbeat(pkt PresencePacket, ip string) {
LastSeen: time.Now(),
},
},
Port: pkt.Port,
OS: pkt.OS,
Port: pkt.Port,
OS: pkt.OS,
PublicKey: pkt.PublicKey,
TrustMismatch: trustMismatch,
}
s.peers[peer.ID] = peer
slog.Info("New device found", "name", pkt.Name, "ip", ip, "component", "discovery")
} else {
// 更新节点
peer.Name = pkt.Name
peer.OS = pkt.OS
// 只有在没有身份不匹配的情况下才更新元数据,防止欺骗攻击导致 UI 闪烁/篡改
if !trustMismatch {
peer.Name = pkt.Name
peer.OS = pkt.OS
peer.PublicKey = pkt.PublicKey
}
peer.Routes[ip] = &RouteState{
IP: ip,
LastSeen: time.Now(),
}
// 如果之前存在不匹配,即使这次匹配了,也不要重置,防止欺骗攻击
peer.TrustMismatch = peer.TrustMismatch || trustMismatch
}
s.peersMutex.Unlock()
@@ -246,7 +297,6 @@ func (s *Service) startCleanup() {
for id, peer := range s.peers {
for ip, route := range peer.Routes {
// 超过10秒没心跳认为下线
if now.Sub(route.LastSeen) > PeerTimeout {
delete(peer.Routes, ip)
changed = true
@@ -274,16 +324,24 @@ func (s *Service) Start() {
go s.startCleanup()
}
func (s *Service) GetPeerByIP(ip string) *Peer {
func (s *Service) GetPeerByIP(ip string) (*Peer, bool) {
s.peersMutex.RLock()
defer s.peersMutex.RUnlock()
for _, p := range s.peers {
if p.Routes[ip] != nil {
return p
return p, true
}
}
return nil
return nil, false
}
func (s *Service) GetPeerByID(id string) (*Peer, bool) {
s.peersMutex.RLock()
defer s.peersMutex.RUnlock()
peer, ok := s.peers[id]
return peer, ok
}
func (s *Service) GetPeers() []Peer {
@@ -294,9 +352,16 @@ func (s *Service) GetPeers() []Peer {
for _, p := range s.peers {
list = append(list, *p)
}
sort.Slice(list, func(i, j int) bool {
return list[i].Name < list[j].Name
})
return list
}
func (s *Service) GetID() string {
return s.ID
}
func (s *Service) GetSelf() Peer {
return s.self
}