Ориентация главы

Объектный стек TLS двухслойен: слой сессий (xtlssession — голая протокольная машина: вход — блоки шифртекста, выход — блоки шифртекста, транспорт целиком решает вызывающий) и слой Stream (глава 87 — сажает сессионную машину на TCP-поток). Эта глава — серверная сторона слоя сессий: xtlsserverconfig конфигурирует идентичность и ALPN, xrtTlsServerCreate строит сессию, Feed* кормит принятым шифртекстом, xrtTlsServerDrive продвигает машину состояний, общая Send-очередь отдаёт ожидающие рейсы — что такое транспорт (мост в памяти, TCP, последовательный порт, собственная IoT-цепочка), протокольной машине безразлично. Три сервер-специфичные вещи также здесь: динамический выбор по SNI (колбэк Select меняет идентичность и протокол по домену), подпись билетов (xrtTlsServerTicket) и принятие восстановления (колбэк Resume, PSK+DHE). Образец главы берёт «переноску в памяти» за транспорт — сервер и настоящий клиент спаррингуют в одном процессе; это же стандартная поза протокольного тестирования.

86.1引入

Когда нужна голая протокольная машина, а не слой Stream? Три реальные сцены. Первая: протокольное тестирование — вести клиент и сервер одновременно в памяти, проверять рейсы побайтно, не трогая сеть. Вторая: собственный транспорт — TLS поверх последовательного порта IoT, BLE или даже очереди сообщений; откуда берутся байты транспорта, решаете вы. Третья: предельный контроль — шлюз пересылает шифртекст по собственному расписанию; событийная модель слоя 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.

Конфигурация сервера и арена первого рейса

xtlsserverconfig заимствует контекст и статическую идентичность на время создания; по успеху сессия держит ссылку и глубоко копирует список ALPN. При RequireProtocol = true отсутствие общего ALPN — явная неудача: протокольная двусмысленность истребляется в рукопожатии. Расширенная таблица, записи сертификатов, подписные входы и временные потоки кодирования первого рейса происходят из ленивой временной арены внутри сессии: простаивающие соединения и не-рукопожатийные сессии не выделяют блоков арены; первый рейс завершён — немедленное безопасное обнуление и освобождение — никакого фиксированного буфера рукопожатия на соединение; 1.2/1.3 делят одну арену и путь очистки. Форма памяти — ключевой контракт для серверов с огромным числом соединений.

Динамический выбор по SNI: колбэк Select

Select исполняется после строгого разбора ClientHello и извлечения SNI/ALPN, но до генерации любого выхода сервера. Колбэк получает SNI запроса и полную нагрузку ALPN (заимствованы лишь на время колбэка); xtlsserverchoice изначально содержит статическую идентичность, посчитанный по предпочтениям сервера индекс протокола и нулевой Cookie; колбэк может сменить идентичность или протокол и записать 64-битный хостовый идентификатор маршрутизации, который XRT не интерпретирует. 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); сессия добавляет ссылку и проверяет версию/suite/SNI/ALPN/срок/допуск возраста. Билет не найден или маршрутизация не совпала — безопасный откат в полное рукопожатие; но «метаданные билета совпал, а binder ошибочен» — провал аутентификации: обязателен fatal Alert, даунгрейдом не обойти (у атакующего нет PSK — его и должно отвергнуть). Восстановление всегда сохраняет ECDHE (PSK+DHE, прямая секретность не деградирует); чистый PSK и 0-RTT не поддерживаются.

Путь TLS 1.2 и KeyUpdate

Серверный путь 1.2 требует EMS, исполняет полный сертификатный ECDHE-хендшейк и атомарно переключает epoch на границе ChangeCipherSpec; без восстановления, без повторного negociación, без KeyUpdate (историческое решение 1.2). Активный и пассивный KeyUpdate 1.3 оба следуют «старый 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> ...

Что только что произошло. (1) Сборка идентичности и верификатора: DER-файлы прочитаны, exampleTlsIdentity строит идентичность (проверка на старте из главы 83); верификатор — минимальный Verify-колбэк (в спарринге доверяем себе). (2) Создание двух сессий: ClientCreate и ServerCreate получают по конфигурации — серверная несёт Identity + Protocols + RequireProtocol; обе сессии не трогают ни одного сокета. (3) Рукопожатие через мост в памяти: exampleTlsHandshake — эталон цикла управления слоя сессий: ClientDrive + ServerDrive продвигаются попеременно, exampleTlsMove Span за Span пересыпает шифртекст из Send-очереди одной стороны в Feed другой — «транспорт» и есть переноска между этими двумя циклами. Подключая настоящую сеть, замените Move на чтение-запись сокета, остальное без изменений. (4) Двусторонние данные: exampleTlsTransfer демонстрирует открытый текст приложения через SessionWrite/Read (тем же циклом Drive и Move); напоследок SessionProtocol запрашивает negociación-ный ALPN — RequireProtocol гарантирует непустоту. (5) Уборка: две сессии 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) Строка 1 — тот же мост-в-памяти рукопожатия, что у образца server (форма заимствования Feed); строка 3 проверяет поочерёдно остальные три формы — Take (принятие владения), Ref (кормление со ссылкой, освобождение ровно раз), Buffer (нуль-копийная прямая навеска буферной цепи главы 65) — четыре владения покрыты все. (2) Строка 2 запрашивает публичные факты сессии: роль (клиент/сервер — один тип объекта), negociación-ные версию и suite, причину ожидания (вход или выход — двусторонняя декомпозиция XTLS_AGAIN), разделяемый контекст. (3) Строка 5 проверяет двусторонние Span: отправляющая сторона собирает отправку семейством Span, принимающая потребляет открытый текст по Span — нуль-копийный словарь приложения дотянут до границы сессии. (4) Строка 6 идёт аутентифицированным закрытием: close_notify отправлен, партнёр получил EOF — облик протокола закрытия главы 82 на слое сессий. Шесть строк вместе — «весь API главы слоя сессий в беге»; держите их за исполняемую шпаргалку главы.

86.4契约

  • Принудительность идентичности: статическая Identity или синхронный Select — хотя бы одно; у неизвестного SNI и отката восстановления всё равно явный путь аутентификации.
  • Изоморфизм управления: четыре формы Feed (заимствование/Take/Ref/Buffer нуль-копийно) + Drive + Send-очередь — клиент и сервер делят один публичный API сессий.
  • Атомарность переключения: 1.3 атомарно режет epoch приложения и READY после проверки клиентского Finished; 1.2 атомарно на границе CCS; неудача не публикует полусостояний.
  • Ленивость арены: временная память первого рейса выделяется по надобности и безопасно обнуляется-освобождается по использовании; у простаивающего соединения ноль буферов рукопожатия.
  • Момент Select: после строгого разбора ClientHello, до любого выхода; SNI/ALPN заимствованы лишь на время колбэка; Cookie — 64-битный хостовый идентификатор маршрутизации (XRT не трактует); контекст заимствован до завершения первого рейса; рекурсивное ведение запрещено.
  • Подпись билетов: владение объектом переносится вызывающему; глобального кеша нет; при AGAIN состояние неизменно и повтор допустим; жёсткий потолок проверяется заранее.
  • Принятие восстановления: не найдено / маршрутизация не совпала — откат в полное рукопожатие; ошибка binder — провал аутентификации (fatal, без даунгрейда); всегда PSK+DHE; без чистого PSK/0-RTT.
  • Границы 1.2: EMS обязателен; без восстановления/повторного negociación/KeyUpdate.
  • KeyUpdate: старый epoch полностью в очередь, новый — одной подачей; противодавление и OOM без полуобновления.
  • Нереализованное: аутентификация клиентскими сертификатами, 0-RTT, асинхронный выбор идентичности; TCP-комбинированный вход — слой Stream главы 87.

Типичные ошибки

Ловушка 1: ведение сессии внутри Select-колбэка (рекурсивный Drive):Ловушка 1: ведение сессии внутри Select-колбэка (рекурсивный Drive)

Симптом: в Select-колбэке «заодно» позвали xrtTlsServerDrive, желая «продвинуть заранее» — перепутанные состояния или провал assert.

Причина: 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» оба обработаны как откат:Ловушка 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: фиксированный буфер рукопожатия на каждое соединение при огромном их числе:Ловушка 3: фиксированный буфер рукопожатия на каждое соединение при огромном их числе

Симптом: память сервера с десятками тысяч простаивающих соединений распухает — по несколько сотен КБ «предвыделённой зоны рукопожатия» на каждое, умноженные на их число.

Причина: неверно понята модель памяти слоя сессий. Временная память первого рейса — ленивая арена: не-рукопожатие не выделяет, рукопожатие завершено — немедленное обнуление и освобождение; долгому проживанию подлежит лишь само состояние сессии.

错误写法
/* самому 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, пронаблюдайте смену выбора индекса протокола. Критерий приёмки: negociación-ный протокол обеих конфигураций совпадает с ручной выкладкой.

Продвнутое: SNI-маршрутизация двух доменов

Реализуйте колбэк Select: приготовьте два комплекта идентичностей (самоподписанные сертификаты двух доменов), выбирайте соответствующую по SNI из ClientHello; Cookie запишите «поколением тенанта» и после READY прочитайте обратно xrtTlsServerCookie для сравнения. Критерии приёмки: оба SNI получают верные сертификаты; запись-чтение Cookie согласованы; неизвестный SNI идёт вашей default-политике (статическая идентичность или явная неудача).

Вызов: стресс фрагментации на мосту в памяти

Поменяйте «переноску целыми Span» в exampleTlsMove на случайную фрагментацию «максимум 7 байтов за раз» — имитация скверного транспорта. Посчитайте потолок числа раундов Drive до завершения рукопожатия. Критерии приёмки: при любом размере фрагмента (от 1 до целого рейса) рукопожатие успешно (протокольная машина поддерживает рекомбинацию сквозь записи — глава 84); худшее число раундов ограничено и объяснимо (порядок «размер рейсов ÷ размер фрагмента»); роста памяти нет (статистика главы 6).

Итоги

ТемаКратко
Позиция слоя сессийголая протокольная машина: Feed шифртекст / Drive продвижение / Send-очередь отдаёт рейсы, транспорт не важен
Цикл управленияFeed×4 (заимствование/Take/Ref/Buffer) → Drive → Send (Size/Front/Consume/Spans)
Конфигурация сервераIdentity или Select — одно обязательно; ALPN глубоко копируется; RequireProtocol — двусмысленность сразу неудача
Арена первого рейсаленивое выделение, обнуление-освобождение по использовании; у простаивающих ноль буферов рукопожатия; 1.2/1.3 общий путь
Selectпосле разбора ClientHello, до выхода; SNI/ALPN заимствованы на время колбэка; Cookie = 64-битный идентификатор маршрутизации
ЗапросыServerName (глубоко скопированный SNI) / SessionProtocol (финальный ALPN) / ServerCookie
Подпись билетовTicket/TicketNew; владение объектом переносится; глобального кеша нет; при AGAIN состояние неизменно
Принятие восстановленияне найдено — откат в полное рукопожатие; ошибка binder — fatal без даунгрейда; PSK+DHE; без 0-RTT
Границы 1.2EMS обязателен; без восстановления/повторного negociación/KeyUpdate
Изоморфизмклиент и сервер делят публичный API сессий — выучили одну сторону, получили обе

📖 Справочники API в этой главе:tls (201 записей)tls_verify (17 записей)tls_server (16 записей)crypto (140 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки