Skip to main content

Module firewall

Module firewall 

Source
Expand description

The host firewall, as the mesh needs it: what inbound traffic the declaration implies this machine must admit, which firewall this host runs, the commands that admit it, what the firewall says now, and what the kernel logged it dropping.

The declaration says who may link; the host firewall says what reaches the socket at all, and the two drift silently. On 2026-10-08 lundra, a declared hub, ran ufw with default deny and no rule for the hub port or the multicast group: every registration and every advertisement from the LAN was dropped at the kernel (187 of them in a week of [UFW BLOCK] lines) while mesh doctor reported every check holding, because every check it ran was a fact of the process and none was a fact of the host. This module makes the admission a derived requirement with a verdict — a firewall check of the doctor — instead of a thing an operator remembers.

Pure where it can be: plan is a function of the declaration and this machine’s addresses; detect of the platform and what is on the path; render of the two; and every judgement of a firewall’s own output is a function of that text. Only observe, kernel_blocks and apply touch the host, and each says when it could not.

What this is not: a firewall manager. It admits the ports the mesh declared, from the private networks the declaration names, and touches no other rule; and it never opens anything on a public address: no rule is planned without a private network to admit from (the multicast group excepted), and the declaration case refuses a public address first.

use std::net::Ipv4Addr;
use majordomus_cli::mesh::firewall::{plan, Role};
use majordomus_cli::mesh::MeshConfig;

let config: MeshConfig = serde_json::from_value(serde_json::json!({
    "schema": "mesh/v1", "kind": "mesh-declaration", "id": "doc", "enabled": true,
    "rendezvous": { "endpoints": ["http://192.168.7.10:8791", "http://100.64.1.2:8791"] },
})).unwrap();
// this machine is the hub at 192.168.7.10: it must admit the group and the hub port
let p = plan(Some(&config), &[Ipv4Addr::new(192, 168, 7, 10)], None);
assert_eq!(p.networks, ["100.64.0.0/10", "192.168.7.0/24"]);
assert!(p.rules.iter().any(|r| r.role == Role::Multicast && r.port == 7741));
assert!(p.rules.iter().any(|r| r.role == Role::Hub && r.port == 8791));
// another machine of the same fleet is no hub and admits only the group
let p = plan(Some(&config), &[Ipv4Addr::new(192, 168, 7, 11)], None);
assert!(p.rules.iter().all(|r| r.role == Role::Multicast));

Structs§

AdmitCommand
One command that admits a rule: the rule’s label, its argument vector exactly as apply executes it, and the same quoted as one line a person pastes into a root shell. The two are projections of one vector, so what the report shows is what runs.
Applied
One command apply ran: the line, whether it exited 0 without printing an error, and everything it printed, so a refusal by the tool is read in the report and not in a log.
FirewallApplyReport
What apply did: the refusal when nothing ran, every command that ran with its outcome, the firewall’s word afterwards, and the report the commands came from. ok is the firewall’s verdict after the run, never the commands’ exit codes alone.
FirewallObservation
The firewall’s own word, judged against the plan: the state, the evidence behind it, and the labels of the rules not admitted when the state is missing.
FirewallPlan
What the declaration implies this host must admit: the rules, the networks they admit from, the hub ports this machine is listed for, and the addresses the plan saw. The answer of plan; an empty rules is “nothing is needed”, which is the answer for no declaration and for a disabled one.
FirewallReport
The whole report: the plan, the backend, the commands, the firewall’s word and the kernel’s, with one verdict sentence and one ok. What mesh.firewall answers and what the doctor’s firewall check reads.
FirewallRule
One inbound admission the mesh needs on this host: a protocol and a port, the group it is addressed to when it is multicast, the networks it may come from, and the sentence that says why. A rule is derived, never written by hand; FirewallRule::label is how a verdict names it.

Enums§

Backend
The firewall front this host runs, as far as the executable can tell without privilege: decided by detect from the platform and the path, and the thing render, observe and apply are specific to.
FirewallObservationState
What the host’s firewall says about the plan, as one word. Only missing is a failure on its own; unobservable is an absence that the kernel log may still turn into one.
Protocol
The transport protocol of a rule: discovery is datagrams, registration and the link protocol are connections. Serialised as the word a firewall front takes.
Role
Why a rule exists: which part of the declaration implied it. The role is what a person reads beside the rule and what a verdict names, and it is serialised in snake case.

Constants§

BLOCK_WINDOW_SECONDS
The window kernel_blocks reads, in seconds: long enough to hold several advertisements (every 15 s by default) and registrations (every 30 s) from any peer the firewall drops, short enough that a rule just admitted is seen to work within minutes instead of an hour.
COMMENT_PREFIX
The comment every rule this module writes carries, so that the rules it admitted can be told from an operator’s own.

Functions§

apply
Run the report’s commands on this host. Refuses — runs nothing — without root and without a backend; every command that runs is recorded with its exit and its output, and the firewall is asked again afterwards so the answer is its word, not the commands’.
count_blocks
Count, per port of the plan, the [UFW BLOCK] lines of a kernel log that name it as the destination. Pure.
detect
Which backend a platform with these executables on its path runs. Pure: the platform and the lookup are given, so the decision is testable on any machine.
detect_here
detect for this process: its platform, and the path it would run the tool from — PATH, and the sbin directories a service manager’s path has and a login shell’s often lacks, because ufw and nft live there.
enclosing
The private range an address belongs to, as the source network of a rule; None for a public address, which the declaration case refuses and no rule is derived for.
judge_application_firewall
Judge the macOS application firewall against the executable the server runs as: off is inactive; on, the executable must be listed as allowed.
judge_nft
Judge nft list ruleset output against the plan: a rule is admitted when a line accepts its dport (to its group, when it has one; from each source, when it has any).
judge_ufw
Judge ufw status output against the plan. Pure.
kernel_blocks
What the kernel logged the firewall dropping toward the plan’s ports in the last BLOCK_WINDOW_SECONDS, per port; None when the log cannot be read from this process (no journalctl, or no membership of the groups that may read it), which is an absence, not a verdict. An empty plan asks nothing.
observe
Ask the host’s firewall about the plan. The judgement is pure; this is the asking, and with no backend there is nothing to ask.
parse_endpoint
http://192.168.7.10:8791 → the address and the port; a host name, a scheme the mesh does not speak, or a missing port is None.
plan
Derive the admissions from the declaration (or its absence), this machine’s addresses and, when its server listens beyond loopback, the server’s port.
render
The commands that admit the plan on the backend. ufw and nftables take one rule per source network; the macOS application firewall admits an executable, so it takes the executable the server runs as, once, whatever the ports — and nothing, when no executable is given.
report
Build the report for this host from the declaration, the addresses and the server’s port, asking the firewall and the kernel log — and asking neither when the plan is empty, because nothing is needed.