platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
162
selective-vpn-api/app/transport_policy_owner_destination_lock.go
Normal file
162
selective-vpn-api/app/transport_policy_owner_destination_lock.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
transportPolicyDomainCachePath = stateDir + "/domain-cache.json"
|
||||
transportPolicyLoadDomainCacheState = loadDomainCacheState
|
||||
)
|
||||
|
||||
func detectTransportDestinationLockConflicts(current, next []TransportPolicyIntent, locks TransportOwnerLockState) []TransportConflictRecord {
|
||||
if len(locks.Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
currentOwners := map[string]string{}
|
||||
for _, raw := range current {
|
||||
norm, key, _, err := normalizeTransportIntent(raw)
|
||||
if err != nil || key == "" || strings.TrimSpace(norm.ClientID) == "" {
|
||||
continue
|
||||
}
|
||||
currentOwners[key] = norm.ClientID
|
||||
}
|
||||
if len(currentOwners) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
conflicts := make([]TransportConflictRecord, 0, 4)
|
||||
var domainCache domainCacheState
|
||||
domainCacheLoaded := false
|
||||
domainResolved := map[string]map[string]struct{}{}
|
||||
|
||||
for _, raw := range next {
|
||||
norm, key, pfx, err := normalizeTransportIntent(raw)
|
||||
if err != nil || key == "" || strings.TrimSpace(norm.ClientID) == "" {
|
||||
continue
|
||||
}
|
||||
prevOwner, exists := currentOwners[key]
|
||||
if !exists || prevOwner == norm.ClientID {
|
||||
continue
|
||||
}
|
||||
|
||||
switch strings.ToLower(strings.TrimSpace(norm.SelectorType)) {
|
||||
case "cidr":
|
||||
if !pfx.IsValid() {
|
||||
continue
|
||||
}
|
||||
for _, lock := range locks.Items {
|
||||
if strings.TrimSpace(lock.ClientID) != prevOwner {
|
||||
continue
|
||||
}
|
||||
dst, err := netip.ParseAddr(strings.TrimSpace(lock.DestinationIP))
|
||||
if err != nil || !dst.Is4() {
|
||||
continue
|
||||
}
|
||||
if !pfx.Contains(dst) {
|
||||
continue
|
||||
}
|
||||
conflicts = append(conflicts, TransportConflictRecord{
|
||||
Key: fmt.Sprintf("destination_lock:%s:%s", key, dst.String()),
|
||||
Type: "destination_lock",
|
||||
Severity: "block",
|
||||
Owners: []string{prevOwner, norm.ClientID},
|
||||
Reason: fmt.Sprintf(
|
||||
"destination %s is sticky-locked to %s by conntrack state",
|
||||
dst.String(),
|
||||
prevOwner,
|
||||
),
|
||||
SuggestedResolution: "wait conntrack expiry or clear conntrack state for destination before owner switch",
|
||||
})
|
||||
}
|
||||
case "domain":
|
||||
ipsBySelector, ok := domainResolved[key]
|
||||
if !ok {
|
||||
if !domainCacheLoaded {
|
||||
domainCache = transportPolicyLoadDomainCacheState(transportPolicyDomainCachePath, nil)
|
||||
domainCacheLoaded = true
|
||||
}
|
||||
ipsBySelector = transportPolicyResolveDomainSelectorIPs(norm.SelectorValue, domainCache)
|
||||
domainResolved[key] = ipsBySelector
|
||||
}
|
||||
if len(ipsBySelector) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, lock := range locks.Items {
|
||||
if strings.TrimSpace(lock.ClientID) != prevOwner {
|
||||
continue
|
||||
}
|
||||
dstRaw := strings.TrimSpace(lock.DestinationIP)
|
||||
dst, err := netip.ParseAddr(dstRaw)
|
||||
if err != nil || !dst.Is4() {
|
||||
continue
|
||||
}
|
||||
dstKey := dst.String()
|
||||
if _, hit := ipsBySelector[dstKey]; !hit {
|
||||
continue
|
||||
}
|
||||
conflicts = append(conflicts, TransportConflictRecord{
|
||||
Key: fmt.Sprintf("destination_lock:%s:%s", key, dstKey),
|
||||
Type: "destination_lock",
|
||||
Severity: "block",
|
||||
Owners: []string{prevOwner, norm.ClientID},
|
||||
Reason: fmt.Sprintf(
|
||||
"domain %s resolves to destination %s sticky-locked to %s by conntrack state",
|
||||
norm.SelectorValue,
|
||||
dstKey,
|
||||
prevOwner,
|
||||
),
|
||||
SuggestedResolution: "wait conntrack expiry or clear conntrack state for destination before owner switch",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return dedupeTransportConflicts(conflicts)
|
||||
}
|
||||
|
||||
func transportPolicyResolveDomainSelectorIPs(selector string, cache domainCacheState) map[string]struct{} {
|
||||
out := map[string]struct{}{}
|
||||
sel := strings.Trim(strings.ToLower(strings.TrimSpace(selector)), ".")
|
||||
if sel == "" {
|
||||
return out
|
||||
}
|
||||
wildcard := false
|
||||
if strings.HasPrefix(sel, "*.") {
|
||||
wildcard = true
|
||||
sel = strings.Trim(strings.TrimPrefix(sel, "*."), ".")
|
||||
}
|
||||
if sel == "" {
|
||||
return out
|
||||
}
|
||||
|
||||
for host := range cache.Domains {
|
||||
h := strings.Trim(strings.ToLower(strings.TrimSpace(host)), ".")
|
||||
if h == "" {
|
||||
continue
|
||||
}
|
||||
if wildcard {
|
||||
if h != sel && !strings.HasSuffix(h, "."+sel) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if h != sel && !strings.HasSuffix(h, "."+sel) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, src := range []domainCacheSource{domainCacheSourceDirect, domainCacheSourceWildcard} {
|
||||
ips := cache.getStoredIPs(h, src)
|
||||
for _, ip := range ips {
|
||||
addr, err := netip.ParseAddr(strings.TrimSpace(ip))
|
||||
if err != nil || !addr.Is4() {
|
||||
continue
|
||||
}
|
||||
out[addr.String()] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user