Ориентация главы
Объектный стек 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概念
Трёхшаговый цикл управления сессией
Трёхшаговый цикл для клиента и сервера полностью изоморфен (у клиента 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 — сервер и настоящий клиент завершают рукопожатие и двусторонние данные на мосту в памяти:
#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; }
Что только что произошло. (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 сессий:
*/ #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; }
Что только что произошло. (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.
Типичные ошибки
Симптом: в 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 */ }
Симптом: ради «высокой доступности» 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; }
你要做的是不要在回调里伪造"找到" */Симптом: память сервера с десятками тысяч простаивающих соединений распухает — по несколько сотен КБ «предвыделённой зоны рукопожатия» на каждое, умноженные на их число.
Причина: неверно понята модель памяти слоя сессий. Временная память первого рейса — ленивая арена: не-рукопожатие не выделяет, рукопожатие завершено — немедленное обнуление и освобождение; долгому проживанию подлежит лишь само состояние сессии.
/* самому 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-ный протокол обеих конфигураций совпадает с ручной выкладкой.
Реализуйте колбэк 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.2 | EMS обязателен; без восстановления/повторного negociación/KeyUpdate |
| Изоморфизм | клиент и сервер делят публичный API сессий — выучили одну сторону, получили обе |
📖 Справочники API в этой главе:tls (201 записей)tls_verify (17 записей)tls_server (16 записей)crypto (140 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки