package api import ( "crypto/sha256" "encoding/hex" "strings" "crypto-miner-server/internal/strategy" ) // LANSeederHint is pushed to miners on auth when fleet roles are enabled. type LANSeederHint struct { AgentID string `json:"agent_id"` IP string `json:"ip,omitempty"` LANFallbackURL string `json:"lan_fallback_url,omitempty"` } func normalizeFleetRole(role string) string { switch strings.ToLower(strings.TrimSpace(role)) { case "seeder": return "seeder" case "miner": return "miner" default: return "auto" } } func subnetPrefix24(ip string) string { ip = strings.TrimSpace(ip) if idx := strings.LastIndex(ip, ":"); idx > 0 && strings.Count(ip, ":") == 1 { ip = ip[:idx] } parts := strings.Split(ip, ".") if len(parts) < 3 { return "" } return parts[0] + "." + parts[1] + "." + parts[2] } func (h *WSHub) storeAgentFleetRole(agentID, role string) { role = normalizeFleetRole(role) if role == "auto" { role = "miner" } h.mu.Lock() tel, ok := h.agentLiveTelemetry[agentID] if !ok { tel = map[string]interface{}{} h.agentLiveTelemetry[agentID] = tel } tel["fleet_role"] = role h.mu.Unlock() } func (h *WSHub) fleetRoleHintForAuth(agentID, bakedRole, clientIP string, seederCapable bool) string { if !h.serverPolicySnapshot().FleetRolesEnabled { return "" } baked := normalizeFleetRole(bakedRole) if baked == "seeder" || baked == "miner" { return baked } subnet := subnetPrefix24(clientIP) if h.subnetHasOnlineSeeder(subnet) { return "miner" } if seederCapable && h.shouldElectSubnetSeeder(agentID, subnet) { return "seeder" } return "miner" } func (h *WSHub) subnetHasOnlineSeeder(subnet string) bool { if subnet == "" { return false } h.mu.RLock() defer h.mu.RUnlock() for id, tel := range h.agentLiveTelemetry { role, _ := tel["fleet_role"].(string) if role != "seeder" { continue } if ac, ok := h.agents[id]; ok && ac != nil { _ = ac if agentIP := h.agentIPLocked(id); subnetPrefix24(agentIP) == subnet { return true } } } return false } func (h *WSHub) shouldElectSubnetSeeder(agentID, subnet string) bool { if subnet == "" { return false } sum := sha256.Sum256([]byte(subnet)) pick := hex.EncodeToString(sum[:4]) return strings.HasPrefix(agentID, pick[:2]) || pick[0]%3 == 0 } func (h *WSHub) agentIPLocked(agentID string) string { if ag, err := h.db.GetAgent(agentID); err == nil && ag != nil { return ag.IP } return "" } func (h *WSHub) lanSeedersForMiner(clientIP string) []LANSeederHint { if !h.serverPolicySnapshot().FleetRolesEnabled { return nil } subnet := subnetPrefix24(clientIP) var out []LANSeederHint h.mu.RLock() for id, tel := range h.agentLiveTelemetry { role, _ := tel["fleet_role"].(string) if role != "seeder" { continue } ip := h.agentIPLocked(id) if subnet != "" && subnetPrefix24(ip) != subnet { continue } fallback := "" if ip != "" { fallback = "http://" + ip + ":8989/api/v1/public/webrtc-mesh/manifest?seeder=" + id } out = append(out, LANSeederHint{AgentID: id, IP: ip, LANFallbackURL: fallback}) } h.mu.RUnlock() return out } func (h *WSHub) ingestFleetPressure(agentID string, broadcast map[string]interface{}) { role, _ := broadcast["fleet_role"].(string) if role != "" { h.storeAgentFleetRole(agentID, role) } seed, seedOK := broadcast["seed_pressure"].(float64) hr, hrOK := broadcast["hashrate_pressure"].(float64) if !seedOK && !hrOK { return } heat := strategy.EmberwakeHeat(role, seed, hr) h.mu.Lock() tel, ok := h.agentLiveTelemetry[agentID] if !ok { tel = map[string]interface{}{} h.agentLiveTelemetry[agentID] = tel } if seedOK { tel["seed_pressure"] = seed } if hrOK { tel["hashrate_pressure"] = hr } tel["emberwake_heat"] = heat h.mu.Unlock() broadcast["emberwake_heat"] = heat }