ESP8266/ESP32实现DNS劫持与NCSI欺骗:打造自己的强制门户
2026/9/11 14:12:15 网站建设 项目流程

我第一次在一张巴掌大的 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 的余量明显更大。

选择哪个芯片取决于你的用途:

  • 只是验证思路、做个 Demo,ESP8266-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 Test
Androidhttp://connectivitycheck.gstatic.com/generate_204返回 HTTP 204
Apple 系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 的 IP
  • HTTP 层:识别探测URI,返回固定内容;识别普通访问,返回门户页面

比如connecttest.txt就返回纯文本Microsoft Connect Testgenerate_204就返回空 body 加 204 状态码。这样操作系统就会认为网络是通的,从而主动触发浏览器打开强制门户页面。这个把探测请求“伪装成正常应答”的过程,就是 NCSI 欺骗的核心。

3. 自己实现 DNS 响应:从报文结构到 DNSServer 库

3.1 DNS 报文没那么神秘

既然要讲实现,绕不开 DNS 报文的格式。很多人觉得协议复杂,是因为没有从二进制的角度去看。实际上,一个标准 DNS 查询报文是非常规整的。

报文开头是 12 字节的固定头部:

  • 事务 ID:2 字节,用于匹配请求和响应
  • Flags:2 字节,包含 QR、Opcode、RD、RA 等标志位
  • 问题数、回答数、权威数、附加数:各 2 字节

头部之后是问题部分,里面是我们要解析的域名、查询类型和查询类别。查询类型一般是 A 记录,也就是把域名映射到 IPv4 地址。

构造响应时,最关键的是这几步:

  1. 把查询报文里的 ID 原样复制,让客户端知道这是对应请求的响应
  2. 把 Flags 设置为 0x8180,表示这是一个标准响应,且请求域名不存在或已处理
  3. 把回答数设为 1
  4. 在响应末尾附加一个 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 框架,目标芯片是 ESP32,ESP8266 也基本能兼容,只是头文件略有差异。

#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 charset="utf-8"> <title>Wi-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", "<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</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,而我对这个路径返回了门户页面,状态码是 200,Android 认为这是一个被篡改的响应,于是判定网络异常。

修正后的做法是把所有已知探测路径都补齐。代码里已经写了三个主流平台的路径,覆盖 Windows、Android、Apple 系,基本能满足日常调试需求。如果你的设备是 Linux 发行版,可以再补上 nmcheck 的路径。

5.3 串口日志是排查问题的最佳工具

DNSServer 库本身不打印解析日志,这给调试带来了一点麻烦。我的做法是在processNextRequest处理前,自己再包一层,手动读取 UDP 包,把里面的域名打印到串口。

这个思路很简单:DNSServer 库绑定 53 端口后,你没法同时让另一个 UDP 绑定同一个端口。但你可以通过修改库源码,在processNextRequest里加一行Serial.println(requestedDomain)。如果你是 Arduino 环境,源码就在库目录下,改起来非常方便。

串口日志的作用不只是确认 DNS 查询有没有到达,更重要的是帮你发现设备在后台偷偷进行了哪些探测。我通过日志发现,某些手机会连发多个不同域名的探测请求,比如connectivitycheck.gstatic.comwww.google.cn同时出现。如果只盯着已知探测路径,很容易漏掉新变种。

5.4 字节序错误导致“别人返回正常,我返回乱码”

手写 DNS 报文时,最容易错的地方是字节序。ESP 是小端架构,但网络字节序是大端。如果你在构造 A 记录时忘了转换,客户端的 nslookup 会显示出类似“非权威应答失败的伪 IP”的情况。

我当时被这个问题卡了快一个小时,后来在 Wireshark 里对比正常 DNS 响应和我的响应,发现 IP 地址反了。修正方法就是统一用htonshtonl处理多字节字段。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 欺骗和规则处理,最后你会有一种“一台小芯片也能掌控整个局域网上网入口”的掌控感。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询