// Package firewall evaluates block/accept policy rules into CIDR blocklists. package firewall import ( "crypto/sha256" "encoding/hex" "sort" "strconv" "strings" ) // Rule is one ordered firewall policy rule for evaluation. type Rule struct { ClientID *string Priority int Action string // "block" | "accept" CommunityID *string } // Evaluate returns a deduplicated flat CIDR list to block in the kernel. // communityPrefixes maps community ID to prefixes; key "" holds prefixes without community. // Default when no rule matches: accept (do not block). func Evaluate(clientID string, rules []Rule, communityPrefixes map[string][]string) []string { ordered := mergeRules(clientID, rules) if len(communityPrefixes) == 0 { return nil } keys := make([]string, 0, len(communityPrefixes)) for k := range communityPrefixes { keys = append(keys, k) } sort.Strings(keys) var out []string seen := make(map[string]struct{}) for _, commKey := range keys { if !shouldBlockCommunity(commKey, ordered) { continue } for _, p := range communityPrefixes[commKey] { p = strings.TrimSpace(p) if p == "" { continue } if _, ok := seen[p]; ok { continue } seen[p] = struct{}{} out = append(out, p) } } return out } func mergeRules(clientID string, rules []Rule) []Rule { var clientRules, tenantRules []Rule for _, r := range rules { if r.ClientID != nil && strings.TrimSpace(*r.ClientID) == clientID { clientRules = append(clientRules, r) continue } if r.ClientID == nil { tenantRules = append(tenantRules, r) } } sort.Slice(clientRules, func(i, j int) bool { return clientRules[i].Priority < clientRules[j].Priority }) sort.Slice(tenantRules, func(i, j int) bool { return tenantRules[i].Priority < tenantRules[j].Priority }) out := make([]Rule, 0, len(clientRules)+len(tenantRules)) out = append(out, clientRules...) out = append(out, tenantRules...) return out } func shouldBlockCommunity(communityKey string, ordered []Rule) bool { for _, r := range ordered { if r.CommunityID == nil || strings.TrimSpace(*r.CommunityID) == communityKey { return strings.EqualFold(strings.TrimSpace(r.Action), "block") } } return false } // RulesVersionHash returns a stable fingerprint of rules for cache headers. func RulesVersionHash(rules []Rule) string { if len(rules) == 0 { return "sha256:empty" } cp := append([]Rule(nil), rules...) sort.Slice(cp, func(i, j int) bool { a, b := cp[i], cp[j] ac, bc := "", "" if a.ClientID != nil { ac = *a.ClientID } if b.ClientID != nil { bc = *b.ClientID } if ac != bc { return ac < bc } if a.Priority != b.Priority { return a.Priority < b.Priority } return a.Action < b.Action }) var b strings.Builder for _, r := range cp { cid := "*" if r.CommunityID != nil { cid = *r.CommunityID } cl := "tenant" if r.ClientID != nil { cl = *r.ClientID } b.WriteString(cl) b.WriteByte('|') b.WriteString(r.Action) b.WriteByte('|') b.WriteString(cid) b.WriteByte('|') b.WriteString(strconv.Itoa(r.Priority)) b.WriteByte(';') } sum := sha256.Sum256([]byte(b.String())) return "sha256:" + hex.EncodeToString(sum[:]) }