强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 引擎对比

特性mbedTLSOpenSSL
体积小 (~100KB)大 (~1MB)
许可证Apache 2.0Apache 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 与进程间通信