mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Merge the open !FIPS access SSID layer from GitHub
This commit is contained in:
@@ -286,6 +286,7 @@ jobs:
|
|||||||
"$FILES_DIR/etc/hotplug.d/net/99-fips"
|
"$FILES_DIR/etc/hotplug.d/net/99-fips"
|
||||||
"$FILES_DIR/etc/uci-defaults/90-fips-setup"
|
"$FILES_DIR/etc/uci-defaults/90-fips-setup"
|
||||||
"$FILES_DIR/usr/bin/fips-mesh-setup"
|
"$FILES_DIR/usr/bin/fips-mesh-setup"
|
||||||
|
"$FILES_DIR/usr/bin/fips-ap-setup"
|
||||||
)
|
)
|
||||||
fail=0
|
fail=0
|
||||||
for f in "${TARGETS[@]}"; do
|
for f in "${TARGETS[@]}"; do
|
||||||
@@ -406,6 +407,7 @@ jobs:
|
|||||||
./usr/bin/fipstop
|
./usr/bin/fipstop
|
||||||
./usr/bin/fips-gateway
|
./usr/bin/fips-gateway
|
||||||
./usr/bin/fips-mesh-setup
|
./usr/bin/fips-mesh-setup
|
||||||
|
./usr/bin/fips-ap-setup
|
||||||
./etc/init.d/fips
|
./etc/init.d/fips
|
||||||
./etc/init.d/fips-gateway
|
./etc/init.d/fips-gateway
|
||||||
./etc/fips/fips.yaml
|
./etc/fips/fips.yaml
|
||||||
@@ -719,7 +721,7 @@ jobs:
|
|||||||
|
|
||||||
for path in \
|
for path in \
|
||||||
usr/bin/fips usr/bin/fipsctl usr/bin/fipstop usr/bin/fips-gateway \
|
usr/bin/fips usr/bin/fipsctl usr/bin/fipstop usr/bin/fips-gateway \
|
||||||
usr/bin/fips-mesh-setup \
|
usr/bin/fips-mesh-setup usr/bin/fips-ap-setup \
|
||||||
etc/init.d/fips etc/init.d/fips-gateway \
|
etc/init.d/fips etc/init.d/fips-gateway \
|
||||||
etc/fips/fips.yaml etc/fips/firewall.sh etc/dnsmasq.d/fips.conf \
|
etc/fips/fips.yaml etc/fips/firewall.sh etc/dnsmasq.d/fips.conf \
|
||||||
etc/sysctl.d/fips-gateway.conf etc/sysctl.d/fips-bridge.conf \
|
etc/sysctl.d/fips-gateway.conf etc/sysctl.d/fips-bridge.conf \
|
||||||
|
|||||||
@@ -26,4 +26,5 @@ X" to "X is done".
|
|||||||
| [host-aliases.md](host-aliases.md) | Use shortnames (`test-us01.fips`, `my-laptop.fips`) instead of full npubs by editing `/etc/fips/hosts` or setting peer aliases |
|
| [host-aliases.md](host-aliases.md) | Use shortnames (`test-us01.fips`, `my-laptop.fips`) instead of full npubs by editing `/etc/fips/hosts` or setting peer aliases |
|
||||||
| [set-up-bluetooth-peer.md](set-up-bluetooth-peer.md) | Configure a Bluetooth Low Energy peer link |
|
| [set-up-bluetooth-peer.md](set-up-bluetooth-peer.md) | Configure a Bluetooth Low Energy peer link |
|
||||||
| [set-up-80211s-mesh-backhaul.md](set-up-80211s-mesh-backhaul.md) | Link OpenWrt FIPS routers over an open 802.11s radio backhaul (FIPS provides encryption, authentication, and routing) |
|
| [set-up-80211s-mesh-backhaul.md](set-up-80211s-mesh-backhaul.md) | Link OpenWrt FIPS routers over an open 802.11s radio backhaul (FIPS provides encryption, authentication, and routing) |
|
||||||
|
| [set-up-open-access-ssid.md](set-up-open-access-ssid.md) | Broadcast the open `!FIPS` access SSID so phones and laptops roam onto the mesh (one ESS: save once, roam every FIPS router) |
|
||||||
| [diagnose-mtu-issues.md](diagnose-mtu-issues.md) | Triage MTU-shaped failures and rule out their imposters (bufferbloat, transport saturation) |
|
| [diagnose-mtu-issues.md](diagnose-mtu-issues.md) | Triage MTU-shaped failures and rule out their imposters (bufferbloat, transport saturation) |
|
||||||
|
|||||||
@@ -21,12 +21,14 @@ Two deliberate choices distinguish this from a stock 802.11s setup:
|
|||||||
handshake, so SAE at L2 would duplicate that work, add a shared
|
handshake, so SAE at L2 would duplicate that work, add a shared
|
||||||
credential to provision across routers, and (on ath10k) force the
|
credential to provision across routers, and (on ath10k) force the
|
||||||
firmware into its slower raw Tx/Rx mode. A stranger can form an
|
firmware into its slower raw Tx/Rx mode. A stranger can form an
|
||||||
802.11s peering with your router, but their frames die at the FIPS
|
802.11s peering with your router *and* a FIPS peer link on top of it —
|
||||||
handshake — the same security model as mDNS and BLE discovery, where
|
the same open model as mDNS and BLE discovery, where the advert is
|
||||||
the advert is only a hint and the handshake is the authentication.
|
only a hint and the handshake authenticates each link (no
|
||||||
What you concede: L2 metadata (MAC addresses, frame sizes) is
|
impersonation, no MITM) rather than gating who may peer. Admission is
|
||||||
visible in the air, and a hostile radio can burn airtime — both true
|
open up to the daemon's max-peers cap. What you concede: any nearby
|
||||||
of any radio link regardless of L2 encryption.
|
radio can peer and reach the FIPS overlay surface; L2 metadata (MAC
|
||||||
|
addresses, frame sizes) is visible in the air; a hostile radio can
|
||||||
|
burn airtime — all inherent to an open radio link.
|
||||||
- **`mesh_fwding 0`** — disables 802.11s's own HWMP routing so each
|
- **`mesh_fwding 0`** — disables 802.11s's own HWMP routing so each
|
||||||
mesh link is a plain neighbor link. FIPS is the routing layer; two
|
mesh link is a plain neighbor link. FIPS is the routing layer; two
|
||||||
routing layers would fight, and broadcast discovery beacons would
|
routing layers would fight, and broadcast discovery beacons would
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
# Set Up the Open FIPS Access SSID (OpenWrt)
|
||||||
|
|
||||||
|
Give phones and laptops a way in: every FIPS router broadcasts the
|
||||||
|
same open SSID — `!FIPS` — from its access radio. Same SSID + unique
|
||||||
|
BSSIDs is one standard ESS, so a client saves the network once and
|
||||||
|
roams between all FIPS routers natively, with no per-router setup and
|
||||||
|
no shared credentials (the Freifunk model). The leading `!` sorts the
|
||||||
|
network to the top of alphabetically ordered pickers (iOS, desktop
|
||||||
|
OSes — Android sorts by signal strength) and is part of the name:
|
||||||
|
SSIDs match byte-for-byte or not at all. The radio layer provides
|
||||||
|
nothing but open L2 to the nearest router; FIPS provides everything
|
||||||
|
else: encryption and authentication (Noise IK), discovery
|
||||||
|
(mDNS/Ethernet beacons), and mobility (the overlay identity survives
|
||||||
|
roaming, so no 802.11r or L2 tricks are needed).
|
||||||
|
|
||||||
|
This is the *access* layer — how clients reach FIPS routers. For the
|
||||||
|
router-to-router *backhaul*, see
|
||||||
|
[set-up-80211s-mesh-backhaul.md](set-up-80211s-mesh-backhaul.md).
|
||||||
|
For all `transports.ethernet.*` configuration keys, see
|
||||||
|
[../reference/configuration.md](../reference/configuration.md).
|
||||||
|
|
||||||
|
## Why open, why this addressing
|
||||||
|
|
||||||
|
Three deliberate choices distinguish this from a stock guest network:
|
||||||
|
|
||||||
|
- **`encryption none`** — the SSID is open on purpose, and it *must*
|
||||||
|
be. Clients key a saved network on SSID **plus security type**: if
|
||||||
|
one router used a PSK and another OWE, the same `FIPS` name would be
|
||||||
|
three different saved networks and roaming would break. Open is the
|
||||||
|
only security type that needs zero provisioning, and OWE is left out
|
||||||
|
for now for exactly this uniformity reason (OWE-transition mode is
|
||||||
|
inconsistent across client vendors). Every FIPS peer link is already
|
||||||
|
authenticated and encrypted by the Noise IK handshake. A stranger
|
||||||
|
can associate *and* form a FIPS peer link — that is the point of open
|
||||||
|
access; the handshake authenticates each link (no impersonation, no
|
||||||
|
MITM) but does not gate who may peer, and admission is open up to the
|
||||||
|
daemon's max-peers cap. What confines a hostile peer is the isolated
|
||||||
|
`fips_ap` zone (no path to br-lan or the WAN — see below), not the
|
||||||
|
handshake. What you concede: any nearby device can reach the FIPS
|
||||||
|
overlay surface (handshake, discovery, lookup, routing) and peer with
|
||||||
|
the router; L2 metadata is visible in the air; a hostile radio can
|
||||||
|
burn airtime — all inherent to an open radio link.
|
||||||
|
- **DHCPv4 from a fixed subnet, plus IPv6 router advertisements.**
|
||||||
|
dnsmasq leases IPv4 out of `10.21.<N>.0/24` (`N` = the radio index;
|
||||||
|
the prefix echoes FIPS port 2121). The subnet is deliberately
|
||||||
|
**identical on every router**: a roaming phone keeps its lease
|
||||||
|
across routers, and dnsmasq's authoritative mode (the OpenWrt
|
||||||
|
default, pinned by the helper) ACKs a renew the new router never
|
||||||
|
issued. odhcpd additionally announces a ULA prefix (`fd..`-range)
|
||||||
|
for stateless SLAAC; DHCPv6 stays off. FIPS itself only needs
|
||||||
|
link-local + mDNS, but Android's provisioning check requires an RA
|
||||||
|
or a DHCP offer and *disconnects* with neither, and plain laptops
|
||||||
|
expect a real IPv4 address. Works with or without an upstream —
|
||||||
|
nothing here depends on the WAN. The IPv6 side stays per-router and
|
||||||
|
disposable; in all cases the FIPS overlay identity, not the IP, is
|
||||||
|
the mobility anchor.
|
||||||
|
- **Isolated interface** — its own network and firewall zone, with no
|
||||||
|
path to `br-lan` and no forwarding to the WAN. Inbound traffic is
|
||||||
|
rejected except DHCPv4, ICMPv6 (SLAAC itself), mDNS, and the FIPS
|
||||||
|
transport ports; the raw-Ethernet transport (EtherType 0x2121) is
|
||||||
|
not IP and never traverses the firewall. AP client isolation is on, so clients
|
||||||
|
cannot reach each other at L2 — two FIPS phones on one router still
|
||||||
|
reach each other through the router at the overlay layer.
|
||||||
|
|
||||||
|
## The "no internet" behavior (expected, one-time acceptance)
|
||||||
|
|
||||||
|
The network intentionally provides **no internet**. On first connect,
|
||||||
|
a phone's validation probe fails and it asks whether to stay on a
|
||||||
|
network without internet access — choose **stay connected** and
|
||||||
|
**don't ask again**. That choice is stored per SSID, so accepting it
|
||||||
|
once covers every FIPS router anywhere.
|
||||||
|
|
||||||
|
After that, the network is marked "connected, no internet"
|
||||||
|
(unvalidated) and the phone keeps **cellular as its default route**
|
||||||
|
while staying associated — normal apps never notice the FIPS network
|
||||||
|
exists. FIPS apps bind their sockets to the Wi-Fi network explicitly,
|
||||||
|
so mesh traffic flows over Wi-Fi while everything else uses cellular.
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
|
||||||
|
- Any FIPS router that should serve phones and laptops directly, not
|
||||||
|
just peer with other routers.
|
||||||
|
- You want clients to roam between FIPS routers with zero per-router
|
||||||
|
or per-site configuration.
|
||||||
|
|
||||||
|
It is the complement of the 802.11s backhaul: the backhaul links
|
||||||
|
routers (clients cannot join it), the access SSID admits clients.
|
||||||
|
Both can share a radio, at an airtime cost (see constraints).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- OpenWrt 22.03+ with the FIPS package installed (fw4; dnsmasq and
|
||||||
|
odhcpd are part of the default images).
|
||||||
|
- Any radio — AP mode needs no special driver support.
|
||||||
|
|
||||||
|
## Step 1 — create the access point(s)
|
||||||
|
|
||||||
|
On **each** router, run the helper once per radio that should serve
|
||||||
|
clients:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fips-ap-setup radio0
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates an open AP with SSID `!FIPS` and client isolation, an
|
||||||
|
isolated network with `10.21.<N>.1/24` and a static ULA `/64`, a
|
||||||
|
DHCPv4 + RA dhcp config (dnsmasq leases, SLAAC, no DHCPv6), and a
|
||||||
|
locked-down `fips_ap` firewall zone — then reloads the radio. Interfaces are named by radio index: `radio0` →
|
||||||
|
`fips-ap0`, `radio1` → `fips-ap1`. Pass a second argument to use a
|
||||||
|
different SSID — but the SSID, like the security type, must be
|
||||||
|
identical on **all** routers or clients will treat them as separate
|
||||||
|
networks and stop roaming.
|
||||||
|
|
||||||
|
On dual-band routers, run it for both radios so clients can pick
|
||||||
|
either band:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fips-ap-setup radio0
|
||||||
|
fips-ap-setup radio1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Channels are free per router.** Unlike the mesh backhaul, there is
|
||||||
|
no same-channel constraint — clients scan when they roam — so leave
|
||||||
|
each router on whatever channel suits its RF environment.
|
||||||
|
|
||||||
|
Equivalent manual UCI (per radio), if you prefer to see what it does
|
||||||
|
(`fdxx:...` stands for a `/64` out of the router's ULA prefix):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uci batch <<'EOF'
|
||||||
|
set wireless.fips_ap_radio0=wifi-iface
|
||||||
|
set wireless.fips_ap_radio0.device='radio0'
|
||||||
|
set wireless.fips_ap_radio0.mode='ap'
|
||||||
|
set wireless.fips_ap_radio0.ssid='!FIPS'
|
||||||
|
set wireless.fips_ap_radio0.encryption='none'
|
||||||
|
set wireless.fips_ap_radio0.isolate='1'
|
||||||
|
set wireless.fips_ap_radio0.ifname='fips-ap0'
|
||||||
|
set wireless.fips_ap_radio0.network='fips_ap_radio0'
|
||||||
|
set network.fips_ap_radio0=interface
|
||||||
|
set network.fips_ap_radio0.proto='static'
|
||||||
|
set network.fips_ap_radio0.ipaddr='10.21.0.1'
|
||||||
|
set network.fips_ap_radio0.netmask='255.255.255.0'
|
||||||
|
set network.fips_ap_radio0.ip6addr='fdxx:xxxx:xxxx:fa00::1/64'
|
||||||
|
set dhcp.fips_ap_radio0=dhcp
|
||||||
|
set dhcp.fips_ap_radio0.interface='fips_ap_radio0'
|
||||||
|
set dhcp.fips_ap_radio0.ra='server'
|
||||||
|
set dhcp.fips_ap_radio0.ra_default='2'
|
||||||
|
set dhcp.fips_ap_radio0.dhcpv6='disabled'
|
||||||
|
set dhcp.fips_ap_radio0.dhcpv4='server'
|
||||||
|
set dhcp.fips_ap_radio0.start='10'
|
||||||
|
set dhcp.fips_ap_radio0.limit='200'
|
||||||
|
EOF
|
||||||
|
uci commit
|
||||||
|
wifi reload
|
||||||
|
```
|
||||||
|
|
||||||
|
plus the `fips_ap` firewall zone (input/forward REJECT, no
|
||||||
|
forwardings, ACCEPT rules for DHCPv4/UDP 67, ICMPv6, UDP 5353/2121,
|
||||||
|
TCP 8443).
|
||||||
|
|
||||||
|
## Step 2 — check the FIPS transport binding
|
||||||
|
|
||||||
|
The `fips.yaml` shipped in the OpenWrt package carries one transport
|
||||||
|
entry per access interface, but **commented out** — so a stock install
|
||||||
|
that never runs this helper logs no per-boot "interface missing"
|
||||||
|
warning. `fips-ap-setup` uncommented the matching `apN` entry in Step 1,
|
||||||
|
and also enabled `node.rendezvous.lan` (the daemon's mDNS/DNS-SD
|
||||||
|
rendezvous — phone FIPS apps cannot see raw-Ethernet beacons, so mDNS
|
||||||
|
is how they find the daemon; the switch is daemon-wide and stays on if
|
||||||
|
you later remove the AP). So there is normally nothing to do here. If
|
||||||
|
you maintain your own config (or ran the manual UCI above instead of
|
||||||
|
the helper), make sure both are present and uncommented:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
node:
|
||||||
|
rendezvous:
|
||||||
|
lan:
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
transports:
|
||||||
|
ethernet:
|
||||||
|
ap0:
|
||||||
|
interface: "fips-ap0"
|
||||||
|
discovery: true
|
||||||
|
announce: true
|
||||||
|
auto_connect: true
|
||||||
|
accept_connections: true
|
||||||
|
ap1:
|
||||||
|
interface: "fips-ap1"
|
||||||
|
discovery: true
|
||||||
|
announce: true
|
||||||
|
auto_connect: true
|
||||||
|
accept_connections: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3 — restart the daemon (order matters)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
/etc/init.d/fips restart
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart fips **after** the AP interface is up. A transport whose
|
||||||
|
interface is missing at startup is logged and skipped, not retried —
|
||||||
|
so if the daemon comes up before the radio, the access transport
|
||||||
|
stays dead until the next restart. (An interface that *vanishes and
|
||||||
|
returns* after startup is recovered automatically; only the missing-
|
||||||
|
at-startup case needs this ordering.)
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
L2 and addressing first, with a phone or laptop connected to `!FIPS`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
iw dev fips-ap0 station dump # one entry per associated client
|
||||||
|
ip addr show dev fips-ap0 # 10.21.0.1/24 and the fd..::1/64
|
||||||
|
cat /tmp/dhcp.leases # one lease per connected client
|
||||||
|
```
|
||||||
|
|
||||||
|
No station entries means a radio problem; an association that drops
|
||||||
|
after ~30 s usually means the client never got an address — check
|
||||||
|
`logread | grep -e dnsmasq -e odhcpd` and that the
|
||||||
|
`dhcp.fips_ap_radio0` section survived
|
||||||
|
(`uci show dhcp | grep fips_ap`).
|
||||||
|
|
||||||
|
Then the FIPS layer on top, for a client running FIPS:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
logread | grep -i beacon # beacons flowing on the new transport
|
||||||
|
fipsctl show peers # client authenticated and connected
|
||||||
|
```
|
||||||
|
|
||||||
|
On the phone itself: the network shows "connected, no internet" and
|
||||||
|
stays associated — that is the designed steady state, not an error.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- **SSID and security type must be uniform across ALL routers.**
|
||||||
|
One router with a PSK (or OWE) under the same name splits the ESS
|
||||||
|
into different saved networks and silently breaks roaming. Never
|
||||||
|
"harden" a single router.
|
||||||
|
- **Airtime is shared per radio.** An access AP and a mesh backhaul
|
||||||
|
on the same radio share one channel. On dual/tri-band hardware,
|
||||||
|
dedicate a band to the backhaul and serve clients on the others.
|
||||||
|
- **Strangers can associate and peer — by design.** Open access means
|
||||||
|
any nearby device can complete the Noise handshake and become a FIPS
|
||||||
|
peer (up to the max-peers cap); the handshake authenticates each link,
|
||||||
|
it does not restrict who joins. They reach only the FIPS overlay
|
||||||
|
surface — the isolated zone gives no path to br-lan or the WAN. Do not
|
||||||
|
add forwardings to the `fips_ap` zone: that would turn the open SSID
|
||||||
|
into a hotspot and hand the isolation away.
|
||||||
|
- **Roaming is client-driven.** Clients decide when to hop BSSIDs
|
||||||
|
(standard ESS behavior); the IPv4 lease survives the hop (same
|
||||||
|
subnet everywhere), the SLAAC address renumbers, and FIPS sessions
|
||||||
|
ride through because the overlay identity is the anchor. Expect a
|
||||||
|
brief L2 gap during the hop, as on any ESS without 802.11r.
|
||||||
|
- **The `10.21.<N>.0/24` convention must hold everywhere.** Lease
|
||||||
|
survival depends on every router serving the same subnet from the
|
||||||
|
same radio index — the helper guarantees this; don't hand-pick
|
||||||
|
per-router subnets. Two routers can lease the same address to two
|
||||||
|
different clients; after a roam the conflict is caught (dnsmasq
|
||||||
|
NAKs a renew for an address in use) and the client re-DHCPs. If a
|
||||||
|
laptop is *also* wired to a LAN that really uses `10.21.<N>.0/24`,
|
||||||
|
its routing table will conflict — a corner case worth knowing, not
|
||||||
|
designing around: the zone forwards nowhere, so the FIPS side never
|
||||||
|
reaches beyond the router either way.
|
||||||
@@ -183,6 +183,7 @@ install -m 0755 "$RELEASE_DIR/fipsctl" "$STAGE_DIR/usr/bin/fipsctl"
|
|||||||
install -m 0755 "$RELEASE_DIR/fipstop" "$STAGE_DIR/usr/bin/fipstop"
|
install -m 0755 "$RELEASE_DIR/fipstop" "$STAGE_DIR/usr/bin/fipstop"
|
||||||
install -m 0755 "$RELEASE_DIR/fips-gateway" "$STAGE_DIR/usr/bin/fips-gateway"
|
install -m 0755 "$RELEASE_DIR/fips-gateway" "$STAGE_DIR/usr/bin/fips-gateway"
|
||||||
install -m 0755 "$FILES_DIR/usr/bin/fips-mesh-setup" "$STAGE_DIR/usr/bin/fips-mesh-setup"
|
install -m 0755 "$FILES_DIR/usr/bin/fips-mesh-setup" "$STAGE_DIR/usr/bin/fips-mesh-setup"
|
||||||
|
install -m 0755 "$FILES_DIR/usr/bin/fips-ap-setup" "$STAGE_DIR/usr/bin/fips-ap-setup"
|
||||||
|
|
||||||
install -d "$STAGE_DIR/etc/init.d"
|
install -d "$STAGE_DIR/etc/init.d"
|
||||||
install -m 0755 "$FILES_DIR/etc/init.d/fips" "$STAGE_DIR/etc/init.d/fips"
|
install -m 0755 "$FILES_DIR/etc/init.d/fips" "$STAGE_DIR/etc/init.d/fips"
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ define Package/fips/install
|
|||||||
# 802.11s mesh backhaul setup helper
|
# 802.11s mesh backhaul setup helper
|
||||||
$(INSTALL_BIN) $(CURDIR)/files/usr/bin/fips-mesh-setup $(1)/usr/bin/fips-mesh-setup
|
$(INSTALL_BIN) $(CURDIR)/files/usr/bin/fips-mesh-setup $(1)/usr/bin/fips-mesh-setup
|
||||||
|
|
||||||
|
# Open "FIPS" access SSID setup helper
|
||||||
|
$(INSTALL_BIN) $(CURDIR)/files/usr/bin/fips-ap-setup $(1)/usr/bin/fips-ap-setup
|
||||||
|
|
||||||
# procd init script
|
# procd init script
|
||||||
$(INSTALL_DIR) $(1)/etc/init.d
|
$(INSTALL_DIR) $(1)/etc/init.d
|
||||||
$(INSTALL_BIN) $(CURDIR)/files/etc/init.d/fips $(1)/etc/init.d/fips
|
$(INSTALL_BIN) $(CURDIR)/files/etc/init.d/fips $(1)/etc/init.d/fips
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ install -m 0755 "$RELEASE_DIR/fipsctl" "$DATA_DIR/usr/bin/fipsctl"
|
|||||||
install -m 0755 "$RELEASE_DIR/fipstop" "$DATA_DIR/usr/bin/fipstop"
|
install -m 0755 "$RELEASE_DIR/fipstop" "$DATA_DIR/usr/bin/fipstop"
|
||||||
install -m 0755 "$RELEASE_DIR/fips-gateway" "$DATA_DIR/usr/bin/fips-gateway"
|
install -m 0755 "$RELEASE_DIR/fips-gateway" "$DATA_DIR/usr/bin/fips-gateway"
|
||||||
install -m 0755 "$FILES_DIR/usr/bin/fips-mesh-setup" "$DATA_DIR/usr/bin/fips-mesh-setup"
|
install -m 0755 "$FILES_DIR/usr/bin/fips-mesh-setup" "$DATA_DIR/usr/bin/fips-mesh-setup"
|
||||||
|
install -m 0755 "$FILES_DIR/usr/bin/fips-ap-setup" "$DATA_DIR/usr/bin/fips-ap-setup"
|
||||||
|
|
||||||
install -d "$DATA_DIR/etc/init.d"
|
install -d "$DATA_DIR/etc/init.d"
|
||||||
install -m 0755 "$FILES_DIR/etc/init.d/fips" "$DATA_DIR/etc/init.d/fips"
|
install -m 0755 "$FILES_DIR/etc/init.d/fips" "$DATA_DIR/etc/init.d/fips"
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ node:
|
|||||||
# - "stun:stun.cloudflare.com:3478"
|
# - "stun:stun.cloudflare.com:3478"
|
||||||
# - "stun:global.stun.twilio.com:3478"
|
# - "stun:global.stun.twilio.com:3478"
|
||||||
|
|
||||||
|
# mDNS/DNS-SD peer rendezvous on the local link. Ships commented (the
|
||||||
|
# daemon default is off); 'fips-ap-setup' uncomments it when creating
|
||||||
|
# the access SSID — phone FIPS apps cannot see raw-Ethernet beacons,
|
||||||
|
# so mDNS is how they find this router's daemon. Daemon-wide switch,
|
||||||
|
# left enabled on 'fips-ap-setup remove'.
|
||||||
|
# lan:
|
||||||
|
# enabled: true
|
||||||
|
|
||||||
tun:
|
tun:
|
||||||
enabled: true
|
enabled: true
|
||||||
name: fips0
|
name: fips0
|
||||||
@@ -64,7 +72,11 @@ dns:
|
|||||||
|
|
||||||
transports:
|
transports:
|
||||||
udp:
|
udp:
|
||||||
bind_addr: "0.0.0.0:2121"
|
# Dual-stack wildcard, not "0.0.0.0": access-SSID clients (phones) learn
|
||||||
|
# this node's addresses from the mDNS advert and prefer the IPv6
|
||||||
|
# link-local — a v4-only bind silently drops their Noise msg1.
|
||||||
|
# OpenWrt is Linux (bindv6only=0), so "[::]" accepts v4 too.
|
||||||
|
bind_addr: "[::]:2121"
|
||||||
# advertise_on_nostr: true
|
# advertise_on_nostr: true
|
||||||
# public: false # false => advertise udp:nat; true => advertise bound host:port
|
# public: false # false => advertise udp:nat; true => advertise bound host:port
|
||||||
# accept_connections: true # default; refuse inbound msg1 when false
|
# accept_connections: true # default; refuse inbound msg1 when false
|
||||||
@@ -126,6 +138,30 @@ transports:
|
|||||||
# auto_connect: true
|
# auto_connect: true
|
||||||
# accept_connections: true
|
# accept_connections: true
|
||||||
|
|
||||||
|
# Open "!FIPS" access SSID for phones and laptops running FIPS. These
|
||||||
|
# entries ship commented out so a stock install that never creates
|
||||||
|
# fips-ap* logs no per-boot "interface missing" bind warning. Running
|
||||||
|
# 'fips-ap-setup <radio>' creates the interface AND uncomments the
|
||||||
|
# matching block here (once per radio; radio0 -> fips-ap0, radio1 ->
|
||||||
|
# fips-ap1); 'fips-ap-setup remove' re-comments it. Restart fips after
|
||||||
|
# — a transport whose interface is missing at startup is skipped, not
|
||||||
|
# retried. The SSID is OPEN and isolated on purpose: FIPS's Noise
|
||||||
|
# handshake is the only security layer, and associated clients reach
|
||||||
|
# nothing but the FIPS handshake surface. See
|
||||||
|
# docs/how-to/set-up-open-access-ssid.md.
|
||||||
|
# ap0:
|
||||||
|
# interface: "fips-ap0"
|
||||||
|
# discovery: true
|
||||||
|
# announce: true
|
||||||
|
# auto_connect: true
|
||||||
|
# accept_connections: true
|
||||||
|
# ap1:
|
||||||
|
# interface: "fips-ap1"
|
||||||
|
# discovery: true
|
||||||
|
# announce: true
|
||||||
|
# auto_connect: true
|
||||||
|
# accept_connections: true
|
||||||
|
|
||||||
# Bluetooth Low Energy transport — requires BlueZ and the 'ble' feature.
|
# Bluetooth Low Energy transport — requires BlueZ and the 'ble' feature.
|
||||||
# ble:
|
# ble:
|
||||||
# adapter: "hci0"
|
# adapter: "hci0"
|
||||||
|
|||||||
+412
@@ -0,0 +1,412 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# fips-ap-setup — configure the open "FIPS" access SSID for phones/laptops.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# fips-ap-setup <radio> [ssid] e.g. fips-ap-setup radio0
|
||||||
|
# fips-ap-setup remove [radio] no radio: remove all instances
|
||||||
|
#
|
||||||
|
# Creates an open AP on the given radio so client devices running FIPS can
|
||||||
|
# reach the router. Every FIPS router broadcasts the SAME SSID ("!FIPS" by
|
||||||
|
# default — the leading '!' sorts it to the top of alphabetically ordered
|
||||||
|
# network pickers): same SSID + unique BSSIDs is one standard ESS, so a
|
||||||
|
# phone saves the network once and roams between all FIPS routers natively.
|
||||||
|
#
|
||||||
|
# - encryption 'none' — the AP is OPEN on purpose. FIPS's Noise IK
|
||||||
|
# handshake authenticates and encrypts everything above the radio, and
|
||||||
|
# the security type must be uniform across ALL routers anyway: clients
|
||||||
|
# key a saved network on SSID + security type, so one router with a PSK
|
||||||
|
# splits the ESS into a different saved network. A stranger can
|
||||||
|
# associate AND form a FIPS peer link — that is the point of open
|
||||||
|
# access. The Noise handshake authenticates each link (no
|
||||||
|
# impersonation of another identity, no MITM); it does NOT gate who
|
||||||
|
# may peer. Admission is open up to the daemon's max-peers cap; the
|
||||||
|
# firewall zone below is what confines every client to the FIPS
|
||||||
|
# overlay (no path to br-lan or the WAN).
|
||||||
|
# - DHCPv4 + RA IPv6 — dnsmasq serves DHCPv4 from a FIXED subnet,
|
||||||
|
# 10.21.<N>.0/24 (echoes FIPS port 2121), identical on every router:
|
||||||
|
# a roaming phone keeps its lease across routers, and dnsmasq's
|
||||||
|
# authoritative mode (the OpenWrt default) ACKs the renew a foreign
|
||||||
|
# router never issued. odhcpd additionally announces a ULA prefix in
|
||||||
|
# router advertisements (stateless SLAAC); DHCPv6 stays off. FIPS
|
||||||
|
# itself only needs link-local + mDNS, but client provisioning checks
|
||||||
|
# (Android disconnects without an RA or a DHCP offer) and plain
|
||||||
|
# laptops both want a real address. The network provides no internet,
|
||||||
|
# so phones mark it unvalidated and keep cellular as the default
|
||||||
|
# route while staying associated.
|
||||||
|
# - ISOLATED — own network and firewall zone: no path to
|
||||||
|
# br-lan, no forwarding to the WAN, and AP client isolation on.
|
||||||
|
# Associated clients reach only the FIPS handshake surface.
|
||||||
|
#
|
||||||
|
# Interfaces are named per radio index (radio0 -> fips-ap0, radio1 ->
|
||||||
|
# fips-ap1). Unlike the 802.11s backhaul there is NO same-channel
|
||||||
|
# constraint — clients scan when they roam, so every router picks its
|
||||||
|
# access channels freely.
|
||||||
|
#
|
||||||
|
# The shipped /etc/fips/fips.yaml carries 'ap0' and 'ap1' entries under
|
||||||
|
# 'transports.ethernet' bound to these names, but commented out — a stock
|
||||||
|
# install that never creates fips-ap* then logs no bind warning. This
|
||||||
|
# helper uncomments the matching entry when it creates an interface and
|
||||||
|
# re-comments it on remove, so the daemon binds the transport without a
|
||||||
|
# manual config edit. It also uncomments the node.rendezvous.lan block
|
||||||
|
# (mDNS/DNS-SD — how phone FIPS apps discover the daemon); that switch is
|
||||||
|
# daemon-wide and stays on at remove. After an interface is up, restart
|
||||||
|
# fips.
|
||||||
|
# See docs/how-to/set-up-open-access-ssid.md for the full guide.
|
||||||
|
|
||||||
|
DEFAULT_SSID="!FIPS"
|
||||||
|
CONFIG="/etc/fips/fips.yaml"
|
||||||
|
|
||||||
|
# Replace $CONFIG with the rewritten $CONFIG.tmp. Force mode 0600 first: the
|
||||||
|
# package installs fips.yaml 0600 (it may hold an inline 'nsec' private key),
|
||||||
|
# and a fresh tmp file would otherwise land world-readable after the move.
|
||||||
|
ap_config_write() {
|
||||||
|
chmod 600 "$CONFIG.tmp" && mv "$CONFIG.tmp" "$CONFIG"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Uncomment the 'ap<idx>' transports.ethernet block in $CONFIG (created by
|
||||||
|
# 'fips-ap-setup'). Reversible with ap_config_disable. Returns:
|
||||||
|
# 0 enabled (or already active) 1 no config file 2 no such block
|
||||||
|
ap_config_enable() {
|
||||||
|
idx="$1"
|
||||||
|
[ -f "$CONFIG" ] || return 1
|
||||||
|
grep -q "^ ap$idx:" "$CONFIG" && return 0
|
||||||
|
grep -q "^ # ap$idx:" "$CONFIG" || return 2
|
||||||
|
awk -v idx="$idx" '
|
||||||
|
$0 ~ ("^ # ap" idx ":[ \t]*$") { blk = 1; sub(/^ # /, " "); print; next }
|
||||||
|
blk && /^ # / { sub(/^ # /, " "); print; next }
|
||||||
|
{ blk = 0; print }
|
||||||
|
' "$CONFIG" > "$CONFIG.tmp" && ap_config_write
|
||||||
|
}
|
||||||
|
|
||||||
|
# Uncomment the 'lan' block under node.rendezvous in $CONFIG — the daemon's
|
||||||
|
# mDNS/DNS-SD responder+browser. Phone FIPS apps cannot open raw-Ethernet
|
||||||
|
# sockets, so mDNS is how they find this router's daemon. The match is
|
||||||
|
# scoped to node.rendezvous: transports.ethernet also has a 'lan' entry at
|
||||||
|
# the same indent. Daemon-wide switch — enabled here, deliberately NOT
|
||||||
|
# re-commented on remove (other transports use it once on). Returns:
|
||||||
|
# 0 enabled (or already active) 1 no config file 2 no such block
|
||||||
|
lan_rendezvous_enable() {
|
||||||
|
[ -f "$CONFIG" ] || return 1
|
||||||
|
state="$(awk '
|
||||||
|
/^[A-Za-z_]/ { top = $1 }
|
||||||
|
top == "node:" && /^ [A-Za-z_]/ { sec = $1 }
|
||||||
|
top == "node:" && sec == "rendezvous:" && /^ lan:[ \t]*$/ { print "active"; exit }
|
||||||
|
top == "node:" && sec == "rendezvous:" && /^ # lan:[ \t]*$/ { print "commented"; exit }
|
||||||
|
' "$CONFIG")"
|
||||||
|
case "$state" in
|
||||||
|
active) return 0 ;;
|
||||||
|
commented) ;;
|
||||||
|
*) return 2 ;;
|
||||||
|
esac
|
||||||
|
awk '
|
||||||
|
/^[A-Za-z_]/ { top = $1 }
|
||||||
|
top == "node:" && /^ [A-Za-z_]/ { sec = $1 }
|
||||||
|
top == "node:" && sec == "rendezvous:" && $0 ~ /^ # lan:[ \t]*$/ { blk = 1; sub(/^ # /, " "); print; next }
|
||||||
|
blk && /^ # / { sub(/^ # /, " "); print; next }
|
||||||
|
{ blk = 0; print }
|
||||||
|
' "$CONFIG" > "$CONFIG.tmp" && ap_config_write
|
||||||
|
}
|
||||||
|
|
||||||
|
# Re-comment the 'ap<idx>' block so the daemon stops binding it (and stops
|
||||||
|
# warning about the now-missing interface). Inverse of ap_config_enable.
|
||||||
|
ap_config_disable() {
|
||||||
|
idx="$1"
|
||||||
|
[ -f "$CONFIG" ] || return 1
|
||||||
|
grep -q "^ ap$idx:" "$CONFIG" || return 0
|
||||||
|
awk -v idx="$idx" '
|
||||||
|
$0 ~ ("^ ap" idx ":[ \t]*$") { blk = 1; sub(/^ /, " # "); print; next }
|
||||||
|
blk && /^ / { sub(/^ /, " # "); print; next }
|
||||||
|
{ blk = 0; print }
|
||||||
|
' "$CONFIG" > "$CONFIG.tmp" && ap_config_write
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: fips-ap-setup <radio> [ssid]" >&2
|
||||||
|
echo " fips-ap-setup remove [radio]" >&2
|
||||||
|
echo "Radios on this device:" >&2
|
||||||
|
uci show wireless 2>/dev/null | sed -n "s/^wireless\.\([^.]*\)=wifi-device$/ \1/p" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# List the UCI section names of fips-managed access-point wifi-ifaces.
|
||||||
|
ap_sections() {
|
||||||
|
uci show wireless 2>/dev/null | sed -n "s/^wireless\.\(fips_ap[^.=]*\)=wifi-iface$/\1/p"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# remove [radio] — delete the wireless, network, dhcp, and firewall sections
|
||||||
|
# created below
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [ "$1" = "remove" ]; then
|
||||||
|
if [ -n "$2" ]; then
|
||||||
|
SECTIONS="fips_ap_$(printf '%s' "$2" | tr -c 'a-zA-Z0-9_' '_')"
|
||||||
|
else
|
||||||
|
SECTIONS="$(ap_sections)"
|
||||||
|
fi
|
||||||
|
[ -n "$SECTIONS" ] || {
|
||||||
|
echo "No fips access-point instances configured."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
for section in $SECTIONS; do
|
||||||
|
ifname="$(uci -q get "wireless.$section.ifname")"
|
||||||
|
uci -q delete "wireless.$section"
|
||||||
|
uci -q delete "network.$section"
|
||||||
|
uci -q delete "dhcp.$section"
|
||||||
|
uci -q del_list "firewall.fips_ap.network=$section"
|
||||||
|
# Re-comment the matching ap<N> transport in fips.yaml so the
|
||||||
|
# daemon stops warning about the interface we just removed.
|
||||||
|
idx="$(printf '%s' "$ifname" | sed -n 's/.*[^0-9]\([0-9]\{1,\}\)$/\1/p')"
|
||||||
|
[ -n "$idx" ] && ap_config_disable "$idx"
|
||||||
|
echo "Removed ${ifname:-$section}."
|
||||||
|
done
|
||||||
|
# Drop the shared zone and its rules once the last instance is gone.
|
||||||
|
if [ -z "$(uci -q get firewall.fips_ap.network)" ]; then
|
||||||
|
uci -q delete firewall.fips_ap
|
||||||
|
uci -q delete firewall.fips_ap_icmpv6
|
||||||
|
uci -q delete firewall.fips_ap_dhcpv4
|
||||||
|
uci -q delete firewall.fips_ap_mdns
|
||||||
|
uci -q delete firewall.fips_ap_fips_udp
|
||||||
|
uci -q delete firewall.fips_ap_fips_tcp
|
||||||
|
fi
|
||||||
|
uci commit wireless
|
||||||
|
uci commit network
|
||||||
|
uci commit dhcp
|
||||||
|
uci commit firewall
|
||||||
|
wifi reload
|
||||||
|
/etc/init.d/dnsmasq reload
|
||||||
|
/etc/init.d/odhcpd reload
|
||||||
|
/etc/init.d/firewall reload
|
||||||
|
echo "Restart fips: /etc/init.d/fips restart"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
RADIO="$1"
|
||||||
|
SSID="${2:-$DEFAULT_SSID}"
|
||||||
|
|
||||||
|
[ -n "$RADIO" ] || usage
|
||||||
|
|
||||||
|
if [ "$(uci -q get "wireless.$RADIO")" != "wifi-device" ]; then
|
||||||
|
echo "Error: '$RADIO' is not a wifi-device in /etc/config/wireless." >&2
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
# One instance per radio: section fips_ap_<radio>, netdev fips-ap<N>
|
||||||
|
# where N is the radio's trailing index (radio0 -> fips-ap0). For radios
|
||||||
|
# named without a trailing number, fall back to the first free index.
|
||||||
|
SECTION="fips_ap_$(printf '%s' "$RADIO" | tr -c 'a-zA-Z0-9_' '_')"
|
||||||
|
IDX="$(printf '%s' "$RADIO" | sed -n 's/.*[^0-9]\([0-9]\{1,\}\)$/\1/p')"
|
||||||
|
[ -n "$IDX" ] || IDX="$(printf '%s' "$RADIO" | sed -n 's/^\([0-9]\{1,\}\)$/\1/p')"
|
||||||
|
if [ -z "$IDX" ]; then
|
||||||
|
IDX=0
|
||||||
|
while uci show wireless 2>/dev/null | grep -q "\.ifname='fips-ap$IDX'"; do
|
||||||
|
IDX=$((IDX + 1))
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
AP_IFNAME="fips-ap$IDX"
|
||||||
|
|
||||||
|
# Refuse a name collision from another radio's instance (e.g. two radios
|
||||||
|
# whose names end in the same digit) rather than silently hijacking it.
|
||||||
|
OWNER="$(uci show wireless 2>/dev/null \
|
||||||
|
| sed -n "s/^wireless\.\(fips_ap[^.=]*\)\.ifname='$AP_IFNAME'$/\1/p")"
|
||||||
|
if [ -n "$OWNER" ] && [ "$OWNER" != "$SECTION" ]; then
|
||||||
|
echo "Error: $AP_IFNAME is already used by section '$OWNER'." >&2
|
||||||
|
echo "Remove it first: fips-ap-setup remove" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Wireless: open AP with client isolation. Clients of the same AP cannot
|
||||||
|
# exchange L2 frames directly — two FIPS phones on one router still reach
|
||||||
|
# each other through the router at the overlay layer. Isolation is an L2
|
||||||
|
# control only: a stranger who peers is an overlay peer like any other, so
|
||||||
|
# the FIPS overlay (not L2) is the trust boundary between clients.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
uci -q delete "wireless.$SECTION"
|
||||||
|
uci set "wireless.$SECTION=wifi-iface"
|
||||||
|
uci set "wireless.$SECTION.device=$RADIO"
|
||||||
|
uci set "wireless.$SECTION.mode=ap"
|
||||||
|
uci set "wireless.$SECTION.ssid=$SSID"
|
||||||
|
uci set "wireless.$SECTION.encryption=none"
|
||||||
|
uci set "wireless.$SECTION.isolate=1"
|
||||||
|
uci set "wireless.$SECTION.ifname=$AP_IFNAME"
|
||||||
|
uci set "wireless.$SECTION.network=$SECTION"
|
||||||
|
|
||||||
|
# Radios ship disabled on fresh OpenWrt installs; a disabled radio would
|
||||||
|
# leave the AP down with no error anywhere visible.
|
||||||
|
if [ "$(uci -q get "wireless.$RADIO.disabled")" = "1" ]; then
|
||||||
|
echo "Note: enabling $RADIO (was disabled)."
|
||||||
|
uci -q delete "wireless.$RADIO.disabled"
|
||||||
|
fi
|
||||||
|
|
||||||
|
CHANNEL="$(uci -q get "wireless.$RADIO.channel")"
|
||||||
|
BAND="$(uci -q get "wireless.$RADIO.band")"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Network: IPv4 from the fixed convention 10.21.<IDX>.1/24 — deterministic,
|
||||||
|
# so every router serving the same radio index lands on the same subnet and
|
||||||
|
# a roaming client's lease stays valid. IPv6 is a static ULA /64 so odhcpd
|
||||||
|
# has a prefix to announce; that space is per-router and disposable — a
|
||||||
|
# roaming phone SLAACs a fresh address on each router, and the FIPS overlay
|
||||||
|
# identity (not the IP) is the mobility anchor. The ULA is derived from the
|
||||||
|
# router's global ULA prefix; a re-run keeps the address already configured.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
AP_ADDR="$(uci -q get "network.$SECTION.ip6addr")"
|
||||||
|
case "$AP_ADDR" in
|
||||||
|
fd*) ;; # keep the existing address on re-run
|
||||||
|
*)
|
||||||
|
ULA_BASE=""
|
||||||
|
ULA_PREFIX="$(uci -q get network.globals.ula_prefix)"
|
||||||
|
case "$ULA_PREFIX" in
|
||||||
|
fd*::/48) ULA_BASE="${ULA_PREFIX%::/48}" ;;
|
||||||
|
esac
|
||||||
|
if [ -z "$ULA_BASE" ]; then
|
||||||
|
HEX="$(head -c 5 /dev/urandom | hexdump -e '5/1 "%02x"')"
|
||||||
|
ULA_BASE="fd$(printf '%s' "$HEX" | cut -c1-2):$(printf '%s' "$HEX" | cut -c3-6):$(printf '%s' "$HEX" | cut -c7-10)"
|
||||||
|
echo "Note: no usable ULA prefix in network.globals — generated $ULA_BASE::/48 for this AP."
|
||||||
|
fi
|
||||||
|
# 64000 = 0xfa00 — high subnet IDs keep clear of br-lan's low
|
||||||
|
# ip6assign allocations from the same ULA prefix.
|
||||||
|
AP_ADDR="$ULA_BASE:$(printf '%04x' $((64000 + IDX)))::1/64"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
AP_ADDR4="10.21.$IDX.1"
|
||||||
|
|
||||||
|
uci -q delete "network.$SECTION"
|
||||||
|
uci set "network.$SECTION=interface"
|
||||||
|
uci set "network.$SECTION.proto=static"
|
||||||
|
uci set "network.$SECTION.ipaddr=$AP_ADDR4"
|
||||||
|
uci set "network.$SECTION.netmask=255.255.255.0"
|
||||||
|
uci set "network.$SECTION.ip6addr=$AP_ADDR"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# DHCP/RA: dnsmasq DHCPv4 leases out of 10.21.<IDX>.0/24, plus router
|
||||||
|
# advertisements for the ULA (stateless SLAAC, no DHCPv6). ra_default '2'
|
||||||
|
# announces a default router even without an upstream default route:
|
||||||
|
# Android's provisioning wants address + route + DNS, and its validation
|
||||||
|
# probe then fails by design (no internet), so the phone keeps cellular as
|
||||||
|
# the default route. 'dhcpv4 server' is read by BOTH dnsmasq (the default
|
||||||
|
# DHCPv4 server) and odhcpd (serves v4 only when odhcpd.maindhcp is set),
|
||||||
|
# so either arrangement hands out leases.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
uci -q delete "dhcp.$SECTION"
|
||||||
|
uci set "dhcp.$SECTION=dhcp"
|
||||||
|
uci set "dhcp.$SECTION.interface=$SECTION"
|
||||||
|
uci set "dhcp.$SECTION.ra=server"
|
||||||
|
uci set "dhcp.$SECTION.ra_default=2"
|
||||||
|
uci set "dhcp.$SECTION.dhcpv6=disabled"
|
||||||
|
uci set "dhcp.$SECTION.dhcpv4=server"
|
||||||
|
uci set "dhcp.$SECTION.start=10"
|
||||||
|
uci set "dhcp.$SECTION.limit=200"
|
||||||
|
|
||||||
|
# Authoritative is the OpenWrt default, but roaming correctness depends on
|
||||||
|
# it (a foreign router must ACK a lease it never issued), so pin it.
|
||||||
|
[ -n "$(uci -q get dhcp.@dnsmasq[0])" ] && uci set dhcp.@dnsmasq[0].authoritative=1
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Firewall: one shared 'fips_ap' zone for all instances. Everything is
|
||||||
|
# rejected except what a FIPS client needs — DHCPv4 (addressing), ICMPv6
|
||||||
|
# (SLAAC itself), mDNS (discovery), and the FIPS UDP/TCP transports (the
|
||||||
|
# handshake surface).
|
||||||
|
# The raw-Ethernet transport (EtherType 0x2121) is not IP and never
|
||||||
|
# traverses the firewall. No forwardings exist, so there is no path to
|
||||||
|
# br-lan or the WAN.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [ "$(uci -q get firewall.fips_ap)" != "zone" ]; then
|
||||||
|
uci set firewall.fips_ap=zone
|
||||||
|
fi
|
||||||
|
uci set firewall.fips_ap.name=fips_ap
|
||||||
|
uci set firewall.fips_ap.input=REJECT
|
||||||
|
uci set firewall.fips_ap.output=ACCEPT
|
||||||
|
uci set firewall.fips_ap.forward=REJECT
|
||||||
|
uci -q del_list "firewall.fips_ap.network=$SECTION"
|
||||||
|
uci add_list "firewall.fips_ap.network=$SECTION"
|
||||||
|
|
||||||
|
# ap_rule <section-suffix> <name> <proto> [dest_port]
|
||||||
|
ap_rule() {
|
||||||
|
rule="firewall.fips_ap_$1"
|
||||||
|
uci -q delete "$rule"
|
||||||
|
uci set "$rule=rule"
|
||||||
|
uci set "$rule.name=$2"
|
||||||
|
uci set "$rule.src=fips_ap"
|
||||||
|
uci set "$rule.proto=$3"
|
||||||
|
uci set "$rule.target=ACCEPT"
|
||||||
|
[ -z "${4:-}" ] || uci set "$rule.dest_port=$4"
|
||||||
|
}
|
||||||
|
|
||||||
|
ap_rule icmpv6 "FIPS-AP-ICMPv6" icmp
|
||||||
|
uci set firewall.fips_ap_icmpv6.family=ipv6
|
||||||
|
ap_rule dhcpv4 "FIPS-AP-DHCPv4" udp 67
|
||||||
|
uci set firewall.fips_ap_dhcpv4.family=ipv4
|
||||||
|
ap_rule mdns "FIPS-AP-mDNS" udp 5353
|
||||||
|
ap_rule fips_udp "FIPS-AP-FIPS-UDP" udp 2121
|
||||||
|
ap_rule fips_tcp "FIPS-AP-FIPS-TCP" tcp 8443
|
||||||
|
|
||||||
|
uci commit wireless
|
||||||
|
uci commit network
|
||||||
|
uci commit dhcp
|
||||||
|
uci commit firewall
|
||||||
|
wifi reload
|
||||||
|
/etc/init.d/dnsmasq reload
|
||||||
|
/etc/init.d/odhcpd reload
|
||||||
|
/etc/init.d/firewall reload
|
||||||
|
|
||||||
|
# Enable the matching ap<N> transport in the shipped fips.yaml (it ships
|
||||||
|
# commented out). Tailor the restart hint to what we could do.
|
||||||
|
ap_config_enable "$IDX"
|
||||||
|
case $? in
|
||||||
|
0) TRANSPORT_NOTE="The ap$IDX transport in $CONFIG that binds '$AP_IFNAME' is
|
||||||
|
now uncommented and enabled." ;;
|
||||||
|
1) TRANSPORT_NOTE="No $CONFIG found — add a transports.ethernet entry binding
|
||||||
|
interface '$AP_IFNAME' by hand." ;;
|
||||||
|
*) TRANSPORT_NOTE="No 'ap$IDX' entry in $CONFIG — add a transports.ethernet
|
||||||
|
entry binding interface '$AP_IFNAME' by hand (copy the ap0 block)." ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Phones discover the daemon via mDNS, not raw-Ethernet beacons — make sure
|
||||||
|
# the daemon-wide mDNS rendezvous is on.
|
||||||
|
if lan_rendezvous_enable; then
|
||||||
|
MDNS_NOTE="node.rendezvous.lan (mDNS) is enabled — phone FIPS apps
|
||||||
|
discover this router via DNS-SD."
|
||||||
|
else
|
||||||
|
MDNS_NOTE="Could not enable mDNS in $CONFIG — set
|
||||||
|
'node.rendezvous.lan.enabled: true' by hand; phone FIPS apps rely
|
||||||
|
on it to discover this router."
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
Created open access SSID '$SSID' as $AP_IFNAME on $RADIO \
|
||||||
|
(band ${BAND:-?}, channel ${CHANNEL:-auto}).
|
||||||
|
DHCPv4 on $AP_ADDR4/24 and RA IPv6 on $AP_ADDR — no internet,
|
||||||
|
isolated from br-lan and the WAN.
|
||||||
|
|
||||||
|
ALL FIPS routers must broadcast this SSID with the same security type
|
||||||
|
(open) — phones then save it once and roam between routers as one
|
||||||
|
network. The 10.21.$IDX.0/24 subnet is the same on every router on
|
||||||
|
purpose: leases survive roaming. Unlike the mesh backhaul, channels
|
||||||
|
are free per router. On a dual-band router, run fips-ap-setup for the
|
||||||
|
other radio too so clients can pick either band.
|
||||||
|
|
||||||
|
On first connect a phone warns that the network has no internet —
|
||||||
|
choose "stay connected" and "don't ask again". That choice is stored
|
||||||
|
per SSID, so it covers every FIPS router.
|
||||||
|
|
||||||
|
Next steps:
|
||||||
|
1. $TRANSPORT_NOTE
|
||||||
|
2. $MDNS_NOTE
|
||||||
|
Restart the daemon AFTER the interface is up — a transport whose
|
||||||
|
interface is missing at startup is skipped, not retried:
|
||||||
|
/etc/init.d/fips restart
|
||||||
|
3. Associate a phone or laptop running FIPS and verify:
|
||||||
|
iw dev $AP_IFNAME station dump
|
||||||
|
and the FIPS link on top of it:
|
||||||
|
fipsctl show peers
|
||||||
|
|
||||||
|
Run 'fips-ap-setup remove' to undo all instances, or
|
||||||
|
'fips-ap-setup remove $RADIO' for just this one.
|
||||||
|
EOF
|
||||||
@@ -14,8 +14,10 @@
|
|||||||
# - encryption 'none' — the mesh is OPEN on purpose. FIPS's Noise IK
|
# - encryption 'none' — the mesh is OPEN on purpose. FIPS's Noise IK
|
||||||
# handshake authenticates and encrypts every peer link, so SAE would
|
# handshake authenticates and encrypts every peer link, so SAE would
|
||||||
# only duplicate that (and on ath10k it forces the slower raw Tx/Rx
|
# only duplicate that (and on ath10k it forces the slower raw Tx/Rx
|
||||||
# firmware mode). A stranger can form an 802.11s peering but cannot
|
# firmware mode). A stranger can form an 802.11s peering AND a FIPS
|
||||||
# pass the FIPS handshake.
|
# peer link — the Noise handshake authenticates each link (no
|
||||||
|
# impersonation of another identity, no MITM), it does not gate who
|
||||||
|
# may peer. Admission is open up to the daemon's max-peers cap.
|
||||||
# - mesh_fwding '0' — disables 802.11s HWMP forwarding so each mesh
|
# - mesh_fwding '0' — disables 802.11s HWMP forwarding so each mesh
|
||||||
# link is a plain L2 neighbor link. FIPS is the routing layer; two
|
# link is a plain L2 neighbor link. FIPS is the routing layer; two
|
||||||
# routing layers would fight.
|
# routing layers would fight.
|
||||||
|
|||||||
+32
-21
@@ -771,21 +771,23 @@ node:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The fips.yaml shipped in the OpenWrt package must keep parsing as the
|
/// The fips.yaml shipped in the OpenWrt package must keep parsing as the
|
||||||
/// config schema evolves. The 802.11s mesh backhaul entries (one per
|
/// config schema evolves. Both the 802.11s mesh backhaul entries
|
||||||
/// radio, so dual-band routers can mesh on both bands) ship commented
|
/// (docs/how-to/set-up-80211s-mesh-backhaul.md) and the open-access SSID
|
||||||
/// out — a stock install that never creates fips-mesh* logs no per-boot
|
/// entries (docs/how-to/set-up-open-access-ssid.md) ship commented out —
|
||||||
/// bind warning; `fips-mesh-setup` uncomments the matching block when it
|
/// one per radio, so dual-band routers can run either on both bands — so
|
||||||
/// creates the interface (docs/how-to/set-up-80211s-mesh-backhaul.md).
|
/// a stock install that never creates fips-mesh*/fips-ap* logs no
|
||||||
/// Verify both states parse: as shipped (mesh inactive), and after the
|
/// per-boot bind warning; `fips-mesh-setup`/`fips-ap-setup` uncomment the
|
||||||
/// uncomment `fips-mesh-setup` performs.
|
/// matching block when they create the interface. Verify both states
|
||||||
|
/// parse: as shipped (both inactive), and after the uncomment the helpers
|
||||||
|
/// perform.
|
||||||
#[test]
|
#[test]
|
||||||
fn shipped_openwrt_config_parses() {
|
fn shipped_openwrt_config_parses() {
|
||||||
let yaml = include_str!("../../packaging/openwrt-ipk/files/etc/fips/fips.yaml");
|
let yaml = include_str!("../../packaging/openwrt-ipk/files/etc/fips/fips.yaml");
|
||||||
|
|
||||||
// As shipped: parses, and the mesh entries are commented out (a
|
// As shipped: parses, and the mesh/ap entries are commented out (a
|
||||||
// running daemon binds no fips-mesh* transport, so no bind warning).
|
// running daemon binds no fips-mesh*/fips-ap* transport, no warning).
|
||||||
let config: Config = serde_yaml::from_str(yaml).expect("shipped OpenWrt fips.yaml");
|
let config: Config = serde_yaml::from_str(yaml).expect("shipped OpenWrt fips.yaml");
|
||||||
for name in ["mesh0", "mesh1"] {
|
for name in ["mesh0", "mesh1", "ap0", "ap1"] {
|
||||||
assert!(
|
assert!(
|
||||||
!config
|
!config
|
||||||
.transports
|
.transports
|
||||||
@@ -796,31 +798,40 @@ node:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// What `fips-mesh-setup` produces: uncomment each mesh block, which
|
// What `fips-mesh-setup`/`fips-ap-setup` produce: uncomment each
|
||||||
// must still parse into a transport entry bound to the right netdev.
|
// block, which must still parse into a transport bound to the right
|
||||||
let config: Config = serde_yaml::from_str(&uncomment_mesh_blocks(yaml))
|
// netdev.
|
||||||
.expect("fips.yaml with mesh transports uncommented");
|
let uncommented =
|
||||||
for (name, interface) in [("mesh0", "fips-mesh0"), ("mesh1", "fips-mesh1")] {
|
uncomment_transport_blocks(&uncomment_transport_blocks(yaml, "mesh"), "ap");
|
||||||
|
let config: Config = serde_yaml::from_str(&uncommented)
|
||||||
|
.expect("fips.yaml with mesh and ap transports uncommented");
|
||||||
|
for (name, interface) in [
|
||||||
|
("mesh0", "fips-mesh0"),
|
||||||
|
("mesh1", "fips-mesh1"),
|
||||||
|
("ap0", "fips-ap0"),
|
||||||
|
("ap1", "fips-ap1"),
|
||||||
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
config
|
config
|
||||||
.transports
|
.transports
|
||||||
.ethernet
|
.ethernet
|
||||||
.iter()
|
.iter()
|
||||||
.any(|(n, eth)| n == Some(name) && eth.interface == interface),
|
.any(|(n, eth)| n == Some(name) && eth.interface == interface),
|
||||||
"{name} backhaul entry missing after uncommenting shipped fips.yaml"
|
"{name} entry missing after uncommenting shipped fips.yaml"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mirror `fips-mesh-setup`'s block uncomment: strip the ` # ` prefix
|
/// Mirror the setup helpers' block uncomment: strip the ` # ` prefix
|
||||||
/// from each `# mesh<N>:` header and its ` # ` continuation lines,
|
/// from each `# <prefix><N>:` header and its ` # ` continuation
|
||||||
/// leaving every other comment untouched.
|
/// lines, leaving every other comment untouched.
|
||||||
fn uncomment_mesh_blocks(yaml: &str) -> String {
|
fn uncomment_transport_blocks(yaml: &str, prefix: &str) -> String {
|
||||||
|
let header = format!(" # {prefix}");
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
let mut in_block = false;
|
let mut in_block = false;
|
||||||
for line in yaml.lines() {
|
for line in yaml.lines() {
|
||||||
let is_header = line
|
let is_header = line
|
||||||
.strip_prefix(" # mesh")
|
.strip_prefix(&header)
|
||||||
.and_then(|r| r.strip_suffix(':'))
|
.and_then(|r| r.strip_suffix(':'))
|
||||||
.is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()));
|
.is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()));
|
||||||
if is_header {
|
if is_header {
|
||||||
|
|||||||
Reference in New Issue
Block a user