271 lines
11 KiB
Go
271 lines
11 KiB
Go
package mesh
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type ScopedSyntheticConfig struct {
|
|
SchemaVersion string `json:"schema_version"`
|
|
ClusterID string `json:"cluster_id"`
|
|
LocalNodeID string `json:"local_node_id"`
|
|
ConfigVersion string `json:"config_version,omitempty"`
|
|
PeerDirectoryVersion string `json:"peer_directory_version,omitempty"`
|
|
PolicyVersion string `json:"policy_version,omitempty"`
|
|
PeerEndpoints map[string]string `json:"peer_endpoints"`
|
|
PeerEndpointCandidates map[string][]PeerEndpointCandidate `json:"peer_endpoint_candidates,omitempty"`
|
|
PeerEndpointObservations map[string]EndpointCandidateHealthObservation `json:"peer_endpoint_observations,omitempty"`
|
|
PeerDirectory []PeerDirectoryEntry `json:"peer_directory,omitempty"`
|
|
RecoverySeeds []PeerRecoverySeed `json:"recovery_seeds,omitempty"`
|
|
RendezvousLeases []PeerRendezvousLease `json:"rendezvous_leases,omitempty"`
|
|
Routes []SyntheticRoute `json:"routes"`
|
|
}
|
|
|
|
type PeerDirectoryEntry struct {
|
|
NodeID string `json:"node_id"`
|
|
RouteIDs []string `json:"route_ids,omitempty"`
|
|
EndpointCount int `json:"endpoint_count"`
|
|
CandidateCount int `json:"candidate_count"`
|
|
ConnectivityModes []string `json:"connectivity_modes,omitempty"`
|
|
RecoverySeed bool `json:"recovery_seed"`
|
|
}
|
|
|
|
type PeerRecoverySeed struct {
|
|
NodeID string `json:"node_id"`
|
|
Endpoint string `json:"endpoint"`
|
|
Transport string `json:"transport"`
|
|
ConnectivityMode string `json:"connectivity_mode,omitempty"`
|
|
Region string `json:"region,omitempty"`
|
|
Priority int `json:"priority"`
|
|
LastVerifiedAt *time.Time `json:"last_verified_at,omitempty"`
|
|
Metadata json.RawMessage `json:"metadata,omitempty"`
|
|
}
|
|
|
|
type PeerRendezvousLease struct {
|
|
LeaseID string `json:"lease_id"`
|
|
PeerNodeID string `json:"peer_node_id"`
|
|
RelayNodeID string `json:"relay_node_id"`
|
|
RelayEndpoint string `json:"relay_endpoint"`
|
|
Transport string `json:"transport"`
|
|
ConnectivityMode string `json:"connectivity_mode,omitempty"`
|
|
RouteIDs []string `json:"route_ids,omitempty"`
|
|
AllowedChannels []string `json:"allowed_channels,omitempty"`
|
|
Priority int `json:"priority"`
|
|
ControlPlaneOnly bool `json:"control_plane_only"`
|
|
IssuedAt time.Time `json:"issued_at"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
Reason string `json:"reason,omitempty"`
|
|
Metadata json.RawMessage `json:"metadata,omitempty"`
|
|
}
|
|
|
|
type PeerEndpointCandidate struct {
|
|
EndpointID string `json:"endpoint_id"`
|
|
NodeID string `json:"node_id"`
|
|
Transport string `json:"transport"`
|
|
Address string `json:"address"`
|
|
AddressFamily string `json:"address_family,omitempty"`
|
|
Reachability string `json:"reachability"`
|
|
NATType string `json:"nat_type,omitempty"`
|
|
ConnectivityMode string `json:"connectivity_mode"`
|
|
Region string `json:"region,omitempty"`
|
|
Priority int `json:"priority"`
|
|
PolicyTags []string `json:"policy_tags,omitempty"`
|
|
LastVerifiedAt *time.Time `json:"last_verified_at,omitempty"`
|
|
Metadata json.RawMessage `json:"metadata,omitempty"`
|
|
}
|
|
|
|
func LoadScopedSyntheticConfig(path string, local PeerIdentity) (ScopedSyntheticConfig, error) {
|
|
payload, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return ScopedSyntheticConfig{}, err
|
|
}
|
|
var cfg ScopedSyntheticConfig
|
|
if err := json.Unmarshal(payload, &cfg); err != nil {
|
|
return ScopedSyntheticConfig{}, fmt.Errorf("parse scoped synthetic mesh config: %w", err)
|
|
}
|
|
if err := cfg.Validate(local); err != nil {
|
|
return ScopedSyntheticConfig{}, err
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func (cfg ScopedSyntheticConfig) Validate(local PeerIdentity) error {
|
|
if cfg.SchemaVersion == "" {
|
|
return fmt.Errorf("scoped synthetic mesh config schema_version is required")
|
|
}
|
|
if cfg.ClusterID == "" || cfg.ClusterID != local.ClusterID {
|
|
return ErrClusterMismatch
|
|
}
|
|
if cfg.LocalNodeID == "" || cfg.LocalNodeID != local.NodeID {
|
|
return ErrNodeMismatch
|
|
}
|
|
for nodeID, endpoint := range cfg.PeerEndpoints {
|
|
if strings.TrimSpace(nodeID) == "" || strings.TrimSpace(endpoint) == "" {
|
|
return fmt.Errorf("scoped synthetic mesh config contains empty peer endpoint")
|
|
}
|
|
if hasUnsupportedEndpointScheme(endpoint) {
|
|
return fmt.Errorf("scoped synthetic mesh config contains non-QUIC peer endpoint")
|
|
}
|
|
}
|
|
for nodeID, candidates := range cfg.PeerEndpointCandidates {
|
|
if strings.TrimSpace(nodeID) == "" {
|
|
return fmt.Errorf("scoped synthetic mesh config contains empty peer endpoint candidate node")
|
|
}
|
|
for _, candidate := range candidates {
|
|
if strings.TrimSpace(candidate.EndpointID) == "" ||
|
|
strings.TrimSpace(candidate.NodeID) == "" ||
|
|
candidate.NodeID != nodeID ||
|
|
strings.TrimSpace(candidate.Transport) == "" ||
|
|
strings.TrimSpace(candidate.Address) == "" ||
|
|
strings.TrimSpace(candidate.Reachability) == "" ||
|
|
strings.TrimSpace(candidate.ConnectivityMode) == "" {
|
|
return fmt.Errorf("scoped synthetic mesh config contains invalid peer endpoint candidate")
|
|
}
|
|
if !isQUICOnlyCandidateTransport(candidate.Transport) || hasUnsupportedEndpointScheme(candidate.Address) {
|
|
return fmt.Errorf("scoped synthetic mesh config contains non-QUIC peer endpoint candidate")
|
|
}
|
|
}
|
|
}
|
|
for endpointID, observation := range cfg.PeerEndpointObservations {
|
|
if strings.TrimSpace(endpointID) == "" || strings.TrimSpace(observation.EndpointID) == "" || observation.EndpointID != endpointID {
|
|
return fmt.Errorf("scoped synthetic mesh config contains invalid peer endpoint observation")
|
|
}
|
|
if observation.ReliabilityScore < 0 || observation.ReliabilityScore > 100 {
|
|
return fmt.Errorf("scoped synthetic mesh config contains invalid peer endpoint observation reliability")
|
|
}
|
|
}
|
|
if err := validatePeerDirectory(cfg.PeerDirectory, cfg.LocalNodeID); err != nil {
|
|
return err
|
|
}
|
|
if err := validateRecoverySeeds(cfg.RecoverySeeds); err != nil {
|
|
return err
|
|
}
|
|
if err := validateRendezvousLeases(cfg.RendezvousLeases, cfg.Routes, cfg.LocalNodeID); err != nil {
|
|
return err
|
|
}
|
|
for _, route := range cfg.Routes {
|
|
if route.ClusterID != cfg.ClusterID {
|
|
return ErrClusterMismatch
|
|
}
|
|
path := routePath(route)
|
|
if len(path) < 2 {
|
|
return ErrInvalidRoutePath
|
|
}
|
|
if !contains(path, cfg.LocalNodeID) {
|
|
return ErrNodeMismatch
|
|
}
|
|
if route.ExpiresAt.IsZero() {
|
|
return fmt.Errorf("scoped synthetic route %q expires_at is required", route.RouteID)
|
|
}
|
|
if !route.ExpiresAt.After(time.Now().UTC()) {
|
|
return ErrRouteExpired
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validatePeerDirectory(entries []PeerDirectoryEntry, localNodeID string) error {
|
|
seen := map[string]struct{}{}
|
|
for _, entry := range entries {
|
|
nodeID := strings.TrimSpace(entry.NodeID)
|
|
if nodeID == "" || nodeID == localNodeID {
|
|
return fmt.Errorf("scoped synthetic mesh config contains invalid peer directory entry")
|
|
}
|
|
if _, duplicate := seen[nodeID]; duplicate {
|
|
return fmt.Errorf("scoped synthetic mesh config contains duplicate peer directory entry")
|
|
}
|
|
seen[nodeID] = struct{}{}
|
|
if entry.EndpointCount < 0 || entry.CandidateCount < 0 {
|
|
return fmt.Errorf("scoped synthetic mesh config contains invalid peer directory count")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hasUnsupportedEndpointScheme(endpoint string) bool {
|
|
endpoint = strings.ToLower(strings.TrimSpace(endpoint))
|
|
if endpoint == "" || !strings.Contains(endpoint, "://") {
|
|
return false
|
|
}
|
|
return !strings.HasPrefix(endpoint, "quic://")
|
|
}
|
|
|
|
func validateRecoverySeeds(seeds []PeerRecoverySeed) error {
|
|
if len(seeds) > 20 {
|
|
return fmt.Errorf("scoped synthetic mesh config contains too many recovery seeds")
|
|
}
|
|
seen := map[string]struct{}{}
|
|
for _, seed := range seeds {
|
|
key := strings.TrimSpace(seed.NodeID) + "\x00" + strings.TrimSpace(seed.Endpoint)
|
|
if strings.TrimSpace(seed.NodeID) == "" ||
|
|
strings.TrimSpace(seed.Endpoint) == "" ||
|
|
strings.TrimSpace(seed.Transport) == "" {
|
|
return fmt.Errorf("scoped synthetic mesh config contains invalid recovery seed")
|
|
}
|
|
if !isQUICOnlyCandidateTransport(seed.Transport) || hasUnsupportedEndpointScheme(seed.Endpoint) {
|
|
return fmt.Errorf("scoped synthetic mesh config contains non-QUIC recovery seed")
|
|
}
|
|
if _, duplicate := seen[key]; duplicate {
|
|
return fmt.Errorf("scoped synthetic mesh config contains duplicate recovery seed")
|
|
}
|
|
seen[key] = struct{}{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateRendezvousLeases(leases []PeerRendezvousLease, routes []SyntheticRoute, localNodeID string) error {
|
|
if len(leases) > 20 {
|
|
return fmt.Errorf("scoped synthetic mesh config contains too many rendezvous leases")
|
|
}
|
|
routesByID := map[string]SyntheticRoute{}
|
|
for _, route := range routes {
|
|
if strings.TrimSpace(route.RouteID) != "" {
|
|
routesByID[route.RouteID] = route
|
|
}
|
|
}
|
|
seen := map[string]struct{}{}
|
|
now := time.Now().UTC()
|
|
for _, lease := range leases {
|
|
if strings.TrimSpace(lease.LeaseID) == "" ||
|
|
strings.TrimSpace(lease.PeerNodeID) == "" ||
|
|
strings.TrimSpace(lease.RelayNodeID) == "" ||
|
|
strings.TrimSpace(lease.RelayEndpoint) == "" ||
|
|
strings.TrimSpace(lease.Transport) == "" ||
|
|
lease.PeerNodeID == lease.RelayNodeID ||
|
|
!lease.ControlPlaneOnly ||
|
|
lease.ExpiresAt.IsZero() ||
|
|
!lease.ExpiresAt.After(now) ||
|
|
(len(lease.Metadata) > 0 && !json.Valid(lease.Metadata)) {
|
|
return fmt.Errorf("scoped synthetic mesh config contains invalid rendezvous lease")
|
|
}
|
|
if !isQUICOnlyCandidateTransport(lease.Transport) || hasUnsupportedEndpointScheme(lease.RelayEndpoint) {
|
|
return fmt.Errorf("scoped synthetic mesh config contains non-QUIC rendezvous lease")
|
|
}
|
|
if _, duplicate := seen[lease.LeaseID]; duplicate {
|
|
return fmt.Errorf("scoped synthetic mesh config contains duplicate rendezvous lease")
|
|
}
|
|
seen[lease.LeaseID] = struct{}{}
|
|
if len(lease.RouteIDs) == 0 {
|
|
continue
|
|
}
|
|
visible := false
|
|
for _, routeID := range lease.RouteIDs {
|
|
route, ok := routesByID[routeID]
|
|
if !ok {
|
|
return fmt.Errorf("scoped synthetic mesh config contains rendezvous lease for unknown route")
|
|
}
|
|
path := routePath(route)
|
|
if contains(path, localNodeID) && contains(path, lease.PeerNodeID) && contains(path, lease.RelayNodeID) {
|
|
visible = true
|
|
}
|
|
}
|
|
if !visible {
|
|
return fmt.Errorf("scoped synthetic mesh config contains out-of-scope rendezvous lease")
|
|
}
|
|
}
|
|
return nil
|
|
}
|