When implementing a WiFi captive portal, triggering native OS popups requires intercepting specific network requests that each operating system uses to check internet connectivity. Here's the technical breakdown of what each platform expects:
Android 4-6+ (Modern Versions)
# Apache
RedirectMatch 302 /generate_204 http://captiveportal.lan
# nginx
location = /generate_204 {
return 302 http://captiveportal.lan;
}
Legacy Android (Pre-4.0)
# Apache
Redirect 302 /check_network_status.txt http://captiveportal.lan
# nginx
location = /check_network_status.txt {
return 302 http://captiveportal.lan;
}
iOS 8+
# Apache
RewriteEngine on
RewriteCond %{HTTP_USER_AGENT} ^CaptiveNetworkSupport(.*)$ [NC]
RewriteRule ^(.*)$ http://captiveportal.lan [L,R=302]
# nginx
if ($http_user_agent ~* "^CaptiveNetworkSupport") {
return 302 http://captiveportal.lan;
}
Legacy iOS (Pre-8)
# Apache
Redirect 302 /hotspot-detect.html http://captiveportal.lan
# nginx
location = /hotspot-detect.html {
return 302 http://captiveportal.lan;
}
Windows 7/8/10 & Windows Phone
# Apache
RedirectMatch 302 /ncsi.txt http://captiveportal.lan
# nginx
location = /ncsi.txt {
return 302 http://captiveportal.lan;
}
# Apache
Redirect 302 /library/test/success.html http://captiveportal.lan
# nginx
location = /library/test/success.html {
return 302 http://captiveportal.lan;
}
Kindle devices don't have a traditional captive portal popup but require:
# Apache
Redirect 302 /kindle-wifi/wifistub.html http://captiveportal.lan
# nginx
location = /kindle-wifi/wifistub.html {
return 302 http://captiveportal.lan;
}
For nginx, combine all rules into a single config:
server {
listen 80;
location = /generate_204 { return 302 http://captiveportal.lan; }
location = /ncsi.txt { return 302 http://captiveportal.lan; }
location = /hotspot-detect.html { return 302 http://captiveportal.lan; }
location = /library/test/success.html { return 302 http://captiveportal.lan; }
location = /kindle-wifi/wifistub.html { return 302 http://captiveportal.lan; }
if ($http_user_agent ~* "^CaptiveNetworkSupport") {
return 302 http://captiveportal.lan;
}
}
Remember to whitelist these endpoints in your firewall rules to allow the initial detection requests through before authentication.
Modern operating systems use specific endpoints to check internet connectivity. When a device connects to a WiFi network, it attempts to access these endpoints. If the response isn't what the OS expects, it triggers the captive portal popup. Here's how different platforms handle this:
Android 4/5/6+ checks http://clients3.google.com/generate_204
# Apache
RedirectMatch 302 /generate_204 http://captiveportal.lan
# nginx
server {
location = /generate_204 {
return 302 http://captiveportal.lan;
}
}
Older Android versions may use different endpoints:
# Apache
RedirectMatch 302 /check_network_status.txt http://captiveportal.lan
# nginx
location = /check_network_status.txt {
return 302 http://captiveportal.lan;
}
iOS 8+ uses a specific user agent:
# Apache .htaccess
RewriteEngine on
RewriteCond %{HTTP_USER_AGENT} ^CaptiveNetworkSupport(.*)$ [NC]
RewriteRule ^(.*)$ http://captiveportal.lan [L,R=302]
# nginx
if ($http_user_agent ~* "^CaptiveNetworkSupport") {
return 302 http://captiveportal.lan;
}
Earlier iOS versions check these endpoints:
# Apache
RedirectMatch 302 /library/test/success.html http://captiveportal.lan
# nginx
location = /library/test/success.html {
return 302 http://captiveportal.lan;
}
Windows 7/8/10 and Windows Phone check http://www.msftncsi.com/ncsi.txt
# Apache
RedirectMatch 302 /ncsi.txt http://captiveportal.lan
# nginx
location = /ncsi.txt {
return 302 http://captiveportal.lan;
}
Apple computers use these endpoints:
# Apache
RedirectMatch 302 /hotspot-detect.html http://captiveportal.lan
RedirectMatch 302 /success.html http://captiveportal.lan
# nginx
location = /hotspot-detect.html {
return 302 http://captiveportal.lan;
}
location = /success.html {
return 302 http://captiveportal.lan;
}
Kindle devices typically check:
# Apache
RedirectMatch 302 /kindle-wifi/wifistub.html http://captiveportal.lan
# nginx
location = /kindle-wifi/wifistub.html {
return 302 http://captiveportal.lan;
}
For nginx, here's a complete server block that handles all major platforms:
server {
listen 80;
server_name _;
# Android
location = /generate_204 {
return 302 http://captiveportal.lan;
}
# iOS
if ($http_user_agent ~* "^CaptiveNetworkSupport") {
return 302 http://captiveportal.lan;
}
# Windows
location = /ncsi.txt {
return 302 http://captiveportal.lan;
}
# macOS
location = /hotspot-detect.html {
return 302 http://captiveportal.lan;
}
# Kindle
location = /kindle-wifi/wifistub.html {
return 302 http://captiveportal.lan;
}
# Fallback for other requests
location / {
return 302 http://captiveportal.lan;
}
}