mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +00:00
feat(openwrt): serve DHCPv4 on the access SSID from a fixed roamable subnet
RA-only addressing satisfied Android's provisioning check but left anything expecting IPv4 with a self-assigned address and a "no IP" complaint. dnsmasq now leases out of 10.21.<N>.0/24 (N = radio index; prefix echoes FIPS port 2121), deliberately identical on every router: a roaming client keeps its lease across the ESS, and dnsmasq's authoritative mode — the OpenWrt default, pinned by the helper — ACKs the renew a foreign router never issued. Lease collisions across routers surface as a NAK on renew and the client re-DHCPs. The dhcp section's 'dhcpv4 server' is read by both dnsmasq (default images) and odhcpd (only with maindhcp), so either arrangement serves. A DHCPv4/udp-67 accept rule joins the fips_ap zone; DHCPv6 stays off, the ULA RA stays as-is, and nothing depends on an upstream. The zone remains isolated — no forwardings, no internet.
This commit is contained in:
@@ -19,7 +19,7 @@ router-to-router *backhaul*, see
|
||||
For all `transports.ethernet.*` configuration keys, see
|
||||
[../reference/configuration.md](../reference/configuration.md).
|
||||
|
||||
## Why open, why RA-only
|
||||
## Why open, why this addressing
|
||||
|
||||
Three deliberate choices distinguish this from a stock guest network:
|
||||
|
||||
@@ -40,19 +40,25 @@ Three deliberate choices distinguish this from a stock guest network:
|
||||
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.
|
||||
- **No DHCP — IPv6 router advertisements only.** odhcpd announces a
|
||||
ULA prefix (`fd..`-range) for stateless SLAAC: no DHCPv4, no DHCPv6,
|
||||
no lease state. FIPS itself only needs link-local + mDNS, but
|
||||
Android's provisioning check requires an RA or a DHCP offer and
|
||||
*disconnects* with neither — RA-only is the minimum that satisfies
|
||||
it. The addressing 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.
|
||||
- **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 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
|
||||
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.
|
||||
|
||||
@@ -83,8 +89,8 @@ Both can share a radio, at an airtime cost (see constraints).
|
||||
|
||||
## Requirements
|
||||
|
||||
- OpenWrt 22.03+ with the FIPS package installed (fw4; odhcpd is part
|
||||
of the default images).
|
||||
- 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)
|
||||
@@ -97,9 +103,9 @@ fips-ap-setup radio0
|
||||
```
|
||||
|
||||
This creates an open AP with SSID `!FIPS` and client isolation, an
|
||||
isolated network with a static ULA `/64`, an RA-only odhcpd config
|
||||
(SLAAC, no DHCP), and a locked-down `fips_ap` firewall zone — then
|
||||
reloads the radio. Interfaces are named by radio index: `radio0` →
|
||||
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
|
||||
@@ -132,20 +138,25 @@ 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='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 ICMPv6, UDP 5353/2121, TCP 8443).
|
||||
forwardings, ACCEPT rules for DHCPv4/UDP 67, ICMPv6, UDP 5353/2121,
|
||||
TCP 8443).
|
||||
|
||||
## Step 2 — check the FIPS transport binding
|
||||
|
||||
@@ -193,13 +204,15 @@ 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 -6 addr show dev fips-ap0 # the fd..::1/64 the router announces
|
||||
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 RA — check
|
||||
`logread | grep odhcpd` and that the `dhcp.fips_ap_radio0` section
|
||||
survived (`uci show dhcp | grep fips_ap`).
|
||||
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:
|
||||
|
||||
@@ -228,6 +241,17 @@ stays associated — that is the designed steady state, not an error.
|
||||
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) and renumber on each router; FIPS sessions
|
||||
(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.
|
||||
|
||||
@@ -22,13 +22,17 @@
|
||||
# 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).
|
||||
# - NO DHCP — odhcpd announces a ULA prefix in router
|
||||
# advertisements (stateless SLAAC, no lease state). FIPS itself only
|
||||
# needs link-local + mDNS, but Android's provisioning check requires an
|
||||
# RA or DHCP and disconnects with neither; RA-only is the minimum that
|
||||
# satisfies it. The network provides no internet, so phones mark it
|
||||
# unvalidated and keep cellular as the default route while staying
|
||||
# associated.
|
||||
# - 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.
|
||||
@@ -128,6 +132,7 @@ if [ "$1" = "remove" ]; then
|
||||
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
|
||||
@@ -137,6 +142,7 @@ if [ "$1" = "remove" ]; then
|
||||
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"
|
||||
@@ -206,11 +212,13 @@ CHANNEL="$(uci -q get "wireless.$RADIO.channel")"
|
||||
BAND="$(uci -q get "wireless.$RADIO.band")"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Network: static ULA /64 so odhcpd has a prefix to announce. The address
|
||||
# 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. Derived from the router's global ULA prefix; a re-run
|
||||
# keeps the address already configured.
|
||||
# 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")"
|
||||
@@ -233,17 +241,24 @@ fd*) ;; # keep the existing address on re-run
|
||||
;;
|
||||
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: router advertisements only — stateless SLAAC, no DHCPv4, no
|
||||
# DHCPv6, no lease state. ra_default '2' announces a default router even
|
||||
# without an upstream default route: Android's provisioning wants address +
|
||||
# route + DNS from the RA, and its validation probe then fails by design
|
||||
# (no internet), so the phone keeps cellular as the default route.
|
||||
# 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"
|
||||
@@ -252,12 +267,19 @@ 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=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 — ICMPv6 (SLAAC itself), mDNS
|
||||
# (discovery), and the FIPS UDP/TCP transports (the handshake surface).
|
||||
# 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.
|
||||
@@ -287,6 +309,8 @@ ap_rule() {
|
||||
|
||||
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
|
||||
@@ -296,6 +320,7 @@ 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
|
||||
|
||||
@@ -314,14 +339,15 @@ esac
|
||||
cat <<EOF
|
||||
Created open access SSID '$SSID' as $AP_IFNAME on $RADIO \
|
||||
(band ${BAND:-?}, channel ${CHANNEL:-auto}).
|
||||
RA-only IPv6 on $AP_ADDR — no DHCP, no internet, isolated from
|
||||
br-lan and the WAN.
|
||||
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. 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.
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user