Complete setup for creating a WiFi access point with captive portal on Raspberry Pi. The Pi receives internet via Ethernet and shares it through WiFi with a "Terms of Service" agreement page that users must accept before gaining internet access.
This setup creates a walled garden WiFi access point where:
- Users connect to WiFi but have no internet initially
- Captive portal automatically appears on iOS, Android, and Windows
- Users must accept terms to gain internet access
- Portal notification dismisses automatically after acceptance
- Systemd service ensures everything persists after reboot
This is work in progress and will not be updated regularly; see it as me sharing my playground with you rather than it being a complete solution.
- Raspberry Pi (any model: Zero W, 3, 4, 5, or Compute Module)
- WiFi Adapter (built-in or USB)
- For models without built-in WiFi (Pi Zero, Pi 1/2): USB WiFi required
- For models with built-in WiFi: Can use built-in or add USB adapter
- Recommended USB chipsets: RTL8188, RT5370, MT7601U, Atheros AR9271
- Example: TP-Link TL-WN722N, Edimax EW-7811Un, Panda Wireless PAU05
- Internet Connection (one of):
- Built-in Ethernet port (Pi 3B+, 4, 5)
- USB Ethernet adapter (Pi Zero, 1, 2, or any model)
- Another WiFi adapter (if you have 2 WiFi adapters total)
- MicroSD card (8GB+ minimum, 16GB+ recommended)
- Power supply (appropriate for your Pi model)
- Raspberry Pi OS (Bullseye or newer) installed
- Internet connection via Ethernet port
- USB WiFi dongle plugged in
- SSH access enabled (or keyboard/monitor attached)
- β Auto-detects network interfaces (WiFi and ethernet)
- β Walled garden - internet blocked until terms accepted
- β Proper captive portal detection for iOS, Android, Windows
- β Automatic portal notification dismissal after acceptance
- β Systemd service for boot persistence
- β HTTP redirect to portal for initial connection
- β IP forwarding enabled for internet access
- β Interface-specific iptables rules for proper routing
# Update system
sudo apt update
sudo apt upgrade -y
# Verify WiFi adapter(s) detected
iw dev
# You should see at least one wlan interface
# Check network interfaces
ip link show
# Look for eth0, enx*, or similar for ethernet
# Verify internet connection (if ethernet connected)
ping -c 4 google.com# Download the setup files
cd ~
git clone https://github.com/queueeee/rpi-captive-portal raspberry-pi-captive-portal
cd raspberry-pi-captive-portal
# Make setup script executable
chmod +x setup.sh
# Run installation (requires root)
sudo ./setup.shThe script will:
- Update system packages
- Detect network interfaces automatically
- Install required packages (hostapd, dnsmasq, nginx, php-fpm)
- Configure WiFi access point
- Set up walled garden firewall
- Create systemd service for persistence
- Configure captive portal detection
- Start all services
Note: The script will auto-detect your network interfaces during installation and prompt you to confirm the configuration.
sudo rebootAfter reboot:
- Connect to the WiFi network "RaspberryPi-Portal" (password: wifi2025)
- You should see a captive portal notification appear automatically
- Click to open the portal and accept the terms
- The notification should dismiss automatically
- You now have internet access
raspberry-pi-captive-portal/
βββ setup.sh # Main installation script
βββ portal/
β βββ index.html # Terms of Service page
β βββ accept.php # Agreement handler
β βββ success.html # Confirmation page
βββ README.md # This file
Edit setup.sh before running:
AP_SSID="RaspberryPi-Portal" # Network name
AP_PASSPHRASE="wifi2025" # WiFi password (min 8 chars) - CHANGE THIS!
AP_CHANNEL=7 # WiFi channel (1-13)
AP_IP="10.0.0.1" # Gateway IP
DHCP_START="10.0.0.10" # DHCP range start
DHCP_END="10.0.0.50" # DHCP range endImportant: Change the default WiFi password before deploying! Users will need to enter this password when connecting to the WiFi network.
If you've already installed and want to change the password:
# Edit the hostapd configuration
sudo nano /etc/hostapd/hostapd.conf
# Find and change this line:
# wpa_passphrase=wifi2025
# Save and exit (Ctrl+X, Y, Enter)
# Restart hostapd
sudo systemctl restart hostapdUsers will need to reconnect with the new password.
Edit portal/index.html to modify:
- Company name
- Terms and conditions text
- Styling and branding
- Logo/images
# View log of accepted users
sudo tail -f /var/log/captive-portal.log
# List connected clients
sudo list-portal-clients.sh- Default iptables policy:
FORWARD DROP(blocks all internet) - DNS queries allowed (port 53) for domain resolution
- HTTP/HTTPS to portal allowed for terms acceptance
- Everything else blocked until user accepts terms
- User connects to WiFi β receives IP from DHCP (10.0.0.10-50)
- HTTP traffic redirected to portal via iptables NAT
- Device checks detection URLs β sees captive portal
- User accepts terms β
accept.phpis called accept.phpcallsgrant-internet-access.shvia sudo- Grant script adds interface-specific iptables rules:
FORWARD -i wlan0 -o eth0 -s CLIENT_IP -j ACCEPT(outbound)FORWARD -i eth0 -o wlan0 -d CLIENT_IP -j ACCEPT(inbound)- NAT PREROUTING exemption for HTTP redirect
- Device rechecks detection URLs β gets success code (204/200)
- Portal notification automatically dismisses
The system properly handles detection URLs for all major platforms:
- iOS/macOS:
/hotspot-detect.html,/library/test/success.htmlβ 200 OK - Android:
/generate_204,/gen_204β 204 No Content - Windows:
/ncsi.txt,/connecttest.txtβ 200 OK with specific text
Detection URLs route to check-portal.php which:
- Checks if client IP has iptables FORWARD rule via
check-internet-access.sh - If accepted: returns appropriate success code to dismiss notification
- If not accepted: redirects to portal page
The captive-portal.service systemd unit ensures everything persists after reboot:
- Enables IP forwarding (
net.ipv4.ip_forward=1) - Unblocks WiFi (RF-kill management)
- Configures WiFi interface with static IP
- Restores HTTP redirect rules
- Runs before hostapd/dnsmasq services
After installation, these diagnostic commands are available:
Comprehensive diagnostic tool
sudo test-captive-portal.shShows:
- Service status (hostapd, dnsmasq, nginx, php-fpm)
- WiFi interface status and IP configuration
- Firewall rules and policies
- Accepted clients count
- Recent portal logs
View connected clients and their status
sudo list-portal-clients.shShows:
- DHCP leases (MAC, IP, hostname)
- Accepted clients with internet access
- Active iptables FORWARD rules
Manually grant internet access to an IP
sudo grant-internet-access.sh 10.0.0.25Enable detailed logging for troubleshooting
sudo enable-portal-debug.shEnables:
- Nginx debug logging
- PHP error logging
- Shows log file locations
# Check if WiFi hardware is recognized
lsusb # For USB adapters
dmesg | grep -i wireless
# Check wireless interfaces
iw dev
ip link show | grep wlan
# If driver is missing, install firmware packages
sudo apt install firmware-atheros firmware-realtek firmware-ralink firmware-brcm80211
# For specific chipsets
sudo apt install raspberrypi-kernel-headers# Check hostapd status
sudo systemctl status hostapd
# View logs
sudo journalctl -u hostapd -n 50
# Check captive portal service
sudo systemctl status captive-portal.service
# Check if RF-kill is blocking WiFi
sudo rfkill list
# Manually unblock
sudo rfkill unblock wifi
# Test hostapd configuration
sudo hostapd -dd /etc/hostapd/hostapd.conf# Check all services
sudo systemctl status hostapd dnsmasq nginx
# Check if AP is broadcasting
iw dev wlan0 info
# Check HTTP redirect rule
sudo iptables -t nat -L PREROUTING -n -v
# Should see: DNAT tcp dpt:80 to:10.0.0.1:80
# Test portal manually
curl -i http://10.0.0.1/# Check IP forwarding (CRITICAL!)
cat /proc/sys/net/ipv4/ip_forward
# MUST return: 1
# If it returns 0, enable it:
sudo sysctl -w net.ipv4.ip_forward=1
sudo sed -i 's/#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/' /etc/sysctl.conf
# Check FORWARD rules for your client IP
sudo iptables -L FORWARD -n -v | grep YOUR_IP
# Check NAT masquerading
sudo iptables -t nat -L POSTROUTING -n -v
# View acceptance logs
sudo journalctl -t captive-portal
sudo tail -f /var/log/captive-portal.log
# Check which interface has internet
ip link show
ping -c 2 8.8.8.8
# Restart services
sudo systemctl restart captive-portal.service
sudo systemctl restart hostapd dnsmasq# Check detection URL response from client device
# Connect from phone/laptop and try:
curl -i http://10.0.0.1/generate_204
# Should return:
# - 204 No Content (if accepted)
# - 302 Found β redirect (if not accepted)
# Check if your IP has iptables rules
sudo iptables -L FORWARD -n | grep YOUR_CLIENT_IP
# Manually test acceptance
sudo grant-internet-access.sh YOUR_CLIENT_IP
# Check PHP execution
sudo tail -f /var/log/nginx/error.log# Check captive portal service (runs on boot)
sudo systemctl status captive-portal.service
# Check if interface has correct IP
ip addr show wlan0
# Should show: 10.0.0.1/24
# Check RF-kill status
sudo rfkill list
# Manually restart everything
sudo systemctl restart captive-portal.service
sleep 3
sudo systemctl restart hostapd
sudo systemctl restart dnsmasq# Check nginx status
sudo systemctl status nginx
# View nginx logs
sudo tail -f /var/log/nginx/error.log
# Detect PHP version
php -v
# Check PHP-FPM
PHP_VER=$(php -r "echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;")
sudo systemctl status php${PHP_VER}-fpm
# Check portal files exist
ls -la /var/www/portal/
# Check permissions
sudo chown -R www-data:www-data /var/www/portal# Check dnsmasq
sudo systemctl status dnsmasq
# View DNS queries
sudo tail -f /var/log/syslog | grep dnsmasq
# Test DNS resolution from client
nslookup google.com 10.0.0.1
# Restart dnsmasq
sudo systemctl restart dnsmasqIf the portal causes boot issues, use the recovery script:
sudo /home/pi/recover-network.shThis will:
- Stop and disable captive portal services
- Restore original network configuration (from backup)
- Reset WiFi interface to normal operation
/etc/hostapd/hostapd.conf- Access point configuration/etc/dnsmasq.conf- DHCP and DNS configuration/etc/nginx/sites-available/captive-portal- Web server config/etc/systemd/system/captive-portal.service- Boot service/etc/sudoers.d/captive-portal- Sudo permissions for www-data
/usr/local/bin/captive-portal-setup.sh- Boot initialization/usr/local/bin/captive-portal-cleanup.sh- Shutdown cleanup/usr/local/bin/grant-internet-access.sh- Grant client access/usr/local/bin/check-internet-access.sh- Check client status/usr/local/bin/test-captive-portal.sh- Diagnostics/usr/local/bin/list-portal-clients.sh- List clients/usr/local/bin/enable-portal-debug.sh- Enable debugging
/var/www/portal/index.html- Terms of service page/var/www/portal/accept.php- Acceptance handler/var/www/portal/check-portal.php- Detection URL handler/var/www/portal/portal-dismissed.html- Success page/var/www/portal/success.html- Alternative success page
/var/log/captive-portal.log- Acceptance events/var/log/nginx/access.log- Web requests/var/log/nginx/error.log- Web errors
The script automatically detects and supports:
| Pi Model | Internet Source | Access Point | Notes |
|---|---|---|---|
| Pi 4/5 | Built-in Ethernet | USB WiFi | Recommended setup |
| Pi 4/5 | Built-in Ethernet | Built-in WiFi | Works, but USB WiFi preferred |
| Pi 3B+ | Built-in Ethernet | Built-in WiFi | Built-in WiFi may be limited |
| Pi Zero W | USB Ethernet | Built-in WiFi | Requires USB hub + ethernet adapter |
| Pi Zero 2W | USB Ethernet | Built-in WiFi | Requires USB OTG adapter |
| Any Pi | 2x USB WiFi | One WiFi adapter | Script will let you choose which |
| Any Pi | None (standalone) | Any WiFi | Portal only, no internet |
If you want to receive internet via WiFi and share via another WiFi:
- Edit
/etc/wpa_supplicant/wpa_supplicant.conf:
network={
ssid="YourHomeWiFi"
psk="YourPassword"
}- The script will detect both WiFi interfaces and let you choose which one for AP
Edit portal/accept.php to expire sessions after X hours:
// Check if session expired (4 hours = 14400 seconds)
if (isset($_SESSION['accepted_time'])) {
if (time() - $_SESSION['accepted_time'] > 14400) {
session_destroy();
header('Location: index.html');
exit;
}
}# Install wondershaper
sudo apt install wondershaper
# Limit USB WiFi to 5Mbps down, 1Mbps up
sudo wondershaper wlan1 5000 1000# Add to dnsmasq.conf
echo "address=/facebook.com/0.0.0.0" | sudo tee -a /etc/dnsmasq.conf
sudo systemctl restart dnsmasq# List DHCP leases
cat /var/lib/misc/dnsmasq.leases
# List ARP table
arp -a
# View hostapd clients
sudo iw dev wlan1 station dump# Install iftop
sudo apt install iftop
# Monitor WiFi interface
sudo iftop -i wlan1- This is a basic captive portal - not enterprise-grade security
- WiFi network is WPA2-protected - users must enter password to connect
- Default WiFi password is
wifi2025- CHANGE THIS before deployment! - Traffic between clients and Pi uses WPA2 encryption
- Consider using WPA2-Enterprise for better security in production
- The portal logs IP addresses and MAC addresses
- The web server (www-data) has sudo permissions for specific scripts only
- Client IPs are validated before adding firewall rules
- iptables rules are interface-specific (not wildcards)
- HTTP redirect only applies to unauthenticated clients
- Keep system updated:
sudo apt update && sudo apt upgrade
# Stop and disable services
sudo systemctl stop captive-portal.service hostapd dnsmasq nginx
sudo systemctl disable captive-portal.service hostapd dnsmasq
# Remove packages
sudo apt remove --purge hostapd dnsmasq nginx php-fpm
# Remove configuration files
sudo rm -rf /etc/hostapd/hostapd.conf
sudo rm -rf /etc/dnsmasq.conf
sudo rm -rf /etc/nginx/sites-available/captive-portal
sudo rm -rf /etc/systemd/system/captive-portal.service
sudo rm -rf /etc/sudoers.d/captive-portal
# Remove portal files
sudo rm -rf /var/www/portal
# Remove scripts
sudo rm -f /usr/local/bin/captive-portal-setup.sh
sudo rm -f /usr/local/bin/captive-portal-cleanup.sh
sudo rm -f /usr/local/bin/grant-internet-access.sh
sudo rm -f /usr/local/bin/check-internet-access.sh
sudo rm -f /usr/local/bin/test-captive-portal.sh
sudo rm -f /usr/local/bin/list-portal-clients.sh
sudo rm -f /usr/local/bin/enable-portal-debug.sh
# Restore iptables
sudo iptables -F
sudo iptables -t nat -F
sudo iptables -P FORWARD ACCEPT
sudo netfilter-persistent save
# Restore network configuration
# If you have backups:
sudo cp /etc/dhcpcd.conf.backup /etc/dhcpcd.conf 2>/dev/null || true
sudo systemctl restart dhcpcdMIT License - Feel free to modify and distribute
Issues and pull requests welcome!
For issues:
- Check troubleshooting section above
- Run diagnostics:
sudo test-captive-portal.sh - Review system logs:
sudo journalctl -xe - Check specific service:
sudo journalctl -u captive-portal -n 50 - Enable debug mode:
sudo enable-portal-debug.sh
Created for Raspberry Pi 4 running Raspberry Pi OS Bookworm. Works with NetworkManager, dhcpcd, and systemd-networkd.
Note: This setup assumes Raspberry Pi OS (Bullseye or newer). Adjustments may be needed for other distributions.