我第一次在一张巴掌大的 ESP8266 开发板上跑通这个项目时确实有点惊讶——这种平时用来点个灯、读个传感器的小单片机居然能稳定地给整个局域网提供 DNS 解析还能顺手把 DNS 劫持和 NCSI 欺骗一起做了。这篇文章不打算讲玄学直接从实战角度拆解整个思路为什么 ESP 能当 DNS 服务器、NCSI 欺骗到底在骗什么、劫持逻辑如何落地以及我踩过的那些隐蔽的坑。如果你对强制门户、Wi-Fi 热点认证、网络接入检测感兴趣这应该是一篇能让你少走弯路的记录。1. 为什么要在 ESP 上跑 DNS 服务器从强制门户说起1.1 场景驱动的技术选型很多人第一反应是DNS 不是应该跑在服务器上吗老老实实用 BIND、用 dnsmasq 不就行了干嘛要折腾 ESP这个问题的答案藏在“强制门户”这个场景里。所谓强制门户就是当你连上某个 Wi-Fi 后不管访问什么网站都会被导向一个登录或认证页面。酒店、商场、机场的免费 Wi-Fi 都是这么干的。实现强制门户最简单粗暴的思路就是把网络里的 DNS 请求全部接管让所有域名都解析到自己的 Web 服务器上其他流量再通过规则放行或转发。传统路由器上做这件事一般靠 dnsmasq 的 address 配置一行address/#/192.168.4.1就能把泛域名指向本地。但当你面对的是一个没有操作系统、没有 dnsmasq 的 ESP8266 或 ESP32 时这件事就变得有点“反直觉”了。可反过来想ESP 的优点是成本低、体积小、功耗低放在一个临时测试环境或者随身携带的渗透测试硬件里比带一台树莓派方便太多。1.2 ESP8266 与 ESP32 的能力边界先泼一盆冷水ESP 的 DNS 服务器不是万能的。它不像 dnsmasq 那样支持完整的 DNS 协议不支持 DNSSEC不支持大型 zone 传输也无法承担高 QPS 的线上压力。但在这个项目里我们根本不需要那些。我实测下来ESP8266 在 SoftAP 模式下每秒处理几十个 DNS 查询完全没问题。如果只是给几个测试设备做解析绰绰有余。ESP32 因为双核和更大的内存表现会更从容尤其是在还要同时跑 Web 服务器、串口日志和规则匹配的场景下ESP32 的余量明显更大。选择哪个芯片取决于你的用途只是验证思路、做个 DemoESP8266-12F 或 NodeMCU 就够了想把它做成一个稍微稳定点的工具或者同时处理 HTTP 页面和多个规则建议直接用 ESP32在 Arduino 环境下无论是 ESP8266 还是 ESP32都有现成的 DNSServer 库。这个库最早是 ESP8266 Arduino Core 自带的后来被移植到了 ESP32核心 API 几乎一致。使用它我们可以用几行代码就把所有 DNS 请求“接住”。2. NCSI 欺骗先解决操作系统信不信你有网的问题2.1 各大操作系统是怎么判断“网络可用的”如果只是做 DNS 劫持把所有域名解析到 ESP你会发现一个很尴尬的问题终端设备根本不会第一时间打开你的强制门户页面。Windows 会提示“无 Internet 访问”Android 可能会直接断开连接或者显示感叹号iPhone 则可能没有任何反应。原因在于现代操作系统都有一个“联网探测”机制。它们会在网络连接后偷偷向一些固定的地址发起探测请求用于判断当前网络是否真正连通。这就是 NCSI 的来源。我整理了几个常见平台的探测特征平台探测地址期望结果Windowshttp://www.msftconnecttest.com/connecttest.txt返回文本Microsoft Connect TestAndroidhttp://connectivitycheck.gstatic.com/generate_204返回 HTTP 204Apple 系http://captive.apple.com/hotspot-detect.html返回一个特定 HTML 页面部分 Linux 发行版http://nmcheck.gnome.org/check_network_status.txt返回文本这些请求的共同特点是频率不高、体积很小、内容固定。操作系统根据返回结果判断网络状态。如果我们把 DNS 劫持了这些探测域名也会被解析到 ESP 上那 ESP 就必须对这些探测请求给出正确的回应。否则设备会认定“这个网络没有互联网”继而影响强制门户的弹出。2.2 不响应 NCSI 的后果我第一次测试时只写了 DNS 劫持没管这些探测请求结果 Windows 端一直显示“无 Internet 访问”浏览器也没有自动弹出认证页面。手动打开浏览器访问一个网址确实能跳到我部署的页面上但终端系统层面已经判了“死刑”很多应用会直接走离线逻辑体验非常割裂。这也是为什么“NCSI 欺骗”在这个项目里是绕不开的环节。它的本质就是让操作系统的探测请求得到“标准答案”从而让系统误以为当前网络已经连上了公网于是放心地把你扔进真正的强制门户流程。2.3 NCSI 欺骗的实现思路有人会问是不是要把这些探测域名单独“放行”到真实世界不行。因为 ESP 的 SoftAP 模式下它本身没有外网出口也不做 NAT 转发探测请求如果被放行反而会超时。正确做法是让这些请求走到 ESP 的本地 Web 服务上然后由 Web 服务根据 URI 返回预期的内容。简单说就是两个层面配合DNS 层把所有域名包括探测域名全部解析到 ESP 的 IPHTTP 层识别探测URI返回固定内容识别普通访问返回门户页面比如connecttest.txt就返回纯文本Microsoft Connect Testgenerate_204就返回空 body 加 204 状态码。这样操作系统就会认为网络是通的从而主动触发浏览器打开强制门户页面。这个把探测请求“伪装成正常应答”的过程就是 NCSI 欺骗的核心。3. 自己实现 DNS 响应从报文结构到 DNSServer 库3.1 DNS 报文没那么神秘既然要讲实现绕不开 DNS 报文的格式。很多人觉得协议复杂是因为没有从二进制的角度去看。实际上一个标准 DNS 查询报文是非常规整的。报文开头是 12 字节的固定头部事务 ID2 字节用于匹配请求和响应Flags2 字节包含 QR、Opcode、RD、RA 等标志位问题数、回答数、权威数、附加数各 2 字节头部之后是问题部分里面是我们要解析的域名、查询类型和查询类别。查询类型一般是 A 记录也就是把域名映射到 IPv4 地址。构造响应时最关键的是这几步把查询报文里的 ID 原样复制让客户端知道这是对应请求的响应把 Flags 设置为 0x8180表示这是一个标准响应且请求域名不存在或已处理把回答数设为 1在响应末尾附加一个 A 记录把目标 IP 写进去A 记录的结构是域名指针通常指向 0xC00C表示引用查询域名、类型、类别、TTL、数据长度和 4 字节 IP。如果完全手写这段二进制逻辑代码量不大但需要非常小心字节序。DNSServer 库之所以受欢迎就是因为它帮你封装了这些细节。3.2 DNSServer 库一键接管所有查询在 Arduino 工程里使用 DNSServer 库最经典的配置是这样的#include ESP8266WiFi.h #include DNSServer.h #include ESP8266WebServer.h const byte DNS_PORT 53; IPAddress apIP(192, 168, 4, 1); DNSServer dnsServer; ESP8266WebServer webServer(80); void setup() { WiFi.mode(WIFI_AP); WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0)); WiFi.softAP(ESP-DNS-Lab); // 把所有域名解析到 ESP 自身 dnsServer.start(DNS_PORT, *, apIP); // 也可以给特定域名单独配置解析 dnsServer.addRecord(status.example.com, apIP); webServer.on(/, handlePortal); webServer.begin(); } void loop() { dnsServer.processNextRequest(); webServer.handleClient(); }dnsServer.start(53, *, apIP)里这个星号是通配符表示所有 DNS 查询都返回 apIP 对应的 A 记录。这是 DNS 劫持的最简实现。addRecord方法可以给某个具体域名单独设置解析地址。比如你想让captive.example.com解析到另一个 IP就能用这个接口加一条。这个能力在后面的“规则化劫持”里很关键。3.3 手动解析 DNS 查询的进阶思路DNSServer 库虽好但它默认只支持“泛解析”和“固定记录”两种方式。如果你想根据域名内容做模糊匹配比如只要域名里包含hotspot就返回 A 地址包含test就返回另一个地址光靠库本身不够灵活。这时候可以选择自己裸写 UDP。在 Arduino 环境下用 WiFiUDP 绑定 53 端口收到查询后解析域名再按规则构造回应。这种方式的好处是完全可控坏处是要处理所有协议细节。我自己的方案是两者的结合先用 DNSServer 库跑通全流程然后在它的基础上重写一个轻量解析器。这种循序渐进的方式比一开始就手写 UDP 服务要容易定位问题。等把整个报文结构吃透了再回头去看 DNSServer 库的源码你会发现它其实很薄核心就是做查询解析、规则匹配、响应构造三件事。4. 把 NCSI 欺骗和 DNS 劫持合并成完整可运行工程4.1 整体架构项目的最终形态是一个 ESP32 SoftAP 热点。设备连上热点后无论访问什么域名DNS 都会被解析到 192.168.4.1。紧接着设备发起的 HTTP 请求会到达 ESP 的 Web 服务。Web 服务需要区分两类请求NCSI 探测请求返回系统期望的标准内容普通请求返回强制门户页面这里核心逻辑是谁先匹配。我的做法是在handleRequest里先判断 URI命中探测路径就直接返回对应内容否则跳转到门户页面。这个顺序不能反因为 Windows 的探测请求通常发生在浏览器被触发之前。4.2 核心代码一个简约但完整的版本下面这个工程我实测过基于 Arduino 框架目标芯片是 ESP32ESP8266 也基本能兼容只是头文件略有差异。#include WiFi.h #include DNSServer.h #include WebServer.h const byte DNS_PORT 53; IPAddress apIP(192, 168, 4, 1); DNSServer dnsServer; WebServer webServer(80); const char *portalPage R( !DOCTYPE html html head meta charsetutf-8 titleWi-Fi 认证/title /head body h3欢迎使用实验室热点/h3 p请阅读并同意使用条款。/p a href/ok同意并继续/a /body /html ); void handleRoot() { String uri webServer.uri(); // NCSI 欺骗系统探测请求 if (uri /connecttest.txt) { webServer.send(200, text/plain, Microsoft Connect Test); return; } if (uri /generate_204) { webServer.send(204, text/plain, ); return; } if (uri /hotspot-detect.html) { webServer.send(200, text/html, HTMLHEADTITLESuccess/TITLE/HEADBODYSuccess/BODY/HTML); return; } // 普通请求返回强制门户页面 webServer.send(200, text/html, portalPage); } void setup() { Serial.begin(115200); WiFi.mode(WIFI_AP); WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0)); WiFi.softAP(ESP-DNS-Lab); dnsServer.setTTL(30); dnsServer.start(DNS_PORT, *, apIP); webServer.onNotFound(handleRoot); webServer.begin(); Serial.println(AP started: 192.168.4.1); } void loop() { dnsServer.processNextRequest(); webServer.handleClient(); }这个代码里我做了两件事值得解释。第一dnsServer.setTTL(30)。TTL 设得越短终端设备缓存 DNS 结果的时间越短。这在你反复调整测试时非常有用能减少“明明改了规则设备却还访问旧 IP”的困惑。实际生产环境可以根据情况调大减少 DNS 查询频率。第二onNotFound(handleRoot)。所有 HTTP 请求不管路径是什么都汇总到同一个处理函数。这样最简单也最不容易漏掉路径。唯一要注意的是某些设备会请求/favicon.ico、/apple-touch-icon.png之类的东西统一返回门户页面问题不大但如果希望日志更干净可以单独处理。4.3 劫持规则的多级扩展前面说过DNSServer 库支持addRecord。这意味着你可以实现“选择性劫持”而不是所有域名一律到 ESP。有个常见需求让某些域名解析到真实公网比如白名单其余全部捕获。但这里有个限制ESP 没有真正的外网转发能力解析到公网 IP 也打不开网页。所以在纯热点模式下泛解析仍然是最符合强制门户目标的方案。如果你想做“DNS 黑名单”也就是把某些特定域名解析到一个无效地址其他域名正常解析那需要手动构建一个 DNS 代理。我试过用 ESP32 同时开启 STA 和 AP 模式STA 连上上游路由器AP 给终端开热点然后在 DNS 处理时做规则匹配匹配到的返回自定义 IP没匹配到的就转发给上游 DNS。这个方案能跑通但复杂度高了一个量级ESP8266 的内存容易吃紧ESP32 会更从容。5. 调试记录我踩过的几个隐蔽的坑5.1 终端设备缓存了 DNS导致“劫持不生效”第一次跑通后我把设备从原来的 Wi-Fi 切换到 ESP 热点打开浏览器访问百度。结果页面没跳到门户反而提示“无法访问”。用 Wireshark 一看终端根本没发 DNS 查询直接用缓存里的旧 IP 发起了 HTTP 请求。问题出在网络切换后终端系统并没有立刻清空旧网络的 DNS 缓存。Windows 在 Wi-Fi 网络切换后通常会刷新 DNS但 Android 不一定。这时候最笨但有效的办法是开关一次 Wi-Fi或者手工清缓存。长期做法就是设置合理的 TTL。TTL 越短缓存保留时间越短劫持生效越快。我当时用的是 30 秒测试阶段非常顺手。但要注意太短的 TTL 会增加 DNS 查询次数ESP 的负担会稍微重一点不过在这种低负载场景下无所谓。5.2 NCSI 探测路径要覆盖全不能只针对 Windows我最初只处理了connecttest.txt在 Windows 上没问题但换上 Android 手机后系统一直不弹出认证提示。查日志发现Android 的探测请求走的是generate_204而我对这个路径返回了门户页面状态码是 200Android 认为这是一个被篡改的响应于是判定网络异常。修正后的做法是把所有已知探测路径都补齐。代码里已经写了三个主流平台的路径覆盖 Windows、Android、Apple 系基本能满足日常调试需求。如果你的设备是 Linux 发行版可以再补上 nmcheck 的路径。5.3 串口日志是排查问题的最佳工具DNSServer 库本身不打印解析日志这给调试带来了一点麻烦。我的做法是在processNextRequest处理前自己再包一层手动读取 UDP 包把里面的域名打印到串口。这个思路很简单DNSServer 库绑定 53 端口后你没法同时让另一个 UDP 绑定同一个端口。但你可以通过修改库源码在processNextRequest里加一行Serial.println(requestedDomain)。如果你是 Arduino 环境源码就在库目录下改起来非常方便。串口日志的作用不只是确认 DNS 查询有没有到达更重要的是帮你发现设备在后台偷偷进行了哪些探测。我通过日志发现某些手机会连发多个不同域名的探测请求比如connectivitycheck.gstatic.com和www.google.cn同时出现。如果只盯着已知探测路径很容易漏掉新变种。5.4 字节序错误导致“别人返回正常我返回乱码”手写 DNS 报文时最容易错的地方是字节序。ESP 是小端架构但网络字节序是大端。如果你在构造 A 记录时忘了转换客户端的 nslookup 会显示出类似“非权威应答失败的伪 IP”的情况。我当时被这个问题卡了快一个小时后来在 Wireshark 里对比正常 DNS 响应和我的响应发现 IP 地址反了。修正方法就是统一用htons和htonl处理多字节字段。DNSServer 库内部已经处理好了这些细节但如果你想自己动手这点一定要记牢。6. 扩展操作与安全须知好玩但要有边界6.1 把项目用于真实的访客网络认证一套能跑通的 ESP 强制门户最大的价值是做网络认证逻辑的小规模验证。我后来把它搬到了一个测试环境的访客 Wi-Fi 里用 ESP32 做接入点后端挂了一个简单的认证表单。用户在页面里输入访客码ESP 验证通过后通知路由器放行对应 MAC 地址。整个过程DNS 劫持负责把所有流量导向门户NCSI 欺骗负责让设备自动弹出页面。这种架构放在商用设备上当然不够看但在实验环境里它用一块几十块钱的板子完成了从“探测感知”到“页面认证”的闭环教学和演示意义很强。6.2 在授权范围内使用避免误伤网络必须强调一点DNS 劫持和 NCSI 欺骗不是只能用于合法场景但滥用会带来隐私和法律风险。这篇文章里所有的实验都应该限制在你自己拥有或者明确获得授权的网络里进行。在公司、学校、公共网络里擅自跑这种设备可能会影响其他人的正常上网也会让你自己陷入麻烦。另外ESP 的 SoftAP 模式默认不做隔离。如果多个设备连到同一个 ESP 热点设备之间是可能互相访问的。在真实部署时尽量在路由器层次启用 AP 隔离或者在后端做好白名单控制。6.3 硬件的稳定性与散热ESP8266 做 DNS 劫持时CPU 会持续处理网络中断发热量比单纯点灯大得多。我跑 24 小时测试时板载稳压器温升明显。如果你打算长期运行建议降低运行频率、加散热片或者直接选 ESP32。ESP32 的双核架构让它有更多余量实测连续跑了三天没有出现丢包率上升或死机的情况。总的来说ESP 当 DNS 服务器这个事技术门槛不高但背后的网络协议知识非常密集。从 DNS 报文结构到 NCSI 探测机制再到 HTTP 应答逻辑每一步都能拆出一堆值得深挖的细节。项目本身不大却把网络基础概念串成了一条完整的链路。如果你也对这块感兴趣建议从最简单的 DNS 劫持开始先把报文抓明白再逐步叠加 NCSI 欺骗和规则处理最后你会有一种“一台小芯片也能掌控整个局域网上网入口”的掌控感。