switchop

package
v1.230.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 14, 2026 License: MPL-2.0 Imports: 18 Imported by: 0

Documentation

Overview

============================================================================= NFTBan v1.164 - Empty-table classifier (switchop ghost cleanup) ============================================================================= SPDX-License-Identifier: MPL-2.0 SPDX-FileCopyrightText: Copyright (c) 2024-2026 Antonios Voulvoulis <contact@nftban.com> meta:name="installer-switchop-classify" meta:type="lib" meta:owner="Antonios Voulvoulis <contact@nftban.com>" meta:created_date="2026-06-08" meta:description="Classify-empty primitive for ghost-table cleanup: count non-structural rule lines so populated/operator tables are preserved" meta:inventory.files="internal/installer/switchop/classify.go" meta:inventory.binaries="" meta:inventory.env_vars="" meta:inventory.config_files="" meta:inventory.systemd_units="" meta:inventory.network="" meta:inventory.privileges="root" =============================================================================

v1.164 switchop classify-empty (PR-A + PR-B)

Ghost-table cleanup historically deleted certain nftables tables unconditionally. That is correct for tables that an iptables-nft compat shim creates purely as a side-effect of iptables being installed (ip filter/nat/mangle/security, inet firewalld) — those are skeletons by construction and never hold operator rules.

It is NOT correct for tables that can legitimately hold operator or kernel content even when an empty skeleton of the same name also occurs in the iptables-nft world. Two cases motivated this primitive:

ip raw / ip6 raw  — an empty `raw` table is the iptables-nft skeleton, but
                    a populated `raw` table holds real NOTRACK / conntrack
                    exemptions an operator (or the kernel default path) put
                    there. Deleting it silently drops those rules.
inet filter       — already classify-then-act'd by the CVE-2025-NFTBAN-001
                    guard (detect/cve_inet_filter.go), which PRESERVES a
                    populated operator-owned table. The unconditional
                    delete in CleanGhostTables could then destroy exactly
                    what the detect phase had deliberately kept.

TableIsEmpty is the shared primitive that lets CleanGhostTables remove only proven-empty skeletons and preserve populated tables. It reuses the same "is this a rule?" structural-line heuristic as the CVE guard's inetFilterRuleCount (detect/cve_inet_filter.go) so the two paths classify identically; this is the generalized form (it also treats set/elements/map structural blocks as non-rules so set-only tables read as empty-of-rules).

=============================================================================

============================================================================= NFTBan v1.230.0 Gate 6R — POST-UPDATE CONVERGENCE CONTRACT ============================================================================= SPDX-License-Identifier: MPL-2.0 SPDX-FileCopyrightText: Copyright (c) 2024-2026 Antonios Voulvoulis <contact@nftban.com> meta:name="installer-switchop-convergence" meta:type="lib" meta:owner="Antonios Voulvoulis <contact@nftban.com>" meta:created_date="2026-09-13" meta:description="Verifies END-TO-END nftables convergence after the switch phase, independently of the rebuild's own success claim. Establishes projection validity, an observed advance of the effective convergence generation, and presence of the required kernel tables; produces the CONVERGENCE_VERIFIED verdict that gates COMMITTED." meta:inventory.files="internal/installer/switchop/convergence.go" meta:inventory.binaries="" meta:inventory.env_vars="" meta:inventory.config_files="/etc/nftban/generated/nftban-boot.nft" meta:inventory.systemd_units="" meta:inventory.network="" meta:inventory.privileges="root" =============================================================================

============================================================================= NFTBan v1.75.1 - Installer nftables Service Enable ============================================================================= SPDX-License-Identifier: MPL-2.0 SPDX-FileCopyrightText: Copyright (c) 2024-2026 Antonios Voulvoulis <contact@nftban.com> meta:name="installer-switchop-enable" meta:type="lib" meta:owner="Antonios Voulvoulis <contact@nftban.com>" meta:created_date="2026-04-04" meta:description="Enable and start nftables service with xt-compat pre-check" meta:inventory.files="internal/installer/switchop/enable.go" meta:inventory.binaries="" meta:inventory.env_vars="" meta:inventory.config_files="" meta:inventory.systemd_units="nftables.service" meta:inventory.network="" meta:inventory.privileges="root" =============================================================================

============================================================================= NFTBan v1.73 - Installer Ghost Table Cleanup ============================================================================= SPDX-License-Identifier: MPL-2.0 SPDX-FileCopyrightText: Copyright (c) 2024-2026 Antonios Voulvoulis <contact@nftban.com> meta:name="installer-switchop-ghost" meta:type="lib" meta:owner="Antonios Voulvoulis <contact@nftban.com>" meta:created_date="2026-04-04" meta:description="Remove ghost nftables tables from conflicting firewalls" meta:inventory.files="internal/installer/switchop/ghost.go" meta:inventory.binaries="" meta:inventory.env_vars="" meta:inventory.config_files="" meta:inventory.systemd_units="" meta:inventory.network="" meta:inventory.privileges="root" =============================================================================

