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§
- Admit
Command - One command that admits a rule: the rule’s label, its argument vector exactly as
applyexecutes 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
applyran: 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. - Firewall
Apply Report - What
applydid: the refusal when nothing ran, every command that ran with its outcome, the firewall’s word afterwards, and the report the commands came from.okis the firewall’s verdict after the run, never the commands’ exit codes alone. - Firewall
Observation - 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. - Firewall
Plan - 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 emptyrulesis “nothing is needed”, which is the answer for no declaration and for a disabled one. - Firewall
Report - The whole report: the plan, the backend, the commands, the firewall’s word and the
kernel’s, with one verdict sentence and one
ok. Whatmesh.firewallanswers and what the doctor’sfirewallcheck reads. - Firewall
Rule - 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::labelis how a verdict names it.
Enums§
- Backend
- The firewall front this host runs, as far as the executable can tell without privilege:
decided by
detectfrom the platform and the path, and the thingrender,observeandapplyare specific to. - Firewall
Observation State - What the host’s firewall says about the plan, as one word. Only
missingis a failure on its own;unobservableis 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_blocksreads, 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 detectfor 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, becauseufwandnftlive there.- enclosing
- The private range an address belongs to, as the source network of a rule;
Nonefor 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 rulesetoutput against the plan: a rule is admitted when a line accepts itsdport(to its group, when it has one; from each source, when it has any). - judge_
ufw - Judge
ufw statusoutput 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;Nonewhen the log cannot be read from this process (nojournalctl, 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 isNone.- 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.