Ориентация главы
Серия SSH открывается. В отличие от «высокоуровневой интеграции» TLS (том 8), философия xssh — радикальное расслоение: четыре нижних блока независимы, без выделений и тестируемы по отдельности — wire (ssh_wire: кодирование/декодирование базовых типов SSH boolean/uint32/string/mpint); packet (ssh_packet: бинарная пакетная рамка RFC 4253 — длина/padding/нагрузка, не привязана ни к шифру, ни к сети); packet AES-GCM (ssh_packet_aes_gcm: пакетная обёртка aes128/256-gcm OpenSSH — заголовок длины открытым текстом как AAD, шифрование на месте, шестнадцатибайтовая метка); transport core (ssh_transport_core: свободная от буферов композиция кодека пакетов + протокольного порядка + бюджетов rekey — синхронный, callback-ный, Future и корутинный клиенты ведут один и тот же контракт состояния). Следующие шесть глав строятся на этом слое.
123.1引入
Форма линии SSH — «луковица»: прикладные данные (в каналах) завёрнуты в шифрованные пакеты, шифрованные пакеты — в TCP. Пакетная рамка (раздел 6 RFC 4253) фиксирует форму луковицы — packet_length(4B)| padding_length(1B)| payload | padding (длина пакета (4Б) | длина паддинга (1Б) | нагрузка | паддинг), причём padding выравнивает весь пакет по блоку (блоки шифра или защита от анализа трафика). Почему callback паддинга обязателен: xrtSshPacketWrite требует от вызывающего xsshpaddingproc и никогда молча не пишет нули — ядро не несёт зависимости от ГСЧ (свобода обрезки); transport пользуется PRNG сессии, тесты — детерминированным паддингом.
Ключевое решение транспортного ядра — трёхфазная транзакция отправки/приёма (Prepare/Commit/Abort): отправка сперва WritePrepare — итоговый линейный пакет пишется в буфер вызывающего (в этот момент sequence/nonce/бюджет ещё не двинуты); после принятия сетевой очередью WriteCommit продвигает их; при XNET_RESULT_AGAIN тот же линейный пакет сохраняется для повтора при writable (переподготавливать нельзя — sequence не переиспользуется); отказ — WriteAbort отбрасывает добавленные байты. Максимум один пакет в ожидании фиксации на направление — при противодавлении память не держится и счётчики не переиспользуются.
123.2概念
wire: система типов SSH
«Формат сериализации» протокола SSH — всего пять базовых типов: boolean (1Б), uint32 (4Б big-endian), uint64 (8Б), string (4Б-префикс длины + байты), mpint (целочисленная форма string — каноничное неотрицательное кодирование, лишние знаковые биты сняты). xsshwriter/xsshreader — пара курсоров без выделений (изоморфна курсорам DER главы 78): WriterInit привязывает буфер вызывающего, WriterWrite* пишет каждый тип, Size отчитывается о выходе; на стороне чтения ReaderInit/Read* заимствует ввод и читает по типам. Строгость: усечение отвергается, хвостовые данные (на уровне сообщения) отвергаются, неканоничный mpint отвергается — весь словарь парсера сообщений SSH.
packet: рамка и атомарность
xrtSshPacketMeasure(载荷长, 块长, &padding长, &packet长) (длина нагрузки, длина блока, &длина паддинга, &длина пакета) вычисляет рамку: нулевая длина блока берёт минимум RFC 8; явная длина блока 8..255; паддинг минимум 4 байта, весь пакет выровнен по блоку; полная линейная длина 4 + packet_length. xrtSshPacketWrite(writer, 载荷, ..., padding回调, 上下文) (writer, нагрузка, ..., callback паддинга, контекст) даёт полный линейный пакет — callback сперва пишет в стековую рабочую область, к цели переходит лишь при успехе (writer и номер последовательности при отказе не меняются). xrtSshPacketRead(reader, 预算, ..., &packet视图) (reader, бюджет, ..., &представление пакета) возвращает заимствующее ввод xsshpacketview: короткий пакет — XSSH_NEED_MORE (инкрементально ждём данные — изоморфизм трёх состояний всей библиотеки); уродливая длина/выравнивание/padding — протокольная ошибка; сверх бюджета — XSSH_ERROR_OVERFLOW. Номера последовательности: uint32 заворачивается естественно; NULL игнорирует их (самостоятельный инструмент рамки). Атомарность: reader завершает всю проверку на копии; только полный успех продвигает ввод, публикует представление и наращивает последовательность — отказ никогда не двигает наполовину.
packet AES-GCM: AEAD-пакеты OpenSSH
Общая обёртка aes128-gcm@openssh.com/aes256-gcm@openssh.com: 4-байтовый packet_length остаётся открытым текстом и служит AAD; padding_length|payload|padding шифруется на месте; хвост — 16-байтовая метка. xsshaesgcm — однонаправленное состояние (чтение и запись инициализируют каждое своё; одно состояние конкурентно не продвигается): ключ 16/32 байта, начальный IV 12 байтов (первые 4 — фиксированный IV + последние 8 — big-endian счётчик вызовов); счётчик нарастает на каждый пакет и останавливается перед UINT64_MAX, никогда не заворачиваясь (нулевая терпимость к переиспользованию nonce — дисциплина главы 75, ставшая протокольной). Четыре входа: AesGcmMeasure (вычисление рамки с выравниванием на 16) / AesGcmWrite (строит в неподтверждённой области writer и шифрует на месте, ноль heap-выделений) / AesGcmRead (сперва аутентификация, потом расшифровка в буфер вызывающего — провал аутентификации не меняет выходного открытого текста; успех с нелегальной структурой обнуляет расшифрованное тело) / AesGcmInvocation (запрос счётчика следующего пакета). Отказы (ёмкость/усечение/callback/аутентификация/исчерпание состояния) не продвигают ни reader, ни writer, ни sequence, ни counter.
transport core: композиция без буферов и NEWKEYS
Семантику ReadAbort стоит прожевать: кодек уже потребил этот пакет — отвергнуть аутентифицированный пакет значит, что протокольный слой больше не доверяет; transport закрывается сразу, «не изображает откат номера последовательности с продолжением общения» (откат последовательности = изготовление окна повторного воспроизведения). Автоматическое распознавание: Prepare автоматически распознаёт три класса особых пакетов KEXINIT/NEWKEYS/USERAUTH_SUCCESS — бюджеты rekey продвигаются на этих границах. Граница сетевой адаптации: буфер отправки можно отдать напрямую xrtNetStreamSend/SendRef; приём может заимствовать непрерывный участок xrtNetStreamBuffer — слой адаптации ведает лишь противодавлением/жизненным циклом/deadline/cancel, правила SSH не переimplements.
123.3示例
Первая полная программа: круг запись-чтение пакетной рамки
Программа ниже из examples/packet — построение и чтение минимального пакета в стиле IGNORE:
#include <inttypes.h> #include <stdio.h> #include <xssh.h> /* 示例 padding 源仅用于展示;真实 transport 应接入安全随机或会话 PRNG。 */ static bool exampleSshPadding(void* pOutput, size_t iSize, ptr pUserData) { uint8* pSeed = (uint8*)pUserData; bytes pBytes = (bytes)pOutput; size_t i; for ( i = 0u; i < iSize; ++i ) { pBytes[i] = (*pSeed)++; } return true; } /* 构建并读取一个最小 SSH_MSG_IGNORE 风格 payload。 */ int main(void) { unsigned char arrWire[32]; uint8 iSeed = 1u; xsshwriter Writer; xsshreader Reader; xsshpacketview Packet; if ( !xrtSshWriterInit(&Writer, arrWire, sizeof(arrWire)) || (xrtSshPacketWrite( &Writer, XRT_BYTES_LITERAL("\2ignore"), 8u, NULL, exampleSshPadding, &iSeed ) != XSSH_OK) || !xrtSshReaderInit( &Reader, (xbytesview){ arrWire, Writer.Size } ) || (xrtSshPacketRead( &Reader, 8u, 0u, NULL, &Packet ) != XSSH_OK) ) { return 1; } printf( "packet=%" PRIu32 " payload=%zu padding=%u\n", Packet.PacketSize, Packet.Payload.Size, (unsigned int)Packet.PaddingSize ); return 0; }
Что только что произошло. (1) xsshwriter привязывает 32-байтовый стековый буфер; xrtSshPacketWrite пишет 8-байтовую нагрузку \2ignore (номер сообщения 2 = SSH_MSG_IGNORE) — детерминированный callback exampleSshPadding даёт паддинг (только для образца; продакшн передаёт xrtSshSecurePadding или PRNG сессии — карточка контракта прямо говорит «детерминированный паддинг образца нельзя использовать в реальном SSH transport»). (2) xrtSshPacketRead вызывается дважды: первый без выходного параметра (лишь проверка), второй берёт xsshpacketview — три представления PacketSize/Payload/Padding заимствуют ввод. (3) Параметр бюджета 8u — явный потолок; реальный transport берёт согласованный лимит. Совпадение круга означает самосогласованность рамки: SSH-редакция паттерна круга DER главы 78.
Вторая полная программа: заявление транспортного ядра о нулевой обузе
Вторая программа из examples/transport_core — форма ресурсов ядра:
#include <stdio.h> #include <xssh.h> /* 创建一个不持有网络和缓冲的 client transport core。 */ int main(void) { xsshtransportcore Core; if ( !xrtSshTransportCoreInit( &Core, XSSH_ROLE_CLIENT, 0u, NULL, ((double)(0u)) / 1000.0) ) { return 1; } printf("transport-core=%zu max-packet=%u\n", sizeof(Core), (unsigned int)Core.Codec.MaxPacketSize); xrtSshTransportCoreClear(&Core); return 0; }
Что только что произошло. (1) xrtSshTransportCoreInit(&Core, XSSH_ROLE_CLIENT, 0, NULL, 0) инициализирует ядро клиентской роли на стеке — нулевая версия параметров берёт лимиты по умолчанию; печатаются sizeof(Core) и Codec.MaxPacketSize — заголовочный комментарий формулирует проектное заявление: «не держит ни сети, ни буферов». Этот вывод sizeof сам есть документация: ядро — предсказуемый стековый объект, а не кучевой монстр. (2) Финал — TransportCoreClear: очистка ядра не касается никаких дескрипторов (оно ничего не открывало). (3) Синхронный, событийный, Future и корутинный клиенты ведут одно и то же Core — форма драйвера навесная (все четыре появятся в главе 126); контракт состояния существует в одном экземпляре. Семья образцов: packet_codec (двойной режим открытый текст/GCM), packet_aes_gcm (состояние GCM и четыре входа), packet_random/transport_tcp_random (интеграция источников случайности), transport_rekey (путь бюджетов rekey), transport_state (машина состояний).
123.4契约
- Строгость wire: усечение/хвосты/неканоничный mpint отвергаются; пара курсоров без выделений; байты writer при отказе неизменны.
- Пакетная рамка: длина блока 8..255 (ноль = умолчание 8); паддинг ≥4, весь пакет выровнен по блоку; линейная длина
4+packet_length. - Обязательность callback паддинга: без молчаливого заполнения нулями; callback сперва в стек, потом фиксация;
XSSH_ERROR_CALLBACKотличим от протокольных ошибок. - Атомарность reader: вся проверка на копии проходит до продвижения ввода/публикации представления/наращивания последовательности; короткий пакет NEED_MORE; сверх бюджета OVERFLOW.
- Состояние GCM: однонаправленное, независимая инициализация; IV = fixed(4) + counter(8 big-endian); счётчик останавливается на UINT64_MAX без заворота.
- Атомарность GCM: сперва аутентификация, потом расшифровка; отказ не двигает reader/writer/sequence/counter; провал аутентификации не меняет открытый текст; успех с нелегальной структурой обнуляет тело.
- Алиасинг GCM: pPlain не перекрывает ввод/состояние; записываемый Payload не перекрывает область этого вывода (сохранение пути шифрования на месте).
- Транзакция отправки: Prepare не продвигает; Commit продвигает; AGAIN сохраняет исходный пакет для повтора (без переподготовки); Abort отбрасывает; максимум один ожидающий пакет на направление.
- Транзакция приёма: Inspect→агрегация→ReadPrepare (аутентификация + классификация)→ReadCommit/Abort; ReadAbort закрывает ядро (аутентифицированный пакет не откатывается).
- NEWKEYS: направление записи закрыто до успеха SetWriteAesGcm; сброс strict-kex + обнуление счётчиков + переключение cipher одним вызовом; KexComplete лишь при обоих направлениях.
- Граница сети: слой адаптации ведает противодавлением/жизненным циклом/deadline/cancel — правила SSH принадлежат ядру и не переimplements.
Типичные ошибки
Симптом: противоположная сторона сообщает об отказе MAC/аутентификации или просто отключается — переподготовка создала новый пакет, чей sequence/nonce конфликтует с уже стоящим в очереди.
Причина: Prepare не продвигает счётчики именно ради повторов при AGAIN: сохраните тот же линейный пакет и повторите постановку в очередь в неизменном виде. Повторный Prepare = пересборка с новым паддингом/nonce, развилка счётчиков против старого пакета, возможно уже частично принятого очередью.
if ( prepare(&Core, Buf) == OK ) { if ( enqueue(Buf) == AGAIN ) { prepare(&Core, Buf); /* пересобран: риск развилки nonce/последовательности */ enqueue(Buf); } }
xrtSshTransportCoreWritePrepareWithPadding(&Core, Buf, ...); if ( enqueue(Buf) == XNET_RESULT_AGAIN ) { wait_writable(); /* сохранить исходный пакет */ enqueue(Buf); /* повторить в неизменном виде */ } /* Abort — только при решении не отправлять */
Симптом: злой сервер объявляет сверхпакет — чтение растёт и растёт до OOM.
Причина: iMaxPacketSize с нулём берёт лимит по умолчанию, но бюджет — линия обороны, а не опция; даже в сценариях самостоятельного инструмента передавайте его явно (хотя бы константу умолчания — самодокументирование).
xrtSshPacketRead(&Reader, 0u, 0u, NULL, &Packet); /* полагается на умолчание - намерение бюджета неясно */
xrtSshPacketRead(&Reader, XSSH_PACKET_MAX_DEFAULT, 0u, NULL, &Packet); /* явный бюджет: код как документ */
Симптом: шифрование и расшифровка путаются — расшифровка пакета N взяла counter пакета N+1.
Причина: xsshaesgcm — однонаправленное состояние; в SSH у каждого направления свои ключ, IV и счётчик; одно состояние на обе стороны — два направления делят счётчик.
xsshaesgcm Gcm; xrtSshAesGcmInit(&Gcm, Key, IV); write_packet(&Gcm, ...); /* используется для записи */ read_packet(&Gcm, ...); /* и для чтения: конфликт counter */
xsshaesgcm WriteGcm, ReadGcm; xrtSshAesGcmInit(&WriteGcm, C2SKey, C2SIv); /* клиент -> сервер */ xrtSshAesGcmInit(&ReadGcm, S2CKey, S2CIv); /* сервер -> клиент */
Практические задания
Writer-ом/reader-ом записать и прочитать по одному boolean/uint32/uint64/string/mpint; проверить круг и байтовую форму (против примеров кодирования RFC 4251). Критерии приёмки: каноничное кодирование mpint (снятие знакового бита) выводимо вручную; усечённый ввод отвергается.
Сканировать PacketMeasure по нагрузкам 0–100 байтов, построить изменения padding/packet_length; проверить разницу выравнивания при длине блока 8 и 16. Затем построить чтение сверх бюджета и проверить OVERFLOW. Критерии приёмки: измеренные значения сходятся с формулами RFC 4253 §6.
Два процесса (или два ядра в одном) строят GCM-канал на выведенных из KEX ключах двух направлений: обмен 1000 пакетов, счётчики монотонны, assert, что после пакета UINT64_MAX-1 запись отвергается (исчерпание состояния — имитировать малым счётчиком). Критерии приёмки: полный дуплекс без ошибочных пакетов; путь исчерпания счётчика отказывает явно, без заворота.
Итоги
| Тема | Кратко |
|---|---|
| Четыре блока | wire (типы) → packet (рамка) → aes_gcm (шифрованные пакеты) → transport core (композиция + порядок + rekey) |
| Типы wire | boolean/uint32/uint64/string/mpint; курсоры без выделений; строгое отвержение |
| Пакетная рамка | 4Б длина + 1Б длина паддинга + нагрузка + паддинг; выравнивание по блоку; линейная длина = 4+packet_length |
| Callback паддинга | обязателен; сперва стек, потом фиксация; ошибки CALLBACK отдельно от протокольных |
| Атомарность reader | вся проверка на копии до продвижения; NEED_MORE/OVERFLOW/протокольная ошибка — три состояния |
| Форма GCM | заголовок длины открытым текстом как AAD; нагрузка шифруется на месте; метка 16Б; counter не заворачивается |
| Атомарность GCM | сперва аутентификация, потом расшифровка; любой отказ не двигает ни один счётчик |
| Транзакция отправки | Prepare (без продвижения) → Commit / AGAIN сохраняет / Abort; один пакет на направление |
| Транзакция приёма | Inspect→агрегация→Prepare→Commit/Abort (закрывает ядро) |
| NEWKEYS | активация по направлениям; сброс strict-kex одним вызовом; оба живы — тогда KexComplete |
| Граница сети | буферы напрямую в Send/SendRef/Buffer — слой адаптации правила SSH не переimplements |
📖 Справочники API в этой главе:xssh-ssh_wire (31 записей)xssh-ssh_packet (3 записей)xssh-ssh_transport_core (21 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки