feat(firewall): implement firewall blocklist feature with client management and policy rules
CI / changes (push) Successful in 12s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 25s
CI / web (push) Successful in 46s
CI / go (push) Successful in 1m15s
CI / bird2 (push) Successful in 18s
CI / release (push) Successful in 3m59s
CI / changes (push) Successful in 12s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 25s
CI / web (push) Successful in 46s
CI / go (push) Successful in 1m15s
CI / bird2 (push) Successful in 18s
CI / release (push) Successful in 3m59s
Introduced a comprehensive firewall blocklist feature, allowing for the management of firewall clients and their associated rules. This includes endpoints for enrolling clients, listing clients and rules, and reporting apply statuses. Enhanced the API to support firewall operations, including the ability to handle block/accept policies. Updated the documentation to reflect these changes and added necessary components in the web UI for better user interaction. Additionally, modified the agent server to support firewall failover and integrated firewall functionality into the existing architecture.
This commit is contained in:
1 parent
276194a9d0
commit
7a3eae98b1
36 files changed
+4581
-174
No files matched your search
@@ -0,0 +1,127 @@
|
||||
// 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[:])
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEvaluate_emptyRules(t *testing.T) {
|
||||
prefixes := map[string][]string{"c1": {"1.2.3.0/24"}}
|
||||
got := Evaluate("client1", nil, prefixes)
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("expected empty blocklist, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluate_onlyAccept(t *testing.T) {
|
||||
wild := (*string)(nil)
|
||||
rules := []Rule{{Priority: 1, Action: "accept", CommunityID: wild}}
|
||||
prefixes := map[string][]string{"c1": {"1.2.3.0/24"}}
|
||||
got := Evaluate("client1", rules, prefixes)
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("accept alone must not block, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluate_explicitBlock(t *testing.T) {
|
||||
cid := "c1"
|
||||
rules := []Rule{{Priority: 1, Action: "block", CommunityID: &cid}}
|
||||
prefixes := map[string][]string{"c1": {"1.2.3.0/24", "5.6.7.8/32"}, "c2": {"9.9.9.9/32"}}
|
||||
got := Evaluate("client1", rules, prefixes)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("expected 2 prefixes, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluate_clientOverrideAccept(t *testing.T) {
|
||||
wild := (*string)(nil)
|
||||
cTrusted := "trusted"
|
||||
clientID := "srv1"
|
||||
rules := []Rule{
|
||||
{Priority: 1, Action: "block", CommunityID: wild},
|
||||
{ClientID: &clientID, Priority: 1, Action: "accept", CommunityID: &cTrusted},
|
||||
}
|
||||
prefixes := map[string][]string{
|
||||
"trusted": {"1.1.1.0/24"},
|
||||
"bad": {"2.2.2.0/24"},
|
||||
}
|
||||
got := Evaluate(clientID, rules, prefixes)
|
||||
if len(got) != 1 || got[0] != "2.2.2.0/24" {
|
||||
t.Fatalf("expected only bad community, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluate_clientOverrideWins(t *testing.T) {
|
||||
cid := "c1"
|
||||
clientID := "srv1"
|
||||
rules := []Rule{
|
||||
{Priority: 1, Action: "accept", CommunityID: &cid},
|
||||
{ClientID: &clientID, Priority: 1, Action: "block", CommunityID: &cid},
|
||||
}
|
||||
prefixes := map[string][]string{"c1": {"1.2.3.0/24"}}
|
||||
got := Evaluate(clientID, rules, prefixes)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("client override should block, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluate_noCommunityKey(t *testing.T) {
|
||||
empty := ""
|
||||
rules := []Rule{{Priority: 1, Action: "block", CommunityID: &empty}}
|
||||
prefixes := map[string][]string{"": {"10.0.0.0/8"}}
|
||||
got := Evaluate("c", rules, prefixes)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("expected prefix without community, got %v", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user