فهرست منبع

Fix captive portal: use NM built-in dnsmasq + add portal detection handlers

Two fixes for hotspot mode:

1. DNS redirect: NetworkManager's shared mode runs its own dnsmasq for
   DHCP+DNS, which blocked our separate dnsmasq from binding port 53.
   Now we install our config into /etc/NetworkManager/dnsmasq-shared.d/
   so NM's own dnsmasq handles the DNS redirect. Removed standalone
   dnsmasq dependency entirely.

2. Captive portal blank page: Added backend endpoints for well-known
   captive portal detection paths (/hotspot-detect.html, /generate_204,
   etc.) that serve a minimal HTML redirect to /wifi-setup. Added
   nginx proxy rules and vite proxy for these paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tuanchris 4 ماه پیش
والد
کامیت
e96be3fe14
7فایلهای تغییر یافته به همراه107 افزوده شده و 87 حذف شده
  1. 4 0
      frontend/vite.config.ts
  2. 2 1
      main.py
  3. 57 1
      modules/wifi/router.py
  4. 9 0
      nginx.conf
  5. 8 44
      wifi/autohotspot
  6. 6 13
      wifi/dnsmasq-hotspot.conf
  7. 21 28
      wifi/setup-wifi.sh

+ 4 - 0
frontend/vite.config.ts

@@ -113,6 +113,10 @@ export default defineConfig({
       },
       // All /api endpoints
       '/api': 'http://localhost:8080',
+      // Captive portal detection probes (hotspot mode)
+      '/hotspot-detect.html': 'http://localhost:8080',
+      '/generate_204': 'http://localhost:8080',
+      '/connecttest.txt': 'http://localhost:8080',
       // Static assets
       '/static': 'http://localhost:8080',
       // Preview images

+ 2 - 1
main.py

@@ -23,7 +23,7 @@ from modules.led.idle_timeout_manager import idle_timeout_manager
 from modules.core.cache_manager import get_cache_path, generate_image_preview, get_pattern_metadata
 from modules.core.version_manager import version_manager
 from modules.core.log_handler import init_memory_handler, get_memory_handler
-from modules.wifi.router import router as wifi_router
+from modules.wifi.router import router as wifi_router, captive_portal_router
 import json
 import base64
 import hashlib
@@ -293,6 +293,7 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
 
 # Include WiFi management router
 app.include_router(wifi_router)
+app.include_router(captive_portal_router)
 
 # Global semaphore to limit concurrent preview processing
 # Prevents resource exhaustion when loading many previews simultaneously

+ 57 - 1
modules/wifi/router.py

@@ -2,7 +2,8 @@
 FastAPI router for WiFi management endpoints.
 """
 
-from fastapi import APIRouter, HTTPException
+from fastapi import APIRouter, HTTPException, Request
+from fastapi.responses import HTMLResponse
 from pydantic import BaseModel
 from typing import Optional
 import logging
@@ -13,6 +14,9 @@ logger = logging.getLogger(__name__)
 
 router = APIRouter(prefix="/api/wifi", tags=["wifi"])
 
+# Separate router for the captive portal handler (no prefix)
+captive_portal_router = APIRouter(tags=["wifi"])
+
 
 class WiFiConnectRequest(BaseModel):
     ssid: str
@@ -63,3 +67,55 @@ async def wifi_forget(req: WiFiForgetRequest):
 async def wifi_saved():
     """Get list of saved WiFi connections."""
     return manager.get_saved_connections()
+
+
+# --- Captive portal detection endpoints ---
+# These handle the well-known URLs that phones/tablets probe to detect captive portals.
+# In hotspot mode, DNS redirects all domains to the Pi, so these probes arrive here.
+# We serve a minimal HTML page that redirects to the WiFi setup page.
+
+CAPTIVE_PORTAL_HTML = """<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>Dune Weaver - WiFi Setup</title>
+    <style>
+        * { margin: 0; padding: 0; box-sizing: border-box; }
+        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+               display: flex; justify-content: center; align-items: center;
+               min-height: 100vh; background: #0a0a0a; color: #fafafa; padding: 1rem; }
+        .card { background: #1a1a1a; border-radius: 12px; padding: 2rem;
+                max-width: 400px; width: 100%; text-align: center; }
+        h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
+        p { color: #888; margin-bottom: 1.5rem; font-size: 0.9rem; }
+        a { display: inline-block; background: #3b82f6; color: white; padding: 0.75rem 2rem;
+            border-radius: 8px; text-decoration: none; font-weight: 500; }
+        a:hover { background: #2563eb; }
+    </style>
+</head>
+<body>
+    <div class="card">
+        <h1>Welcome to Dune Weaver</h1>
+        <p>Connect to your home WiFi to get started.</p>
+        <a href="/wifi-setup">Set Up WiFi</a>
+    </div>
+</body>
+</html>"""
+
+
+@captive_portal_router.get("/hotspot-detect.html", response_class=HTMLResponse)
+@captive_portal_router.get("/generate_204", response_class=HTMLResponse)
+@captive_portal_router.get("/connecttest.txt", response_class=HTMLResponse)
+@captive_portal_router.get("/ncsi.txt", response_class=HTMLResponse)
+@captive_portal_router.get("/redirect", response_class=HTMLResponse)
+@captive_portal_router.get("/canonical.html", response_class=HTMLResponse)
+async def captive_portal_detect():
+    """Handle captive portal detection probes.
+
+    Phones and tablets check these well-known URLs after connecting to WiFi.
+    In hotspot mode, DNS resolves all domains to the Pi, so these probes
+    arrive at our server. Returning anything other than the expected response
+    triggers the OS to show a captive portal browser.
+    """
+    return HTMLResponse(content=CAPTIVE_PORTAL_HTML, status_code=200)

+ 9 - 0
nginx.conf

@@ -12,6 +12,15 @@ server {
         try_files $uri $uri/ /index.html;
     }
 
+    # Captive portal detection probes → backend serves redirect page
+    # In hotspot mode, DNS resolves all domains to the Pi.
+    # Phones probe these well-known paths to detect captive portals.
+    location ~ ^/(hotspot-detect\.html|generate_204|connecttest\.txt|ncsi\.txt|redirect|canonical\.html)$ {
+        proxy_pass http://backend:8080;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+    }
+
     # API proxy to backend container
     location /api/ {
         proxy_pass http://backend:8080;

+ 8 - 44
wifi/autohotspot

@@ -6,15 +6,16 @@
 # If a known WiFi network is found, connect to it.
 # If not, create a "Dune Weaver" hotspot so users can configure WiFi via the app.
 #
-# Requires: NetworkManager (Pi Trixie), dnsmasq (for DNS redirect in hotspot mode)
+# DNS redirect for captive portal is handled by NetworkManager's built-in
+# dnsmasq via config in /etc/NetworkManager/dnsmasq-shared.d/.
+#
+# Requires: NetworkManager (Pi Trixie)
 #
 
 set -euo pipefail
 
 HOTSPOT_CON_NAME="DuneWeaver-Hotspot"
 MODE_FILE="/tmp/dw-wifi-mode"
-DNSMASQ_CONF="/etc/dune-weaver/dnsmasq-hotspot.conf"
-DNSMASQ_PID="/run/dune-weaver-dnsmasq.pid"
 SCAN_TIMEOUT=30
 SCAN_INTERVAL=5
 IFACE="wlan0"
@@ -93,9 +94,6 @@ activate_client_mode() {
     # Deactivate hotspot if it's active
     nmcli con down "$HOTSPOT_CON_NAME" 2>/dev/null || true
 
-    # Stop DNS redirect if running
-    stop_dns_redirect
-
     # Connect
     if nmcli con up "$con_name" 2>/dev/null; then
         local ip
@@ -117,12 +115,12 @@ activate_hotspot_mode() {
     nmcli dev disconnect "$IFACE" 2>/dev/null || true
 
     # Activate the hotspot profile
+    # NM's shared mode starts built-in dnsmasq for DHCP+DNS.
+    # Our config in /etc/NetworkManager/dnsmasq-shared.d/ makes it
+    # redirect all DNS queries to 10.42.0.1 for captive portal detection.
     if nmcli con up "$HOTSPOT_CON_NAME" 2>/dev/null; then
         log "Hotspot active. SSID: $(nmcli -t -f 802-11-wireless.ssid con show "$HOTSPOT_CON_NAME" 2>/dev/null | cut -d: -f2)"
-
-        # Start DNS redirect for captive portal
-        start_dns_redirect
-
+        log "DNS redirect active via NM dnsmasq (captive portal mode)"
         echo "hotspot" > "$MODE_FILE"
         return 0
     else
@@ -131,40 +129,6 @@ activate_hotspot_mode() {
     fi
 }
 
-# Start dnsmasq for DNS redirect (captive portal detection)
-start_dns_redirect() {
-    if [ ! -f "$DNSMASQ_CONF" ]; then
-        log "WARNING: dnsmasq config not found at $DNSMASQ_CONF"
-        return 1
-    fi
-
-    # Kill any existing dnsmasq instance we started
-    stop_dns_redirect
-
-    # Start dnsmasq with our config (DNS-only, no DHCP — NM handles DHCP)
-    # Use port 53 and bind to hotspot interface
-    dnsmasq --conf-file="$DNSMASQ_CONF" \
-             --interface="$IFACE" \
-             --bind-interfaces \
-             --pid-file="$DNSMASQ_PID" \
-             --log-facility=/var/log/dune-weaver-dns.log \
-             2>/dev/null
-
-    log "DNS redirect started (captive portal mode)"
-}
-
-# Stop our dnsmasq instance
-stop_dns_redirect() {
-    if [ -f "$DNSMASQ_PID" ]; then
-        local pid
-        pid=$(cat "$DNSMASQ_PID" 2>/dev/null || true)
-        if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
-            kill "$pid" 2>/dev/null || true
-        fi
-        rm -f "$DNSMASQ_PID"
-    fi
-}
-
 # Main logic
 main() {
     log "Starting autohotspot check..."

+ 6 - 13
wifi/dnsmasq-hotspot.conf

@@ -1,8 +1,11 @@
 # Dune Weaver - DNS redirect for captive portal detection
 #
-# Used in hotspot mode only. Redirects ALL DNS queries to the Pi
-# so that phones/tablets trigger captive portal detection and
-# open the Dune Weaver app automatically.
+# This file is installed to /etc/NetworkManager/dnsmasq-shared.d/
+# It is automatically loaded by NetworkManager's built-in dnsmasq
+# when a "shared" connection (hotspot) is active.
+#
+# Redirects ALL DNS queries to the Pi so that phones/tablets trigger
+# captive portal detection and open the Dune Weaver app automatically.
 #
 # iOS checks: captive.apple.com
 # Android checks: connectivitycheck.gstatic.com
@@ -13,13 +16,3 @@
 
 # Respond to all DNS queries with the hotspot IP
 address=/#/10.42.0.1
-
-# Don't use upstream DNS servers (we want ALL queries to resolve to us)
-no-resolv
-
-# Don't read /etc/hosts
-no-hosts
-
-# Only handle DNS (port 53), not DHCP (NetworkManager handles DHCP)
-no-dhcp-interface=wlan0
-port=53

+ 21 - 28
wifi/setup-wifi.sh

@@ -6,6 +6,9 @@
 # hotspot when no known network is available. Users connect to the hotspot
 # and configure WiFi through the Dune Weaver app.
 #
+# DNS redirect for captive portal is handled by NetworkManager's built-in
+# dnsmasq (via dnsmasq-shared.d config), not a separate dnsmasq instance.
+#
 # Run: bash wifi/setup-wifi.sh
 # Or:  dw wifi setup
 #
@@ -27,7 +30,7 @@ HOTSPOT_CON_NAME="DuneWeaver-Hotspot"
 HOTSPOT_SSID="Dune Weaver"
 HOTSPOT_IP="10.42.0.1/24"
 IFACE="wlan0"
-CONF_DIR="/etc/dune-weaver"
+NM_DNSMASQ_DIR="/etc/NetworkManager/dnsmasq-shared.d"
 
 print_step() {
     echo -e "\n${BLUE}==>${NC} ${GREEN}$1${NC}"
@@ -63,23 +66,6 @@ check_prereqs() {
     print_success "Prerequisites OK"
 }
 
-# Install dnsmasq for DNS redirect (captive portal)
-install_dnsmasq() {
-    print_step "Installing dnsmasq..."
-
-    if command -v dnsmasq &>/dev/null; then
-        echo "dnsmasq already installed"
-    else
-        sudo apt update
-        sudo DEBIAN_FRONTEND=noninteractive apt install -y dnsmasq
-    fi
-
-    # Disable the default dnsmasq service — we manage it manually in autohotspot
-    sudo systemctl disable --now dnsmasq 2>/dev/null || true
-
-    print_success "dnsmasq installed and default service disabled"
-}
-
 # Create the NetworkManager hotspot connection profile
 create_hotspot_profile() {
     print_step "Creating hotspot connection profile..."
@@ -110,23 +96,30 @@ create_hotspot_profile() {
     print_success "Hotspot profile created: SSID='$ssid', IP=${HOTSPOT_IP%/*}"
 }
 
-# Copy configuration files
-install_configs() {
-    print_step "Installing configuration files..."
+# Install DNS redirect config for captive portal
+install_dns_redirect() {
+    print_step "Installing DNS redirect for captive portal..."
 
-    # Create config directory
-    sudo mkdir -p "$CONF_DIR"
+    # NetworkManager's shared mode runs its own dnsmasq.
+    # Configs in dnsmasq-shared.d/ are loaded automatically when a shared
+    # connection is active. This redirects ALL DNS to the Pi's hotspot IP,
+    # triggering captive portal detection on phones/tablets.
+    sudo mkdir -p "$NM_DNSMASQ_DIR"
+    sudo cp "$SCRIPT_DIR/dnsmasq-hotspot.conf" "$NM_DNSMASQ_DIR/dune-weaver-captive.conf"
 
-    # Copy dnsmasq config
-    sudo cp "$SCRIPT_DIR/dnsmasq-hotspot.conf" "$CONF_DIR/dnsmasq-hotspot.conf"
-    echo "Installed $CONF_DIR/dnsmasq-hotspot.conf"
+    print_success "DNS redirect installed at $NM_DNSMASQ_DIR/dune-weaver-captive.conf"
+}
+
+# Copy autohotspot script and service
+install_configs() {
+    print_step "Installing autohotspot script..."
 
     # Copy autohotspot script
     sudo cp "$SCRIPT_DIR/autohotspot" /usr/local/bin/autohotspot
     sudo chmod +x /usr/local/bin/autohotspot
     echo "Installed /usr/local/bin/autohotspot"
 
-    print_success "Configuration files installed"
+    print_success "Autohotspot script installed"
 }
 
 # Install and enable the systemd service
@@ -146,8 +139,8 @@ main() {
     echo ""
 
     check_prereqs
-    install_dnsmasq
     create_hotspot_profile
+    install_dns_redirect
     install_configs
     install_service