337 lines
12 KiB
Go
337 lines
12 KiB
Go
package mesh
|
|
|
|
import (
|
|
"net"
|
|
"net/netip"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
PeerConnectionIntentMaintain = "maintain"
|
|
PeerConnectionIntentProbe = "probe"
|
|
PeerConnectionIntentRecover = "recover"
|
|
)
|
|
|
|
const (
|
|
PeerTransportModeDirect = "direct"
|
|
PeerTransportModePrivateLAN = "private_lan"
|
|
PeerTransportModeCorporateLAN = "corporate_lan"
|
|
PeerTransportModeOutboundOnly = "outbound_only"
|
|
PeerTransportModeRelayRequired = "relay_required"
|
|
PeerTransportModeRelayControl = "relay_control"
|
|
PeerTransportModeUnknown = "unknown"
|
|
)
|
|
|
|
type PeerConnectionIntentPlanConfig struct {
|
|
PeerCache PeerCacheSnapshot
|
|
RecoveryPlan PeerRecoveryPlan
|
|
RendezvousLeases []PeerRendezvousLease
|
|
PreferredRegion string
|
|
Now time.Time
|
|
}
|
|
|
|
type PeerConnectionIntentPlan struct {
|
|
Mode string `json:"mode"`
|
|
IntentCount int `json:"intent_count"`
|
|
MaintainCount int `json:"maintain_count"`
|
|
ProbeCount int `json:"probe_count"`
|
|
RecoverCount int `json:"recover_count"`
|
|
DirectCount int `json:"direct_count"`
|
|
PrivateLANCount int `json:"private_lan_count"`
|
|
CorporateLANCount int `json:"corporate_lan_count"`
|
|
OutboundOnlyCount int `json:"outbound_only_count"`
|
|
RelayRequiredCount int `json:"relay_required_count"`
|
|
RelayControlCount int `json:"relay_control_count"`
|
|
RendezvousRequiredCount int `json:"rendezvous_required_count"`
|
|
RendezvousResolvedCount int `json:"rendezvous_resolved_count"`
|
|
RendezvousLeaseCount int `json:"rendezvous_lease_count"`
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
Intents []PeerConnectionIntent `json:"intents,omitempty"`
|
|
}
|
|
|
|
type PeerConnectionIntent struct {
|
|
NodeID string `json:"node_id"`
|
|
Action string `json:"action"`
|
|
Reason string `json:"reason"`
|
|
Endpoint string `json:"endpoint,omitempty"`
|
|
ConnectionState string `json:"connection_state"`
|
|
Transport string `json:"transport,omitempty"`
|
|
TransportMode string `json:"transport_mode"`
|
|
Reachability string `json:"reachability,omitempty"`
|
|
ConnectivityMode string `json:"connectivity_mode,omitempty"`
|
|
NATType string `json:"nat_type,omitempty"`
|
|
Region string `json:"region,omitempty"`
|
|
PolicyTags []string `json:"policy_tags,omitempty"`
|
|
RequiresRendezvous bool `json:"requires_rendezvous"`
|
|
RendezvousResolved bool `json:"rendezvous_resolved"`
|
|
DirectCandidate bool `json:"direct_candidate"`
|
|
RelayCandidate bool `json:"relay_candidate"`
|
|
BestCandidateID string `json:"best_candidate_id,omitempty"`
|
|
BestPeerCertSHA256 string `json:"best_peer_cert_sha256,omitempty"`
|
|
RendezvousLeaseID string `json:"rendezvous_lease_id,omitempty"`
|
|
RelayNodeID string `json:"relay_node_id,omitempty"`
|
|
RelayEndpoint string `json:"relay_endpoint,omitempty"`
|
|
ControlPlaneOnly bool `json:"control_plane_only"`
|
|
RecoverySeed bool `json:"recovery_seed"`
|
|
Priority int `json:"priority"`
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
}
|
|
|
|
func PlanPeerConnectionIntents(cfg PeerConnectionIntentPlanConfig) PeerConnectionIntentPlan {
|
|
now := normalizedNow(cfg.Now)
|
|
entryByNode := map[string]PeerCacheEntry{}
|
|
for _, entry := range cfg.PeerCache.Entries {
|
|
if strings.TrimSpace(entry.NodeID) == "" {
|
|
continue
|
|
}
|
|
entryByNode[entry.NodeID] = entry
|
|
}
|
|
|
|
intents := make([]PeerConnectionIntent, 0, len(cfg.RecoveryPlan.Candidates))
|
|
for _, candidate := range cfg.RecoveryPlan.Candidates {
|
|
if strings.TrimSpace(candidate.NodeID) == "" {
|
|
continue
|
|
}
|
|
entry := entryByNode[candidate.NodeID]
|
|
intent := PeerConnectionIntent{
|
|
NodeID: candidate.NodeID,
|
|
Action: connectionIntentAction(candidate),
|
|
Reason: candidate.Reason,
|
|
Endpoint: candidate.Endpoint,
|
|
ConnectionState: candidate.ConnectionState,
|
|
Transport: firstNonEmpty(candidate.BestTransport, entry.BestTransport),
|
|
Reachability: entry.BestReachability,
|
|
ConnectivityMode: entry.BestConnectivity,
|
|
NATType: entry.BestNATType,
|
|
Region: entry.BestRegion,
|
|
PolicyTags: append([]string{}, entry.BestPolicyTags...),
|
|
BestCandidateID: firstNonEmpty(candidate.BestCandidateID, entry.BestCandidateID),
|
|
BestPeerCertSHA256: entry.BestPeerCertSHA256,
|
|
RendezvousLeaseID: entry.RendezvousLeaseID,
|
|
RelayNodeID: entry.RelayNodeID,
|
|
RelayEndpoint: entry.RelayEndpoint,
|
|
RelayCandidate: entry.RelayControl,
|
|
ControlPlaneOnly: entry.RelayControl,
|
|
RecoverySeed: candidate.RecoverySeed || entry.RecoverySeed,
|
|
Priority: candidate.Priority,
|
|
GeneratedAt: now,
|
|
}
|
|
mode, requiresRendezvous, directCandidate := classifyPeerTransport(intent, cfg.PreferredRegion)
|
|
intent.TransportMode = mode
|
|
intent.RequiresRendezvous = requiresRendezvous
|
|
intent.DirectCandidate = directCandidate
|
|
if intent.RequiresRendezvous {
|
|
if lease, ok := rendezvousLeaseForPeer(cfg.RendezvousLeases, intent.NodeID, now); ok {
|
|
applyRendezvousLease(&intent, lease, cfg.PeerCache.LocalNodeID)
|
|
}
|
|
}
|
|
intents = append(intents, intent)
|
|
}
|
|
sort.SliceStable(intents, func(i, j int) bool {
|
|
if intents[i].Priority != intents[j].Priority {
|
|
return intents[i].Priority > intents[j].Priority
|
|
}
|
|
return intents[i].NodeID < intents[j].NodeID
|
|
})
|
|
|
|
plan := PeerConnectionIntentPlan{
|
|
Mode: cfg.RecoveryPlan.Mode,
|
|
IntentCount: len(intents),
|
|
GeneratedAt: now,
|
|
Intents: intents,
|
|
}
|
|
for _, intent := range intents {
|
|
switch intent.Action {
|
|
case PeerConnectionIntentMaintain:
|
|
plan.MaintainCount++
|
|
case PeerConnectionIntentProbe:
|
|
plan.ProbeCount++
|
|
case PeerConnectionIntentRecover:
|
|
plan.RecoverCount++
|
|
}
|
|
switch intent.TransportMode {
|
|
case PeerTransportModeDirect:
|
|
plan.DirectCount++
|
|
case PeerTransportModePrivateLAN:
|
|
plan.PrivateLANCount++
|
|
case PeerTransportModeCorporateLAN:
|
|
plan.CorporateLANCount++
|
|
case PeerTransportModeOutboundOnly:
|
|
plan.OutboundOnlyCount++
|
|
case PeerTransportModeRelayRequired:
|
|
plan.RelayRequiredCount++
|
|
case PeerTransportModeRelayControl:
|
|
plan.RelayControlCount++
|
|
}
|
|
if intent.RequiresRendezvous {
|
|
plan.RendezvousRequiredCount++
|
|
}
|
|
if intent.RendezvousResolved {
|
|
plan.RendezvousResolvedCount++
|
|
}
|
|
if intent.RendezvousLeaseID != "" {
|
|
plan.RendezvousLeaseCount++
|
|
}
|
|
}
|
|
return plan
|
|
}
|
|
|
|
func connectionIntentAction(candidate PeerRecoveryCandidate) string {
|
|
switch candidate.Reason {
|
|
case "maintain_ready":
|
|
return PeerConnectionIntentMaintain
|
|
case "recover_degraded", "recover_seed", "recover_warm", "recover_peer":
|
|
return PeerConnectionIntentRecover
|
|
default:
|
|
return PeerConnectionIntentProbe
|
|
}
|
|
}
|
|
|
|
func classifyPeerTransport(intent PeerConnectionIntent, preferredRegion string) (string, bool, bool) {
|
|
transport := strings.ToLower(strings.TrimSpace(intent.Transport))
|
|
connectivity := strings.ToLower(strings.TrimSpace(intent.ConnectivityMode))
|
|
reachability := strings.ToLower(strings.TrimSpace(intent.Reachability))
|
|
region := strings.TrimSpace(intent.Region)
|
|
preferredRegion = strings.TrimSpace(preferredRegion)
|
|
tags := lowerStringSet(intent.PolicyTags)
|
|
|
|
if strings.Contains(transport, "relay") || connectivity == "relay_required" || reachability == "relay" {
|
|
return PeerTransportModeRelayRequired, true, false
|
|
}
|
|
if connectivity == "outbound_only" || reachability == "outbound_only" {
|
|
return PeerTransportModeOutboundOnly, true, false
|
|
}
|
|
if tags["corp-lan"] || tags["same-site"] {
|
|
return PeerTransportModeCorporateLAN, false, true
|
|
}
|
|
if tags["private-lan"] || reachability == "private" || endpointHasPrivateHost(intent.Endpoint) {
|
|
if preferredRegion != "" && region != "" && !strings.EqualFold(region, preferredRegion) {
|
|
return PeerTransportModeRelayRequired, true, false
|
|
}
|
|
return PeerTransportModePrivateLAN, false, true
|
|
}
|
|
if strings.Contains(transport, "direct") || reachability == "public" || connectivity == "direct" {
|
|
return PeerTransportModeDirect, false, true
|
|
}
|
|
return PeerTransportModeUnknown, false, false
|
|
}
|
|
|
|
func rendezvousLeaseForPeer(leases []PeerRendezvousLease, peerNodeID string, now time.Time) (PeerRendezvousLease, bool) {
|
|
now = normalizedNow(now)
|
|
candidates := make([]PeerRendezvousLease, 0, len(leases))
|
|
for _, lease := range leases {
|
|
if strings.TrimSpace(lease.PeerNodeID) != peerNodeID ||
|
|
strings.TrimSpace(lease.RelayEndpoint) == "" ||
|
|
strings.TrimSpace(lease.RelayNodeID) == "" ||
|
|
!lease.ControlPlaneOnly ||
|
|
lease.ExpiresAt.IsZero() ||
|
|
!lease.ExpiresAt.After(now) {
|
|
continue
|
|
}
|
|
candidates = append(candidates, lease)
|
|
}
|
|
if len(candidates) == 0 {
|
|
return PeerRendezvousLease{}, false
|
|
}
|
|
sort.SliceStable(candidates, func(i, j int) bool {
|
|
leftPriority := candidates[i].Priority
|
|
rightPriority := candidates[j].Priority
|
|
if leftPriority <= 0 {
|
|
leftPriority = 100
|
|
}
|
|
if rightPriority <= 0 {
|
|
rightPriority = 100
|
|
}
|
|
if leftPriority != rightPriority {
|
|
return leftPriority < rightPriority
|
|
}
|
|
if !candidates[i].ExpiresAt.Equal(candidates[j].ExpiresAt) {
|
|
return candidates[i].ExpiresAt.After(candidates[j].ExpiresAt)
|
|
}
|
|
return candidates[i].LeaseID < candidates[j].LeaseID
|
|
})
|
|
return candidates[0], true
|
|
}
|
|
|
|
func applyRendezvousLease(intent *PeerConnectionIntent, lease PeerRendezvousLease, localNodeID string) {
|
|
localRelay := strings.TrimSpace(lease.RelayNodeID) == strings.TrimSpace(localNodeID)
|
|
if !localRelay {
|
|
intent.Endpoint = strings.TrimRight(strings.TrimSpace(lease.RelayEndpoint), "/")
|
|
}
|
|
if localRelay {
|
|
intent.Transport = "reverse_quic"
|
|
} else {
|
|
intent.Transport = firstNonEmpty(lease.Transport, "relay_quic")
|
|
}
|
|
intent.TransportMode = PeerTransportModeRelayControl
|
|
intent.RequiresRendezvous = false
|
|
intent.RendezvousResolved = true
|
|
intent.DirectCandidate = false
|
|
intent.RelayCandidate = true
|
|
intent.RendezvousLeaseID = lease.LeaseID
|
|
intent.RelayNodeID = lease.RelayNodeID
|
|
intent.RelayEndpoint = strings.TrimRight(strings.TrimSpace(lease.RelayEndpoint), "/")
|
|
intent.ControlPlaneOnly = true
|
|
if certSHA256 := rendezvousLeasePeerCertSHA256(lease); certSHA256 != "" && !localRelay {
|
|
intent.BestPeerCertSHA256 = certSHA256
|
|
}
|
|
if lease.ConnectivityMode != "" {
|
|
intent.ConnectivityMode = lease.ConnectivityMode
|
|
}
|
|
}
|
|
|
|
func endpointHasPrivateHost(rawEndpoint string) bool {
|
|
addr, ok := endpointHostAddr(rawEndpoint)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return addr.IsPrivate() || addr.IsLoopback() || addr.IsLinkLocalUnicast()
|
|
}
|
|
|
|
func endpointHasUnspecifiedHost(rawEndpoint string) bool {
|
|
addr, ok := endpointHostAddr(rawEndpoint)
|
|
return ok && addr.IsUnspecified()
|
|
}
|
|
|
|
func endpointHostAddr(rawEndpoint string) (netip.Addr, bool) {
|
|
rawEndpoint = strings.TrimSpace(rawEndpoint)
|
|
if rawEndpoint == "" {
|
|
return netip.Addr{}, false
|
|
}
|
|
host := rawEndpoint
|
|
if parsed, err := url.Parse(rawEndpoint); err == nil && parsed.Host != "" {
|
|
host = parsed.Host
|
|
}
|
|
if splitHost, _, err := net.SplitHostPort(host); err == nil {
|
|
host = splitHost
|
|
}
|
|
addr, err := netip.ParseAddr(strings.Trim(host, "[]"))
|
|
if err != nil {
|
|
return netip.Addr{}, false
|
|
}
|
|
return addr, true
|
|
}
|
|
|
|
func lowerStringSet(values []string) map[string]bool {
|
|
out := map[string]bool{}
|
|
for _, value := range values {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
if value != "" {
|
|
out[value] = true
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
if strings.TrimSpace(value) != "" {
|
|
return strings.TrimSpace(value)
|
|
}
|
|
}
|
|
return ""
|
|
}
|