// Package spreadrouter computes BGP-style spread routes from Path Tracer sessions, // agent clearance, lane success history, and latency. package spreadrouter import ( "net" "strings" "crypto-miner-server/internal/clearance" ) // SpreadMinClearance is the minimum clearance required for spread actions (L2). const SpreadMinClearance = clearance.L2 // HopSnapshot is one agent hop in an active Path Tracer session. type HopSnapshot struct { AgentID string AgentName string Subnet string SessionID string HopIndex int Connected bool } // SubnetDiscovery links a discovering hop to hosts on a target subnet. type SubnetDiscovery struct { Subnet string AgentID string Hosts []string } // SessionSnapshot is routable state from one Path Tracer session. type SessionSnapshot struct { SessionID string Hops []HopSnapshot Discoveries []SubnetDiscovery } // FleetAgentSnapshot is a connected fleet agent used for routing. type FleetAgentSnapshot struct { AgentID string AgentName string Subnet string Clearance int LatencyMs int JoinLane string Connected bool } // LaneSuccessStat is historical join-lane success on a subnet. type LaneSuccessStat struct { Subnet string JoinLane string Success int } // Input feeds the route table builder. type Input struct { Sessions []SessionSnapshot FleetAgents []FleetAgentSnapshot LaneSuccess []LaneSuccessStat TargetSubnets []string RequestedLane string ErasureLanesEnabled bool } // RouteEdge is a weighted edge from a seed hop to a target subnet. type RouteEdge struct { FromAgentID string `json:"from_agent_id"` FromAgentName string `json:"from_agent_name,omitempty"` ToSubnet string `json:"to_subnet"` SessionID string `json:"session_id,omitempty"` HopIndex int `json:"hop_index,omitempty"` JoinLane string `json:"join_lane,omitempty"` Clearance int `json:"clearance_level"` LaneSuccess float64 `json:"lane_success_rate"` LatencyMs int `json:"latency_ms,omitempty"` Weight float64 `json:"weight"` } // RouteRecommendation is the best seed hop for one target subnet. type RouteRecommendation struct { TargetSubnet string `json:"target_subnet"` SeedAgentID string `json:"seed_agent_id"` SeedAgentName string `json:"seed_agent_name,omitempty"` EgressAgentID string `json:"egress_agent_id"` EgressHopIndex int `json:"egress_hop_index,omitempty"` SessionID string `json:"session_id,omitempty"` JoinLane string `json:"join_lane,omitempty"` ClearanceLevel int `json:"clearance_level"` Score float64 `json:"score"` Reason string `json:"reason,omitempty"` ErasureLanesEnabled bool `json:"erasure_lanes_enabled,omitempty"` } // SpreadRouteHint is attached to signed deploy plans for agent egress routing. type SpreadRouteHint struct { TargetSubnet string `json:"target_subnet"` SeedAgentID string `json:"seed_agent_id"` SeedAgentName string `json:"seed_agent_name,omitempty"` EgressAgentID string `json:"egress_agent_id"` EgressHopIndex int `json:"egress_hop_index,omitempty"` SessionID string `json:"session_id,omitempty"` JoinLane string `json:"join_lane,omitempty"` Score float64 `json:"score,omitempty"` ClearanceLevel int `json:"clearance_level,omitempty"` // ErasureLanesEnabled signals parallel Reed–Solomon lane redundancy on deploy plans. ErasureLanesEnabled bool `json:"erasure_lanes_enabled,omitempty"` } // RouteTable holds weighted edges and recommendations. type RouteTable struct { Edges []RouteEdge Routes []RouteRecommendation bySubnet map[string]RouteRecommendation } const ( weightClearance = 0.35 weightLane = 0.40 weightLatency = 0.25 ) type candidate struct { agentID string agentName string subnet string sessionID string hopIndex int clearance int latencyMs int joinLane string laneRate float64 discovered bool } // Build constructs a route table from Path Tracer sessions and fleet telemetry. func Build(in Input) *RouteTable { rt := &RouteTable{bySubnet: make(map[string]RouteRecommendation)} laneRates := laneSuccessRates(in.LaneSuccess, in.RequestedLane) fleetByID := make(map[string]FleetAgentSnapshot, len(in.FleetAgents)) for _, ag := range in.FleetAgents { fleetByID[ag.AgentID] = ag } targets := normalizeTargets(in) for _, target := range targets { cands := collectCandidates(in, target, fleetByID, laneRates) rec, edges := scoreCandidates(target, in.RequestedLane, in.ErasureLanesEnabled, cands) if rec.SeedAgentID != "" { rt.Routes = append(rt.Routes, rec) rt.bySubnet[target] = rec } rt.Edges = append(rt.Edges, edges...) } return rt } // Recommend returns the best route for a target subnet. func (rt *RouteTable) Recommend(targetSubnet string) (RouteRecommendation, bool) { if rt == nil { return RouteRecommendation{}, false } targetSubnet = NormalizeSubnet(targetSubnet) rec, ok := rt.bySubnet[targetSubnet] return rec, ok } // ToHint converts a recommendation into a deploy-plan hint. func ToHint(rec RouteRecommendation) *SpreadRouteHint { if rec.SeedAgentID == "" { return nil } return &SpreadRouteHint{ TargetSubnet: rec.TargetSubnet, SeedAgentID: rec.SeedAgentID, SeedAgentName: rec.SeedAgentName, EgressAgentID: rec.EgressAgentID, EgressHopIndex: rec.EgressHopIndex, SessionID: rec.SessionID, JoinLane: rec.JoinLane, Score: rec.Score, ClearanceLevel: rec.ClearanceLevel, ErasureLanesEnabled: rec.ErasureLanesEnabled, } } func normalizeTargets(in Input) []string { seen := make(map[string]bool) var out []string add := func(s string) { s = NormalizeSubnet(s) if s == "" || seen[s] { return } seen[s] = true out = append(out, s) } for _, t := range in.TargetSubnets { add(t) } for _, sess := range in.Sessions { for _, d := range sess.Discoveries { add(d.Subnet) } } for _, ag := range in.FleetAgents { add(ag.Subnet) } return out } func collectCandidates(in Input, target string, fleet map[string]FleetAgentSnapshot, laneRates map[string]float64) []candidate { seen := make(map[string]bool) var out []candidate add := func(c candidate) { if c.agentID == "" { return } if ag, ok := fleet[c.agentID]; ok { if c.clearance == 0 { c.clearance = ag.Clearance } if c.latencyMs == 0 { c.latencyMs = ag.LatencyMs } if c.joinLane == "" { c.joinLane = ag.JoinLane } if c.agentName == "" { c.agentName = ag.AgentName } if !ag.Connected { return } } if c.clearance < SpreadMinClearance { return } key := c.agentID + "|" + target if seen[key] { return } seen[key] = true if c.laneRate == 0 { c.laneRate = laneRates[target+"|"+strings.TrimSpace(c.joinLane)] if c.laneRate == 0 { c.laneRate = laneRates[target+"|"] } } out = append(out, c) } for _, sess := range in.Sessions { for _, hop := range sess.Hops { ag := fleet[hop.AgentID] c := candidate{ agentID: hop.AgentID, agentName: hop.AgentName, subnet: hop.Subnet, sessionID: sess.SessionID, hopIndex: hop.HopIndex, clearance: ag.Clearance, latencyMs: ag.LatencyMs, joinLane: ag.JoinLane, } for _, d := range sess.Discoveries { if NormalizeSubnet(d.Subnet) == target && d.AgentID == hop.AgentID { c.discovered = true break } } if c.discovered || NormalizeSubnet(hop.Subnet) == target { add(c) } } } for _, ag := range in.FleetAgents { if !ag.Connected || ag.Clearance < SpreadMinClearance { continue } if NormalizeSubnet(ag.Subnet) != target { continue } add(candidate{ agentID: ag.AgentID, agentName: ag.AgentName, subnet: ag.Subnet, clearance: ag.Clearance, latencyMs: ag.LatencyMs, joinLane: ag.JoinLane, laneRate: laneRates[target+"|"+strings.TrimSpace(ag.JoinLane)], }) } return out } func scoreCandidates(target, requestedLane string, erasureLanes bool, cands []candidate) (RouteRecommendation, []RouteEdge) { var edges []RouteEdge var best RouteRecommendation var bestScore float64 for _, c := range cands { clearanceScore := clearancePreference(c.clearance) laneScore := c.laneRate if laneScore <= 0 && strings.TrimSpace(requestedLane) != "" && strings.EqualFold(c.joinLane, requestedLane) { laneScore = 0.5 } latencyScore := latencyPreference(c.latencyMs) weight := weightClearance*clearanceScore + weightLane*laneScore + weightLatency*latencyScore if c.discovered { weight += 0.05 } edges = append(edges, RouteEdge{ FromAgentID: c.agentID, FromAgentName: c.agentName, ToSubnet: target, SessionID: c.sessionID, HopIndex: c.hopIndex, JoinLane: c.joinLane, Clearance: c.clearance, LaneSuccess: laneScore, LatencyMs: c.latencyMs, Weight: weight, }) if weight > bestScore { bestScore = weight reason := "minimum-clearance route" if c.discovered { reason = "path-tracer discovery on subnet" } else if NormalizeSubnet(c.subnet) == target { reason = "fleet agent on target subnet" } best = RouteRecommendation{ TargetSubnet: target, SeedAgentID: c.agentID, SeedAgentName: c.agentName, EgressAgentID: c.agentID, EgressHopIndex: c.hopIndex, SessionID: c.sessionID, JoinLane: firstNonEmpty(requestedLane, c.joinLane), ClearanceLevel: c.clearance, Score: weight, Reason: reason, ErasureLanesEnabled: erasureLanes, } } } return best, edges } func clearancePreference(level int) float64 { if level < SpreadMinClearance { return 0 } excess := float64(level - SpreadMinClearance) maxExcess := float64(clearance.L4 - SpreadMinClearance) if maxExcess <= 0 { return 1 } if excess > maxExcess { excess = maxExcess } return 1 - excess/maxExcess } func latencyPreference(ms int) float64 { if ms <= 0 { return 1 } return 1 / (1 + float64(ms)/100) } func laneSuccessRates(stats []LaneSuccessStat, requestedLane string) map[string]float64 { type bucket struct { total int lane int } bySubnet := make(map[string]*bucket) for _, s := range stats { sub := NormalizeSubnet(s.Subnet) if sub == "" || s.Success <= 0 { continue } b := bySubnet[sub] if b == nil { b = &bucket{} bySubnet[sub] = b } b.total += s.Success if requestedLane != "" && strings.EqualFold(s.JoinLane, requestedLane) { b.lane += s.Success } } out := make(map[string]float64) for sub, b := range bySubnet { if b.total <= 0 { continue } out[sub+"|"] = clamp01(float64(b.total) / float64(b.total+3)) if requestedLane != "" && b.lane > 0 { out[sub+"|"+requestedLane] = clamp01(float64(b.lane) / float64(b.total)) } } return out } func clamp01(v float64) float64 { if v < 0 { return 0 } if v > 1 { return 1 } return v } func firstNonEmpty(parts ...string) string { for _, p := range parts { if strings.TrimSpace(p) != "" { return strings.TrimSpace(p) } } return "" } // NormalizeSubnet returns a /24-style prefix for routing keys. func NormalizeSubnet(s string) string { s = strings.TrimSpace(strings.ToLower(s)) if s == "" { return "" } if strings.HasSuffix(s, ".x") { return strings.TrimSuffix(s, ".x") } return SubnetFromIP(s) } // SubnetFromIP extracts a routable subnet prefix from an IP or CIDR-ish string. func SubnetFromIP(ip string) string { ip = strings.TrimSpace(ip) if ip == "" { return "" } host := ip if h, _, err := net.SplitHostPort(ip); err == nil { host = h } if strings.Count(host, ".") == 2 { return host } parsed := net.ParseIP(host) if parsed == nil { return "" } if v4 := parsed.To4(); v4 != nil { parts := strings.Split(host, ".") if len(parts) >= 3 { return strings.Join(parts[:3], ".") } } if strings.Contains(host, ":") { parts := strings.Split(host, ":") if len(parts) >= 3 { return strings.Join(parts[:3], ":") } } return "" }