Documentation
¶
Index ¶
- Constants
- Variables
- func AppendAudit(statePath, action, ip, reason, source string, duration time.Duration)
- func CloudflareRangesCover(v4, v6 []string, ip string) bool
- func CountryDBDir(cfg *FirewallConfig, statePath string) string
- func CoversExpiry(held, wanted time.Time) bool
- func EffectiveDOSExemptNets(cfg *FirewallConfig, providerNets []*net.IPNet) (v4, v6 []*net.IPNet)
- func FetchCloudflareIPs(ctx context.Context) (ipv4, ipv6 []string, err error)
- func InferProvenance(action, reason string) string
- func LoadCFRefreshTime(statePath string) time.Time
- func LoadCFState(statePath string) (ipv4, ipv6 []string)
- func LookupIP(dbPath string, ip string) []string
- func MergeInfraIPs(topLevel, firewallSpecific []string) []string
- func ParseOutAllowDst(dst string) (*net.IPNet, error)
- func ProtocolAllowNames() []string
- func SameBlockedEntry(a, b BlockedEntry) bool
- func SaveCFState(statePath string, ipv4, ipv6 []string, refreshed time.Time) error
- func Supported() bool
- func UpdateGeoIPDB(dbPath string, countryCodes []string) (int, error)
- type ActionElement
- type ActionKernel
- type ActionMember
- type ActionObservation
- type ActionRequest
- type ActionRuleset
- type ActionSet
- type ActionStore
- type AllowedEntry
- type AuditEntry
- type BlockAction
- type BlockAdmission
- type BlockAnswer
- type BlockBatchResult
- type BlockCheck
- type BlockEffect
- type BlockImport
- type BlockMember
- type BlockOutcome
- type BlockRefusal
- type BlockResult
- type BlockVictim
- type BlockVictimEffect
- type BlockedEntry
- type DynDNSResolver
- func (d *DynDNSResolver) AddHost(host string)
- func (d *DynDNSResolver) RegisterInfraHost(host string)
- func (d *DynDNSResolver) Run(stopCh <-chan struct{})
- func (d *DynDNSResolver) SetFindingSink(sink func(host string))
- func (d *DynDNSResolver) SetInfraEngine(eng interface{ ... })
- func (d *DynDNSResolver) UnresolvableHosts() []string
- type Engine
- func (e *Engine) AllowIP(ip string, reason string) error
- func (e *Engine) AllowIPPort(ip string, port int, proto string, reason string) (resultErr error)
- func (e *Engine) Apply() (resultErr error)
- func (e *Engine) ApplyBlockBatch(members []BlockMember) ([]BlockResult, error)
- func (e *Engine) AttachLifecycle(l *Lifecycle) error
- func (e *Engine) BlockActionsUnproven() error
- func (e *Engine) BlockAdmissionSnapshot(members []BlockMember, shadow []string) (BlockAdmission, error)
- func (e *Engine) BlockBatchRequest(header ActionRequest, members []ActionRequest, budget *ScanAdmission) (BlockBatchResult, error)
- func (e *Engine) BlockIP(ip string, reason string, timeout time.Duration) error
- func (e *Engine) BlockIPForUndo(ip, reason string, ttl time.Duration) (before, after *BlockedEntry, resultErr error)
- func (e *Engine) BlockIPForce(ip string, reason string, timeout time.Duration) (resultErr error)
- func (e *Engine) BlockIPForcePreserveLifetime(ip, reason string, timeout time.Duration) (resultErr error)
- func (e *Engine) BlockIPOutcome(ip string, reason string, timeout time.Duration) (outcome BlockOutcome, resultErr error)
- func (e *Engine) BlockIPOutcomeWithFindingID(ip, reason string, timeout time.Duration, findingID string) (outcome BlockOutcome, resultErr error)
- func (e *Engine) BlockIPRequest(req ActionRequest, budget *ScanAdmission) (BlockOutcome, error)
- func (e *Engine) BlockSubnet(cidr string, reason string, timeout time.Duration) (resultErr error)
- func (e *Engine) BlockSubnetRequest(req ActionRequest, budget *ScanAdmission) (resultErr error)
- func (e *Engine) BlockSubnetWithFindingID(cidr, reason string, timeout time.Duration, findingID string) (resultErr error)
- func (e *Engine) BlockedCount() int
- func (e *Engine) BlockedSubnetCovering(ip string) (string, bool)
- func (e *Engine) BlockedSubnets() []SubnetEntry
- func (e *Engine) CheckBlockBatch(members []BlockMember) ([]BlockRefusal, error)
- func (e *Engine) CheckBlockBatchEffects(members []BlockMember) ([]BlockCheck, error)
- func (e *Engine) CleanExpiredAllows() int
- func (e *Engine) CleanExpiredSubnets() int
- func (e *Engine) CloudflareCovers(ip string) bool
- func (e *Engine) CloudflareIPs() (ipv4, ipv6 []string)
- func (e *Engine) Config() *FirewallConfig
- func (e *Engine) DropInfraResolved(host string)
- func (e *Engine) DurableActionsEnabled() bool
- func (e *Engine) FirewallScanBudget(window string) (int, error)
- func (e *Engine) FlushBlocked() (resultErr error)
- func (e *Engine) HoldBlockBatches(held bool)
- func (e *Engine) IsAllowed(ip string) bool
- func (e *Engine) IsBlocked(ip string) bool
- func (e *Engine) IsBlockedLive(ip string) (bool, error)
- func (e *Engine) IsSubnetBlocked(cidr string) bool
- func (e *Engine) LeaseNow() time.Time
- func (e *Engine) LiveBlockedSet() (LiveBlockedSnapshot, error)
- func (e *Engine) MarkBlockActionsUnverified(actions []string) (int, error)
- func (e *Engine) PendingActions() ([]FirewallAction, error)
- func (e *Engine) ProbeBlockState() error
- func (e *Engine) PromoteToPermanentBlock(ip, reason string) (resultErr error)
- func (e *Engine) PromoteToPermanentBlockWithFindingID(ip, reason, findingID string) (resultErr error)
- func (e *Engine) RecoverActions() error
- func (e *Engine) RefreshDOSExemptSets(providerNets []*net.IPNet) error
- func (e *Engine) RemoveAllowIP(ip string) (resultErr error)
- func (e *Engine) RemoveAllowIPBySource(ip, source string) (resultErr error)
- func (e *Engine) RemoveAllowIPPort(ip string, port int, proto string) (resultErr error)
- func (e *Engine) RemoveBlockActionRows(actions []string) (int, error)
- func (e *Engine) ResolveAction(id, outcome, detail string) (FirewallAction, error)
- func (e *Engine) ResolvedInfraIPs(hosts []string) []string
- func (e *Engine) RestoreBlockIfUnchanged(ip string, expected, prior *BlockedEntry) (resultErr error)
- func (e *Engine) RetireBlockActions(actions []BlockAction) ([]bool, error)
- func (e *Engine) RuleCounts() RuleCounts
- func (e *Engine) RulesetSnapshot() (current, applied string, err error)
- func (e *Engine) SetBlockActionsUnproven(cause error)
- func (e *Engine) SetBootClock(read func() (bootID string, sinceBoot time.Duration, err error))
- func (e *Engine) SetConfig(cfg *FirewallConfig)
- func (e *Engine) SetDOSExemptProviderNets(nets []*net.IPNet)
- func (e *Engine) SetDryRunEnabledFunc(fn func() bool)
- func (e *Engine) SetDryRunRecorder(fn func(ip, reason string, timeout time.Duration))
- func (e *Engine) SetLeaseFloor(floor time.Time)
- func (e *Engine) SetShutdownContext(ctx context.Context)
- func (e *Engine) SetSoftAllowChecker(fn func(ip string) bool)
- func (e *Engine) SetVerdictAsker(...)
- func (e *Engine) StateSetAside() string
- func (e *Engine) Status() map[string]interface{}
- func (e *Engine) TempAllowIP(ip string, reason string, timeout time.Duration) error
- func (e *Engine) UnblockIP(ip string) (resultErr error)
- func (e *Engine) UnblockIPForUndo(ip string) (before *BlockedEntry, resultErr error)
- func (e *Engine) UnblockSubnet(cidr string) (resultErr error)
- func (e *Engine) UndoAction(req ActionRequest) (FirewallAction, error)
- func (e *Engine) UpdateCloudflareSet(ipv4, ipv6 []string) error
- func (e *Engine) UpdateInfraResolved(host string, ips []string)
- func (e *Engine) ValidateSubnetBlock(cidr string) error
- type ExistingBlock
- type FirewallAction
- type FirewallConfig
- type FirewallState
- type Lifecycle
- func (l *Lifecycle) DeliverAudit() error
- func (l *Lifecycle) Execute(plan FirewallAction, kernel ActionKernel) (FirewallAction, error)
- func (l *Lifecycle) Recover(kernel ActionKernel) error
- func (l *Lifecycle) Resolve(id, outcome, detail string, kernel ActionKernel) (FirewallAction, error)
- func (l *Lifecycle) Undo(req ActionRequest, kernel ActionKernel) (FirewallAction, error)
- type LiveBlockedSnapshot
- type OutAllowRule
- type PortAllowEntry
- type PortFloodRule
- type ProtocolAllowRule
- type RuleCounts
- type ScanAdmission
- type StateStore
- type SubnetEntry
Constants ¶
const ( SourceUnknown = "unknown" SourceWebUI = "web_ui" SourceCLI = "cli" SourceAutoResponse = "auto_response" SourceChallenge = "challenge" SourceWhitelist = "whitelist" SourceDynDNS = "dyndns" SourceSystem = "system" )
const ( // CloudflareCoverageWarning is shared by every operator-facing block // response so API, CLI, and finding wording cannot drift apart. CloudflareCoverageWarning = "IP is inside a Cloudflare allow range; ports 80/443 from it are still accepted" )
const MaxActionMembers = MaxBlockBatchMembers
MaxActionMembers bounds one batch manifest. It matches the block batch applier, so one durable batch can carry one applier batch.
const MaxBlockBatchMembers = 64
MaxBlockBatchMembers bounds one block batch, as the admission ledger bounds an execution batch (spec 5.5).
const MaxBlockImports = 256
MaxBlockImports bounds the imports one snapshot lists: the soonest to expire, which admission evicts first among imports.
const MaxBlockReasonBytes = 256
MaxBlockReasonBytes bounds the reason a batch member writes on its row.
const MaxShadowBlocks = 20000
MaxShadowBlocks bounds the forecast addresses one admission snapshot counts: a dry-run forecast models at most this many active blocks.
Variables ¶
var ( ErrActionDryRun = errors.New("firewall action suppressed by dry-run policy") ErrActionAuditPending = errors.New("firewall action verified; audit delivery pending") ErrActionMissing = errors.New("firewall action not found") ErrActionUnknown = errors.New("firewall action outcome requires recovery") ErrActionFailed = errors.New("firewall action was not applied") ErrScanBudget = errors.New("firewall scan budget exhausted") )
var ( // ErrStateCommitUncertain means the transaction body succeeded but storage // did not acknowledge commit. The new revision may be visible; callers must // reconcile before retrying and must not infer rollback or durable success. ErrStateCommitUncertain = errors.New("firewall state commit outcome uncertain") // ErrStateUninitialized means no complete snapshot has been committed. ErrStateUninitialized = errors.New("firewall state is uninitialized") // ErrStateConflict requires re-reading state before constructing another change. ErrStateConflict = errors.New("firewall state revision conflict") // ErrStateCorrupt means stored state cannot safely be used or overwritten. ErrStateCorrupt = errors.New("firewall state is corrupt or unsupported") // ErrStateUnsupported means a newer build wrote the stored value. It is // refused like corruption: this build can neither read nor overwrite it. ErrStateUnsupported = fmt.Errorf("%w: written by a newer build", ErrStateCorrupt) )
var ErrAdmissionOwnsBlock = errors.New("an admission action owns the block")
ErrAdmissionOwnsBlock refuses an automatic rewrite of a block an admission action wrote: its owner retires it (design C8).
var ErrBlockBatchRejected = errors.New("kernel rejected the block batch")
ErrBlockBatchRejected marks a member that failed because the kernel rejected the whole transaction; a failure before the send does not carry it. A failed state restore carries ErrBlockBatchState instead. The owner retries rejected members one at a time (P9).
var ErrBlockBatchState = errors.New("firewall batch state is unproven")
ErrBlockBatchState requires the owner to hold automatic execution while the engine's state safety changes lack a durable acknowledgement.
var ErrBlockChanged = errors.New("block changed since the action; undo is no longer available")
ErrBlockChanged prevents undo from replacing a later firewall decision.
var ErrConfirmPending = errors.New("a firewall change awaits confirmation")
ErrConfirmPending refuses admission's block batches while an apply-confirmed window can roll the firewall back (design C9).
var ErrIPProtected = errors.New("ip is protected from auto-block")
ErrIPProtected identifies safety refusals for IP and subnet blocks, including local scopes, the host's own addresses, operator infra_ips, default routes, and subnets overlapping allowed addresses. Callers check errors.Is(err, ErrIPProtected) to treat the refusal as an expected no-op; they neither record a block nor log it as a failure. The triggering finding or incident is unaffected, so suspicious activity attributed to a protected address is still surfaced.
var ErrLongerBlock = errors.New("IP has a longer block; unblock it explicitly before shortening its lifetime")
ErrLongerBlock requires an explicit unblock before shortening a timed block.
var ErrPermanentBlock = errors.New("IP is permanently blocked; unblock it explicitly before applying a timed block")
ErrPermanentBlock requires an explicit unblock before a timed Web UI block.
var ErrStateNotKeptAside = errors.New("firewall state unreadable and not kept aside")
ErrStateNotKeptAside requires a new startup after unreadable state could not be preserved. Retrying with a fresh engine can miss an unsynced rename.
var ErrUnsupportedPlatform = errors.New("nftables firewall not supported on this platform")
ErrUnsupportedPlatform is returned by every mutating Engine method on platforms without nftables support. The stubs used to return nil, which let callers record blocks that never reached any kernel; failing loudly keeps a miswired caller from trusting a phantom mutation.
Functions ¶
func AppendAudit ¶
AppendAudit writes an audit entry to the JSONL audit log. Rotates the log when it exceeds 10 MB.
func CloudflareRangesCover ¶
CloudflareRangesCover reports whether ip falls inside any of the given Cloudflare CIDR ranges. The input chain accepts Cloudflare edges on TCP 80/443 before the blocked-IP drop, so blocking a covered IP does not stop its web traffic; callers use this to warn the operator at block time. Malformed CIDRs are skipped so one bad cache line cannot hide coverage.
func CountryDBDir ¶
func CountryDBDir(cfg *FirewallConfig, statePath string) string
CountryDBDir returns the directory holding the per-country CIDR files: country_db_path when set, otherwise the geoip directory under the state path, which is where `csm firewall update-geoip` writes. Every consumer (engine, CLI update and lookup) resolves through here so an empty setting cannot leave country blocking armed on one surface and inert on another.
func CoversExpiry ¶
CoversExpiry reports whether an effect ending at held lasts as long as one asked to end at wanted: a permanent effect, zero held, or one ending no earlier than wanted within the readback slack. A dry-run forecast judges its own blocks by it, so they cover exactly as real ones do.
func EffectiveDOSExemptNets ¶
func EffectiveDOSExemptNets(cfg *FirewallConfig, providerNets []*net.IPNet) (v4, v6 []*net.IPNet)
EffectiveDOSExemptNets returns the union of operator ranges and (when enabled) provider ranges, split into IPv4 and IPv6 *net.IPNet slices. providerNets may be nil. Invalid operator entries are skipped (validation already rejected them at load; this is defense in depth).
func FetchCloudflareIPs ¶
FetchCloudflareIPs downloads the current Cloudflare IP ranges, honouring ctx. A caller shutting down cancels it rather than waiting out the HTTP timeout: on a host that cannot reach cloudflare.com -- an egress-restricted server among them -- that wait is the full timeout, twice.
func InferProvenance ¶
InferProvenance classifies a firewall entry source from structured action/reason text. This keeps provenance logic centralized instead of spreading fragile string checks throughout the web UI and firewall call sites.
func LoadCFRefreshTime ¶
LoadCFRefreshTime reads the last CF refresh time from state.
func LoadCFState ¶
LoadCFState reads the cached Cloudflare CIDRs.
func LookupIP ¶
LookupIP finds which country CIDR files contain the given IP. Returns matching country codes.
func MergeInfraIPs ¶
MergeInfraIPs returns the effective firewall infra list in configuration order. Equivalent IP spellings share one entry because nftables enforces the parsed address or network, not the operator's original text.
func ParseOutAllowDst ¶
ParseOutAllowDst accepts a bare IP or a CIDR and returns it as a network. A bare address becomes a host route; mapped IPv4 prefixes use IPv4 widths.
func ProtocolAllowNames ¶
func ProtocolAllowNames() []string
ProtocolAllowNames lists the protocols firewall.protocol_allow accepts.
func SameBlockedEntry ¶
func SameBlockedEntry(a, b BlockedEntry) bool
SameBlockedEntry compares snapshots without relying on time.Time locations.
func SaveCFState ¶
SaveCFState persists the Cloudflare CIDRs for status and coverage checks.
func Supported ¶
func Supported() bool
Supported reports whether this build includes the nftables engine.
func UpdateGeoIPDB ¶
UpdateGeoIPDB downloads country CIDR lists from a public source. Creates one file per country code per family: {dbPath}/{CC}.cidr (IPv4) and {dbPath}/{CC}.cidr6 (IPv6). IPv6 is best-effort so a country with no v6 allocation does not fail the update. The return value is the number of CIDR files refreshed.
Types ¶
type ActionElement ¶
type ActionKernel ¶
type ActionKernel interface {
ObserveFirewallAction(FirewallAction) (ActionObservation, error)
ApplyFirewallAction(FirewallAction) error
}
ActionKernel compares the affected targets, including provenance and expiry. Recovery only observes; it never calls ApplyFirewallAction.
type ActionMember ¶
type ActionMember struct {
Request ActionRequest `json:"request"`
Budget *ScanAdmission `json:"budget,omitempty"`
// Victims are the rows this member removes to make room, exactly as they
// were, so undo can prove they are still current.
Victims []BlockedEntry `json:"victims,omitempty"`
}
ActionMember is one immutable single-target request inside a batch, with its admission data and the effects its plan records.
type ActionObservation ¶
type ActionObservation struct{ Before, After bool }
type ActionRequest ¶
type ActionRequest struct {
ID string `json:"id"`
Operation string `json:"operation"`
Target string `json:"target"`
Reason string `json:"reason"`
Actor string `json:"actor"`
ActorDetail string `json:"actor_detail,omitempty"`
Source string `json:"source"`
FindingID string `json:"finding_id,omitempty"`
IncidentID string `json:"incident_id,omitempty"`
UndoOf string `json:"undo_of,omitempty"`
Automatic bool `json:"automatic,omitempty"`
TTL time.Duration `json:"ttl,omitempty"`
}
ActionRequest is the identity and policy input for one firewall decision. ID identifies a request, not an individual attempt to contact the kernel.
type ActionRuleset ¶
type ActionRuleset struct {
Generation uint32 `json:"generation_before"`
Marker string `json:"marker"`
}
ActionRuleset proves an atomic whole-ruleset application. An unrelated namespace edit invalidates this conservative recovery proof.
type ActionSet ¶
type ActionSet struct {
Exists bool `json:"exists"`
Name string `json:"name"`
Elements []ActionElement `json:"elements"`
}
ActionSet is detached recovery evidence for one complete nftables set. Expiry is absolute so neither retry nor undo can renew a timed element.
type ActionStore ¶
type ActionStore interface {
StateStore
AdmitFirewallAction(FirewallAction) (FirewallAction, bool, error)
ReadFirewallAction(string) (FirewallAction, error)
// ReadFirewallBatchMember returns the batch of two or more members that
// indexes a member ID, or ErrActionMissing.
ReadFirewallBatchMember(string) (FirewallAction, error)
PendingFirewallActions() ([]FirewallAction, error)
TransitionFirewallAction(string, string, string, time.Time) (FirewallAction, error)
// RetireFirewallAction commits the state an unknown action retires to
// at the given instant (RetiredActionState) and marks it retired.
RetireFirewallAction(string, time.Time) (FirewallAction, error)
FirewallAuditPending() ([]FirewallAction, error)
AcknowledgeFirewallAudit(string, uint64) error
ReadFirewallScanBudget(string) (int, error)
}
ActionStore owns atomic admission, accounting, outcome and audit delivery state. Implementations must keep kernel and audit I/O outside transactions.
type AllowedEntry ¶
type AllowedEntry struct {
IP string `json:"ip"`
Reason string `json:"reason"`
Source string `json:"source,omitempty"`
Port int `json:"port,omitempty"` // 0 = all ports
ExpiresAt time.Time `json:"expires_at,omitempty"` // zero = permanent
}
AllowedEntry represents an allowed IP with metadata.
type AuditEntry ¶
type AuditEntry struct {
Timestamp time.Time `json:"timestamp"`
Action string `json:"action"` // block, unblock, allow, remove_allow, flush, apply
IP string `json:"ip,omitempty"`
Reason string `json:"reason,omitempty"`
Source string `json:"source,omitempty"`
Duration string `json:"duration,omitempty"`
}
AuditEntry records a firewall modification for compliance and forensics.
func ReadAuditLog ¶
func ReadAuditLog(statePath string, limit int) []AuditEntry
ReadAuditLog returns the last N audit entries from the log.
type BlockAction ¶
type BlockAction struct {
Action, IP string
}
BlockAction names the row an admission attempt wrote: its action ID and address.
type BlockAdmission ¶
type BlockAdmission struct {
// Answers holds one answer per member, in member order.
Answers []BlockAnswer
// Temporary is how many timed elements the blocked sets hold, both
// address families together; Limit is the configured temporary limit,
// zero for none.
Temporary, Limit int
// The timed occupants by recorded ownership (spec 5.6): Admission
// counts the elements of rows admission actions wrote, Imported those
// of rows legacy automatic blocks marked, and Unclaimed every other
// timed element plus two elements per timed subnet row, which
// admission never evicts.
Admission, Imported, Unclaimed int
// Drain bounds how long the last unclaimed or imported occupant has left
// after collection and member checks; zero when no time remains,
// -time.Second when a kernel expiry is missing.
Drain time.Duration
// ShadowHeld names the forecast addresses the sets already hold as a
// timed or an untimed element.
ShadowHeld map[string]bool
// Imports lists the imports a member may evict, soonest expiry first,
// at most MaxBlockImports; none while startup could not prove
// admission outcomes.
Imports []BlockImport
}
BlockAdmission is a read-only answer for one admission turn, from one snapshot of the state file and the kernel: each member's local checks and the temporary occupancy. No member takes a temporary slot and no repeated member is refused: the caller decides which members are ready and checks them with ReadyCapacity.
func (BlockAdmission) Full ¶
func (a BlockAdmission) Full() bool
Full reports whether the temporary set has no slot left for any member.
func (BlockAdmission) ReadyCapacity ¶
func (a BlockAdmission) ReadyCapacity(ready []BlockMember) []BlockRefusal
ReadyCapacity answers the members a turn is ready to run, in order, as a batch check answers its capacity: a member repeating an earlier one's address or action is a duplicate, and once the occupancy reaches the limit the rest find the set full. Only a member that passes takes a slot.
type BlockAnswer ¶
type BlockAnswer struct {
Action, IP string
BlockCheck
}
BlockAnswer is an admission snapshot's answer for one member, naming the member it answers.
type BlockBatchResult ¶
type BlockBatchResult struct {
Action FirewallAction
Refusals []BlockRefusal
}
BlockBatchResult is what a durable block batch did: the action it ran or replayed, and, in call order, why each member was left out (empty when the member ran). A replayed batch reports no refusals.
type BlockCheck ¶
type BlockCheck struct {
Refusal BlockRefusal
Existing ExistingBlock
}
BlockCheck is the batch check's answer for one member: its refusal and, for an existing effect, the block that holds the address.
type BlockEffect ¶
type BlockEffect string
BlockEffect is what a batch did to one member.
const ( // BlockApplied: the kernel holds the member's element as encoded. BlockApplied BlockEffect = "applied" // BlockFailed: proven not applied; nothing of it is in the kernel. BlockFailed BlockEffect = "failed" // BlockUnknown: the element may or may not be in the kernel. Its row // stays, marked unverified. BlockUnknown BlockEffect = "unknown" )
type BlockImport ¶
BlockImport is a known automatic block admission may evict (spec 6): a timed, verified row a legacy automatic block stamped, alone at its canonical address, whose element the snapshot proves live. ID is its effect name, which stays the same while the row does.
type BlockMember ¶
type BlockMember struct {
Action string
IP string
Reason string
ExpiresAt time.Time
Victim BlockVictim
}
BlockMember is one timed full-address block an admission attempt may apply: the attempt's ID, the canonical address, the reason written on its row and the absolute expiry its element never outlives. Victim is the admission block whose slot it takes, as the ledger claimed it; zero when it takes a free slot.
type BlockOutcome ¶
type BlockOutcome string
BlockOutcome reports what the firewall engine actually did in response to a BlockIPOutcome call. Auto-response callers consult it to decide whether to apply local side effects (state.IPs append, BlocksThisHour++, permanent threat-db insert, AUTO-BLOCK finding) - those should only fire when the kernel firewall was mutated.
Returned values:
- BlockOutcomeLive: nftables was mutated. Caller should record the block locally and emit the operator-facing Critical "AUTO-BLOCK" finding.
- BlockOutcomeDryRun: auto_response.dry_run intercepted the call. No kernel mutation occurred. Caller should NOT record a real block; emit a Warning-level dry-run notice instead so operators can see what would have been blocked without believing the block landed.
- BlockOutcomeAllowed: the verdict callback returned "allow", so CSM intentionally did not block. Caller should NOT record a block and should NOT emit an AUTO-BLOCK finding (the panel already knows it downgraded the decision).
- BlockOutcomeAllowlisted: the IP is on a soft-allow list (operator allowed_ips, a port-specific operator allow, or a verified-bot range), so the auto-block path declined to block it. The nftables input chain drops @blocked_ips before it accepts operator allows, so an allowlisted IP added to blocked_ips would still be dropped; keeping it out of the set is the only safe fix. Operator `firewall deny` uses BlockIPForce, which bypasses this gate, so an explicit deny still wins. Caller should NOT record a block.
- BlockOutcomeNoop: the IP was already blocked, or a guard (infra-IP, malformed IP, deny-limit) rejected the call. Caller should treat the call as a no-op locally.
const ( BlockOutcomeLive BlockOutcome = "live" BlockOutcomeDryRun BlockOutcome = "dry_run" BlockOutcomeAllowed BlockOutcome = "allowed" BlockOutcomeAllowlisted BlockOutcome = "allowlisted" BlockOutcomeNoop BlockOutcome = "noop" )
type BlockRefusal ¶
type BlockRefusal string
BlockRefusal says why a batch member cannot run; empty means it can.
const ( // BlockRefusedInvalid: a malformed action ID, address, reason or // expiry, or an address not in canonical form. BlockRefusedInvalid BlockRefusal = "invalid" // BlockRefusedExpired: less than a millisecond of the block is left, or // a durable batch member too short to outlive its verification. BlockRefusedExpired BlockRefusal = "expired" // BlockRefusedUnsupported: the address family is not enabled. BlockRefusedUnsupported BlockRefusal = "unsupported" // BlockRefusedProtected: infra, the host's own address or one that is // never a packet's source. BlockRefusedProtected BlockRefusal = "protected" // BlockRefusedAllowed: an operator allow or a verified crawler. BlockRefusedAllowed BlockRefusal = "allowlisted" // BlockRefusedDuplicate: an earlier member names the same address or // action. BlockRefusedDuplicate BlockRefusal = "duplicate" // BlockRefusedExisting: a verified row and its kernel element already // block the address for at least the member's lifetime: an untimed // element for a permanent row, or one with the time a timed row has // left when that row ends no earlier than the member (spec 5.16). BlockRefusedExisting BlockRefusal = "existing_effect" // BlockRefusedPending: a row or a kernel element names the address // without proving an enforced block that covers the member: the row is // unverified, has no element or one with another lifetime, an element // has no row, or the proven block ends before the member. Recovery or // that block's end has to settle it first. BlockRefusedPending BlockRefusal = "pending_recovery" // BlockRefusedFull: no temporary deny slot is left. BlockRefusedFull BlockRefusal = "set_full" )
type BlockResult ¶
type BlockResult struct {
Effect BlockEffect
Refusal BlockRefusal
Existing ExistingBlock
Err error
Victim BlockVictimEffect
}
BlockResult is one member's outcome: its effect, the check that refused it before the kernel batch, or the error behind a failed or unknown one. Existing names the block behind an existing-effect refusal. Victim is what the batch did to the member's victim; empty when it named none.
type BlockVictim ¶
BlockVictim is an admission block a member removes to take its slot (spec 5.6): its action, its canonical address and the absolute expiry it was applied with.
type BlockVictimEffect ¶
type BlockVictimEffect string
BlockVictimEffect is what a batch did to a member's victim.
const ( // VictimRemoved: deleted in the member's transaction, and the readback // proved it gone. VictimRemoved BlockVictimEffect = "removed" // VictimSpared: never deleted, because the member did not run, the // kernel rejected the transaction or the victim no longer qualified. VictimSpared BlockVictimEffect = "spared" // VictimUnproven: the deletion was sent, but neither an uncertain // reply nor the readback proves it. VictimUnproven BlockVictimEffect = "unproven" )
type BlockedEntry ¶
type BlockedEntry struct {
IP string `json:"ip"`
Reason string `json:"reason"`
Source string `json:"source,omitempty"`
BlockedAt time.Time `json:"blocked_at"`
ExpiresAt time.Time `json:"expires_at"` // zero = permanent
// ActionID names the admission attempt that wrote the row; empty for
// every other block. Older builds ignore it.
ActionID string `json:"action_id,omitempty"`
// Unverified marks a row whose kernel element is unproven: the attempt
// ended unknown or has not been settled. It is not reported as a block
// and not restored into the kernel.
Unverified bool `json:"unverified,omitempty"`
// Automatic marks a timed row a legacy automatic block wrote: explicit
// provenance, where Source is read from the reason text (spec 6).
// Admission rows carry ActionID instead. Older builds ignore it.
Automatic bool `json:"automatic,omitempty"`
}
BlockedEntry represents a blocked IP with metadata.
type DynDNSResolver ¶
type DynDNSResolver struct {
// contains filtered or unexported fields
}
DynDNSResolver periodically resolves hostnames and updates the firewall allowed set.
func NewDynDNSResolver ¶
func NewDynDNSResolver(hosts []string, engine interface { AllowIP(ip string, reason string) error RemoveAllowIPBySource(ip string, source string) error }) *DynDNSResolver
NewDynDNSResolver creates a resolver for the given hostnames.
func (*DynDNSResolver) AddHost ¶
func (d *DynDNSResolver) AddHost(host string)
AddHost appends a hostname to the resolver's host list. It is safe to call concurrently. Used in tests to add hosts after construction.
func (*DynDNSResolver) RegisterInfraHost ¶
func (d *DynDNSResolver) RegisterInfraHost(host string)
RegisterInfraHost marks host as an infra hostname. Every subsequent successful resolution will, in addition to AllowIP, call engine.UpdateInfraResolved so the resolved IPs feed the infra-block guard. Idempotent. Wire an infra engine via SetInfraEngine before the resolver's first tick to make this effective.
func (*DynDNSResolver) Run ¶
func (d *DynDNSResolver) Run(stopCh <-chan struct{})
Run starts the periodic resolver. Blocks until stopCh is closed.
func (*DynDNSResolver) SetFindingSink ¶
func (d *DynDNSResolver) SetFindingSink(sink func(host string))
SetFindingSink installs the callback invoked when a host has been unresolvable for longer than gracePeriod. Called from the daemon at startup, after the alert pipeline is wired.
func (*DynDNSResolver) SetInfraEngine ¶
func (d *DynDNSResolver) SetInfraEngine(eng interface { UpdateInfraResolved(host string, ips []string) DropInfraResolved(host string) })
SetInfraEngine wires the engine that receives infra-mode resolution updates. Setting it to nil disables infra routing without affecting the regular allowed-IPs path.
func (*DynDNSResolver) UnresolvableHosts ¶
func (d *DynDNSResolver) UnresolvableHosts() []string
UnresolvableHosts lists infra_ips hostnames currently failing to resolve beyond the grace period.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine manages the nftables firewall ruleset. Manages the nftables ruleset via netlink.
func ConnectExisting ¶
func ConnectExisting(cfg *FirewallConfig, statePath string) (*Engine, error)
ConnectExisting connects to an already-running CSM firewall. Used by CLI commands to modify the live ruleset without reapplying all rules.
func NewEngine ¶
func NewEngine(cfg *FirewallConfig, statePath string) (*Engine, error)
NewEngine creates a new nftables firewall engine.
func (*Engine) AllowIP ¶
AllowIP adds an IP to the allowed set and persists it. If the IP is currently blocked, the block is removed first.
func (*Engine) AllowIPPort ¶
AllowIPPort adds a port-specific IP allow. The rule is persisted to state and applied on the next Apply(). For immediate effect, call Apply() after.
func (*Engine) Apply ¶
Apply builds and atomically applies the complete nftables ruleset. All operations (delete old table + create new table/rules + populate persisted block/allow entries) are batched into a single netlink transaction. If the flush fails, the kernel keeps whatever ruleset was running before - the server is never left without a firewall. Equally important: the new ruleset never appears with EMPTY blocked sets between the table-swap and the persisted-state load; an attacker IP from state.json is blocked from the moment the new table becomes the live one.
func (*Engine) ApplyBlockBatch ¶
func (e *Engine) ApplyBlockBatch(members []BlockMember) ([]BlockResult, error)
ApplyBlockBatch runs the members of one batch: it checks them again against the current state and kernel, writes every remaining row (with its action ID) in one state write, and in one netlink transaction on a fresh connection with a deadline deletes each qualifying victim and adds the members' elements; then it reads the sets back. A member is applied only when the readback shows its element as encoded, and its victim is removed only when the readback of the victim's set shows it gone; a member whose removal is unproven is unknown. A victim's row stays until the owner retires it after the ledger records the outcome, so a restart before that restores it. A victim that could expire before the kernel runs the transaction is spared and its member alone fails set_full. A kernel rejection, including the ENOENT of a victim removed before the send, fails every member and restores the state; the owner retries with new charged attempts (spec 5.5). Any other uncertainty leaves the members unknown, their rows marked unverified. An error means nothing was written or sent.
func (*Engine) AttachLifecycle ¶
AttachLifecycle is a construction-time injection, not a migration. The owner must already have initialized the complete store and own the daemon lock.
func (*Engine) BlockActionsUnproven ¶
BlockActionsUnproven is the cause SetBlockActionsUnproven recorded, or nil.
func (*Engine) BlockAdmissionSnapshot ¶
func (e *Engine) BlockAdmissionSnapshot(members []BlockMember, shadow []string) (BlockAdmission, error)
BlockAdmissionSnapshot answers an admission turn without changing anything: each member's local checks against one snapshot of the state file and the kernel, as a batch check makes them, and the temporary occupancy from the same snapshot. No member takes a slot and no repeat is a duplicate (BlockAdmission.ReadyCapacity checks the members the caller is ready to run). shadow names the canonical addresses a forecast holds, at most MaxShadowBlocks; the answer counts those the sets already hold. An error refuses the whole call: too many members or addresses, a malformed or repeated address, durable state, or no snapshot. Remote checks run outside the engine lock.
func (*Engine) BlockBatchRequest ¶
func (e *Engine) BlockBatchRequest(header ActionRequest, members []ActionRequest, budget *ScanAdmission) (BlockBatchResult, error)
BlockBatchRequest runs automatic temporary blocks as one durable batch (spec 5.5). Each member is checked on its own against one committed snapshot and refused members are left out; the rest share one admission, one kernel transaction and one outcome. A lone valid member runs as an ordinary one-request action under its own ID. A repeated call naming a stored batch observes it and never plans it again. Nothing evicts for a batch: a full temporary set refuses the member.
func (*Engine) BlockIP ¶
BlockIP adds an IP to the blocked set with optional timeout. timeout 0 = permanent block.
Thin wrapper over BlockIPOutcome that discards the outcome. Existing callers that only need success/error semantics keep working; auto- response callers should use BlockIPOutcome so they can suppress local side effects (state mutation, AUTO-BLOCK alert) when the kernel was not actually touched.
func (*Engine) BlockIPForUndo ¶
func (e *Engine) BlockIPForUndo(ip, reason string, ttl time.Duration) (before, after *BlockedEntry, resultErr error)
BlockIPForUndo captures both sides of a forced operator block under the mutation lock. A later CLI decision must never become part of this undo.
func (*Engine) BlockIPForce ¶
BlockIPForce adds an IP to the blocked set unconditionally, bypassing the auto_response.dry_run gate. Use this for operator-initiated commands (CLI, Web UI manual block) where the operator has explicitly decided to block.
func (*Engine) BlockIPForcePreserveLifetime ¶
func (e *Engine) BlockIPForcePreserveLifetime(ip, reason string, timeout time.Duration) (resultErr error)
BlockIPForcePreserveLifetime is the Web UI timed-block path. The guard and replacement share the engine lock, including concurrent CLI promotions.
func (*Engine) BlockIPOutcome ¶
func (e *Engine) BlockIPOutcome(ip string, reason string, timeout time.Duration) (outcome BlockOutcome, resultErr error)
BlockIPOutcome is the AUTO-RESPONSE entry point. It performs the same guards, verdict-callback consultation, and dry-run gating as BlockIP, but additionally reports which path was taken via BlockOutcome so the caller can decide whether to record local state. See the BlockOutcome godoc for the meaning of each return value.
Operator-initiated commands (csm firewall block, Web UI manual block) must call BlockIPForce instead, which skips the dry-run gate unconditionally.
func (*Engine) BlockIPOutcomeWithFindingID ¶
func (e *Engine) BlockIPOutcomeWithFindingID(ip, reason string, timeout time.Duration, findingID string) (outcome BlockOutcome, resultErr error)
BlockIPOutcomeWithFindingID preserves the originating audit identity across every automatic outcome without changing the block policy or result.
func (*Engine) BlockIPRequest ¶
func (e *Engine) BlockIPRequest(req ActionRequest, budget *ScanAdmission) (BlockOutcome, error)
BlockIPRequest carries durable request identity and source-specific admission policy through the existing automatic and manual safety gates.
func (*Engine) BlockSubnet ¶
BlockSubnet adds a CIDR range to the blocked subnets set (IPv4 or IPv6). timeout 0 = permanent block.
func (*Engine) BlockSubnetRequest ¶
func (e *Engine) BlockSubnetRequest(req ActionRequest, budget *ScanAdmission) (resultErr error)
func (*Engine) BlockSubnetWithFindingID ¶
func (e *Engine) BlockSubnetWithFindingID(cidr, reason string, timeout time.Duration, findingID string) (resultErr error)
BlockSubnetWithFindingID records a causal finding for an automatic subnet decision. Manual and maintenance callers use BlockSubnet without one.
func (*Engine) BlockedCount ¶
BlockedCount returns the number of live blocked IP entries the engine is enforcing. Sourced from the same state file Status() uses, so `/api/v1/status` and `csm firewall status` agree on the number. Expired entries are pruned by loadStateFile before being counted.
func (*Engine) BlockedSubnetCovering ¶
BlockedSubnetCovering reports the blocked CIDR (if any) that contains ip. The input-chain drops blocked_nets before the allowed_ips accept, so an allow on an IP inside a blocked subnet has no effect: the subnet drop still fires. Callers surface this so an operator is not told an IP is reachable when a subnet rule still blocks it. The subnet block stays authoritative by design (see subnetSafetyGuardLocked); this only reports, it does not unblock.
func (*Engine) BlockedSubnets ¶
func (e *Engine) BlockedSubnets() []SubnetEntry
BlockedSubnets returns a snapshot of the active persisted subnet blocks. The returned slice is a fresh copy: loadStateFile reuses the warm shared state cache, so the slice it returns can alias internal engine state. Copy it here so a caller mutating the result cannot corrupt the cache. SubnetEntry is a value type (strings + time.Time, no reference fields), so a slice copy is a sufficient deep copy.
func (*Engine) CheckBlockBatch ¶
func (e *Engine) CheckBlockBatch(members []BlockMember) ([]BlockRefusal, error)
CheckBlockBatch answers, before anything is reserved, which members of a batch can run: each is checked on its own against one snapshot of the state file and the kernel, and only the members that fail are refused. An error refuses the whole call: an empty or oversized batch, durable state, or no snapshot.
func (*Engine) CheckBlockBatchEffects ¶
func (e *Engine) CheckBlockBatchEffects(members []BlockMember) ([]BlockCheck, error)
CheckBlockBatchEffects is CheckBlockBatch with the block that holds the address of each member refused as an existing effect.
func (*Engine) CleanExpiredAllows ¶
CleanExpiredAllows removes expired temporary allows from the set and state. An IP is only removed from nftables if no non-expired entries remain for it. Called periodically by the daemon.
func (*Engine) CleanExpiredSubnets ¶
CleanExpiredSubnets removes expired temporary subnet blocks from nftables and state.
func (*Engine) CloudflareCovers ¶
CloudflareCovers reports whether ip is inside a cached Cloudflare range, meaning a block of it leaves TCP 80/443 reachable (the CF accept precedes the blocked drop in the input chain).
func (*Engine) CloudflareIPs ¶
CloudflareIPs returns the currently configured Cloudflare CIDRs from the cached state.
func (*Engine) Config ¶
func (e *Engine) Config() *FirewallConfig
Config returns the ruleset input the engine currently applies.
func (*Engine) DropInfraResolved ¶
DropInfraResolved clears all resolved IPs for a host. Equivalent to UpdateInfraResolved(host, nil); separate name surfaces operator intent at call sites that purposefully retire a hostname.
func (*Engine) DurableActionsEnabled ¶
func (*Engine) FlushBlocked ¶
FlushBlocked removes all IPs from the blocked set and clears persisted state.
func (*Engine) HoldBlockBatches ¶
HoldBlockBatches holds admission's member checks, batches, probes and retirements while an apply-confirmed window is open: its rollback restores the firewall from a snapshot and would erase the rows they write (design C9). The read-only admission snapshot of a forecast still answers. It waits for a call in flight.
func (*Engine) IsAllowed ¶
IsAllowed reports whether ip is on the operator allowed_ips set, read from the in-memory cache built from state.json. Mirrors IsBlocked, including the canonical-form retry for IPv4-mapped IPv6 callers.
func (*Engine) IsBlocked ¶
IsBlocked returns true if the IP is currently in the engine's blocked state. Uses the persisted state file (which is cleaned of expired entries on load).
The lookup is O(1) via the blockedIPIndex map populated from the cached state. Linear scans over the parsed slice are gone -- on hosts with hundreds of persisted blocks the scan was the dominant cost of every connection-handler IsBlocked check.
func (*Engine) IsBlockedLive ¶
IsBlockedLive queries the live nftables set, not the in-memory cache built from state.json. The cache can drift from the kernel when nft auto-expires entries faster than CSM rewrites state.json, or when an out-of-band flush happens. Reconcile loops should consult this method so the local tracker shrinks in lock-step with the kernel; per-packet hot paths should stay on IsBlocked since this issues a netlink RTT.
Malformed IPs are reported as absent. Netlink and engine-initialization failures are returned so callers can keep their cached answer instead of deleting local state on a transient lookup failure.
func (*Engine) IsSubnetBlocked ¶
IsSubnetBlocked returns true if the CIDR is present in the persisted subnet block state.
func (*Engine) LeaseNow ¶
LeaseNow reads the lease clock for a caller outside the engine: the admission owner sets new expiries on it, so a wall clock step back cannot shorten them once the engine encodes their timeouts.
func (*Engine) LiveBlockedSet ¶
func (e *Engine) LiveBlockedSet() (LiveBlockedSnapshot, error)
LiveBlockedSet dumps the blocked v4 and v6 sets once and returns a membership snapshot. IsBlockedLive issues a full set dump per call, so a reconcile pass over N tracked IPs cost N dumps of the entire set; callers testing more than one IP should snapshot instead.
A nil set is left uncovered rather than reported empty, and so is one whose dump failed: the returned snapshot always describes whichever families did answer, with the failures joined into the error. Callers keep their cached view for uncovered families instead of erasing local state on a transient netlink error, while a family that dumped cleanly stays reconciled.
func (*Engine) MarkBlockActionsUnverified ¶
MarkBlockActionsUnverified flags the rows written by the named attempts as unproven, in one state write, and reports how many changed. Startup calls it with the ledger's unresolved attempts before Apply, so their rows are neither restored nor reported as blocks. While a failed state write holds batches it writes the safe state even when nothing is named, and a successful write releases the hold.
func (*Engine) PendingActions ¶
func (e *Engine) PendingActions() ([]FirewallAction, error)
PendingActions reports the actions recovery could not settle. While any exists the engine refuses new mutations, so this is what an operator needs to see before deciding anything.
func (*Engine) ProbeBlockState ¶
ProbeBlockState proves the current snapshot without sending an action.
func (*Engine) PromoteToPermanentBlock ¶
PromoteToPermanentBlock upgrades an existing temporary block on ip to a permanent one: it clears the kernel timeout by deleting the timed element and re-adding it without a timeout, and zeroes ExpiresAt in state. The ordinary block path cannot do this during PermBlock escalation because it skips an already-blocked IP, so the kernel timeout would otherwise expire the block the operator wanted made permanent. Returns an error if the IP is not currently blocked (nothing to promote).
func (*Engine) PromoteToPermanentBlockWithFindingID ¶
func (e *Engine) PromoteToPermanentBlockWithFindingID(ip, reason, findingID string) (resultErr error)
PromoteToPermanentBlockWithFindingID ties escalation to its triggering observation while preserving the existing permanent-block transaction.
func (*Engine) RecoverActions ¶
func (*Engine) RefreshDOSExemptSets ¶
RefreshDOSExemptSets repopulates the dos_exempt_nets[6] interval sets with a new provider overlay in a single batched kernel transaction. If the kernel batch fails the previous set contents remain active and dosExemptProviderNets is not updated, preserving the last-known good state.
func (*Engine) RemoveAllowIP ¶
RemoveAllowIP removes an IP from the allowed set and state.
func (*Engine) RemoveAllowIPBySource ¶
RemoveAllowIPBySource removes only allow entries from a specific source. The IP is only removed from the nftables set if no other sources remain.
func (*Engine) RemoveAllowIPPort ¶
RemoveAllowIPPort removes a port-specific IP allow from state.
func (*Engine) RemoveBlockActionRows ¶
RemoveBlockActionRows removes, in one state write, the rows the named attempts wrote and reports how many went. Startup calls it with the ledger's settled attempts (failed, or retired after their expiry) once every unproven one is marked: nothing they applied can remain, so their rows only hold an address. An error reports no completed removal; the next start reads the saved state and tries again.
func (*Engine) ResolveAction ¶
func (e *Engine) ResolveAction(id, outcome, detail string) (FirewallAction, error)
ResolveAction records an outcome an operator established by hand for an action the kernel cannot prove. The committed cache follows the outcome, whether it came from the operator or from the kernel proving it after all.
func (*Engine) ResolvedInfraIPs ¶
ResolvedInfraIPs copies the last successful DNS answers for the requested infrastructure hosts. Callers choose which configured hosts they protect.
func (*Engine) RestoreBlockIfUnchanged ¶
func (e *Engine) RestoreBlockIfUnchanged(ip string, expected, prior *BlockedEntry) (resultErr error)
RestoreBlockIfUnchanged applies a saved lifetime only while the action's firewall snapshot still matches. The comparison and mutation share e.mu.
func (*Engine) RetireBlockActions ¶
func (e *Engine) RetireBlockActions(actions []BlockAction) ([]bool, error)
RetireBlockActions reports, for each expired unknown attempt or evicted import, whether nothing it may have applied can remain in the kernel (spec 5.4): its address has no element, or the element belongs to a verified row of another action. An import stays held while its own row proves the live element, even if an alias does too. Each clear row (same action ID or import name and address) goes in one state write, under the same engine lock as the snapshot, so no batch writes the address in between. An element still there otherwise keeps the attempt held, as after a forward wall step that made its expiry look past, and so does an address whose family the snapshot cannot read. An error answers nothing.
func (*Engine) RuleCounts ¶
func (e *Engine) RuleCounts() RuleCounts
RuleCounts returns the cardinality of every firewall rule category from the engine state file with expired temp bans pruned. Callers needing a live count (e.g. Prometheus gauges) must use this rather than the bbolt store, which holds only the migration-time snapshot.
func (*Engine) RulesetSnapshot ¶
RulesetSnapshot returns the live rules and the baseline captured immediately after the last successful Apply. The engine lock keeps our own concurrent reconfiguration from splitting the pair. An empty baseline means capture failed or this engine attached to an existing table without applying it.
func (*Engine) SetBlockActionsUnproven ¶
SetBlockActionsUnproven records, in memory only, why startup could not read the ledger. The rows its attempts wrote are restored as before, but none is ever chosen to make room for a new block: nothing proves which of them the kernel enforces. The next start reads the ledger again.
func (*Engine) SetBootClock ¶
SetBootClock gives the engine the boot identity and the time since boot, read together. Without it the engine keeps no lease checkpoint.
func (*Engine) SetConfig ¶
func (e *Engine) SetConfig(cfg *FirewallConfig)
SetConfig replaces the ruleset input the next Apply builds from. The engine was constructed with a value copy of the firewall block taken at daemon start, so every re-apply path (`csm firewall restart`, `apply-confirmed`, SIGHUP) rebuilt the old rules while reporting success and the operator's csm.yaml edit landed unprotected at the next restart. A nil cfg is ignored.
func (*Engine) SetDOSExemptProviderNets ¶
SetDOSExemptProviderNets stores the mail-provider IP ranges used by the dos_exempt_nets set. The daemon calls this before Apply() and on each provider refresh. Nil is valid when the daemon has no provider data yet; createSets() will produce an empty set in that case.
func (*Engine) SetDryRunEnabledFunc ¶
SetDryRunEnabledFunc installs the callback BlockIP uses to decide whether auto_response.dry_run should intercept an automatic block. Nil means live.
func (*Engine) SetDryRunRecorder ¶
func (*Engine) SetLeaseFloor ¶
SetLeaseFloor moves the lease clock forward to floor when it is behind it. The daemon passes the admission clock's saved time before the first Apply: a lower bound for a new boot, where the lease checkpoint proves no downtime (lease_checkpoint_linux.go).
func (*Engine) SetShutdownContext ¶
SetShutdownContext installs a context whose cancellation aborts any in-flight verdict callback. The daemon ties this to its stopCh so a graceful shutdown does not have to wait for an unresponsive panel callback to return.
func (*Engine) SetSoftAllowChecker ¶
SetSoftAllowChecker installs the callback the auto-block path consults to decide whether an IP belongs to a verified-bot range. Nil disables the verified-bot side of the soft-allow gate (operator allowed_ips is still honoured). The daemon wires this to threatintel so the firewall package stays free of that import.
func (*Engine) SetVerdictAsker ¶
func (e *Engine) SetVerdictAsker(fn func(ctx context.Context, ip, reason string) (string, string, string, error))
SetVerdictAsker installs the verdict callback the daemon constructs at startup. Nil disables the verdict callback (the gate skips entirely).
func (*Engine) StateSetAside ¶
StateSetAside names where an unreadable state file was kept when the engine first read its state, or "" when it read it.
func (*Engine) Status ¶
Status returns current firewall statistics.
Takes e.mu so the cached state can be read coherently. Before the cache existed loadStateFile was lock-free because every call did its own ReadFile + Unmarshal; now that loadStateFile mutates the shared cache + index, the lock is required.
func (*Engine) TempAllowIP ¶
TempAllowIP adds a temporary allow with expiry. Uses the same allowed set but tracks expiry in state - CleanExpiredAllows removes them periodically.
func (*Engine) UnblockIPForUndo ¶
func (e *Engine) UnblockIPForUndo(ip string) (before *BlockedEntry, resultErr error)
UnblockIPForUndo captures the removed block in the same critical section.
func (*Engine) UnblockSubnet ¶
UnblockSubnet removes a CIDR range from the blocked subnets set (IPv4 or IPv6).
func (*Engine) UndoAction ¶
func (e *Engine) UndoAction(req ActionRequest) (FirewallAction, error)
func (*Engine) UpdateCloudflareSet ¶
UpdateCloudflareSet flushes and repopulates the Cloudflare nftables sets.
func (*Engine) UpdateInfraResolved ¶
UpdateInfraResolved records the IP set last resolved for an infra hostname. Replaces any previous entry for that host so the resolver's per-tick refresh leaves no stale ghost IPs. Pass an empty ips slice to remove the host entirely (e.g. when DNS stopped resolving).
func (*Engine) ValidateSubnetBlock ¶
ValidateSubnetBlock runs the same safety and capability checks as BlockSubnet without changing persisted or kernel firewall state.
type ExistingBlock ¶
ExistingBlock names the block that already holds an address, so a member refused as an existing effect can link to it (spec 5.17): the admission action that wrote its row, or "eff_" and a digest of an operator or earlier row's identity, and when it ends (zero: permanent). The same row gets the same ID on every check and after a restart.
type FirewallAction ¶
type FirewallAction struct {
Ruleset *ActionRuleset `json:"ruleset,omitempty"`
KernelBefore []ActionSet `json:"kernel_before,omitempty"`
KernelAfter []ActionSet `json:"kernel_after,omitempty"`
Request ActionRequest `json:"request"`
// Members is the batch manifest. Without it the action is one member, its
// Request and Budget, as every record written before manifests is. With
// it, a batch of several members uses Request as its header.
Members []ActionMember `json:"members,omitempty"`
Before FirewallState `json:"before"`
After FirewallState `json:"after"`
Revision uint64 `json:"revision"`
Phase string `json:"phase"`
// Retired marks an unknown outcome recovery settled as expired_unproven:
// every effect had expired and the kernel held the retired state. The
// outcome stays unknown, but it no longer holds later mutations.
Retired bool `json:"retired,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Detail string `json:"detail,omitempty"`
Budget *ScanAdmission `json:"budget,omitempty"`
AuditVersion uint64 `json:"audit_version"`
AuditAck uint64 `json:"audit_ack"`
}
FirewallAction retains both complete states until the kernel outcome can be proved. Admission does not publish After as committed firewall state.
func (FirewallAction) ActionMembers ¶
func (a FirewallAction) ActionMembers() []ActionMember
ActionMembers returns the batch manifest, deriving the single member of an action that carries none. Callers must not modify the result.
func (FirewallAction) IsRequest ¶
func (a FirewallAction) IsRequest(req ActionRequest) bool
IsRequest reports whether a is the one-member action for exactly req, so a batch whose header happens to match is never replayed as one request.
func (FirewallAction) SameManifest ¶
func (a FirewallAction) SameManifest(b FirewallAction) bool
SameManifest reports whether b is the same batch as a: the same header, the same members in the same order and the same effects. Admission data is not compared: a replay in a later window keeps the charge taken first.
type FirewallConfig ¶
type FirewallConfig struct {
Enabled bool `yaml:"enabled"`
// Open ports (IPv4)
TCPIn []int `yaml:"tcp_in"`
TCPOut []int `yaml:"tcp_out"`
UDPIn []int `yaml:"udp_in"`
UDPOut []int `yaml:"udp_out"`
// IPv6 - enable dual-stack filtering
IPv6 bool `yaml:"ipv6"`
TCP6In []int `yaml:"tcp6_in"` // if empty, uses tcp_in
TCP6Out []int `yaml:"tcp6_out"` // if empty, uses tcp_out
UDP6In []int `yaml:"udp6_in"` // if empty, uses udp_in
UDP6Out []int `yaml:"udp6_out"` // if empty, uses udp_out
// Ports restricted to infra IPs only
RestrictedTCP []int `yaml:"restricted_tcp"`
// RequiredTCPOut declares outbound TCP ports a service on this host needs.
// It is checked, never merged: validation warns when an effective outbound
// family policy omits one, so an integration can state its
// requirement in its own conf.d fragment and have `csm doctor` verify it
// against the effective policy.
RequiredTCPOut []int `yaml:"required_tcp_out"`
// TCPOutAllow permits outbound TCP to a specific destination on a port
// range. tcp_out is []int and cannot express a range, so a host acting as
// a client for a range-using protocol (passive FTP data channels) has no
// way to state its need without opening every high port to the internet.
// Empty means no destination-scoped egress, which is the prior behaviour.
TCPOutAllow []OutAllowRule `yaml:"tcp_out_allow"`
// ProtocolAllow accepts an IP protocol that has no ports, such as VRRP
// between the members of a high-availability pair, to and from listed
// peers only. tcp/udp/icmp have their own keys and are not accepted here.
ProtocolAllow []ProtocolAllowRule `yaml:"protocol_allow"`
// Passive FTP range
PassiveFTPStart int `yaml:"passive_ftp_start"`
PassiveFTPEnd int `yaml:"passive_ftp_end"`
// Infra IPs (CIDR notation)
InfraIPs []string `yaml:"infra_ips"`
// Rate limiting (per-source nftables meters). SYN/conn-rate/UDP are
// dual-stack (IPv6 keyed per /64); ConnLimit is IPv4-only (per-source
// ct count uses nf_conncount, whose GC has a kernel UAF on the el8 kernel).
ConnRateLimit int `yaml:"conn_rate_limit"` // new connections per minute per source (IPv6 per /64)
SYNFloodProtection bool `yaml:"syn_flood_protection"`
ConnLimit int `yaml:"conn_limit"` // max concurrent connections per IPv4 source, IPv4 only (0 = disabled)
// Per-port flood protection - per-source rate limit per port and IP family.
PortFlood []PortFloodRule `yaml:"port_flood"`
// UDP flood protection - per-source rate limit on UDP packets (IPv6 per /64)
UDPFlood bool `yaml:"udp_flood"`
UDPFloodRate int `yaml:"udp_flood_rate"` // packets per second
UDPFloodBurst int `yaml:"udp_flood_burst"` // burst allowance
// Country blocking
CountryBlock []string `yaml:"country_block"` // ISO country codes
CountryDBPath string `yaml:"country_db_path"`
// Ports to drop silently without logging (reduces log noise from scanners)
DropNoLog []int `yaml:"drop_nolog"`
// Max blocked IPs (prevents memory exhaustion, 0 = unlimited)
DenyIPLimit int `yaml:"deny_ip_limit"`
DenyTempIPLimit int `yaml:"deny_temp_ip_limit"`
// Outbound SMTP restriction - block outgoing mail except from allowed users
SMTPBlock bool `yaml:"smtp_block"`
SMTPAllowUsers []string `yaml:"smtp_allow_users"` // usernames allowed to send
SMTPPorts []int `yaml:"smtp_ports"`
// Dynamic DNS - resolve hostnames to IPs, update allowed set periodically
DynDNSHosts []string `yaml:"dyndns_hosts"`
// Logging
LogDropped bool `yaml:"log_dropped"`
LogRate int `yaml:"log_rate"` // log entries per minute
// DoS-exempt ranges - source CIDRs excluded from connection/mail-port
// meters and auto-subnet escalation. Intended for shared-source ranges
// such as carrier CGNAT blocks and well-known mail-provider egress.
DOSExemptRanges []string `yaml:"dos_exempt_ranges"`
DOSExemptKnownMailProviders *bool `yaml:"dos_exempt_known_mail_providers"`
}
FirewallConfig defines the nftables firewall configuration.
func DefaultConfig ¶
func DefaultConfig() *FirewallConfig
DefaultConfig returns a sensible default firewall configuration matching a typical cPanel server.
func (*FirewallConfig) ExemptKnownMailProviders ¶
func (c *FirewallConfig) ExemptKnownMailProviders() bool
ExemptKnownMailProviders reports whether bundled mail-provider ranges are included in the DoS-exempt set. Defaults to true when unset.
type FirewallState ¶
type FirewallState struct {
Blocked []BlockedEntry `json:"blocked"`
BlockedNet []SubnetEntry `json:"blocked_nets"`
Allowed []AllowedEntry `json:"allowed"`
PortAllowed []PortAllowEntry `json:"port_allowed"`
}
FirewallState is persisted to disk for restore on restart.
func LoadState ¶
func LoadState(statePath string) (*FirewallState, error)
LoadState reads the authoritative firewall state file directly without requiring a running engine. A missing state file is a valid fresh-host state and returns an empty FirewallState. Only enforced blocks are returned: an unverified row is held for recovery and never restored to the kernel.
func RetiredActionState ¶
func RetiredActionState(a FirewallAction, at time.Time) (FirewallState, bool)
RetiredActionState is the committed state an unknown block action retires to as expired_unproven (spec 5.4): every member target is timed and expired, and every row its Before and After states disagree on is a verified timed block that has expired at at. Committed state keeps the rows both states agree on. Any other action reports false and waits for an operator's Resolve: one that is not a block, changes anything but blocked rows, or has an effect that is permanent, unverified or still live.
type Lifecycle ¶
type Lifecycle struct {
Store ActionStore
Audit func(FirewallAction) error
// RetirementClockTrusted runs with the engine lock held and must not
// call back into the engine. Nil or false refuses retirement; an error
// also reaches recovery's caller, even if the returned bool is true.
RetirementClockTrusted func() (bool, error)
// contains filtered or unexported fields
}
Lifecycle is owned by one engine. Its lock serializes execution with recovery and undo, while the store protects admission against competing requests.
func (*Lifecycle) DeliverAudit ¶
func (*Lifecycle) Execute ¶
func (l *Lifecycle) Execute(plan FirewallAction, kernel ActionKernel) (FirewallAction, error)
Execute admits once and executes only a fresh request. Repeated IDs inspect the original evidence; they never renew the plan or replay a kernel batch.
func (*Lifecycle) Recover ¶
func (l *Lifecycle) Recover(kernel ActionKernel) error
Recover proves incomplete outcomes without executing host mutations. A proven rejected action is recovered successfully, though its result is failed.
func (*Lifecycle) Resolve ¶
func (l *Lifecycle) Resolve(id, outcome, detail string, kernel ActionKernel) (FirewallAction, error)
Resolve records the outcome an operator established by hand for an action the kernel could not prove or retire. Kernel evidence still wins: a proven outcome is recorded instead of the asserted one, and a settled action is never rewritten. Detail names who decided, and reaches the audit trail.
func (*Lifecycle) Undo ¶
func (l *Lifecycle) Undo(req ActionRequest, kernel ActionKernel) (FirewallAction, error)
Undo admits an inverse state transition, never a command string. A whole action needs its complete result current; a member needs its own row current.
type LiveBlockedSnapshot ¶
type LiveBlockedSnapshot struct {
V4 map[string]struct{}
V6 map[string]struct{}
HasV4 bool
HasV6 bool
}
LiveBlockedSnapshot is a point-in-time membership view of the kernel's blocked IP sets, taken by dumping each configured family set once.
HasV4 / HasV6 record which families the snapshot actually covers. A set can be absent -- firewall.ipv6 disabled leaves the v6 set nil -- and an uncovered family must never read as "not blocked", or a reconcile pass would prune every tracked block of that family on its first cycle.
func (LiveBlockedSnapshot) Contains ¶
func (s LiveBlockedSnapshot) Contains(ip string) (blocked, known bool)
Contains reports whether ip is in the live blocked set. known is false when the snapshot does not cover the IP's address family, in which case the caller must keep its cached answer rather than treat ip as unblocked.
Family selection mirrors the per-IP path: an IPv4-mapped IPv6 address resolves against the v4 set, because that is the set the block path keys it into. A malformed IP is reported as definitively absent, matching IsBlockedLive.
type OutAllowRule ¶
type OutAllowRule struct {
Dst string `yaml:"dst"`
PortStart int `yaml:"port_start"`
PortEnd int `yaml:"port_end"`
}
OutAllowRule permits outbound TCP to one destination on a port range. Dst is an IP or CIDR; 0.0.0.0/0 and ::/0 mean any destination in that family.
type PortAllowEntry ¶
type PortAllowEntry struct {
IP string `json:"ip"`
Port int `json:"port"`
Proto string `json:"proto"` // "tcp" or "udp"
Reason string `json:"reason"`
Source string `json:"source,omitempty"`
}
PortAllowEntry represents a port-specific IP allow (e.g. tcp|in|d=PORT|s=IP).
type PortFloodRule ¶
type PortFloodRule struct {
Port int `yaml:"port"`
Proto string `yaml:"proto"` // "tcp" or "udp"
Hits int `yaml:"hits"` // max new connections
Seconds int `yaml:"seconds"` // time window in seconds
}
PortFloodRule defines per-port connection rate limiting.
type ProtocolAllowRule ¶
ProtocolAllowRule accepts one portless IP protocol from and to its peers. Each peer is an IP or CIDR, parsed as a tcp_out_allow destination.
type RuleCounts ¶
RuleCounts holds firewall rule cardinalities sourced from the engine state file, which is the authoritative store. The parallel bbolt fw:* buckets are written only during migration, so anything counting live rules must read the engine, not the store. Expired temp bans are excluded.
func (RuleCounts) Total ¶
func (c RuleCounts) Total() int
Total returns the sum across all rule categories.
type ScanAdmission ¶
ScanAdmission uses the existing scan hourly policy. A nil admission is exempt; callers must not charge non-scan sources to the scan budget.
type StateStore ¶
type StateStore interface {
ReadFirewallState() (FirewallState, uint64, error)
ReplaceFirewallState(expectedRevision uint64, state FirewallState) (uint64, error)
}
StateStore is the firewall-owned, lossless persistence contract. It does not activate the engine's storage cutover. No method filters, normalizes, merges, infers provenance, renews expiry, or touches the kernel.
Read returns detached state and its revision, or only an error. Replace atomically replaces all four collections if the expected revision matches. Revision zero initializes a previously uninitialized snapshot. A successful replacement returns the new revision; an error returns zero. Callers must not mutate input slices during a call. Times retain their persisted instant and offset, not process-local monotonic clock or location metadata.
Action admission must later share the private store transaction that replaces state; callers must not compose this operation with separate action writes.
Source Files
¶
- action_retire.go
- action_store.go
- actionlog.go
- allow_mutation_linux.go
- audit.go
- block_batch.go
- block_batch_linux.go
- block_retire_linux.go
- block_rows_linux.go
- block_timeout.go
- block_undo_linux.go
- blocked_snapshot.go
- cloudflare.go
- cloudflare_fetch.go
- config.go
- country_db.go
- dyndns.go
- effectiveset.go
- engine.go
- engine_block_batch_linux.go
- engine_lifecycle_linux.go
- geoip.go
- infra_sources_linux.go
- intervals_linux.go
- ip_helpers.go
- lease_checkpoint_linux.go
- lease_clock_linux.go
- lifecycle.go
- lifecycle_ruleset_linux.go
- lifecycle_socket_linux.go
- mutation_state_linux.go
- outcome.go
- port_flood.go
- protected_error_linux.go
- provenance.go
- rule_counts.go
- rule_counts_linux.go
- ruleset_snapshot_linux.go
- smtp_allowlist.go
- state.go
- state_store.go
- support_linux.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package rollback implements the firewall settings tentative-apply workflow: a save with a deadline that auto-reverts unless the operator confirms before the timer expires.
|
Package rollback implements the firewall settings tentative-apply workflow: a save with a deadline that auto-reverts unless the operator confirms before the timer expires. |