============================================================================= NFTBan v1.73 - Installer Firewall Rebuild ============================================================================= SPDX-License-Identifier: MPL-2.0 SPDX-FileCopyrightText: Copyright (c) 2024-2026 Antonios Voulvoulis <contact@nftban.com> meta:name="installer-switchop-rebuild" meta:type="lib" meta:owner="Antonios Voulvoulis <contact@nftban.com>" meta:created_date="2026-04-04" meta:description="Run nftban firewall rebuild as a MANDATORY convergence step. No outer deadline: the step scales with host firewall state (lab2 ~35s vs srv3 ~453s) and an arbitrary constant killed legitimate convergences. Interruption is its own verdict class and is FATAL to the install — a killed rebuild is not DEGRADED, it is CONVERGENCE DID NOT COMPLETE." meta:inventory.files="internal/installer/switchop/rebuild.go" meta:inventory.binaries="" meta:inventory.env_vars="" meta:inventory.config_files="" meta:inventory.systemd_units="" meta:inventory.network="" meta:inventory.privileges="root" =============================================================================

============================================================================= NFTBan v1.229.12 - Rebuild Result Contract (P12-A01 / P12-A01b) ============================================================================= SPDX-License-Identifier: MPL-2.0 SPDX-FileCopyrightText: Copyright (c) 2024-2026 Antonios Voulvoulis <contact@nftban.com> meta:name="rebuild-result" meta:type="lib" meta:owner="Antonios Voulvoulis <contact@nftban.com>" meta:description="Typed shell->Go rebuild result: the shell reports a RebuildDisposition; Go owns installer continuation. Retires rc as semantic authority." meta:inventory.privileges="none"

⛔ WHY THIS EXISTS The shell rebuild compressed a rich transaction outcome into rc 0/1/2, and Go assigned installer meaning to it. `return 1` meant ALL of: expected-deferred, template missing, publish failed, apply failed, generation-commit failed, and any bash programming error. That single overload produced two opposite production defects:

P12-A01   an EXPECTED pre-daemon degradation was escalated to a fatal rollback
P12-A01b  a FAILED GENERATION COMMIT was accepted as "DEGRADED" and the install continued

The authority split is now explicit:

SHELL  RebuildDisposition   — what happened to this rebuild transaction
GO     InstallerContinuation — whether installation may proceed

⛔ rc IS PROCESS EVIDENCE ONLY. It is NEVER sufficient semantic evidence on this path.

============================================================================= NFTBan v1.229.13 - Installer Boot Projection Render (P12-FPA port, Lane 3C) ============================================================================= SPDX-License-Identifier: MPL-2.0 SPDX-FileCopyrightText: Copyright (c) 2024-2026 Antonios Voulvoulis <contact@nftban.com> meta:name="installer-switchop-renderboot" meta:type="lib" meta:owner="Antonios Voulvoulis <contact@nftban.com>" meta:created_date="2026-08-28" meta:description="Generate the persistent boot projection BEFORE the managed distro include is pointed at it. Delegates to the shell render authority via the CLI, the same canonical installer->shell interface switchop.Rebuild already uses." meta:inventory.files="internal/installer/switchop/renderboot.go" meta:inventory.binaries="" meta:inventory.env_vars="" meta:inventory.config_files="" meta:inventory.systemd_units="" meta:inventory.network="" meta:inventory.privileges="root" =============================================================================

============================================================================= NFTBan v1.73 - Installer SSH Port Live Set Guard ============================================================================= SPDX-License-Identifier: MPL-2.0 SPDX-FileCopyrightText: Copyright (c) 2024-2026 Antonios Voulvoulis <contact@nftban.com> meta:name="installer-switchop-sshguard" meta:type="lib" meta:owner="Antonios Voulvoulis <contact@nftban.com>" meta:created_date="2026-04-04" meta:description="Ensure SSH port is in live nft sets before rebuild" meta:inventory.files="internal/installer/switchop/sshguard.go" meta:inventory.binaries="" meta:inventory.env_vars="" meta:inventory.config_files="" meta:inventory.systemd_units="" meta:inventory.network="" meta:inventory.privileges="root" =============================================================================

============================================================================= NFTBan v1.75.1 - Installer Takeover Operations ============================================================================= SPDX-License-Identifier: MPL-2.0 SPDX-FileCopyrightText: Copyright (c) 2024-2026 Antonios Voulvoulis <contact@nftban.com> meta:name="installer-switchop-takeover" meta:type="lib" meta:owner="Antonios Voulvoulis <contact@nftban.com>" meta:created_date="2026-04-04" meta:description="Disable conflicting firewalls during takeover with CSF panel disarm" meta:inventory.files="internal/installer/switchop/takeover.go" meta:inventory.binaries="" meta:inventory.env_vars="" meta:inventory.config_files="" meta:inventory.systemd_units="" meta:inventory.network="" meta:inventory.privileges="root" =============================================================================

