Skip to content

Commit aebe482

Browse files
authored
Merge pull request #1239 from luandro/hotfix/pirania
Pirania hackaton final fix
2 parents 142c27f + 8140cb0 commit aebe482

37 files changed

Lines changed: 2473 additions & 395 deletions

packages/pirania/Leeme.md

Lines changed: 183 additions & 84 deletions
Large diffs are not rendered by default.

packages/pirania/Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ define Package/$(PKG_NAME)
44
SUBMENU:=Captive Portals
55
SECTION:=net
66
CATEGORY:=Network
7-
MAINTAINER:=Asociación Civil AlterMundi <info@altermundi.net>
7+
MAINTAINER:=Luandro <luandro@gmail.com>
88
TITLE:=Captive portal with vouchers.
9-
DEPENDS:=+ip6tables-mod-nat +ipset +shared-state +shared-state-pirania \
9+
DEPENDS:=+nftables +kmod-nft-bridge +shared-state +shared-state-pirania \
1010
+uhttpd-mod-lua +lime-system +luci-lib-jsonc \
11-
+liblucihttp-lua +luci-lib-nixio +libubus-lua +libuci-lua
11+
+luci-lib-nixio +libubus-lua +libuci-lua
1212
PKGARCH:=all
1313
endef
1414

packages/pirania/PIRANIA_SYSTEM.md

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# Pirania system overview
2+
3+
This document explains how Pirania is organized, what each component does, and how the captive-portal flow works in both voucher and read-for-access modes. It is based on the current code under `packages/pirania/`.
4+
5+
## 1. What Pirania is
6+
7+
Pirania is a captive portal for OpenWrt/LibreMesh nodes. It controls Internet access by MAC address and provides two access modes:
8+
9+
- **Voucher mode**: users must enter a voucher code; the voucher is then bound to their device MAC.
10+
- **Read-for-access mode**: users view a portal page and wait a short countdown; their MAC is temporarily authorized.
11+
12+
The authorization list is stored locally and synchronized into nftables rules so that authorized devices bypass the portal.
13+
14+
## 2. High-level architecture
15+
16+
```
17+
Client device
18+
-> nftables rules (captures DNS/HTTP/HTTPS for unauthorized MACs)
19+
-> local DNS on port 59053 (pirania-dnsmasq)
20+
-> local HTTP redirect on port 59080 (pirania-uhttpd)
21+
-> portal pages on /www/portal/
22+
-> CGI handlers authorize MAC
23+
-> captive-portal update (refresh nftables MAC set)
24+
-> normal Internet access
25+
```
26+
27+
The central behavior is implemented by:
28+
29+
- `packages/pirania/files/usr/bin/captive-portal`
30+
- `packages/pirania/files/etc/init.d/pirania-dnsmasq`
31+
- `packages/pirania/files/etc/init.d/pirania-uhttpd`
32+
- `packages/pirania/files/www/pirania-redirect/redirect`
33+
- `packages/pirania/files/usr/lib/lua/portal/portal.lua`
34+
35+
## 3. Configuration (UCI)
36+
37+
The main configuration file is `packages/pirania/files/etc/config/pirania`.
38+
39+
Key options in `base_config`:
40+
41+
- `enabled`: whether the portal is active at boot
42+
- `with_vouchers`: toggle voucher vs read-for-access mode
43+
- `portal_domain`: domain used for portal URLs (default `thisnode.info`)
44+
- `url_auth`, `url_authenticated`, `url_info`, `url_fail`: portal page paths
45+
- `db_path`: voucher database directory (JSON files)
46+
- `hooks_path`: directory for hook scripts (e.g., shared-state sync)
47+
- `allowlist_ipv4`, `allowlist_ipv6`: ranges that bypass the captive portal
48+
- `allowlist_ipv4_url_insecure`: escape hatch to retry downloaded IPv4 allowlists without TLS certificate validation; currently enabled by default for compatibility, but should only be used where TLS validation is known to be broken
49+
50+
Access-mode options live in `config access_mode 'read_for_access'`:
51+
52+
- `url_portal`: path to the read-for-access page
53+
- `duration_m`: authorization duration in minutes
54+
55+
## 4. Services and startup
56+
57+
- `packages/pirania/files/etc/init.d/pirania` starts the portal if enabled and runs hooks.
58+
- `packages/pirania/files/etc/init.d/pirania-dnsmasq` runs a dedicated dnsmasq on port 59053.
59+
- `packages/pirania/files/etc/init.d/pirania-uhttpd` runs a small uhttpd on port 59080.
60+
- `packages/pirania/files/etc/uci-defaults/90-captive-portal-cron` installs a cron job to refresh nftables every 10 minutes.
61+
62+
## 5. Traffic capture (nftables)
63+
64+
`packages/pirania/files/usr/bin/captive-portal` sets up nftables rules in the `inet pirania` table:
65+
66+
- Creates sets for authorized MACs (`pirania-auth-macs`) and allowlisted IPv4/IPv6 destination ranges.
67+
- Redirects DNS (UDP/53) to port 59053 for unauthorized MACs.
68+
- Redirects HTTP (TCP/80) to port 59080 for unauthorized MACs.
69+
- Lets HTTPS (TCP/443) pass prerouting and rejects unauthorized traffic with a TCP reset in the input/forward filter path.
70+
- Allows traffic for MACs in `pirania-auth-macs`, and allows allowlisted destinations to bypass redirects and HTTPS blocking.
71+
72+
Authorized MACs come from `packages/pirania/files/usr/bin/pirania_authorized_macs`, which delegates to the Lua portal library and returns either voucher-based or read-for-access MACs.
73+
74+
## 6. DNS hijack
75+
76+
`packages/pirania/files/etc/init.d/pirania-dnsmasq` starts a dnsmasq instance that:
77+
78+
- Answers `thisnode.info` with the node IP.
79+
- Uses shared-state hosts from `/var/hosts/shared-state-dnsmasq_hosts`.
80+
- Sends unknown domains to a fallback IP (1.2.3.4).
81+
82+
This ensures the portal domain resolves locally when the user is captured.
83+
84+
## 7. HTTP redirect service
85+
86+
`packages/pirania/files/etc/init.d/pirania-uhttpd` starts an HTTP server on port 59080 serving `packages/pirania/files/www/pirania-redirect/redirect`.
87+
88+
The redirect script:
89+
90+
- Builds a `prev` URL from the original request.
91+
- Picks the portal entry point based on `with_vouchers`:
92+
- Voucher mode: `base_config.url_auth`
93+
- Read-for-access mode: `read_for_access.url_portal`
94+
- Sends a 302 redirect to `http://<portal_domain><path>?prev=<original>`.
95+
96+
## 8. Portal pages and assets
97+
98+
Static portal pages are under `packages/pirania/files/www/portal/`:
99+
100+
- `auth.html` (voucher entry)
101+
- `info.html` (waiting/info screen)
102+
- `authenticated.html` (success)
103+
- `fail.html` (error)
104+
- `read_for_access.html` (non-voucher flow)
105+
106+
Portal content (title, text, logo, colors) is stored in `packages/pirania/files/etc/pirania/portal.json`. The Lua module `packages/pirania/files/usr/lib/lua/portal/portal.lua` can read/write this content and also synchronize it via shared-state (`pirania_persistent`).
107+
108+
## 9. Voucher subsystem
109+
110+
Voucher logic is implemented in `packages/pirania/files/usr/lib/lua/voucher/` and exposed via the CLI `packages/pirania/files/usr/bin/voucher`.
111+
112+
Key files:
113+
114+
- `vouchera.lua`: main voucher model and operations (create, activate, invalidate, list, status checks).
115+
- `store.lua`: JSON file storage (`db_path/<id>.json`).
116+
- `config.lua`: reads `db_path`, `hooks_path`, pruning settings.
117+
- `hooks.lua`: executes hook scripts under `hooks_path/<action>/` on database changes.
118+
- `utils.lua`: URL parsing and IP/MAC lookup via ARP/neigh tables.
119+
120+
Voucher lifecycle:
121+
122+
1. **Create**: `voucher add` calls `vouchera.create`, which writes a JSON file and triggers `hooks.run('db_change')`.
123+
2. **Activate**: voucher code is bound to a MAC and `captive-portal update` refreshes nftables.
124+
3. **Invalidate**: sets `invalidation_date`, keeping the record for pruning; also refreshes nftables if needed.
125+
4. **Prune**: old expired/invalidated vouchers are removed when `vouchera.init()` runs.
126+
127+
The CLI wraps these operations in `packages/pirania/files/usr/bin/voucher`.
128+
129+
## 10. Read-for-access subsystem
130+
131+
Read-for-access mode uses:
132+
133+
- `packages/pirania/files/usr/lib/lua/read_for_access/read_for_access.lua`
134+
- `packages/pirania/files/usr/lib/lua/read_for_access/cgi_handlers.lua`
135+
136+
MACs are stored in `/tmp/pirania/read_for_access/auth_macs` with an expiration timestamp (based on system uptime). When a user completes the portal wait, their MAC is added and `captive-portal update` refreshes nftables.
137+
138+
## 11. CGI endpoints
139+
140+
Portal pages call CGI scripts under `packages/pirania/files/www/cgi-bin/pirania/`:
141+
142+
- `preactivate_voucher`: validates voucher and either redirects to `info.html` (JS flow) or activates immediately (no-JS flow).
143+
- `activate_voucher`: final activation endpoint, binds voucher to MAC.
144+
- `authorize_mac`: used by read-for-access to authorize a MAC for a limited time.
145+
- `client_ip`: legacy endpoint that references old modules and is not used by current voucher flow.
146+
147+
## 12. Ubus/rpcd API
148+
149+
The ubus service is implemented in `packages/pirania/files/usr/libexec/rpcd/pirania` and exposed via ACLs in `packages/pirania/files/usr/share/rpcd/acl.d/pirania.json`.
150+
151+
Supported calls include:
152+
153+
- `get_portal_config`, `set_portal_config`, `disable`
154+
- `show_url`, `change_url`
155+
- `add_vouchers`, `list_vouchers`, `invalidate`, `rename`
156+
- `get_portal_page_content`, `set_portal_page_content`
157+
158+
These are consumed by Lime-App or other management tools.
159+
160+
## 13. Tests
161+
162+
Pirania tests live under `packages/pirania/tests/` and cover portal flows, voucher logic, rpcd handlers, and CGI helpers.
163+
164+
## 14. End-to-end flow summary
165+
166+
Voucher mode:
167+
168+
1. User hits an external site; DNS/HTTP are redirected to Pirania.
169+
2. User lands on `auth.html` and submits a voucher code.
170+
3. `preactivate_voucher` checks the code; if valid, the user waits on `info.html` (JS flow) and then calls `activate_voucher`.
171+
4. Voucher binds to MAC and nftables set is refreshed.
172+
5. User is redirected to the original URL or `authenticated.html`.
173+
174+
Read-for-access mode:
175+
176+
1. User hits an external site; DNS/HTTP are redirected to Pirania.
177+
2. User lands on `read_for_access.html` and waits the countdown.
178+
3. `authorize_mac` stores the MAC with a short TTL and refreshes nftables.
179+
4. User is redirected to the original URL or `authenticated.html`.
180+
181+
## 15. Notes and caveats
182+
183+
- The current implementation uses **nftables** (not iptables) via `captive-portal`.
184+
- `catch_interfaces` and `catch_bridged_interfaces` are applied to nftables rules: L3 interfaces via `iifname` matching in the inet table, L2 bridged interfaces via bridge-family marking.
185+
- **Enforcement is per-node.** Each node applies its own nftables rules independently. The voucher database is shared across the mesh via `shared-state-pirania`, but traffic capture and filtering happen only on nodes where Pirania is enabled. Clients connecting to a node with Pirania disabled will bypass voucher enforcement entirely, even if other nodes in the mesh have it active. For consistent mesh-wide access control, ensure Pirania is enabled on **all** client-facing nodes.
186+
- The `client_ip` CGI script depends on legacy modules (`voucher.logic`, `voucher.db`) and is non-functional (see #1249).
187+
188+
---
189+
190+
This document describes the current state of the code. For CLI usage and examples, see `Readme.md`.

0 commit comments

Comments
 (0)