nanomsg / NNG 消息库完全教程 / 第 7 章:TLS 与安全通信
7.1 安全通信概述
在分布式系统中,消息传输的安全性至关重要。NNG 原生支持 TLS(Transport Layer Security),提供数据加密、身份认证和完整性保护。
7.1.1 安全需求
| 需求 | 说明 | NNG 支持 |
|---|---|---|
| 加密 | 防止窃听 | ✅ TLS 1.2 / 1.3 |
| 完整性 | 防止篡改 | ✅ TLS MAC |
| 身份认证 | 验证对端身份 | ✅ mTLS |
| 防重放 | 防止消息重放 | ✅ TLS 序列号 |
注意:TLS 功能仅 NNG 支持,nanomsg 不支持 TLS。这是选择 NNG 的重要理由之一。
7.2 NNG TLS 编译
7.2.1 启用 TLS
NNG 的 TLS 支持需要在编译时启用,且依赖 TLS 后端库:
选项 1:使用 mbedTLS(默认)
# 安装 mbedTLS
sudo apt install -y libmbedtls-dev
# 编译 NNG
cd nng/build
cmake .. -DCMAKE_BUILD_TYPE=Release \
-DNNG_ENABLE_TLS=ON \
-DNNG_TLS_ENGINE=mbedtls
make -j$(nproc)
sudo make install
选项 2:使用 OpenSSL
# 安装 OpenSSL
sudo apt install -y libssl-dev
# 编译 NNG
cd nng/build
cmake .. -DCMAKE_BUILD_TYPE=Release \
-DNNG_ENABLE_TLS=ON \
-DNNG_TLS_ENGINE=openssl
make -j$(nproc)
sudo make install
7.2.2 TLS 引擎对比
| 特性 | mbedTLS | OpenSSL |
|---|---|---|
| 体积 | 小 (~100KB) | 大 (~1MB) |
| 许可证 | Apache 2.0 | Apache 2.0 (3.x) / dual (1.x) |
| FIPS 认证 | ❌ | ✅ (部分版本) |
| 嵌入式友好 | ✅ | ⚠️ |
| 生态成熟度 | 中 | 高 |
建议:嵌入式场景选 mbedTLS;需要 FIPS 合规选 OpenSSL。
7.2.3 验证 TLS 支持
#include <stdio.h>
#include <nng/nng.h>
int main() {
// 尝试创建 TLS 拨号器来验证 TLS 是否可用
nng_socket sock;
nng_dialer dialer;
int rv;
nng_pair0_open(&sock);
rv = nng_dialer_create(&dialer, sock);
if (rv == 0) {
printf("NNG TLS support: OK\n");
}
nng_close(sock);
return 0;
}
7.3 证书生成
7.3.1 自签名证书(开发/测试)
# 生成 CA 私钥
openssl genrsa -out ca.key 4096
# 生成 CA 自签名证书
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
-subj "/CN=MyCA/O=MyOrg"
# 生成服务端私钥
openssl genrsa -out server.key 2048
# 生成服务端证书签名请求(CSR)
openssl req -new -key server.key -out server.csr \
-subj "/CN=localhost/O=MyOrg"
# 使用 CA 签发服务端证书
openssl x509 -req -days 365 -in server.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt
# 生成客户端私钥(用于 mTLS)
openssl genrsa -out client.key 2048
# 生成客户端 CSR
openssl req -new -key client.key -out client.csr \
-subj "/CN=client/O=MyOrg"
# 使用 CA 签发客户端证书
openssl x509 -req -days 365 -in client.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out client.crt
7.3.2 文件清单
certs/
├── ca.crt # CA 证书(客户端/服务端都需要)
├── ca.key # CA 私钥(仅签发时使用)
├── server.crt # 服务端证书
├── server.key # 服务端私钥
├── client.crt # 客户端证书(mTLS)
└── client.key # 客户端私钥(mTLS)
7.3.3 生产环境证书
生产环境应使用 CA 签发的证书:
| CA 类型 | 适用场景 | 示例 |
|---|---|---|
| 公共 CA | 面向互联网的服务 | Let’s Encrypt, DigiCert |
| 私有 CA | 内部服务通信 | 自建 CA, HashiCorp Vault |
| 临时证书 | 开发测试 | 自签名证书 |
7.4 TLS 服务端配置
7.4.1 基本 TLS 服务端
#include <nng/nng.h>
#include <nng/transport/tls/tls.h>
#include <nng/protocol/reqrep0/rep.h>
#include <stdio.h>
#include <string.h>
int main() {
nng_socket sock;
nng_listener listener;
int rv;
// 创建 REP Socket
if ((rv = nng_rep0_open(&sock)) != 0) {
fprintf(stderr, "nng_rep0_open: %s\n", nng_strerror(rv));
return 1;
}
// 创建 TLS 配置
nng_tls_config *tls;
if ((rv = nng_tls_config_alloc(&tls, NNG_TLS_MODE_SERVER)) != 0) {
fprintf(stderr, "nng_tls_config_alloc: %s\n", nng_strerror(rv));
return 1;
}
// 加载服务端证书和私钥
if ((rv = nng_tls_config_own_cert(tls, "server.crt", "server.key", NULL)) != 0) {
fprintf(stderr, "nng_tls_config_own_cert: %s\n", nng_strerror(rv));
return 1;
}
// 加载 CA 证书(用于验证客户端,可选)
if ((rv = nng_tls_config_ca_file(tls, "ca.crt")) != 0) {
fprintf(stderr, "nng_tls_config_ca_file: %s\n", nng_strerror(rv));
return 1;
}
// 创建 Listener
if ((rv = nng_listener_create(&listener, sock)) != 0) {
fprintf(stderr, "nng_listener_create: %s\n", nng_strerror(rv));
return 1;
}
// 将 TLS 配置应用到 Listener
if ((rv = nng_listener_setopt(listener, NNG_OPT_TLS_CONFIG, tls, sizeof(tls))) != 0) {
fprintf(stderr, "nng_listener_setopt: %s\n", nng_strerror(rv));
return 1;
}
// 启动监听
if ((rv = nng_listener_start(listener, 0)) != 0) {
fprintf(stderr, "nng_listener_start: %s\n", nng_strerror(rv));
return 1;
}
printf("TLS server listening on tls+tcp://0.0.0.0:4444\n");
// 消息循环
while (1) {
char *buf = NULL;
size_t sz;
if (nng_recv(sock, &buf, &sz, NNG_FLAG_ALLOC) == 0) {
printf("Received: %.*s\n", (int)sz, buf);
nng_free(buf, sz);
const char *reply = "OK (encrypted)";
nng_send(sock, (void *)reply, strlen(reply), 0);
}
}
nng_tls_config_free(tls);
nng_close(sock);
return 0;
}
7.5 TLS 客户端配置
7.5.1 基本 TLS 客户端
#include <nng/nng.h>
#include <nng/transport/tls/tls.h>
#include <nng/protocol/reqrep0/req.h>
#include <stdio.h>
#include <string.h>
int main() {
nng_socket sock;
nng_dialer dialer;
int rv;
if ((rv = nng_req0_open(&sock)) != 0) {
fprintf(stderr, "nng_req0_open: %s\n", nng_strerror(rv));
return 1;
}
// 创建 TLS 配置
nng_tls_config *tls;
if ((rv = nng_tls_config_alloc(&tls, NNG_TLS_MODE_CLIENT)) != 0) {
fprintf(stderr, "nng_tls_config_alloc: %s\n", nng_strerror(rv));
return 1;
}
// 加载 CA 证书(验证服务端身份)
if ((rv = nng_tls_config_ca_file(tls, "ca.crt")) != 0) {
fprintf(stderr, "nng_tls_config_ca_file: %s\n", nng_strerror(rv));
return 1;
}
// 创建 Dialer
if ((rv = nng_dialer_create(&dialer, sock)) != 0) {
fprintf(stderr, "nng_dialer_create: %s\n", nng_strerror(rv));
return 1;
}
// 将 TLS 配置应用到 Dialer
if ((rv = nng_dialer_setopt(dialer, NNG_OPT_TLS_CONFIG, tls, sizeof(tls))) != 0) {
fprintf(stderr, "nng_dialer_setopt: %s\n", nng_strerror(rv));
return 1;
}
// 启动连接
if ((rv = nng_dialer_start(dialer, 0)) != 0) {
fprintf(stderr, "nng_dialer_start: %s\n", nng_strerror(rv));
return 1;
}
printf("Connected to TLS server\n");
// 发送请求
const char *msg = "Hello, TLS!";
nng_send(sock, (void *)msg, strlen(msg), 0);
// 接收响应
char *buf = NULL;
size_t sz;
if (nng_recv(sock, &buf, &sz, NNG_FLAG_ALLOC) == 0) {
printf("Reply: %.*s\n", (int)sz, buf);
nng_free(buf, sz);
}
nng_tls_config_free(tls);
nng_close(sock);
return 0;
}
7.6 mTLS 双向认证
mTLS(Mutual TLS)要求客户端也出示证书,服务端验证客户端身份。适用于内部服务间的零信任通信。
7.6.1 服务端(要求客户端证书)
// 在服务端 TLS 配置中添加:
// 1. 加载 CA 证书
nng_tls_config_ca_file(tls, "ca.crt");
// 2. 设置验证模式为 REQUIRED(必须验证客户端证书)
nng_tls_config_auth_mode(tls, NNG_TLS_AUTH_MODE_REQUIRED);
7.6.2 客户端(出示证书)
// 在客户端 TLS 配置中添加:
// 1. 加载客户端证书和私钥
nng_tls_config_own_cert(tls, "client.crt", "client.key", NULL);
// 2. 加载 CA 证书(验证服务端)
nng_tls_config_ca_file(tls, "ca.crt");
7.6.3 mTLS 完整服务端示例
#include <nng/nng.h>
#include <nng/transport/tls/tls.h>
#include <nng/protocol/reqrep0/rep.h>
#include <stdio.h>
#include <string.h>
int main() {
nng_socket sock;
nng_listener listener;
nng_tls_config *tls;
int rv;
nng_rep0_open(&sock);
// 创建 TLS 配置(服务端模式)
nng_tls_config_alloc(&tls, NNG_TLS_MODE_SERVER);
// 加载服务端证书
nng_tls_config_own_cert(tls, "server.crt", "server.key", NULL);
// 加载 CA 证书
nng_tls_config_ca_file(tls, "ca.crt");
// 要求客户端证书(mTLS)
nng_tls_config_auth_mode(tls, NNG_TLS_AUTH_MODE_REQUIRED);
// 应用配置
nng_listener_create(&listener, sock);
nng_listener_setopt(listener, NNG_OPT_TLS_CONFIG, tls, sizeof(tls));
if ((rv = nng_listener_start(listener, 0)) != 0) {
fprintf(stderr, "Listen error: %s\n", nng_strerror(rv));
return 1;
}
printf("mTLS server on tls+tcp://0.0.0.0:4444\n");
// 消息循环
while (1) {
char *buf = NULL;
size_t sz;
if (nng_recv(sock, &buf, &sz, NNG_FLAG_ALLOC) == 0) {
printf("Received: %.*s\n", (int)sz, buf);
nng_free(buf, sz);
nng_send(sock, "OK", 2, 0);
}
}
nng_tls_config_free(tls);
nng_close(sock);
return 0;
}
7.6.4 TLS 认证模式
| 模式 | 说明 | 使用场景 |
|---|---|---|
NNG_TLS_AUTH_MODE_NONE | 不验证对端 | 仅加密,无认证 |
NNG_TLS_AUTH_MODE_OPTIONAL | 可选验证 | 服务端可选验证客户端 |
NNG_TLS_AUTH_MODE_REQUIRED | 必须验证 | mTLS 双向认证 |
7.7 WSS(加密 WebSocket)
NNG 支持 WSS(WebSocket over TLS),浏览器可以通过加密的 WebSocket 连接到 NNG 服务。
7.7.1 WSS 服务端
#include <nng/nng.h>
#include <nng/transport/ws/websocket.h>
#include <nng/transport/tls/tls.h>
#include <nng/protocol/pubsub0/pub.h>
#include <stdio.h>
int main() {
nng_socket sock;
nng_listener listener;
nng_tls_config *tls;
int rv;
nng_pub0_open(&sock);
// TLS 配置
nng_tls_config_alloc(&tls, NNG_TLS_MODE_SERVER);
nng_tls_config_own_cert(tls, "server.crt", "server.key", NULL);
// 创建 WSS Listener
nng_listener_create(&listener, sock);
nng_listener_setopt(listener, NNG_OPT_TLS_CONFIG, tls, sizeof(tls));
// 使用 wss:// 协议
if ((rv = nng_listener_start(listener, 0)) != 0) {
fprintf(stderr, "nng_listener_start: %s\n", nng_strerror(rv));
return 1;
}
printf("WSS server listening\n");
// 发布消息
while (1) {
const char *msg = "Hello from WSS!";
nng_send(sock, (void *)msg, strlen(msg), 0);
sleep(1);
}
nng_tls_config_free(tls);
nng_close(sock);
return 0;
}
7.7.2 浏览器连接(JavaScript)
// 浏览器中使用 WebSocket 连接 NNG WSS 服务
const ws = new WebSocket('wss://localhost:443/data');
ws.onopen = function() {
console.log('Connected to NNG WSS server');
};
ws.onmessage = function(event) {
console.log('Received:', event.data);
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
};
注意:浏览器连接需要使用有效的 TLS 证书(自签名证书需要手动信任)。
7.8 TLS 配置选项
7.8.1 TLS 配置 API
| 函数 | 用途 |
|---|---|
nng_tls_config_alloc() | 分配 TLS 配置 |
nng_tls_config_free() | 释放 TLS 配置 |
nng_tls_config_own_cert() | 设置本端证书和私钥 |
nng_tls_config_ca_file() | 加载 CA 证书文件 |
nng_tls_config_ca_chain() | 加载 CA 证书链 |
nng_tls_config_auth_mode() | 设置认证模式 |
nng_tls_config_server_name() | 设置 SNI 主机名 |
nng_tls_config_version() | 设置 TLS 最低版本 |
7.8.2 TLS 版本控制
// 设置最低 TLS 版本为 1.2
nng_tls_config_version(tls, NNG_TLS_1_2, NNG_TLS_1_3);
// 仅允许 TLS 1.3
nng_tls_config_version(tls, NNG_TLS_1_3, NNG_TLS_1_3);
| 版本 | 安全性 | 兼容性 | 推荐 |
|---|---|---|---|
| TLS 1.0 | ❌ 弱 | ✅ 广 | ❌ 不推荐 |
| TLS 1.1 | ❌ 弱 | ✅ 广 | ❌ 不推荐 |
| TLS 1.2 | ✅ 强 | ✅ 广 | ✅ 推荐 |
| TLS 1.3 | ✅ 最强 | ⚠️ 较新 | ✅ 最佳 |
7.8.3 SNI(Server Name Indication)
// 客户端设置 SNI 主机名
nng_tls_config_server_name(tls, "myserver.example.com");
// 用于服务端使用虚拟主机时区分证书
7.9 证书轮换
7.9.1 证书轮换策略
在生产环境中,证书会过期,需要定期轮换。NNG 支持动态更新 TLS 配置:
// 方法:创建新的 TLS 配置,替换旧配置
void reload_certificates(nng_listener listener) {
nng_tls_config *new_tls;
nng_tls_config_alloc(&new_tls, NNG_TLS_MODE_SERVER);
nng_tls_config_own_cert(new_tls, "server_new.crt", "server_new.key", NULL);
nng_tls_config_ca_file(new_tls, "ca.crt");
// 更新 Listener 的 TLS 配置
nng_listener_setopt(listener, NNG_OPT_TLS_CONFIG, new_tls, sizeof(new_tls));
printf("Certificates reloaded\n");
}
7.9.2 自动轮换脚本
#!/bin/bash
# rotate_certs.sh —— 证书轮换脚本
CERT_DIR="/etc/nng/certs"
DAYS_BEFORE_EXPIRY=30
# 检查证书是否即将过期
check_expiry() {
local cert="$1"
local expiry=$(openssl x509 -enddate -noout -in "$cert" | cut -d= -f2)
local expiry_epoch=$(date -d "$expiry" +%s)
local now_epoch=$(date +%s)
local days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ $days_left -lt $DAYS_BEFORE_EXPIRY ]; then
echo "Certificate $cert expires in $days_left days. Renewing..."
return 0
fi
return 1
}
# 轮换证书
rotate_cert() {
# 使用 certbot 或内部 CA 生成新证书
certbot renew --cert-name myserver.example.com
# 复制到 NNG 证书目录
cp /etc/letsencrypt/live/myserver.example.com/fullchain.pem $CERT_DIR/server.crt
cp /etc/letsencrypt/live/myserver.example.com/privkey.pem $CERT_DIR/server.key
# 通知应用重新加载(发送 SIGHUP 或 IPC 命令)
pkill -HUP my_nng_server
}
if check_expiry "$CERT_DIR/server.crt"; then
rotate_cert
fi
7.10 安全最佳实践
7.10.1 证书管理
| 实践 | 说明 |
|---|---|
| 使用私有 CA | 内部服务使用自建 CA 签发证书 |
| 定期轮换 | 证书有效期不超过 1 年 |
| 证书吊销 | 使用 CRL 或 OCSP 吊销泄露的证书 |
| 私钥保护 | 私钥文件权限设为 600,使用 HSM 存储 |
| 最小权限 | 只授予必要的证书扩展(Key Usage) |
7.10.2 配置建议
// 生产环境 TLS 配置检查清单
nng_tls_config *tls;
nng_tls_config_alloc(&tls, NNG_TLS_MODE_SERVER);
// ✅ 使用强证书
nng_tls_config_own_cert(tls, "server.crt", "server.key", NULL);
// ✅ 设置最低 TLS 版本
nng_tls_config_version(tls, NNG_TLS_1_2, NNG_TLS_1_3);
// ✅ 启用 mTLS(内部服务)
nng_tls_config_auth_mode(tls, NNG_TLS_AUTH_MODE_REQUIRED);
// ✅ 加载 CA 证书
nng_tls_config_ca_file(tls, "ca.crt");
7.11 注意事项
性能影响:TLS 握手会增加延迟(首次连接约 50-200ms),数据传输阶段加密开销约 5-15%。对于高频小消息 RPC,可考虑复用连接。
证书格式:NNG 要求 PEM 格式的证书。DER 格式需要转换:
openssl x509 -inform DER -in cert.der -out cert.pem
私钥密码:如果私钥有密码保护,使用
nng_tls_config_own_cert()的第三个参数传入密码。
7.12 扩展阅读
上一章:第 6 章:可扩展性与性能 | 下一章:第 8 章:IPC 与进程间通信