switchop

package
v1.229.12 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: MPL-2.0 Imports: 16 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.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.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 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")
)

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 Rebuild

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

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)
2 = FAILED    (rollback happened)
3 = FATAL     (rollback also failed)

Exit 1 (DEGRADED) is expected during upgrade: module-scoped chains require the daemon to be running, which may not be the case yet. Only exit 2+ is treated as a fatal rebuild failure.

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 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 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 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"
)

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"
)

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"`
	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