CI / changes (push) Successful in 13s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 27s
CI / web (push) Successful in 1m1s
CI / go (push) Successful in 1m16s
CI / bird2 (push) Successful in 17s
CI / release (push) Successful in 4m4s
Introduced a new API endpoint for retrieving the install context of the firewall client, which includes the bundle seed, configuration status, and suggested control plane URL. Updated the frontend to utilize this new endpoint, enhancing the user experience by dynamically displaying relevant information. Additionally, added type definitions for the install context and integrated it into the existing firewall management flow.
644 lines
20 KiB
Go
644 lines
20 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"evobgp/internal/authkey"
|
|
"evobgp/internal/firewall"
|
|
"evobgp/internal/store"
|
|
)
|
|
|
|
func (s *Server) registerFirewallRoutes(m *http.ServeMux) {
|
|
m.HandleFunc("GET /firewall/install-context", s.handleFirewallInstallContext)
|
|
m.HandleFunc("GET /firewall/clients", s.handleListFirewallClients)
|
|
m.HandleFunc("GET /firewall/clients/{id}", s.handleGetFirewallClient)
|
|
m.HandleFunc("GET /firewall/clients/{id}/preview", s.handleFirewallClientPreview)
|
|
m.HandleFunc("PATCH /firewall/clients/{id}", s.handlePatchFirewallClient)
|
|
m.HandleFunc("POST /firewall/clients/{id}/approve", s.handleApproveFirewallClient)
|
|
m.HandleFunc("POST /firewall/clients/{id}/revoke", s.handleRevokeFirewallClient)
|
|
m.HandleFunc("DELETE /firewall/clients/{id}", s.handleDeleteFirewallClient)
|
|
|
|
m.HandleFunc("GET /firewall/rules", s.handleListFirewallRules)
|
|
m.HandleFunc("POST /firewall/rules", s.handleCreateFirewallRule)
|
|
m.HandleFunc("PATCH /firewall/rules/{id}", s.handlePatchFirewallRule)
|
|
m.HandleFunc("DELETE /firewall/rules/{id}", s.handleDeleteFirewallRule)
|
|
m.HandleFunc("POST /firewall/rules:reorder", s.handleReorderFirewallRules)
|
|
|
|
m.HandleFunc("GET /firewall/blocklist", s.handleFirewallBlocklist)
|
|
m.HandleFunc("POST /firewall/apply-report", s.handleFirewallApplyReport)
|
|
m.HandleFunc("POST /firewall/heartbeat", s.handleFirewallHeartbeat)
|
|
}
|
|
|
|
func (s *Server) handleFirewallInstallContext(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "operator") {
|
|
return
|
|
}
|
|
seed := strings.TrimSpace(s.bundleSeedHex)
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"bundle_seed": seed,
|
|
"bundle_seed_configured": seed != "",
|
|
"suggested_cp_url": requestBaseURL(r),
|
|
"install_sh_url": requestBaseURL(r) + "/v1/firewall/install.sh",
|
|
})
|
|
}
|
|
|
|
func requestBaseURL(r *http.Request) string {
|
|
scheme := "https"
|
|
if r.TLS == nil {
|
|
if xf := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")); xf != "" {
|
|
scheme = strings.ToLower(strings.Split(xf, ",")[0])
|
|
} else if strings.EqualFold(r.URL.Scheme, "http") {
|
|
scheme = "http"
|
|
}
|
|
}
|
|
host := strings.TrimSpace(r.Host)
|
|
if xf := strings.TrimSpace(r.Header.Get("X-Forwarded-Host")); xf != "" {
|
|
host = strings.TrimSpace(strings.Split(xf, ",")[0])
|
|
}
|
|
if host == "" {
|
|
return ""
|
|
}
|
|
return scheme + "://" + host
|
|
}
|
|
|
|
func (s *Server) handleFirewallEnrollPublic(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
writeProblem(w, http.StatusMethodNotAllowed, "Method Not Allowed", "POST required")
|
|
return
|
|
}
|
|
seed := strings.TrimSpace(r.Header.Get("X-EvoBGP-Seed"))
|
|
if seed == "" || s.bundleSeedHex == "" || !strings.EqualFold(seed, s.bundleSeedHex) {
|
|
writeProblem(w, http.StatusForbidden, "Forbidden", "invalid or missing X-EvoBGP-Seed")
|
|
return
|
|
}
|
|
var body struct {
|
|
Name string `json:"name"`
|
|
Hostname string `json:"hostname"`
|
|
ClientToken string `json:"client_token"`
|
|
ClientVersion string `json:"client_version"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil && !errors.Is(err, io.EOF) {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid JSON body")
|
|
return
|
|
}
|
|
name := strings.TrimSpace(body.Name)
|
|
tok := strings.TrimSpace(body.ClientToken)
|
|
if name == "" || tok == "" {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "name and client_token are required")
|
|
return
|
|
}
|
|
if !strings.HasPrefix(tok, "evobgp_fw_") {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "client_token must use evobgp_fw_ prefix")
|
|
return
|
|
}
|
|
tenantID, err := s.firewallEnrollTenantID()
|
|
if err != nil {
|
|
writeInternalError(w, "internal", err)
|
|
return
|
|
}
|
|
hash := authkey.HashToken(tok)
|
|
prefix := tok
|
|
if len(prefix) > 12 {
|
|
prefix = prefix[:12]
|
|
}
|
|
client, err := s.store.CreateFirewallClient(tenantID, &store.FirewallClientCreate{
|
|
Name: name,
|
|
Hostname: strings.TrimSpace(body.Hostname),
|
|
TokenPrefix: prefix,
|
|
TokenHash: hash,
|
|
ClientVersion: strings.TrimSpace(body.ClientVersion),
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, store.ErrInvalidInput) {
|
|
writeProblem(w, http.StatusConflict, "Conflict", "client token already enrolled")
|
|
return
|
|
}
|
|
writeInternalError(w, "internal", err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, map[string]any{
|
|
"client_id": client.ID,
|
|
"status": client.Status,
|
|
"message": "pending operator approval in EvoBGP UI",
|
|
})
|
|
}
|
|
|
|
func (s *Server) firewallEnrollTenantID() (string, error) {
|
|
tid, _, _, _, _ := s.store.DemoIDs()
|
|
if tid != "" {
|
|
return tid, nil
|
|
}
|
|
ids, err := s.store.ListTenantIDs()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(ids) == 0 {
|
|
return "", errors.New("httpapi: no tenant for firewall enroll")
|
|
}
|
|
return ids[0], nil
|
|
}
|
|
|
|
func (s *Server) handleFirewallInstallScript(w http.ResponseWriter, r *http.Request) {
|
|
s.serveFirewallScript(w, "install.sh")
|
|
}
|
|
|
|
func (s *Server) handleFirewallSyncScript(w http.ResponseWriter, r *http.Request) {
|
|
s.serveFirewallScript(w, "evobgp-firewall.sh")
|
|
}
|
|
|
|
func (s *Server) serveFirewallScript(w http.ResponseWriter, name string) {
|
|
path := filepath.Join("scripts", "firewall", name)
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
writeProblem(w, http.StatusNotFound, "Not Found", "script not found")
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/x-shellscript; charset=utf-8")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(b)
|
|
}
|
|
|
|
func (s *Server) handleListFirewallClients(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
items, err := s.store.ListFirewallClients(a.TenantID)
|
|
if err != nil {
|
|
writeInternalError(w, "internal", err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
}
|
|
|
|
func (s *Server) handleGetFirewallClient(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
id := strings.TrimSpace(r.PathValue("id"))
|
|
client, err := s.store.GetFirewallClient(a.TenantID, id)
|
|
if err != nil {
|
|
writeProblem(w, http.StatusNotFound, "Not Found", "client not found")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, client)
|
|
}
|
|
|
|
func (s *Server) handlePatchFirewallClient(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "operator") {
|
|
return
|
|
}
|
|
id := strings.TrimSpace(r.PathValue("id"))
|
|
var patch store.FirewallClientPatch
|
|
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid JSON body")
|
|
return
|
|
}
|
|
client, err := s.store.UpdateFirewallClient(a.TenantID, id, &patch)
|
|
if err != nil {
|
|
writeProblem(w, http.StatusNotFound, "Not Found", "client not found")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, client)
|
|
}
|
|
|
|
func (s *Server) handleApproveFirewallClient(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "operator") {
|
|
return
|
|
}
|
|
id := strings.TrimSpace(r.PathValue("id"))
|
|
client, err := s.store.ApproveFirewallClient(a.TenantID, id, a.APIKeyID)
|
|
if err != nil {
|
|
writeProblem(w, http.StatusNotFound, "Not Found", "client not found")
|
|
return
|
|
}
|
|
_ = s.firewallResolver.Reload(s.store)
|
|
go s.replicateFirewallStateToSpeakers(a.TenantID)
|
|
writeJSON(w, http.StatusOK, client)
|
|
}
|
|
|
|
func (s *Server) handleRevokeFirewallClient(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "operator") {
|
|
return
|
|
}
|
|
id := strings.TrimSpace(r.PathValue("id"))
|
|
if err := s.store.RevokeFirewallClient(a.TenantID, id); err != nil {
|
|
writeProblem(w, http.StatusNotFound, "Not Found", "client not found")
|
|
return
|
|
}
|
|
_ = s.firewallResolver.Reload(s.store)
|
|
go s.replicateFirewallStateToSpeakers(a.TenantID)
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "revoked"})
|
|
}
|
|
|
|
func (s *Server) handleDeleteFirewallClient(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "operator") {
|
|
return
|
|
}
|
|
id := strings.TrimSpace(r.PathValue("id"))
|
|
if err := s.store.DeleteFirewallClient(a.TenantID, id); err != nil {
|
|
writeProblem(w, http.StatusNotFound, "Not Found", "client not found")
|
|
return
|
|
}
|
|
_ = s.firewallResolver.Reload(s.store)
|
|
go s.replicateFirewallStateToSpeakers(a.TenantID)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleListFirewallRules(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
scope := strings.TrimSpace(r.URL.Query().Get("scope"))
|
|
var clientID *string
|
|
if scope == "client" {
|
|
cid := strings.TrimSpace(r.URL.Query().Get("client_id"))
|
|
if cid == "" {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "client_id required for scope=client")
|
|
return
|
|
}
|
|
clientID = &cid
|
|
}
|
|
items, err := s.store.ListFirewallRules(a.TenantID, clientID)
|
|
if err != nil {
|
|
writeInternalError(w, "internal", err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
}
|
|
|
|
func (s *Server) handleCreateFirewallRule(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "operator") {
|
|
return
|
|
}
|
|
var body struct {
|
|
Scope string `json:"scope"`
|
|
ClientID *string `json:"client_id"`
|
|
Action string `json:"action"`
|
|
CommunityID *string `json:"community_id"`
|
|
Comment string `json:"comment"`
|
|
Priority *int `json:"priority"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid JSON body")
|
|
return
|
|
}
|
|
var clientID *string
|
|
if strings.TrimSpace(body.Scope) == "client" {
|
|
if body.ClientID == nil || strings.TrimSpace(*body.ClientID) == "" {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "client_id required for scope=client")
|
|
return
|
|
}
|
|
cid := strings.TrimSpace(*body.ClientID)
|
|
clientID = &cid
|
|
}
|
|
rule, err := s.store.CreateFirewallRule(a.TenantID, clientID, &store.FirewallRuleCreate{
|
|
Priority: body.Priority,
|
|
Action: body.Action,
|
|
CommunityID: body.CommunityID,
|
|
Comment: body.Comment,
|
|
})
|
|
if err != nil {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "invalid rule")
|
|
return
|
|
}
|
|
go s.replicateFirewallStateToSpeakers(a.TenantID)
|
|
writeJSON(w, http.StatusCreated, rule)
|
|
}
|
|
|
|
func (s *Server) handlePatchFirewallRule(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "operator") {
|
|
return
|
|
}
|
|
id := strings.TrimSpace(r.PathValue("id"))
|
|
var patch store.FirewallRulePatch
|
|
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid JSON body")
|
|
return
|
|
}
|
|
rule, err := s.store.UpdateFirewallRule(a.TenantID, id, &patch)
|
|
if err != nil {
|
|
writeProblem(w, http.StatusNotFound, "Not Found", "rule not found")
|
|
return
|
|
}
|
|
go s.replicateFirewallStateToSpeakers(a.TenantID)
|
|
writeJSON(w, http.StatusOK, rule)
|
|
}
|
|
|
|
func (s *Server) handleDeleteFirewallRule(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "operator") {
|
|
return
|
|
}
|
|
id := strings.TrimSpace(r.PathValue("id"))
|
|
if err := s.store.DeleteFirewallRule(a.TenantID, id); err != nil {
|
|
writeProblem(w, http.StatusNotFound, "Not Found", "rule not found")
|
|
return
|
|
}
|
|
go s.replicateFirewallStateToSpeakers(a.TenantID)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleReorderFirewallRules(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "operator") {
|
|
return
|
|
}
|
|
var body struct {
|
|
Scope string `json:"scope"`
|
|
ClientID *string `json:"client_id"`
|
|
OrderedIDs []string `json:"ordered_ids"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid JSON body")
|
|
return
|
|
}
|
|
var clientID *string
|
|
if strings.TrimSpace(body.Scope) == "client" {
|
|
if body.ClientID == nil || strings.TrimSpace(*body.ClientID) == "" {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "client_id required")
|
|
return
|
|
}
|
|
cid := strings.TrimSpace(*body.ClientID)
|
|
clientID = &cid
|
|
}
|
|
if err := s.store.ReorderFirewallRules(a.TenantID, clientID, body.OrderedIDs); err != nil {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "invalid reorder")
|
|
return
|
|
}
|
|
go s.replicateFirewallStateToSpeakers(a.TenantID)
|
|
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
|
}
|
|
|
|
func (s *Server) handleFirewallBlocklist(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireFirewall(w, a) {
|
|
return
|
|
}
|
|
client, err := s.store.GetFirewallClient(a.TenantID, a.APIKeyID)
|
|
if err != nil {
|
|
writeProblem(w, http.StatusUnauthorized, "Unauthorized", "unknown firewall client")
|
|
return
|
|
}
|
|
if client.Status != "approved" {
|
|
w.Header().Set("Retry-After", "60")
|
|
writeProblem(w, http.StatusForbidden, "Forbidden", "client pending approval")
|
|
return
|
|
}
|
|
_ = s.store.TouchFirewallClientLastSeen(client.ID, "cp", clientIP(r), r.UserAgent())
|
|
resp, err := s.buildFirewallBlocklist(r.Context(), client)
|
|
if err != nil {
|
|
if errors.Is(err, errNoFirewallRevision) {
|
|
writeProblem(w, http.StatusNotFound, "Not Found", "no published revision")
|
|
return
|
|
}
|
|
writeInternalError(w, "internal", err)
|
|
return
|
|
}
|
|
w.Header().Set("X-EvoBGP-Source", "cp")
|
|
w.Header().Set("X-EvoBGP-Revision-ID", resp.RevisionID)
|
|
w.Header().Set("X-EvoBGP-Generated-At", resp.GeneratedAt)
|
|
w.Header().Set("X-EvoBGP-Rules-Version", resp.RulesVersion)
|
|
if strings.Contains(r.Header.Get("Accept"), "text/plain") {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
for _, p := range resp.Prefixes {
|
|
_, _ = w.Write([]byte(p + "\n"))
|
|
}
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (s *Server) handleFirewallApplyReport(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireFirewall(w, a) {
|
|
return
|
|
}
|
|
var body struct {
|
|
Status string `json:"status"`
|
|
Error string `json:"error"`
|
|
PrefixCount int `json:"prefix_count"`
|
|
IPCount int `json:"ip_count"`
|
|
Version string `json:"version"`
|
|
KernelMethod string `json:"kernel_method"`
|
|
Source string `json:"source"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid JSON body")
|
|
return
|
|
}
|
|
src := strings.TrimSpace(body.Source)
|
|
if src == "" {
|
|
src = "cp"
|
|
}
|
|
_ = s.store.TouchFirewallClientLastApply(a.APIKeyID, src, body.Status, body.Error, body.PrefixCount, body.IPCount)
|
|
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
|
}
|
|
|
|
func (s *Server) handleFirewallHeartbeat(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireFirewall(w, a) {
|
|
return
|
|
}
|
|
var body struct {
|
|
Source string `json:"source"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
src := strings.TrimSpace(body.Source)
|
|
if src == "" {
|
|
src = "cp"
|
|
}
|
|
_ = s.store.TouchFirewallClientLastSeen(a.APIKeyID, src, clientIP(r), r.UserAgent())
|
|
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
|
}
|
|
|
|
func (s *Server) handleFirewallClientPreview(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
id := strings.TrimSpace(r.PathValue("id"))
|
|
client, err := s.store.GetFirewallClient(a.TenantID, id)
|
|
if err != nil {
|
|
writeProblem(w, http.StatusNotFound, "Not Found", "client not found")
|
|
return
|
|
}
|
|
resp, err := s.buildFirewallBlocklist(r.Context(), client)
|
|
if err != nil {
|
|
if errors.Is(err, errNoFirewallRevision) {
|
|
writeProblem(w, http.StatusNotFound, "Not Found", "no published revision")
|
|
return
|
|
}
|
|
writeInternalError(w, "internal", err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
var errNoFirewallRevision = errors.New("httpapi: no firewall revision")
|
|
|
|
type firewallBlocklistResponse struct {
|
|
ClientID string `json:"client_id"`
|
|
RevisionID string `json:"revision_id"`
|
|
GeneratedAt string `json:"generated_at"`
|
|
Source string `json:"source"`
|
|
RulesApplied int `json:"rules_applied"`
|
|
CommunitiesEvaluated int `json:"communities_evaluated"`
|
|
CommunitiesBlocked int `json:"communities_blocked"`
|
|
Prefixes []string `json:"prefixes"`
|
|
Total int `json:"total"`
|
|
Hash string `json:"hash"`
|
|
RulesVersion string `json:"-"`
|
|
}
|
|
|
|
func (s *Server) buildFirewallBlocklist(ctx context.Context, client *store.FirewallClient) (*firewallBlocklistResponse, error) {
|
|
_ = ctx
|
|
revs, _, _ := s.store.ListRevisions(client.TenantID, "", "", 1)
|
|
if len(revs) == 0 {
|
|
return nil, errNoFirewallRevision
|
|
}
|
|
rev := revs[0]
|
|
prefixesByCommunity, commCount, err := s.loadPrefixesByCommunity(client.TenantID, rev.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rules, err := s.store.ListAllFirewallRulesForClient(client.TenantID, client.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fwRules := storeRulesToFirewall(rules)
|
|
blocked := firewall.Evaluate(client.ID, fwRules, prefixesByCommunity)
|
|
blockedComm := countBlockedCommunities(client.ID, fwRules, prefixesByCommunity)
|
|
hash := prefixListHash(blocked)
|
|
return &firewallBlocklistResponse{
|
|
ClientID: client.ID,
|
|
RevisionID: rev.ID,
|
|
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
|
|
Source: "cp",
|
|
RulesApplied: len(rules),
|
|
CommunitiesEvaluated: commCount,
|
|
CommunitiesBlocked: blockedComm,
|
|
Prefixes: blocked,
|
|
Total: len(blocked),
|
|
Hash: hash,
|
|
RulesVersion: firewall.RulesVersionHash(fwRules),
|
|
}, nil
|
|
}
|
|
|
|
func (s *Server) loadPrefixesByCommunity(tenantID, revisionID string) (map[string][]string, int, error) {
|
|
out := make(map[string][]string)
|
|
communities := make(map[string]struct{})
|
|
cursor := ""
|
|
for {
|
|
rows, next, more := s.store.ListRevisionPrefixes(tenantID, revisionID, cursor, 5000)
|
|
for _, row := range rows {
|
|
key := ""
|
|
if row.CommunityID != nil {
|
|
key = strings.TrimSpace(*row.CommunityID)
|
|
}
|
|
communities[key] = struct{}{}
|
|
out[key] = append(out[key], strings.TrimSpace(row.Prefix))
|
|
}
|
|
if !more {
|
|
break
|
|
}
|
|
cursor = next
|
|
}
|
|
return out, len(communities), nil
|
|
}
|
|
|
|
func storeRulesToFirewall(rules []*store.FirewallRule) []firewall.Rule {
|
|
out := make([]firewall.Rule, 0, len(rules))
|
|
for _, r := range rules {
|
|
var cid *string
|
|
if r.CommunityID != nil {
|
|
v := *r.CommunityID
|
|
cid = &v
|
|
}
|
|
var cl *string
|
|
if r.ClientID != nil {
|
|
v := *r.ClientID
|
|
cl = &v
|
|
}
|
|
out = append(out, firewall.Rule{
|
|
ClientID: cl,
|
|
Priority: r.Priority,
|
|
Action: r.Action,
|
|
CommunityID: cid,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func countBlockedCommunities(clientID string, rules []firewall.Rule, prefixesByCommunity map[string][]string) int {
|
|
n := 0
|
|
for k := range prefixesByCommunity {
|
|
ordered := mergeRulesForCount(clientID, rules)
|
|
for _, r := range ordered {
|
|
if r.CommunityID == nil || strings.TrimSpace(*r.CommunityID) == k {
|
|
if strings.EqualFold(r.Action, "block") {
|
|
n++
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func mergeRulesForCount(clientID string, rules []firewall.Rule) []firewall.Rule {
|
|
var clientRules, tenantRules []firewall.Rule
|
|
for _, r := range rules {
|
|
if r.ClientID != nil && *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 := append([]firewall.Rule{}, clientRules...)
|
|
return append(out, tenantRules...)
|
|
}
|
|
|
|
func prefixListHash(prefixes []string) string {
|
|
cp := append([]string(nil), prefixes...)
|
|
sort.Strings(cp)
|
|
sum := sha256.Sum256([]byte(strings.Join(cp, "\n")))
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func clientIP(r *http.Request) string {
|
|
if xff := strings.TrimSpace(r.Header.Get("X-Forwarded-For")); xff != "" {
|
|
parts := strings.Split(xff, ",")
|
|
return strings.TrimSpace(parts[0])
|
|
}
|
|
host := r.RemoteAddr
|
|
if i := strings.LastIndex(host, ":"); i >= 0 {
|
|
return host[:i]
|
|
}
|
|
return host
|
|
}
|