本章导读

TLS 的对象栈有两层:会话层(xtlssession——裸协议机,输入密文块、输出密文块,传输完全由调用方决定)与 Stream 层(第 87 章——把会话机挂上 TCP 流)。本章讲会话层的服务端侧:xtlsserverconfig 配置身份与 ALPN,xrtTlsServerCreate 建会话,Feed* 喂收到的密文、xrtTlsServerDrive 推进状态机、公共 Send 队列取待发航班——传输是什么(内存桥、TCP、串口、自定义 IoT 链路)协议机毫不关心。服务端特有的三件事也在本章:SNI 动态选择(Select 回调按域名换身份与协议)、票据签发(xrtTlsServerTicket)与恢复接受(Resume 回调,PSK+DHE)。本章示例用"内存搬运"当传输,服务端与真实客户端在进程内对打——这也是协议测试的标准姿势。

86.1引入

什么时候需要裸协议机而不是 Stream 层?三种真实场景。其一:协议测试——你想在内存里同时驱动客户端与服务端、逐字节检查航班,不碰网络。其二:自定义传输——IoT 串口、蓝牙 BLE、甚至消息队列上跑 TLS,传输字节从哪来由你决定。其三:极致控制——网关要把密文按自定义调度转发,Stream 层的事件模型不合身。

会话层的服务端比客户端多一个"等待"维度:客户端创建即发首航,服务端创建后等待 ClientHello——收到什么 SNI、什么 ALPN,可能决定用哪个身份应答(一台服务器服务多个域名的标准场景)。这个"看菜下单"的能力由 Select 回调承载;不需要路由就给静态 Identity,二者必须有一个——保证任何客户端(包括恢复回退的)都有明确认证路径。

86.2概念

会话驱动的三步循环

1Feed把传输收到的密文交给会话(Feed 借用 / FeedTake 拥有 / FeedRef 引用 / FeedBuffer 零拷贝链)
2DrivexrtTlsServerDrive 在记录与握手预算内推进状态机
3Send公共 Send 队列取出待发密文(SendSize/SendFront/SendConsume / SendSpans)

三步循环对客户端与服务端完全同构(客户端用 xrtTlsClientDrive)——掌握一侧另一侧免费。喂入四种形态对应四种所有权(借用/接管/引用/缓冲链——第 65 章的四类追加在这里重演);Send 队列的 Size/Front/Consume 消费三件与第 65 章缓冲链同一套词汇。TLS 1.3 完整证书航班在 Drive 中依次生成 ServerHello、EncryptedExtensions、Certificate、CertificateVerify、Finished(第 84 章时序),验证客户端 Finished 后原子切换到应用 epoch 与 READY。

服务端配置与首航 Arena

xtlsserverconfig 创建期间借用上下文与静态身份,成功后会话持引用并深复制 ALPN 列表。RequireProtocol = true 时没有共同 ALPN 明确失败——协议歧义在握手期消灭。首航的扩展表、证书条目、签名输入与临时编码流来自会话内惰性临时 Arena:空闲连接与未握手会话不分配 Arena 块,首航完成立即安全清零释放——不为每个连接保留固定握手缓冲,1.2/1.3 复用同一 Arena 与清理路径。内存形态对高连接数服务端是关键契约。

SNI 动态选择:Select 回调

Select 在 ClientHello 严格解析、SNI/ALPN 提取之后、任何服务端输出生成之前执行。回调收到请求中的 SNI 与完整 ALPN 负载(仅回调期间借用),xtlsserverchoice 初始含静态身份、按服务端偏好算好的协议下标与零值 Cookie;回调可替换身份或协议,并写入一个 XRT 不解释的 64 位宿主路由标识。xrtTlsServerCookie 在成功选择后返回该标识——传输适配层用它把已握手连接关联回"选择身份时的配置代"(比如选择时查的租户配置版本)。约束:上下文只借用到首航完成;不能递归驱动同一会话。xrtTlsServerName 返回深复制的 SNI(视图稳定到会话销毁)、xrtTlsSessionProtocol 返回最终 ALPN。

票据签发与恢复接受

启用 XRT_FEATURE_TLS_SERVER_RESUME 后服务端两侧接入恢复:

  • 签发:READY 后 xrtTlsServerTicket(会话, 不透明ticket, 寿命, &恢复对象) 把调用方给的 ticket 编码成 NewSessionTicket 发送,并把对应服务端恢复对象的所有权交给调用方;TicketNew 便捷入口用 32 字节安全随机 ticket 与 86400 秒默认寿命。XRT 不维护进程全局票据缓存——按租户/容量/过期/持久化自选 Map(第 18 章)、分片缓存或外部存储。发送硬上限在随机数与派生前预检;XTLS_AGAIN 时输出对象为 NULL、写序号与恢复状态不变——排空后原样重试。
  • 接受:Resume 回调收到 SNI、ALPN、不透明 ticket 与混淆年龄,返回借用的不可变 xtlsresume(第 88 章的对象契约);会话加引用后校验版本/套件/SNI/ALPN/有效期/年龄容差。未找到票据或路由不匹配安全回退完整握手;但"票据元数据匹配而 binder 错误"是认证失败——必须 fatal Alert,不能降级绕过(攻击者拿不到 PSK 就该被拒)。恢复始终保留 ECDHE(PSK+DHE,前向保密不降级),不支持纯 PSK 与 0-RTT。

TLS 1.2 路径与 KeyUpdate

服务端 1.2 路径要求 EMS,执行 ECDHE 证书完整握手、在 ChangeCipherSpec 边界原子切换 epoch;无恢复、无重新协商、无 KeyUpdate(1.2 的历史决定)。1.3 的主动/被动 KeyUpdate 都遵循"旧 epoch 完整排队、新 epoch 一次提交"——发送背压与分配失败不产生半更新态。

86.3示例

第一个完整程序:传输无关的服务端对打

下面的程序来自 examples/tls/server/main.c——服务端与真实客户端在内存桥上完成握手与双向数据:

examples/tls/server/main.c源码 ↗
#include "../common.h"



/* 范例验证器只接管信任决策,协议签名和 Finished 仍由 XRT 验证。 */
static xtlsverifydecision exampleTlsAccept(
	const xtlspeer* pPeer,
	ptr pContext
)
{
	(void)pContext;
	return ((pPeer != NULL) && (pPeer->Role == XTLS_SERVER) &&
		(pPeer->CertificateCount != 0)) ?
		XTLS_VERIFY_ACCEPT : XTLS_VERIFY_REJECT;
}



/* 把一个会话的全部待发密文复制到对端并精确消费。 */
static bool exampleTlsMove(xtlssession* pSource, xtlssession* pTarget)
{
	xnetspan Span;

	while ( xrtTlsSessionSendSize(pSource) != 0 ) {
		if ( !xrtTlsSessionSendFront(pSource, &Span) ||
			(Span.Size == 0) || (xrtTlsSessionFeed(
				pTarget, Span.Data, Span.Size
			) != XTLS_OK) || !xrtTlsSessionSendConsume(
				pSource, Span.Size
			) ) {
			return false;
		}
	}
	return true;
}



/* 驱动双方并搬运密文,直到会话同时进入 READY。 */
static bool exampleTlsHandshake(
	xtlssession* pClient,
	xtlssession* pServer
)
{
	for ( size_t i = 0; i < 4096u; i++ ) {
		xtlsresult ClientResult = xrtTlsClientDrive(pClient);
		xtlsresult ServerResult = xrtTlsServerDrive(pServer);

		if ( ((ClientResult != XTLS_OK) &&
			 (ClientResult != XTLS_AGAIN)) ||
			((ServerResult != XTLS_OK) &&
			 (ServerResult != XTLS_AGAIN)) ||
			!exampleTlsMove(pClient, pServer) ||
			!exampleTlsMove(pServer, pClient) ) {
			return false;
		}
		if ( (xrtTlsSessionState(pClient) == XTLS_STATE_READY) &&
			(xrtTlsSessionState(pServer) == XTLS_STATE_READY) ) {
			return true;
		}
	}
	return false;
}



/* 发送一段应用数据,并在对端驱动后完整读取。 */
static bool exampleTlsTransfer(
	xtlssession* pSource,
	xtlssession* pTarget,
	bool bTargetServer,
	cstr sText
)
{
	char Output[64];
	size_t iSize = strlen(sText);
	size_t iWritten = 0;
	size_t iRead = 0;
	xtlsresult Result;

	if ( (iSize > sizeof(Output)) || (xrtTlsSessionWrite(
		pSource, sText, iSize, &iWritten
	) != XTLS_OK) || (iWritten != iSize) ||
		!exampleTlsMove(pSource, pTarget) ) {
		return false;
	}
	Result = bTargetServer ?
		xrtTlsServerDrive(pTarget) : xrtTlsClientDrive(pTarget);
	if ( (Result != XTLS_OK) && (Result != XTLS_AGAIN) ) {
		return false;
	}
	return (xrtTlsSessionRead(
		pTarget, Output, sizeof(Output), &iRead
	) == XTLS_OK) && (iRead == iSize) &&
		(memcmp(Output, sText, iSize) == 0);
}



/* 打印当前线程保存的结构化根错误。 */
static void exampleTlsError(void)
{
	const xerror* pError = xrtGetError();

	if ( pError != NULL ) {
		fprintf(
			stderr, "%s: %s\n",
			xrtErrorOperation(pError), xrtErrorMessage(pError)
		);
	}
}
























 */


/* 用真实客户端演示传输无关 TLS 1.3 服务端握手与双向数据。 */
int main(int argc, char** argv)
{
	static const xstrview Protocols[] = {
		XRT_STR_INIT("h2"),
		XRT_STR_INIT("http/1.1")
	};
	uint8* pCertificateData = NULL;
	uint8* pPrivateData = NULL;
	size_t iCertificateSize = 0;
	size_t iPrivateSize = 0;
	xbytesview Certificate;
	xtlsidentity* pIdentity = NULL;
	xtlsverifierconfig VerifierConfig;
	xtlsverifier* pVerifier = NULL;
	xtlsclientconfig ClientConfig;
	xtlsserverconfig ServerConfig;
	xtlssession* pClient = NULL;
	xtlssession* pServer = NULL;
	xbytesview Protocol;
	int iResult = 1;

	if ( argc != 4 ) {
		printf(
			"usage: server <rsa|p256|p384|ed25519> "
			"<certificate.der> <private.der>\n"
		);
		return 0;
	}
	if ( !exampleTlsReadFile(
		argv[2], &pCertificateData, &iCertificateSize
	) || !exampleTlsReadFile(
		argv[3], &pPrivateData, &iPrivateSize
	) ) {
		fprintf(stderr, "failed to read DER identity\n");
		goto Cleanup;
	}
	Certificate = (xbytesview) {
		pCertificateData, iCertificateSize
	};
	pIdentity = exampleTlsIdentity(
		argv[1], &Certificate, 1u,
		(xbytesview) { pPrivateData, iPrivateSize }
	);
	if ( pIdentity == NULL ) {
		exampleTlsError();
		goto Cleanup;
	}
	xrtTlsVerifierConfigInit(&VerifierConfig);
	VerifierConfig.Verify = exampleTlsAccept;
	pVerifier = xrtTlsVerifierCreate(&VerifierConfig);
	if ( pVerifier == NULL ) {
		exampleTlsError();
		goto Cleanup;
	}
	xrtTlsClientConfigInit(&ClientConfig);
	ClientConfig.ServerName = XRT_STR_LITERAL("example.com");
	ClientConfig.Protocols = Protocols;
	ClientConfig.ProtocolCount = sizeof(Protocols) / sizeof(Protocols[0]);
	ClientConfig.Verifier = pVerifier;
	xrtTlsServerConfigInit(&ServerConfig);
	ServerConfig.Identity = pIdentity;
	ServerConfig.Protocols = Protocols;
	ServerConfig.ProtocolCount = sizeof(Protocols) / sizeof(Protocols[0]);
	ServerConfig.RequireProtocol = true;
	pClient = xrtTlsClientCreate(&ClientConfig, NULL);
	pServer = xrtTlsServerCreate(&ServerConfig, NULL);
	if ( (pClient == NULL) || (pServer == NULL) ||
		!exampleTlsHandshake(pClient, pServer) ||
		!exampleTlsTransfer(pClient, pServer, true, "ping") ||
		!exampleTlsTransfer(pServer, pClient, false, "pong") ||
		!xrtTlsSessionProtocol(pServer, &Protocol) ) {
		exampleTlsError();
		goto Cleanup;
	}
	printf(
		"ready, protocol=%.*s, client=ping, server=pong\n",
		(int)Protocol.Size, (const char*)Protocol.Data
	);
	iResult = 0;

Cleanup:
	xrtTlsSessionDestroy(pServer);
	xrtTlsSessionDestroy(pClient);
	xrtTlsVerifierRelease(pVerifier);
	xrtTlsIdentityRelease(pIdentity);
	free(pPrivateData);
	free(pCertificateData);
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/tls/server/main.c -lws2_32 -liphlpapiusage: server <rsa|p256|p384|ed25519> ...

刚才发生了什么。 ① 身份与验证器装配:DER 文件读入后 exampleTlsIdentity 构造身份(第 83 章的启动期验证);验证器用最小 Verify 回调(对打场景信任自己)。② 双会话创建:ClientCreate 与 ServerCreate 各拿一份配置——服务端配 Identity + Protocols + RequireProtocol;两边会话都不碰任何 socket。③ 内存桥握手:exampleTlsHandshake 是会话层驱动循环的范本——ClientDrive + ServerDrive 交替推进,exampleTlsMove 把一边 Send 队列的密文逐 Span Feed 给对端——"传输"就是这两个循环之间的搬运。接真实网络时把 Move 换成 socket 读写,其余不变。④ 双向数据:exampleTlsTransfer 演示应用明文的 SessionWrite/Read(同样经 Drive 与 Move 的循环);最后 SessionProtocol 查询协商出的 ALPN——RequireProtocol 保证它非空。⑤ 清理段两会话 Destroy → 验证器与身份 Release → 文件缓冲 free——会话层对象全是"Destroy/Release 一次"的简单生命周期。

第二个完整程序:会话层全特性巡检

第二个程序来自 examples/tls/session_tour/main.c——公共会话 API 的六行自检:

examples/tls/session_tour/main.c源码 ↗



























 */

#include <stdio.h>
#include <string.h>
#include <xrt.h>
#include "../embedded_identity.h"

/* FeedRef 的释放回调:记录次数。 */
static void exampleRelease(ptr pContext, cbytes pData, size_t iSize)
{
	int* pCount = (int*)pContext;

	(void)pData;
	(void)iSize;
	++(*pCount);
}

/* 验证器回调:只接管信任决策,协议签名仍由 XRT 验证。 */
static xtlsverifydecision exampleAccept(
	const xtlspeer* pPeer,
	ptr pContext
)
{
	(void)pContext;
	return ((pPeer != NULL) && (pPeer->CertificateCount != 0u)) ?
		XTLS_VERIFY_ACCEPT : XTLS_VERIFY_REJECT;
}

/* 用 SendSpans 聚集全部待发密文,按指定形态喂给对端。 */
typedef enum examplefeed {
	EXAMPLE_FEED_BORROW,
	EXAMPLE_FEED_TAKE,
	EXAMPLE_FEED_REF,
	EXAMPLE_FEED_BUFFER
} examplefeed;

/* 按角色驱动会话推进。 */
static xtlsresult exampleDrive(xtlssession* pSession)
{
	return (xrtTlsSessionRole(pSession) == XTLS_SERVER) ?
		xrtTlsServerDrive(pSession) :
		xrtTlsClientDrive(pSession);
}

static bool exampleMove(
	xtlssession* pSource,
	xtlssession* pTarget,
	examplefeed Kind,
	int* pReleased
)
{
	xnetspan Spans[8];
	xnetbuf Chain;

	
	 * 顺序固定为:喂入 → 驱动对端 → 消费源 Span。 */
	while ( xrtTlsSessionSendSize(pSource) != 0u ) {
		xtlsresult Result;

		if ( (xrtTlsSessionSendSpans(pSource, Spans, 8u) == 0u) ||
			(Spans[0].Size == 0u) ) {
			return false;
		}
		switch ( Kind ) {
			case EXAMPLE_FEED_BORROW:
				Result = xrtTlsSessionFeedBorrow(pTarget,
					Spans[0].Data, Spans[0].Size);
				break;
			case EXAMPLE_FEED_TAKE: {
				ptr pCopy = xrtMalloc(Spans[0].Size);

				if ( pCopy == NULL ) {
					return false;
				}
				memcpy(pCopy, Spans[0].Data, Spans[0].Size);
				Result = xrtTlsSessionFeedTake(pTarget,
					pCopy, Spans[0].Size);
				break;
			}
			case EXAMPLE_FEED_REF:
				Result = xrtTlsSessionFeedRef(pTarget,
					Spans[0].Data, Spans[0].Size,
					exampleRelease, (ptr)pReleased);
				break;
			case EXAMPLE_FEED_BUFFER:
			default:
				if ( !xrtNetBufInit(&Chain, NULL) ||
					!xrtNetBufAppend(&Chain, Spans[0].Data,
						Spans[0].Size) ) {
					return false;
				}
				Result = xrtTlsSessionFeedBuffer(pTarget,
					&Chain);
				break;
		}
		if ( Result != XTLS_OK ) {
			return false;
		}
		Result = exampleDrive(pTarget);
		if ( (Result != XTLS_OK) && (Result != XTLS_AGAIN) ) {
			return false;
		}
		if ( !xrtTlsSessionSendConsume(pSource,
				Spans[0].Size) ) {
			return false;
		}
	}
	return true;
}

/* 双向驱动 + 搬运,直到双方 READY。 */
static bool exampleHandshake(
	xtlssession* pClient,
	xtlssession* pServer,
	int* pReleased
)
{
	for ( size_t i = 0; i < 4096u; i++ ) {
		xtlsresult C = xrtTlsClientDrive(pClient);
		xtlsresult S = xrtTlsServerDrive(pServer);

		if ( ((C != XTLS_OK) && (C != XTLS_AGAIN)) ||
			((S != XTLS_OK) && (S != XTLS_AGAIN)) ||
			!exampleMove(pClient, pServer, EXAMPLE_FEED_BORROW,
				pReleased) ||
			!exampleMove(pServer, pClient, EXAMPLE_FEED_BORROW,
				pReleased) ) {
			return false;
		}
		/* exampleMove 内部已驱动对端;此处再补一轮源侧驱动。 */
		if ( (xrtTlsSessionState(pClient) == XTLS_STATE_READY) &&
			(xrtTlsSessionState(pServer) == XTLS_STATE_READY) ) {
			return true;
		}
	}
	return false;
}

/* 单向:写应用数据 → 搬运 → 驱动对端。 */
static bool exampleSend(
	xtlssession* pSource,
	xtlssession* pTarget,
	bool bTargetServer,
	cstr sText,
	examplefeed Kind,
	int* pReleased
)
{
	size_t iWritten = 0;
	xtlsresult Result;

	(void)bTargetServer;
	(void)Result;
	if ( (xrtTlsSessionWrite(pSource, sText, strlen(sText),
			&iWritten) != XTLS_OK) ||
		(iWritten != strlen(sText)) ||
		!exampleMove(pSource, pTarget, Kind, pReleased) ) {
		return false;
	}
	return true;
}

int main(void)
{
	xtlsidentity* pIdentity = NULL;
	xtlsverifier* pVerifier = NULL;
	xtlsclientconfig ClientConfig;
	xtlsserverconfig ServerConfig;
	xtlssession* pClient = NULL;
	xtlssession* pServer = NULL;
	xnetspan Spans[4];
	int iReleased = 0;
	int iResult = 1;

	/* ---- 建立双会话:嵌入证书 + 接受式验证器。 ---- */
	pIdentity = exampleEmbeddedIdentity();
	{
		xtlsverifierconfig VerifierConfig;

		xrtTlsVerifierConfigInit(&VerifierConfig);
		VerifierConfig.Verify = exampleAccept;
		pVerifier = xrtTlsVerifierCreate(&VerifierConfig);
	}
	if ( (pIdentity == NULL) || (pVerifier == NULL) ) {
		goto Cleanup;
	}
	xrtTlsClientConfigInit(&ClientConfig);
	ClientConfig.ServerName = XRT_STR_LITERAL("localhost");
	ClientConfig.VerifyName = XRT_STR_LITERAL("localhost");
	ClientConfig.Verifier = pVerifier;
	xrtTlsServerConfigInit(&ServerConfig);
	ServerConfig.Identity = pIdentity;
	pClient = xrtTlsClientCreate(&ClientConfig, NULL);
	pServer = xrtTlsServerCreate(&ServerConfig, NULL);
	if ( (pClient == NULL) || (pServer == NULL) ||
		!exampleHandshake(pClient, pServer, &iReleased) ||
		(xrtTlsSessionFeedSize(pClient) != 0u) ) {
		goto Cleanup;
	}
	printf("session: handshake via feed-borrow -> both ready ok\n");

	/* ---- 会话属性:角色/版本/套件/事件位/上下文。 ---- */
	if ( (xrtTlsSessionRole(pClient) != XTLS_CLIENT) ||
		(xrtTlsSessionRole(pServer) != XTLS_SERVER) ||
		(xrtTlsSessionVersion(pClient) != XTLS_VERSION_13) ||
		(xrtTlsSessionCipher(pClient) == 0u) ||
		(xrtTlsSessionWait(pClient) == 0u) ||
		/* 未显式配置 Context 时会话仍挂在共享默认上下文上。 */
		(xrtTlsSessionContext(pClient) == NULL) ||
		(xrtTlsSessionContext(pServer) == NULL) ) {
		goto Cleanup;
	}
	printf("session: role/version/cipher/wait/context ok\n");

	/* ---- Take/Ref 喂入:接管与释放回调恰好一次。 ---- */
	if ( !exampleSend(pClient, pServer, true, "take",
			EXAMPLE_FEED_TAKE, &iReleased) ||
		(xrtTlsSessionPlainSize(pServer) != 4u) ||
		!exampleSend(pClient, pServer, true, "ref!",
			EXAMPLE_FEED_REF, &iReleased) ) {
		goto Cleanup;
	}
	{
		/* 驱动消费 Ref 喂入的数据后释放回调恰好一次。 */
		for ( int i = 0; (i < 100) && (iReleased == 0); i++ ) {
			(void)xrtTlsServerDrive(pServer);
			xrtSleep(1u);
		}
		if ( iReleased != 1 ) {
			goto Cleanup;
		}
	}
	printf("session: feed-take + feed-ref (release once) ok\n");

	/* ---- Buffer 零复制:密文缓冲链整体移交。 ---- */
	if ( !exampleSend(pServer, pClient, false, "chain",
			EXAMPLE_FEED_BUFFER, &iReleased) ||
		(xrtTlsSessionPlainSize(pClient) != 5u) ) {
		goto Cleanup;
	}
	printf("session: feed-buffer zero-copy chain ok\n");

	/* ---- 明文队列零复制检查 + 消费。 ---- */
	{
		char arrText[16];
		size_t iCount;
		size_t iSize;

		/* 发送侧 Span 计数与聚集同族——上一轮 Move 已清空。 */
		if ( (xrtTlsSessionSendSpanCount(pClient) != 0u) ) {
			goto Cleanup;
		}
		iSize = xrtTlsSessionPlainSize(pClient);
		iCount = xrtTlsSessionPlainSpanCount(pClient);
		if ( (iSize != 5u) || (iCount == 0u) ||
			!xrtTlsSessionPlainFront(pClient, &Spans[0]) ||
			(Spans[0].Size == 0u) ||
			(xrtTlsSessionPlainSpans(pClient, Spans, 4u) !=
				iCount) ) {
			goto Cleanup;
		}
		memcpy(arrText, Spans[0].Data,
			Spans[0].Size < 5u ? Spans[0].Size : 5u);
		/* 前缀可能分散在多个 Span——按总长复制拼合。 */
		{
			size_t iOffset = 0;

			for ( size_t i = 0; (i < iCount) && (iOffset < 5u);
				i++ ) {
				size_t n = Spans[i].Size;

				if ( n > 5u - iOffset ) {
					n = 5u - iOffset;
				}
				memcpy(arrText + iOffset, Spans[i].Data, n);
				iOffset += n;
			}
			if ( (iOffset != 5u) ||
				(memcmp(arrText, "chain", 5u) != 0) ||
				!xrtTlsSessionPlainConsume(pClient, 5u) ||
				(xrtTlsSessionPlainSize(pClient) != 0u) ) {
				goto Cleanup;
			}
		}
	}
	printf("session: send spans gathered + plain spans consumed ok\n");

	/* ---- 关闭:Close 排队 close_notify,对端驱动后 Eof。 ---- */
	if ( xrtTlsSessionClose(pClient) != XTLS_OK ) {
		goto Cleanup;
	}
	if ( !exampleMove(pClient, pServer, EXAMPLE_FEED_BORROW,
			&iReleased) ) {
		goto Cleanup;
	}
	{
		xtlsresult Result = xrtTlsServerDrive(pServer);

		if ( (Result != XTLS_OK) && (Result != XTLS_AGAIN) ) {
			goto Cleanup;
		}
	}
	{
		xtlsalertlevel Level;
		xtlsalert Alert;

		
		 * 干净关闭后 PeerAlert 为真且代码恰为 close_notify。 */
		if ( (xrtTlsSessionEof(pServer) != XTLS_OK) ||
			!xrtTlsSessionPeerAlert(pServer, &Level, &Alert) ||
			(Level != XTLS_ALERT_WARNING) ||
			(Alert != XTLS_ALERT_CLOSE_NOTIFY) ) {
			goto Cleanup;
		}
	}
	printf("session: close_notify -> peer eof ok\n");
	iResult = 0;

Cleanup:
	xrtTlsSessionDestroy(pServer);
	xrtTlsSessionDestroy(pClient);
	xrtTlsVerifierRelease(pVerifier);
	xrtTlsIdentityRelease(pIdentity);
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -I examples/tls -include xrt.h impl.c examples/tls/session_tour/main.c -lws2_32 -liphlpapisession: handshake via feed-borrow -> both ready oksession: role/version/cipher/wait/context oksession: feed-take + feed-ref (release once) oksession: feed-buffer zero-copy chain oksession: send spans gathered + plain spans consumed oksession: close_notify -> peer eof ok

刚才发生了什么。 ① 第 1 行是 server 示例同款的内存桥握手(借用形态 Feed);第 3 行把另外三种喂入形态逐一验证——Take(接管所有权)、Ref(加引用喂、恰好释放一次)、Buffer(第 65 章缓冲链零拷贝直挂)——四种所有权全覆盖。② 第 2 行查询会话公开事实:角色(客户端/服务端同一对象类型)、协商版本与套件、等待原因(输入还是输出——XTLS_AGAIN 的两态分解)、共享上下文。③ 第 5 行验证双向 Span:发送侧 Span 族聚合发送、接收侧明文按 Span 消费——应用层的零拷贝词汇延伸到会话边界。④ 第 6 行走认证关闭:close_notify 发出、对端收到 EOF——第 82 章的关闭协议在会话层的样子。六行合起来就是"会话层这一章的全部 API 在跑"——把它当本章的可执行速查表。

86.4契约

  • 身份强制:静态 Identity 或同步 Select 至少一个——未知 SNI 与恢复回退仍有明确认证路径。
  • 驱动同构:Feed 四形态(借用/Take/Ref/Buffer 零拷贝)+ Drive + Send 队列,客户端服务端共享同一套公共会话 API。
  • 原子切换:1.3 在客户端 Finished 验证后原子切应用 epoch 与 READY;1.2 在 CCS 边界原子切换;失败不发布半状态。
  • Arena 惰性:首航临时内存按需分配、用后安全清零释放;空闲连接零握手缓冲。
  • Select 时机:ClientHello 严格解析后、任何输出前;SNI/ALPN 仅回调期间借用;Cookie 为 64 位宿主路由标识(XRT 不解释);上下文借到首航完成;禁止递归驱动。
  • 票据签发:对象所有权转移给调用方;无全局缓存;AGAIN 时状态不变可重试;硬上限预检。
  • 恢复接受:未找到/路由不匹配回退完整握手;binder 错误是认证失败(fatal,不降级);始终 PSK+DHE;无纯 PSK/0-RTT。
  • 1.2 边界:EMS 必须;无恢复/重协商/KeyUpdate。
  • KeyUpdate:旧 epoch 完整排队、新 epoch 一次提交;背压与 OOM 无半更新。
  • 未实现:客户端证书认证、0-RTT、异步身份选择;TCP 组合入口属第 87 章 Stream 层。

常见错误

坑 1:Select 回调里驱动会话(递归 Drive)

症状:Select 回调里顺手调 xrtTlsServerDrive 想"提前推进"——状态错乱或断言失败。

原因:Select 运行在 Drive 内部(首航生成前);此时递归 Drive 同一会话等于重入状态机。回调的职责只是"选身份、选协议、写 Cookie",推进留给外层循环。

错误写法
static void onSelect(xtlsserverchoice* pChoice, ...) {
	pick_identity(pChoice);
	xrtTlsServerDrive(pSession);   /* 重入状态机:未定义行为 */
}
正确写法
static void onSelect(xtlsserverchoice* pChoice, ...) {
	pick_identity(pChoice);        /* 只做选择 */
	pChoice->Cookie = tenant_generation();  /* 路由标识 */
	/* 推进由外层 feed/drive/send 循环完成 */
}
坑 2:把"票据未找到"与"binder 错误"都当回退处理

症状:为了"高可用",Resume 回调查不到票据返回回退、binder 校验失败也回退完整握手——攻击者伪造票据元数据即可绕过 PSK 认证探测。

原因:两类失败的安全语义完全不同。未找到/路由不匹配:客户端可能持旧票据,正常业务路径,回退合理。binder 错误:客户端声称持有某票据的 PSK 却证明不了——要么票据被盗要么就是攻击,必须 fatal Alert。

错误写法
static const xtlsresume* onResume(...) {
	const xtlsresume* pFound = cache_lookup(Ticket);
	if ( (pFound == NULL) || !quick_check(Ticket) ) {
		return NULL;   /* 一律回退:binder 错误也被吞掉 */
	}
	return pFound;
}
正确写法



   你要做的是不要在回调里伪造"找到" */
坑 3:高连接数下给每连接配固定握手缓冲

症状:万级空闲连接的服务端内存暴涨——每连接几百 KB 的"预分配握手区"乘以连接数。

原因:误解了会话层的内存模型。首航临时内存是惰性 Arena:未握手不分配、握手完立即清零释放;需要长期驻留的只有会话状态本身。

错误写法
/* 自行给每个连接 malloc 一块"握手工作区"挂着 */
struct conn { uint8 HandshakeArena[262144]; ... };  /* 全部白占 */
正确写法
   闲置连接近零握手内存,容量规划只看活跃握手数 */
pSession = xrtTlsServerCreate(&Config, pPool);

编程练习

基础:对打全流程

用 OpenSSL 生成 Ed25519 自签证书导出 DER,跑通 server ed25519 cert.der key.der——确认输出 ready, protocol=h2, client=ping, server=pong(ALPN 首选项命中)。把 RequireProtocol 换 false 再改 Protocols 只留 h2,观察协议下标的选择变化。验收标准:两种配置的协商协议与手推一致。

进阶:双域名 SNI 路由

实现 Select 回调:准备两套身份(自签两个域名的证书),按 ClientHello 的 SNI 选择对应身份;Cookie 写入"租户代次"并在 READY 后用 xrtTlsServerCookie 读回对比。验收标准:两个 SNI 各自拿到正确证书;Cookie 读写一致;未知 SNI 走你定义的默认策略(静态身份或明确失败)。

挑战:内存桥上的分片压力测试

把 exampleTlsMove 的"整 Span 搬运"改成"每次最多搬 7 字节"的随机分片——模拟恶劣传输。统计握手完成的 Drive 轮数上限。验收标准:任意分片大小(1 到整条)握手都成功(协议机支持跨记录重组——第 84 章);最坏轮数有界并可解释(航班大小 ÷ 分片大小量级);无内存增长(第 6 章统计)。

小结

知识点速查
会话层定位裸协议机:Feed 密文 / Drive 推进 / Send 队列取航班,传输无关
驱动循环Feed×4(借用/Take/Ref/Buffer)→ Drive → Send(Size/Front/Consume/Spans)
服务端配置Identity 或 Select 必须一个;ALPN 深复制;RequireProtocol 歧义即失败
首航 Arena惰性分配、用后清零释放;空闲连接零握手缓冲;1.2/1.3 共用路径
SelectClientHello 解析后、输出前;SNI/ALPN 回调期借用;Cookie=64 位路由标识
查询ServerName(深复制 SNI)/ SessionProtocol(最终 ALPN)/ ServerCookie
票据签发Ticket/TicketNew;对象所有权转移;无全局缓存;AGAIN 状态不变
恢复接受未找到回退完整握手;binder 错误 fatal 不降级;PSK+DHE;无 0-RTT
1.2 边界EMS 必须;无恢复/重协商/KeyUpdate
同构性客户端与服务端共享公共会话 API——学一侧得两侧

📖 本章涉及的 API 参考手册:tls(201 个条目)tls_verify(17 个条目)tls_server(16 个条目)crypto(140 个条目)

点击查看完整签名、参数约束、返回值与错误说明