Files
elmprodvpn/selective-vpn-api/app/transport_policy_validate.go

127 lines
3.5 KiB
Go

package app
import (
"fmt"
"sort"
)
func validateTransportPolicy(next []TransportPolicyIntent, current []TransportPolicyIntent, clients []TransportClient) transportValidationResult {
clientByID := map[string]TransportClient{}
for _, c := range clients {
clientByID[c.ID] = c
}
type ownerSet map[string]struct{}
ownership := map[string]ownerSet{}
seenSameOwner := map[string]int{}
cidrs := make([]cidrIntent, 0)
conflicts := make([]TransportConflictRecord, 0)
block := 0
warn := 0
normalized := make([]TransportPolicyIntent, 0, len(next))
for i, raw := range next {
norm, key, pfx, err := normalizeTransportIntent(raw)
if err != nil {
conflicts = append(conflicts, TransportConflictRecord{
Key: fmt.Sprintf("intent:%d", i),
Type: "invalid_intent",
Severity: "block",
Reason: err.Error(),
SuggestedResolution: "fix selector/client fields",
})
block++
continue
}
if _, ok := clientByID[norm.ClientID]; !ok {
conflicts = append(conflicts, TransportConflictRecord{
Key: key,
Type: "unknown_client",
Severity: "block",
Owners: []string{norm.ClientID},
Reason: "client not found",
SuggestedResolution: "create client or fix client_id",
})
block++
continue
}
if ownership[key] == nil {
ownership[key] = ownerSet{}
}
ownership[key][norm.ClientID] = struct{}{}
seenSameOwner[key+"|"+norm.ClientID]++
if seenSameOwner[key+"|"+norm.ClientID] > 1 {
conflicts = append(conflicts, TransportConflictRecord{
Key: key,
Type: "duplicate_intent",
Severity: "warn",
Owners: []string{norm.ClientID},
Reason: "duplicate selector for same client",
SuggestedResolution: "deduplicate identical intents",
})
warn++
}
if pfx.IsValid() {
cidrs = append(cidrs, cidrIntent{ClientID: norm.ClientID, Prefix: pfx, Key: key})
}
normalized = append(normalized, norm)
}
for key, owners := range ownership {
if len(owners) <= 1 {
continue
}
own := make([]string, 0, len(owners))
for id := range owners {
own = append(own, id)
}
sort.Strings(own)
conflicts = append(conflicts, TransportConflictRecord{
Key: key,
Type: "ownership",
Severity: "block",
Owners: own,
Reason: "selector is assigned to multiple clients",
SuggestedResolution: "keep exactly one owner for selector",
})
block++
}
for i := 0; i < len(cidrs); i++ {
for j := i + 1; j < len(cidrs); j++ {
a, b := cidrs[i], cidrs[j]
if a.ClientID == b.ClientID {
continue
}
if !prefixOverlap(a.Prefix, b.Prefix) {
continue
}
key := "cidr_overlap:" + a.Prefix.String() + "|" + b.Prefix.String()
conflicts = append(conflicts, TransportConflictRecord{
Key: key,
Type: "cidr_overlap",
Severity: "block",
Owners: []string{a.ClientID, b.ClientID},
Reason: "CIDR selectors overlap across different clients",
SuggestedResolution: "split CIDR ranges or keep one owner",
})
block++
}
}
conflicts = dedupeTransportConflicts(conflicts)
diff := computeTransportPolicyDiff(current, normalized)
return transportValidationResult{
Normalized: normalized,
Conflicts: conflicts,
Summary: TransportPolicyValidateSummary{
BlockCount: block,
WarnCount: warn,
},
Diff: diff,
Valid: block == 0,
}
}