mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 19:18:25 +00:00
Everything the release needs except the version number, which stays at 0.5.0-dev until the tag. The changelog entry covers only the work that is new on this line. The point release's forty-six entries arrived under their own heading with the forward merge and are left alone; the twenty that remained are regrouped by topic and eight more added for changes no entry covered. Three of those eight matter to someone upgrading. Five root modules and four re-exports left the public library surface and Node::connections narrowed, none of it recorded anywhere; the entry names what to use instead and distinguishes the removed connection-phase enum from the Noise type of the same name, which is a different type that still exists. Tracing targets moved, so an existing RUST_LOG filter stops matching rather than erroring. And the handshake resend interval key no longer governs the first resend, which is now a constant, though it still governs later ones. Seven more entries cover the work that landed after the first content pass was written: the experimental native datagram API, the fipsctl probe diagnostic, per-instance transport addressing, the app-owned UDP socket seam, and the connect, disconnect and path-MTU fixes. The four bug fixes among them all reach the deployed line, so the release notes no longer claim this release carries exactly one fix for a shipped bug; it carries four. There is no security section, because after the split every security entry belongs to the point release. The release notes say so plainly rather than leaving a reader upgrading across both releases to conclude this one carries no security work. The notes are organized by audience, since the release spans OpenWrt routers, embedders, FreeBSD, and the existing platforms, and a single list serves none of them. The native datagram API is given a section of its own rather than folded into the embedding seam: it is a client-facing API rather than a way to host a node, and its one rule with no Berkeley-socket counterpart, that the v1 wire carries no half-close, needs to be somewhere a client author will read it. FreeBSD is advertised as supported on x86_64 only, stated wherever the platform appears. Android is advertised as an embedding seam and not as a supported platform: a compile-gated library surface with no artifact and no host application guide. The configuration table rename is carried through every shipped file that taught the old spelling: nine documentation files, the OpenWrt sample config and a test generator, twenty-two sites in all. Guides written this same cycle were among them, which is how the omission was found. The documentation that arrived with the native API was checked for the same omission and was already clean. The compatibility tests keep the old spelling deliberately, since they exist to test the fold. The changelog section is the fold of master's [Unreleased], not a snapshot of it. An earlier version of this commit took a copy that then drifted, so each section ended up holding a bullet the other did not and re-folding them would have picked a winner silently. Both causes were fixed on master instead — the NixOS module had never been recorded there, and the pre-release batch of fixes was new — so [Unreleased] is a strict superset and this is a copy rather than a merge. [0.5.0] carries all forty-six bullets byte for byte, [Unreleased] is empty, and [0.4.2] is untouched, checked by hashing it against master's copy. The BLE work landed after the content pass and gets one summary entry in the changelog and one section in the release notes rather than nine bullets: the ble_available gate replacing target_os = "linux", packet-boundary recovery for stream-oriented backends, peer recognition by node identity instead of a rotating link address, the L2CAP PSM moving into the backend seam and onto the advertisement, the embedder-supplied Android radio, bounded probe retry, and inbound handshakes moved off the accept loop. The two release-notes copies no longer share their link paths. Relative links resolve from one directory only, so the seven written for docs/releases/ all 404ed from the root copy. The root copy now uses paths from the repository root and the versioned copy keeps the ../ form; both sets were resolved against the tree. The same two links are broken the same way in the v0.4.0 through v0.4.2 notes, left as shipped history. The contributor tallies are re-derived against maint..HEAD rather than adjusted: twenty commits from outside the project and 171 from me, with Arjen at fifteen and fr34aky at two. An earlier count of twelve and 138 was carried from a measurement taken three days before this content was written, and the BLE branch widened the gap after it. Arjen's NixOS flake module, the UDP sin6_scope_id fix and most of the BLE rework were uncredited, as was fr34aky's L2CAP PSM seam. They want one last re-derive at tag time if anything lands before the tag. A sweep of all 99 tracked markdown files against the tree corrected fifty-three of them. Four told the reader to run a build.sh that does not exist; the only harness builder is testing/scripts/build.sh. The BLE build prerequisites were described as optional on the strength of a probe that build.rs does not perform, and bluez was named a build prerequisite when libdbus-sys asks only for libdbus-1-dev and pkg-config and bluez is the runtime daemon. Link cost is the primary sort key in next-hop ranking, not reserved for future use; Ethernet runs on macOS as well as Linux; the BLE MTU is the L2CAP CoC MTU rather than a negotiated ATT_MTU; effective Ethernet MTU is 1497; the LAN discovery subsystem is src/mdns and eight citations still named a src/discovery that never existed here. The connectivity states in three tutorials were invented, and their jq filters matched nothing including healthy peers. One command filtered on a literal fd97: address prefix, which only the first byte of fixes, so it returned empty for all but one reader in 256 and every later step using the variable failed silently. transports.tor.advertise_on_nostr was undocumented despite being validated against node.rendezvous.nostr.enabled. The transport design document gains the BLE section it never had, written from the source: the backend cascade and its compile_error tripwire, the platform gate, the PSM advertisement wire layout and the byte budget that forces a 16-bit service-data key, and the probe and admission bounds. Three source files carried the same class of staleness and are corrected with the documentation: the OpenWrt ipk usage line and Makefile error text both named a packaging/openwrt that does not exist, and chaos.sh parsed --subnet without listing it. Folded in with the content commit, having been prepared alongside it: The three GitHub Action pins that had gone stale. Every third-party action is pinned to a commit SHA, nothing reports that a pin has aged, and re-resolving all ten against their tags found dorny/test-reporter@v2, taiki-e/install-action@v2 and vmactions/freebsd-vm@v1 had moved. The three install-action@nextest references stay unpinned, since that action reads the tool to install from the ref name. check-action-pins.sh passes at 75 references and all nine workflow files parse. The lockfile refresh, which is the mutating half of the dependency sweep. Thirty-six packages move to their latest semver-compatible versions and every one is transitive; nothing declared in Cargo.toml changes version. No advisory forces any of them. It was taken before the validation battery, because a gate run against a lockfile that later moves proves nothing about what ships. The sha2 0.10 to 0.11, hkdf 0.12 to 0.13 and bech32 0.11 to 0.12 majors, three of the four deferred at v0.4.0 for change surface rather than security. All three land with no source change. sha2 and hkdf must move together, since both depend on digest 0.11, and neither changes an algorithm. That matters because the chaining-key KDF in the Noise handshake is built on Hkdf::<Sha256>, where an output change would be a wire break rather than a compile error; no known-answer vectors exist for that path, so the wire-compatibility gate is what covers it. secp256k1 0.31 is deliberately absent, since nostr's own requirement would leave two copies of the ECC library in the tree. The README support matrix, rebuilt as one feature table broken out by Linux variety. A single Linux column hid that Debian, Ubuntu, Arch and NixOS are one glibc build differing in packaging, that OpenWrt is musl and drops BLE, and that Android is not a daemon platform. Transport rows sort by how many platforms carry them. A Native API row reads its platform set from the cfg gates. The installer row becomes a package format row naming the artifact, and only the .deb is exercised per release. Four changelog and release-note gaps the BLE re-walk found: a Bluetooth LE bullet stranded inside the released 0.4.2 section, a missing Fixed entry for the scan and probe loop counting a pool-refused connection as an established link, the unnamed embedder call that installs an application-owned radio, and the fact that stopping the transport now stops scanning as well as advertising. Three release-document gaps found walking the unsurveyed commits: the UDP reuse-flag fix stated in the direction opposite to the one it was made, with the silent second-daemon bind it prevents left unsaid; the corrected native-API socket paragraph carried into both release-note copies, which still named SOCK_SEQPACKET on FreeBSD and two kernels where three are handled; and the coordinate-cache hardening, which shipped with no text anywhere despite adding four operator-visible status fields. That last entry states plainly that the checks are mitigations and not a closure, since the coordinate is still not authenticated. Also folded in, the documentation pass that followed the content commit: A stage-pipeline diagram for the probe, embedded in the fipsctl reference under the five-stage list. It draws the five stages left to right with each stage's failure reasons below it, and the bypass that skips both lookup stages when the coordinates are cached or the target is a direct peer. Its branches come from the probe state machine rather than from the report, so the path stage is drawn as the one failure that does not stop the probe. A rewrite of the README's "What FIPS does" section. It now opens with what a machine running FIPS gets, rather than with the two deployment modes, and gives the self-organizing and permissionless property its own paragraph since it holds for both modes. A regrouping of the README's feature list into the mesh, getting traffic onto it, and running a node, with a bullet added for the native datagram API, which had none despite sitting in the support matrix. The Quick start now leads with the released packages rather than a source build. It also fixes a real defect: the package enables fips.service and fips-dns.service and starts neither on a fresh install, so .fips name resolution was silently dead until the next reboot and neither page said to start the service. A rewrite of the release notes. They opened with seven subsections of upgrade caveats and reached the first feature two hundred lines in; they now open with a summary of the release and elaborate below it in the same order. Android is stated as supported through an embedded crate rather than as a standalone daemon, consistently across all three documents. The OpenWrt pair is corrected: it is 802.11s between routers with FIPS supplying encryption, authentication and routing, plus a convention of an open !FIPS SSID a client joins over WiFi, not meshing over a router's own radios. The probe's path output is described as the least-common-ancestor walk, which is the worst-case fallback route rather than the route a packet takes. Detail that did not change what a reader does was cut from the notes and kept in the changelog.
519 lines
19 KiB
Markdown
519 lines
19 KiB
Markdown
# Host a Service of Your Own
|
|
|
|
In [reach-mesh-services](reach-mesh-services.md) you used
|
|
ordinary IPv6 tools to reach services on other mesh nodes. This
|
|
tutorial flips the direction. You will bring up a small HTTP
|
|
server on your machine, make a deliberate choice about which
|
|
interface it binds to, and turn on the mesh firewall to keep
|
|
the exposure to what you intended. By the end you will have
|
|
hosted your first peer-reachable service and made an informed
|
|
decision about who can reach it.
|
|
|
|
The whole exercise should take about twenty minutes. You should
|
|
have already worked through
|
|
[persistent-identity](persistent-identity.md) so that the npub
|
|
your service is reachable at does not change between restarts.
|
|
|
|
## What you'll build
|
|
|
|
```text
|
|
┌──────────────────────────────────────────┐
|
|
│ your fips node │
|
|
│ │
|
|
│ python3 -m http.server │
|
|
│ --bind <fips0-addr> 8080 │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ fips0 fd97:….:Y ◀─── port 8080 open │
|
|
└────────────┬─────────────────────────────┘
|
|
│
|
|
│ reachable as
|
|
│ http://<your-npub>.fips:8080/
|
|
▼
|
|
any mesh peer that can route to <your-npub>.fips
|
|
```
|
|
|
|
You will have:
|
|
|
|
- A single-page HTTP server bound to `fips0` only — not the
|
|
public internet, not your LAN, just the mesh.
|
|
- The mesh firewall baseline active, default-deny on `fips0`
|
|
inbound, with one explicit drop-in that allows TCP/8080.
|
|
- A clear understanding of which interface choice corresponds
|
|
to which audience.
|
|
|
|
## Why bind interface matters
|
|
|
|
The single most important decision when hosting a service is
|
|
**which interface (and therefore which audience) the service is
|
|
exposed to**. This is true for every IPv6 service, not just
|
|
FIPS — but FIPS makes it stark because your machine often has
|
|
several interfaces with very different exposure profiles.
|
|
|
|
> **Bind interface = exposure surface.** A typical FIPS host
|
|
> has at least two distinct audiences:
|
|
>
|
|
> - `fips0` (`fd97:…`, reachable as `<your-npub>.fips`) —
|
|
> reachable only from FIPS peers that have a working link to
|
|
> your node. Bound by Noise authentication and (optionally)
|
|
> the peer ACL.
|
|
> - `eth0` / `wlan0` (your LAN address) — reachable from anyone
|
|
> on your local network segment, with no FIPS auth in the
|
|
> way.
|
|
>
|
|
> When you run a server, the `--bind` argument decides which of
|
|
> these audiences sees the service. Binding to a *specific*
|
|
> address opts in to one audience. Binding to `[::]` or
|
|
> `0.0.0.0` opts in to **all** of them at once — including any
|
|
> you forgot you had.
|
|
>
|
|
> **Audit what's already listening.** Bringing up `fips0` adds
|
|
> a new audience to every service on this host that was already
|
|
> bound to `0.0.0.0` or `[::]`. SSH, your web server, a database
|
|
> — if any of them was listening on all interfaces before you
|
|
> joined the mesh, they are now reachable from mesh peers too.
|
|
> A quick `ss -tulnp` will show you everything currently
|
|
> listening and on which addresses. The mesh firewall (Step 5
|
|
> below) is one way to bring those exposures back under explicit
|
|
> control; rebinding the affected services to a specific
|
|
> non-mesh address is another.
|
|
|
|
There is nothing FIPS-specific about this rule; it applies to
|
|
SSH, web servers, databases, anything. FIPS just gives you the
|
|
option of "mesh peers only" as a distinct audience, which most
|
|
hosts otherwise don't have.
|
|
|
|
## Step 1: Find your node's mesh address
|
|
|
|
You need the `fd97:...` address assigned to your `fips0`
|
|
adapter. Two equivalent ways to get it:
|
|
|
|
```sh
|
|
ip -6 addr show fips0
|
|
```
|
|
|
|
Look for the `inet6 fd...` line: every mesh address begins with
|
|
`fd`, and the rest is derived from your key, so it will not be
|
|
`fd97` unless you are lucky. The address up to the `/` is what
|
|
you want.
|
|
|
|
Or via the daemon:
|
|
|
|
```sh
|
|
sudo fipsctl show status
|
|
```
|
|
|
|
The JSON has an `ipv6_addr` field — that is your address.
|
|
|
|
For the rest of this tutorial we will write the address as
|
|
`<your-fips0-addr>`. Substitute the actual `fd97:...` value
|
|
when you run the commands. Save it to a shell variable for
|
|
convenience:
|
|
|
|
```sh
|
|
FIPS0_ADDR=$(ip -6 addr show fips0 | awk '/inet6 fd/ {print $2}' | cut -d/ -f1)
|
|
echo "$FIPS0_ADDR"
|
|
```
|
|
|
|
You should also know your npub from
|
|
[persistent-identity](persistent-identity.md):
|
|
|
|
```sh
|
|
NPUB=$(sudo cat /etc/fips/fips.pub)
|
|
echo "$NPUB"
|
|
```
|
|
|
|
`<your-npub>.fips` and `<your-fips0-addr>` are two names for
|
|
the same destination.
|
|
|
|
## Step 2: Bring up an HTTP server bound to fips0
|
|
|
|
Make a small directory with one file in it so the server has
|
|
something to serve:
|
|
|
|
```sh
|
|
mkdir -p /tmp/mesh-demo
|
|
echo '<h1>Hello from the mesh</h1>' > /tmp/mesh-demo/index.html
|
|
cd /tmp/mesh-demo
|
|
```
|
|
|
|
Start a Python HTTP server bound to your `fips0` address:
|
|
|
|
```sh
|
|
python3 -m http.server --bind "$FIPS0_ADDR" 8080
|
|
```
|
|
|
|
Leave the server running. The terminal will show:
|
|
|
|
```text
|
|
Serving HTTP on fd97:... port 8080 (http://[fd97:...]:8080/) ...
|
|
```
|
|
|
|
Two things to notice:
|
|
|
|
- The "Serving HTTP on …" line names your `fd97:...` address
|
|
explicitly. Python is binding only to that one address.
|
|
- The default would have been `0.0.0.0` — which is **not**
|
|
what you want here. Without `--bind`, the server would be
|
|
reachable from your LAN and any public IP this host has,
|
|
not just from the mesh.
|
|
|
|
## Step 3: Verify the service locally
|
|
|
|
Open a second terminal. From the same host, fetch the page:
|
|
|
|
```sh
|
|
curl -6 "http://[$FIPS0_ADDR]:8080/"
|
|
```
|
|
|
|
Expect:
|
|
|
|
```text
|
|
<h1>Hello from the mesh</h1>
|
|
```
|
|
|
|
Now fetch it by name. Both forms should work:
|
|
|
|
```sh
|
|
curl -6 "http://${NPUB}.fips:8080/"
|
|
```
|
|
|
|
The daemon's local DNS responder turned the npub-form name into
|
|
the `fd97:...` address, and the request landed at your HTTP
|
|
server.
|
|
|
|
> **What just happened.** The kernel routed your local request
|
|
> via the loopback path because the destination address is
|
|
> assigned to one of your own interfaces. You exercised the
|
|
> client side (DNS, IPv6 socket open, HTTP request) and the
|
|
> server side (HTTP listener, response). What you have not yet
|
|
> verified is reachability *from another mesh node*. That is
|
|
> the next concern.
|
|
|
|
If you have `fipstop` available, open it now in another terminal
|
|
and switch to the **Node** tab. The right-half of the Traffic
|
|
block — the **Listening on fips0** panel — should list a `tcp`
|
|
row at port 8080 with a `python(<pid>)` Process column. The State
|
|
column reads `OPEN` because the firewall has not been turned on
|
|
yet; everything bound to `fips0` is currently mesh-reachable. The
|
|
yellow banner above the panel says
|
|
"fips-firewall.service inactive — all listeners exposed". Both
|
|
signals will flip in the next two steps.
|
|
|
|
## Step 4: Reachability from a mesh node
|
|
|
|
Any mesh node — a direct peer, or a node several hops away —
|
|
reaches your service the same way you reached `test-us01` in
|
|
[reach-mesh-services](reach-mesh-services.md): it looks up
|
|
`<your-npub>.fips`, gets back your `fd97:...` address, opens
|
|
a TCP connection to it, and the FIPS data plane carries the
|
|
packets across the mesh to you. From the remote host the curl
|
|
looks identical to yours:
|
|
|
|
```sh
|
|
curl -6 "http://${NPUB}.fips:8080/"
|
|
```
|
|
|
|
If you have a second machine on the mesh — or you can ask
|
|
another operator to try it — this is the moment to confirm.
|
|
A node that can already `ping6 ${NPUB}.fips` should also be
|
|
able to fetch your page. If it can ping but the curl times
|
|
out, jump to [Troubleshooting](#troubleshooting) — but most
|
|
likely the firewall step in this tutorial has not happened
|
|
yet, so a remote attempt right now will succeed straight
|
|
through to your HTTP server.
|
|
|
|
That is the problem. With no firewall in place, *any* mesh
|
|
node that can route to you — your direct peers, and every node
|
|
beyond them in the mesh — can reach port 8080. You have not
|
|
yet made a deliberate decision about whether you want that.
|
|
The rest of this tutorial replaces the implicit "every port
|
|
with a listener is reachable" with an explicit "only the ports
|
|
I have opened are reachable, optionally only from specific
|
|
mesh nodes." That is the firewall's job, and it is the only
|
|
mechanism in play in the rest of this tutorial.
|
|
|
|
(There is a separate, unrelated control called the *peer ACL*
|
|
that decides which npubs may establish a peer connection with
|
|
your node at the transport layer. It is not part of the
|
|
firewall and does not affect what is described below; Step 7
|
|
is a brief signpost to it.)
|
|
|
|
## Step 5: Activate the mesh firewall baseline
|
|
|
|
FIPS ships a default-deny nftables baseline at
|
|
`/etc/fips/fips.nft` that restricts inbound traffic on `fips0`
|
|
to ICMPv6 echo and conntrack replies. The baseline is **not**
|
|
enabled by default — activation is an explicit step the
|
|
operator has to take.
|
|
|
|
Activate it:
|
|
|
|
```sh
|
|
sudo systemctl enable --now fips-firewall.service
|
|
```
|
|
|
|
This loads the table immediately and arranges for it to load
|
|
on every subsequent boot. Confirm:
|
|
|
|
```sh
|
|
sudo nft list table inet fips
|
|
```
|
|
|
|
You will see one chain named `inbound` hooked at `input`,
|
|
roughly:
|
|
|
|
```text
|
|
table inet fips {
|
|
chain inbound {
|
|
type filter hook input priority filter; policy accept;
|
|
iifname != "fips0" return
|
|
ct state established,related accept
|
|
icmpv6 type echo-request accept
|
|
counter packets 0 bytes 0 drop
|
|
}
|
|
}
|
|
```
|
|
|
|
The chain admits ICMPv6 echo (so `ping6` from any mesh node
|
|
still works) and conntrack replies (so your *outbound*
|
|
connections still get their replies back). Everything else
|
|
inbound on `fips0` hits the final `counter ... drop`.
|
|
|
|
> **What this changed.** Your HTTP server is still running and
|
|
> still reachable *from this same host* (same-host traffic to
|
|
> `fd97:...` goes via the loopback path, which has
|
|
> `iifname != "fips0"` and short-circuits at the first rule).
|
|
> But any mesh node trying to reach `fd97:...:8080` now has its
|
|
> TCP SYN dropped before it can reach your server. From the
|
|
> remote end the connection times out.
|
|
|
|
The fipstop panel reflects the change immediately: the yellow
|
|
"firewall inactive" banner disappears, the panel title becomes a
|
|
plain "Listening on fips0", and your `tcp 8080 python(<pid>)` row
|
|
flips to **DarkGray** with `filt` in the State column. Every
|
|
other row also goes DarkGray — none of them have an explicit
|
|
accept rule yet, and the chain falls through to `counter drop`.
|
|
|
|
So the firewall is in the right shape but in the wrong state
|
|
for our purpose: we *want* mesh nodes to reach port 8080. The
|
|
next step opens that one port.
|
|
|
|
## Step 6: Open port 8080 via a drop-in
|
|
|
|
Drop-ins live under `/etc/fips/fips.d/` with the `.nft`
|
|
suffix. Each file is included into the `inbound` chain at the
|
|
marked point and may contain any nftables rule lines valid in
|
|
that context.
|
|
|
|
Create one for your HTTP service:
|
|
|
|
```sh
|
|
sudo tee /etc/fips/fips.d/http-mesh-demo.nft >/dev/null <<'EOF'
|
|
tcp dport 8080 accept
|
|
EOF
|
|
```
|
|
|
|
Reload the firewall:
|
|
|
|
```sh
|
|
sudo systemctl reload-or-restart fips-firewall.service
|
|
```
|
|
|
|
Confirm the rule is live:
|
|
|
|
```sh
|
|
sudo nft list table inet fips
|
|
```
|
|
|
|
The `inbound` chain now contains your `tcp dport 8080 accept`
|
|
rule between the conntrack rule and the final `counter drop`.
|
|
|
|
A curl from any mesh node will now reach the HTTP server. The
|
|
path is: remote node's mesh data plane → forwarded across the
|
|
mesh → your direct peer's link to you → `fips0` ingress →
|
|
`inbound` chain → matches `tcp dport 8080 accept` → delivered
|
|
to the HTTP server.
|
|
|
|
In the fipstop panel, your `tcp 8080 python(<pid>)` row flips
|
|
back to **default White** with `OPEN` in the State column on the
|
|
next poll tick. No other row changes — they remain DarkGray
|
|
`filt` because you have only opened this one port. The panel
|
|
doubles as a security screen for the rest of the tutorial: any
|
|
service whose row reads `OPEN` is mesh-reachable, anything
|
|
DarkGray is filtered. If you later add a saddr-restricted
|
|
drop-in (covered just below), the row will land at `filt?`
|
|
rather than `OPEN`, signalling that the rule exists but is
|
|
source-scoped — the panel deliberately does not classify
|
|
restricted accepts as fully open.
|
|
|
|
If you only want to expose the service to a *specific* node
|
|
or set of nodes, source-filter the rule. The address filter
|
|
applies to the mesh-source address as it arrives on `fips0`,
|
|
which is the originating node's address — not necessarily a
|
|
direct peer. Replace the drop-in contents with something like:
|
|
|
|
```nft
|
|
ip6 saddr fd97:1234:5678:9abc:def0:1234:5678:9abc tcp dport 8080 accept
|
|
```
|
|
|
|
The source address is the node's mesh address, which it
|
|
publishes in its `fips.pub` (and which you can resolve from
|
|
its npub). For multiple nodes, use a set:
|
|
|
|
```nft
|
|
ip6 saddr {
|
|
fd97:1111:2222:3333:4444:5555:6666:7777,
|
|
fd97:8888:9999:aaaa:bbbb:cccc:dddd:eeee
|
|
} tcp dport 8080 accept
|
|
```
|
|
|
|
For the worked example, leave the drop-in unfiltered — any
|
|
mesh node that can route to you can fetch your page.
|
|
|
|
## Step 7: A note on the peer ACL
|
|
|
|
The firewall you just configured is the only control in scope
|
|
for this tutorial. There is a separate, optional control
|
|
called the *peer ACL* that you may run across in other docs;
|
|
it is unrelated to the firewall and worth a sentence here only
|
|
so you do not confuse the two.
|
|
|
|
The peer ACL decides which npubs may establish a peer
|
|
connection with your node at the transport layer. It does not
|
|
look at ports, drop-ins, or `fips0` traffic. You do not need
|
|
it for this tutorial.
|
|
|
|
For when you do:
|
|
|
|
- [../how-to/enable-nostr-discovery.md](../how-to/enable-nostr-discovery.md)
|
|
— operator recipe.
|
|
- [../reference/security.md § Peer ACL](../reference/security.md#peer-acl)
|
|
— file format, evaluation order, alias handling.
|
|
|
|
## Step 8: Stop the server and tidy up
|
|
|
|
When you are done, stop the HTTP server in the first terminal
|
|
with `Ctrl-C`. The drop-in stays in place; remove it if you
|
|
do not want port 8080 reachable after the demo:
|
|
|
|
```sh
|
|
sudo rm /etc/fips/fips.d/http-mesh-demo.nft
|
|
sudo systemctl reload-or-restart fips-firewall.service
|
|
```
|
|
|
|
The `fips-firewall.service` itself can stay enabled —
|
|
default-deny on `fips0` is a sensible posture even with no
|
|
extra services running. To turn it back off:
|
|
|
|
```sh
|
|
sudo systemctl disable --now fips-firewall.service
|
|
```
|
|
|
|
## What you've learned
|
|
|
|
- **Bind interface = audience.** Binding to a specific address
|
|
opts in to one audience; binding to wildcard
|
|
(`0.0.0.0` / `[::]`) opts in to *all* of them, including
|
|
ones you forgot you had. For mesh-only exposure, bind to
|
|
your `fd97:...` address. The fipstop **Listening on fips0**
|
|
panel marks wildcard binds with a trailing `*` after the
|
|
process name as a reminder that the bind is not
|
|
fips0-specific.
|
|
- **Same-host loopback is misleading.** A local curl to your
|
|
own `fd97:...` address goes via the loopback path, not
|
|
through `fips0` ingress. To actually verify mesh-side
|
|
reachability you need a second machine, or to read what
|
|
the firewall is doing in `nft list table inet fips`.
|
|
- **The mesh firewall is opt-in.** `fips-firewall.service` is
|
|
not enabled by default. Once it is enabled, `fips0` is
|
|
default-deny except for ICMPv6 echo and conntrack replies.
|
|
- **Ports open via drop-ins.** Each file under
|
|
`/etc/fips/fips.d/*.nft` adds rules into the `inbound`
|
|
chain. Source-filter with `ip6 saddr` to scope a port to
|
|
specific mesh nodes.
|
|
- **Two independent controls at two different layers.** The
|
|
firewall is a layer-3 filter on `fips0`: it controls which
|
|
TCP/UDP ports are reachable and (optionally) which mesh
|
|
source addresses may reach them. The peer ACL is a
|
|
transport-layer admission filter on Noise handshakes: it
|
|
controls which npubs may become direct peers of your node.
|
|
They are unrelated — the ACL does not touch fips0 traffic,
|
|
and the firewall does not look at npubs.
|
|
|
|
You now have the mental model for hosting any IPv6 service
|
|
behind a deliberate exposure policy. The mechanics generalize:
|
|
SSH on port 22, a database on port 5432, a custom protocol on
|
|
its own port — same `--bind` rule, same drop-in shape.
|
|
|
|
## Troubleshooting
|
|
|
|
- **A remote mesh node cannot reach the service after the
|
|
firewall reload.** Check the drop-in syntax with
|
|
`sudo nft -c -f /etc/fips/fips.nft` before reloading; a
|
|
syntax error in any drop-in causes the whole table to fail
|
|
to load and the previous rules persist. Then
|
|
`sudo nft list table inet fips` to confirm your
|
|
`tcp dport 8080 accept` rule is present in the `inbound`
|
|
chain.
|
|
- **Local curl works, remote curl times out.** The packet is
|
|
reaching `fips0` ingress and being dropped by the baseline.
|
|
Either your drop-in did not load (see above) or it has a
|
|
source filter that excludes the remote node's address.
|
|
- **Local curl fails after binding to fips0.** Double-check
|
|
that your `FIPS0_ADDR` matches the address shown in
|
|
`ip -6 addr show fips0`. The Python server message also
|
|
echoes the bound address — confirm it starts with `fd97:`,
|
|
not `127.0.0.1` or `::`.
|
|
- **`Address already in use` from Python.** Another process
|
|
holds port 8080. Pick a different port (`8081`, `9000`, …)
|
|
for both the `python3 -m http.server` invocation and the
|
|
drop-in.
|
|
- **Watch the firewall counter to confirm drops.** The
|
|
`counter ... drop` line at the bottom of the chain
|
|
increments on every dropped inbound packet. After a remote
|
|
mesh node attempts to reach a port you have not opened,
|
|
`sudo nft list table inet fips` will show the counter
|
|
packet count rising.
|
|
- **Use fipstop to spot-check listener and filter state.** The
|
|
Listening on fips0 panel on the Node tab shows every
|
|
fips0-reachable listener and its current filter state. A row
|
|
staying `filt` after you expected `OPEN` usually means the
|
|
drop-in failed to load (a syntax error in any file under
|
|
`/etc/fips/fips.d/` aborts the whole reload, leaving the
|
|
previous ruleset in place) or the drop-in carries a source
|
|
filter and now reads `filt?` rather than `OPEN`.
|
|
|
|
## What's next
|
|
|
|
- [ground-up-mesh.md](ground-up-mesh.md) — Bring up two devices
|
|
on a shared physical link — Ethernet, WiFi, or Bluetooth —
|
|
with no pre-existing IP infrastructure between them. The
|
|
second deployment mode of FIPS, where the mesh is the
|
|
network rather than an overlay on top of one. Coexists with
|
|
overlay peers; the same daemon carries both.
|
|
|
|
For more depth on the firewall and ACL surface:
|
|
|
|
- [../how-to/enable-mesh-firewall.md](../how-to/enable-mesh-firewall.md)
|
|
— operator recipes for the baseline, drop-in patterns,
|
|
and how to fold the baseline into an existing
|
|
`nftables.conf`.
|
|
- [../reference/security.md](../reference/security.md) —
|
|
consolidated security reference: nftables baseline rules,
|
|
drop-in format, peer ACL semantics, default exposures by
|
|
transport, threat-resistance matrix.
|
|
- [../design/fips-security.md](../design/fips-security.md) —
|
|
threat model, why the baseline is opt-in, the metadata-
|
|
privacy posture.
|
|
|
|
If you want to host a service that is *not* on a FIPS node — say,
|
|
an existing HTTP server on a regular LAN box — and expose it to
|
|
mesh peers through a `fips-gateway`, that's the inbound
|
|
port-forward mode: the gateway runs a mesh-side listener on `fips0`
|
|
and forwards to a LAN target. The operator recipe is at
|
|
[../how-to/deploy-gateway.md#configure-the-inbound-half](../how-to/deploy-gateway.md#configure-the-inbound-half);
|
|
a hand-held walk-through on an OpenWrt AP is at
|
|
[deploy-fips-gateway.md](deploy-fips-gateway.md) under "Advanced"
|
|
in [README.md](README.md).
|