You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 11:49:51