A production-ready Hotel WiFi Captive Portal system with enterprise-grade features, PMS integrations, bandwidth management, and a modern admin dashboard.
- 🔐 Multiple Authentication Methods - Room + Last Name, Voucher Codes
- 🏨 PMS Integrations - Opera Cloud, Opera FIAS, Cloudbeds, Mews, Custom REST
- 📊 Bandwidth Management - Traffic Control (tc) with HTB shaping
- 🛡️ Modern Firewall - nftables with O(1) set lookups
- 📱 Responsive Admin Dashboard - Real-time analytics, session management
- 🎫 Voucher System - PDF generation with QR codes
- 🌐 Multi-language Support - Thai/English localization
- 🔧 GUI Installer - PyQt6 desktop installer for easy deployment
- 🔗 Self-Service Disconnect - Guests type
http://logout.wifito disconnect from any device - 🧬 Dual DNS Architecture - Separate dnsmasq instances for unauthenticated (port 53) and authenticated (port 5354) clients
- 📋 MAC Address Bypass - Allow specific devices to bypass authentication
- 📡 RFC 8910/8908 Captive Portal API - Modern captive portal detection standard
- System Architecture
- Authentication Flow
- Session Lifecycle
- Network Topology
- Quick Start
- Installation
- Configuration
- Technology Stack
- Documentation
- Contributing
- License
graph TB
subgraph "Guest Devices"
G1[📱 Phone]
G2[💻 Laptop]
G3[📺 Smart TV]
end
subgraph "Gateway Server"
subgraph "Network Layer"
NF[nftables<br/>Firewall]
TC[tc HTB<br/>Bandwidth]
DNS[dnsmasq :53<br/>DHCP/DNS]
DNSA[dnsmasq-auth :5354<br/>Auth DNS]
end
subgraph "Application Layer"
FP[FastAPI Portal<br/>:8080]
AD[Admin Dashboard<br/>:8080/admin]
SM[Session Manager]
SCH[APScheduler]
end
subgraph "Data Layer"
PG[(PostgreSQL)]
RD[(Redis)]
end
subgraph "PMS Adapters"
OP[Opera Cloud]
OF[Opera FIAS]
CB[Cloudbeds]
MW[Mews]
SA[Standalone DB]
end
end
subgraph "External"
WAN[🌐 Internet]
end
G1 & G2 & G3 --> DNS
DNS --> FP
DNSA --> FP
FP --> SM
SM --> NF & TC
SM --> PG & RD
SM --> OP & OF & CB & MW & SA
SM --> DNS & DNSA
SCH --> SM
NF --> WAN
AD --> SM
flowchart TD
A[📱 Guest connects to WiFi] --> B[DNS Query Redirected]
B --> C[🌐 Portal Page Loaded]
C --> D{Choose Auth Method}
D -->|Room Auth| E[Enter Room + Last Name]
D -->|Voucher| F[Enter Voucher Code]
E --> G[PMS Lookup]
G --> H{Valid Guest?}
H -->|No| I[❌ Access Denied]
H -->|Yes| J[✅ Create Session]
F --> K[Voucher Validation]
K --> L{Valid Voucher?}
L -->|No| I
L -->|Yes| J
J --> M[Add to nftables Whitelist]
M --> N[Apply Bandwidth Limits]
N --> O[🌐 Internet Access Granted]
O --> P[Session Active]
P --> Q{Expiration Trigger}
Q -->|Timeout| R[Remove from Whitelist]
Q -->|Checkout Sync| R
Q -->|Manual Kick| R
Q -->|Data Limit| R
Q -->|Self-Disconnect| R
R --> S[❌ Session Expired]
P --> SD[Visit http://logout.wifi]
SD --> Q
stateDiagram-v2
[*] --> Connecting: Guest connects to WiFi
Connecting --> Portal: DNS redirect
Portal --> Authenticating: Submit credentials
Authenticating --> Validating: PMS/Voucher check
Validating --> Active: ✅ Auth success
Validating --> Portal: ❌ Auth failed
Active --> Active: Data tracking
Active --> Active: Bandwidth applied
Active --> Expiring: Time limit reached
Active --> Expiring: Checkout detected
Active --> Expiring: Manual kick
Active --> Expiring: Data limit reached
Active --> Expiring: Self-disconnect (logout.wifi)
Expiring --> Expired: Remove from whitelist
Expiring --> Expired: Remove bandwidth limits
Expired --> [*]: Session closed
note right of Active
- nftables whitelist active
- tc bandwidth shaping
- Byte counter tracking
- Auto-renewal possible
end note
flowchart LR
subgraph Internet
WWW[🌐 Internet]
end
subgraph Gateway["Ubuntu Gateway Server"]
WAN[eth0<br/>WAN Interface]
WIFI[wlan0<br/>WiFi Interface]
subgraph Portal["Captive Portal :8080"]
HTTP[FastAPI Server]
end
subgraph NetStack["Network Stack"]
NFT[nftables<br/>Filtering]
HTC[tc HTB<br/>QoS]
DNSM[dnsmasq :53<br/>DHCP + DNS]
DNSA[dnsmasq-auth :5354<br/>Auth DNS]
end
end
subgraph Guests["Guest Network"]
D1[📱 Device 1]
D2[💻 Device 2]
D3[📱 Device 3]
end
WWW <--> WAN
WAN --> NFT
NFT <--> HTC
HTC <--> WIFI
WIFI <--> D1 & D2 & D3
D1 & D2 & D3 --> DNSM
DNSM --> Portal
DNSA --> Portal
NFT -->|dns_bypass set| DNSA
Portal --> NFT
- Ubuntu 22.04 LTS or 24.04 LTS
- Python 3.12+
- PostgreSQL 14+
- Redis 6+
- Two network interfaces (WiFi + WAN)
# Clone the repository
git clone https://github.com/anirut/WiFi-Captive-Portal.git
cd wifi-captive-portal
# Run the GUI installer
python installer/main.pyThe PyQt6 installer provides:
- ✅ System validation
- ✅ Dependency installation
- ✅ Database setup
- ✅ Service configuration
- ✅ Automatic rollback on failure
# Clone and enter directory
git clone https://github.com/anirut/WiFi-Captive-Portal.git
cd wifi-captive-portal
# Run installation script
sudo bash scripts/install.sh
# Configure environment
cp .env.example .env
nano .env
# Start the service
sudo systemctl start captive-portalwifi-captive-portal/
├── app/
│ ├── core/ # Config, database, auth, models
│ ├── network/ # nftables, tc, session management
│ ├── pms/ # PMS adapters (Opera, Cloudbeds, Mews)
│ ├── voucher/ # Voucher generation & validation
│ ├── portal/ # Guest-facing routes & templates
│ ├── admin/ # Admin dashboard routes & templates
│ └── main.py # FastAPI entry point
├── docs/ # Documentation
├── scripts/ # Installation & setup scripts
├── installer/ # PyQt6 GUI installer
├── static/ # CSS, JS, images
├── tests/ # Test suite
├── alembic/ # Database migrations
├── .env.example # Environment template
└── requirements.txt # Python dependencies
Create a .env file from the template:
cp .env.example .env| Variable | Description | Default |
|---|---|---|
SECRET_KEY |
JWT signing key (≥32 chars) | - |
ENCRYPTION_KEY |
Fernet key for credentials | - |
DATABASE_URL |
PostgreSQL connection string | - |
REDIS_URL |
Redis connection string | redis://localhost:6379/0 |
WIFI_INTERFACE |
WiFi interface name | wlan0 |
WAN_INTERFACE |
WAN interface name | eth0 |
PORTAL_IP |
Portal IP address | 10.0.0.1 |
PORTAL_PORT |
Portal port | 8080 |
DNS_UPSTREAM_IP |
Upstream DNS for authenticated clients | 8.8.8.8 |
flowchart TB
subgraph DNSModes["DNS Mode Selection"]
mode{DNS_MODE}
mode -->|redirect| RED[Redirect Mode<br/>All DNS → Portal IP]
mode -->|forward| FWD[Forward Mode<br/>DNS → Upstream Server]
end
subgraph RedirectFlow["Redirect Mode Flow"]
R1[Guest DNS Query] --> R2[dnsmasq :53 intercepts]
R2 --> R3[Returns Portal IP]
R3 --> R4[Guest loads portal]
end
subgraph ForwardFlow["Forward Mode Flow"]
F1[Guest DNS Query] --> F2[dnsmasq :53 forwards]
F2 --> F3[Upstream DNS resolves]
F3 --> F4[Returns real IP]
end
subgraph AuthFlow["Authenticated Client DNS"]
A1[Auth Client DNS] --> A2[nftables redirects to :5354]
A2 --> A3[dnsmasq-auth resolves]
A3 -->|logout.wifi / logout| A4[Returns Portal IP]
A3 -->|other domains| A5[Upstream DNS → real IP]
end
subgraph LogoutURL["Self-Service Logout URL"]
L1[Guest types http://logout.wifi]
L1 --> L2[DNS resolves to Portal IP]
L2 --> L3{Active session?}
L3 -->|Yes| L4[Shows disconnect.html]
L3 -->|No| L5[Shows login.html]
L4 --> L6[Click Disconnect]
L6 --> L5
end
RED --> RedirectFlow
FWD --> ForwardFlow
FWD & RED --> AuthFlow
Note:
logout.wifiworks on all platforms including macOS/iOS/Chrome (which require a dot in the hostname to treat it as a DNS name). DHCP search domainwifiis also set so clients can use barehttp://logoutafter DHCP renewal. DoH (DNS-over-HTTPS) is blocked for authenticated clients to ensurelogout.wifialways resolves correctly.
flowchart TB
subgraph Portal["Captive Portal"]
AUTH[Authentication Request]
SM[Session Manager]
end
subgraph Factory["PMS Factory"]
LF[load_adapter]
CACHE[Cached Adapter]
LF --> CACHE
end
subgraph Adapters["PMS Adapters"]
subgraph Opera["Opera PMS"]
OC[Opera Cloud<br/>REST + OAuth2]
OF[Opera FIAS<br/>TCP/XML]
end
subgraph Cloud["Cloud PMS"]
CB[Cloudbeds<br/>REST API]
MW[Mews<br/>REST + Webhook]
end
subgraph Custom["Custom"]
CR[Custom REST<br/>Configurable Mapping]
SA[Standalone<br/>Local Database]
end
end
subgraph Data["Guest Data"]
GI[GuestInfo Dataclass]
GI --> |room_number| R1[Room Number]
GI --> |last_name| R2[Last Name]
GI --> |check_in| R3[Check-in Date]
GI --> |check_out| R4[Check-out Date]
GI --> |room_type| R5[Room Type]
end
AUTH --> SM
SM --> LF
CACHE --> OC & OF & CB & MW & CR & SA
OC & OF & CB & MW & CR & SA --> GI
| PMS | Protocol | Webhook | Polling |
|---|---|---|---|
| Opera Cloud | REST/OAuth2 | ✅ | - |
| Opera FIAS | TCP/XML | - | ✅ |
| Cloudbeds | REST API | - | ✅ |
| Mews | REST API | ✅ | - |
| Custom REST | Configurable | ✅ | ✅ |
| Standalone | Local DB | - | - |
The admin dashboard provides comprehensive management:
mindmap
root((Admin Dashboard))
Dashboard
Real-time Statistics
Active Sessions
Recent Activity
Sessions
View All Sessions
Kick Sessions
Filter by Status
Vouchers
Create Single
Batch Generate
PDF Export
QR Codes
Policies
Bandwidth Limits
Device Limits
Room Type Mapping
Analytics
Session Charts
Bandwidth Usage
Peak Hours
Auth Breakdown
Settings
PMS Configuration
Branding
Terms & Conditions
Languages
Network
DHCP Config
DNS Config
Interface Status
MAC Bypass
Device Allowlist
Auto-expiry
Captive Portal API
RFC 8910/8908
Modern Detection
# Create virtual environment
python -m venv .venv
source .venv/bin/activate
# Install dependencies
pip install -r requirements.txt
pip install -r requirements-dev.txt
# Run database migrations
alembic upgrade head
# Start development server
uvicorn app.main:app --reload --host 0.0.0.0 --port 8080# Run all tests
pytest tests/ -v
# Run with coverage
pytest tests/ --cov=app --cov-report=term-missing
# Run single test
pytest tests/test_portal/test_portal_routes.py::test_room_auth_success -vflowchart LR
subgraph Security["Security Layers"]
RL[Rate Limiting<br/>5 attempts/10 min]
JWT[JWT Auth<br/>with jti revocation]
ENC[Fernet Encryption<br/>for credentials]
HMAC[HMAC-SHA256<br/>Webhook signatures]
HASH[bcrypt<br/>Password hashing]
end
subgraph Network["Network Security"]
NF[nftables<br/>Stateful filtering]
WLIST[Whitelist<br/>IP-based access]
ISOL[Client Isolation<br/>No inter-device traffic]
end
Security --> Network
| Document | Description |
|---|---|
| Features | Complete feature documentation |
| Installation Guide | Step-by-step installation |
| User Manual | Guest & Admin user guide |
| GUI Installer Guide | Desktop installer usage |
| CLAUDE.md | Development guidance |
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- FastAPI - Modern Python web framework
- nftables - Linux packet filtering
- HTMX - Modern HTML-first frontend
- Tailwind CSS - Utility-first CSS framework
Made with ❤️ for the hospitality industry