The Complete Developer’s Guide to Captive Portal Detection Across All Major OS Platforms


2 views

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;
    }
}