Refactor RDP proxy handling and update related tests
This commit is contained in:
@@ -0,0 +1,390 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FabricChannelTargetKind string
|
||||
|
||||
const (
|
||||
FabricChannelTargetNode FabricChannelTargetKind = "node"
|
||||
FabricChannelTargetPool FabricChannelTargetKind = "pool"
|
||||
)
|
||||
|
||||
type FabricChannelLifecycleState string
|
||||
|
||||
const (
|
||||
FabricChannelOpening FabricChannelLifecycleState = "opening"
|
||||
FabricChannelOpen FabricChannelLifecycleState = "open"
|
||||
FabricChannelDraining FabricChannelLifecycleState = "draining"
|
||||
FabricChannelClosed FabricChannelLifecycleState = "closed"
|
||||
)
|
||||
|
||||
type FabricRouteMode string
|
||||
|
||||
const (
|
||||
FabricRouteDirect FabricRouteMode = "direct_quic"
|
||||
FabricRouteLAN FabricRouteMode = "lan_quic"
|
||||
FabricRouteReverse FabricRouteMode = "reverse_quic"
|
||||
FabricRouteRelay FabricRouteMode = "relay_quic"
|
||||
FabricRouteICE FabricRouteMode = "ice_quic"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFabricChannelInvalid = errors.New("fabric channel request is invalid")
|
||||
ErrFabricRouteNotFound = errors.New("fabric route not found")
|
||||
)
|
||||
|
||||
type FabricChannelSpec struct {
|
||||
ChannelID string
|
||||
ClusterID string
|
||||
SourceNodeID string
|
||||
TargetKind FabricChannelTargetKind
|
||||
TargetID string
|
||||
TrafficClass string
|
||||
MinBandwidth int64
|
||||
StickyKey string
|
||||
CreatedAt time.Time
|
||||
ForbiddenHops []string
|
||||
}
|
||||
|
||||
type FabricServiceChannelTarget struct {
|
||||
Kind FabricChannelTargetKind
|
||||
PoolIDs []string
|
||||
NodeIDs []string
|
||||
SelectedNodeID string
|
||||
ServiceRole string
|
||||
SelectionPolicy string
|
||||
SingleMemberPool bool
|
||||
}
|
||||
|
||||
type FabricServiceChannelRequest struct {
|
||||
SchemaVersion string
|
||||
ChannelID string
|
||||
ClusterID string
|
||||
OrganizationID string
|
||||
UserID string
|
||||
ResourceID string
|
||||
SourceNodeID string
|
||||
SourceRole string
|
||||
ServiceClass string
|
||||
Target FabricServiceChannelTarget
|
||||
TrafficClass string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type FabricChannel struct {
|
||||
Spec FabricChannelSpec
|
||||
State FabricChannelLifecycleState
|
||||
RouteID string
|
||||
TargetNode string
|
||||
OpenedAt time.Time
|
||||
LastReroute time.Time
|
||||
BytesSent uint64
|
||||
BytesRecv uint64
|
||||
FramesSent uint64
|
||||
FramesRecv uint64
|
||||
RerouteCount uint64
|
||||
}
|
||||
|
||||
type FabricRouteHop struct {
|
||||
NodeID string
|
||||
Mode FabricRouteMode
|
||||
EndpointID string
|
||||
Address string
|
||||
PeerCertSHA256 string
|
||||
}
|
||||
|
||||
type FabricRoute struct {
|
||||
RouteID string
|
||||
ClusterID string
|
||||
SourceNodeID string
|
||||
DestinationNodeID string
|
||||
PoolID string
|
||||
Hops []FabricRouteHop
|
||||
BaseLatencyMs int
|
||||
JitterMs int
|
||||
LossPermille int
|
||||
Capacity int
|
||||
ActiveChannels int
|
||||
RelayCount int
|
||||
LastUpdatedAt time.Time
|
||||
Healthy bool
|
||||
Degraded bool
|
||||
}
|
||||
|
||||
type FabricRouteSet struct {
|
||||
TargetKind FabricChannelTargetKind
|
||||
TargetID string
|
||||
Primary FabricRoute
|
||||
WarmStandby []FabricRoute
|
||||
ColdFallbacks []FabricRoute
|
||||
}
|
||||
|
||||
type FabricAdjacency struct {
|
||||
FromNodeID string
|
||||
ToNodeID string
|
||||
Mode FabricRouteMode
|
||||
RTTMs int
|
||||
JitterMs int
|
||||
LossPermille int
|
||||
Capacity int
|
||||
ActiveChannels int
|
||||
ThroughputBps int64
|
||||
PressurePercent int
|
||||
Healthy bool
|
||||
PassiveOutbound bool
|
||||
LocalSegmentID string
|
||||
NATGroupID string
|
||||
LastObservedAt time.Time
|
||||
LastFailureReason string
|
||||
}
|
||||
|
||||
type FabricRouteChoice struct {
|
||||
Route FabricRoute
|
||||
Score int
|
||||
Reason string
|
||||
PressureBefore int
|
||||
PressureAfter int
|
||||
}
|
||||
|
||||
type FabricRouteSchedulerConfig struct {
|
||||
LatencyWeight int
|
||||
JitterWeight int
|
||||
LossWeight int
|
||||
PressureWeight int
|
||||
HopPenalty int
|
||||
RelayPenalty int
|
||||
DegradedPenalty int
|
||||
ProjectedChannelCost int
|
||||
HardMaxRoutePressure int
|
||||
}
|
||||
|
||||
type FabricRouteScheduler struct {
|
||||
Config FabricRouteSchedulerConfig
|
||||
}
|
||||
|
||||
func NewFabricRouteScheduler(cfg FabricRouteSchedulerConfig) FabricRouteScheduler {
|
||||
return FabricRouteScheduler{Config: normalizeFabricRouteSchedulerConfig(cfg)}
|
||||
}
|
||||
|
||||
func (s FabricRouteScheduler) ChooseRoute(spec FabricChannelSpec, routeSet FabricRouteSet, now time.Time) (FabricRouteChoice, error) {
|
||||
if err := ValidateFabricChannelSpec(spec); err != nil {
|
||||
return FabricRouteChoice{}, err
|
||||
}
|
||||
routes := flattenFabricRouteSet(routeSet)
|
||||
if len(routes) == 0 {
|
||||
return FabricRouteChoice{}, ErrFabricRouteNotFound
|
||||
}
|
||||
forbidden := stringSet(spec.ForbiddenHops)
|
||||
choices := make([]FabricRouteChoice, 0, len(routes))
|
||||
for _, route := range routes {
|
||||
if !fabricRouteUsable(spec, route, forbidden, now) {
|
||||
continue
|
||||
}
|
||||
choice := s.scoreRoute(route)
|
||||
if s.Config.HardMaxRoutePressure > 0 && choice.PressureAfter > s.Config.HardMaxRoutePressure {
|
||||
continue
|
||||
}
|
||||
choice.Route = route
|
||||
choices = append(choices, choice)
|
||||
}
|
||||
if len(choices) == 0 {
|
||||
return FabricRouteChoice{}, ErrFabricRouteNotFound
|
||||
}
|
||||
sort.SliceStable(choices, func(i, j int) bool {
|
||||
if choices[i].Score != choices[j].Score {
|
||||
return choices[i].Score < choices[j].Score
|
||||
}
|
||||
if choices[i].PressureAfter != choices[j].PressureAfter {
|
||||
return choices[i].PressureAfter < choices[j].PressureAfter
|
||||
}
|
||||
if choices[i].Route.BaseLatencyMs != choices[j].Route.BaseLatencyMs {
|
||||
return choices[i].Route.BaseLatencyMs < choices[j].Route.BaseLatencyMs
|
||||
}
|
||||
return choices[i].Route.RouteID < choices[j].Route.RouteID
|
||||
})
|
||||
return choices[0], nil
|
||||
}
|
||||
|
||||
func ValidateFabricChannelSpec(spec FabricChannelSpec) error {
|
||||
if strings.TrimSpace(spec.ChannelID) == "" || strings.TrimSpace(spec.ClusterID) == "" || strings.TrimSpace(spec.SourceNodeID) == "" || strings.TrimSpace(spec.TargetID) == "" {
|
||||
return ErrFabricChannelInvalid
|
||||
}
|
||||
switch spec.TargetKind {
|
||||
case FabricChannelTargetNode, FabricChannelTargetPool:
|
||||
return nil
|
||||
default:
|
||||
return ErrFabricChannelInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func FabricChannelSpecFromServiceRequest(req FabricServiceChannelRequest, localNodeID string, now time.Time) (FabricChannelSpec, error) {
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
}
|
||||
sourceNodeID := firstNonEmpty(strings.TrimSpace(req.SourceNodeID), strings.TrimSpace(localNodeID))
|
||||
targetKind := req.Target.Kind
|
||||
if targetKind == "" {
|
||||
targetKind = FabricChannelTargetPool
|
||||
}
|
||||
targetID := firstNonEmpty(firstString(req.Target.PoolIDs), strings.TrimSpace(req.Target.SelectedNodeID), firstString(req.Target.NodeIDs))
|
||||
if targetKind == FabricChannelTargetNode {
|
||||
targetID = firstNonEmpty(strings.TrimSpace(req.Target.SelectedNodeID), firstString(req.Target.NodeIDs), targetID)
|
||||
}
|
||||
spec := FabricChannelSpec{
|
||||
ChannelID: firstNonEmpty(strings.TrimSpace(req.ChannelID), strings.TrimSpace(req.ResourceID)),
|
||||
ClusterID: strings.TrimSpace(req.ClusterID),
|
||||
SourceNodeID: sourceNodeID,
|
||||
TargetKind: targetKind,
|
||||
TargetID: targetID,
|
||||
TrafficClass: firstNonEmpty(strings.TrimSpace(req.TrafficClass), serviceClassDefaultTrafficClass(req.ServiceClass)),
|
||||
StickyKey: strings.TrimSpace(req.ResourceID),
|
||||
CreatedAt: now,
|
||||
}
|
||||
if err := ValidateFabricChannelSpec(spec); err != nil {
|
||||
return FabricChannelSpec{}, err
|
||||
}
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func serviceClassDefaultTrafficClass(serviceClass string) string {
|
||||
switch strings.TrimSpace(strings.ToLower(serviceClass)) {
|
||||
case FabricServiceClassVPNPackets:
|
||||
return FabricServiceChannelBulk
|
||||
case FabricServiceClassRemoteWorkspace:
|
||||
return FabricServiceChannelInteractive
|
||||
default:
|
||||
return FabricServiceChannelReliable
|
||||
}
|
||||
}
|
||||
|
||||
func firstString(values []string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s FabricRouteScheduler) scoreRoute(route FabricRoute) FabricRouteChoice {
|
||||
cfg := normalizeFabricRouteSchedulerConfig(s.Config)
|
||||
pressureBefore := fabricRoutePressurePercent(route, 0)
|
||||
pressureAfter := fabricRoutePressurePercent(route, cfg.ProjectedChannelCost)
|
||||
score := route.BaseLatencyMs*cfg.LatencyWeight +
|
||||
route.JitterMs*cfg.JitterWeight +
|
||||
route.LossPermille*cfg.LossWeight +
|
||||
pressureAfter*cfg.PressureWeight +
|
||||
len(route.Hops)*cfg.HopPenalty +
|
||||
route.RelayCount*cfg.RelayPenalty
|
||||
if route.Degraded {
|
||||
score += cfg.DegradedPenalty
|
||||
}
|
||||
reason := "latency_load_score"
|
||||
if pressureAfter >= 90 {
|
||||
reason = "capacity_pressure_avoidance"
|
||||
}
|
||||
if route.RelayCount > 0 {
|
||||
reason = "relay_fallback_available"
|
||||
}
|
||||
return FabricRouteChoice{Score: score, Reason: reason, PressureBefore: pressureBefore, PressureAfter: pressureAfter}
|
||||
}
|
||||
|
||||
func normalizeFabricRouteSchedulerConfig(cfg FabricRouteSchedulerConfig) FabricRouteSchedulerConfig {
|
||||
if cfg.LatencyWeight <= 0 {
|
||||
cfg.LatencyWeight = 10
|
||||
}
|
||||
if cfg.JitterWeight <= 0 {
|
||||
cfg.JitterWeight = 4
|
||||
}
|
||||
if cfg.LossWeight <= 0 {
|
||||
cfg.LossWeight = 8
|
||||
}
|
||||
if cfg.PressureWeight <= 0 {
|
||||
cfg.PressureWeight = 12
|
||||
}
|
||||
if cfg.HopPenalty <= 0 {
|
||||
cfg.HopPenalty = 5
|
||||
}
|
||||
if cfg.RelayPenalty <= 0 {
|
||||
cfg.RelayPenalty = 25
|
||||
}
|
||||
if cfg.DegradedPenalty <= 0 {
|
||||
cfg.DegradedPenalty = 500
|
||||
}
|
||||
if cfg.ProjectedChannelCost <= 0 {
|
||||
cfg.ProjectedChannelCost = 1
|
||||
}
|
||||
if cfg.HardMaxRoutePressure < 0 {
|
||||
cfg.HardMaxRoutePressure = 0
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func flattenFabricRouteSet(routeSet FabricRouteSet) []FabricRoute {
|
||||
routes := make([]FabricRoute, 0, 1+len(routeSet.WarmStandby)+len(routeSet.ColdFallbacks))
|
||||
if strings.TrimSpace(routeSet.Primary.RouteID) != "" {
|
||||
routes = append(routes, routeSet.Primary)
|
||||
}
|
||||
routes = append(routes, routeSet.WarmStandby...)
|
||||
routes = append(routes, routeSet.ColdFallbacks...)
|
||||
return routes
|
||||
}
|
||||
|
||||
func fabricRouteUsable(spec FabricChannelSpec, route FabricRoute, forbidden map[string]struct{}, now time.Time) bool {
|
||||
if strings.TrimSpace(route.RouteID) == "" || !route.Healthy {
|
||||
return false
|
||||
}
|
||||
if route.ClusterID != "" && spec.ClusterID != "" && route.ClusterID != spec.ClusterID {
|
||||
return false
|
||||
}
|
||||
if route.SourceNodeID != "" && route.SourceNodeID != spec.SourceNodeID {
|
||||
return false
|
||||
}
|
||||
switch spec.TargetKind {
|
||||
case FabricChannelTargetNode:
|
||||
if route.DestinationNodeID != "" && route.DestinationNodeID != spec.TargetID {
|
||||
return false
|
||||
}
|
||||
case FabricChannelTargetPool:
|
||||
if route.PoolID != "" && route.PoolID != spec.TargetID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, hop := range route.Hops {
|
||||
if _, blocked := forbidden[hop.NodeID]; blocked {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func fabricRoutePressurePercent(route FabricRoute, projected int) int {
|
||||
if route.Capacity <= 0 {
|
||||
return 100
|
||||
}
|
||||
active := route.ActiveChannels + projected
|
||||
if active <= 0 {
|
||||
return 0
|
||||
}
|
||||
pressure := (active * 100) / route.Capacity
|
||||
if pressure > 100 {
|
||||
return 100
|
||||
}
|
||||
return pressure
|
||||
}
|
||||
|
||||
func stringSet(values []string) map[string]struct{} {
|
||||
out := make(map[string]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value != "" {
|
||||
out[value] = struct{}{}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user