Index

Constants

View Source
const (
	// CronManifestSchemaVersion is the on-disk schema version of the
	// manifest. Reader rejects manifests whose schema_version field
	// does not match this constant exactly. Bumping this constant is
	// a contract event (treat as Amendment-1 §31 A.4 evolution).
	CronManifestSchemaVersion = "1.0.0"

	// CronManifestDir is the on-disk directory the manifest writer
	// stores backups + manifest.json under. Hardcoded by §42.2 lock.
	CronManifestDir = "/var/lib/nftban/state/csf-cron-backup"

	// CronManifestFile is the absolute path of the manifest JSON
	// file (CronManifestDir + "/manifest.json").
	CronManifestFile = "/var/lib/nftban/state/csf-cron-backup/manifest.json"

	// CronCSFSrcPath / CronLFDSrcPath are the canonical /etc/cron.d
	// source paths the writer backs up and the reader restores to.
	// Hardcoded by §42.2 lock — only these two cron files are
	// backed up; never any other /etc/cron.d/* entry.
	CronCSFSrcPath = "/etc/cron.d/csf-cron"
	CronLFDSrcPath = "/etc/cron.d/lfd-cron"
)
View Source
const BootProjectionPath = "/etc/nftban/generated/nftban-boot.nft"

BootProjectionPath is the boot projection the include authority points at. ⛔ NOT /etc/nftban/nftables.conf — that is the retired legacy include.

View Source
const ConvergenceGenerationPath = "/run/nftban/convergence-generation"

ConvergenceGenerationPath is the effective convergence generation counter. Canonical value of NFTBAN_PLAN_GENERATION_FILE (lib/module_authority.sh). tmpfs, so it resets at boot; only the BEFORE/AFTER DELTA within one run is read here, never the absolute value.

View Source
const RebuildResultSchemaSupported = "1"

RebuildResultSchemaSupported is the producer schema this consumer knows how to interpret. ⛔ A newer schema is NOT compatible until PROVEN compatible — same rule the shell applies to the validator schema. Unknown version => abort, never "assume forward compatible".

Variables

View Source
var (
	// ErrCronManifestSchemaMismatch is returned by the reader when
	// manifest.json parses but its schema_version does not match
	// CronManifestSchemaVersion exactly.
	ErrCronManifestSchemaMismatch = errors.New("cron manifest: schema_version mismatch")

	// ErrCronManifestSHA256Mismatch is returned by the reader when
	// a manifest entry's recorded sha256 does not match the actual
	// sha256 of the on-disk backup file. Indicates corruption or
	// tampering — restore MUST refuse this entry.
	ErrCronManifestSHA256Mismatch = errors.New("cron manifest: sha256 mismatch — backup file does not match manifest entry")

	// ErrCronManifestUnknownEntry is returned when the manifest lists
	// a Path that is not one of the two §42.2-locked cron source
	// paths (/etc/cron.d/csf-cron or /etc/cron.d/lfd-cron). Defensive
	// guard; readers MUST refuse unknown entries.
	ErrCronManifestUnknownEntry = errors.New("cron manifest: entry path is not in the §42.2 locked set {csf-cron, lfd-cron}")

	// ErrCronManifestParseFailed is returned by the reader when
	// manifest.json cannot be parsed as JSON. Distinct from a
	// missing manifest (ReadCronBackupManifest returns ok=false in
	// that case, no error).
	ErrCronManifestParseFailed = errors.New("cron manifest: failed to parse manifest.json")
)
View Source
var ErrRebuildNotExecuted = errors.New("rebuild produced no result contract and no execution witness: execution was NOT established, so this is not a rebuild failure; the effective kernel generation is NOT attributable to this update transaction — convergence is DEFERRED, not complete")

ErrRebuildNotExecuted — no result contract AND no execution witness.

The root-cause guard for a missing record. "No record" has two root causes with OPPOSITE safety meanings:

never started    -> the firewall was NOT touched  -> deferred, retry
started, aborted -> the firewall MAY be touched   -> FAILED_REBUILD, fail closed

Which one occurred is established from the execution witness, never assumed.

View Source
var ErrRebuildRefusedBusy = errors.New("rebuild REFUSED_BUSY: every attempt within the installer deadline was refused by the convergence lock; no rebuild executed, the firewall was not modified, and the effective kernel generation is NOT attributable to this update transaction — convergence is DEFERRED, not complete")

ErrRebuildRefusedBusy — every attempt inside the caller's deadline was REFUSED.

⛔ THIS IS NOT A FAILURE AND MUST NEVER BE MAPPED TO FAILED_REBUILD. No rebuild executed, so nothing failed; the convergence is simply still owed and the install is DEFERRED pending a retry.

