Documentation: network policy, security model, forensics, operations, API, testing, limitations

This commit is contained in:
Andy Dixon
2026-10-09 17:58:58 +01:00
parent 01b568aaf7
commit 1c31e3b3d5
9 changed files with 487 additions and 1 deletions
+74
View File
@@ -0,0 +1,74 @@
# PiLens
PiLens turns a Raspberry Pi into a self-contained Wi-Fi test network with packet
capture and Wireshark-grade analysis in the browser. You connect an **authorised**
test device to the Pi's access point and watch its traffic: live packets, a
protocol tree from TShark, and the original bytes with field highlighting. The
original captures are kept unmodified, with SHA-256 integrity hashes.
No cloud service, account or internet connection is needed for management,
capture or analysis.
> **Use it only on devices and networks you own or are explicitly authorised to
> inspect.** PiLens has no covert, impersonation or credential-harvesting
> features. Encrypted traffic (HTTPS, QUIC, VPNs, encrypted DNS) stays encrypted:
> in this version PiLens shows packets and metadata, not decrypted content.
## Status
| Phase | Scope | State |
|---|---|---|
| 1 | Installer, services, auth, AP / DHCP / DNS / firewall, health | implemented |
| 2 | dumpcap capture, pcapng storage, live analyser, filters, export / import | implemented |
| 3 | Conversations, follow TCP stream, DNS/TLS explorers, statistics | not started |
| 4 | Optional HTTPS inspection (mitmproxy, client-trusted CA) | not started |
| 5 | Hardening, advanced UI, benchmarks | partial |
Pages for later phases are not in the UI. PiLens never shows controls that do
nothing. For what was tested and how, see [docs/testing.md](docs/testing.md).
## Quick start
On a Raspberry Pi 4/5 with Raspberry Pi OS Lite (64-bit, Bookworm or Trixie),
connected by **Ethernet**:
```sh
tar xzf pilens-<version>-linux-arm64.tar.gz
cd pilens-<version>-linux-arm64
sudo ./install.sh
```
The installer prints the management URL, the certificate fingerprint and a
one-time setup token. Open the URL from a machine on the management network and
complete the setup wizard. The wizard creates the access point and turns the
Wi-Fi interface into the AP, so **manage the Pi over Ethernet**.
For unattended installation: `sudo ./install.sh --non-interactive --config setup.yaml`
(see `setup.example.yaml`).
## Building
Requirements: Go ≥ 1.24, Node ≥ 20. Docker is used only for the test suite.
```sh
make package # dist/pilens-<ver>-linux-arm64.tar.gz (cross-compiled, static)
make test # unit tests + TypeScript typecheck
make test-docker # tshark/dumpcap tests + network-namespace integration suite
```
## Documentation
- [Architecture and design decisions](docs/architecture.md)
- [Installation, first run, upgrade and recovery](docs/operations.md)
- [Network topology and firewall policy](docs/network.md)
- [Security and privacy model](docs/security.md)
- [Forensic evidence handling](docs/forensics.md)
- [API](docs/api.md)
- [Testing and hardware acceptance checklist](docs/testing.md)
- [Known limitations](docs/limitations.md)
## Licence
AGPL-3.0-or-later. Bundled JavaScript dependencies (React, MIT) are compatible.
TShark/dumpcap (GPL-2.0+), dnsmasq (GPL-2.0/3.0) and nftables (GPL-2.0) are
installed from Debian packages and run as separate programs.
+41
View File
@@ -0,0 +1,41 @@
# API
All endpoints are under `https://<pi>:8443/api/`. Except `GET /health`,
`GET /setup/status`, `POST /setup/admin` and `POST /auth/login`, every request
needs the session cookie. Every non-GET request also needs the
`X-CSRF-Token` header (value from `/auth/session` or the login response).
Unauthenticated POSTs need `X-Requested-With: pilens`. Bodies are JSON, limited
to 1 MiB, and unknown fields are rejected.
| Method & path | Purpose |
|---|---|
| `GET /health` | Liveness `{ok:true}` |
| `GET /setup/status` | `{needs_admin, setup_complete, version}` |
| `POST /setup/admin` | `{token, username, password, authorised:true}` → session |
| `POST /auth/login` | `{username, password, totp?}` → `{user, csrf}`; 401 `totp_required` |
| `POST /auth/logout` · `GET /auth/session` · `POST /auth/password {Current, New}` | |
| `POST /auth/totp/begin` · `/enable {Code}` · `/disable {Password}` | TOTP lifecycle |
| `GET /audit` | Last 500 audit events |
| `GET /events` | SSE stream: `packets` `{session, rows}`, `capture` (session object, every 1 s while capturing), `resync` (`{session}` or `{ui_dropped}`), `ping` |
| `GET /system/interfaces` | Detected interfaces, Wi-Fi capabilities, bootstrap management, caller address |
| `GET /system/health` | System, helper, capture, storage, upstream, forwarding state |
| `GET /network` | Current config (passphrase redacted) |
| `POST /network/validate` | `{valid, errors[]}`, including lock-out detection |
| `POST /network/apply` | Transactional apply. `{result:{applied, rolled_back, steps[], error}}`. 502 on failure (rolled back). |
| `POST /setup/complete` | Marks first-run done |
| `GET /ap/clients` · `PUT /devices/{mac} {Label, Notes}` | AP clients (leases + stations + labels) |
| `GET /captures` · `POST /captures {name, interfaces[], capture_filter, ring_segments}` | List / start |
| `POST /captures/live/stop` · `POST /captures/live/pause {Paused}` | |
| `POST /captures/import?name=` | Raw pcap/pcapng body |
| `GET /captures/{id}` · `PATCH {Name, Description, Tags, Notes, Preserved}` · `DELETE ?confirm={id}` | |
| `GET /captures/{id}/packets?offset&limit&filter` | Page of summary rows. With `filter`, `nums[]` gives packet numbers per position. |
| `GET /captures/{id}/packets/{n}` | `{detail:{row, bytes(hex), tree[], segment, note}, error?}` |
| `GET /captures/{id}/export?filter=` | pcapng (or pcap for pcap imports), original records |
| `GET /captures/{id}/manifest` · `POST /captures/{id}/verify` | Integrity |
| `POST /filters/validate {Kind: display|capture, Filter, Interface}` | tshark / dumpcap compile check |
| `GET/POST /filters/saved` · `DELETE /filters/saved/{id}` | Saved filters |
| `GET /storage` · `PUT /storage {max_bytes, reserve_bytes, segment_bytes, import_bytes}` | |
Tree nodes: `{label, field, show, pos, size, verified, children}`. `verified`
means the frame bytes at `pos..pos+size` were checked against tshark's raw
value, so highlighting them is accurate.
+50
View File
@@ -0,0 +1,50 @@
# Forensic evidence handling
## What is evidence and what is derived
| Data | Status |
|---|---|
| `captures/<session>/seg-NNNNNN.pcapng` (live) | **Original**. dumpcap's pcapng output, written byte for byte. Each segment starts with the section and interface headers copied verbatim, so it opens on its own. |
| `captures/<session>/original.pcap[ng]` (import) | **Original as received**. The uploaded file, unchanged. |
| SHA-256 per segment | Computed while writing, and over the whole file for imports. Stored in SQLite. |
| Packet summary rows (SQLite) | Derived: tshark output, regenerable |
| Protocol trees, filters, exports with a filter | Derived. A filtered export copies the original packet records verbatim, but as a selection it is a new file. |
PiLens never labels reconstructed or synthetic data as an original capture.
## Recorded metadata (per session)
Session `meta` and the manifest record:
- UTC start/stop (milliseconds), capture interfaces and capture filter
- PiLens and TShark versions, hostname
- `clock_synchronised` from `timedatectl` at start. If this is not `yes`,
absolute timestamps may be wrong. A Pi has no battery-backed clock, so make
sure NTP is working before you capture evidence.
- Segment size, ring-buffer setting, pause/resume log, packets discarded while
paused
- dumpcap's own "received/dropped" report, verbatim (kernel and pcap drops)
## Verifying
- UI: Capture Library → *Integrity* → *Verify hashes now*. This recomputes
SHA-256 for every complete segment.
- Manifest: *Download manifest* gives JSON with every file, its hash, packet
count and the metadata above.
- Offline: `sha256sum seg-*.pcapng` and compare with the manifest.
## Caveats
- **Capture loss** happens under load. Check the drop report. A capture with
drops is incomplete.
- **Segment boundaries**: summaries and dissections are computed per segment,
so TCP stream indices and reassembly restart at each boundary. The packet
bytes are unaffected.
- **Interrupted captures** (power loss): on restart, the last segment is
scanned, counted and hashed as found, and the session is marked
`interrupted`. A truncated final packet record may be present.
- **Pause capture** discards packets while paused. The pause log in the
session metadata records when.
- **Randomised MAC addresses** do not identify hardware. PiLens never merges
device identities automatically.
- DNS answers do not prove which host a later connection went to.
+32
View File
@@ -0,0 +1,32 @@
# Known limitations (v0.1)
**Encryption.** PiLens captures packets; it does not decrypt them. HTTPS, QUIC/HTTP-3, TLS 1.3 with
Encrypted ClientHello, DNS-over-HTTPS/TLS, and VPN traffic are visible only as encrypted
packets plus whatever metadata the protocol exposes in clear (for TLS ≤1.3 without ECH: SNI,
versions, ALPN; for TLS 1.2: certificates). The optional HTTPS inspection mode (client-installed
CA via mitmproxy) is **not implemented** in this version, and even when it is, it will not work
against certificate pinning, private trust stores, mutual TLS, QUIC, or apps that bypass proxies.
**Not yet implemented:** conversations view, follow TCP stream, DNS/TLS/HTTP explorers,
statistics charts, findings/diagnostics rules, capture scheduling, the strict inspection mode,
WireGuard, packet comparison. They are not shown in the UI.
**IPv6** is blocked for AP clients rather than routed (see network.md).
**AP features.** Max-client limits are not available through NetworkManager. WPA2/WPA3
transition mode is not offered. WPA3 (SAE) in AP mode depends on the Wi-Fi driver and
firmware. On the Raspberry Pi's brcmfmac it may fail, in which case the change is
rolled back.
**Capture.**
- One live capture at a time.
- Display filters on large captures run tshark over every segment. That takes seconds per GiB
on a Pi 4/5, and results are cached per filter.
- The packet table's scroll height is limited by browser element-size limits (about 1.5 million
rows at compact density). Use a display filter for larger captures.
- Capture drop counts are reported by dumpcap when the capture stops, not live.
**Performance figures** have not yet been measured on hardware, so none are claimed.
`docs/testing.md` lists the benchmarks to run.
**Single administrator role.** Every account has full rights.
+86
View File
@@ -0,0 +1,86 @@
# Network topology and policy
```
upstream LAN / internet
│ eth0 (uplink + management, DHCP client as before)
┌──────┴───────────────────────────────┐
│ Raspberry Pi │
│ nft table inet pilens │
│ NAT (masquerade) AP subnet → uplink │
│ pilens-dnsmasq: DHCP+DNS on wlan0 │
└──────┬───────────────────────────────┘
│ wlan0 AP "PiLens-Lab", 192.168.50.1/24 (static, NM keyfile)
test device(s) 192.168.50.100–199
```
The Pi **routes**: it does not bridge. AP clients are always on their own subnet
behind NAT, so every packet they send crosses the Pi's IP stack. That is what
makes capture and policy enforcement complete.
## Interface discovery
Interface names are discovered, not assumed. The installer picks:
- **Uplink**: the interface holding the IPv4 default route.
- **Management**: the interface your SSH session arrived on, falling back to the
uplink. The default allowed sources are that interface's connected subnet,
plus your SSH client's /32 if it lies outside that subnet (for example when
you connect through NAT or a VPN).
- **AP**: the first interface with `/sys/class/net/<if>/wireless`.
All three can be changed in the wizard or under Network Settings. Validation
rejects an AP interface that is also used for management or uplink, and any AP
subnet that overlaps a management subnet.
## Firewall rules, in words
| Traffic | Decision | Why |
|---|---|---|
| AP client → Pi UDP 67 (DHCP) | accept | Address assignment |
| AP client → Pi gateway :53 (DNS) | accept | Resolution through the Pi, so DNS is observable |
| AP client → Pi ICMP echo / errors | accept | Basic diagnostics |
| AP client → Pi anything else (SSH, :8443, …) | **drop** | The AP network is untrusted |
| AP client IPv6 (to the Pi or forwarded) | **drop** | IPv6 is not inspected, so it is closed rather than left open |
| AP client → private / link-local / CGNAT / multicast / management nets | **drop** | Clients cannot pivot into your LAN |
| AP client → explicit `allow_cidrs` | accept | An opt-in exception, e.g. a lab server |
| AP client → public addresses via uplink | accept when mode = `internet` | The purpose of the gateway |
| AP client → anything, mode = `deny` | **drop** | Observe-only lab mode (DHCP and DNS still work) |
| Anything → AP subnet, not a reply | **drop** | No inbound connections to clients |
| Any interface → :8443 from a non-allowed source | **drop** | Management isolation |
| Everything else | not touched by PiLens | Host SSH and other services keep their policy |
Multicast, broadcast and mDNS from clients are not forwarded. DHCP broadcasts
reach dnsmasq because they are delivered locally, not forwarded. With **client
isolation** enabled, the Wi-Fi driver stops clients talking to each other
directly. Without it, client-to-client frames on the same radio never reach the
IP layer, but they are still visible to a capture on the AP interface.
Rules are applied atomically: one `nft -f` both deletes and recreates the
table. A ruleset that fails `nft -c` is never loaded.
## Failure behaviour
| Event | Result |
|---|---|
| Ethernet unplugged / upstream gateway unreachable | Clients keep DHCP and DNS (answers from cache or overrides; other lookups fail). Forwarded traffic has no route and fails. The dashboard shows "no default route". |
| DNS upstream fails | dnsmasq returns SERVFAIL. Visible in captures. |
| Capture engine crashes | Forwarding is unaffected in capture-only mode. Interrupted segments are hashed and closed on restart, and the session is marked *failed / interrupted*. |
| Storage exhausted | The oldest unpreserved sessions are deleted. If none are left, capture stops with an explicit error. Forwarding continues. |
| Network apply fails | Automatic rollback to the previous NM keyfile, dnsmasq config and nft table, and the failure is reported with each step. |
| Firewall restore fails at boot | `pilens-firewall.service` fails, but **IPv4 forwarding stays disabled** (it is never persisted), so clients cannot be routed. |
| Reboot | `pilens-firewall` loads the last good table before NetworkManager starts the AP. Forwarding is enabled only after that. |
| Helper unavailable | The UI shows forwarding state "unknown". The last loaded table stays in force. |
TLS-proxy related behaviour (mode B/C) belongs to a later phase.
## IPv6
IPv6 forwarding and inspection are not implemented. The AP profile sets
`ipv6.method=disabled`, so clients get no router advertisements from the Pi,
and the firewall drops any IPv6 a client sends. Clients therefore fall back to
IPv4, which is fully routed and captured.
## Regulatory domain
The country code is applied with `iw reg set` on every apply and at boot. Set
it correctly: it determines the legal channels and transmit power.
+82
View File
@@ -0,0 +1,82 @@
# Operations: install, first run, upgrade, recovery
## Requirements
- Raspberry Pi 4 or 5 (other arm64 Debian systems with NetworkManager and an AP-capable radio
may work but are untested)
- Raspberry Pi OS Lite 64-bit, Bookworm or Trixie, with NetworkManager (the default)
- Ethernet connected to the network you will manage it from
- ≥ 1 GiB free disk. An SSD (USB 3 or NVMe) is strongly recommended for captures: SD cards
are slow and wear out.
## Install
```sh
tar xzf pilens-<ver>-linux-arm64.tar.gz && cd pilens-<ver>-linux-arm64
sudo ./install.sh # explains every change and asks first
```
What it does, in order: checks the platform; detects interfaces and your SSH path; checks AP
support, disk, RAM and conflicting services; shows the plan; backs up
`/etc/NetworkManager/system-connections` and `nft list ruleset` to
`/var/backups/pilens/<time>/`; installs `tshark wireshark-common nftables dnsmasq-base iw`
from apt; installs PiLens to `/opt/pilens`; creates user `pilens`; installs systemd units;
initialises the database and TLS certificate; starts the services and checks they are healthy.
If any step fails, the trap rolls back (on an upgrade, the previous version is restored).
It does **not** touch existing network connections. The access point is created by the
wizard.
Options: `--mgmt-iface eth0 --mgmt-subnet 192.168.1.0/24` to override detection, `--yes`,
`--non-interactive --config setup.yaml`.
## First run
Open the URL printed by the installer and compare the certificate fingerprint. The wizard
asks for the setup token, creates the administrator, and then walks through management access,
uplink, Wi-Fi, client subnet, forwarding, inspection mode and storage. "Apply" performs the
transactional network change. If the browser loses power or the page reloads, the wizard
resumes: progress is kept in the browser, and the server holds what has actually been applied.
## Upgrade
Run the new package's `install.sh`. It detects the existing installation, keeps
`/var/lib/pilens`, replaces the binaries and units, and restarts. Migrations run automatically
on start. If the health check fails, the previous version is restored from the backup.
## Recovery
| Problem | Action |
|---|---|
| Services unhealthy, permissions wrong, dumpcap lost capabilities | `sudo /opt/pilens/repair.sh` |
| Forgotten password | `sudo -u pilens /opt/pilens/bin/pilens reset-password -user admin` (also disables TOTP and revokes sessions) |
| Locked out by the management subnet | On the console: `sudo nft delete table inet pilens`, then fix the subnet in the UI and apply. The table is restored at the next apply or reboot. |
| AP broken after a manual NM change | Re-apply from Network Settings, or `sudo nmcli connection up pilens-ap` |
| Need to return the Wi-Fi to client mode | `sudo nmcli connection down pilens-ap`. Your earlier Wi-Fi profile was never modified. |
| Bug report | `sudo /opt/pilens/diagnose.sh` (secrets redacted) |
| Remove | `sudo /opt/pilens/uninstall.sh` (keeps captures) or `--purge` |
The database is `/var/lib/pilens/pilens.db` (SQLite, WAL). To back it up: stop `pilens`, then
copy `pilens.db*` and `captures/`.
## Capture storage on an SSD
```sh
sudo systemctl stop pilens
sudo mkdir -p /mnt/ssd/pilens-captures && sudo chown pilens:pilens /mnt/ssd/pilens-captures
sudo systemctl edit pilens
# [Service]
# ExecStart=
# ExecStart=/opt/pilens/bin/pilens serve -data /var/lib/pilens -listen :8443 -captures /mnt/ssd/pilens-captures
# ReadWritePaths=/mnt/ssd/pilens-captures
# [Unit]
# RequiresMountsFor=/mnt/ssd/pilens-captures
sudo systemctl start pilens
```
## Service architecture and shutdown
`pilens-firewall` (oneshot, before networking) → `pilens-helper` (root) → `pilens-dnsmasq` →
`pilens` (Type=notify, watchdog 60 s). On SIGTERM, pilens stops accepting requests, stops any
capture (dumpcap flushes, the last segment is fsynced and hashed), and then exits. The timeout
is 60 s.
+71
View File
@@ -0,0 +1,71 @@
# Security and privacy model
Captured traffic can contain credentials, tokens, private messages, and
medical or financial data. PiLens treats the appliance as a privileged,
sensitive system.
## Trust boundaries
| Boundary | Untrusted side | Controls |
|---|---|---|
| Browser ↔ pilens | Management network | TLS, source allow-list (nftables **and** middleware), Argon2id passwords, session cookies `Secure; HttpOnly; SameSite=Strict`, CSRF token header, login throttling, optional TOTP, audit log |
| AP clients ↔ Pi | Wi-Fi network | nftables drops everything except DHCP, DNS and ICMP. Management is unreachable even through the uplink address. |
| pilens ↔ pilens-helper | pilens process | Unix socket 0660 root:pilens. `SO_PEERCRED` uid check. Fixed operations only. The helper re-validates the full config. Requests are capped at 1 MiB. |
| Packet data ↔ parsers | Anyone on the wire / uploaded files | Our pcap/pcapng readers bounds-check every length (16 MiB block cap). tshark runs via `rlimit-exec` (RLIMIT_AS, RLIMIT_CPU, no core dumps, `no_new_privs`) with a timeout and empty environment. |
| Packet data ↔ browser | Packet content | Rendered as text nodes by React. No `dangerouslySetInnerHTML` and no HTML preview. CSP `default-src 'self'`, `frame-ancestors 'none'`. |
| Uploads | Uploaded file | Size limit enforced while streaming. Magic-number check (pcap/pcapng only, no archives, so no decompression bombs). Server-generated file names, so no path traversal. Session IDs are strictly whitelisted (`^[0-9TZ]{16}-[0-9a-f]{6}$`). |
## Privilege separation
- **pilens** (user `pilens`): no root and no ambient capabilities. Capture uses
`dumpcap`, whose file capabilities are granted to group `wireshark`, which is
Debian's standard model. The unit's `CapabilityBoundingSet=CAP_NET_RAW
CAP_NET_ADMIN` limits what any child can ever gain. `NoNewPrivileges` cannot
be set because it would disable dumpcap's file capabilities.
`ProtectSystem=strict` leaves only `/var/lib/pilens` writable.
- **pilens-helper** (root): about 400 lines. It can write the PiLens NM
keyfile, its own state directory and `ip_forward`; run `nft`, `nmcli`, `iw`
and `systemctl` with fixed argv; and nothing else. There is no generic
command execution.
- **pilens-dnsmasq**: Debian's dnsmasq, which drops privileges itself.
## Secrets
| Secret | Where | Exposure |
|---|---|---|
| Admin password | Argon2id hash in SQLite (0640 pilens) | Never returned |
| Session tokens | Only the SHA-256 is stored | Cookie only |
| Wi-Fi passphrase | SQLite + helper state (0600 root) + NM keyfile (0600 root) | Write-only in the API. Never in a process argv. Redacted in diagnostics. |
| Setup token | `/var/lib/pilens/setup-token` (0600) | Deleted when the first admin is created |
| Management TLS key | `/var/lib/pilens/tls/mgmt.key` (0600) | Never exported. Separate from any future interception CA. |
## Data protection
- PiLens currently stores no decrypted payloads, because it decrypts nothing.
- Captures are deleted by retention (oldest unpreserved first) or explicitly
by an admin, who must confirm. Deleting a session removes its files and all
index rows.
- Exports, deletions, configuration changes, logins (including failures),
password/TOTP changes and setup are recorded in the audit log.
- No telemetry. The only outbound traffic the appliance generates is what the
OS already does (apt, NTP) and the DNS forwarding it performs for AP clients.
## Reviewed attack classes
| Class | Mitigation |
|---|---|
| Command injection | All execs use argv lists, never a shell. Filter strings are passed as single arguments and checked for control characters. Interface names are matched against `^[A-Za-z0-9_.-]{1,15}$`. |
| Config injection (dnsmasq/NM keyfile) | Every rendered value is strictly validated (SSID charset, hostnames as RFC 1123 labels, IPs parsed), so newlines cannot reach a config file. |
| SQL injection | Parameterised queries only |
| Path traversal | IDs are server-generated and whitelisted. Segment file names come from the database, not the client. |
| XSS via packet content | React text rendering, CSP, no HTML rendering of captured data |
| SSRF | The server makes no outbound requests based on user input |
| Auth bypass | Every `/api/*` route except health, setup status, first-admin creation and login goes through `requireAuth`. The setup endpoint needs the setup token, and the INSERT is conditional on no user existing (race-safe). |
| Resource exhaustion | Bounded SSE queues (slow clients drop updates, never capture). Bounded tshark feed. Per-process rlimits. Upload and request size limits. Storage quota and reserve. Filter cache capped at 16 entries. |
| Malicious pcap | Bounds-checked block reader, fuzz-style malformed-input tests, tshark resource limits |
| Lock-out | The API refuses a management configuration that would exclude the caller's own address |
Dependency inventory: Go modules `modernc.org/sqlite`, `golang.org/x/crypto`
(Argon2id) and `gopkg.in/yaml.v3`; npm packages `react` and `react-dom`, plus
`vite` and `typescript` at build time. Run `govulncheck ./...` and `npm audit`
before each release.
+50
View File
@@ -0,0 +1,50 @@
# Testing
## Automated
| Command | What it covers | Needs |
|---|---|---|
| `make test` | Config validation, nft/dnsmasq/NM rendering, Argon2id, TOTP (RFC 6238 vector), throttling, helper parsers, pcap/pcapng reader incl. malformed input, API auth flow (setup token, authorised-use, CSRF, source allow-list, lock-out detection, logout, throttling), TypeScript typecheck | Go, Node |
| `make test-docker` | Everything above that needs tshark/dumpcap (import → index → filter → PDML with verified byte ranges → filtered export re-read by tshark → hash verify; live capture with segment rotation, counts agreement, per-segment validity), **plus** the netns suite below | Docker |
The netns suite (`tests/integration/run.sh`) builds an upstream/Pi/client topology from
veth pairs and runs the real `pilens` binary plus the configuration rendered by `pilens-helper`:
DHCP lease and options, DNS override, NAT to a public address, blocking of RFC 1918 /
management / IPv6 / inbound / Pi-SSH, management reachable only from the allowed subnet (not
from AP clients, not even via the uplink address), first-admin creation, capture on the AP
interface through the API, display filter, packet detail, export opened by tshark, integrity
check, CSRF rejection, deny mode, and survival of an unrelated nft table.
Not covered by automation: NetworkManager AP activation and its rollback (needs a real radio;
`mac80211_hwsim` is not available in containers), systemd unit behaviour, installer.
## Hardware acceptance checklist (Raspberry Pi)
Record date, Pi model, OS, PiLens version and the result for each item.
| # | Test | Expected |
|---|---|---|
| H1 | Fresh install on Pi OS Lite via SSH over Ethernet | Installer completes. Prints URL, fingerprint, token. Existing connections unchanged. |
| H2 | Wizard: create admin, apply AP | Steps all ✓. SSID visible. Ethernet SSH session not interrupted. |
| H3 | Phone joins the AP | Lease in the pool, Pi as DNS, internet works |
| H4 | Phone → LAN host (e.g. router admin page) | Blocked |
| H5 | Phone → `https://<pi-ap-ip>:8443` and SSH to the Pi | Blocked |
| H6 | Capture on wlan0 while browsing | Packets live in the UI. Export opens in desktop Wireshark. Verify = ok. |
| H7 | Cold boot | AP returns. Firewall table loaded before the AP (`journalctl -b -u pilens-firewall -u NetworkManager`). Forwarding works. |
| H8 | Power loss during capture | Session marked interrupted. Segment hashed. Earlier segments verify. |
| H9 | Unplug Ethernet for 1 min, replug | Dashboard shows no default route, then recovers. Clients keep their leases. |
| H10 | Client disconnects and reconnects | Device list updates. Same lease. |
| H11 | `kill -9` of pilens during a capture | systemd restarts it. Session interrupted. Forwarding unaffected. |
| H12 | Storage exhaustion (small quota) | Oldest unpreserved session deleted, or capture stops with an error. Preserved session intact. |
| H13 | Apply an invalid AP config (e.g. WPA3 on a radio without SAE) | Rolled back, previous AP restored, error shown |
| H14 | `uninstall.sh` then `install.sh` | Captures kept. Old Wi-Fi client profile works again after uninstall. |
| H15 | `repair.sh` after `setcap -r /usr/bin/dumpcap` | Capabilities restored |
| H16 | Throughput: iperf3 through the AP while capturing | Record Mbit/s, dumpcap drops, ingest lag |
| H17 | Import a 500 MB pcapng | Indexing time recorded. UI responsive. |
TLS-interception hardware tests belong to phase 4.
## Results log
Hardware results are recorded here only after they have actually been run. See the
release notes or commit log for the latest run.
+1 -1
View File
@@ -94,7 +94,7 @@ check "public server sees NATed source (10.99.0.2)" sh -c "ip netns exec up ss -
refuse "client blocked from RFC1918 destination 172.16.5.5" ip netns exec cl curl -s -m 3 -o /dev/null http://172.16.5.5/ refuse "client blocked from RFC1918 destination 172.16.5.5" ip netns exec cl curl -s -m 3 -o /dev/null http://172.16.5.5/
refuse "client blocked from management/upstream subnet 10.99.0.1" ip netns exec cl curl -s -m 3 -o /dev/null http://10.99.0.1:8080/ refuse "client blocked from management/upstream subnet 10.99.0.1" ip netns exec cl curl -s -m 3 -o /dev/null http://10.99.0.1:8080/
refuse "IPv6 from client is not forwarded" ip netns exec cl ping -6 -c 1 -W 2 fd01::1 refuse "IPv6 from client is not forwarded" ip netns exec cl ping -6 -c 1 -W 2 fd01::1
refuse "client blocked from Pi SSH port" ip netns exec cl bash -c 'exec 3<>/dev/tcp/192.168.50.1/22' refuse "client blocked from Pi SSH port" ip netns exec cl timeout 3 bash -c 'exec 3<>/dev/tcp/192.168.50.1/22'
refuse "upstream cannot open connections into the AP subnet" ip netns exec up ping -c 1 -W 2 "$CIP" refuse "upstream cannot open connections into the AP subnet" ip netns exec up ping -c 1 -W 2 "$CIP"
echo "== Real pilens service in the pi namespace" echo "== Real pilens service in the pi namespace"