基于lwIP与mbed TLS的嵌入式HTTPS服务器实现技术问询
实现基于lwIP + mbed TLS的嵌入式HTTPS服务器方案
我之前在几个嵌入式项目里刚好折腾过这个——用lwIP自带的HTTPD结合mbed TLS改造成HTTPS服务器,给你分享下具体的思路和代码片段,应该能帮你少踩点坑。
核心改造逻辑
lwIP的httpd本质是基于TCP连接做HTTP解析,我们要做的就是给TCP套一层TLS加密:在TCP连接建立后先完成TLS握手,把原来直接操作TCP socket的读写,替换成经过mbed TLS加密的读写。核心步骤大概是:
- 在httpd处理新连接的阶段(比如
httpd_accept回调)创建mbed TLS的SSL上下文 - 把TCP的socket绑定到SSL上下文的IO回调上
- 完成TLS握手(一定要用非阻塞方式,别卡死lwIP的线程)
- 后续httpd的所有数据收发都通过SSL上下文走,替换原来的
recv/send为mbedtls_ssl_read/mbedtls_ssl_write
关键代码片段示例
1. 全局初始化mbed TLS环境
首先要在系统启动时初始化mbed TLS的基础组件,比如随机数生成器、证书和私钥:
#include "mbedtls/ssl.h" #include "mbedtls/certs.h" #include "mbedtls/ctr_drbg.h" #include "mbedtls/entropy.h" // 全局mbed TLS上下文(可以根据需求改成动态分配) mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context ctr_drbg; mbedtls_ssl_config ssl_cfg; mbedtls_x509_crt cert; mbedtls_pk_context pkey; void mbedtls_server_init(void) { mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_init(&ctr_drbg); // 初始化随机数生成器,有硬件随机源的话这里可以替换成硬件接口 mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, NULL, 0); mbedtls_x509_crt_init(&cert); mbedtls_pk_init(&pkey); // 加载服务器证书和私钥,实际项目里建议从加密Flash读取,别硬编码 // 这里用mbed TLS自带的测试证书做示例 int ret = mbedtls_x509_crt_parse(&cert, (const unsigned char*) mbedtls_test_crt, mbedtls_test_crt_len); if (ret != 0) { // 证书加载失败,做错误处理 return; } ret = mbedtls_pk_parse_key(&pkey, (const unsigned char*) mbedtls_test_key, mbedtls_test_key_len, NULL, 0); if (ret != 0) { // 私钥加载失败,做错误处理 return; } mbedtls_ssl_config_init(&ssl_cfg); mbedtls_ssl_config_defaults(&ssl_cfg, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); // 绑定证书和私钥到SSL配置 mbedtls_ssl_conf_ca_chain(&ssl_cfg, cert.next, NULL); mbedtls_ssl_conf_own_cert(&ssl_cfg, &cert, &pkey); // 绑定随机数生成器 mbedtls_ssl_conf_rng(&ssl_cfg, mbedtls_ctr_drbg_random, &ctr_drbg); }
2. 改造lwIP httpd的新连接处理
lwIP的httpd默认在httpd.c的httpd_accept函数处理新TCP连接,我们要在这里插入TLS握手的逻辑:
// 自定义结构体,把httpd连接和SSL上下文绑定起来 typedef struct { struct httpd_state *conn; mbedtls_ssl_context ssl; } httpd_ssl_conn_t; err_t httpd_accept(void *arg, struct tcp_pcb *newpcb, err_t err) { struct httpd_state *conn; httpd_ssl_conn_t *ssl_conn; // 保留lwIP httpd原来的连接初始化逻辑 conn = (struct httpd_state *)mem_malloc(sizeof(struct httpd_state)); if (conn == NULL) { return ERR_MEM; } memset(conn, 0, sizeof(struct httpd_state)); conn->pcb = newpcb; tcp_arg(newpcb, conn); tcp_recv(newpcb, httpd_recv); tcp_err(newpcb, httpd_err); tcp_poll(newpcb, httpd_poll, 4); // 初始化SSL上下文 ssl_conn = (httpd_ssl_conn_t *)mem_malloc(sizeof(httpd_ssl_conn_t)); if (ssl_conn == NULL) { mem_free(conn); return ERR_MEM; } memset(ssl_conn, 0, sizeof(httpd_ssl_conn_t)); ssl_conn->conn = conn; mbedtls_ssl_init(&ssl_conn->ssl); // 绑定全局SSL配置到当前连接的SSL上下文 mbedtls_ssl_setup(&ssl_conn->ssl, &ssl_cfg); // 把TCP socket绑定到SSL的IO回调,需要实现下面的send/recv回调 mbedtls_ssl_set_bio(&ssl_conn->ssl, newpcb, httpd_ssl_send, httpd_ssl_recv, NULL); // 启动非阻塞TLS握手,注意处理WANT_READ/WANT_WRITE的情况 int ret = mbedtls_ssl_handshake(&ssl_conn->ssl); if (ret != 0 && ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { // 握手失败,清理资源 mbedtls_ssl_free(&ssl_conn->ssl); mem_free(ssl_conn); mem_free(conn); tcp_close(newpcb); return ERR_CONN; } // 把SSL上下文绑定到httpd连接结构体(需要修改httpd_state,加一个void* ssl_ptr字段) conn->ssl_ptr = ssl_conn; return ERR_OK; }
3. 实现mbed TLS的IO回调函数
mbed TLS需要我们提供读写TCP的回调,把lwIP的TCP操作转成mbed TLS能识别的接口:
static int httpd_ssl_send(void *ctx, const unsigned char *buf, size_t len) { struct tcp_pcb *pcb = (struct tcp_pcb *)ctx; err_t err; u16_t sent_bytes; // 用lwIP的tcp_write发送数据 err = tcp_write(pcb, buf, len, TCP_WRITE_FLAG_COPY); if (err != ERR_OK) { return MBEDTLS_ERR_SSL_IO; } // 触发TCP发送 sent_bytes = tcp_output(pcb); if (sent_bytes != len) { // 数据没发完,告诉mbed TLS稍后再试 return MBEDTLS_ERR_SSL_WANT_WRITE; } return len; } static int httpd_ssl_recv(void *ctx, unsigned char *buf, size_t len) { struct tcp_pcb *pcb = (struct tcp_pcb *)ctx; err_t err; u16_t recv_bytes; // 从TCP读取数据 err = tcp_recv(pcb, buf, len, &recv_bytes); if (err != ERR_OK) { return MBEDTLS_ERR_SSL_IO; } if (recv_bytes == 0) { // 对方关闭连接 return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY; } return recv_bytes; }
4. 改写httpd的接收和发送逻辑
原来的httpd_recv直接从TCP读取数据,现在要改成从SSL上下文读取:
err_t httpd_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) { struct httpd_state *conn = (struct httpd_state *)arg; httpd_ssl_conn_t *ssl_conn = (httpd_ssl_conn_t *)conn->ssl_ptr; unsigned char recv_buf[1024]; // 根据你的MTU调整大小 int ret; // 先处理未完成的TLS握手(非阻塞模式下可能需要多次调用) ret = mbedtls_ssl_handshake(&ssl_conn->ssl); if (ret != 0 && ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { // 握手失败,关闭连接 httpd_close_conn(conn); return ERR_OK; } // 从SSL读取加密后的数据 ret = mbedtls_ssl_read(&ssl_conn->ssl, recv_buf, sizeof(recv_buf)); if (ret <= 0) { if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { // 对方正常关闭连接 httpd_close_conn(conn); } return ERR_OK; } // 把读取到的明文转成lwIP的pbuf,交给原来的httpd处理逻辑 struct pbuf *new_p = pbuf_alloc(PBUF_TRANSPORT, ret, PBUF_RAM); if (new_p != NULL) { memcpy(new_p->payload, recv_buf, ret); // 调用httpd的原始处理函数,比如httpd_process_input httpd_process_input(conn, new_p); pbuf_free(new_p); } return ERR_OK; }
同样,原来的httpd发送响应的函数(比如httpd_send)也要改成用mbedtls_ssl_write来发送数据,逻辑和接收类似。
踩坑提示
- 内存限制:嵌入式设备内存紧张,mbed TLS的SSL上下文占内存不小,建议调整mbed TLS的编译配置(比如减少支持的加密套件、关闭不必要的TLS扩展)
- 非阻塞必须严格遵守:lwIP通常是单线程或轻量级线程环境,绝对不能用阻塞的TLS操作,否则会卡死整个系统
- 证书安全:服务器证书和私钥别硬编码在代码里,建议存在加密的Flash分区,或者用设备的安全存储模块
- 性能优化:如果设备有硬件加密加速模块,一定要让mbed TLS调用硬件接口,不然加密解密会拖慢系统
替代方案补充
如果实在不想改lwIP httpd,可以看看lwIP的contrib库有没有第三方HTTPS实现,或者试试mongoose——轻量级嵌入式Web服务器,原生支持HTTPS,也能和lwIP、mbed TLS配合。不过你说暂时没找到合适的,那改造lwIP httpd确实是最直接的方案。
内容的提问来源于stack exchange,提问作者wildfireheart
相关产品推荐
相关产品推荐