Functions

func AssertSSHInLiveSet

func AssertSSHInLiveSet(exec executor.Executor, sshPort int, log *logging.Logger)

AssertSSHInLiveSet verifies the SSH port exists in the live nft tcp_ports_in sets for both ip and ip6. If missing, adds it. Call after EnableNftables (nftban tables must exist) and before/after rebuild.

func CleanGhostTables

func CleanGhostTables(exec executor.Executor, log *logging.Logger)

CleanGhostTables removes known ghost nftables tables.

Three classes, by removability:

  • ghostTables (filter/nat/mangle/security/firewalld): unconditional delete — these are always compat-shim skeletons.
  • rawGhostTables (ip raw / ip6 raw): classify-empty (PR-A) — delete only if proven empty; preserve populated operator/kernel content.
  • inet filter: classify-empty + override (PR-B) — delete only if proven empty; preserve populated operator-owned table unless the operator explicitly sets NFTBAN_ALLOW_REMOVE_INET_FILTER=1.

Ignores errors for tables that don't exist.

func ComputeCronBackupSHA256 added in v1.100.4

func ComputeCronBackupSHA256(content []byte) string

ComputeCronBackupSHA256 returns the lowercase-hex sha256 of the given content. Used by the writer to record the manifest entry and by the reader to verify the on-disk backup file's integrity before A.4 restoration.

func DisableConflicts

func DisableConflicts(exec executor.Executor, conflicts []detect.Conflict, panel detect.PanelType, log *logging.Logger) error

DisableConflicts stops, disables, and masks all conflicting firewalls. For CSF conflicts on DirectAdmin servers, also disarms CustomBuild so that `./build update` does not re-enable CSF.

func EnableNftables

func EnableNftables(exec executor.Executor, distro *detect.DistroInfo, log *logging.Logger) error

EnableNftables enables and starts the nftables service, then verifies. Runs cleanXtCompat() first to remove stale xt target rules that would prevent nftables from starting (common on CSF/cPanel servers).

func InjectEmergencySSH added in v1.74.0

func InjectEmergencySSH(exec executor.Executor, sshPort int, log *logging.Logger) error

InjectEmergencySSH creates a minimal inet table that accepts the SSH port. This table acts as a last-resort safety net during install transitions. It MUST be removed only after nftban rules are proven in the kernel. Idempotent: deletes any existing emergency table before creating.

Priority -1: evaluated before nftban chains (priority 0). Policy accept: fail-open — safety net, not security boundary.

func ReadConvergenceGeneration added in v1.230.0

func ReadConvergenceGeneration(exec executor.Executor) int64

ReadConvergenceGeneration returns the effective convergence generation, or -1 when it cannot be read.

⛔ -1 IS "NOT OBSERVED", NOT ZERO. The shell treats an absent file as generation 0 for its own comparisons, but here the two must stay apart: an unreadable counter is an absence of evidence and must never be differenced against a real value to manufacture a "did not advance" conclusion.

ENOENT != ABSENCE. ABSENT_QUERY != RESOURCE_ABSENT.

func RemoveEmergencySSH added in v1.74.0

func RemoveEmergencySSH(exec executor.Executor, log *logging.Logger)

RemoveEmergencySSH removes the emergency SSH table. Call only after nftban rules are proven in the kernel with SSH port present. No-op if table doesn't exist.

func RenderBoot added in v1.229.13

func RenderBoot(exec executor.Executor, log *logging.Logger) error

RenderBoot runs "nftban firewall render-boot", which renders the canonical package-owned schema, validates it with nft -c, and publishes it atomically to the boot projection path. It does NOT load a ruleset.

WHY THIS EXISTS SEPARATELY FROM Rebuild: render.IntegrateSystemConf must not point the distro include at an artifact that does not exist yet, and it runs in phasePrepare — before nftables is enabled and before the SSH-safety invariants of `rebuild` hold. Rebuild both renders AND loads, so it cannot be moved that early. This renders only.

⛔ IT IS NOT A RENDERING AUTHORITY. It shells out to the CLI exactly as switchop.Rebuild does, which is the established installer->shell interface in this codebase; the render semantics stay in one place, in the shell.

Failure is FATAL to the caller by contract: without a published projection the include must not be repointed, and continuing would produce a host whose boot include names a file that was never created.

func SetRebuildResultBaseDirForTest added in v1.229.12

func SetRebuildResultBaseDirForTest(dir string) func()

SetRebuildResultBaseDirForTest overrides the publish directory and returns a restore func. The variable above exists to be overridden — CI has no writable /run/nftban, so a hardcoded path fails there while passing locally as root — but it is package-private, which leaves out-of-package callers (cmd/nftban-installer end-to-end tests that drive runInstall) unable to use it. This is a TEST AFFORDANCE ONLY: it changes the directory, never the protocol. Uniqueness, atomic rename and operation_id binding are unchanged.

func SetRefusalBackoffForTest added in v1.230.0

func SetRefusalBackoffForTest(base, ceiling time.Duration) func()

SetRefusalBackoffForTest shrinks the retry backoff and returns a restore func. ⛔ TEST AFFORDANCE ONLY. It changes the DURATION, never the bound: the attempt cap and the caller's deadline remain the two things that stop the loop.

func TableIsEmpty added in v1.164.0

func TableIsEmpty(exec executor.Executor, family, table string) bool

func VerifyCronBackupEntry added in v1.100.4

func VerifyCronBackupEntry(exec executor.Executor, entry CronManifestEntry) ([]byte, error)

VerifyCronBackupEntry reads the on-disk backup for entry and compares its sha256 to the manifest record. Returns ErrCronManifestSHA256Mismatch on mismatch. Reads via the executor abstraction.

Types

type ConvergenceInputs added in v1.230.0

type ConvergenceInputs struct {
	// ProjectionGenerated is true ONLY when the authoritative render succeeded IN THIS
	// RUN (phaseData.bootProjectionReady). ⛔ Never os.Stat, never mtime, never size.
	ProjectionGenerated bool
	// ApplyClaimedComplete is the rebuild's OWN claim (disposition COMPLETE with a
	// committed transaction). ⛔ Necessary, never sufficient — T3 exists precisely
	// because this claim was true while the kernel was unchanged.
	ApplyClaimedComplete bool
	// ApplyDeferred is true when the rebuild reported DEFERRED_RUNTIME.
	//
	// ⛔ AN EXPECTED DEFERRAL, NOT A SUCCESS SIGNAL. It suppresses the T6 "nothing
	// converged" failure (an unadvanced generation is what a deferral MEANS) and
	// nothing more — it never satisfies a leg and never produces ConvergenceVerified.
	ApplyDeferred bool
	// GenerationBefore is the counter read BEFORE the rebuild ran, from
	// ReadConvergenceGeneration. -1 means it could not be read.
	GenerationBefore int64
}

ConvergenceInputs are the facts the caller already holds. They are PASSED, never re-derived here — the same discipline --install-context follows.

type ConvergenceResult added in v1.230.0

type ConvergenceResult struct {
	Verdict ConvergenceVerdict
	// Legs is an ordered, human-readable evidence list — one line per leg, each stating
	// what was OBSERVED, not what was assumed.
	Legs []string
	// Detail names the first leg that decided a non-VERIFIED verdict.
	Detail string
}

ConvergenceResult carries the verdict and the per-leg evidence behind it.

func VerifyPostUpdateConvergence added in v1.230.0

func VerifyPostUpdateConvergence(exec executor.Executor, log *logging.Logger, in ConvergenceInputs) ConvergenceResult

VerifyPostUpdateConvergence establishes the post-update convergence contract:

