docs: four-section /docs/ restructure with new-user content, accuracy pass, and gateway feature-set rewrite

Restructures /docs/ by reader purpose (tutorials, how-to,
reference, design), adds the new-user-progression and
operator-recipe content the prior layout lacked, runs an
accuracy pass against current source across the pre-existing
design docs, and rewrites the gateway feature-set documentation
end-to-end around its actual operational profile (a niche
feature designed for systems already serving DHCP/DNS to a
LAN, with two independent halves — outbound LAN→mesh, inbound
mesh→LAN — sharing one nftables table, one binary, and one
control socket). Top-level README and getting-started rewritten
around two equally-weighted deployment modes (overlay on
existing IP networks; ground-up over non-IP transports).

## Additions

- 11 new tutorials in docs/tutorials/: an 8-step new-user
  progression from single-daemon test-mesh peering through
  to a ground-up two-device mesh, an IPv6-adapter side-trip
  walkthrough, an Advanced Tutorials index, and a hand-held
  OpenWrt walk-through for fips-gateway deployment that
  exercises both halves of the feature.
- 12 new how-tos in docs/how-to/: firewall activation,
  Nostr discovery (resolve / advertise / open across five
  scenarios), Tor onion (directory + control_port modes),
  UDP buffer tuning, unprivileged-user setup, persistent
  identity, host aliases, Bluetooth LE peering, MTU
  diagnostics, manual Linux-host gateway deployment (covers
  both halves), gateway troubleshooting (organised by half),
  and a section index.
- 9 new reference docs in docs/reference/: configuration,
  wire formats, control-socket protocol, four CLI references
  (fips, fipsctl, fipstop, fips-gateway), security posture
  matrix, and Nostr events catalog. Configuration and
  wire-formats are renamed-and-extended from prior design/
  versions; the other seven are net-new.
- 6 new design docs: fips-concepts, fips-architecture, and
  fips-prior-work split out of the deleted fips-intro.md;
  consolidated fips-mmp and fips-mtu aggregations; and a
  new generic port-advertisement-and-nat-traversal doc
  (Nostr-signaled port advertisement plus UDP NAT-traversal
  protocol, FIPS as an example implementation, suitable for
  eventual NIP submission).
- Top-level docs/getting-started.md walking through the
  binary-installer-only Install story.
- packaging/common/hosts pre-populated with the eight public
  test-mesh nodes so shortnames resolve out of the box on
  every fresh install.

## Changes

- 23 wire-format diagrams relocated to reference/diagrams/
  alongside the wire-formats move.
- 4 design diagrams corrected against source code
  (fips-protocol-stack, fips-identity-derivation,
  fips-coordinate-discovery, fips-routing-decision).
