Files
EvoBGP/internal/store/memory_firewall.go
T
Denozordec 5edbd656ba
CI / changes (push) Successful in 11s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 29s
CI / web (push) Successful in 1m6s
CI / go (push) Successful in 1m23s
CI / bird2 (push) Successful in 17s
CI / release (push) Successful in 4m42s
feat(firewall): add packet statistics tracking for firewall clients
Enhanced the firewall client functionality by introducing packet statistics tracking, including the cumulative count of packets dropped and accepted. Updated the API to support these new fields and modified the database schema accordingly. Improved the firewall scripts to collect and report packet statistics, ensuring better visibility into client performance. Adjusted the UI components to display packet counts in the clients table, enhancing user experience and monitoring capabilities.
2026-07-09 00:50:38 +07:00

466 lines
12 KiB
Go

package store
import (
"encoding/hex"
"sort"
"strings"
"time"
"github.com/google/uuid"
)
func (m *Memory) ListFirewallClients(tenantID string) ([]*FirewallClient, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var out []*FirewallClient
for _, rec := range m.firewallClients {
if rec.TenantID == tenantID {
out = append(out, firewallClientCopy(&rec.FirewallClient))
}
}
sort.Slice(out, func(i, j int) bool { return out[i].CreatedAt.After(out[j].CreatedAt) })
return out, nil
}
func (m *Memory) GetFirewallClient(tenantID, id string) (*FirewallClient, error) {
m.mu.RLock()
defer m.mu.RUnlock()
rec, ok := m.firewallClients[id]
if !ok || rec.TenantID != tenantID {
return nil, ErrNotFound
}
return firewallClientCopy(&rec.FirewallClient), nil
}
func (m *Memory) CreateFirewallClient(tenantID string, in *FirewallClientCreate) (*FirewallClient, error) {
if in == nil || strings.TrimSpace(in.Name) == "" || len(in.TokenHash) != 32 {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.tenants[tenantID]; !ok {
return nil, ErrTenantScope
}
hashKey := hex.EncodeToString(in.TokenHash)
if _, dup := m.firewallHashIndex[hashKey]; dup {
return nil, ErrInvalidInput
}
now := time.Now().UTC()
id := uuid.NewString()
rec := &firewallClientRec{
FirewallClient: FirewallClient{
ID: id,
TenantID: tenantID,
Name: strings.TrimSpace(in.Name),
Hostname: strings.TrimSpace(in.Hostname),
TokenPrefix: in.TokenPrefix,
Status: "pending",
ClientVersion: strings.TrimSpace(in.ClientVersion),
CreatedAt: now,
},
TokenHash: append([]byte(nil), in.TokenHash...),
}
m.firewallClients[id] = rec
m.firewallHashIndex[hashKey] = id
return firewallClientCopy(&rec.FirewallClient), nil
}
func (m *Memory) UpdateFirewallClient(tenantID, id string, patch *FirewallClientPatch) (*FirewallClient, error) {
if patch == nil {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
rec, ok := m.firewallClients[id]
if !ok || rec.TenantID != tenantID {
return nil, ErrNotFound
}
if patch.Name != nil {
n := strings.TrimSpace(*patch.Name)
if n == "" {
return nil, ErrInvalidInput
}
rec.Name = n
}
if patch.Hostname != nil {
rec.Hostname = strings.TrimSpace(*patch.Hostname)
}
return firewallClientCopy(&rec.FirewallClient), nil
}
func (m *Memory) ApproveFirewallClient(tenantID, id, approverAPIKeyID string) (*FirewallClient, error) {
m.mu.Lock()
defer m.mu.Unlock()
rec, ok := m.firewallClients[id]
if !ok || rec.TenantID != tenantID {
return nil, ErrNotFound
}
if rec.Status == "revoked" {
return nil, ErrInvalidInput
}
now := time.Now().UTC()
rec.Status = "approved"
rec.ApprovedAt = &now
rec.ApprovedByAPIKeyID = strings.TrimSpace(approverAPIKeyID)
rec.RevokedAt = nil
return firewallClientCopy(&rec.FirewallClient), nil
}
func (m *Memory) RevokeFirewallClient(tenantID, id string) error {
m.mu.Lock()
defer m.mu.Unlock()
rec, ok := m.firewallClients[id]
if !ok || rec.TenantID != tenantID {
return ErrNotFound
}
now := time.Now().UTC()
rec.Status = "revoked"
rec.RevokedAt = &now
return nil
}
func (m *Memory) DeleteFirewallClient(tenantID, id string) error {
m.mu.Lock()
defer m.mu.Unlock()
rec, ok := m.firewallClients[id]
if !ok || rec.TenantID != tenantID {
return ErrNotFound
}
hashKey := hex.EncodeToString(rec.TokenHash)
delete(m.firewallHashIndex, hashKey)
delete(m.firewallClients, id)
for rid, rule := range m.firewallRules {
if rule.ClientID != nil && *rule.ClientID == id {
delete(m.firewallRules, rid)
}
}
return nil
}
func (m *Memory) LookupFirewallClientByTokenHash(hash []byte) (*FirewallClient, error) {
if len(hash) != 32 {
return nil, ErrNotFound
}
m.mu.RLock()
defer m.mu.RUnlock()
id, ok := m.firewallHashIndex[hex.EncodeToString(hash)]
if !ok {
return nil, ErrNotFound
}
rec, ok := m.firewallClients[id]
if !ok {
return nil, ErrNotFound
}
return firewallClientCopy(&rec.FirewallClient), nil
}
func (m *Memory) TouchFirewallClientLastSeen(id, source, clientIP, clientVersion string) error {
m.mu.Lock()
defer m.mu.Unlock()
rec, ok := m.firewallClients[id]
if !ok {
return ErrNotFound
}
now := time.Now().UTC()
rec.LastSeenAt = &now
rec.LastSeenAtSource = strings.TrimSpace(source)
rec.LastSeenIP = strings.TrimSpace(clientIP)
if v := strings.TrimSpace(clientVersion); v != "" {
rec.ClientVersion = v
}
return nil
}
func (m *Memory) TouchFirewallClientLastApply(id, source, status, errMsg string, prefixCount, ipCount int, packetsDropped, packetsAccepted int64) error {
m.mu.Lock()
defer m.mu.Unlock()
rec, ok := m.firewallClients[id]
if !ok {
return ErrNotFound
}
now := time.Now().UTC()
rec.LastApplyAt = &now
rec.LastApplySource = strings.TrimSpace(source)
rec.LastApplyStatus = strings.TrimSpace(status)
rec.LastApplyError = strings.TrimSpace(errMsg)
rec.LastApplyPrefixCount = prefixCount
rec.LastApplyIPCount = ipCount
rec.LastApplyPacketsDropped = packetsDropped
rec.LastApplyPacketsAccepted = packetsAccepted
return nil
}
func (m *Memory) ListActiveFirewallClientHashes() ([]FirewallClientAuthRow, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var out []FirewallClientAuthRow
for _, rec := range m.firewallClients {
if rec.Status != "approved" {
continue
}
out = append(out, FirewallClientAuthRow{
ID: rec.ID,
TenantID: rec.TenantID,
TokenHash: append([]byte(nil), rec.TokenHash...),
})
}
return out, nil
}
func (m *Memory) ListApprovedFirewallClientsForReplication(tenantID string) ([]FirewallClientReplicationRow, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var out []FirewallClientReplicationRow
for _, rec := range m.firewallClients {
if rec.TenantID != tenantID || rec.Status != "approved" {
continue
}
out = append(out, FirewallClientReplicationRow{
ClientID: rec.ID,
Name: rec.Name,
TokenHashHex: hex.EncodeToString(rec.TokenHash),
})
}
sort.Slice(out, func(i, j int) bool { return out[i].ClientID < out[j].ClientID })
return out, nil
}
func (m *Memory) ListFirewallRules(tenantID string, clientID *string) ([]*FirewallRule, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var out []*FirewallRule
for _, rule := range m.firewallRules {
if rule.TenantID != tenantID {
continue
}
if clientID == nil {
if rule.ClientID != nil {
continue
}
} else if rule.ClientID == nil || *rule.ClientID != *clientID {
continue
}
out = append(out, firewallRuleCopy(rule))
}
sort.Slice(out, func(i, j int) bool { return out[i].Priority < out[j].Priority })
return out, nil
}
func (m *Memory) ListAllFirewallRulesForClient(tenantID, clientID string) ([]*FirewallRule, error) {
tenantRules, err := m.ListFirewallRules(tenantID, nil)
if err != nil {
return nil, err
}
cid := clientID
clientRules, err := m.ListFirewallRules(tenantID, &cid)
if err != nil {
return nil, err
}
out := make([]*FirewallRule, 0, len(tenantRules)+len(clientRules))
out = append(out, clientRules...)
out = append(out, tenantRules...)
return out, nil
}
func (m *Memory) ListAllFirewallRulesForReplication(tenantID string) ([]*FirewallRule, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var out []*FirewallRule
for _, rule := range m.firewallRules {
if rule.TenantID == tenantID {
out = append(out, firewallRuleCopy(rule))
}
}
sort.Slice(out, func(i, j int) bool {
ac, bc := "", ""
if out[i].ClientID != nil {
ac = *out[i].ClientID
}
if out[j].ClientID != nil {
bc = *out[j].ClientID
}
if ac != bc {
return ac < bc
}
return out[i].Priority < out[j].Priority
})
return out, nil
}
func (m *Memory) CreateFirewallRule(tenantID string, clientID *string, in *FirewallRuleCreate) (*FirewallRule, error) {
if in == nil || !ValidFirewallAction(in.Action) {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.tenants[tenantID]; !ok {
return nil, ErrTenantScope
}
if clientID != nil {
if rec, ok := m.firewallClients[*clientID]; !ok || rec.TenantID != tenantID {
return nil, ErrNotFound
}
}
priority := m.nextFirewallRulePriorityLocked(tenantID, clientID)
if in.Priority != nil && *in.Priority >= 1 {
priority = *in.Priority
}
if m.firewallRulePriorityTakenLocked(tenantID, clientID, priority, "") {
return nil, ErrInvalidInput
}
now := time.Now().UTC()
id := uuid.NewString()
rule := &FirewallRule{
ID: id,
TenantID: tenantID,
ClientID: clientID,
Priority: priority,
Action: strings.ToLower(strings.TrimSpace(in.Action)),
CommunityID: in.CommunityID,
Comment: strings.TrimSpace(in.Comment),
CreatedAt: now,
UpdatedAt: now,
}
m.firewallRules[id] = rule
return firewallRuleCopy(rule), nil
}
func (m *Memory) UpdateFirewallRule(tenantID, ruleID string, patch *FirewallRulePatch) (*FirewallRule, error) {
if patch == nil {
return nil, ErrInvalidInput
}
m.mu.Lock()
defer m.mu.Unlock()
rule, ok := m.firewallRules[ruleID]
if !ok || rule.TenantID != tenantID {
return nil, ErrNotFound
}
if patch.Action != nil {
if !ValidFirewallAction(*patch.Action) {
return nil, ErrInvalidInput
}
rule.Action = strings.ToLower(strings.TrimSpace(*patch.Action))
}
if patch.ClearCommunity {
rule.CommunityID = nil
} else if patch.CommunityID != nil {
rule.CommunityID = patch.CommunityID
}
if patch.Comment != nil {
rule.Comment = strings.TrimSpace(*patch.Comment)
}
rule.UpdatedAt = time.Now().UTC()
return firewallRuleCopy(rule), nil
}
func (m *Memory) DeleteFirewallRule(tenantID, ruleID string) error {
m.mu.Lock()
defer m.mu.Unlock()
rule, ok := m.firewallRules[ruleID]
if !ok || rule.TenantID != tenantID {
return ErrNotFound
}
delete(m.firewallRules, ruleID)
return nil
}
func (m *Memory) ReorderFirewallRules(tenantID string, clientID *string, orderedIDs []string) error {
m.mu.Lock()
defer m.mu.Unlock()
scope := make(map[string]*FirewallRule)
for id, rule := range m.firewallRules {
if rule.TenantID != tenantID {
continue
}
if clientID == nil {
if rule.ClientID != nil {
continue
}
} else if rule.ClientID == nil || *rule.ClientID != *clientID {
continue
}
scope[id] = rule
}
if len(orderedIDs) != len(scope) {
return ErrInvalidInput
}
now := time.Now().UTC()
for i, id := range orderedIDs {
rule, ok := scope[id]
if !ok {
return ErrInvalidInput
}
rule.Priority = i + 1
rule.UpdatedAt = now
}
return nil
}
func (m *Memory) nextFirewallRulePriorityLocked(tenantID string, clientID *string) int {
max := 0
for _, rule := range m.firewallRules {
if rule.TenantID != tenantID {
continue
}
if clientID == nil {
if rule.ClientID != nil {
continue
}
} else if rule.ClientID == nil || *rule.ClientID != *clientID {
continue
}
if rule.Priority > max {
max = rule.Priority
}
}
return max + 1
}
func (m *Memory) firewallRulePriorityTakenLocked(tenantID string, clientID *string, priority int, exceptID string) bool {
for id, rule := range m.firewallRules {
if id == exceptID || rule.TenantID != tenantID || rule.Priority != priority {
continue
}
if clientID == nil {
if rule.ClientID == nil {
return true
}
continue
}
if rule.ClientID != nil && *rule.ClientID == *clientID {
return true
}
}
return false
}
func firewallClientCopy(c *FirewallClient) *FirewallClient {
cp := *c
cp.LastSeenAt = cloneTime(c.LastSeenAt)
cp.LastApplyAt = cloneTime(c.LastApplyAt)
cp.ApprovedAt = cloneTime(c.ApprovedAt)
cp.RevokedAt = cloneTime(c.RevokedAt)
return &cp
}
func firewallRuleCopy(r *FirewallRule) *FirewallRule {
cp := *r
if r.ClientID != nil {
v := *r.ClientID
cp.ClientID = &v
}
if r.CommunityID != nil {
v := *r.CommunityID
cp.CommunityID = &v
}
return &cp
}
func cloneTime(t *time.Time) *time.Time {
if t == nil {
return nil
}
v := *t
return &v
}