OpenWrt 集成 openNDS 进行 强制门户(Captive Portal) 认证
OpenWrt 集成 openNDS 进行 强制门户(Captive Portal) 认证
openNDS 是一个高性能、轻量级的强制门户(Captive Portal)解决方案。它的核心作用是在公共Wi-Fi网络中,作为一道“看门人”,在设备获得完整互联网访问权限之前,先向用户展示一个特定的登录或通知页面(也就是我们常说的“认证页”或“Splash Page”)。
具体来说,它的作用和价值体现在以下几个方面:
🚪 核心作用:公共网络接入管理
它主要扮演一个“边界控制网关”的角色,用于管理从公共局域网到互联网的访问。这个过程通常是自动触发的:
-
自动拦截:当你的手机或电脑连接到一个由openNDS管理的Wi-Fi网络时,几乎所有现代设备都会自动发起一个网络请求(Captive Portal Detection,CPD)。
-
展示页面:openNDS会识别并拦截这个请求,然后向你的设备推送一个“Splash Page”。
-
授予权限:你在页面上完成所需的操作(比如点击“继续”按钮、同意条款或输入凭证)后,才能获得完整的互联网访问权限。
✨ 两种开箱即用的方式
为了适应不同场景,openNDS默认提供了两种简单的方式:
-
“点击继续”(Click to Continue):这是默认模式。用户连接网络后,只需点击一个按钮(比如“我同意条款”)即可上网,主要用于告知服务条款和记录基本设备信息。
-
用户名/邮箱登录(username/email-address login):用户需要输入用户名和邮箱地址才能继续。这种方式可以用于收集用户信息,便于后续统计或营销,同时也能记录访问日志。
🛠️ 为什么值得关注:功能强大的“引擎”
openNDS的独特之处在于,它不仅仅是一个简单的页面显示工具,更是一个功能强大的开发平台,被设计为复杂认证系统的“引擎”。这得益于它提供的几个关键API:
-
Forward Authentication Service (FAS):这是最主要的认证服务。它允许你把认证页面放在一个独立的网络服务器上(可以是在路由器本地、局域网内,甚至是云端),从而实现高度定制化的登录逻辑。
-
PreAuth:可以看作是FAS的轻量级版本。认证页面直接由openNDS自带的web服务器生成,无需额外搭建独立服务器,非常适合那些硬件资源(如内存、闪存)有限的路由器。
-
BinAuth:一种后认证脚本。当用户认证成功后,可以触发执行一个自定义的脚本或程序,用于实现更复杂的后续操作,比如统计流量、设置带宽限制等。
📜 项目起源:与NoDogSplash的渊源
openNDS源自一个叫NoDogSplash的老牌强制门户项目。后来开发者为了能更专注于开发和创新,决定将其拆分为两个项目:
-
openNDS:目标是开发定制化、功能丰富的强制门户解决方案,也就是现在我们在讨论的这个。
-
NoDogSplash:则专注于保持极简和轻量,以兼容硬件资源极其有限的旧设备。
所以,可以简单地把openNDS理解为NoDogSplash的“进化加强版”,它在易用性的基础上,提供了更广阔的开发和定制空间。
支持 流量配额 上下行限速 以及 通过 FAS 集成 Radius 计费
-
流量配额:支持对单个客户端设置上传和下载的数据总量配额,单位是KB。当客户端使用量超过配额后,会被强制下线,需要重新认证才能继续上网。
-
上下行限速:支持限制客户端的上下行速率,单位是Kb/s。它内置了动态令牌桶过滤器,允许客户端在短时间内突发高速传输(比如快速加载网页),但如果平均速率持续超过阈值,就会开始限速,从而实现“公平使用策略”。你还可以通过配置OpenWrt的SQM(智能队列管理)软件包来实现更精细的流量整形。
-
通过FAS集成Radius计费:这是可行的,主要通过以下两种方式实现:
-
FAS (Forward Authentication Service) 的三种级别:
-
级别 3 (fas_secure_enable=3):这是最标准的方式,使用Authmon守护进程。FAS可以决定每个客户端的配额和速率,并覆盖全局配置,是实现与Radius等外部认证系统集成的推荐路径。
-
级别 0, 1, 2:在这些级别下,也可以通过BinAuth脚本或
ndsctl auth命令来传递并覆盖配额和速率值。
-
-
BinAuth (二进制认证) 脚本:BinAuth是openNDS内置的“后认证”脚本机制,在客户端认证、下线、配额用完等事件发生时被调用。通过编写自定义的BinAuth脚本,你可以:
-
动态下发策略:脚本可以解析从FAS传来的自定义变量,从而覆盖单个客户端的会话时长、数据速率和数据量配额。
-
响应配额事件:当客户端的下载或上传配额用尽时,BinAuth会收到
downquota_deauth或upquota_deauth事件,此时脚本可以执行你定义的操作,比如与Radius服务器进行计费结算。
-
-
⚙️ 配置示例
你可以通过编辑 /etc/config/opennds 文件来设置全局的流量配额和速率限制:
# 全局数据量配额 (单位: KB)
option uploadquota '512000' # 上传配额 500MB
option downloadquota '1024000' # 下载配额 1GB
# 全局数据速率限制 (单位: Kb/s)
option uploadrate '2000' # 上传速率限制为 2Mb/s
option downloadrate '5000' # 下载速率限制为 5Mb/s
# 速率计算窗口 (秒)
option ratecheckwindow '2'
请注意:在openNDS的语境中,"上传"指的是客户端向互联网发送数据,"下载"指的是客户端从互联网接收数据。你可以在社区论坛找到更详细的讨论,例如关于配置配额后是直接断网还是降低速率的方案。
Windows 系统电脑 和 iOS 苹果手机 连接这个网络 进行 强制门户(Captive Portal) 认证
支持主流系统(Windows、Apple iOS、Android 等)的网络探测与认证门户(Captive Portal)检测流程,设备系统无需安装任何第三方软件。支持本地流量配额和限速,配合FAS可实现远程统一云计费和限速。
当前的OpenNDS集成了preauthenticated_fqdn_list(预认证客户端的条件FQDN访问)功能,可以理解是一个白名单功能,用于在未登录的情况仅能访问指定URL的充值网站,反馈网站等,提升用户体验。