- 10 pre-existing design docs reconciled with current
  source. Numeric corrections: stale link-MMP report bounds
  (now [1s, 5s] with 200 ms cold-start floor); UDP default
  MTU (now 1280, IPv6 minimum); node_addr formula
  (SHA-256(pubkey)[..16]); Noise patterns (IK at link, XK
  at session); peer-ACL semantics (strict allowlist requires
  ALL in peers.deny); daemon DNS upstream ([::1]:5354);
  on-the-wire bloom-filter size (1,071 bytes); obsolete
  Cargo-feature references (PR #79 dropped them) removed.
- Transport framing tightened across the docs: TCP is for
  UDP-filtered networks (not NAT traversal); Tor is a
  deployment mode (not failover); WebSocket dropped (not a
  shipped FIPS transport); WiFi promoted to Implemented via
  Ethernet in infrastructure mode; classic-Bluetooth row
  removed (BLE is the only Bluetooth-mode transport).
- docs/design/fips-gateway.md rewritten end-to-end to lead
  with the niche-feature framing and the two-halves
  structure. Title moved from "FIPS Outbound LAN Gateway"
  to "FIPS Gateway"; architecture section describes the
  common machinery (the fips-gateway service, the nftables
  table, the control socket) before splitting into separate
  "Outbound Half" and "Inbound Half" sections of equal
  weight; security considerations split per-half; no Future
  Work section (speculative directions live in the project
  tracker, not in protocol design docs). Inbound port
  forwarding is a first-class half rather than a buried
  "Implemented Extensions" subsection.
- Gateway terminology unified across all gateway docs as a
  separate Linux service running alongside the fips daemon
  (its own systemd unit / OpenWrt init script). Container-
  pattern terms (sidecar) are reserved for the
  Docker/Kubernetes sidecar deployment examples — the
  testing/sidecar/ tree, examples/k8s-sidecar/,
  examples/sidecar-nostr-relay/,
  examples/wireguard-sidecar-macos/, and the related
  CHANGELOG / top-level README entries — where the term
  carries its standard container meaning.
- Net-new design body content: rekey section in
  fips-mesh-layer (Noise IK msg1/msg2 over the established
  link, K-bit cutover, drain window, smaller-NodeAddr-wins
  tie-breaker on dual-init); Mesh Size Estimation and
  Antipoison FPR Cap sections in fips-bloom-filters;
  Mesh-Interface Query Filter subsection in
  fips-ipv6-adapter; failure-suppression knobs and clock-
  skew tolerance in fips-nostr-discovery; loop-rejection
  and mid-chain ancestor swap added to spanning-tree
  propagation / stability rules; Priority Chain in
  fips-mesh-operation renumbered to match the
  routing-decision diagram.
- Top-level README: dropped the stale nostr-discovery
  cargo-feature parenthetical. docs/README.md and the four
  section READMEs (tutorials, how-to, reference, design)
  refreshed for the new structure; index rows reflect both
  halves of the gateway feature and the new fips-gateway
  CLI reference.
- Cargo.toml [package.metadata.deb] assets path updated for
  the fips-security.md move; .gitignore /reference/ rule
  anchored to repo root so docs/reference/ is trackable.
- packaging/openwrt-ipk/files/etc/fips/fips.yaml
  configuration-doc URL updated to the new
  docs/reference/configuration.md location.

## Deletions

- docs/design/fips-intro.md (split into the three new intro
  design docs).
- docs/design/document-relationships.svg (orphan, no longer
  referenced).
- docs/proposals/ tree removed; the only proposal it
  contained (the Nostr UDP hole-punch protocol) was
  rewritten as the new generic
  design/port-advertisement-and-nat-traversal.md.
This commit is contained in:
Johnathan Corgan
2026-05-08 03:02:12 +00:00
parent 4cdf382038
commit 5abf9a9325
91 changed files with 11474 additions and 3278 deletions
+73
View File
@@ -0,0 +1,73 @@
# Tutorials
If you have just installed FIPS, this is where to start. The
tutorials below take you from a freshly-installed daemon to a
node that:
- Has joined the public test mesh and can reach other nodes on it.
- Carries a stable identity that other operators can address.
- Discovers peers — and is discoverable — over Nostr.
- Hosts and consumes real services across the mesh.
Each tutorial is a complete, working session at the keyboard. You
configure something, restart the daemon, watch it come up, and
verify the result. The point is to build muscle memory, not to
cover every option.
> **Read them in order.** Each tutorial assumes the state the
> previous one left you in. If you skip ahead, the cross-references
> that lead you back may not match what you have on disk.
## The new-user progression
| # | Tutorial | What you'll do |
| - | -------- | -------------- |
| 1 | [join-the-test-mesh.md](join-the-test-mesh.md) | Add one public test peer to your config, watch the link come up, ping that peer and a second mesh node it routes you to. The starting point for everything else. |
| 2 | [persistent-identity.md](persistent-identity.md) | Pin your daemon to a stable Nostr keypair so your address stops changing on every restart. Other operators can now add you to their `peers:` lists; the services you host get a fixed name. |
| 3 | [resolve-peers-via-nostr.md](resolve-peers-via-nostr.md) | Stop hard-coding peer addresses. Drop the address line from your peer entry and let the daemon look up the current endpoint from public Nostr relays at dial time. |
| 4 | [advertise-your-node.md](advertise-your-node.md) | Publish your own UDP endpoint to Nostr so any operator who knows your npub can reach you, with a short final section on `udp:nat` traversal for symmetric-NAT networks. |
| 5 | [open-discovery.md](open-discovery.md) | Switch to `policy: open` and let your peer list populate itself from the ambient `fips-overlay-v1` namespace. Hands-off mesh participation. |
| 6 | [reach-mesh-services.md](reach-mesh-services.md) | Drive ordinary IPv6 tools — `ping6`, `nc`, `traceroute6`, `curl`, `ssh` — at mesh nodes by `<npub>.fips`. Get a feel for the daemon's IPv6 adapter, which makes unmodified IPv6 software work over the mesh. |
| 7 | [host-a-service.md](host-a-service.md) | Bring up an HTTP server bound to `fips0` so mesh nodes can reach it, with a deliberate exposure decision (mesh-only vs every interface), and the mesh firewall as a default-deny baseline. The peer ACL (a separate, transport-layer control over which npubs may peer with your node) is briefly mentioned alongside. |
| 8 | [ground-up-mesh.md](ground-up-mesh.md) | Bring up a second deployment mode: two devices joined by Ethernet (or WiFi, or BLE) with no IP infrastructure between them. The mesh emerges from layer 2 up. Coexists with overlay peers — the same daemon can carry both. |
After tutorial 8 you have a fully participating mesh node that
reaches services hosted by other mesh nodes and hosts services of its own,
with identity, discovery, reachability, an explicit exposure
policy, and an understanding of both deployment modes — overlay
on top of existing IP, and ground-up where the mesh is the
network.
There is also a side trip you can take any time after tutorial 1:
- [ipv6-adapter-walkthrough.md](ipv6-adapter-walkthrough.md) —
trace one `ssh` from DNS query through session setup to the
far-side TUN, using `fipstop` and `fipsctl` to watch each step.
Optional, but if you like seeing how the pieces fit together,
this is the doc that shows you.
## Advanced
These are not part of the new-user progression. They assume you
have already worked through the tutorials above and now want to
fold FIPS into a wider network deployment.
- [deploy-fips-gateway.md](deploy-fips-gateway.md) — Stand up a
`fips-gateway` on an OpenWrt access point so unmodified LAN
hosts can reach `<npub>.fips` destinations through a DNS-
allocated virtual IPv6 pool and kernel nftables NAT, with no
per-host FIPS install. Also walks through one inbound port
forward exposing a LAN service to mesh peers. Aimed at
operators bridging a LAN segment into the overlay from the
edge router. For a non-OpenWrt host the same deployment is
in [../how-to/deploy-gateway.md](../how-to/deploy-gateway.md).
## When to use the how-to guides instead
The tutorials here walk through one specific path each. The
how-to guides under [../how-to/](../how-to/) are the operator
recipes — alternative provisioning paths, less-common
configurations, troubleshooting techniques. Once you have the
shape of FIPS in your head from these tutorials, the how-tos are
where you'll go to look up "how do I do X?" without being walked
through the surrounding context.
+413
View File
@@ -0,0 +1,413 @@
# Advertise Your Node on Nostr
After
[resolve-peers-via-nostr](resolve-peers-via-nostr.md) your
daemon can look up a peer's current endpoint by npub. This
tutorial flips it around: you publish a signed advert listing
your own endpoint(s), so any other operator who knows your
npub can dial you the same way you dialed `test-us01`.
The whole exercise should take about ten minutes if you have
a public IP or full-cone home NAT. A short final section
covers the alternative path for symmetric-NAT networks.
## What you'll build
```text
┌───────────────────────┐
│ your fips daemon │
│ persistent npub │
└──────────┬────────────┘
│ signed advert (Kind 37195)
│ { udp:<your-public-ip>:2121, ... }
│ refreshes every 30 min
┌──────────────────────────────────────────┐
│ Nostr relays │
│ relay.damus.io / nos.lol / offchain.pub│
└──────────────────────┬───────────────────┘
│ "what's <your-npub>'s endpoint?"
┌───────────┴───────────┐
│ another fips daemon │
│ knows your npub, │
│ via_nostr: true │
└───────────────────────┘
```
You will change two things in `/etc/fips/fips.yaml`:
- Flip `discovery.nostr.advertise` from `false` to `true`.
- Add `advertise_on_nostr: true` and `public: true` under
`transports.udp`.
After restart, your daemon publishes a Kind 37195 event tied
to your npub, listing the UDP endpoint other peers should
dial.
## How advertising works
> **Adverts are signed Nostr events.** Every advert is a Kind
> 37195 event signed by your daemon's secret key. Anyone
> reading it can verify the advert really came from the npub
> claiming the endpoint. The advert is the `(npub → current
> endpoints)` mapping, signed and published.
The advert lists transports the daemon is willing to expose,
and only those:
> **Endpoints are opt-in per transport.** Only transports
> with `advertise_on_nostr: true` are listed in your advert.
> Transports without that flag stay private — they still
> work for peers who reach you via static config, but they
> won't appear in your published advert.
For UDP specifically, the daemon needs to know what IP and
port to put in the advert:
> **Determining the advertised endpoint (wildcard-bound UDP).**
> With UDP bound to a wildcard like `0.0.0.0:2121`, the daemon
> doesn't know its own public IP at startup. You have two ways
> to tell it what to put in the advert:
>
> - `public: true` — daemon does a one-shot STUN observation
> against the configured STUN servers and uses the reflexive
> IPv4 it learns. Right when your public IP is dynamic or
> you'd rather not pin it in config. Works for nodes with a
> directly-bound public IP and for nodes behind full-cone
> NAT (most home routers).
> - `external_addr: "<ip>[:<port>]"` — explicit override.
> Right when you already know your public IP — a static
> residential IP, an Elastic IP behind 1:1 NAT, a cloud
> instance whose advertised port differs from the bind
> port — and you don't want to depend on STUN reachability.
> Required for TCP on cloud setups where binding directly
> to the public IP returns `EADDRNOTAVAIL`.
>
> If you bind UDP to a specific public IP rather than
> `0.0.0.0`, neither flag is needed — the daemon advertises
> whatever it's bound to.
Adverts don't sit on the relays forever:
> **TTL and refresh.** Adverts have a 1-hour expiration
> (NIP-40 `expiration` tag) and the daemon re-publishes every
> 30 minutes. If your daemon goes offline, your advert decays
> from caches in roughly an hour and consumers stop trying.
## Step 1: Confirm your starting state
You should be coming out of
[resolve-peers-via-nostr](resolve-peers-via-nostr.md) with:
- A persistent npub (`fipsctl show status | grep '"npub"'`).
- Nostr discovery in consume-only mode
(`discovery.nostr.enabled: true`,
`discovery.nostr.advertise: false`).
- A peer entry for `test-us01` with `via_nostr: true` and no
static address. `fipsctl show peers` shows the link
established.
If any of those isn't true, finish the previous tutorials
first.
Capture your npub now — you'll need it for the verification
step:
```sh
sudo fipsctl show status | grep '"npub"'
```
Copy the value.
## Step 2: Enable advertising in the config
Open `/etc/fips/fips.yaml` and change two things.
**Change 1: flip `advertise` to `true`.** Find the
`discovery.nostr` block under `node:` and set:
```yaml
node:
identity:
persistent: true
discovery:
nostr:
enabled: true
advertise: true
```
(The previous tutorial set `advertise: false`; you're flipping
that bit now.)
**Change 2: add the UDP advert flags.** Find the `udp:` block
under `transports:`. The wildcard-bind default
(`0.0.0.0:2121`) means the daemon needs help knowing what to
advertise — pick one of the two approaches from the callout
above.
If you want STUN auto-discovery (works for full-cone NATs and
nodes with a directly-bound public IP):
```yaml
transports:
udp:
bind_addr: "0.0.0.0:2121"
advertise_on_nostr: true
public: true
```
If you already know your public IP (e.g., a static residential
IP or a cloud Elastic IP behind 1:1 NAT) and want to skip the
STUN dependency:
```yaml
transports:
udp:
bind_addr: "0.0.0.0:2121"
advertise_on_nostr: true
external_addr: "203.0.113.45:2121"
```
Replace `203.0.113.45:2121` with your actual public IP and
port. The bare-IP form `external_addr: "203.0.113.45"` is also
accepted; the daemon combines it with the bind port. You may
set both `public: true` and `external_addr` together — the
explicit override wins, with STUN as a logging cross-check.
`advertise_on_nostr: true` is the bit that says "include this
transport in my published advert" — common to both paths.
Save the file.
## Step 3: Restart the daemon
```sh
sudo systemctl restart fips
sudo systemctl status fips
```
Status should show `active (running)`. Within a few seconds the
daemon will:
1. Run a one-shot STUN observation against the default STUN
servers to learn its public IP.
2. Build a Kind 37195 advert listing
`udp:<public-ip>:2121` (and any other transports you have
`advertise_on_nostr: true` on).
3. Sign the advert with the daemon's nsec.
4. Publish it to the three default advert relays.
5. Schedule a refresh every 30 minutes.
If STUN fails (for example, if the network blocks outbound
UDP/3478), the daemon emits a WARN line in the journal and
suppresses the UDP entry from the advert rather than publishing
a wrong address. The link to `test-us01` from the previous
tutorial keeps working regardless — only the publish side is
gated on STUN.
Quick sanity check on the journal:
```sh
sudo journalctl -u fips -n 200 | grep -iE 'STUN|advert|warn' | head -20
```
If you see `WARN` lines mentioning STUN or wildcard-bind
fallthrough, jump to [Troubleshooting](#troubleshooting); the
rest of the tutorial assumes the publish succeeded.
## Step 4: Verify your advert is on the network
The advert is a public Nostr event — anyone, including you,
can fetch it. With the `nak` Nostr CLI installed, query the
relays for adverts published by your npub:
```sh
nak req -k 37195 -d "fips-overlay-v1" \
-a $(nak decode <your-npub> | jq -r .pubkey) \
--limit 1 wss://relay.damus.io
```
Replace `<your-npub>` with the npub you copied in Step 1. The
inner `nak decode` converts your bech32 npub to the hex pubkey
the relay filter expects.
Expect one event back. The interesting fields:
- `pubkey` — your npub in hex form.
- `tags` — includes `["d","fips-overlay-v1"]` (the namespace),
`["protocol","fips-overlay-v1"]`, and an `["expiration", …]`
tag set ~1 hour in the future.
- `content` — JSON listing the `endpoints` array. You should
see one entry like:
```json
{"transport":"udp","addr":"<your-public-ip>:2121"}
```
That `<your-public-ip>` is what STUN learned. Confirm it
matches what you'd expect for your network — for a home node,
it should be your residential IP, not a `192.168.x.x` LAN
address.
## Step 5: Watch for inbound connections
Your advert is now consumable by any FIPS daemon running open
discovery on the same `fips-overlay-v1` namespace. The public
test mesh nodes do exactly this — they subscribe to all
adverts in the namespace and try to dial new publishers.
Within a minute or two of restart, run:
```sh
sudo fipsctl show peers
```
In addition to your configured `test-us01` peer, you may see
an entry for `test-us03` (the open-discovery test mesh node).
It will have `connectivity` active and its own
`transport_addr`. This peering appeared without you
configuring anything — the test-mesh open-discovery node saw
your advert, dialed the endpoint, and Noise IK established
the link.
If no inbound peers appear, that's not necessarily a failure
of advertising — it just means no one has consumed your advert
*and* dialed back yet. The advert is on the relays regardless,
verifiable in Step 4.
## What you've learned
- **Adverts are publish + sign.** Every running FIPS daemon
with `advertise: true` publishes a signed advert; reading it
is one Nostr event lookup.
- **Endpoint inclusion is per-transport.** Only the transports
you set `advertise_on_nostr: true` on appear in the advert.
- **`public: true` invokes STUN.** Wildcard-bound UDP with
`public: true` runs a one-shot STUN observation to learn
its public IP.
- **Refresh is automatic.** Adverts re-publish every 30
minutes; consumers cache them with a 1-hour staleness
bound.
- **The publish side stands alone.** Once your advert is on
the relays, peers can dial you whether you're advertising
to them specifically or not. The test mesh's open-discovery
nodes will pick you up automatically.
## If you're behind symmetric NAT
`public: true` + STUN works on most home and office NATs (the
full-cone variety) and on nodes with a directly-bound public
IP. It does *not* work on symmetric NAT, where the NAT mapping
is keyed on (source-port, destination-host) so the IP/port
your STUN server saw isn't the IP/port a different peer would
see.
For symmetric-NAT networks the alternative is `udp:nat` mode,
which advertises a placeholder `udp:nat` endpoint along with
the daemon's signaling-relay and STUN-server lists, and
performs UDP hole-punching at dial time. Both sides need to be
running matching configs and at least one side needs a
non-symmetric NAT for the punch to succeed; symmetric on both
sides is not reliably traversable and will time out.
The minimal config switch:
```yaml
transports:
udp:
bind_addr: "0.0.0.0:2121"
advertise_on_nostr: true
public: false # ← was true; change to false
```
And add the signaling/STUN block under `discovery.nostr`:
```yaml
discovery:
nostr:
enabled: true
advertise: true
dm_relays:
- "wss://relay.damus.io"
- "wss://nos.lol"
stun_servers:
- "stun:stun.l.google.com:19302"
- "stun:stun.cloudflare.com:3478"
```
For the full setup including peer-side config and the punch-
duration knob, see
[../how-to/enable-nostr-discovery.md § Capability 2c](../how-to/enable-nostr-discovery.md#sub-scenario-2c-udp-hole-punching-for-nodes-behind-nat).
Separately from NAT considerations, FIPS supports running a
node behind a Tor onion service as a deployment shape in its
own right — chosen for the privacy, anonymity, and
censorship-resistance properties it brings, not as a fallback
when UDP or TCP fail. If those properties are an independent
goal for your node, see
[../how-to/enable-nostr-discovery.md § Sub-scenario 2b](../how-to/enable-nostr-discovery.md#sub-scenario-2b-tor-onion-node)
and
[../how-to/deploy-tor-onion.md](../how-to/deploy-tor-onion.md).
## Troubleshooting
If your advert doesn't appear on the relays:
- **STUN failed.** Check the journal for WARN lines mentioning
STUN or wildcard-bind. The most common causes are outbound
UDP/3478 blocked or DNS for `stun.l.google.com` failing.
Try: `dig stun.l.google.com` and
`nc -uvz stun.l.google.com 19302` to verify reachability.
- **Wrong public IP advertised.** If `nak` shows your advert
with a non-public address (e.g., `10.x.x.x` or
`192.168.x.x`), STUN didn't see your real public IP — likely
you're behind a CGNAT that NATs your STUN traffic too, or a
corporate firewall that proxies it. Switch to the
`external_addr` form from Step 2 with your actual public
IP, or replace `public: true` with the bound interface IP
directly under `bind_addr`.
- **Relay reachability.** `nak req` against a relay you can
reach but no events return — possibly the publish failed
silently because the daemon couldn't connect to that
specific relay. Try the other two:
```sh
nak req ... wss://nos.lol
nak req ... wss://offchain.pub
```
- **`advertise_on_nostr` typo.** YAML is case-sensitive and
silently ignores unknown keys. If `nak` returns no advert at
all, double-check the spelling on the UDP block and that
`discovery.nostr.advertise: true` is also set.
## What's next
- **Open discovery.**
[open-discovery](open-discovery.md) flips the consume side
symmetric — switch your daemon to `policy: open` and watch
your peer list populate from the ambient
`fips-overlay-v1` namespace, the same mechanism
`test-us03` is using right now to find you.
- **Host a service of your own.**
[host-a-service](host-a-service.md) walks through bringing up
an HTTP server addressable as `<your-npub>.fips`, the same
way the connecting node now reaches `test-us01`. The natural
follow-on now that other operators can dial you by npub.
For the operator-style scenario reference covering all five
shapes of Nostr discovery side-by-side (consume-only,
publish-direct, publish-Tor, NAT traversal, open):
- [../how-to/enable-nostr-discovery.md](../how-to/enable-nostr-discovery.md)
For the wire-format and discovery design:
- [../reference/nostr-events.md](../reference/nostr-events.md)
— Kind 37195 advert format, Kind 21059 traversal signaling.
- [../design/fips-nostr-discovery.md](../design/fips-nostr-discovery.md)
— discovery runtime design, security and threat model.
+468
View File
@@ -0,0 +1,468 @@
# Deploy a `fips-gateway` on an OpenWrt AP
In every other tutorial in this set you put FIPS *on* the host that
needs to talk to the mesh. This one is the exception. Here you stand
up a `fips-gateway` on an OpenWrt access point so the unmodified LAN
behind it — phones, laptops, smart-home gear — can reach mesh
destinations by `<npub>.fips` without any FIPS software of their own,
and so a service running on a LAN box can be exposed to mesh peers
through a port forward. Two halves, one binary, one config.
This is **advanced** material. It assumes you have already worked
through [join-the-test-mesh](join-the-test-mesh.md) on some other
machine, you understand what `<npub>.fips` means, and now you want to
fold an existing LAN into the mesh from the edge router rather than
installing FIPS on every device. The whole exercise should take about
forty-five minutes.
## What you'll build
```text
┌──────────────────────────────────────────────────────────────┐
│ OpenWrt access point │
│ │
│ br-lan ┌──────────────┐ fips0 ┌──────────────┐ │
│ (LAN side) ◀──│ fips-gateway │──────────▶│ fips daemon │ │
│ │ (service) │ │ │──┼─▶ mesh
│ │ │ fd97:.. │ fd97:.. │ │
│ │ fd01::/112 │ │ │ │
│ │ pool │ │ │ │
│ └──────┬───────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ nftables NAT │
│ (inet fips_gateway) │
└─────────────────┬────────────────────────────────────────────┘
│ br-lan
┌──────────────────────────────────────────────┐
│ LAN clients (phones, laptops, smart-home) │
│ │
│ no FIPS install — just IPv6 + DNS │
│ ▲ │
│ │ curl http://test-us01.fips/ │
│ └────── DNS to dnsmasq ──▶ gateway DNS │
│ │
└──────────────────────────────────────────────┘
```
By the end you will have:
- An OpenWrt AP whose LAN clients can fetch `http://test-us01.fips/`
with no FIPS software installed on them.
- An inbound port forward exposing one LAN service to the mesh as
`<your-gateway-npub>.fips:<port>`.
- An understanding of which LAN-side glue OpenWrt automates for you
(DNS forwarding, RA route, IPv6 prefix) and which the operator owns
(port forwards, mesh firewall).
## Why an OpenWrt AP
The gateway has very specific dependencies on the box it runs on. It
needs to own DNS for the LAN, it needs to advertise an IPv6 route to
the LAN, it needs a stable LAN-side interface, and it needs to be the
default IPv6 router for the segment. An OpenWrt-based access point
already does all of those things — it runs `dnsmasq`, it runs
`odhcpd` for IPv6 RA, it owns `br-lan`, and clients are already using
it as their gateway. The OpenWrt ipk leans into that: the `gateway:`
block in `/etc/fips/fips.yaml` is pre-populated, and the
`/etc/init.d/fips-gateway` init script wires up the LAN-side glue
automatically when you start the service.
On a non-OpenWrt host the same integration is manual; that path is
covered by [../how-to/deploy-gateway.md](../how-to/deploy-gateway.md).
## Prerequisites
- An OpenWrt 22.03+ AP serving DHCP and DNS to a wired or wireless
LAN segment.
- The FIPS ipk installed and a working FIPS daemon on the AP. If you
haven't done that yet, follow
[../../packaging/openwrt-ipk/README.md](../../packaging/openwrt-ipk/README.md)
for the install, then come back here.
- The AP joined to the mesh — at least one healthy peer link. If it
isn't, work through [join-the-test-mesh](join-the-test-mesh.md) on
the AP first.
- Root SSH to the AP. Every command in this tutorial runs on the AP.
- A LAN client (phone, laptop) to test the outbound half from.
## Step 1: Verify the FIPS daemon is up
Confirm the daemon is running, the TUN is up, and at least one mesh
peer is reachable:
```sh
service fips status
ip -6 addr show fips0
fipsctl show peers
```
You should see:
- `service fips status` reports `running`.
- `fips0` exists and has one `inet6 fd97:...` address. That is the
AP's mesh-side identity.
- `fipsctl show peers` lists at least one peer with active
connectivity (not `idle` / not zero bytes).
Confirm the AP can resolve a known mesh node by name:
```sh
ping6 -c 2 test-us01.fips
```
If any of these fail, fix the daemon side first — the gateway is a
separate service that runs alongside a working daemon, not a
substitute for one.
## Step 2: Inspect the pre-populated gateway config
The OpenWrt ipk ships `/etc/fips/fips.yaml` with the `gateway:` block
already filled in. View the relevant section:
```sh
sed -n '/^gateway:/,$p' /etc/fips/fips.yaml
```
You will see roughly:
```yaml
gateway:
enabled: true
pool: "fd01::/112" # virtual IP range (up to 65535 addresses)
lan_interface: "br-lan" # LAN-facing interface for proxy NDP
dns:
listen: "[::1]:5353" # gateway DNS listener (dnsmasq forwards here)
upstream: "[::1]:5354" # FIPS daemon DNS resolver (matches daemon default)
ttl: 60 # DNS TTL and mapping lifetime (seconds)
pool_grace_period: 60 # seconds after last session before reclaiming
```
Three things to notice:
- `pool: "fd01::/112"` — the virtual-IP CIDR the gateway hands out
to LAN clients. 65 536 addresses, the gateway's hard cap. Pick a
different `fdXX::/N` prefix if `fd01::/112` collides with anything
on your network.
- `lan_interface: "br-lan"` — the OpenWrt LAN bridge. The gateway
installs proxy-NDP entries on this interface so LAN clients can
ARP-equivalent for pool addresses.
- `dns.listen: "[::1]:5353"` — the gateway's DNS listener is bound
to IPv6 loopback only. dnsmasq, which owns LAN port 53, forwards
`.fips` queries to it. The init script wires up that forwarding;
you don't bind to a LAN address yourself.
For the full reference, see
[../reference/configuration.md § Gateway](../reference/configuration.md#gateway-gateway).
## Step 3: Enable and start fips-gateway
The service is shipped disabled — enable it once and start it:
```sh
service fips-gateway enable
service fips-gateway start
```
Behind that single command, the init script
(`/etc/init.d/fips-gateway`) does five things:
1. **Loads gateway sysctls.** `net.ipv6.conf.all.proxy_ndp=1` and
`net.ipv6.conf.all.forwarding=1` from
`/etc/sysctl.d/fips-gateway.conf`.
2. **Reconfigures dnsmasq via UCI** so `.fips` queries arriving at
the LAN's port 53 are forwarded to the gateway's loopback
listener on port 5353 instead of going straight to the daemon's
resolver on port 5354. (Dnsmasq still owns 53; the gateway sits
in front of the daemon for `.fips` only.)
3. **Adds a global-scope IPv6 prefix** to `br-lan`. Without a
non-ULA address on the local interface, Android and Chrome
suppress AAAA queries entirely — they assume the LAN has no
real IPv6 and don't bother. The init script adds a small
benchmarking-range prefix to convince them otherwise.
4. **Adds an RA route for the virtual pool.** A UCI `route6` entry
under `dhcp` tells `odhcpd` to advertise the pool CIDR via Router
Advertisement (RFC 4191), so LAN clients learn how to reach pool
addresses automatically. No per-client static routes needed.
5. **Spawns `fips-gateway` under procd** with `--config
/etc/fips/fips.yaml`, with crash-respawn.
Verify it is running:
```sh
service fips-gateway status
logread | grep fips-gateway | tail
```
Expect a `running` status and a startup log line of the form
`fips-gateway 0.x.y starting`, followed by entries for DNS bind, NAT
table install, and pool initialisation.
> **What just changed on the LAN.** The AP is now offering two
> things it wasn't offering a moment ago: AAAA records under `.fips`
> that resolve to virtual IPs in `fd01::/112`, and a route to that
> CIDR in its Router Advertisements. Existing LAN clients pick both
> up the next time they re-resolve a name and the next time `odhcpd`
> sends an RA, respectively. No reboot required on the client side.
## Step 4: Test the outbound half from a LAN client
From a phone or laptop on the AP's LAN — anything that does IPv6 and
DNS, with no FIPS software installed — try one of the public test
mesh nodes:
```sh
dig test-us01.fips AAAA
ping6 -c 4 test-us01.fips
curl -6 http://test-us01.fips/
```
Expectations:
- `dig` returns an AAAA in `fd01::...`, **not** `fd97:...`. The
`fd01:` address is the gateway's virtual-IP allocation; the LAN
client never sees the raw mesh address.
- `ping6` succeeds. ICMPv6 echo travels through the NAT pipeline and
back.
- `curl` fetches the page (whatever the test mesh is currently
serving on `test-us01`).
> **What just happened end to end.** Your client asked dnsmasq for
> `test-us01.fips`. Dnsmasq forwarded the query to the gateway's
> loopback listener on port 5353. The gateway forwarded the query on
> to the daemon's resolver on port 5354. The daemon answered with
> `test-us01`'s mesh address (`fd97:...`). The gateway allocated a
> virtual IP from `fd01::/112`, installed nftables DNAT/SNAT/
> masquerade rules pinning that virtual IP to the mesh address,
> installed a proxy-NDP entry on `br-lan` so the client could resolve
> the virtual IP at the link layer, and returned the virtual IP in
> the AAAA reply. Your client then routed traffic to the virtual IP
> via the RA-advertised pool route, the AP's kernel rewrote the
> destination to the mesh address, and the daemon's adapter carried
> the packets across the mesh. Return traffic followed conntrack
> back. The client never knew the mesh existed.
## Step 5 (optional): Inspect the gateway state
The gateway exposes its own control socket separate from the daemon's.
Two useful queries:
```sh
echo '{"command":"show_gateway"}' | nc -U /run/fips/gateway.sock
echo '{"command":"show_mappings"}' | nc -U /run/fips/gateway.sock
```
`show_gateway` reports pool utilisation, the DNS listen address,
uptime, and the conntrack/NAT counters. `show_mappings` lists each
allocated virtual IP, the mesh address it points at, the DNS name
that triggered the allocation, and the mapping's lifecycle state
(`Allocated`, `Active`, `Draining`).
For the full command catalog and JSON shapes, see
[../reference/control-socket.md § Gateway Command Catalog](../reference/control-socket.md#gateway-command-catalog).
The same data is rendered visually in the **Gateway** tab of
[`fipstop`](../reference/cli-fipstop.md).
If you want to see the kernel rules the gateway installed:
```sh
nft list table inet fips_gateway
```
You will see DNAT, SNAT, and masquerade chains populated with one
rule per active mapping.
## Step 6: Add an inbound port-forward for a LAN service
The outbound half is the steady-state use of a gateway. The inbound
half — exposing a LAN service to mesh peers — is a separate decision,
configured per service under `gateway.port_forwards[]`.
For the worked example, run a one-page static web server on the AP
itself, bound to its `br-lan` address, and expose it to the mesh
through a port-forward. Anything would do — the point of the exercise
is the port-forward, not the service. We use what is already on the
AP: `busybox httpd`. In a real deployment the LAN-side target would
typically be a separate host (a NAS, a home server, a dev box on the
LAN); the rule shape is identical.
Find the AP's `br-lan` IPv6 address and save it for the rest of the
step:
```sh
BR_LAN_ADDR=$(ip -6 addr show br-lan \
| awk '/inet6 fd|inet6 2/ && !/scope link/ {print $2}' \
| head -1 | cut -d/ -f1)
echo "$BR_LAN_ADDR"
```
Pick from the global-scope benchmarking prefix the init script added
in Step 3, or your own ULA if `br-lan` has one — anything except a
link-local `fe80::/10` address.
Set up a one-file docroot and start a foreground `busybox httpd`
bound to that LAN address on port 8000:
```sh
mkdir -p /tmp/mesh-demo
echo '<h1>Hello from the mesh-gateway demo</h1>' > /tmp/mesh-demo/index.html
busybox httpd -f -p "[${BR_LAN_ADDR}]:8000" -h /tmp/mesh-demo
```
Leave it running in this shell. Open a second SSH session on the AP
to add the port-forward.
Edit `/etc/fips/fips.yaml`. Inside the existing `gateway:` block,
add a `port_forwards:` list:
```yaml
gateway:
enabled: true
pool: "fd01::/112"
lan_interface: "br-lan"
dns:
listen: "[::1]:5353"
upstream: "[::1]:5354"
ttl: 60
pool_grace_period: 60
port_forwards:
- listen_port: 8080
proto: tcp
target: "[<BR_LAN_ADDR>]:8000"
```
Substitute the real address for `<BR_LAN_ADDR>`. The IPv6 form
(`[addr]:port`) is required — IPv4 targets are rejected at config
load.
Restart the gateway so it re-reads the config:
```sh
service fips-gateway restart
```
From any *other* mesh node, fetch the demo page through the gateway
using the AP's npub:
```sh
# on the AP, get the npub:
NPUB=$(cat /etc/fips/fips.pub)
echo "$NPUB"
```
Then on the remote mesh node:
```sh
curl -6 "http://${NPUB}.fips:8080/"
```
Expect:
```text
<h1>Hello from the mesh-gateway demo</h1>
```
The connection landed on the gateway's `fips0` ingress on TCP/8080,
nftables DNAT rewrote the destination to `[BR_LAN_ADDR]:8000`,
LAN-side masquerade rewrote the source so `busybox httpd` saw a
LAN-routable address, and the response retraced via conntrack.
> **What you exposed.** With the port-forward active, every mesh
> peer that can route to your AP can hit `${NPUB}.fips:8080/` and
> reach this service. That is exactly what the inbound half is
> for — but if you want to scope visibility to a specific subset
> of peers, the FIPS mesh firewall is the layer that does it; see
> [../how-to/enable-mesh-firewall.md](../how-to/enable-mesh-firewall.md).
> The port-forward rule and the firewall rule are independent: the
> port-forward installs the rewrite; the firewall decides who is
> allowed to reach the listen port.
## Step 7: Tidy up
In the first shell, stop `busybox httpd` with `Ctrl-C`. The demo
docroot at `/tmp/mesh-demo` can stay — it is wiped on reboot — or
remove it now (`rm -rf /tmp/mesh-demo`).
If you want to keep the outbound half but withdraw the inbound
forward, remove the `port_forwards:` entry from `/etc/fips/fips.yaml`
and `service fips-gateway restart`. The mesh-side listener disappears
and so does the corresponding nftables rule.
To turn the gateway off entirely:
```sh
service fips-gateway stop
service fips-gateway disable
```
The init script's `stop_service` handler reverses the LAN-side
integration on the way out: dnsmasq's `.fips` forwarder is pointed
back at the daemon's port 5354, the RA route for the pool is
withdrawn from `odhcpd`, and the global-scope IPv6 prefix on
`br-lan` is removed. The LAN reverts to the state it was in before
you ran `service fips-gateway start` in Step 3.
The daemon and the rest of `/etc/fips/` are untouched. Existing mesh
peering on the AP itself continues to work.
## What you've learned
- **The gateway is a niche feature for a niche box.** Most FIPS
hosts run the daemon and reach the mesh directly. The gateway
exists so an AP can fold an entire unmodified LAN behind it into
the mesh in one place.
- **Two halves of the same binary.** Outbound mode hands LAN clients
virtual IPs and NATs them onto the mesh; inbound mode listens on
`fips0` and forwards to LAN targets. They share one nftables
table, one control socket, and one config block, but each half
has its own use case.
- **OpenWrt does the LAN-side glue for you.** The init script
reconfigures dnsmasq, installs the RA route, adds the global IPv6
prefix, and loads sysctls. On a non-OpenWrt host that integration
is manual — see [../how-to/deploy-gateway.md](../how-to/deploy-gateway.md).
- **Inbound forwards stay manual on every distro.** The
`port_forwards[]` block is uniform across hosts, and on every
distro you still own the decision of which LAN target to expose
and on which mesh-side port.
- **The mesh firewall is a separate decision.** Opening a port
forward on the gateway side does not open it on the firewall
side; if `fips-firewall.service` is enabled, you still need a
drop-in that admits the listen port.
## Troubleshooting
If something doesn't work as described above, the operator-recipe
guide [../how-to/troubleshoot-gateway.md](../how-to/troubleshoot-gateway.md)
groups the common failures by symptom:
| Symptom | Where to look |
| ------- | ------------- |
| LAN client gets `fd97:...`, not `fd01:...` | DNS path: dnsmasq still pointing at port 5354. See "DNS queries fail". |
| `dig` succeeds with a pool address but `ping6` times out | Pool route or proxy NDP. See "Virtual IP unreachable from client". |
| `ping6` works but TCP times out | NAT pipeline or mesh-side firewall. See "Ping works but TCP does not". |
| Gateway service won't start | "No gateway section in configuration" recipe. |
| Inbound `curl` hits the listen port but never reaches the LAN target | Mesh-side firewall first, then the port-forward rule. |
The first thing the troubleshoot guide does in any of these cases is
ask the gateway directly via `show_gateway` and `show_mappings`. If
the mapping you expect is not there, the failure is on the DNS path;
if it is there in `state: Active` but traffic still fails, the
failure is downstream.
## What's next
- [../how-to/deploy-gateway.md](../how-to/deploy-gateway.md) —
Manual deployment on a non-OpenWrt Linux host. Same gateway,
same config, but you wire up dnsmasq/Unbound/etc. yourself,
install a pool route per LAN client (or via your own RA daemon),
and manage the systemd unit instead of the procd init script.
- [../design/fips-gateway.md](../design/fips-gateway.md) — The
design doc: NAT pipeline (DNAT, SNAT, masquerade, inbound DNAT),
virtual-IP pool lifecycle (Allocated -> Active -> Draining ->
reclaimed), DNS resolution flow, conntrack integration.
- [../reference/configuration.md § Gateway](../reference/configuration.md#gateway-gateway)
— Every field of the `gateway:` block, including the conntrack
timeout overrides not used in this tutorial.
- [../reference/cli-fips-gateway.md](../reference/cli-fips-gateway.md)
— The `fips-gateway` binary's CLI options, exit codes, and
environment variables.
+518
View File
@@ -0,0 +1,518 @@
# Build a Mesh from the Ground Up
The earlier tutorials in this progression rode existing IP — your
daemon reached `test-us01` over the public internet through your
ISP, your ISP's upstream, and however many hops separate you from
the test node. That is the *overlay* deployment mode of FIPS:
useful, but not the new ground.
This tutorial is about the other mode. Two devices, a wire (or a
radio link) between them, no IP between them, and FIPS daemons on
each end. The two daemons discover each other over the raw link,
peer over Noise, and bring up an end-to-end mesh with addressing,
naming, and reachability — all from layer 2 up. There is no DHCP,
no router, no upstream. The mesh is the network.
This is the deployment mode FIPS was designed for. Overlay mode
exists because riding existing IP is a useful convenience; the
ground-up mode is what FIPS uniquely enables.
> **The two modes are not exclusive.** A node can carry overlay
> peers and ground-up peers at the same time — different transports
> on the same daemon. If you have already worked through
> [join-the-test-mesh](join-the-test-mesh.md), the static peer to
> `test-us01` you configured there can stay in place; the Ethernet
> peer you add in this tutorial sits alongside it. Traffic flows
> through whichever path is shortest by mesh metric, and a node on
> one side can reach a node on the other through your machine
> acting as a bridge between the two.
## What you'll build
```text
┌──────────────────────┐ raw Ethernet frames ┌──────────────────────┐
│ node A │ ─────────────────────── │ node B │
│ npub1aaa… │ EtherType 0x2121 │ npub1bbb… │
│ fips0 fd97:..:A │ no IP between them │ fips0 fd97:..:B │
└──────────────────────┘ └──────────────────────┘
│ │
│ a single Ethernet cable │
│ (or both NICs on the same │
│ unmanaged switch — no DHCP, │
│ no router, no IP at all) │
└─────────────────────────────────────────────────┘
```
Two machines, each running `fips`, joined by a physical Ethernet
link. After the worked example:
- The two daemons have discovered each other via L2 beacons on
the link, peered over Noise IK, and brought up an FMP link.
- Each `fips0` adapter has a routable mesh address; each can
ping the other by `<npub>.fips`.
- Nothing between the two machines speaks IP. The link carries
raw FIPS frames at EtherType `0x2121`.
The whole exercise should take about twenty minutes if you have
the hardware ready.
## Why ground-up
Most networking tutorials assume IP is already there: an address
arrived from DHCP, a default gateway routes you onward, DNS
resolves names. FIPS does not need any of that. Two devices and
a way to deliver bytes between them at layer 2 is enough — FIPS
supplies the rest:
- **Identity**: each daemon has an npub (the same kind you saw
in the overlay tutorials). Nothing in the ground-up case
depends on a network identity from a router; the npub is the
identity.
- **Addressing**: the `fips0` adapter takes an `fd97:...` ULA
derived from the npub. No DHCP. No SLAAC. The address is
cryptographically tied to the identity.
- **Discovery**: each daemon broadcasts a small beacon on the
link advertising its npub; the other daemon's listener picks
it up and dials in over the same link.
- **Routing**: the FIPS mesh layer builds its own spanning tree
across whatever links it has. Add a third node (peered to
either A or B) and traffic reaches it transparently.
The point is not that ground-up replaces overlay. It's that
overlay is one of two modes the same daemon supports, and
ground-up is what unlocks the use cases overlay cannot —
ad-hoc local meshes, partitioned networks, situations where
no IP infrastructure exists or can be relied on.
## Prerequisites
Two devices (call them **node A** and **node B**) and a way to
join them at layer 2:
- Ethernet (the worked example): a direct cable between two
modern NICs (auto-MDI/MDIX handles crossover for you), or
both machines on a small unmanaged switch with no DHCP
server. USB-Ethernet dongles work; a typical "USB-to-RJ45"
adapter is fine on either end. The link does **not** need
to be the machine's primary network interface — a second
NIC dedicated to the mesh is the cleanest setup.
- WiFi (a one-line variation, covered later): both machines
associated to a common AP that has client (station)
isolation **off**.
- Bluetooth LE (a separate worked example via a how-to,
covered later): two BLE-capable Linux hosts within roughly
10 metres line of sight.
On both nodes:
- `fips` installed and running, per [getting-started](../getting-started.md).
- A persistent identity from
[persistent-identity](persistent-identity.md). Ephemeral
identities work, but on each restart the npub regenerates
and you'll have to re-check `fipsctl show peers` to see the
new identity. Persistent makes the lesson stick.
- The daemon running with `CAP_NET_RAW` (the shipped systemd
unit runs as root and gets this for free; running
interactively from a user account requires `setcap`
noted at the relevant step below).
You do **not** need:
- An IP address on the chosen interface. The Ethernet
transport opens a raw socket directly; the kernel does not
need to assign an IP to the NIC.
- A default route. The mesh routes itself.
- DNS resolution between the machines via any external
service. The local `.fips` resolver supplies names from
the npubs the daemons exchange.
## Step 1: Identify the link interface on each node
On each node, list the network interfaces and pick the one that
sits on the link between the two machines. If it's a dedicated
NIC for the mesh, that NIC has no other purpose; if it's a
USB-Ethernet dongle, plug it in first so the kernel names it.
```sh
ip link show
```
Pick out the interface name. Common forms:
- `enp3s0`, `eno1` — built-in NICs under predictable naming.
- `eth0` — older or container-style naming.
- `enxAABBCCDDEEFF` — USB-Ethernet dongles often appear under
this MAC-derived form.
Bring the interface up if it isn't:
```sh
sudo ip link set dev <interface> up
```
Confirm:
```sh
ip -br link show <interface>
```
You want `UP` and `LOWER_UP` in the flags. The interface does
not need an IP address — `LOWER_UP` indicates the NIC sees
carrier (cable plugged into something at the other end), and
that is all the Ethernet transport needs.
For the rest of the tutorial we'll write the chosen interface
as `<eth>`. Substitute the actual name on each node when you
run the commands. Note that node A and node B may have
different interface names — that is normal.
> **No IP needed.** If your chosen interface has an address
> from a previous DHCP lease, leave it alone or remove it with
> `sudo ip addr flush dev <eth>` — the FIPS Ethernet transport
> uses raw `AF_PACKET` sockets that bypass the IP stack
> entirely. The interface needs to be `up` and `LOWER_UP`,
> nothing more.
## Step 2: Configure the Ethernet transport on each node
Edit `/etc/fips/fips.yaml` on **both** nodes. Under
`transports:`, add an `ethernet:` block. The key settings are
the four discovery flags — both nodes must opt in to all four,
and they default to off:
```yaml
transports:
ethernet:
interface: "<eth>" # the name from Step 1
announce: true # broadcast our beacon on the link
discovery: true # listen for beacons (default; shown for clarity)
auto_connect: true # dial peers we discover
accept_connections: true # accept dial-ins from peers we discover
```
Each flag does one thing:
- `announce: true` — emit a small beacon every
`beacon_interval_secs` (default 30s) carrying our npub.
- `discovery: true` — listen for incoming beacons; populate a
candidate-peer list keyed by source MAC and observed npub.
- `auto_connect: true` — when we see a beacon from an npub
we have not yet peered with, initiate the outbound Noise
handshake.
- `accept_connections: true` — when a remote npub initiates
the handshake on this transport, complete it.
If only one node sets `announce`, the other won't see it; if
only one side sets `auto_connect` or `accept_connections`, the
roles are asymmetric and the link won't establish unless both
are configured. The cleanest pattern for a ground-up tutorial
is "all four flags on both ends."
> **Multiple Ethernet links.** If a node has more than one
> physical interface that participates in the mesh, configure
> each one as a *named instance* under `ethernet:`:
>
> ```yaml
> transports:
> ethernet:
> lan:
> interface: "eth0"
> announce: true
> discovery: true
> auto_connect: true
> accept_connections: true
> dongle:
> interface: "enx00aabbccddee"
> announce: true
> # ...
> ```
>
> Each named instance runs its own socket and discovery state.
> A single ground-up link only needs the flat form shown
> first; named instances become useful when the same node
> bridges multiple physical segments.
## Step 3: Grant the daemon permission to open raw sockets
The Ethernet transport opens an `AF_PACKET` `SOCK_DGRAM` socket
bound to the chosen interface. That requires `CAP_NET_RAW`.
If you installed FIPS via the Debian package and run via the
shipped systemd unit, the daemon runs as root and has
`CAP_NET_RAW` already — there is nothing to do here. Skip to
Step 4.
If you are running the daemon interactively as your user (a
from-source / development setup), grant the capability once on
the binary:
```sh
sudo setcap CAP_NET_RAW,CAP_NET_ADMIN+ep "$(which fips)"
```
`CAP_NET_ADMIN` is what the daemon needs for the `fips0` TUN
adapter regardless; `CAP_NET_RAW` is the ground-up addition.
The `setcap` invocation only needs to be repeated when the
binary is replaced.
## Step 4: Restart the daemon on each node
```sh
sudo systemctl restart fips
```
Or, if running interactively, restart your `fips` invocation
in whichever way you started it.
Watch the startup logs for the Ethernet transport coming up:
```sh
sudo journalctl -u fips -f --since="1 minute ago"
```
Look for landmarks like:
- A line indicating the Ethernet transport opened the chosen
interface and started its receive loop.
- Periodic outbound beacon messages (one per
`beacon_interval_secs` window).
- After the second beacon round on the *other* node, an
inbound beacon parsed and a candidate-peer entry created.
- Once each side dials, a Noise handshake completion log
message naming the remote npub.
Beacon interval defaults to 30s, so the first peering can take
up to a minute (one beacon window per side, plus handshake).
Lower the interval for the tutorial if you want faster
feedback:
```yaml
transports:
ethernet:
# ...
beacon_interval_secs: 10 # minimum allowed
```
## Step 5: Verify the link
On either node:
```sh
sudo fipsctl show peers
```
Expect one entry whose `npub` matches the **other** node and
whose `addresses` line shows `transport: ethernet`. Your
existing overlay peers (if any from earlier tutorials) appear
alongside it. Each peer has its own row, and the link status
columns show whether the Noise session is up.
```sh
sudo fipsctl show transports
```
Confirms that the Ethernet transport is running and shows the
beacon counters incrementing. Both `beacons_sent` and
`beacons_received` should be non-zero if the link is healthy.
## Step 6: Reach the other node by name
On node A, ping node B by `.fips` name. Get node B's npub
from its `fipsctl show status` output (it's the persistent
identity you established earlier), then:
```sh
ping6 npub1bbb…long-string….fips
```
Expect ICMPv6 echo replies. The path is:
1. The local `.fips` resolver translates the npub-form name
into an `fd97:...` mesh address (cryptographically derived
from the npub on both ends — the resolver does the
computation locally, with no network round trip).
2. The kernel routes the packet via `fips0`.
3. The FIPS daemon accepts it from the TUN, looks up the
mesh route, and hands it to the FMP link to node B.
4. The Ethernet transport on node A frames the FMP packet as
a raw EtherType `0x2121` Ethernet frame addressed to node
B's MAC, learned from B's beacons.
5. Node B's daemon receives the frame, peels off the
Ethernet/FIPS framing, and the packet emerges on node B's
`fips0`.
6. The kernel on node B sees an inbound ICMPv6 echo and
replies, and the same path runs in reverse.
If you have a hosts file with shortnames configured (see
[host-aliases](../how-to/host-aliases.md)), substitute the
shortname for the full npub form.
## Step 7: Try a forward composition
If node A also has the `test-us01` overlay peer from
[join-the-test-mesh](join-the-test-mesh.md), node B can
reach `test-us01` *through* node A — even though node B has
no direct internet path of its own:
On node B:
```sh
ping6 npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98.fips
```
The packet leaves B's `fips0`, traverses the Ethernet link to
A, gets forwarded by A across the overlay UDP transport to
`test-us01`, and the reply comes back the same way.
This is the composition the chapter intro flagged: the two
deployment modes coexist on a single daemon. Node A is
participating in the test mesh via the internet *and* in your
local Ethernet mesh. From node B's perspective, the test mesh
is reachable. From `test-us01`'s perspective, B is reachable.
The mesh handles the rest.
## Variations
### WiFi (AP mode), same shape as Ethernet
Replace `<eth>` with the WiFi interface name (typically
`wlan0` or `wlp3s0`) on each node. The WiFi NIC is presented
as an Ethernet-class interface to the kernel by the
`mac80211` abstraction; the FIPS Ethernet transport opens
the same `AF_PACKET` socket on it. No FIPS-side configuration
change beyond the interface name.
What you do need on the AP side:
- Both nodes associated to the same SSID.
- **Client (station) isolation must be OFF** on the AP.
Most consumer routers ship with it off; many guest
networks and "secure" enterprise APs ship with it on.
When client isolation is on, the AP refuses to forward
station-to-station frames — the broadcast beacons never
arrive at the other node, and discovery fails silently.
If beacons aren't crossing, this is the first thing to
check.
There is no FIPS-specific configuration for WiFi versus
Ethernet on the daemon side; the choice is purely the
adapter name.
### Bluetooth LE (experimental but works)
BLE is a separate transport (`transports.ble.*`) with its own
discovery model — L2CAP advertisements rather than raw L2
broadcasts. The shape of the tutorial is the same (advertise +
scan + auto-connect + accept), but the prerequisites are
different: BlueZ, `bluetoothd`, an HCI adapter, and the
`bluetooth` group or capability set.
The full operator recipe is in
[../how-to/set-up-bluetooth-peer.md](../how-to/set-up-bluetooth-peer.md).
Mark this transport as experimental: it works in most
configurations but the BLE stack has more variability than
Ethernet — adapter quirks, BlueZ version differences, and the
shorter range all matter.
The BLE transport is **Linux-only** at present; macOS and
Windows builds skip it.
## What you've learned
- **Ground-up is the new ground.** FIPS does not need any IP
infrastructure between two devices to mesh them. A wire (or
a radio link), `CAP_NET_RAW`, and a few config flags on each
end are sufficient. The mesh supplies its own identity,
addressing, discovery, and routing.
- **Discovery is a four-flag opt-in.** `announce`, `discovery`,
`auto_connect`, and `accept_connections` each control one
thing; both ends must agree before a link will form.
- **The two modes coexist.** Overlay peers and ground-up peers
ride the same daemon — same FMP link layer, same FSP session
layer, same `fips0` adapter. A node can be a bridge between
the two without any extra plumbing.
- **No IP on the link.** The Ethernet transport bypasses the
kernel IP stack via `AF_PACKET`. Whether the interface has
an IP address is irrelevant; whether it has carrier is what
matters.
- **Names work the same way.** `<npub>.fips` resolves locally
via the cryptographically-derived ULA. The resolver does
not care whether the destination is reached over Ethernet,
UDP overlay, or some hop chain combining both.
## Troubleshooting
- **No beacons received.** On either node, `sudo fipsctl show
transports` should show `beacons_received` incrementing
every `beacon_interval_secs` once the other node is also
running. If it stays at zero:
- Confirm the chosen interface is `LOWER_UP` (carrier
present).
- Confirm the other node is announcing (its `beacons_sent`
should be non-zero).
- On WiFi: confirm AP client isolation is off.
- On a switch: confirm the switch is unmanaged or that
EtherType `0x2121` is not being filtered. Most consumer
switches forward all EtherTypes; managed switches
sometimes don't.
- **Beacons received but no peer entry.** The handshake is
failing. Tail logs (`journalctl -u fips`) for Noise
handshake errors. Common causes: peer ACL active and not
including the remote npub (out of scope for this tutorial,
but check `/etc/fips/peers.allow` if you have set one);
daemon's clock drift large enough to fail freshness
checks (rare).
- **Daemon won't start with the Ethernet transport.** Likely
a permissions error. Check `journalctl -u fips` for an
`EPERM` or "operation not permitted" message; if running
interactively, confirm the binary has `CAP_NET_RAW`
(`getcap "$(which fips)"`).
- **Beacons in both directions, peers entries on both sides,
but ping6 times out.** The handshake completed but the FSP
session is not flowing data. Check `fipsctl show peers`'s
link status columns — if the FMP link is healthy but FSP
is not, the mesh-layer side is fine and the issue is one
layer up. The
[reach-mesh-services § Troubleshooting](reach-mesh-services.md#troubleshooting)
section covers symptoms at this level.
- **`AF_PACKET` socket bind fails on a kernel-protected
interface.** Some hardened kernels (`grsec`, certain
containers, certain VMs) restrict raw-socket access even
with `CAP_NET_RAW`. The daemon log will name the failing
syscall. The fix is host-side: relax the restriction or
pick a different interface.
## What's next
You now have the second deployment mode of FIPS in your
hands. From here:
- **Add a third node.** Bring up a third machine on the same
Ethernet segment, configure it identically, and watch all
three nodes form a mesh. The FIPS spanning tree picks a
root and routing converges within a few beacon intervals.
- **Mix transports.** Add an overlay peer (per
[join-the-test-mesh](join-the-test-mesh.md)) to one of
your ground-up nodes; the local mesh now reaches the test
mesh through that node, and vice versa.
- **Host services.** Anything you do on `fips0` with overlay
peers — bind an HTTP server (per
[host-a-service](host-a-service.md)), reach a service via
the daemon's IPv6 adapter (per
[reach-mesh-services](reach-mesh-services.md)) — works
identically on a ground-up mesh. The data plane is the
same.
For more depth on the link-layer machinery:
- [../reference/transports.md § Ethernet](../reference/transports.md)
— full Ethernet transport reference (counter inventory,
per-instance configuration, MTU model).
- [../reference/configuration.md § Ethernet](../reference/configuration.md#ethernet-transportsethernet)
— every configuration key and its default.
- [../how-to/set-up-bluetooth-peer.md](../how-to/set-up-bluetooth-peer.md)
— operator recipe for the BLE variant.
- [../design/fips-transport-layer.md](../design/fips-transport-layer.md)
— the design doc that describes the per-link MTU model and
why each transport is treated as link-layer rather than
network-layer.
+477
View File
@@ -0,0 +1,477 @@
# 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 fd97:...` line. The address up to the `/`
is what you want.
Or via the daemon:
```sh
sudo fipsctl show identities
```
The first JSON entry has `local: true` and a `ula` 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 fd97:/ {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.
## 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.
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.
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.
- **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.
## 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#inbound-port-forwarding](../how-to/deploy-gateway.md#inbound-port-forwarding);
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).
+231
View File
@@ -0,0 +1,231 @@
# IPv6 Adapter Walkthrough
You have completed [join-the-test-mesh](join-the-test-mesh.md).
Your daemon is peered with `test-us01` and you can ping mesh
nodes by `.fips` name. This tutorial walks the plumbing that
makes that possible: what happens between the moment your shell
types `ssh user@<peer>.fips` and the moment a TCP SYN arrives at
sshd on the far side. Each step is something you can observe
with the running daemon from the previous tutorial.
By the end you will be comfortable reading `fipstop` output and
you will know which design doc to consult when something looks
off.
> **Prerequisites.** The daemon from
> [join-the-test-mesh.md](join-the-test-mesh.md) is running and
> peered with at least one test-mesh node, and your host's local
> resolver is forwarding `.fips` queries to the daemon's DNS
> responder (the system fips-dns.service drop-in does this
> automatically on systemd hosts).
## The path we're tracing
```text
shell ──ssh──> libc resolver ──.fips──> fips DNS ──AAAA──> fd97:...:test-us01
kernel IPv6 stack
fips0 (TUN)
your fips daemon
(FSP session setup,
FMP forwarding)
UDP / internet
test-us01's fips daemon
fips0 (TUN)
kernel IPv6 stack
sshd
```
In a multi-hop mesh the middle would have additional FMP
forwarders between your daemon and the destination. For this
walkthrough you have a single direct link to `test-us01`, which
keeps the trace simple.
## Step 1: Watch the DNS resolution
Ask the system resolver to translate `test-us01`'s npub into its
mesh address:
```sh
dig npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98.fips AAAA +short
```
You should see one AAAA record returning a `fd97:...` address.
The prefix is the FIPS ULA range (`fd00::/8`, with `fd97:...`
covering the address space derived from npubs).
The query went through `systemd-resolved` (or your platform
equivalent), which routed `.fips` queries to the daemon's local
responder via the drop-in installed by `fips-dns.service`. To
confirm, query the daemon directly:
```sh
dig @::1 -p 5354 npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98.fips AAAA +short
```
Same answer, same fast turnaround — no external DNS traffic in
either case.
The mapping `npub → fd00::/8 address` is deterministic. The
responder hashes the public key into 16 bytes, prepends the
prefix, and returns the result. There is no shared registry; the
address space is self-allocating from the public-key namespace.
If you ask for any non-`.fips` suffix, the responder returns
`REFUSED` — it is intentionally a stub for this single zone, not a
recursive resolver. An unknown `.fips` name returns `NXDOMAIN`.
The full DNS integration is documented in
[../design/fips-ipv6-adapter.md](../design/fips-ipv6-adapter.md).
## Step 2: Watch the session being created
Open `fipstop` against your daemon's control socket:
```sh
sudo fipstop
```
Press `Tab` until you reach the **Sessions** tab. Before any
TCP traffic to `test-us01`, the table is empty (or has rows from
earlier exchanges).
In another terminal, kick off a TCP connection from your host
toward `test-us01`:
```sh
ssh -o ConnectTimeout=5 user@test-us01.fips
```
(`test-us01.fips` resolves to the same address as the npub
form via the installer's `/etc/fips/hosts` entry.)
(It is fine if the SSH attempt fails authentication or if no
sshd is exposed on the far side — what we want to observe is the
session machinery firing, not a successful login.)
In `fipstop`'s Sessions tab you should see a new row appear with:
- `state` cycling from `initiating` to `awaiting_msg3` to
`established` (the three FSP handshake states).
- `display_name` showing `test-us01` (the `alias` you set in your
`peers:` block in the previous tutorial).
- A non-zero `last_activity_ms`.
Once `established`, the session row stays put until idle-timeout
expires. The traffic counters and MMP metrics tick as data flows.
> **Watch for.** Some intermediate states may be too fast to
> see at the default `fipstop` refresh rate of 2 s. Run
> `sudo fipstop -r 1` for a faster refresh during the exercise.
## Step 3: Watch the per-session metrics
Switch to the **Performance** tab. Each established session has
a session-layer MMP entry showing:
- `srtt_ms` — smoothed end-to-end round-trip time. Over a
public-internet path this typically lands in the tens of
milliseconds; for a US-coast destination from a US client you
might see 3080 ms steady-state.
- `loss_rate` — fraction of in-flight payloads inferred lost from
counter gaps. Stays at 0 on a healthy link; small bursts during
congestion or path changes.
- `path_mtu` — the end-to-end MTU the session-layer MMP currently
believes is in force. Starts at the IPv6 floor and climbs as
PathMtuNotification echoes arrive.
- `etx` and `goodput_bps` — derived metrics, useful as
steady-state indicators.
The same metrics are available without the TUI:
```sh
sudo fipsctl show sessions | jq '.sessions[] | {display_name, state, mmp}'
```
What these numbers mean is documented in
[../design/fips-mmp.md](../design/fips-mmp.md). Briefly: SRTT is
RFC 6298-style with α = 1/8; loss is bidirectional, inferred from
counter gaps in MMP reports; path MTU is end-to-end-echoed with
hysteresis on increase.
## Step 4: Watch the link below the session
Switch to the **Peers** tab. Each authenticated peer has its own
link-layer MMP block, distinct from the session-layer one above.
The link-layer metrics measure a single hop (here, your daemon
`test-us01` over UDP), independent of any session that
traverses it.
Compare the link-layer SRTT for `test-us01` to the session-layer
SRTT of the session you just created. Because your reach to
`test-us01` is one direct hop, the two should be very close —
the session has no transit forwarders to add latency.
If you reach a node that `test-us01` forwards to (try the
`test-us02` ping from the previous tutorial), the session-layer
SRTT for that destination will be measurably larger than the
link-layer SRTT to `test-us01`. The difference is the time
`test-us01` spent forwarding plus the hop from `test-us01` to
`test-us02`.
In a deeper mesh this divergence grows: link-layer SRTT measures
the direct neighbour, session-layer SRTT measures the full
end-to-end path.
## Step 5: Read the relevant design docs
You have now seen the moving parts. To go from "I can read these
metrics" to "I understand why each one moves the way it does":
- [../design/fips-ipv6-adapter.md](../design/fips-ipv6-adapter.md)
— DNS responder, identity cache, TUN reader/writer, IPv6 header
compression, MTU enforcement at the TUN boundary.
- [../design/fips-session-layer.md](../design/fips-session-layer.md)
— FSP session lifecycle: msg1 / msg2 / msg3, the rekey state
machine, the drain window for old sessions during cutover.
- [../design/fips-mmp.md](../design/fips-mmp.md) — both link-layer
and session-layer MMP: report format, SRTT estimation,
loss/jitter/ETX computation, the trend indicators.
- [../design/fips-mtu.md](../design/fips-mtu.md) — what `path_mtu`
in `show sessions` means: the proactive forward-path field, the
reactive `MtuExceeded` mechanism, the hysteresis on increase.
- [../design/fips-architecture.md](../design/fips-architecture.md)
— the two-layer encryption model: link-layer Noise IK over
each hop, end-to-end Noise XK over the session.
## What you've learned
- A `.fips` name resolves through a daemon-local stub responder.
The mapping from npub to `fd00::/8` address is deterministic
and needs no registry.
- The kernel IPv6 stack treats the TUN adapter as an ordinary
interface; packets to `fd00::/8` go out via that route. The
daemon reads them off the TUN, looks up an FSP session for the
destination (creating one if needed), and forwards them onward
through its peers.
- The session layer (FSP) and the link layer (FMP) each maintain
their own MMP metrics. Session-layer metrics measure the path
end-to-end; link-layer metrics measure a single hop. The two
align when the destination is your direct peer; they diverge
when traffic traverses additional hops.
- `fipstop` exposes both views in real time. `fipsctl show sessions`,
`fipsctl show peers`, and `fipsctl show transports` cover the
same ground programmatically.
When something looks off in production, the `fipsctl show *`
queries are usually the first stop; the relevant design doc tells
you what the numbers mean and what they should do.
+324
View File
@@ -0,0 +1,324 @@
# Join the FIPS Test Mesh
In this tutorial you will connect your FIPS daemon to a public
test peer over UDP, watch the link come up, and reach the peer's
mesh address from your machine. By the end you will have seen one
complete end-to-end flow — config, handshake, live link, traffic
— for a real peer somewhere out on the public internet.
The whole exercise should take about ten minutes. If you have
already worked through [getting-started.md](../getting-started.md)
and have the `fips` daemon running on your host, you have
everything you need.
## What you'll build
```text
┌────────────────────┐ UDP/IPv4 ┌──────────────────────┐
│ your fips node │ ──────────────────────── │ test-us01 │
│ ephemeral npub │ test-us01.fips.network │ npub1qmc3...zel98 │
│ fips0 fd97:..:Y │ :2121 │ fips0 fd97:..:T │
└────────────────────┘ └──────────────────────┘
```
Your daemon will peer with one of the public test nodes the
project maintains. `test-us01` has a stable DNS name, listens on
UDP/2121, and is reachable from any network that permits arbitrary
outbound UDP.
> **Peer vs. node.** In FIPS terminology, a *peer* is a node
> you have a direct link to — same Noise IK handshake, same
> transport socket. A *node* is any participant on the mesh,
> whether you peer with it directly or reach it through one or
> more hops via your peer's connections. Peering is a local
> configuration choice; reachability is mesh-wide. One good peer
> connects you to everyone the rest of the mesh connects to.
After the link to `test-us01` establishes, your daemon's `fips0`
adapter can reach `test-us01` itself and — through `test-us01`'s
connections — any other node on the test mesh, exactly as if you
had a direct connection to each of them.
> **About the test mesh.** The project maintains a small roster
> of public test nodes (`test-us01` through `test-uk01`) intended
> for new-user on-ramps and integration testing. They accept
> inbound peering from arbitrary npubs without prior coordination.
> A future reference doc will list the full roster; for this
> tutorial you only need `test-us01` as your peer, and `test-us02`
> later on as a second mesh destination to demonstrate
> forwarding.
## Step 1: Confirm the daemon is running
```sh
sudo systemctl status fips
```
Expect `active (running)`. If it is not running, the
[getting-started](../getting-started.md) guide covers installation
and service management. While you're checking, note your daemon's
current npub:
```sh
sudo fipsctl show status
```
Look for the `npub` field. With the default ephemeral-identity
config, this regenerates on every restart — that is fine for the
tutorial. `test-us01` admits any inbound npub.
## Step 2: Add a static peer to the daemon config
Edit `/etc/fips/fips.yaml`. Find the line that reads `peers: []`
and replace it with:
```yaml
peers:
- npub: "npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98"
alias: "test-us01"
addresses:
- transport: udp
addr: "test-us01.fips.network:2121"
connect_policy: auto_connect
```
What each field does:
- `npub` — the canonical Nostr public key of `test-us01`. This is
who your daemon will mutually authenticate with over Noise IK.
- `alias` — a short name your daemon will use when referring to
this peer in logs and `fipsctl show peers` output. Optional.
- `addresses` — one or more transport endpoints. UDP on the
published hostname and port is the most direct path.
- `connect_policy: auto_connect` — your daemon initiates an
outbound connection rather than waiting for the peer to reach
in.
## Step 3: Restart the daemon
```sh
sudo systemctl restart fips
```
Watch the daemon's journal as it comes back up and dials the
peer:
```sh
sudo journalctl -u fips -f
```
Within a few seconds you should see lines mentioning:
- An outbound connection attempt to `test-us01` or
`test-us01.fips.network:2121`
- A handshake completion (a "Noise IK link handshake complete"
style line, or "peer authenticated" with the test-us01 npub)
- An MMP / link metrics entry naming `test-us01`
If the handshake does not complete within roughly 30 seconds, jump
to [Troubleshooting](#troubleshooting) below.
## Step 4: Verify the link
```sh
sudo fipsctl show peers
```
Expect one entry whose `alias` is `test-us01`. Useful fields:
- `connectivity` — should be active / authenticated.
- `transport_addr` — the resolved UDP endpoint your daemon is
using to reach `test-us01`.
- `transport_type``udp`.
- `mmp.srtt_ms` — appears once the first MMP report has been
exchanged. This is your round-trip time to `test-us01`.
The transport view confirms your UDP listener and the peer
mapping:
```sh
sudo fipsctl show transports
```
## Step 5: Ping your peer
`test-us01`'s mesh address derives from its npub. Address it as
`<npub>.fips` and your daemon's local DNS responder will translate
that to its `fd97:...` mesh address.
First see the resolved address:
```sh
dig npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98.fips AAAA +short
```
You should see one `fd97:...` line.
Now ping it:
```sh
ping6 -c 4 npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98.fips
```
Expect four replies. The first packet may take noticeably longer
than subsequent ones — that round trip includes destination
discovery, FSP session establishment, and the proactive path-MTU
probe. After that, the RTT settles to a steady value reflecting
the path between your host and `test-us01`.
This confirms the direct link works. So far, though, you have only
reached the peer you configured. The next step demonstrates the
mesh-wide reach that peering buys you.
## Step 6: Reach a different node through the mesh
`test-us02` is another public test node. You did **not** add it
to your `peers:` block — your daemon has no direct link to it.
But because `test-us01` participates in the same mesh and has its
own connections to other nodes, your daemon can reach `test-us02`
through `test-us01` without any additional configuration.
```sh
ping6 -c 4 npub10yffd020a4ag8zcy75f9pruq3rnghvvhd5hphl9s62zgp35s560qrksp9u.fips
```
Same form, different npub. Expect replies. The packets travel
from your daemon to `test-us01` over the direct UDP link, then
onward through `test-us01` (and possibly other test-mesh nodes)
to reach `test-us02`'s `fips0` adapter. Replies retrace the path.
This is the central FIPS guarantee: **peering is local, but
reachability is mesh-wide.** You only need one good peer to talk
to everyone else they (transitively) talk to.
If the `test-us02` ping fails while the `test-us01` ping
succeeded, the test mesh's routing between those two nodes is
momentarily unhealthy — try again in a minute, or pick a different
test node from the roster. The link to your peer is unaffected.
## What you've learned
You now have a single FIPS node connected to one peer in the
public test mesh, with reach to every node that mesh routes you
to. You have seen:
- **Identity.** Your daemon's ephemeral keypair authenticated to
`test-us01` over Noise IK without either side trusting anyone in
advance.
- **Transports.** A UDP socket on your host carries
authenticated, encrypted mesh frames to your peer. No central
server, no VPN concentrator.
- **Peering vs. reachability.** You configured one peer
(`test-us01`) and got reach to a second node (`test-us02`) for
free, through the mesh. The same shape extends to every other
node `test-us01` can reach.
- **Naming.** The local `.fips` resolver translated npub-form
hostnames into their `fd97:...` mesh addresses with no external
DNS traffic.
- **End-to-end.** ICMPv6 traffic over the FIPS data plane reached
both destinations and came back, end-to-end encrypted along
every link layer in the path.
> **By the way: shortnames.** Those long `npub1...fips`
> destinations are the canonical addresses, but the installer
> ships an `/etc/fips/hosts` file with shortname entries for
> the public test mesh, so `test-us01.fips` and
> `test-us02.fips` resolve to the same addresses without
> typing 80 characters of bech32. You can add your own entries
> too. See
> [../how-to/host-aliases.md](../how-to/host-aliases.md). The
> rest of the tutorials use shortnames where they're available.
## Troubleshooting
If the handshake does not complete:
- **Outbound UDP may be blocked.** Some networks filter
arbitrary outbound UDP or block return traffic. From a
UDP-filtered network you cannot reach peers that only
publish UDP endpoints — your reachable peers are limited
to those that accept incoming TCP (outbound TCP is
typically allowed even on networks that block UDP). The
test-mesh nodes publish a TCP endpoint on port 443 for
exactly this case; replace the `udp` entry in the peer's
`addresses:` block with the TCP equivalent:
```yaml
addresses:
- transport: tcp
addr: "test-us01.fips.network:443"
```
Restart the daemon and re-check `fipsctl show peers`. The
link will be slower than UDP but is the supported transport
for restrictive egress environments.
- **Confirm the testnode is reachable at the IP layer.** Run
`dig +short test-us01.fips.network` to confirm DNS, then
`nc -uvz test-us01.fips.network 2121` to confirm UDP
reachability.
- **Confirm your config parsed.** `sudo journalctl -u fips -n 50`
near the daemon-start time will show config-load lines and any
parse errors.
- **Time skew.** A heavily skewed system clock can make
signature validation fail. `timedatectl status` should show
the system clock as synchronized.
## What's next
These are the natural follow-on tutorials in the new-user
progression. Some are still being written and will appear
alongside this one in the [tutorials/](.) directory.
- **Make your node's identity persistent.**
[persistent-identity](persistent-identity.md) walks through
pinning the daemon to a stable Nostr keypair so your npub
does not change across restarts — the prerequisite for other
operators adding you to their `peers:` blocks.
- **Resolve peers via Nostr.**
[resolve-peers-via-nostr](resolve-peers-via-nostr.md) is the
smallest useful step toward Nostr-mediated discovery:
configure a peer by npub alone and let the daemon look up
the current endpoint from public relays. The first of three
tutorials covering Nostr discovery; the others —
[advertise-your-node](advertise-your-node.md) and
[open-discovery](open-discovery.md) — round out the
publish and ambient-consume sides.
- **Trace a connection end-to-end.**
[ipv6-adapter-walkthrough](ipv6-adapter-walkthrough.md) walks
the data path from a `.fips` DNS query through session setup
to the far-side TUN adapter, using `fipstop` and `fipsctl` to
observe each step.
- **Reach services on other mesh nodes.**
[reach-mesh-services](reach-mesh-services.md) generalizes the
`ping6` you just ran to any IPv6-capable tool — `nc`,
`traceroute6`, `curl`, `ssh` — addressed by `<npub>.fips`.
The point is that the FIPS data plane is just IPv6;
applications don't need to know they're on a mesh.
- **Host a service of your own.**
[host-a-service](host-a-service.md) walks through bringing up
a small HTTP server bound to `fips0` so mesh nodes can reach
it, with a deliberate exposure decision (mesh-only vs every
interface), the mesh firewall, and a brief signpost to the
separate, unrelated peer ACL (which controls who may peer
with your node, not what they can reach on your `fips0`).
- [ground-up-mesh](ground-up-mesh.md) — Bring up two devices on
a shared physical link — Ethernet, WiFi, or Bluetooth — with
no pre-existing IP infrastructure. The second deployment mode
of FIPS, coexisting on the same daemon as the overlay peer to
`test-us01` you just configured.
For "what just happened, in detail":
- [../design/fips-architecture.md](../design/fips-architecture.md) —
the protocol stack and the two-layer encryption model.
- [../design/fips-mesh-layer.md](../design/fips-mesh-layer.md) —
Noise IK link encryption, hop-by-hop forwarding.
- [../design/fips-session-layer.md](../design/fips-session-layer.md)
— end-to-end Noise XK, session lifecycle.
- [../design/fips-ipv6-adapter.md](../design/fips-ipv6-adapter.md) —
the TUN, the local DNS responder, MTU enforcement.
+338
View File
@@ -0,0 +1,338 @@
# Open Discovery: Find Peers Without Configuration
After
[advertise-your-node](advertise-your-node.md), your daemon
publishes its endpoint on Nostr and other open-discovery nodes
on the test mesh have already started dialing you. This
tutorial flips the symmetry: turn your own daemon into a
consumer of every advert in the namespace, so any operator
who's publishing becomes a candidate peer of yours.
The whole exercise should take about ten minutes. After it,
you'll be a full participant in the ambient
`fips-overlay-v1` namespace — publishing your own advert
*and* discovering everyone else's.
## What you'll build
```text
┌─────────────────────────────────────────────────────┐
│ Nostr relays — fips-overlay-v1 namespace │
│ adverts from: test-us01..uk01, others │
└────────────────┬─────────────────────────▲──────────┘
│ │
│ subscribe to all │ your own
│ adverts in namespace │ advert
│ │
▼ │
┌────────────────────────────────────────────────────┐
│ your fips daemon (policy: open) │
│ │
│ peers list grows ambient as adverts arrive: │
│ test-us01 ← was static │
│ test-us03 ← inbound (was already there) │
│ test-de01, test-es01, test-uk01, test-us04... │
│ plus any other publisher in the namespace │
└────────────────────────────────────────────────────┘
```
You will change one thing in `/etc/fips/fips.yaml`: under
`discovery.nostr`, set `policy: open` (the default is
`configured_only`). After restart, the daemon subscribes to
every Kind 37195 advert in the `fips-overlay-v1` namespace and
queues the publishers for outbound connection attempts.
## How open discovery works
> **Discovery policy.** `discovery.nostr.policy` decides what
> the daemon does with incoming advert data. Two values:
>
> - `configured_only` (the default): the daemon only consumes
> adverts for peers it has explicitly listed with
> `via_nostr: true`. This is what you've been running
> through the previous two Nostr tutorials.
> - `open`: the daemon subscribes to every advert in the
> configured `app` namespace. Any publisher becomes a
> candidate peer, no `peers:` list entry needed.
Switching the policy doesn't disturb anything that was already
working:
> **Open is additive, not exclusive.** Switching to
> `policy: open` doesn't replace your static peers — both
> mechanisms run in parallel. Configured peers stay in your
> `peers:` block and continue to be dialed via their static
> addresses or `via_nostr` lookups; open-discovered peers
> stack on top from the ambient namespace. You can run open
> discovery with a populated `peers:` list (the path this
> tutorial walks, since you're keeping `test-us01`), with
> `peers: []` for pure ambient discovery, or with a long
> `peers:` list and open layered on top to broaden reach.
The namespace is what scopes who's visible to whom:
> **The namespace is the scope.** `discovery.nostr.app`
> defaults to `fips-overlay-v1` — the namespace the public
> test mesh uses. Setting a different value (e.g.,
> `app: "my-experiment.v1"`) carves out a private discovery
> set: only nodes that share your `app` value find each
> other. For this tutorial we stay on the default and join
> the public namespace.
Open discovery is best-effort by design — not every
discovered peer will connect:
> **Best-effort, by design.** Many discovered peers will fail
> to connect — they may be offline, behind incompatible NAT,
> running a different protocol version, or have peer ACLs
> that reject you. That's normal for ambient discovery;
> connection attempts are best-effort and rate-limited by
> `open_discovery_max_pending` (default 64). Your peer list
> grows over time as candidates land in the cache, not all
> at once on restart.
The peer ACL is the admission-control surface, separate from
discovery:
> **Open is admission-free under your peer ACL.** Open
> discovery does not bypass the peer ACL — every candidate
> still has to pass it. By default the ACL accepts everyone,
> so any publisher in the namespace becomes a connection
> candidate. If you rely on a non-default ACL for admission
> control, verify it is set the way you want *before*
> enabling `policy: open`. See
> [../reference/security.md](../reference/security.md) for
> the ACL format.
## Step 1: Confirm your starting state
You should be coming out of
[advertise-your-node](advertise-your-node.md) with:
- Persistent identity, advertising enabled
(`discovery.nostr.advertise: true`), UDP advertising on
Nostr (`transports.udp.advertise_on_nostr: true`).
- A static `test-us01` peer entry that the daemon dials
outbound; possibly an inbound `test-us03` peer (the
open-discovery test mesh node that dialed in after seeing
your advert).
Capture the current peer count for comparison:
```sh
sudo fipsctl show peers | grep -c 'npub'
```
You'll likely see 1 (just `test-us01`) up to a handful, depending
on how many open-discovery test mesh nodes have already dialed
you.
## Step 2: Switch the discovery policy to `open`
Open `/etc/fips/fips.yaml` and find the `discovery.nostr`
block. Add (or change) the `policy` line:
```yaml
node:
identity:
persistent: true
discovery:
nostr:
enabled: true
advertise: true
policy: open
```
That's the only change. Notes on what you don't have to touch:
- **You don't have to drop the `peers:` block.** Static peers
and open-discovered peers coexist; static entries get
priority for direct dialing, open-discovered ones are
layered on top.
- **You don't have to set `app`.** The default
`fips-overlay-v1` is the namespace the public test mesh
uses; staying with the default is what gets you discovered.
- **You don't have to set `open_discovery_max_pending`.** The
default of 64 is plenty for a tutorial; only tune it if you
see the daemon log
`open-discovery: max-pending reached, deferring`.
Save the file.
## Step 3: Restart and let discovery populate
```sh
sudo systemctl restart fips
```
Give the daemon a minute or two. Open discovery doesn't fire
all at once — the daemon subscribes to the relays, accumulates
adverts as they arrive (or as relays return historical
events), and queues each publisher for a dial attempt.
## Step 4: Inspect the discovered peer list
```sh
sudo fipsctl show peers
```
You should see considerably more entries than before:
- `test-us01` — still there, still using the configured
static dial path.
- `test-us03` — same as before (the open-discovery test
mesh node that dials you when it sees your advert).
- `test-us04`, `test-de01`, `test-es01`, `test-uk01` — the
other test mesh nodes; your daemon picked their adverts up
from the namespace and dialed them.
- Plus any other operator publishing on `fips-overlay-v1`
(community nodes, other operators' experiments).
Each entry has its own `connectivity` state. Some will be
`active` (handshake completed). Some will show as
`connecting` and may transition to `failed` shortly after —
that's normal; the publisher might be offline, the advert
might be stale, or NAT traversal failed for that pair.
To get a list of just the active links:
```sh
sudo fipsctl show peers | jq '.peers[] | select(.connectivity == "active") | .npub'
```
The peer count will continue to drift over time as adverts
expire and new ones arrive. This is steady-state behavior,
not a transient.
## Step 5: Confirm the mesh-wide reach
You can now reach any of the discovered nodes the same way
you reached `test-us01` and `test-us02` in
[join-the-test-mesh](join-the-test-mesh.md). Pick one of the
new test mesh nodes from the peer list and ping it by its
shortname:
```sh
ping6 -c 4 test-uk01.fips
```
(`test-uk01` is the United Kingdom test node; the installer's
`/etc/fips/hosts` entry resolves it to the corresponding npub.
Substitute any active peer's shortname or full
`<npub>.fips`.)
Expect four replies. The packet path may go through your
direct link to that peer (if the open-discovery dial succeeded
and the link is up) or via a test-mesh forwarder (if the
direct link is down but the destination is still reachable
through the mesh). Either way, the npub-as-name addressing
works the same way.
## What you've learned
- **Open discovery is the consume side of full ambient
participation.** With `policy: open` plus advertising, your
daemon both publishes its advert *and* consumes everyone
else's in the namespace.
- **The namespace defines the scope.** Everyone publishing
with the same `app` value finds each other; different
namespaces are isolated discovery sets.
- **Best-effort means failure is normal.** Many discovered
peers won't actually connect; that's expected and
rate-limited by `open_discovery_max_pending`.
- **Static and open coexist.** Configured peers keep working
with their own dial paths; open-discovered peers stack on
top.
- **The peer ACL still gates everything.** Open is
admission-free relative to discovery, not relative to your
ACL — the ACL is what you'd use to restrict who can connect
if you don't want a fully open posture.
## Custom namespaces for private experiments
If you want to use FIPS open discovery for a private set of
nodes — colleagues, a workshop cohort, a specific deployment
— set a custom `app` value:
```yaml
discovery:
nostr:
enabled: true
advertise: true
policy: open
app: "my-team.experiment-1"
```
All nodes participating in the experiment use the same
`app` string. Pick something distinctive — short identifiers
risk colliding with other operators' experiments. Once your
nodes use a custom `app`, they no longer find or are found
by the public test mesh (the public mesh uses
`fips-overlay-v1`).
## Troubleshooting
If your peer list doesn't grow past the inbound peers from
the previous tutorial:
- **Wait.** Open discovery accumulates adverts; the first
pass after restart can take a couple of minutes to populate
on a new subscription.
- **Verify the namespace.** With `app:` unset, the daemon
uses `fips-overlay-v1`. If you set a custom `app:` for an
experiment, your daemon is in a different namespace than
the public test mesh and will only find peers using the
same value.
- **Check relay reachability.** Open discovery is a
subscription rather than one-shot queries — if the
WebSocket connection to the relays is failing repeatedly,
no adverts arrive. Look for relay-connection errors in
`sudo journalctl -u fips -n 200`.
- **`policy: open` typo.** YAML accepts and ignores unknown
values silently. If `fipsctl show status` (or the daemon's
startup log) shows `policy: configured_only`, the YAML
didn't parse the new value — re-check spelling and
indentation.
If too many peers are appearing and you want to dial down:
- **Lower `open_discovery_max_pending`.** Default 64; setting
it to e.g. 16 caps in-flight connection attempts. Adverts
beyond that wait in a queue.
- **Use a custom `app`.** Move to a private namespace where
only nodes you're coordinating with publish.
- **Use the peer ACL.** See
[../reference/security.md](../reference/security.md) for
the ACL format if you want explicit allow/deny rules.
## What's next
- **Reach services on other mesh nodes.**
[reach-mesh-services](reach-mesh-services.md) drives `nc`,
`traceroute6`, `curl`, and `ssh` at peers by `.fips` name —
any of the open-discovered peers in your list, or any node
you reach through them.
- **Host a service of your own.**
[host-a-service](host-a-service.md) brings up an HTTP server
addressable as `<your-npub>.fips`, bound to `fips0` so the
exposure is mesh-only, behind the mesh firewall.
- [ground-up-mesh](ground-up-mesh.md) — Bring up two devices on
a shared physical link (Ethernet, WiFi, or Bluetooth) with no
pre-existing IP infrastructure. The second deployment mode of
FIPS, a parallel to the overlay-on-internet path the
Nostr-discovery tutorials covered.
For the operator-style scenario reference covering all five
shapes of Nostr discovery side-by-side:
- [../how-to/enable-nostr-discovery.md](../how-to/enable-nostr-discovery.md)
§ Capability 3 — open discovery configuration knobs.
For the wire format and discovery design:
- [../reference/nostr-events.md](../reference/nostr-events.md)
— Kind 37195 advert format and the `app` namespace tag.
- [../design/fips-nostr-discovery.md](../design/fips-nostr-discovery.md)
— discovery runtime design, security and threat model.
+304
View File
@@ -0,0 +1,304 @@
# Make Your Node's Identity Persistent
After completing
[join-the-test-mesh](join-the-test-mesh.md), your daemon is
connected to the public test mesh — but its identity is
ephemeral. Every restart generates a fresh Nostr keypair, so the
npub the rest of the world would use to reach you changes every
time. This tutorial walks through pinning your node to a stable
keypair, locating it on disk, and protecting it.
The whole exercise should take about ten minutes.
## What you'll build
```text
┌─────────────────────────────────────────┐
│ /etc/fips/fips.yaml │
│ node: │
│ identity: │
│ persistent: true ← this flag │
└────────────────┬────────────────────────┘
│ daemon reads on start
┌─────────────────────────────────────────┐
│ /etc/fips/fips.key 0600 root:root │
│ /etc/fips/fips.pub 0644 root:root │
└─────────────────────────────────────────┘
your stable nsec / npub
```
After this tutorial your node will have:
- A keypair on disk that the daemon reuses across restarts.
- An npub you can hand to other operators so they can add you to
their `peers:` list once and have the entry keep working.
- A clear understanding of which file holds the secret and how
to keep it that way.
## Why a stable identity matters
In FIPS your Nostr keypair *is* your node's identity in the most
literal sense. Several things derive from it:
- Your `fd97:...` mesh address — derived from the public key.
- Your `<npub>.fips` DNS name — the npub itself with `.fips`
appended.
- Every authenticated connection — Noise IK at the mesh layer,
XK at the session layer, both prove you hold the matching
secret key.
> **A keypair, briefly.** Nostr identities are secp256k1
> keypairs. The private half is the `nsec` (a bech32-encoded
> secret key); the public half is the `npub` (a bech32-encoded
> public key). The daemon needs the `nsec` to sign messages and
> complete handshakes; the rest of the world only sees the
> `npub` and uses it as your address.
The daemon supports two ways of holding that keypair:
> **Ephemeral vs. persistent.**
>
> - *Ephemeral* (the default): the daemon mints a brand-new
> keypair every time it starts, kept only in memory. No
> long-term secret is ever written to disk; nothing on your
> machine ties one run to the next; the npub your daemon
> presents to the network is fresh on every restart. This is
> the safe-by-default posture — your node has no persistent
> identity unless you explicitly ask for one.
> - *Persistent*: the daemon reads (or, on first start,
> generates and writes) a keypair stored at
> `/etc/fips/fips.key`. The npub stays the same across
> restarts, reboots, and reinstalls as long as that file is
> preserved. You take on the cost of protecting an on-disk
> secret in exchange for being addressable by a stable name.
Persistent identity is a deliberate trade. You give up the
ephemeral default's privacy posture — once your npub is stable,
every connection your node makes is correlatable across time —
and you take on a real secret-management responsibility. In
return you get two things you can't get any other way:
1. **Other operators can reference you by npub.** In
[join-the-test-mesh](join-the-test-mesh.md) you added
`test-us01` to your `peers:` list by its npub. That entry
only works because `test-us01`'s npub doesn't change. If
anyone is going to reach you the same way, your npub has to
be just as stable.
2. **Services on your node get a fixed address.** The
[host-a-service](host-a-service.md) tutorial walks through
running an HTTP server addressable as
`<your-npub>.fips`. Clients reach the service by that
name; if your npub changes on every restart, every
client's address book breaks.
Both of these are good reasons. Neither is automatic — if your
node is purely a *client*, reaching out to others without
hosting anything itself, you may legitimately want to stay on
the ephemeral default. The rest of this tutorial assumes you've
decided you want a stable identity.
## Step 1: Note your current ephemeral npub
Before changing anything, capture the npub the daemon is using
right now so you can compare against it after the switch.
```sh
sudo fipsctl show status | grep '"npub"'
```
You'll see one line like:
```text
"npub": "npub1abc...xyz"
```
Make a note of it. We expect this to change.
## Step 2: Enable persistent identity in the config
Open `/etc/fips/fips.yaml` and find the `node:` block. The
shipped default has the relevant fragment commented out; make it
look like this:
```yaml
node:
identity:
persistent: true
```
Save the file. That is the only configuration change.
The daemon's behavior on the next restart:
- If `/etc/fips/fips.key` already exists, load it and use that
identity.
- If it does not exist, generate a fresh keypair, write it to
`/etc/fips/fips.{key,pub}` with the correct file modes, and
use that.
## Step 3: Restart the daemon
```sh
sudo systemctl restart fips
sudo systemctl status fips
```
Status should show `active (running)` within a couple of
seconds. Confirm the new identity is in use:
```sh
sudo fipsctl show status | grep '"npub"'
```
The npub should be **different** from the one in Step 1 — the
daemon discarded the old in-memory ephemeral keypair and minted
a new one which it has now persisted to disk. From here forward
this is *your* npub.
## Step 4: Locate the keypair on disk
The daemon wrote two files:
```sh
sudo ls -l /etc/fips/fips.key /etc/fips/fips.pub
```
Expect:
```text
-rw------- 1 root root ... fips.key
-rw-r--r-- 1 root root ... fips.pub
```
The public file is safe to share — it is your address:
```sh
sudo cat /etc/fips/fips.pub
```
This must match the `npub` reported by `sudo fipsctl show
status`. Hand this string to anyone you want to be reachable
from; they paste it into their own `peers:` block as the `npub:`
field.
The private file is the secret. **Do not** `cat` or paste its
contents anywhere — there is no reason to see it, and any line
of shell history or screen capture that contains it has captured
the secret.
## Step 5: Verify it survives a restart
Restart once more to confirm the daemon is reading `fips.key`
rather than re-generating it:
```sh
sudo systemctl restart fips
sudo fipsctl show status | grep '"npub"'
```
The npub should match Step 3 exactly. If it does not, the daemon
was unable to read `fips.key` (most likely a permission problem)
— see [Troubleshooting](#troubleshooting).
## Step 6: Protect the nsec
`fips.key` is the only thing standing between you and someone
else impersonating your node. The daemon ships it with the right
permissions; the operator's job is to keep them that way.
What that means in practice:
- **Do not loosen the file mode.** `0600` (read/write for owner
only — here `root`) is correct; `chmod 0644` to "fix" a
permission error puts the secret on display to every account
on the host.
- **Do not commit it to source control.** If you maintain
configuration in a Git repo, exclude `fips.key`; if you use
ansible-vault or a similar mechanism, encrypt it. `fips.pub`
may be checked in freely.
- **Do not paste it into chat or email.** Operators sometimes
share config snippets to demonstrate a setup; redact the
contents of `fips.key` to a placeholder before doing so.
- **Back it up the way you back up an SSH host key.** Treat the
file (or its contents) the same way you would treat
`/etc/ssh/ssh_host_ed25519_key`: encrypted, offline, available
to recover the *same* identity if the host disappears.
There is no in-protocol "key change" message in FIPS. If
`fips.key` is lost, the npub is lost — your node will come back
up with a new identity and every downstream reference to the
old one will be stale.
## What you've learned
- **Identity = keypair.** Every FIPS node is a Nostr keypair;
the npub is its address, the nsec is its credential.
- **The flag.** `node.identity.persistent: true` in
`/etc/fips/fips.yaml` is the difference between ephemeral and
persistent identity.
- **Where it lives.** `/etc/fips/fips.key` and
`/etc/fips/fips.pub`, mode `0600` and `0644`, owned
`root:root`.
- **What to share.** `fips.pub` is public; `fips.key` is not.
- **What it buys you.** A npub other operators can add to their
`peers:` list once, and that addresses the services your node
will eventually run.
## Troubleshooting
If the post-restart npub does not match `fips.pub`:
- **Check file permissions.**
`sudo ls -l /etc/fips/fips.key`. If the mode is not `0600` or
the owner is not `root:root`, the daemon may have refused to
read it. Restore with
`sudo chmod 0600 /etc/fips/fips.key && sudo chown root:root
/etc/fips/fips.key`.
- **Check the journal.** `sudo journalctl -u fips -n 100` after
the restart will show one of:
- `Loaded persistent identity from key file path=...` — good.
- `Generated persistent identity, saved to key file ...`
also good, but only expected on the first start after the
flag flip.
- `Using ephemeral identity (new keypair each start)` — the
config flag was not picked up; re-check the indentation of
the `persistent: true` line.
If you see `Generated persistent identity...` on every start,
the file is being written but not read on subsequent starts;
this is almost always the same permission/path issue.
## What's next
- **Resolve peer addresses via Nostr.**
[resolve-peers-via-nostr](resolve-peers-via-nostr.md) walks
through the smallest useful step toward Nostr-mediated
discovery: keep your peer entry, drop its hard-coded address,
and let the daemon look up the current endpoint from public
Nostr relays. The first of three tutorials covering Nostr
discovery; advertising your own node and open ambient
discovery come next.
- **Reach services on other mesh nodes.**
[reach-mesh-services](reach-mesh-services.md) drives `nc`,
`traceroute6`, `curl`, and `ssh` at peers by `.fips` name and
shows that the FIPS data plane is just IPv6 from an
application's point of view.
- **Host a service of your own.**
[host-a-service](host-a-service.md) walks through bringing up
an HTTP server addressable as `<your-npub>.fips`, bound to
`fips0` so the exposure is mesh-only, behind the mesh
firewall.
For the alternative provisioning paths — minting a keypair with
`fipsctl keygen` before the daemon ever starts, or importing an
existing Nostr `nsec` — and the key-rotation procedure, see the
operator-style recipe at
[../how-to/persistent-identity.md](../how-to/persistent-identity.md).
For the full identity model:
- [../design/fips-architecture.md](../design/fips-architecture.md)
— how npubs become `NodeAddr`s and IPv6 ULAs.
+253
View File
@@ -0,0 +1,253 @@
# Reach Services on Other Mesh Nodes
In [join-the-test-mesh](join-the-test-mesh.md) you used `ping6`
to reach `test-us01` and `test-us02` by their `.fips` names.
This tutorial generalizes that to any IPv6-capable tool you
already use — `nc`, `traceroute6`, `curl`, `ssh`, `scp`,
anything — and gets you comfortable with the daemon's IPv6
adapter, which makes the FIPS mesh look like an ordinary IPv6
network to applications that already know how to use IPv6.
The whole exercise should take about ten minutes.
## What you'll do
You'll ping a mesh node (recap), attempt a TCP connection to it
with `nc`, and trace the packet path with `traceroute6` — all
by hostname. By the end you will have driven three different
IPv6 tools at a mesh address and seen each one work the same
way it works on the regular internet.
> **An IPv6 adapter for a non-IPv6 mesh.** The FIPS network
> itself routes blobs of data between npub-addressed nodes;
> on its own it has nothing to do with IPv6. The daemon
> includes an *IPv6 adapter* that presents the mesh as an
> ordinary IPv6 interface (`fips0`), so existing IP software
> works without modification. The kernel routes packets to
> it, applications open IPv6 sockets through it, and the
> adapter handles encapsulating each packet and routing it
> through the mesh to the matching adapter on the other side.
> Any tool that speaks IPv6 works unchanged.
The IPv6 adapter is currently the main way operators use the
FIPS network, which is why most of the new-user progression is
about it. Native applications can use the mesh without going
through IPv6 at all, but that is out of scope for this tutorial.
## Addressing a mesh node
Throughout this tutorial — and any time you reach across the
mesh — use a node's `.fips` hostname directly. There are two
forms:
- **`<npub>.fips`** — the canonical form. Every node has one,
always. This is the long bech32 npub with `.fips` appended.
- **`<shortname>.fips`** — the convenience form, *if* you (or
the package) have an entry for the node in
`/etc/fips/hosts`. The installer ships entries for the
public test mesh, so `test-us01.fips` works on a fresh
install.
These are real hostnames as far as your kernel is concerned.
Pass them to any IPv6-capable tool — `ping6`, `nc`, `curl`,
`ssh`, `traceroute6`, anything — the same way you would pass a
hostname on the public internet. There is no separate
"resolve to address first" step you ever need to perform; if
the tool takes a hostname, it accepts a `.fips` hostname.
> **Where the address comes from.** Every FIPS node's mesh
> address is the SHA-256 of its public key, truncated to the
> bottom 64 bits and prepended with `fd97:`. Names of the form
> `<npub>.fips` and any shortname mapped in `/etc/fips/hosts`
> are aliases for that address. The daemon's local DNS
> responder hands the answer back to your kernel without ever
> talking to a remote DNS server.
## Step 1: Ping a mesh node (recap)
```sh
ping6 -c 4 test-us01.fips
```
You did this in [join-the-test-mesh](join-the-test-mesh.md).
Four replies, RTT in the tens of milliseconds (depending on
where you are relative to `test-us01`). Nothing new — but it
confirms the mesh data plane is healthy before you try
anything else.
## Step 2: Attempt a TCP connection
`ping6` proves ICMPv6 reaches the destination. To prove TCP
reaches it, use `nc` (netcat) to attempt a connection to a port.
Pick any port — whether it has a service listening or not, the
attempt proves the data plane carries your TCP segments
end-to-end:
```sh
nc -6 -vz test-us01.fips 22 2>&1
```
You will see one of two outcomes:
```text
Connection to test-us01.fips 22 port [tcp/ssh] succeeded!
```
or:
```text
nc: connect to test-us01.fips port 22 (tcp) failed: Connection refused
```
Both are good. The first means a service is listening on that
port and accepted your TCP handshake. The second means your TCP
SYN reached the remote node's kernel, which sent back a TCP RST
because no service was bound — and that RST traveled all the way
back through the mesh to your `nc` process.
> **What a `Connection refused` proves.** A connection-refused
> response is *not* a network failure. It means the destination
> host is alive and reachable, the TCP stack on the far end
> processed your SYN, and the reply made it home. Compare with
> what you would get if the address were unreachable:
> `Network is unreachable` or a timeout. Either of the two
> outcomes above demonstrates a working end-to-end TCP path.
If the port you tried happens to have a service, attach `-`
instead of `-z` and you can read the banner directly:
```sh
nc -6 -v test-us01.fips 22
```
The remote node's SSH banner, if any, will print on the next
line. Type `Ctrl-C` to disconnect — you have not authenticated,
just banner-grabbed.
If `nc` is not installed, the same demonstration works with
`curl` against TCP/80:
```sh
curl -6 -v --connect-timeout 5 http://test-us01.fips/ 2>&1 | head
```
The TCP connection result is in the first few lines of `curl`'s
verbose output. The HTTP response code is irrelevant — what
matters is whether the connection itself succeeded.
## Step 3: Trace the path
`traceroute6` shows the IPv6 hops between you and a
destination:
```sh
traceroute6 -n test-us02.fips
```
You will see exactly one line — `test-us02`'s mesh address.
That is the only IPv6 hop between your `fips0` and
`test-us02`'s `fips0`, even though at the FIPS-mesh layer
your packet is being forwarded through your peer `test-us01`
on the way to `test-us02`. The mesh-layer forwarding is
invisible to `traceroute6` because it lives below the IPv6
adapter.
> **Two layers, two ideas of "hop".** The FIPS mesh routes
> blobs between npub-addressed nodes and can pass through
> several intermediate peers — your packet to `test-us02` is
> handed off to `test-us01` first. The IPv6 adapter, sitting
> on top of that, presents every reachable mesh node as a
> direct IPv6 neighbor: one hop, on a flat fabric. From
> `traceroute6`'s perspective the multi-hop FIPS path is
> hidden — it sees only the source and destination IPv6
> adapters. To see what's happening at the mesh layer, see
> [ipv6-adapter-walkthrough](ipv6-adapter-walkthrough.md),
> which traces one `ssh` request from DNS query to far-side
> TUN with `fipstop` and `fipsctl` running alongside.
If `traceroute6` is not installed, `mtr` and other IPv6 path
tools produce the same single-hop result. The single-hop
behavior is a property of the IPv6 adapter, not of the tool.
## What you've learned
You have driven three IPv6 tools at mesh nodes you reach over
the mesh, all by `.fips` hostname, and they all worked the same
way they work everywhere else:
- **Addressing.** `<npub>.fips` is the canonical hostname for
any node; `<shortname>.fips` is the convenience form when
`/etc/fips/hosts` has an entry. Use these in any tool that
takes an IPv6 hostname — there is no separate resolution
step you ever need to perform.
- **Reachability.** `ping6` confirms the remote node's `fips0`
answers ICMPv6 echo from your `fips0`.
- **TCP.** `nc` confirms TCP segments traverse the mesh and the
far side responds (whether with a banner, a refusal, or a
service of its own).
- **Path.** `traceroute6` shows exactly one IPv6 hop to any
reachable mesh node, because the multi-hop FIPS-mesh-layer
forwarding lives below the IPv6 adapter and is invisible
to IPv6 tooling.
The conceptual takeaway is the one in the callout at the top:
the daemon's IPv6 adapter takes care of presenting the FIPS
mesh as ordinary IPv6 to every tool you already know. To
consume any service hosted on any mesh node — SSH, HTTP, file
transfer, custom protocols — you use the IPv6 client you
would use anywhere else. The hostname looks unusual
(`<npub>.fips`), but the API surface is unchanged.
## Troubleshooting
If a tool reports "Network is unreachable" or hangs:
- **Confirm the link is healthy.**
`sudo fipsctl show peers` should show `test-us01` with active
connectivity. If the link to your direct peer is down, nothing
past it is reachable.
- **Confirm `fips0` is up.** `ip -6 addr show fips0` should show
one `fd97:...` address. If `fips0` is missing, the daemon did
not bring up the TUN — verify the daemon is running with the
privileges it needs. The default is to run as root; if you
dropped privileges per
[../how-to/run-as-unprivileged-user.md](../how-to/run-as-unprivileged-user.md),
re-check that the `setcap` and systemd override survived your
last package upgrade.
- **Confirm the name resolves.** If `ping6 test-us01.fips`
fails with `unknown host` or `Name or service not known`,
the system resolver is not consulting the daemon's `.fips`
responder. The installer wires this up automatically; the
"Reaching mesh nodes by name" section of
[../getting-started.md](../getting-started.md) describes
what the wiring looks like and how to confirm it.
If `nc` or `curl` reports a timeout (rather than a refusal or
success), the destination node is unreachable from your
daemon — possible mesh-routing transient. Try again, or ping
first: if `ping6` succeeds but TCP times out, it is the
specific port being filtered on the destination, not a path
problem.
## What's next
- [host-a-service](host-a-service.md) — Bring up a small HTTP
server on your node, bind it to `fips0` so it is mesh-only,
and confirm another mesh node (or your own machine) can
reach it through the same data plane you just exercised.
Covers bind-interface choice and the mesh firewall.
For "what's actually in those packets":
- [../design/fips-architecture.md](../design/fips-architecture.md)
— the protocol stack and the two-layer encryption model.
- [../design/fips-mesh-layer.md](../design/fips-mesh-layer.md) —
Noise IK link encryption, hop-by-hop forwarding.
- [../design/fips-session-layer.md](../design/fips-session-layer.md)
— end-to-end Noise XK between source and destination.
For the trace-it-yourself version of the path you just
exercised, see
[ipv6-adapter-walkthrough](ipv6-adapter-walkthrough.md), which
walks one `ssh` from DNS query through session setup to the
far-side TUN with `fipstop` and `fipsctl` running alongside.
+300
View File
@@ -0,0 +1,300 @@
# Resolve Peer Addresses via Nostr
After
[persistent-identity](persistent-identity.md), your daemon has
a stable npub and is peered with `test-us01` over a hard-coded
UDP address (`test-us01.fips.network:2121`). That static
address works fine until `test-us01` moves to a new IP, swaps
ports, or starts publishing additional endpoints you'd want to
reach. The npub is stable; the set of network endpoints behind
it may not be.
This tutorial shows the smallest useful step toward Nostr-
mediated discovery: keep the peer entry but drop its address,
let your daemon ask public Nostr relays for the peer's current
endpoint, and verify the link still works. You will not be
publishing anything yourself yet — this is the consume-only
case.
The whole exercise should take about ten minutes.
## What you'll build
```text
┌──────────────────────────┐
│ Nostr relays │
│ relay.damus.io │
│ nos.lol │
│ offchain.pub │
└────────────▲─────────────┘
"what's test-us01's │ signed advert
current address?" │ (Kind 37195)
│ from test-us01
┌───────────────────────┐ │
│ your fips daemon │ ─────────┘
│ peers: │
│ - test-us01 npub │ ─── dial resolved UDP ──▶ test-us01
│ via_nostr: true │
└───────────────────────┘
```
You'll change two things in `/etc/fips/fips.yaml`:
- Add a `node.discovery.nostr` block that turns the consume-
side of Nostr discovery on.
- Edit the existing `test-us01` peer entry to drop its hard-
coded `addresses:` block and add `via_nostr: true`.
After restart, the daemon will fetch `test-us01`'s current
advert from the relays, use the endpoint listed there, and
peer normally.
## How Nostr discovery resolves an address
Every FIPS daemon with `node.discovery.nostr.advertise: true`
publishes a signed Nostr event (Kind 37195) listing the
transport endpoints it is willing to accept connections on.
The event is signed by the daemon's secret key, so anyone
who has the corresponding npub can verify the advert really
came from that node.
`test-us01` runs with `advertise: true`. Its current advert is
visible to any Nostr client.
> **Identity is stable; endpoints are not.** A peer's npub is
> a long-lived identifier — it is who they are. Their UDP
> address, port, or transport choice is metadata that may
> change. Nostr discovery lets you bind your peer entry to the
> npub and lets the relay tell your daemon the current
> endpoint at dial time.
There are two halves to this — consuming adverts (looking up
peers by npub) and publishing adverts (being lookup-able). This
tutorial covers only the consume half.
> **Consume vs. publish.** This tutorial enables only the
> *consume* side: your daemon queries relays to resolve peers
> by npub. It does not publish an advert of its own — others
> still cannot find you by your npub yet. The next tutorial
> (`advertise-your-node`) handles the publish side.
## Step 1: Confirm your starting state
You should currently have:
- A persistent npub from
[persistent-identity](persistent-identity.md). Confirm:
```sh
sudo fipsctl show status | grep '"npub"'
```
- A working static peering with `test-us01`. Confirm:
```sh
sudo fipsctl show peers
```
Expect `test-us01` listed with `connectivity` active and a
`transport_addr` of roughly `217.77.8.91:2121`.
If either of those isn't true, finish the previous two
tutorials first; the Nostr discovery layer is built on top of
that working state.
## Step 2: Enable the consume side of Nostr discovery
Open `/etc/fips/fips.yaml` and add a `discovery` block under
`node:`:
```yaml
node:
identity:
persistent: true
discovery:
nostr:
enabled: true
advertise: false
```
Two knobs, one job each:
- `enabled: true` turns on the Nostr discovery runtime — the
daemon connects to a default relay set
(`wss://relay.damus.io`, `wss://nos.lol`,
`wss://offchain.pub`) and is now able to query and consume
adverts.
- `advertise: false` keeps the publish side off. Your daemon
will not publish an advert of its own at this stage. (This
is the default, but it is good practice to make the choice
explicit while you're learning.)
## Step 3: Switch the peer entry to `via_nostr`
Find the `peers:` block you added during
[join-the-test-mesh](join-the-test-mesh.md) and change it from
this:
```yaml
peers:
- npub: "npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98"
alias: "test-us01"
addresses:
- transport: udp
addr: "test-us01.fips.network:2121"
connect_policy: auto_connect
```
to this:
```yaml
peers:
- npub: "npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98"
alias: "test-us01"
via_nostr: true
connect_policy: auto_connect
```
What changed: the `addresses:` list is gone, replaced by
`via_nostr: true`. The npub stays — it is what the daemon
matches against the advert publisher's pubkey.
Save the file.
## Step 4: Restart the daemon
```sh
sudo systemctl restart fips
sudo systemctl status fips
```
The status output should show `active (running)` within a
couple of seconds. The Nostr discovery runtime starts alongside
the rest of the daemon, fetches `test-us01`'s advert from the
default relays, and uses the endpoint listed there to dial.
The resolution itself happens at debug-log level, so you will
not see it in the default-level journal. The user-facing way to
confirm everything worked is `fipsctl show peers` in the next
step. (To watch the resolution in the journal, run the daemon
manually with `RUST_LOG=fips::discovery::nostr=debug`; not
necessary for this tutorial.)
## Step 5: Verify the resolved endpoint
```sh
sudo fipsctl show peers
```
`test-us01` should appear with `connectivity` active and a
`transport_addr` reflecting the address that was resolved from
the advert — `217.77.8.91:2121` at time of writing. That field
is the strong signal: nothing in your config gave the daemon
that IP, yet there it is.
You can confirm independently that the address came from the
advert. The advert is a public Nostr event — anyone can fetch
it. With the `nak` Nostr CLI installed:
```sh
nak req -k 37195 -d "fips-overlay-v1" \
-a 06f11c31227938ab98ba982280d2826f66a063f9efe7e342e81d6a76c677d7ed \
--limit 1 wss://relay.damus.io
```
(That hex pubkey is the same identity as
`npub1qmc3...zel98` — Nostr filters take hex.) The `content`
field of the returned event lists the `endpoints` array; one
of its entries should match what `fipsctl show peers` is
using. That is what your daemon just did, signed and verified
by the Nostr layer.
## Step 6: Confirm reachability still works
```sh
ping6 -c 4 test-us01.fips
```
Expect four replies, exactly as in
[join-the-test-mesh](join-the-test-mesh.md) (which used the
full npub form). Nothing about the data plane has changed;
only the way you discovered the endpoint to dial.
## What you've learned
- **Adverts are signed.** Every Nostr discovery advert is
signed by the publisher's secret key, so the address you
resolved through a public relay is trustworthy in the same
sense the peer's npub is.
- **`via_nostr` replaces a static address.** A peer entry
with no `addresses:` block and `via_nostr: true` directs
the daemon to look the endpoint up at dial time.
- **The relay set is small and public.** Three default
relays today; the daemon round-robins queries across them.
No central FIPS infrastructure is involved.
- **Static and Nostr can mix.** You replaced the static
address with `via_nostr` here, but you could have kept both
— when both are present, static endpoints are tried first
and Nostr-resolved endpoints are appended as a fallback.
Useful when you want a fast-path direct dial but a
resilient fallback.
## Troubleshooting
If the link does not come up:
- **No advert on the relays.** If the peer's daemon is
offline or has `advertise: false`, no advert exists for
your daemon to consume. Verify with `nak` (Step 5) — if the
query returns nothing, that is the problem and it is on the
peer's side. Re-add the static `addresses:` entry as a
fallback while you wait for the peer to come back up.
- **Relay reachability.** `Connected to relay` lines should
appear for at least one of the three default relays. If
none do, your network may be filtering outbound WebSocket
traffic or DNS for those hostnames. Check the journal for
TLS/DNS errors.
- **Stale cache.** The daemon caches resolved endpoints
briefly. If a peer's advert changes mid-session and you
hit a stale entry, restart the daemon to force a fresh
query.
- **Persistent identity not on.** If the journal shows
`Using ephemeral identity (new keypair each start)`, the
daemon falls back to ephemeral and the consume-side may
not behave as expected. Re-check
[persistent-identity](persistent-identity.md) Step 2.
## What's next
- **Advertise your own node.**
[advertise-your-node](advertise-your-node.md) publishes your
daemon's UDP endpoint on Nostr so other operators can add
you to their `peers:` list with `via_nostr: true` and reach
you the way you just reached `test-us01`. Includes a section
on `udp:nat` NAT traversal for symmetric-NAT networks.
- **Discover peers with no prior configuration.**
[open-discovery](open-discovery.md) switches your daemon to
`policy: open` so the ambient namespace itself populates
your peer list — no static `peers:` entries required (the
static ones can stay too; the two mechanisms run in
parallel).
For the operator-style scenario reference covering all five
shapes of Nostr discovery side-by-side (consume-only,
publish-direct, publish-Tor, NAT traversal, open):
- [../how-to/enable-nostr-discovery.md](../how-to/enable-nostr-discovery.md)
— five scenarios with minimal YAML fragments.
For the design and security model:
- [../design/fips-nostr-discovery.md](../design/fips-nostr-discovery.md)
— discovery runtime architecture, advert format, threat
model.
For the wire-format details:
- [../reference/nostr-events.md](../reference/nostr-events.md)
— Kind 37195 advert format, Kind 21059 traversal signaling,
Kind 10050 inbox-relay list.