libwebsockets 实战:用 lws_system 状态机集成 Captive Portal(门户劫持)检测

发布时间:2026/10/14 1:06:49
libwebsockets 实战:用 lws_system 状态机集成 Captive Portal(门户劫持)检测 后端网络通信【免费下载链接】libwebsocketscanonical libwebsockets.org networking library项目地址https://gitcode.com/gh_mirrors/li/libwebsockets点击查看免费下载本文以 libwebsockets 官方示例minimal-examples-lowlevel/http-client/minimal-http-client-captive-portal为主体详解如何把 captive portal强制门户检测接入lws_system系统状态机在 DHCP 配网完成后触发一个 HTTP 探测根据响应码区分「真实互联网 / 被门户劫持 / 完全无网络」三种结局并在确认网络可用后自动放行应用进入LWS_SYSTATE_OPERATIONAL状态。读完本文你将掌握lws_system_ops_t.captive_portal_detect_request、lws_system_cpd_start/set/state_get这套 API 的完整用法、LWS_SYSTATE_CPD_PRE_TIME/LWS_SYSTATE_CPD_POST_TIME两个触发时机的差异以及应用侧如何用状态通知器notifier钩住整个状态推进过程。1. 背景设备为什么需要 captive portal 检测无线设备IoT 网关、智能家居设备等接入的 Wi-Fi 网络常被中间拦截典型的咖啡厅、酒店、机场热点会在你真正上网之前弹出一个登录或「点击同意」页面。从设备视角看此时 DHCP 租约已经到手、DNS 也「正常」应答但所有 HTTP 请求都会被重定向到门户页——也就是说网络层看起来一切正常应用层却被拦截了。设备需要一种手段判断自己是否处于这种状态。业界常见方案见 READMEs/README.captive-portal-detection.mdAndroid 方案使用最广HTTP GEThttp://connectivitycheck.android.com/generate_204真实互联网会返回204 No Content被劫持时通常收到 3xx 重定向可能跳转到 https 门户页或直接返回 200 并携带门户页 HTML——总之收不到 204变体方案期望特定 body 内容的 200、DNS 解析设备已知的固定 IP 并比对结果、发起 https 连接并检测服务端证书是否「对不上」有歧义门户状态对一个连接是「粘性」的只要不打算登录门户DHCP 获取之后检测一次即可如果应用会去完成门户登录则登录后应再次检测。libwebsockets 将这件事抽象为一组通用 API平台/用户代码自己实现检测策略lws 核心只负责在正确的系统状态时机触发检测、保存结果、并在结果出来后推进状态机。本文示例正是这套机制的完整参考实现。2. 示例总览检测与 lws_system 状态的协作流程示例的 READMEREADME.md描述了整体流程lws_system状态机推进到 DHCP 之后的LWS_SYSTATE_CPD_PRE_TIME状态时应用注册的状态通知器被触发调用lws_system_cpd_start(cx)lws 转而调用应用提供的captive_portal_detect_request实现示例向http://connectivitycheck.android.com/generate_204发起 HTTP GET如果收到204回调中调用lws_system_cpd_set(cx, LWS_CPD_INTERNET_OK)——lws 保存该结果并推动状态机向OPERATIONAL前进进入LWS_SYSTATE_OPERATIONAL后应用确认 CPD 结果为LWS_CPD_INTERNET_OK随后发起一次对https://warmcat.com/的正式 HTTPS 请求作为「主业务」演示如果检测结果不是INTERNET_OK应用直接退出不再尝试读取 warmcat.com。CPD 结果只有四种取值定义于 include/libwebsockets/lws-system.h常量含义LWS_CPD_UNKNOWN上次 DHCP 获取以来还没有做过测试LWS_CPD_INTERNET_OK没有门户CPD 测试通过可以正常上网LWS_CPD_CAPTIVE_PORTAL推断出处于强制门户之后被重定向/拦截LWS_CPD_NO_INTERNET什么都连不上3. 构建与运行README 原始内容构建$ cmake . make运行$ ./bin/lws-minimal-http-client-captive-portal [2020/03/11 13:07:07:4519] U: LWS minimal http client captive portal detect [2020/03/11 13:07:07:4519] N: lws_create_context: using ss proxy bind (null), port 0, ads (null) [2020/03/11 13:07:07:5022] U: callback_cpd_http: established with resp 204 [2020/03/11 13:07:07:5023] U: app_system_state_nf: OPERATIONAL, cpd 1 [2020/03/11 13:07:07:5896] U: Connected to 46.105.127.147, http response: 200 [2020/03/11 13:07:07:5931] U: RECEIVE_CLIENT_HTTP_READ: read 4087 [2020/03/11 13:07:07:5931] U: RECEIVE_CLIENT_HTTP_READ: read 4096 [2020/03/11 13:07:07:6092] U: RECEIVE_CLIENT_HTTP_READ: read 4087 [2020/03/11 13:07:07:6092] U: RECEIVE_CLIENT_HTTP_READ: read 4096 [2020/03/11 13:07:07:6112] U: RECEIVE_CLIENT_HTTP_READ: read 4087 [2020/03/11 13:07:07:6113] U: RECEIVE_CLIENT_HTTP_READ: read 4096 [2020/03/11 13:07:07:6113] U: RECEIVE_CLIENT_HTTP_READ: read 2657 [2020/03/11 13:07:07:6113] U: LWS_CALLBACK_COMPLETED_CLIENT_HTTP [2020/03/11 13:07:07:6119] U: main: finished OK输出日志逐行印证了流程callback_cpd_http: established with resp 204是 CPD 探测拿到 204app_system_state_nf: OPERATIONAL, cpd 1表明系统状态已推进到 OPERATIONAL 且 CPD 结果为 1即LWS_CPD_INTERNET_OK随后是对 warmcat.com 的 HTTPS 请求响应 200与若干块响应体读取最后finished OK。命令行支持-d设置日志级别如-d 15与--help由 lws-switches 机制lws_cmdline_option_handle_builtin统一处理。该示例的 CMakeLists.txt 明确了它对 libwebsockets 编译开关的硬性要求这四点在移植时要对照检查require_lws_config(LWS_ROLE_H1 1 requirements) # HTTP/1.1 角色CPD 探测用 require_lws_config(LWS_WITH_TLS 1 requirements) # TLSwarmcat.com 的 HTTPS 请求用 require_lws_config(LWS_WITH_CLIENT 1 requirements) # 客户端连接 require_lws_config(LWS_WITH_SYS_STATE 1 requirements) # lws_system 系统状态机4. 源码走读三个角色各司其职示例源码minimal-http-client-captive-portal.c虽然只有 300 多行但把 CPD 集成拆成了三个清晰的角色CPD 专用协议回调、主业务协议回调、系统状态通知器。4.1 双协议表把 CPD 探测与主业务隔离static const struct lws_protocols protocols[] { { http, callback_http, /* 主业务读 warmcat.com */ 0, 0, 0, NULL, 0 }, { lws-cpd-http, callback_cpd_http, /* CPD 专用判定 204 / 重定向 / 失败 */ 0, 0, 0, NULL, 0 }, LWS_PROTOCOL_LIST_TERM };两个客户端连接CPD 探测与 warmcat.com 主请求通过lws_client_connect_info.protocol字段绑定到不同协议从而使用各自的回调。CPD 的判定逻辑独立成协议正是 README 强调的「captive portal detection scheme 在用户代码里实现可按需修改策略」。4.2 触发检测实现captive_portal_detect_request平台实现的挂载点是lws_system_ops_t的函数指针static int captive_portal_detect_request(struct lws_context *cx) { struct lws_client_connect_info i; memset(i, 0, sizeof i); i.context cx; i.port 80; i.address connectivitycheck.android.com; i.path /generate_204; i.host i.address; i.origin i.address; i.method GET; i.protocol lws-cpd-http; return !lws_client_connect_via_info(i); } static lws_system_ops_t ops { .captive_portal_detect_request captive_portal_detect_request };要点这个函数只负责发起异步探测不阻塞等待结果——结果稍后由callback_cpd_http回报给 lws返回值语义是「启动是否成功」return !lws_client_connect_via_info(i)把 lws 的 0成功转成 0失败转成非 0i.protocol lws-cpd-http让这条连接落到 CPD 专用回调换成其他策略自有探测域名、DNS 比对、特定 body 校验等只需替换这个函数体和对应回调的判定逻辑lws 侧代码零改动。在 lws-system.h 中该成员的定义与注释明确了它的契约int (*captive_portal_detect_request)(struct lws_context *context); /** Check if we can go out on the internet cleanly, or if we are being * redirected or intercepted by a captive portal. * Start the check that proceeds asynchronously, and report the results * by calling lws_captive_portal_detect_result() api */注意头文件注释里提到的结果回报 API 在 READMEs/README.captive-portal-detection.md 中写作lws_system_cpd_result()当前仓库头文件的正式签名是lws_system_cpd_set(struct lws_context *context, lws_cpd_result_t result)本文及示例代码均以当前仓库签名lws_system_cpd_set()为准。4.3 回报结果callback_cpd_http的三分支判定case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP: resp (int)lws_http_client_http_response(wsi); if (!resp) break; lwsl_user(%s: established with resp %d\n, __func__, resp); switch (resp) { case HTTP_STATUS_NO_CONTENT: /* 收到 204说明是真互联网端点 */ lws_system_cpd_set(lws_get_context(wsi), LWS_CPD_INTERNET_OK); return 0; /* also case HTTP_STATUS_OK: ... */ default: break; } /* fallthru */ case LWS_CALLBACK_CLIENT_HTTP_REDIRECT: lws_system_cpd_set(lws_get_context(wsi), LWS_CPD_CAPTIVE_PORTAL); /* dont follow it, just report it */ return 1; case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: case LWS_CALLBACK_CLOSED_CLIENT_HTTP: /* only the first result counts */ lws_system_cpd_set(lws_get_context(wsi), LWS_CPD_NO_INTERNET); break;判定矩阵事件判定结果连接建立且响应码为204真实互联网LWS_CPD_INTERNET_OK收到3xx 重定向LWS_CALLBACK_CLIENT_HTTP_REDIRECT被门户拦截LWS_CPD_CAPTIVE_PORTAL连接错误 / 连接关闭含门户直接回 200 门户页导致探测流终止的场景无法触达LWS_CPD_NO_INTERNET两个细节值得注意重定向分支return 1——不跟随跳转只上报「存在门户」结论。是否跟随、如何解析门户 URL 完全留给用户代码注释only the first result counts与核心实现呼应lws_system_cpd_set()内部对已有非 UNKNOWN 结果会直接忽略重复写入见下文第 5 节所以「第一次结果说了算」。4.4 状态通知器在CPD_PRE_TIME触发检测、在OPERATIONAL干正事static int app_system_state_nf(lws_state_manager_t *mgr, lws_state_notify_link_t *link, int current, int target) { struct lws_context *cx lws_system_context_from_system_mgr(mgr); switch (target) { case LWS_SYSTATE_CPD_PRE_TIME: if (lws_system_cpd_state_get(cx)) return 0; /* allow it */ lwsl_info(%s: LWS_SYSTATE_CPD_PRE_TIME\n, __func__); lws_system_cpd_start(cx); /* well move the state on when we get a result */ return 1; case LWS_SYSTATE_OPERATIONAL: if (current LWS_SYSTATE_OPERATIONAL) { lwsl_user(%s: OPERATIONAL, cpd %d\n, __func__, lws_system_cpd_state_get(cx)); if (lws_system_cpd_state_get(cx) ! LWS_CPD_INTERNET_OK) { lwsl_warn(%s: Theres no internet...\n, __func__); lws_default_loop_exit(context); break; } memset(i, 0, sizeof i); i.context context; i.ssl_connection LCCSCF_USE_SSL; i.ssl_connection | LCCSCF_H2_QUIRK_OVERFLOWS_TXCR | LCCSCF_H2_QUIRK_NGHTTP2_END_STREAM; i.port 443; i.address warmcat.com; i.path /; i.host i.address; i.origin i.address; i.method GET; i.protocol protocols[0].name; lws_client_connect_via_info(i); break; } default: break; } return 0; }这里体现了 lws 状态通知器notifier的两个关键能力定义见 include/libwebsockets/lws-state.h拦截veto通知器返回 1 表示「本次状态迁移暂停」由通知器自己发起依赖操作等依赖完成后再由 lws 内部或回调把状态推下去。CPD_PRE_TIME分支在 CPD 尚未出结果时返回 1把状态机卡在时间校验之前直到lws_system_cpd_set()收到结果后由 lws 核心调用lws_state_transition_steps()继续前进接管takeover通知器可以发起「迁移目标之后的正事」——进入OPERATIONAL且current target时即状态已经稳定落地应用才发起 warmcat.com 请求。注册方式是在 context 创建信息中挂 notifier 列表static lws_state_notify_link_t nl; /* 全局 */ ... nl.name app; nl.notify_cb app_system_state_nf; info.register_notifier_list app_notifier_list; /* nl, NULL 结尾 */ context lws_create_context(info);lws_state_notify_link_t只有三个成员list链接、notify_cb回调、name名称在lws_create_context()内部由register_notifier_list统一注册到系统状态管理器。4.5 主业务回调与退出控制callback_httpwarmcat.com 请求是标准的客户端 HTTP 读取模式LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP用lws_get_peer_simple()打印对端lws_http_client_http_response()取响应码存入全局statusLWS_CALLBACK_RECEIVE_CLIENT_HTTP未解释的原始内容用lws_http_client_read(wsi, px, lenx)让 lws 解析并转发LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ每块chunked 内容去头后到达示例只打印长度源码中预留了#if 0的 HTML dump 开关LWS_CALLBACK_COMPLETED_CLIENT_HTTP/LWS_CALLBACK_CLOSED_CLIENT_HTTP请求完成bad status ! 200记录成败lws_default_loop_exit(context)结束主循环lws_cancel_service()打断 poll 等待。main()的收尾用return bad把 HTTP 成败映射为进程退出码SIGINT也通过lws_default_loop_exit()干净退出。context 配置要点lws_context_info_defaults(info, NULL); lws_cmdline_option_handle_builtin(argc, argv, info); info.port CONTEXT_PORT_NO_LISTEN; /* 纯客户端不监听 */ info.options LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; info.system_ops ops; /* 挂 CPD 实现 */ info.protocols protocols;5. lws 核心侧实现CPD 三件套是怎么工作的理解示例行为只需看清 lib/core/context.c 里三个核心函数lws_system_cpd_start(cx)发起int lws_system_cpd_start(struct lws_context *cx) { if (cx-options LWS_SERVER_OPTION_CPD_BYPASS) { lws_system_cpd_set(cx, LWS_CPD_INTERNET_OK); return 0; } cx-captive_portal_detect LWS_CPD_UNKNOWN; /* if theres a platform implementation, use it */ if (lws_system_get_ops(cx) lws_system_get_ops(cx)-captive_portal_detect_request) return lws_system_get_ops(cx)-captive_portal_detect_request(cx); #if defined(LWS_WITH_SECURE_STREAMS) /* 否则尝试 SS captive_portal_detect 流如果启用 */ return lws_ss_sys_cpd(cx); #else return 0; #endif }可以看出优先级链若 context options 带有LWS_SERVER_OPTION_CPD_BYPASS定义于 include/libwebsockets/lws-context-vhost.h直接置为INTERNET_OK跳过检测——这是给可信环境如内网自测用的旁路开关把状态复位为LWS_CPD_UNKNOWN若用户提供了captive_portal_detect_request本示例的情况调用它否则回退到 Secure Streams 内置的captive_portal_detect流类型见 lib/secure-streams/system/captive-portal-detect/captive-portal-detect.c成功建流并拿到远端 QOS ACK 即LWS_CPD_INTERNET_OK超时/重试耗尽/断开则LWS_CPD_NO_INTERNET。lws_system_cpd_set(cx, result)回报void lws_system_cpd_set(struct lws_context *cx, lws_cpd_result_t result) { if (cx-captive_portal_detect ! LWS_CPD_UNKNOWN) return; ... cx-captive_portal_detect (uint8_t)result; #if defined(LWS_WITH_SYS_STATE) #if defined(LWS_WITH_SYS_SMD) lws_smd_msg_printf(cx, LWSSMDCL_NETWORK, {\type\:\cpd\,\result\:\%s\}, cname[cx-captive_portal_detect]); #endif /* if nothing is there to intercept anything, go all the way */ if (cx-mgr_system.state ! LWS_SYSTATE_POLICY_INVALID) lws_state_transition_steps(cx-mgr_system, LWS_SYSTATE_OPERATIONAL); #endif }三个行为要点首次结果粘性结果一旦不是UNKNOWN就不再被覆盖这解释了示例注释「only the first result counts」SMD 广播结果会作为{type:cpd,result:OK|Captive|No internet}消息广播到LWSSMDCL_NETWORK类其他组件如 lib/secure-streams/protocols/ss-h1.c 中的消费方可订阅自动推进状态机结果落定后立即lws_state_transition_steps()向OPERATIONAL推进——也就是说示例通知器里return 1卡住的迁移就是在这里被解锁的。lws_system_cpd_state_get(cx)查询简单返回cx-captive_portal_detect当前值供应用如示例的OPERATIONAL分支或 SS 等子系统查询。另有一个延迟版接口lws_system_cpd_start_defer(cx, defer_us)context.c基于 sorted usec list 在指定微秒后重发检测用于「稍后再查」的场景lib/io/client/connect2.c 中保留了 DNS 不可达时「1 秒后复查 CPD」的调用点当前处于#if 0中从源码结构看属于预留的重检钩子。此外SMD 消息也可以从外部触发/影响 CPDcontext.c 的lws_system_smd_cb收到{trigger:cpdcheck}会重跑检测收到平台栈上报的{type:ipacq}表示平台自己完成了 DHCP 并拿到了 IP则置ipacq_seen并立即启动 CPD——此时 CPD 结果只是「参考信息」状态机不再因等待 CPD 而卡死。这个行为有专门的自测验证minimal-examples-lowlevel/api-tests/api-test-system-cpd 通过两阶段断言「无 IP 时不得进入 OPERATIONAL / 收到合成 ipacq 后必须在无 CPD 策略的情况下进入 OPERATIONAL」。6. 触发时机LWS_SYSTATE_CPD_PRE_TIME与LWS_SYSTATE_CPD_POST_TIMElws 系统状态序列中为 CPD 预留了两个位置lws-system.hLWS_SYSTATE_DHCP, /* at least one net iface configured */ LWS_SYSTATE_CPD_PRE_TIME, /* DHCP 之后、ntpclient 之前的 CPD 适合不依赖时间非 https的方案 */ LWS_SYSTATE_TIME_VALID, /* 时间有效后 TLS 才能工作 */ LWS_SYSTATE_CPD_POST_TIME, /* ntpclient 完成后的 CPD适合 https 方案 */选择依据是方案是否需要可信时间Android 式的明文 HTTP 204 探测不校验证书时间无关放CPD_PRE_TIME更早拿到结论本示例即用此时机基于 https 证书校验的探测需要先让 ntpclient 把系统时间校准TIME_VALID应挂到CPD_POST_TIME。通知器的写法完全对称把app_system_state_nf的switch (target)中LWS_SYSTATE_CPD_PRE_TIME改成LWS_SYSTATE_CPD_POST_TIME即可切换时机无需改动 lws 侧任何代码。从源码结构看还有一条「软门槛」逻辑在启用 Secure Streams 驱动drivers的配置下若平台栈已通过 SMD 报告过 IP 获取ipacq_seenCPD_PRE_TIME的检查直接放行、不再阻塞状态机context.c——即 CPD 结论对状态推进是「advisory参考性」的但结果仍可通过 SMD 与lws_system_cpd_state_get()查询应用可以像本示例一样在OPERATIONAL后自行把关。7. 把这套集成搬进你的项目清单编译开关LWS_ROLE_H1、LWS_WITH_TLS若主业务用 HTTPS、LWS_WITH_CLIENT、LWS_WITH_SYS_STATE写探测函数实现captive_portal_detect_request()返回非 0 表示发起失败策略可任意204 探测、DNS 比对、自有服务均可写判定回调用一个独立lws_protocols协议承接探测连接在LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP/LWS_CALLBACK_CLIENT_HTTP_REDIRECT/LWS_CALLBACK_CLIENT_CONNECTION_ERROR/LWS_CALLBACK_CLOSED_CLIENT_HTTP中按策略调用lws_system_cpd_set()三个结果分别对应LWS_CPD_INTERNET_OK/LWS_CPD_CAPTIVE_PORTAL/LWS_CPD_NO_INTERNET注册状态通知器通过register_notifier_list挂lws_state_notify_link_t在LWS_SYSTATE_CPD_PRE_TIME或POST_TIME中lws_system_cpd_start()并返回 1 拦截迁移在LWS_SYSTATE_OPERATIONAL且current target中用lws_system_cpd_state_get()把关后执行主业务context 配置info.system_ops ops可信测试环境可用LWS_SERVER_OPTION_CPD_BYPASS跳过理解行为边界首次结果粘滞结果通过 SMD{type:cpd,...}广播平台栈自行 DHCP 时 CPD 对状态推进转为参考性见 api-test-system-cpd 自测。8. 小结libwebsockets 把 captive portal 检测从「一段散落的网络代码」升级为系统状态机中的一等公民lws_system_ops_t.captive_portal_detect_request让平台代码注入任意检测策略lws_system_cpd_start/set/state_get三件套负责触发、结果粘滞记录与自动推进状态LWS_SYSTATE_CPD_PRE_TIME / CPD_POST_TIME两个状态位点分别适配非时间依赖与基于 https 的方案。官方示例 minimal-http-client-captive-portal 以 300 余行完整演示了「DHCP 后自动检测 → 204 判定真互联网 → 状态放行 → OPERATIONAL 后发起 HTTPS 主请求」的闭环是移植该机制到设备固件时的最佳起点。赞分享后端网络通信【免费下载链接】libwebsocketscanonical libwebsockets.org networking library项目地址https://gitcode.com/gh_mirrors/li/libwebsockets点击查看免费下载相关推荐libwebsockets 最小 HTTP 客户端示例基于 lws_system 状态机的 Captive Portal 检测实战libwebsockets 最小 HTTP 客户端示例基于 lws_system 状态机的 Captive Portal 检测实战 本篇技术指南基于当前仓库人工智能AI Agent多模态语音AI 应用libwebsockets 局域网强制门户Captive Portal检测机制与实现指南libwebsockets 局域网强制门户Captive Portal检测机制与实现指南 本文以 READMEs/README.captive portal后端网络通信libwebsockets 系统状态推进与 Captive Portal 检测门控api-test-system-cpd 源码级解析libwebsockets 系统状态推进与 Captive Portal 检测门控api test system cpd 源码级解析 导读 api test后端网络通信创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询