pirania
Captive Portal for routers that want to share their Internet connection via vouchers.

Voucher and Captive Portal solution for community networks
This tool allows an administrator to manage a voucher system to get through the gateway.
It could be used in a community that wants to share an Internet connection and for that the user's pay a fraction each, but needs the payment from everyone. So the vouchers allows to control the payments via the control of the access to Internet.
Additionally vouchers usage can be deactivated in order to use the captive portal only to show valuable information for network visitors.
Features
These are the currently implemented features:
- Runs directly from the OpenWrt router: no need for extra hardware
- Integrates it's administration with Ubus and LiMe App
- Has a command-line interface for listing, creating and removing vouchers
- Voucher database is shared among nodes in the network
- Portal "splash" screen content (logo, title, main text, etc) is distributed across the network.
- Can be used without vouchers (read-for-access mode).
- Tranca Redes: scheduled second-layer access control that restricts authorized MACs to specific destination categories during configured hours.
Prerequisites
This software assumes that will be running on a OpenWrt distribution (because uses uci for config). Needs the nftables package installed (provides the nft command and kernel modules).
Install
- add the libremesh software feed to opkg
- opkg install pirania
Command line
epoc is expressed in Unix Timestamp format. You can use a tool like unixtimestamp.com to get a date in the correct format.
captive-portal start
Starts pirania. If you want pirania to automatically turn on use: uci set pirania.base_config.enabled=1 && uci commit
captive-portal stop
Stops pirania. If you want pirania to stop automatically turning on use: uci set pirania.base_config.enabled=0 && uci commit
voucher list
Lists all the vouchers.
voucher list_active
List all the vouchers that are currently active.
voucher list_expired
List all expired vouchers.
voucher list_available
List all vouchers available for activation.
voucher add
Create a new voucher. This voucher will start deactivated and not bonded to any device.
Params:
name: a name used to identify the voucherduration-m: duration of the voucher in minutes. If no value is provided a permanent voucher will be created. The duration takes affect when the voucher is activated.activation-deadline: after this date (unix time) the voucher cannot be activated.
Flags:
--unrestrictedor-u: create an unrestricted voucher that bypasses Tranca Redes restrictions.--durationor-d: specify duration in minutes (alternative to positional arg).--deadline: specify activation deadline (alternative to positional arg).
To create a 60 minutes voucher: voucher add my-voucher-name 60
To create an unrestricted permanent voucher: voucher add my-voucher-name --unrestricted
voucher activate
Activates a voucher, assigning a mac address. After the activation, the device with this MAC address will have internet access.
Params:
secret-code: the password of the voucher.mac: the MAC address of the device that will have access.
Ex: voucher activate mysecret 00:11:22:33:44:55
voucher deactivate
Deactivate a voucher of the specified ID.
Params:
ID: a string used to identify the voucher.
Ex: voucher deactivate Qzt3WF
voucher invalidate
Invalidate a voucher (soft delete). The voucher remains in the database until pruned.
Params:
ID: a string used to identify the voucher.
Ex: voucher invalidate Qzt3WF
voucher is_mac_authorized
Check if a specific mac address is authorized.
Params:
mac: a device's mac address
Ex: voucher is_mac_authorized d0:82:7a:49:e2:37
voucher show_authorized_macs
Show MAC addresses of all currently active vouchers.
CLI usage example
$ voucher list
$ voucher add san-notebook 60
Q3TJZS san-notebook ZRJUXN xx:xx:xx:xx:xx:xx Wed Sep 8 23:47:40 2021 60 - 1 normal
$ voucher list
Q3TJZS san-notebook ZRJUXN xx:xx:xx:xx:xx:xx Wed Sep 8 23:47:40 2021 60 - 1 normal
$ voucher list_active
$ voucher activate ZRJUXN 00:11:22:33:44:55
Voucher activated!
$ voucher list
Q3TJZS san-notebook ZRJUXN 00:11:22:33:44:55 Wed Sep 8 23:47:40 2021 60 Thu Sep 9 00:48:33 2021 2 normal
$ voucher list_active
Q3TJZS san-notebook ZRJUXN 00:11:22:33:44:55 Wed Sep 8 23:47:40 2021 60 Thu Sep 9 00:48:33 2021 2 normal
$ voucher deactivate Q3TJZS
ok
$ voucher list_active
$ voucher list
Q3TJZS san-notebook ZRJUXN xx:xx:xx:xx:xx:xx Wed Sep 8 23:47:40 2021 60 - 3 normalHow it works
It uses nftables rules to filter inbound connections outside the mesh network.
General overview of file hierarchy and function
files/
/etc/config/pirania is the UCI config
/etc/pirania/vouchers/ (default path) contains the database of vouchers
/etc/init.d/pirania-uhttpd starts a uhttpd on port 59080 that replies any request with a redirect towards a preset URL
/usr/lib/lua/voucher/ contains lua libraries used by /usr/bin/voucher
/usr/bin/voucher is a CLI to manage the db (has functions list, list_active, show_authorized_macs, add, activate, deactivate and is_mac_authorized)
/usr/bin/captive-portal sets up nftables rules to capture traffic
/usr/libexec/rpcd/pirania ubus pirania API (this is used by the web frontend)
/usr/share/rpcd/acl.d/pirania.json ACL for the ubus pirania API
/etc/shared-state/publishers/shared-state-publish_vouchers inserts into shared-state the local voucher db
/etc/shared-state/hooks/pirania/generate_vouchers bring updated or new vouchers from the shared-state database into the local voucher db
/usr/lib/lua/read_for_access contains the library used by
/usr/lib/lua/portal to manage access in read for access mode (aka without vouchers)
/usr/bin/tranca-redes-scheduler evaluates the Tranca Redes schedule and toggles the active stateubus API
get_portal_config-> returns portal configuration (activated, with_vouchers)set_portal_config(activated, with_vouchers)-> configures and starts/stops the portaldisable-> disables the portal and stops captive-portalshow_url-> returns the configured portal URLchange_url(url)-> changes the portal URLadd_vouchers(name, qty, duration_m, activation_deadline, permanent, unrestricted)-> creates voucherslist_vouchers-> lists all vouchersrename(id, name)-> renames a voucherinvalidate(id)-> invalidates a voucherget_portal_page_content-> returns portal page content (title, text, logo, etc.)set_portal_page_content(...)-> sets portal page content
Under the hood
Traffic capture
/usr/bin/captive-portal sets up nftables rules to capture traffic. It creates a set of rules and native nft sets:
- pirania-auth-macs: authorized macs go into this set. starts empty.
- pirania-allowlist-ipv4: with the members of the allowlist in the config file (10.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12)
- pirania-allowlist-ipv6: same as ipv4 but for ipv6
Traffic capture is scoped to configured interfaces:
catch_interfaces: L3 interfaces for direct nftables matchingcatch_bridged_interfaces: L2 bridged interfaces (e.g. Wi-Fi AP) for bridge-family marking
Rules:
- DNS packets, that are not from the allowed sets, are redirected to our own captive portal DNS at 59053
- HTTP packets, that are not from the allowed sets, are redirected to our own captive portal HTTP at 59080
- packets from the allowed sets are allowed
- the rest of the packets are rejected (drop and send an error to the client)
HTTP flow
/etc/init.d/pirania-uhttpd starts a HTTP server (uhttpd) on port 59080 that replies any request with a redirect towards a preset URL.
- In case that voucher usage is activated:
pirania.base_config.url_auth. - Otherwise:
pirania.read_for_access.url_portalThis is performed by the lua script/www/pirania-redirect/redirect. As both url are in the allowlist ip range (http://thisnode.info/portal/ by default) then the "normal" HTTP server listening in port 80 will answer after the redirect.
So the flow when using vouchers is:
- navigate to a non allowed ip: for example
http://orignal.org/baz/?foo=bar - get redirected with a 302 where you can put a voucher code to enter:
http://thisnode.info/cgi-bin/portal/auth.html?prev=http%3A%2F%2Foriginal.org%2Fbaz%2F%3Ffoo%3Dbar - submiting the form should perform a GET to
http://thisnode.info/cgi-bin/pirania/preactivate_voucher?voucher=secretcode&prev=http%3A%2F%2Foriginal.org%2Fbaz%2F%3Ffoo%3Dbar - The preactivate_voucher script does two different things depending on javascript support:
- If nojs=true then the voucher is activated with the client MAC (taken from the ARP table with its IP) and the voucher code. If the activation succeeds it redirects to
url_authenticated. - If nojs=false there is a check if the voucher code would be valid (there is an unused and valid voucher with that code). If the voucher would be valid then a redirect to the portal INFO page(
pirania.base_config.url_info) is performed with the voucher code as param url. The portal info shows the updated information of the community and there is a time that you have to wait to be able to continue (This is done with JS). When the timer reaches 0 you can click in continue. This redirects now tohttp://thisnode.info/cgi-bin/pirania/activate_voucher?voucher=secretcode. Theactivate_voucherscript does the voucher activation. then it redirects tourl_authenticated. If the code fails it will redirect tohttp://thisnode.info/cgi-bin/portal/fail.htmlthat is identical to auth.html but with an error message.
- If nojs=true then the voucher is activated with the client MAC (taken from the ARP table with its IP) and the voucher code. If the activation succeeds it redirects to
The flow without using vouchers (read for access mode) is:
- navigate to a non allowed ip: for example
http://orignal.org/baz/?foo=bar - get redirected with a 302 to:
http://thisnode.info/cgi-bin/portal/read_for_access.html?prev=http%3A%2F%2Foriginal.org%2Fbaz%2F%3Ffoo%3Dbar - Once there if the client has js support then a countdown of 15 seconds is shown and when it reaches 0 the user can click on continue, which sends a GET request to
http://thisnode.info/cgi-bin/pirania/authorize_mac?prev=http%3A%2F%2Foriginal.org%2Fbaz%2F%3Ffoo%3Dbarwhich will trigger a redirection toprevurl. - If there the client has no js support, then the button is enabled immediately, and after clicking in continue a redirection to
url_authenticatedis triggered.
Tranca Redes
Tranca Redes is a second-layer access control mode that can restrict internet access during scheduled hours. When active:
- Authorized MACs can only reach destinations in configured category allowlists
- Unrestricted voucher holders bypass Tranca restrictions entirely
- Unauthorized MACs remain blocked as usual
Configuration is in /etc/config/pirania under the tranca_redes section:
enabled: enable/disable Tranca Redesstart_time/end_time: schedule in HH:MM format (supports overnight wrap)days: which days of the week Tranca is activeallowlist_category: references category sections with IPv4 URL sources
The scheduler (/usr/bin/tranca-redes-scheduler) runs via cron every minute to evaluate the schedule.
Makefile
include ../../libremesh.mk
define Package/$(PKG_NAME)
SUBMENU:=Captive Portals
SECTION:=net
CATEGORY:=Network
MAINTAINER:=Luandro <luandro@gmail.com>
TITLE:=Captive portal with vouchers.
DEPENDS:=+nftables +kmod-nft-bridge +shared-state +shared-state-pirania \
+uhttpd-mod-lua +lime-system +luci-lib-jsonc \
+luci-lib-nixio +libubus-lua +libuci-lua
PKGARCH:=all
endef
define Package/$(PKG_NAME)/description
Captive Portal for routers that want to share their Internet connection via vouchers.
endef
$(eval $(call BuildPackage,$(PKG_NAME)))undefined