Documentation: network policy, security model, forensics, operations, API, testing, limitations
This commit is contained in:
+41
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user