第3篇:全景架构图 —— 让这张图刻在你脑子里

📅 2026/6/27 9:37:00
第3篇:全景架构图 —— 让这张图刻在你脑子里
第3篇全景架构图 —— 让这张图刻在你脑子里一、所有复杂系统都是插线板我老婆前段时间第一次进我们公司的机房。她看着满墙的网线、交换机、服务器站了三十秒然后说了一句话“这看起来就像一个巨大的插线板。”我说对差不多就是这个意思。所有复杂系统拆开来看都是一堆东西连在一起。区别只在于有的连线是看得见的网线有的是看不见的内存里的指针、回调函数、线程间的消息队列。今天这篇文章我要往你脑子里刻一张图。后面 27 篇文章的每一篇都是在这张图上找一个地方下车细逛。如果你读完这篇之后只记住了一件事那就记这个从手机到公网一个数据包经过的六个关卡。二、用户视角这个系统怎么用在进入技术细节之前我们先站在一个普通用户的角度看这个系统到底怎么用。你有一台 Windows 电脑插着网线或者连着 WiFi能正常上网。你下载了 wifi2socks.exe放在任意目录下。目录里还有一个 config.ini 配置文件。双击 exe。一个命令行窗口弹出来刷刷刷打印几行日志。然后——什么都不会发生。没有 GUI 窗口弹出来没有桌面图标没有提示音。唯一的变化是你的手机上突然搜到了一个叫wifi2socks_ap的 WiFi 热点。输入密码password123连上去。打开浏览器访问www.example.com。通了。如果你想知道系统现在在干什么打开浏览器访问http://localhost:8088/api/stats。一个 JSON 显示着当前的流量统计、在线设备数、转发状态。这就是全部的用户体验。没有配置向导没有复杂设置不需要理解 VLAN、旁路由、静态路由表。就是一个 exe双击连 WiFi上网。但这背后的技术是我们接下来 27 篇文章的全部内容。三、架构全景图六大关卡一个数据包从你的手机到公网要经过六个关卡。每个关卡是一组模块负责一类工作。你的手机 (192.168.137.101) │ │ 无线电波 ▼ ╔══════════════════════════════════════════════════════════╗ ║ 关卡1WiFi 接入层 ║ ║ ┌──────────────────────────────────────────────────┐ ║ ║ │ WiFiAPManager │ WifiAPListener │ ║ ║ │ (WinRT 热点) │ (设备连接/断开事件) │ ║ ║ └──────────────────────────────────────────────────┘ ║ ║ 职责创建 WiFi Direct AP管理设备接入 ║ ╚══════════════════════════════════════════════════════════╝ │ │ 以太网帧 ▼ ╔══════════════════════════════════════════════════════════╗ ║ 关卡2NDIS 数据包拦截层 [内核态] ║ ║ ┌──────────────────────────────────────────────────┐ ║ ║ │ WinPkFilter NDIS Driver │ PacketPool │ ║ ║ │ (网卡层拦截所有帧) │ (32768个预分配buffer) │ ║ ║ └──────────────────────────────────────────────────┘ ║ ║ 职责在二层拦截所有出入站数据包批量上交用户态 ║ ╚══════════════════════════════════════════════════════════╝ │ │ 拦截到的包 ▼ ╔══════════════════════════════════════════════════════════╗ ║ 关卡3基础协议服务层 ║ ║ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ║ ║ │ DhcpServer │ │ ARP透传 │ │ ICMP处理 │ ║ ║ │ (分配IP租约)│ │ (网关可达) │ │ (ping响应) │ ║ ║ └─────────────┘ └─────────────┘ └─────────────┘ ║ ║ 职责让设备拿到IP、让网关可达、让基础网络协议正常工作 ║ ╚══════════════════════════════════════════════════════════╝ │ │ 经过协议处理的TCP/UDP包 ▼ ╔══════════════════════════════════════════════════════════╗ ║ 关卡4路由决策层 ║ ║ ┌────────────┐ ┌──────────┐ ┌──────────────────┐ ║ ║ │ RuleManager│ │GeoChecker│ │ NatManager │ ║ ║ │(CIDR域名) │ │(IP域名) │ │ (连接追踪NAT) │ ║ ║ └────────────┘ └──────────┘ └──────────────────┘ ║ ║ 职责决定这个包走直连、转发、还是阻断追踪每个连接 ║ ╚══════════════════════════════════════════════════════════╝ │ ┌────┴────┐ │ 路由结果 │ └────┬────┘ ┌───────────────┼───────────────┐ ▼ ▼ ▼ DIRECT FORWARD BLOCK (直连) (转发) (丢包) │ │ │ ▼ │ ╔══════════════════════════════════╗ │ ║ 关卡5透明转发层 ║ │ ║ ┌──────────┐ ┌──────────┐ ║ │ ║ │ProxyTCP │ │ UdpProxy │ ║ │ ║ │(TCP中转) │ │(UDP转发) │ ║ │ ║ └──────────┘ └──────────┘ ║ │ ║ ┌──────────┐ ┌──────────┐ ║ │ ║ │TlsParser │ │Connection│ ║ │ ║ │(SNI嗅探) │ │ Pool │ ║ │ ║ └──────────┘ └──────────┘ ║ │ ║ 职责重定向桥接数据中转 ║ │ ╚══════════════════════════════════╝ │ │ ▼ ▼ ╔══════════════════════════════════════════════════════════╗ ║ 关卡6DNS 系统 ║ ║ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ ║ ║ │ DnsCache │ │DnsOverTcp │ │DnsProbeTracker│ ║ ║ │(结果缓存)│ │(TCP fallback)│ │(App流量分类) │ ║ ║ └──────────┘ └──────────────┘ └───────────────┘ ║ ║ 职责DNS接管分流缓存安全解析 ║ ╚══════════════════════════════════════════════════════════╝ │ ▼ ┌──────────┐ │ 公网 │ │ (经过或 │ │ 不经过 │ │ 上游) │ └──────────┘这就是全貌。一张图六大关卡二十几个模块。但要真正理解一个系统光有静态结构图不够。你得看它动起来。四、数据流一个 TCP 连接的完整生命周期假设手机192.168.137.101上的浏览器要访问https://www.example.com。下面是全过程。阶段 0预备已就绪在我们双击 exe 的那一刻系统已经完成了启动序列WiFi 热点已广播WiFiAPManager::Start()DHCP 服务器已就绪DhcpServer::start()WinPkFilter 驱动已绑定 WiFi Direct 适配器WinPkFilterDriver::start_batch()GeoIP/GeoSite 数据已加载GeoChecker::init()IOCP Worker 线程池已就绪WorkerPool::init(8)HTTP API 服务已监听SimpleHttpServer::start()手机连上 WiFi → DHCP 分配 IP192.168.137.101→ 手机拿到网关 IP192.168.137.1和 DNS 服务器地址。阶段 1DNS 查询浏览器要先解析www.example.com。手机发出一个 DNS 查询UDP 目标端口 53。这个 UDP 包在 NDIS 层被 WinPkFilter 拦截。程序看到目标端口53→ DNS 包→ 转入 DNS 处理逻辑先查 DnsCache“www.example.com 解析过吗”没命中 → 判断域名归属GeoSite 查一下 → example类别 → 走转发通过 DnsOverTcpProxy 向公共 DNS 发起 DNS 查询走上游转发通道拿到响应 → 写入 DnsCache → 构造 DNS 响应包返回给手机手机拿到了www.example.com的 IP93.184.216.34。阶段 2TCP 连接拦截浏览器向93.184.216.34:443发起 TCP SYN。这个 SYN 包在 NDIS 层被拦截。程序提取五元组192.168.137.101:45678 → 93.184.216.34:443, TCP。NatManager创建一条映射key make_key(192.168.137.101, 45678)→OriginalDestination(93.184.216.34, 443, ...)。这条映射的意义是“记住这个连接是手机发起的目标是 example.com。”规则引擎上场。先查 RuleManager有没有用户自定义规则匹配93.184.216.34假设没有。再查 GeoCheckeris_cn_ip(93.184.216.34)→ false不是中国 IP。路由结果FORWARD。阶段 3透明转发程序把 SYN 包重定向到本地的一个端口。所谓重定向就是修改 IP 头和 TCP 头的目标地址让它指向127.0.0.1:3080AppListener 监听的端口。AppListener 收到这个连接从 NAT 缓存里查出真正的目标地址93.184.216.34:443。然后发起一次全新的 TCP 连接——程序作为客户端通过上游服务器连接到真正的 example.com 服务器。现在有两个 TCP 连接连接 A手机 ↔ wifi2socks手机上以为自己在跟 example.com 说话连接 Bwifi2socks ↔ example.com真正的连接程序的工作变成了在连接 A 和连接 B 之间桥接数据。手机发来的数据 → 转发到连接 B。连接 B 收到的数据 → 转发给手机。关键点连接 A 和连接 B 是两次独立的、完整的TCP 连接。各自的超时重传、ACK/SEQ 管理、拥塞窗口——全由操作系统内核维护。程序不需要自己实现 TCP 状态机只需要在应用层做数据转发。这就是重定向到本地端口方案的优雅之处。阶段 4TLS 握手与 SNI 嗅探手机发出 TLS ClientHello包含 SNI “www.example.com”。连接 A 的服务端AppListener 的 3080 端口在收到手机发来的第一批数据里嗅探到 SNI 域名 → “www.example.com”确认路由决策正确。然后把 ClientHello 原样转发到连接 B → example.com 收到 → 回复 ServerHello → 转发回连接 A → 手机收到。HTTPS 连接建立。后续的加密数据在两个连接之间透明桥上往来穿梭。阶段 5连接结束用户关闭浏览器标签页。手机发出 TCP FIN → 经过连接 A 到达程序 → 程序转发 FIN 到连接 B → example.com 回复 FIN-ACK → 转发回手机。两个连接各自完成四次挥手NAT 映射在超时后被清理。这就是一个 TCP 连接从生到死的完整故事。五、控制平面不处理数据包但决定数据包命运的东西上面的六大关卡属于数据平面——直接处理数据包。还有一组模块属于控制平面——不碰数据包但决定了数据包怎么走。┌─────────────────────────────────────────────────┐ │ 控制平面 │ │ │ │ config.ini ──→ ConfigLoader ──→ g_config │ │ │ │ HTTP API ──→ RuleManager.reload() ──→ 规则更新 │ │ (:8088) │ │ │ │ geoip.dat ──→ GeoChecker ──→ IP归属查询 │ │ geosite.dat │ │ │ │ Logger ──→ wifi2socks.log (异步日志) │ │ │ │ TrafficManager ──→ 流量统计 JSON │ │ │ │ MainDispatcher ──→ 主线程任务调度 │ └─────────────────────────────────────────────────┘控制平面的信息流动方向config.ini→g_config→ 所有模块读取配置HTTP API →RuleManager.reload()→ 新规则立即生效geoip.dat/geosite.dat→GeoChecker内存数据结构 → O(1) 或 O(log N) 查询TrafficManager← 各模块上报的字节数 → HTTP API 输出 JSONWorker 线程 →MainDispatcher.queue_task()→ 主线程执行六、模块速查表25 个模块的一句话档案#模块一句话所在关卡1WiFiAPManager用 WinRT 创建 WiFi Direct 热点WiFi接入2WifiAPListener监听设备连接/断开事件WiFi接入3WinPkFilter Driver内核态二层包拦截真正的核心NDIS拦截4WinPkFilterDriver驱动用户态封装reader loop worker queueNDIS拦截5PacketPool32768个预分配缓冲区避免运行时 new/deleteNDIS拦截6DhcpServer自建 DHCP 服务器分配 IP 和 DNS基础协议7Adapter有线网卡管理发现适配器、配置静态 IP基础协议8NatManager连接追踪表TCP五元组 UDP四元组路由决策9RuleManager用户规则引擎Direct/Forward/Block路由决策10GeoCheckerGeoIP 二分查找 GeoSite 域名分类路由决策11ConfigLoader解析 config.ini加载所有配置项控制平面12ProxyTCPTCP 透明转发重定向桥接数据转发透明转发13UdpProxyUDP 透明转发虚拟端口转发超时管理透明转发14AppListener本地 TCP/UDP 监听接收重定向流量透明转发15TlsParser解析 TLS ClientHello 提取 SNI 域名透明转发16ConnectionPool上游连接复用池透明转发17DnsCacheDNS 结果缓存TTL 自适应DNS系统18DnsOverTcpProxyDNS over TCP 转发通过上游DNS系统19DnsProbeTrackerDNS 查询域名分类厂商/App识别DNS系统20WorkerPool/WorkerIOCP 异步 I/O 线程池8线程共享1个IOCP基础架构21MainDispatcher主线程任务队列跨线程任务投递基础架构22TrafficManager流量统计字节数速率控制平面23SimpleHttpServer内嵌 HTTP API 服务器:8088控制平面24Logger异步日志DEBUG/INFO/WARN/ERROR控制平面25wifi2socks.cppmain() 函数启动序列编排入口七、启动顺序为什么这个顺序不能乱main()函数的启动顺序是在多次启动失败→排查→调整顺序之后收敛到的最优解。它不是随便排的① Winsock WinRT 初始化 ② load_config(config.ini) ← 一切模块都依赖配置 ③ resolve_ap_mode() ← WiFi 还是网线 ④ AsyncLogger.init() ← 日志系统要尽早可用 ⑤ WiFiAPManager.Start() ← 热点先广播 ⑥ DhcpServer.start() ← (仅有线模式) 让设备能拿IP ⑦ TrafficManager.start() ← 开始统计 ⑧ GeoChecker.init() ← 加载 geoip.dat geosite.dat ⑨ PacketPool.init(32768) ← 数据包缓冲池要就绪 ⑩ adapter_name find_adapter() ← 找到要绑定的网卡 ⑪ WorkerPool.init(8) ← IOCP 线程池启动 ⑫ UdpProxy.initialize() ← UDP 转发器就绪 ⑬ WinPkFilterDriver.start_batch() ← ★ 开始截包(从这里开始包涌入) ⑭ SimpleHttpServer.start() ← API 最后启动 ⑮ while(true) { 主循环 } ← 事件循环超时检查健康监控KeepAlive⑬ 必须在 ⑧⑨⑩⑪⑫ 之后——因为一旦start_batch()调用数据包就开始以每秒几千个的速度涌入。前面所有的设施——GeoIP 数据、PacketPool、Worker 线程、NAT 表——都必须已经准备就绪。否则程序会在第一个包到来时崩溃。八、这张图够不够如果你对上面这张图有了大致印象——六大关卡、两条平面、25 个模块、一个启动序列——那这篇就完成了使命。不需要记住每一个细节只需要知道什么东西在哪个关卡。后面每一篇我们会在某个关卡停下来打开引擎盖看里面是怎么转的。下一篇NDIS。本文是《从0到1编写一个硬核软路由》系列的第三篇。上一篇第2篇Windows上劫持流量的N种姿势 | 下一篇第4篇NDIS驱动是什么鬼