root@mt300v2:~# root@mt300v2:~# cat /etc/config/opennds config opennds option enabled '1' option debuglevel '3' option faskey 'd5a54f05fefc844b53fb9b201817c2dd3a27190bfe9b31f4b13022e77ad97010' option allow_preemptive_authentication '1' option login_option_enabled '3' option themespec_path '/usr/lib/opennds/theme_voucher.sh' root@mt300v2:~# root@mt300v2:~# root@mt300v2:~# uci show opennds opennds.@opennds[0]=opennds opennds.@opennds[0].enabled='1' opennds.@opennds[0].debuglevel='3' opennds.@opennds[0].faskey='d5a54f05fefc844b53fb9b201817c2dd3a27190bfe9b31f4b13022e77ad97010' opennds.@opennds[0].allow_preemptive_authentication='1' opennds.@opennds[0].login_option_enabled='3' opennds.@opennds[0].themespec_path='/usr/lib/opennds/theme_voucher.sh' root@mt300v2:~# root@mt300v2:~# root@mt300v2:~# tree /etc/opennds/ /etc/opennds/ ├── config.uci ├── fas-aes-https.php ├── fas-aes.php ├── fas-hid-https.php ├── fas-hid.php └── htdocs ├── images │ └── splash.jpg └── splash.css 3 directories, 7 files root@mt300v2:~# root@mt300v2:~# root@mt300v2:~# tree /usr/lib/opennds/ /usr/lib/opennds/ ├── authmon.sh ├── binauth_log.sh ├── client_params.sh ├── custombinauth.sh ├── dnsconfig.sh ├── download_resources.sh ├── get_client_interface.sh ├── libopennds.sh ├── post-request.php ├── restart.sh ├── splash.jpg ├── splash.png ├── theme_click-to-continue-basic.sh ├── theme_click-to-continue-custom-placeholders.sh ├── theme_user-email-login-basic.sh ├── theme_user-email-login-custom-placeholders.sh ├── theme_voucher.sh ├── voucher_generator.sh └── vouchers.txt 1 directory, 19 files root@mt300v2:~# root@mt300v2:~# root@mt300v2:~# ls -alh /tmp/nds* srwxr-xr-x 1 root root 0 Jul 23 22:15 /tmp/ndsctl.sock -rw-r--r-- 1 root root 1 Jul 23 22:15 /tmp/ndsdebuglevel /tmp/ndscids: drwxr-xr-x 3 root root 100 Jul 23 22:15 . drwxrwxrwt 22 root root 720 Jul 23 22:15 .. -rw-r--r-- 1 root root 11 Jul 23 22:16 heartbeat -rw-r--r-- 1 root root 142 Jul 23 22:15 ndsinfo drwxr-xr-x 2 root root 40 Jul 23 22:15 preemptive_auth /tmp/ndslog: drwxr-xr-x 2 root root 100 Jul 23 21:56 . drwxrwxrwt 22 root root 720 Jul 23 22:15 .. -rw-r--r-- 1 root root 0 Jul 23 18:40 authlog.log -rw-r--r-- 1 root root 0 Jul 23 18:40 binauthlog.log -rw-r--r-- 1 root root 930 Jul 23 21:56 vouchers.txt root@mt300v2:~# root@mt300v2:~#
root@mt300v2:/usr/lib/opennds# ndsctl status ================== openNDS Status ==== Version: 10.3.1 Uptime: 3m 35s Gateway Name: [ openNDS Node:9483c40c47e6 ] Debug Level: [ 3 ] Gateway FQDN: [ status.client ] Managed interface: br-lan Upstream gateway(s) [ online:192.168.110.1,eth0.2 ] MHD Server [ version 1.0.2 ] listening on: http://192.168.100.1:2050 Maximum Html Page size is [ 10240 ] Bytes Preemptive Authentication is Enabled Binauth Script: /usr/lib/opennds/binauth_log.sh ThemeSpec Core Library: /usr/lib/opennds/libopennds.sh FAS: Secure Level 1, URL: http://status.client:2050/opennds_preauth/ Client Check Interval: 15s Rate Check Window: 2 check intervals (30s) Preauthenticated Client Idle Timeout: 30m Authenticated Client Idle Timeout: 120m Download rate limit threshold (default per client): no limit Upload rate limit threshold (default per client): no limit Download quota (default per client): no limit Upload quota (default per client): no limit Total download: 0 kByte; average: 0.00 kbit/s Total upload: 0 kByte; average: 0.00 kbit/s ==== Client authentications since start: 0 Current clients: 0 ==== Trusted MAC addresses: none ==== Walledgarden FQDNs: none Walledgarden Ports: all ==== Blocklist FQDNs: none Blocklist Ports: all ======== root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# ls authmon.sh libopennds.sh theme_user-email-login-basic.sh binauth_log.sh post-request.php theme_user-email-login-custom-placeholders.sh client_params.sh restart.sh theme_voucher.sh custombinauth.sh splash.jpg voucher_generator.sh dnsconfig.sh splash.png vouchers.txt download_resources.sh theme_click-to-continue-basic.sh get_client_interface.sh theme_click-to-continue-custom-placeholders.sh root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# cat theme_voucher.sh #!/bin/sh #Copyright (C) The openNDS Contributors 2004-2023 #Copyright (C) BlueWave Projects and Services 2015-2024 #Copyright (C) Francesco Servida 2023 #This software is released under the GNU GPL license. # # Warning - shebang sh is for compatibliity with busybox ash (eg on OpenWrt) # This must be changed to bash for use on generic Linux # # Title of this theme: title="theme_voucher" # functions: generate_splash_sequence() { login_with_voucher } header() { # Define a common header html for every page served gatewayurl=$(printf "${gatewayurl//%/\\x}") echo "<!DOCTYPE html> <html> <head> <meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\"> <meta http-equiv=\"Pragma\" content=\"no-cache\"> <meta http-equiv=\"Expires\" content=\"0\"> <meta charset=\"utf-8\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> <link rel=\"shortcut icon\" href=\"/images/splash.jpg\" type=\"image/x-icon\"> <link rel=\"stylesheet\" type=\"text/css\" href=\"/splash.css\"> <title>$gatewayname</title> </head> <body> <div class=\"offset\"> <div class=\"insert\" style=\"max-width:100%;\"> " } footer() { # Define a common footer html for every page served year=$(date +'%Y') echo " <hr> <div style=\"font-size:0.5em;\"> <br> <img style=\"height:60px; width:60px; float:left;\" src=\"$gatewayurl""$imagepath\" alt=\"Splash Page: For access to the Internet.\"> © Portal: Projects and Services 2015 - $year<br> <br> Portal Version: $version <br><br><br><br> </div> </div> </div> </body> </html> " exit 0 } login_with_voucher() { # This is the simple click to continue splash page with no client validation. # The client is however required to accept the terms of service. if [ "$tos" = "accepted" ]; then #echo "$tos <br>" #echo "$voucher <br>" voucher_validation footer fi voucher_form footer } check_voucher() { # Strict Voucher Validation for shell escape prevention - Only alphanumeric (and dash character) allowed. if validation=$(echo -n $voucher | grep -E "^[a-zA-Z0-9-]{9}$"); then #echo "Voucher Validation successful, proceeding" : #no-op else #echo "Invalid Voucher - Voucher must be alphanumeric (and dash) of 9 chars." return 1 fi ############################################################################################################################## # WARNING # The voucher roll is written to on every login # If its location is on router flash, this **WILL** result in non-repairable failure of the flash memory # and therefore the router itself. This will happen, most likely within several months depending on the number of logins. # # The location is set here to be the same location as the openNDS log (logdir) # By default this will be on the tmpfs (ramdisk) of the operating system. # Files stored here will not survive a reboot. voucher_roll="$logdir""vouchers.txt" # # In a production system, the mountpoint for logdir should be changed to the mount point of some external storage # eg a usb stick, an external drive, a network shared drive etc. # # See "Customise the Logfile location" at the end of this file # ############################################################################################################################## output=$(grep $voucher $voucher_roll | head -n 1) # Store first occurence of voucher as variable #echo "$output <br>" #Matched line if [ $(echo -n $output | wc -w) -ge 1 ]; then #echo "Voucher Found - Checking Validity <br>" current_time=$(date +%s) voucher_token=$(echo -n $output | sed -r "s#([a-zA-Z0-9-]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)#\1#") voucher_rate_down=$(echo -n $output | sed -r "s#([a-zA-Z0-9-]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)#\2#") voucher_rate_up=$(echo -n $output | sed -r "s#([a-zA-Z0-9-]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)#\3#") voucher_quota_down=$(echo -n $output | sed -r "s#([a-zA-Z0-9-]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)#\4#") voucher_quota_up=$(echo -n $output | sed -r "s#([a-zA-Z0-9-]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)#\5#") voucher_time_limit=$(echo -n $output | sed -r "s#([a-zA-Z0-9-]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)#\6#") voucher_first_punched=$(echo -n $output | sed -r "s#([a-zA-Z0-9-]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)#\7#") # Set limits according to voucher upload_rate=$voucher_rate_up download_rate=$voucher_rate_down upload_quota=$voucher_quota_up download_quota=$voucher_quota_down if [ $voucher_first_punched -eq 0 ]; then #echo "First Voucher Use" # "Punch" the voucher by setting the timestamp to now voucher_expiration=$(($current_time + $voucher_time_limit * 60)) # Override session length according to voucher session_length=$voucher_time_limit sed -i -r "s/($voucher.*,)(0)/\1$current_time/" $voucher_roll return 0 else #echo "Voucher Already Used, Checking validity <br>" # Current timestamp <= than Punch Timestamp + Validity (minutes) * 60 secs/minute voucher_expiration=$(($voucher_first_punched + $voucher_time_limit * 60)) if [ $current_time -le $voucher_expiration ]; then time_remaining=$(( ($voucher_expiration - $current_time) / 60 )) #echo "Voucher is still valid - You have $time_remaining minutes left <br>" # Override session length according to voucher session_length=$time_remaining # Nothing to change in the roll return 0 else #echo "Voucher has expired, please try another one <br>" # Delete expired voucher from roll sed -i "/$voucher/"d $voucher_roll return 1 fi fi else echo "No Voucher Found - Retry <br>" return 1 fi # Should not get here return 1 } voucher_validation() { originurl=$(printf "${originurl//%/\\x}") check_voucher if [ $? -eq 0 ]; then #echo "Voucher is Valid, click Continue to finish login<br>" # Refresh quotas with ones imported from the voucher roll. quotas="$session_length $upload_rate $download_rate $upload_quota $download_quota" # Set voucher used (useful if for accounting reasons you track who received which voucher) userinfo="$title - $voucher" # Authenticate and write to the log - returns with $ndsstatus set auth_log # output the landing page - note many CPD implementations will close as soon as Internet access is detected # The client may not see this page, or only see it briefly auth_success=" <p> <big-red> You are now logged in and have been granted access to the Internet. </big-red> <hr> </p> This voucher is valid for $session_length minutes. <hr> <p> <italic-black> You can use your Browser, Email and other network Apps as you normally would. </italic-black> </p> <p> Your device originally requested <b>$originurl</b> <br> Click or tap Continue to go to there. </p> <form> <input type=\"button\" VALUE=\"Continue\" onClick=\"location.href='$originurl'\" > </form> <hr> " auth_fail=" <p> <big-red> Something went wrong and you have failed to log in. </big-red> <hr> </p> <hr> <p> <italic-black> Your login attempt probably timed out. </italic-black> </p> <p> <br> Click or tap Continue to try again. </p> <form> <input type=\"button\" VALUE=\"Continue\" onClick=\"location.href='$originurl'\" > </form> <hr> " if [ "$ndsstatus" = "authenticated" ]; then echo "$auth_success" else echo "$auth_fail" fi else echo "<big-red>Voucher is not Valid, click Continue to restart login<br></big-red>" echo " <form> <input type=\"button\" VALUE=\"Continue\" onClick=\"location.href='$originurl'\" > </form> " fi # Serve the rest of the page: read_terms footer } voucher_form() { # Define a click to Continue form # From openNDS v10.2.0 onwards, QL code scanning is supported to pre-fill the "voucher" field in this voucher_form page. # # The QL code must be of the link type and be of the following form: # # http://[gatewayfqdn]/login?voucher=[voucher_code] # # where [gatewayfqdn] defaults to status.client (can be set in the config) # and [voucher_code] is of course the unique voucher code for the current user # Get the voucher code: voucher_code=$(echo "$cpi_query" | awk -F "voucher%3d" '{printf "%s", $2}' | awk -F "%26" '{printf "%s", $1}') echo " <med-blue> Welcome! </med-blue><br> <hr> Your IP: $clientip <br> Your MAC: $clientmac <br> <hr> <form action=\"/opennds_preauth/\" method=\"get\"> <input type=\"hidden\" name=\"fas\" value=\"$fas\"> <input type=\"checkbox\" name=\"tos\" value=\"accepted\" required> I accept the Terms of Service<br> Voucher #: <input type=\"text\" name=\"voucher\" value=\"$voucher_code\" required><br> <input type=\"submit\" value=\"Connect\" > </form> <br> " read_terms footer } read_terms() { #terms of service button echo " <form action=\"/opennds_preauth/\" method=\"get\"> <input type=\"hidden\" name=\"fas\" value=\"$fas\"> <input type=\"hidden\" name=\"terms\" value=\"yes\"> <input type=\"submit\" value=\"Read Terms of Service \" > </form> " } display_terms() { # This is the all important "Terms of service" # Edit this long winded generic version to suit your requirements. #### # WARNING # # It is your responsibility to ensure these "Terms of Service" are compliant with the REGULATIONS and LAWS of your Country or State. # In most locations, a Privacy Statement is an essential part of the Terms of Service. #### #Privacy echo " <b style=\"color:red;\">Privacy.</b><br> <b> By logging in to the system, you grant your permission for this system to store any data you provide for the purposes of logging in, along with the networking parameters of your device that the system requires to function.<br> All information is stored for your convenience and for the protection of both yourself and us.<br> All information collected by this system is stored in a secure manner and is not accessible by third parties.<br> </b><hr> " # Terms of Service echo " <b style=\"color:red;\">Terms of Service for this Hotspot.</b> <br> <b>Access is granted on a basis of trust that you will NOT misuse or abuse that access in any way.</b><hr> <b>Please scroll down to read the Terms of Service in full or click the Continue button to return to the Acceptance Page</b> <form> <input type=\"button\" VALUE=\"Continue\" onClick=\"history.go(-1);return true;\"> </form> " # Proper Use echo " <hr> <b>Proper Use</b> <p> This Hotspot provides a wireless network that allows you to connect to the Internet. <br> <b>Use of this Internet connection is provided in return for your FULL acceptance of these Terms Of Service.</b> </p> <p> <b>You agree</b> that you are responsible for providing security measures that are suited for your intended use of the Service. For example, you shall take full responsibility for taking adequate measures to safeguard your data from loss. </p> <p> While the Hotspot uses commercially reasonable efforts to provide a secure service, the effectiveness of those efforts cannot be guaranteed. </p> <p> <b>You may</b> use the technology provided to you by this Hotspot for the sole purpose of using the Service as described here. You must immediately notify the Owner of any unauthorized use of the Service or any other security breach.<br><br> We will give you an IP address each time you access the Hotspot, and it may change. <br> <b>You shall not</b> program any other IP or MAC address into your device that accesses the Hotspot. You may not use the Service for any other reason, including reselling any aspect of the Service. Other examples of improper activities include, without limitation: </p> <ol> <li> downloading or uploading such large volumes of data that the performance of the Service becomes noticeably degraded for other users for a significant period; </li> <li> attempting to break security, access, tamper with or use any unauthorized areas of the Service; </li> <li> removing any copyright, trademark or other proprietary rights notices contained in or on the Service; </li> <li> attempting to collect or maintain any information about other users of the Service (including usernames and/or email addresses) or other third parties for unauthorized purposes; </li> <li> logging onto the Service under false or fraudulent pretenses; </li> <li> creating or transmitting unwanted electronic communications such as SPAM or chain letters to other users or otherwise interfering with other user's enjoyment of the service; </li> <li> transmitting any viruses, worms, defects, Trojan Horses or other items of a destructive nature; or </li> <li> using the Service for any unlawful, harassing, abusive, criminal or fraudulent purpose. </li> </ol> " # Content Disclaimer echo " <hr> <b>Content Disclaimer</b> <p> The Hotspot Owners do not control and are not responsible for data, content, services, or products that are accessed or downloaded through the Service. The Owners may, but are not obliged to, block data transmissions to protect the Owner and the Public. </p> The Owners, their suppliers and their licensors expressly disclaim to the fullest extent permitted by law, all express, implied, and statutary warranties, including, without limitation, the warranties of merchantability or fitness for a particular purpose. <br><br> The Owners, their suppliers and their licensors expressly disclaim to the fullest extent permitted by law any liability for infringement of proprietory rights and/or infringement of Copyright by any user of the system. Login details and device identities may be stored and be used as evidence in a Court of Law against such users. <br> " # Limitation of Liability echo " <hr><b>Limitation of Liability</b> <p> Under no circumstances shall the Owners, their suppliers or their licensors be liable to any user or any third party on account of that party's use or misuse of or reliance on the Service. </p> <hr><b>Changes to Terms of Service and Termination</b> <p> We may modify or terminate the Service and these Terms of Service and any accompanying policies, for any reason, and without notice, including the right to terminate with or without notice, without liability to you, any user or any third party. Please review these Terms of Service from time to time so that you will be apprised of any changes. </p> <p> We reserve the right to terminate your use of the Service, for any reason, and without notice. Upon any such termination, any and all rights granted to you by this Hotspot Owner shall terminate. </p> " # Indemnity echo " <hr><b>Indemnity</b> <p> <b>You agree</b> to hold harmless and indemnify the Owners of this Hotspot, their suppliers and licensors from and against any third party claim arising from or in any way related to your use of the Service, including any liability or expense arising from all claims, losses, damages (actual and consequential), suits, judgments, litigation costs and legal fees, of every kind and nature. </p> <hr> <form> <input type=\"button\" VALUE=\"Continue\" onClick=\"history.go(-1);return true;\"> </form> " footer } #### end of functions #### ################################################# # # # Start - Main entry point for this Theme # # # # Parameters set here overide those # # set in libopennds.sh # # # ################################################# # Quotas and Data Rates ######################################### # Set length of session in minutes (eg 24 hours is 1440 minutes - if set to 0 then defaults to global sessiontimeout value): # eg for 100 mins: # session_length="100" # # eg for 20 hours: # session_length=$((20*60)) # # eg for 20 hours and 30 minutes: # session_length=$((20*60+30)) session_length="0" # Set Rate and Quota values for the client # The session length, rate and quota values could be determined by this script, on a per client basis. # rates are in kb/s, quotas are in kB. - if set to 0 then defaults to global value). upload_rate="0" download_rate="0" upload_quota="0" download_quota="0" quotas="$session_length $upload_rate $download_rate $upload_quota $download_quota" # Define the list of Parameters we expect to be sent sent from openNDS ($ndsparamlist): # Note you can add custom parameters to the config file and to read them you must also add them here. # Custom parameters are "Portal" information and are the same for all clients eg "admin_email" and "location" ndscustomparams="" ndscustomimages="" ndscustomfiles="" ndsparamlist="$ndsparamlist $ndscustomparams $ndscustomimages $ndscustomfiles" # The list of FAS Variables used in the Login Dialogue generated by this script is $fasvarlist and defined in libopennds.sh # # Additional custom FAS variables defined in this theme should be added to $fasvarlist here. additionalthemevars="tos voucher" fasvarlist="$fasvarlist $additionalthemevars" # You can choose to define a custom string. This will be b64 encoded and sent to openNDS. # There it will be made available to be displayed in the output of ndsctl json as well as being sent # to the BinAuth post authentication processing script if enabled. # Set the variable $binauth_custom to the desired value. # Values set here can be overridden by the themespec file #binauth_custom="This is sample text sent from \"$title\" to \"BinAuth\" for post authentication processing." # Encode and activate the custom string #encode_custom # Set the user info string for logs (this can contain any useful information) userinfo="$title" ############################################################################################################################## # Customise the Logfile location. ############################################################################################################################## #Note: the default uses the tmpfs "temporary" directory to prevent flash wear. # Override the defaults to a custom location eg a mounted USB stick. #mountpoint="/mylogdrivemountpoint" #logdir="$mountpoint/ndslog/" #logname="ndslog.log" root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# cat voucher_generator.sh #!/bin/sh # voucher_generator.sh # Hotspot Voucher Generator - Compatible with theme_voucher.sh # Generates vouchers in format: XXXX-XXXX (total 9 chars including dash) # ============ Configuration Parameters ============ RATE_DOWN=1024 # Download rate limit (kb/s) RATE_UP=1024 # Upload rate limit (kb/s) QUOTA_DOWN=0 # Download quota (kB, 0=unlimited) QUOTA_UP=0 # Upload quota (kB, 0=unlimited) VALIDITY=1440 # Validity period (minutes, 1440=24 hours) VOUCHER_QTY=10 # Number of vouchers to generate VOUCHER_FILE="vouchers.txt" # Output filename VERBOSE=0 # Verbose output (1=enabled, 0=disabled) # ================================================== # Character set (uppercase letters + digits) CHARS="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" # Generate random alphanumeric string randomstring() { local length=${1:-4} local result="" # Read from /dev/urandom and filter alphanumeric characters result=$(head -c 100 /dev/urandom 2>/dev/null | tr -cd 'A-Z0-9' | head -c $length) # Fallback if result length is insufficient if [ ${#result} -lt $length ]; then local chars_len=36 local temp="" for i in $(seq 1 $length); do local idx=$(( ($$ + RANDOM + i) % chars_len + 1 )) local char=$(echo "$CHARS" | cut -c$idx) temp="${temp}${char}" done result="$temp" fi echo "$result" } # Generate voucher code (format: XXXX-XXXX) # Total length: 4 chars + dash + 4 chars = 9 characters generate_voucher_code() { local part1=$(randomstring 4) local part2=$(randomstring 4) echo "${part1}-${part2}" } # Generate a single voucher line generate_voucher_line() { local code=$(generate_voucher_code) echo "${code},${RATE_DOWN},${RATE_UP},${QUOTA_DOWN},${QUOTA_UP},${VALIDITY},0" } # Generate vouchers and append to file generate_vouchers() { # Create directory if it doesn't exist local dir=$(dirname "$VOUCHER_FILE") if [ ! -d "$dir" ] && [ "$dir" != "." ]; then mkdir -p "$dir" 2>/dev/null fi # Create file if it doesn't exist if [ ! -f "$VOUCHER_FILE" ]; then if [ $VERBOSE -eq 1 ]; then echo "File $VOUCHER_FILE does not exist, creating new file..." >&2 fi touch "$VOUCHER_FILE" fi # Get existing count local existing_count=$(wc -l < "$VOUCHER_FILE" 2>/dev/null || echo 0) if [ $VERBOSE -eq 1 ]; then echo "File: $VOUCHER_FILE" >&2 echo "Existing vouchers: $existing_count" >&2 echo "Generating $VOUCHER_QTY new vouchers..." >&2 fi local temp_file="/tmp/vouchers_temp_$$.txt" local added_count=0 local duplicate_skipped=0 local max_attempts=$((VOUCHER_QTY * 20)) local attempts=0 > "$temp_file" while [ $added_count -lt $VOUCHER_QTY ] && [ $attempts -lt $max_attempts ]; do local line=$(generate_voucher_line) local code=$(echo "$line" | cut -d',' -f1) attempts=$((attempts + 1)) # Check if code already exists in existing file or temp file # theme_voucher.sh uses grep to find voucher codes if ! grep -q "^${code}," "$VOUCHER_FILE" 2>/dev/null && ! grep -q "^${code}," "$temp_file" 2>/dev/null; then echo "$line" >> "$temp_file" added_count=$((added_count + 1)) if [ $VERBOSE -eq 1 ] && [ $((added_count % 5)) -eq 0 ]; then echo " Generated $added_count vouchers..." >&2 fi else duplicate_skipped=$((duplicate_skipped + 1)) fi done # Append to target file cat "$temp_file" >> "$VOUCHER_FILE" # Output newly added vouchers (stdout) cat "$temp_file" # Show statistics if [ $VERBOSE -eq 1 ]; then local new_total=$(wc -l < "$VOUCHER_FILE" 2>/dev/null || echo 0) echo "=====================================" >&2 if [ $added_count -eq $VOUCHER_QTY ]; then echo "Successfully appended $added_count unique vouchers" >&2 else echo "Warning: Only appended $added_count unique vouchers (requested $VOUCHER_QTY)" >&2 fi echo "Total vouchers in file: $new_total" >&2 echo "Newly added: $added_count" >&2 echo "Duplicates skipped: $duplicate_skipped" >&2 echo "Total attempts: $attempts" >&2 echo "=====================================" >&2 fi rm -f "$temp_file" } # Initialize random seed RANDOM=$(head -c 4 /dev/urandom 2>/dev/null | hexdump -e '1/4 "%u"' | cut -c1-8) if [ -z "$RANDOM" ] || [ "$RANDOM" -eq 0 ]; then RANDOM=$$ fi # Run generate_vouchers root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# cat vouchers.txt EY27-BJIZ,1024,1024,0,0,1440,0 X1HZ-RVIE,1024,1024,0,0,1440,0 CM3C-DTGM,1024,1024,0,0,1440,0 T629-ZU1L,1024,1024,0,0,1440,0 BWR3-JH7A,1024,1024,0,0,1440,0 GRK7-EVO7,1024,1024,0,0,1440,0 SND4-CHYG,1024,1024,0,0,1440,0 UR62-AX70,1024,1024,0,0,1440,0 JF2G-H0GH,1024,1024,0,0,1440,0 SD5V-QZJL,1024,1024,0,0,1440,0 root@mt300v2:/usr/lib/opennds#
root@mt300v2:~# root@mt300v2:~# root@mt300v2:~# chmod 755 /usr/lib/opennds/theme_voucher.sh root@mt300v2:~# root@mt300v2:~# chmod 755 /usr/lib/opennds/voucher_generator.sh root@mt300v2:~# root@mt300v2:~# cd /usr/lib/opennds/ root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# ./voucher_generator.sh EY27-BJIZ,1024,1024,0,0,1440,0 X1HZ-RVIE,1024,1024,0,0,1440,0 CM3C-DTGM,1024,1024,0,0,1440,0 T629-ZU1L,1024,1024,0,0,1440,0 BWR3-JH7A,1024,1024,0,0,1440,0 GRK7-EVO7,1024,1024,0,0,1440,0 SND4-CHYG,1024,1024,0,0,1440,0 UR62-AX70,1024,1024,0,0,1440,0 JF2G-H0GH,1024,1024,0,0,1440,0 SD5V-QZJL,1024,1024,0,0,1440,0 root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# cat ./vouchers.txt EY27-BJIZ,1024,1024,0,0,1440,0 X1HZ-RVIE,1024,1024,0,0,1440,0 CM3C-DTGM,1024,1024,0,0,1440,0 T629-ZU1L,1024,1024,0,0,1440,0 BWR3-JH7A,1024,1024,0,0,1440,0 GRK7-EVO7,1024,1024,0,0,1440,0 SND4-CHYG,1024,1024,0,0,1440,0 UR62-AX70,1024,1024,0,0,1440,0 JF2G-H0GH,1024,1024,0,0,1440,0 SD5V-QZJL,1024,1024,0,0,1440,0 root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# cp /usr/lib/opennds/vouchers.txt /tmp/ndslog/vouchers.txt root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# cp /usr/lib/opennds/splash.jpg /etc/opennds/htdocs/images/splash.jpg root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# service opennds status running root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# service opennds stop root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# service opennds status inactive root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# service opennds start uci: Invalid argument This is openNDS version 10.3.1 root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# root@mt300v2:/usr/lib/opennds# logread -e opennds -f Thu Jul 23 22:21:32 2026 daemon.info opennds[26646]: option faskey is [ d5a54f05fefc844b53fb9b201817c2dd3a27190bfe9b31f4b13022e77ad97010 ] Thu Jul 23 22:21:33 2026 daemon.info opennds[26646]: option log_mountpoint is [ /tmp ] Thu Jul 23 22:21:34 2026 daemon.info opennds[26646]: option webroot is [ /etc/opennds/htdocs ] Thu Jul 23 22:21:35 2026 daemon.info opennds[26646]: option authdir is [ opennds_auth ] Thu Jul 23 22:21:35 2026 daemon.info opennds[26646]: option denydir is [ opennds_deny ] Thu Jul 23 22:21:36 2026 daemon.info opennds[26646]: option preauthdir is [ opennds_preauth ] Thu Jul 23 22:21:37 2026 daemon.info opennds[26646]: option ndsctl_sock is [ ndsctl.sock ] Thu Jul 23 22:21:38 2026 daemon.info opennds[26646]: option authentication_mark is [ 0x00030000 ] Thu Jul 23 22:21:38 2026 daemon.info opennds[26646]: option binauth is [ /usr/lib/opennds/binauth_log.sh ] Thu Jul 23 22:21:39 2026 daemon.info opennds[26646]: option faspath is [ / ] Thu Jul 23 22:21:39 2026 daemon.notice opennds[26646]: fwhook received a signal that the system firewall is restarting Thu Jul 23 22:21:39 2026 daemon.info opennds[26646]: option themespec_path is [ /usr/lib/opennds/theme_voucher.sh ] Thu Jul 23 22:21:40 2026 daemon.info opennds[26646]: option fasremoteip is [ disabled ] Thu Jul 23 22:21:41 2026 daemon.info opennds[26646]: option fasremotefqdn is [ disabled ] Thu Jul 23 22:21:41 2026 daemon.info opennds[26646]: option fas_ssl is [ wget ] Thu Jul 23 22:21:42 2026 daemon.info opennds[26646]: option debuglevel is [ 3 ] Thu Jul 23 22:21:42 2026 daemon.debug opennds[26646]: safe_snprintf: [ 47 ] Thu Jul 23 22:21:42 2026 daemon.debug opennds[26646]: Executing command: /usr/lib/opennds/libopennds.sh "debuglevel" "3" Thu Jul 23 22:21:42 2026 daemon.debug opennds[26646]: Reading command output Thu Jul 23 22:21:43 2026 daemon.debug opennds[26646]: command output: [3] Thu Jul 23 22:21:43 2026 daemon.debug opennds[26646]: debuglevel [3] signaled to externals - [3] acknowledged Thu Jul 23 22:21:43 2026 daemon.debug opennds[26646]: safe_snprintf: [ 70 ] Thu Jul 23 22:21:43 2026 daemon.debug opennds[26646]: Executing command: /usr/lib/opennds/libopennds.sh get_option_from_config 'sessiontimeout' Thu Jul 23 22:21:43 2026 daemon.debug opennds[26646]: Reading command output Thu Jul 23 22:21:44 2026 daemon.debug opennds[26646]: command output: [] Thu Jul 23 22:21:44 2026 daemon.info opennds[26646]: option sessiontimeout is [ 1440 ] Thu Jul 23 22:21:44 2026 daemon.debug opennds[26646]: safe_snprintf: [ 74 ] Thu Jul 23 22:21:44 2026 daemon.debug opennds[26646]: Executing command: /usr/lib/opennds/libopennds.sh get_option_from_config 'preauthidletimeout' Thu Jul 23 22:21:44 2026 daemon.debug opennds[26646]: Reading command output Thu Jul 23 22:21:45 2026 daemon.debug opennds[26646]: command output: [] Thu Jul 23 22:21:45 2026 daemon.info opennds[26646]: option preauthidletimeout is [ 30 ] Thu Jul 23 22:21:45 2026 daemon.debug opennds[26646]: safe_snprintf: [ 71 ] Thu Jul 23 22:21:45 2026 daemon.debug opennds[26646]: Executing command: /usr/lib/opennds/libopennds.sh get_option_from_config 'authidletimeout' Thu Jul 23 22:21:45 2026 daemon.debug opennds[26646]: Reading command output Thu Jul 23 22:21:46 2026 daemon.debug opennds[26646]: command output: [] Thu Jul 23 22:21:46 2026 daemon.info opennds[26646]: option authidletimeout is [ 120 ] Thu Jul 23 22:21:46 2026 daemon.debug opennds[26646]: safe_snprintf: [ 66 ] Thu Jul 23 22:21:46 2026 daemon.debug opennds[26646]: Executing command: /usr/lib/opennds/libopennds.sh get_option_from_config 'maxclients' Thu Jul 23 22:21:46 2026 daemon.debug opennds[26646]: Reading command output Thu Jul 23 22:21:47 2026 daemon.debug opennds[26646]: command output: [] Thu Jul 23 22:21:47 2026 daemon.info opennds[26646]: option maxclients is [ 250 ] Thu Jul 23 22:21:47 2026 daemon.debug opennds[26646]: safe_snprintf: [ 83 ] Thu Jul 23 22:21:47 2026 daemon.debug opennds[26646]: Executing command: /usr/lib/opennds/libopennds.sh get_option_from_config 'enable_serial_number_suffix' Thu Jul 23 22:21:47 2026 daemon.debug opennds[26646]: Reading command output Thu Jul 23 22:21:48 2026 daemon.debug opennds[26646]: command output: [] Thu Jul 23 22:21:48 2026 daemon.info opennds[26646]: option enable_serial_number_suffix is [ 1 ] Thu Jul 23 22:21:48 2026 daemon.debug opennds[26646]: safe_snprintf: [ 79 ] Thu Jul 23 22:21:48 2026 daemon.debug opennds[26646]: Executing command: /usr/lib/opennds/libopennds.sh get_option_from_config 'dhcp_default_url_enable' Thu Jul 23 22:21:48 2026 daemon.debug opennds[26646]: Reading command output Thu Jul 23 22:21:49 2026 daemon.debug opennds[26646]: command output: [] Thu Jul 23 22:21:49 2026 daemon.info opennds[26646]: option dhcp_default_url_enable is [ 1 ] Thu Jul 23 22:21:49 2026 daemon.debug opennds[26646]: safe_snprintf: [ 67 ] Thu Jul 23 22:21:49 2026 daemon.debug opennds[26646]: Executing command: /usr/lib/opennds/libopennds.sh get_option_from_config 'gatewayport' Thu Jul 23 22:21:49 2026 daemon.debug opennds[26646]: Reading command output Thu Jul 23 22:21:50 2026 daemon.debug opennds[26646]: command output: [] Thu Jul 23 22:21:50 2026 daemon.info opennds[26646]: option gatewayport is [ 2050 ] Thu Jul 23 22:21:50 2026 daemon.debug opennds[26646]: safe_snprintf: [ 63 ] Thu Jul 23 22:21:50 2026 daemon.debug opennds[26646]: Executing command: /usr/lib/opennds/libopennds.sh get_option_from_config 'fasport' Thu Jul 23 22:21:50 2026 daemon.debug opennds[26646]: Reading command output Thu Jul 23 22:21:51 2026 daemon.debug opennds[26646]: command output: [] Thu Jul 23 22:21:51 2026 daemon.info opennds[26646]: option fasport is [ 0 ] Thu Jul 23 22:21:51 2026 daemon.debug opennds[26646]: safe_snprintf: [ 76 ] Thu Jul 23 22:21:51 2026 daemon.debug opennds[26646]: Executing command: /usr/lib/opennds/libopennds.sh get_option_from_config 'login_option_enabled' Thu Jul 23 22:21:51 2026 daemon.debug opennds[26646]: Reading command output Thu Jul 23 22:21:52 2026 daemon.debug opennds[26646]: command output: [3] Thu Jul 23 22:21:52 2026 daemon.info opennds[26646]: option login_option_enabled is [ 3 ] Thu Jul 23 22:21:52 2026 daemon.debug opennds[26646]: safe_snprintf: [ 72 ] Thu Jul 23 22:21:52 2026 daemon.debug opennds[26646]: Executing command: /usr/lib/opennds/libopennds.sh get_option_from_config 'use_outdated_mhd' Thu Jul 23 22:21:52 2026 daemon.debug opennds[26646]: Reading command output Thu Jul 23 22:21:52 2026 daemon.debug opennds[26646]: command output: [] Thu Jul 23 22:21:52 2026 daemon.info opennds[26646]: option use_outdated_mhd is [ 0 ] Thu Jul 23 22:21:52 2026 daemon.debug opennds[26646]: safe_snprintf: [ 69 ] Thu Jul 23 22:21:52 2026 daemon.debug opennds[26646]: Executing command: /usr/lib/opennds/libopennds.sh get_option_from_config 'max_page_size' Thu Jul 23 22:21:52 2026 daemon.debug opennds[26646]: Reading command output
Vouchers ThemeSpec
This ThemeSpec provides a simple portal requiring a voucher to login.
Vouchers can be generated according to voucher specifications in any way, either manually or programmatically.
An example voucher file is provided, along with a python script to generate more.
Installation (openWRT)
Copy the themespec (theme_voucher.sh) file and the voucher.txt file.
SSH into a terminal session on the router and use the following commands:
cd /usr/lib/opennds
wget https://raw.githubusercontent.com/openNDS/openNDS/master/community/themespec/theme_voucher/theme_voucher.sh
cd /tmp/ndslog
wget https://raw.githubusercontent.com/openNDS/openNDS/master/community/themespec/theme_voucher/vouchers.txt
Now you need to activate the voucher script.
Use the following commands:
chmod 744 /usr/lib/opennds/theme_voucher.sh
uci set opennds.@opennds[0].login_option_enabled='3'
uci set opennds.@opennds[0].themespec_path='/usr/lib/opennds/theme_voucher.sh'
uci commit opennds
service opennds restart
OpenNDS should now be running the voucher script.
Test it by connecting again with your phone or tablet. You will need a voucher code from the example vouchers.txt file you downloaded.
There is also a voucher roll generator.
You can run this on your computer or even on the router (but you will need python installed wherever you run it).
Download it like this:
wget https://raw.githubusercontent.com/openNDS/openNDS/master/community/themespec/theme_voucher/voucher_generator.py
The generator is a very simple program and could easily be re-written in any programming language.
Voucher Roll
The vouchers are contained in a "voucher roll", the path to which is defined in the themespec file and on OpenWrt defaults to ./tmp/ndslog/vouchers.txt
It should be changed to an external storage medium. (See the last section of the file, entitled "Customise the Logfile location").theme_voucher.sh
Flash Wearout Notice
WARNING
The voucher roll is written to on every login
If its location is on router flash, this WILL result in non-repairable failure of the flash memory and therefore the router itself.
Failure will happen, most likely within several months depending on the number of logins.
A safe location is set by default to be the same location as the openNDS log (logdir) ie on the tmpfs (ramdisk) of the operating system.
Files stored here will not survive a reboot.
In a production system, the mountpoint for logdir should be changed to the mount point of some external storage eg a usb stick, an external drive, a network shared drive etc.
Voucher Specifications
File MUST be:
CSV style table, with comma (",") separators. No headers.
7 Columns: voucher code, speed limit down, speed limit up, quota down, quota up, voucher validity (minutes), 0 (placeholder for when voucher is used)
The Voucher-Code MUST respect the following:
9 characters
alphanumeric or dash ("-") character (eg. 12345abcd, 1234-abcd, abcd-efgh)
Each Voucher entry will be on a separate line in the vouchers.txt file





http://status.client/?advanced=checked
http://status.client/login?
http://status.client/opennds_deny/?
============ End

浙公网安备 33010602011771号