EXPECTED PACKAGE
  -> expected generated projection      (ProjectionGenerated, established this run)
  -> nft -c validation PASS             (probed here, on the projection's CONTENT)
  -> actual apply/rebuild result PASS   (ApplyClaimedComplete, the claim)
  -> effective kernel generation verified (probed here, independently of the claim)
  -> required kernel objects verified   (probed here)
  -> only then COMMITTED

⛔ THE PACKAGE LEG IS DELIBERATELY NOT EVALUATED HERE and is reported as such. It is owned by payload verification, and the only evidence available at this point would be a package version string — which this gate forbids as a convergence input. Claiming it would be exactly the collapse being removed.

CLAIM ONLY WHAT THE SYSTEM KNOWS.

It is READ-ONLY. It probes; it never mutates, never re-applies, never "fixes".

type ConvergenceVerdict added in v1.230.0

type ConvergenceVerdict string

ConvergenceVerdict is the installer's post-update convergence conclusion.

const (
	// ConvergenceVerified — every evaluated leg passed. The only value that may
	// support COMMITTED.
	ConvergenceVerified ConvergenceVerdict = "VERIFIED"
	// ConvergenceNotConverged — a leg POSITIVELY FAILED. T3/T4/T6.
	ConvergenceNotConverged ConvergenceVerdict = "NOT_CONVERGED"
	// ConvergenceDeferred — the rebuild deliberately deferred its module projection
	// (DEFERRED_RUNTIME), so the generation was intentionally not advanced. Convergence
	// debt is outstanding; this is NOT a verified convergence.
	//
	// ⛔ THIS VERDICT IS THE RULING MADE MACHINE-READABLE. It exists so a deferral can
	// be carried forward as an INTERMEDIATE DISPOSITION without ever being spelled
	// VERIFIED. Deleting it — or folding it into ConvergenceVerified because "the run
	// was clean otherwise" — is exactly the simplification the owner ruling forbids.
	ConvergenceDeferred ConvergenceVerdict = "DEFERRED"
	// ConvergenceUnverified — a leg could not be OBSERVED at all. ⛔ Not a pass and not
	// a failure: an unobservable leg is an absence of evidence, and manufacturing either
	// verdict from it is the error this whole gate exists to remove.
	ConvergenceUnverified ConvergenceVerdict = "UNVERIFIED"
)

type CronManifest added in v1.100.4

type CronManifest struct {
	SchemaVersion string              `json:"schema_version"`
	CapturedAt    time.Time           `json:"captured_at"`
	Files         []CronManifestEntry `json:"files"`
}

CronManifest is the manifest.json on-disk shape.

func ReadCronBackupManifest added in v1.100.4

func ReadCronBackupManifest(exec executor.Executor, log *logging.Logger) (CronManifest, bool, error)

ReadCronBackupManifest returns the parsed manifest if present and schema-valid. Three return shapes:

  • Manifest absent (no manifest.json at CronManifestFile): returns (zero, false, nil). Caller (A.4 step) treats this as the graceful soft-skip case for pre-PR-26 hosts.
  • Manifest present but corrupt (parse failure or schema mismatch): returns (zero, true, ErrCronManifestParseFailed or ErrCronManifestSchemaMismatch). Caller refuses A.4.
  • Manifest present and parseable: returns (manifest, true, nil). Caller still verifies per-entry sha256 against on-disk backups before restoring.

Per-entry integrity is the caller's responsibility (use ComputeCronBackupSHA256 + compare to entry.SHA256). The reader here only validates the manifest structure.

func WriteCronBackupManifest added in v1.100.4

func WriteCronBackupManifest(exec executor.Executor, log *logging.Logger) (CronManifest, error)

WriteCronBackupManifest captures the two §42.2-locked cron files (CronCSFSrcPath, CronLFDSrcPath) before they are removed at install-time. For each file that exists:

  • Reads content via exec.ReadFile.
  • Reads metadata via exec.Stat (mode/uid/gid/size).
  • Computes sha256.
  • Writes the content to CronManifestDir/<backup-name> via exec.WriteFileAtomic.

Then writes manifest.json containing exactly the entries that were backed up. Entries for files that did not exist at capture time are NOT included in the manifest.

Returns the manifest that was written, plus an error if any step failed. The caller (disarmCSFArtifacts) MAY proceed with the rm even if manifest writing fails — the rm is the install-time invariant; the manifest is best-effort fidelity. But on success the caller should log the manifest path so the operator can observe it.

MUST be called BEFORE the cron files are removed; otherwise the content read returns os.ErrNotExist and the entry is skipped.

type CronManifestEntry added in v1.100.4

type CronManifestEntry struct {
	Path       string `json:"path"`        // absolute /etc/cron.d/<name>
	BackupName string `json:"backup_name"` // basename within CronManifestDir
	SHA256     string `json:"sha256"`      // hex sha256 of the backed-up content
	Mode       uint32 `json:"mode"`        // os.FileMode-compatible permission bits
	UID        int    `json:"uid"`
	GID        int    `json:"gid"`
	Size       int64  `json:"size"`
}

CronManifestEntry records one backed-up cron file.

type InstallerContinuation added in v1.229.12

type InstallerContinuation string

InstallerContinuation is GO's policy decision. This is the authority that moved.

const (
	ContinueComplete InstallerContinuation = "CONTINUE_COMPLETE"
	ContinueDeferred InstallerContinuation = "CONTINUE_DEFERRED"
	Abort            InstallerContinuation = "ABORT"

	// RetryRefused — v1.230.0 Gate 6R. NOT a continuation and NOT an abort: nothing
	// happened, so the convergence is still entirely owed. The caller retries within
	// the deadline it already has; if every attempt is refused the install is DEFERRED,
	// never FAILED.
	RetryRefused InstallerContinuation = "RETRY_REFUSED"
)

type RebuildDisposition added in v1.229.12

type RebuildDisposition string

RebuildDisposition is the SHELL's report about its own transaction.

const (
	DispositionComplete        RebuildDisposition = "COMPLETE"
	DispositionDeferredRuntime RebuildDisposition = "DEFERRED_RUNTIME"
	DispositionRegression      RebuildDisposition = "REGRESSION"
	DispositionFatal           RebuildDisposition = "FATAL"

	// DispositionRefused — v1.230.0 Gate 6R. THE REBUILD NEVER STARTED.
	//
	//	REFUSED  rebuild NEVER STARTED, firewall NOT modified (convergence lock held)
	//	FAILED   rebuild EXECUTED and failed
	//	TIMEOUT  rebuild EXECUTED and did not finish in budget
	//
	// Before this existed, a refusal published NO record at all and the consumer
	// collapsed ABSENCE OF CONTRACT into FAILED_REBUILD on a host whose enforcement
	// had not been touched (dns1, v1.229.13 -> v1.229.14).
	//
	// ⛔ THE CONSUMER READS THIS FIELD, NOT THE STDERR SENTENCE. No code on this path
	// may match "convergence already in progress" or any other message text.
	DispositionRefused RebuildDisposition = "REFUSED"
)

type RebuildObservation added in v1.230.0

type RebuildObservation struct {
	// Observed is false ONLY when no rebuild subprocess ever ran, in which case
	// ExitCode and Duration are meaningless and MUST NOT be recorded as measurements.
	Observed bool
	ExitCode int
	Duration time.Duration
	Attempts int

	// Disposition is the shell's own report for the verdict-bearing attempt, "" when no
	// usable contract was obtained. ⛔ IT IS A CLAIM, NOT A VERIFICATION — carried so the
	// post-update convergence contract can TEST it against kernel-side evidence
	// (VerifyPostUpdateConvergence), never so a caller can trust it.
	Disposition RebuildDisposition
	// Committed is the contract's own transaction.committed flag. Same warning.
	Committed bool
}

RebuildObservation is WHAT WAS ACTUALLY OBSERVED of the rebuild subprocess, for the caller to RECORD. It is evidence, not a verdict — the verdict is the returned error.

⛔ WHY IT EXISTS (dns1, same transaction as the refusal above). install_state carried, in ONE file, for ONE run:

FAILURE_REASON=... produced no usable result contract (exit 1): ...
REBUILD_EXIT_CODE=0
REBUILD_DURATION_MS=0

against an installer.log that recorded `(exit=1)` and `elapsed=31.22s`. The prose was right and the MACHINE-READABLE FIELDS WERE WRONG — because nothing in the tree ever assigned them. They were struct zero-values persisted in the shape of measurements. Automation reads the structured field in preference to the prose, so a failed install read as a clean rebuild.

A FIELD THAT IS NEVER WRITTEN IS NOT A DEFAULT. IT IS A FABRICATED MEASUREMENT.

⛔ ExitCode AND Duration DESCRIBE THE SAME ATTEMPT — the last one, the one the verdict came from. They are deliberately NOT mixed (a last-attempt rc beside a summed duration would be a fresh internal contradiction of exactly the kind this closes). Attempts carries the count so a reader can see there were earlier, refused tries.

func Rebuild

Rebuild runs "nftban firewall rebuild" and returns an error if it fails. Shell rebuild exit code contract (authoritative — do not redefine):

0 = PROTECTED (all checks passed)
1 = DEGRADED  (firewall operational, some module checks failed) or REFUSED
2 = FAILED    (rollback happened)
3 = FATAL     (rollback also failed)

rc is PROCESS EVIDENCE ONLY. The structured result contract is the authority; rc 1 no longer distinguishes anything on its own, which is precisely why the refusal had to become a contract rather than a new number.

ctx BOUNDS THE REFUSAL RETRIES AND NOTHING ELSE.

⛔ THE REBUILD EXECUTION STAYS ON context.Background() (LANE 6A, 219bd781): its duration scales with host firewall state (lab2 ~35 s vs srv3 ~453 s) and no outer constant is correct across that range. A REFUSED attempt executed nothing, so waiting to try again is legitimately bounded by the installer's existing deadline — there is no second, independent timeout here.

type RebuildOnceResult added in v1.230.0

type RebuildOnceResult struct {
	Observation RebuildObservation
	// Result is the parsed contract, or nil when none was usable.
	Result *RebuildResult
	// ReadErr is why no usable contract was obtained.
	ReadErr error
	// Executed — did the rebuild cross the execution boundary? Established from the
	// witness, or trivially from a contract that exists at all.
	Executed bool
	Stderr   string
}

runRebuildAttempt performs ONE rebuild invocation.

⛔ EVERY ATTEMPT IS ITS OWN OPERATION: fresh operation id, fresh result path, fresh witness path. Reusing them across a retry would recreate exactly the stale-record and cross-run hazards the per-operation contract was introduced to remove. RebuildOnceResult is one contract-bearing rebuild invocation, for a caller plane that owns its own classification.

func RebuildOnceNoRetry added in v1.230.0

func RebuildOnceNoRetry(exec executor.Executor, log *logging.Logger, command string, baseArgs []string) RebuildOnceResult

RebuildOnceNoRetry runs ONE rebuild on the CALLER'S OWN command path, obtains the result contract and the execution witness, and DOES NOT RETRY.

⛔ THE TWO PLANES ARE NOT FLATTENED, DELIBERATELY. `nftban update apply` invokes the rebuild WITHOUT --install-context, so the shell wrapper applies the INTERACTIVE FAIL-FAST lock policy: it refuses immediately instead of waiting out a bounded queue. Retrying under that policy would be inventing a lifecycle wait the plane never declared, and the doctrine here is that context is PASSED, never inferred. So the refusal RETRY behaviour of the installer plane does not travel; a refusal on this plane is reported to the caller at once.

What DOES apply on every plane: a refusal is a CONTRACT, not a bare rc, and an observation is a MEASUREMENT, not a zero-value.

SAME FEATURE NAME != SAME RUNTIME AUTHORITY.

type RebuildResult added in v1.229.12

type RebuildResult struct {
	SchemaVersion     string             `json:"schema_version"`
	OperationID       string             `json:"operation_id"`
	Context           string             `json:"context"`
	Disposition       RebuildDisposition `json:"disposition"`
	ReasonCodes       []string           `json:"reason_codes"`
	RollbackPerformed bool               `json:"rollback_performed"`
	// Modified / EnforcementUnchanged — v1.230.0 Gate 6R mutation facts.
	//
	// ⛔ CONSUMED FOR REFUSED ONLY. The producer can PROVE them there (the convergence
	// lock was never acquired, so nothing downstream of it ran) and emits fail-closed
	// placeholders — modified=true, enforcement_unchanged=false — everywhere else.
	// Reading them for any other disposition would be reading a placeholder as a fact.
	//     A FIELD MAY ONLY BE CONSUMED WHERE ITS VALUE IS PROVEN, NOT MERELY PRESENT.
	Modified             bool               `json:"modified"`
	EnforcementUnchanged bool               `json:"enforcement_unchanged"`
	Transaction          rebuildTransaction `json:"transaction"`
	Retry                rebuildRetry       `json:"retry"`
	PreStatus            string             `json:"pre_status"`
	PostStatus           string             `json:"post_status"`
	EmittedAt            string             `json:"emitted_at"`
}

RebuildResult is the per-operation record the shell publishes atomically.

func ReadRebuildResult added in v1.229.12

func ReadRebuildResult(path, wantOperationID string) (*RebuildResult, error)

ReadRebuildResult loads and validates the per-operation record.

⛔ EVERY FAILURE HERE IS FATAL TO THE INSTALL. That is the property that makes unexpected shell aborts safe WITHOUT Go having to model bash: an aborted shell publishes no record, and a missing record aborts the install. We do not need to enumerate bash failure modes.

func (*RebuildResult) Continuation added in v1.229.12

func (r *RebuildResult) Continuation() InstallerContinuation

Continuation maps the shell's disposition to Go's installer policy.

⛔ UNKNOWN VALUES ABORT. Schema evolution must default to safe, never to "continue".

func (*RebuildResult) ContradictsExitCode added in v1.229.12

func (r *RebuildResult) ContradictsExitCode(rc int) bool

ContradictsExitCode reports whether the record and the process rc disagree.

⛔ rc is not the authority, but a CONTRADICTION is itself evidence of a broken contract and must abort. Consistent pairs: COMPLETE/0, DEFERRED_RUNTIME/1, REGRESSION|FATAL/>=2.

type TableClass added in v1.228.11

type TableClass int

TableIsEmpty reports whether the named nft table exists and contains zero rule lines — i.e. it is a bare iptables-nft / distro skeleton safe to remove. It runs `nft list table <family> <table>` and counts non-structural, non-blank lines (the same heuristic the CVE inet-filter guard uses).

Conservative by contract: if the table does not exist, or `nft` errors (non-zero exit), TableIsEmpty returns false so the caller does NOT treat it as a removable skeleton. Callers gate the actual delete on NftTableExists first; the existence re-check here is defense-in-depth for the error case. TableClass is the tri-state result of classifying a foreign nft table.

TableIsEmpty collapses "populated" and "could not observe" into a single false, which is correct for a delete gate (both must refuse) but wrong for reporting: an install that could not CLASSIFY a table has not established that its cleanup was safe, and must not report clean success. v1.228.11 separates the two so callers can preserve in both cases while telling the truth about which one happened.

const (
	// TableClassUnknown — `nft` could not be read. Observation failure is NEVER
	// permission to delete (OBSERVATION_FAILURE != SECURITY_STATE_EMPTY).
	TableClassUnknown TableClass = iota
	// TableClassAbsent — the table does not exist; nothing to do.
	TableClassAbsent
	// TableClassEmpty — exists with zero rule lines: a bare iptables-nft/distro
	// skeleton, eligible for cleanup.
	TableClassEmpty
	// TableClassPopulated — holds real rules. Ownership is NOT established by
	// the table name, so this is foreign-or-unknown and must be preserved.
	TableClassPopulated
)

func ClassifyTable added in v1.228.11

func ClassifyTable(exec executor.Executor, family, table string) TableClass

ClassifyTable reports the tri-state class of a table using the same structural-line heuristic as TableIsEmpty, so the delete gate and the reporting path can never disagree.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL