Configuring DNSMASQ for Captive Portal: Redirecting Device Detection URLs to Local Server While Blocking Internet Access


4 views

When implementing a captive portal, we need to carefully manage two key network interfaces:

eth1 - Connected to WAN (192.168.0.107)
wlan0 - Hosting hotspot (172.24.1.1/24)

The main issue arises when using address=/#/172.24.1.1 which captures ALL DNS requests, including those from our local services. Here's a better approach:

interface=wlan0
listen-address=172.24.1.1
server=8.8.8.8
domain-needed
bogus-priv
dhcp-range=172.24.1.50,172.24.1.150,12h

# Targeted redirects instead of wildcard
address=/clients1.google.com/172.24.1.1
address=/connectivitycheck.gstatic.com/172.24.1.1
address=/captive.apple.com/172.24.1.1
address=/msftconnecttest.com/172.24.1.1
address=/detectportal.firefox.com/172.24.1.1

Complement your DNS configuration with proper firewall rules:

# NAT for clients
iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE

# Block internet access but allow DNS
iptables -A FORWARD -i wlan0 -o eth1 -p tcp --dport 53 -j ACCEPT
iptables -A FORWARD -i wlan0 -o eth1 -p udp --dport 53 -j ACCEPT
iptables -A FORWARD -i wlan0 -o eth1 -j DROP

# Allow established connections
iptables -A FORWARD -i eth1 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT

# Allow access to local portal server
iptables -t nat -A PREROUTING -i wlan0 -p tcp --dport 80 -j DNAT --to-destination 172.24.1.1:80
iptables -t nat -A PREROUTING -i wlan0 -p tcp --dport 443 -j DNAT --to-destination 172.24.1.1:443

Enhance your Express server to handle various device detection patterns:

const express = require('express');
const app = express();

// Android detection
app.get('/generate_204', (req, res) => {
    res.status(302).set('Location', '/portal').end();
});

// Windows detection
app.get('/ncsi.txt', (req, res) => {
    res.status(200).send('Microsoft NCSI');
});

// Apple detection
app.get('/hotspot-detect.html', (req, res) => {
    res.redirect('/portal');
});

// Firefox detection
app.get('/success.txt', (req, res) => {
    res.status(200).send('success');
});

// Main portal
app.get('/portal', (req, res) => {
    res.send(
        <h1>Welcome to Our Network</h1>
        <form action="/authenticate" method="post">
            <!-- Your auth form here -->
        </form>
    );
});

app.listen(80);

Use these curl commands to verify your setup:

# Test Android detection
curl -v http://clients1.google.com/generate_204

# Test Apple detection
curl -v http://captive.apple.com/hotspot-detect.html

# Test Windows detection
curl -v http://www.msftconnecttest.com/connecttest.txt

For high-traffic environments, consider these optimizations:

# Increase DNSMASQ cache
cache-size=1000
local-ttl=300

# Add negative caching
neg-ttl=60

When setting up a captive portal with DNSMASQ and hostapd, we encounter a common challenge: how to intercept all client DNS requests while maintaining connectivity for internal services. The existing configuration shows:

interface=wlan0
listen-address=172.24.1.1
server=8.8.8.8
address=/#/172.24.1.1
except-interface=eth1

Using address=/#/172.24.1.1 creates a catch-all DNS wildcard that affects all queries, including those from local scripts. We need more granular control to distinguish between captive portal detection requests and regular internal traffic.

Instead of the wildcard approach, we should specifically target known captive portal detection URLs:

# Captive portal detection URLs for various OSes
address=/generate_204/172.24.1.1
address=/connectivity-check/172.24.1.1
address=/hotspot-detect.html/172.24.1.1
address=/ncsi.txt/172.24.1.1
address=/success.txt/172.24.1.1
address=/kindle-wifi/wifistub.html/172.24.1.1

For proper isolation between wlan0 and eth1, we need complementary iptables rules:

# NAT and forwarding rules
iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
iptables -A FORWARD -i eth1 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i wlan0 -o eth1 -j DROP

# Allow local traffic
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

The Express server should handle all common detection endpoints:

app.get('/generate_204', (req, res) => {
    res.status(204).end();
});

app.get('/ncsi.txt', (req, res) => {
    res.status(200).send('Microsoft NCSI');
});

app.get('/hotspot-detect.html', (req, res) => {
    res.redirect('/portal');
});

app.get('/connectivity-check', (req, res) => {
    res.json({status: "redirect"});
});

For better control, we can implement interface-specific handling:

# Restrict to wlan0 only
interface=wlan0
bind-interfaces
listen-address=172.24.1.1

# Standard DNS forwarding
server=8.8.8.8
server=8.8.4.4

# Specific captive portal redirects
address=/clients1.google.com/172.24.1.1
address=/captive.apple.com/172.24.1.1
address=/msftncsi.com/172.24.1.1
address=/connectivity-check.ubuntu.com/172.24.1.1

# DHCP configuration
dhcp-range=172.24.1.50,172.24.1.150,12h
dhcp-option=option:router,172.24.1.1

Verify your setup with these commands:

# Check DNS responses
dig @172.24.1.1 clients1.google.com
dig @172.24.1.1 example.com

# Test captive portal detection
curl -v http://clients1.google.com/generate_204
curl -v http://captive.apple.com/hotspot-detect.html