基于Boost.Asio的C++ TLS服务器:如何从原始ClientHello数据中读取ALPN且不执行握手?
基于Boost.Asio的C++ TLS服务器:如何从原始ClientHello数据中读取ALPN且不执行握手?
嘿,这个问题我熟,刚好之前做过类似的需求,给你一步步拆解解决方法!
一、核心思路梳理
你当前用MSG_PEEK预读socket数据的方向完全正确——这样不会把数据从socket缓冲区移除,之后Boost.Asio的TLS握手还能正常读取这些内容。接下来要做的就是手动解析TLS ClientHello的二进制结构,定位到ALPN扩展字段。
TLS ClientHello是分层结构:记录层头 → 握手层头 → ClientHello固定主体 → 扩展列表,ALPN就是扩展列表里的一个特定条目(扩展类型码为0x0010)。
二、完整解析代码实现
我把解析逻辑直接整合到你的现有代码里,加上详细注释,你可以直接复用:
async_read(*socket, boost::asio::null_buffers(), [this, socket] (const boost::system::error_code& ec, std::size_t bytes_transferred) { if (ec) { std::cout << "Failed to read into the null_buffers()"; return; } char client_hello_buf[8192]; int length = recv(socket->native_handle(), client_hello_buf, sizeof(client_hello_buf), MSG_PEEK); if (length <= 0) { std::cout << "Failed to peek data from socket"; return; } // 定义TLS协议相关常量 const uint8_t TLS_HANDSHAKE_CONTENT_TYPE = 0x16; const uint8_t CLIENT_HELLO_HANDSHAKE_TYPE = 0x01; const uint16_t ALPN_EXTENSION_TYPE = 0x0010; // 第一步:验证并解析TLS记录层头 if (length < 5) { std::cout << "Insufficient data for TLS record layer header"; return; } uint8_t content_type = reinterpret_cast<uint8_t*>(client_hello_buf)[0]; if (content_type != TLS_HANDSHAKE_CONTENT_TYPE) { std::cout << "Received non-handshake TLS record"; return; } // 读取记录层总长度(网络字节序转主机序) uint16_t record_total_len = ntohs(*reinterpret_cast<uint16_t*>(client_hello_buf + 3)); if (length < 5 + record_total_len) { std::cout << "Incomplete handshake record, need to wait for more data"; return; } // 第二步:解析握手层头 const uint8_t* handshake_data = reinterpret_cast<uint8_t*>(client_hello_buf) + 5; uint8_t handshake_type = handshake_data[0]; if (handshake_type != CLIENT_HELLO_HANDSHAKE_TYPE) { std::cout << "Received non-ClientHello handshake message"; return; } // 读取握手消息长度(3字节网络序转主机序) uint32_t handshake_msg_len = (static_cast<uint32_t>(handshake_data[1]) << 16) | (static_cast<uint32_t>(handshake_data[2]) << 8) | static_cast<uint32_t>(handshake_data[3]); if (record_total_len < 4 + handshake_msg_len) { std::cout << "Incomplete ClientHello message content"; return; } // 第三步:跳过ClientHello固定字段,定位到扩展列表 const uint8_t* ch_body = handshake_data + 4; size_t offset = 0; // 跳过客户端TLS版本号(2字节) offset += 2; // 跳过随机数(32字节) offset += 32; // 跳过会话ID:先读长度,再跳对应字节数 uint8_t session_id_len = ch_body[offset]; offset += 1 + session_id_len; // 跳过密码套件列表:读总长度,再跳对应字节数 uint16_t cipher_suites_len = ntohs(*reinterpret_cast<const uint16_t*>(ch_body + offset)); offset += 2 + cipher_suites_len; // 跳过压缩方法列表:读长度,再跳对应字节数 uint8_t comp_method_len = ch_body[offset]; offset += 1 + comp_method_len; // 读取扩展列表总长度 uint16_t extensions_total_len = ntohs(*reinterpret_cast<const uint16_t*>(ch_body + offset)); offset += 2; // 第四步:遍历扩展列表,寻找ALPN std::vector<std::string> alpn_protocols; while (offset < (2 + 32 + 1 + session_id_len + 2 + cipher_suites_len + 1 + comp_method_len + 2 + extensions_total_len)) { // 每个扩展的开头是2字节类型码+2字节长度 if (offset + 4 > handshake_msg_len) { break; // 扩展数据不完整,直接终止解析 } uint16_t ext_type = ntohs(*reinterpret_cast<const uint16_t*>(ch_body + offset)); uint16_t ext_len = ntohs(*reinterpret_cast<const uint16_t*>(ch_body + offset + 2)); offset += 4; if (ext_type == ALPN_EXTENSION_TYPE) { // 找到ALPN扩展,解析协议列表 if (offset + 1 > handshake_msg_len) break; uint8_t alpn_list_len = ch_body[offset]; offset += 1; size_t alpn_offset = 0; while (alpn_offset < alpn_list_len) { if (offset +1 > handshake_msg_len) break; uint8_t proto_len = ch_body[offset]; offset +=1; if (offset + proto_len > handshake_msg_len) break; alpn_protocols.emplace_back(reinterpret_cast<const char*>(ch_body + offset), proto_len); offset += proto_len; alpn_offset += 1 + proto_len; } break; // 找到ALPN后无需继续遍历其他扩展 } else { // 跳过当前非ALPN扩展 offset += ext_len; } } // 第五步:输出解析结果 if (!alpn_protocols.empty()) { std::cout << "Client offered ALPN protocols:" << std::endl; for (const auto& proto : alpn_protocols) { std::cout << "- " << proto << std::endl; } } else { std::cout << "No ALPN extension found in ClientHello" << std::endl; } });
三、关于最小读取字节数的问题
我给你算清楚两个关键数值:
- 最小的不带任何扩展的ClientHello总长度是51字节:
- TLS记录层头:5字节(内容类型+版本+长度)
- 握手层:46字节(握手类型+长度+ClientHello最小固定字段)
- 如果你要确保能读取到可能存在的ALPN扩展,没有固定的最小值(因为ALPN是可选扩展,协议列表长度不固定),但实际开发中为了避免反复读取,建议直接读取至少1024字节,或者直到记录层指示的完整数据都被读取。
关键注意事项
MSG_PEEK的使用是核心:它只会复制socket缓冲区的数据,不会移除数据,这样之后Boost.Asio的TLS握手就能正常读取这些内容完成流程。- TLS所有多字节字段都是网络字节序(大端),必须用
ntohs/ntohl转成主机序再处理,否则会出现解析错误。 - 一定要处理数据不完整的情况:如果预读的字节数不够,要继续等待更多数据再解析,不能硬解析不完整的二进制数据。
- 这段代码兼容TLS 1.2和TLS 1.3:TLS 1.3的ClientHello结构为了兼容性,和TLS 1.2的ALPN扩展格式完全一致。
内容来源于stack exchange
相关产品推荐
相关产品推荐

