Ориентация главы
Самое будничное применение SSH — не открыть shell, а прокопать туннель: ssh -L 8080:db:80 (локальная переадресация — трафик локального 8080 доставляется через SSH на db:80, достижимый сервером) и ssh -R 9000:localhost:9000 (удалённая переадресация — сервер доставляет трафик 9000 обратно к вам). Протокол из двух частей: канал direct-tcpip (носитель локальной переадресации — канал, открываемый клиентом с целевым адресом) и канал forwarded-tcpip (носитель удалённой переадресации — канал, открываемый сервером после получения вашего глобального запроса tcpip-forward). Расслоение xssh как всегда: ssh_forward_message (чисто протокольная нагрузка — кодирование/декодирование сообщений, границы семантики портов, listener не строит и DNS не разбирает) и ssh_client_forward (клиентский слой композиции — DirectTcpipOpen/ForwardedTcpipAccept/TcpipForward/TcpipForwardCancel, четыре входа, без скрытого локального listener/потоков/ожидания). Настоящий сервис переадресации наверху компонует XRT listener/stream с оконными механизмами каналов — глава даёт детали и способ сборки.
128.1引入
Семантика адресов протокола переадресации стоит внимательного чтения: канал direct-tcpip несёт 要连的地址:端口 + 来源地址:端口 (адрес:порт для соединения + адрес:порт источника) — цель объявляется клиентом (разрешает ли — решает слой политики сервера; «контроль доступа к цели остаётся за политикой сервера» — дословно из контракта); forwarded-tcpip наоборот — канал от сервера несёт 被连的目标 + 发起者来源 (соединённая цель + источник инициатора). Границы семантики портов: RFC 4254 несёт порты в uint32, но xssh единообразно ограничивает их 0..65535 на границе семантического кодирования/декодирования — нелегальная запись не продвигает writer, нелегальное чтение — протокольная ошибка (нормализация «переносится в uint32, а фактически 16 бит» — семантический слой строче формата линии). Динамическое выделение порта 0: запрос tcpip-forward с портом 0 = поручить серверу назначить — ответ успеха несёт реальный порт (TcpipForwardSuccessRead извлекает — способ получить удалённый динамический порт).
Токен-корреляция глобальных запросов: tcpip-forward — глобальный запрос want-reply — при отправке берётся токен; успех/провал возвращается через Global-событие клиента с тем же токеном — коррелятор при нескольких глобальных запросах в полёте (глобальная редакция replyqueue главы 125).
128.2概念
Две формы и потоки данных
Слой сообщений: пять классов нагрузки и семантика заимствования
ssh_forward_message реализует пять: tcpip-forward (отправка) / cancel-tcpip-forward (отмена) / ответ успеха с динамическим портом (чтение) / open direct-tcpip (отправка+чтение) / open forwarded-tcpip (чтение). Заимствование адресов: адреса и источники заимствуются дословно как SSH string — принудительного превращения в socket address нет (цель может быть именем хоста с точки зрения сервера — разрешать её на клиенте и бессмысленно, и обычно невозможно). Переиспользование общих построителей: writer напрямую переиспользует общие префиксы global/channel — итоговая нагрузка завершается за один проход, без временного буфера Fields (путь без выделений). Строгое чтение: общий envelope разбирается первым, поля типа читаются строго, хвостовое содержимое отвергается (N-е появление защиты от smuggling).
Клиентский слой композиции: четыре входа
xrtSshClientDirectTcpipOpen: открыть direct-tcpip (старт локальной переадресации/собственных туннелей).xrtSshClientForwardedTcpipAccept: в callbackPacketили во время HOLD разобрать открытый сервером forwarded-tcpip — после фиксации read-транзакции клиент автоматически шлёт confirmation (контракт порядка: «зафиксировать read-транзакцию, отправить ответ, зафиксировать состояние канала» — строго три шага; приложение не должно слать confirmation прямо в callback Packet).xrtSshClientTcpipForward: отправить глобальный запрос want-reply (порт 0 для динамического выделения); токен берётся из динамического ограниченного FIFO.xrtSshClientTcpipForwardCancel: отменить тот же удалённый адрес.- Отвержение неизвестных/неразрешённых каналов peer:
xrtSshClientChannelReject; при принятии Packet без решения клиент по умолчанию отвечаетUNKNOWN_CHANNEL_TYPE— peer не ждёт вечно (протокольная вежливость, ставшая умолчанием).
Сборка полноценного сервиса переадресации
Детали в сборе; форма сервиса = сборка наверху: локальная переадресация = listener главы 67 + DirectTcpipOpen на каждое соединение + двунаправленный перенос (канальный I/O ↔ TCP-поток); удалённая = TcpipForward + локальный listener (на входящие открытия сервера) + ForwardedTcpipAccept. Способы смерти переноса (сцепка противодавления, полузакрытие, отмена) все переиспользуют существующий словарь — нового в этой главе только «протокольные сообщения и семантика двух сторон».
128.3示例
Первая полная программа: построение сообщения direct-tcpip
Программа ниже из examples/forward_message — сообщение open канала локальной переадресации:
#include <stdio.h> #include <xssh.h> /* 构建一个 direct-tcpip channel open。 */ int main(void) { unsigned char arrPayload[128]; xsshwriter Writer; if ( !xrtSshWriterInit(&Writer, arrPayload, sizeof(arrPayload)) || (xrtSshDirectTcpipOpenWrite( &Writer, 0u, 1024u * 1024u, 32768u, XRT_BYTES_LITERAL("db.internal"), 5432u, XRT_BYTES_LITERAL("127.0.0.1"), 50000u ) != XSSH_OK) ) { return 1; } printf("direct-tcpip-payload=%zu\n", Writer.Size); return 0; }
Что только что произошло. (1) xrtSshDirectTcpipOpenWrite(&Writer, 通道号, 初始窗口 1 MiB, max-packet 32 KiB, 目标 "db.internal":5432, 来源 "127.0.0.1":50000) (номер канала, начальное окно 1 МиБ, max-packet 32 КиБ, цель, источник) — типичные параметры локальной переадресации: цель — адрес базы с точки зрения сервера (имя хоста заимствуется дословно — сервер его разрешает), источник — локальный подключившийся. (2) окно/max-packet — ровно объявление способностей канала главы 125 — начальный бюджет переадресационного канала. (3) Все параметры портов лежат в семантических границах 0..65535 — передача 65536 будет отвергнута на слое записи (writer не продвигается). Это сообщение плюс пакетная рамка канала (глава 125) — один полный запрос туннеля.
Вторая полная программа: грань конфигурации переадресации
Вторая программа из examples/client_forward — заявление ресурсов слоя композиции:
#include <stdio.h> #include <xrt/ssh_client_forward.h> /* 展示 forwarding 发起端不引入隐藏 listener 或线程。 */ int main(void) { xsshclientconfig Config; if ( !xrtSshClientConfigInit(&Config) ) { return 1; } printf("global-replies=%zu\n", Config.GlobalReplyLimit); return 0; }
Что только что произошло. (1) Печать умолчания GlobalReplyLimit у xrtSshClientConfigInit — ёмкость FIFO токенов глобальных запросов (потолок находящихся в полёте tcpip-forward/cancel): ограниченный FIFO — бюджет глобальных запросов (ещё одна бюджетная линия после auth guard главы 124 — запросы переадресации тоже лимитированы). (2) Заголовочный комментарий повторяет проектную границу: «инициатор переадресации не вводит скрытый listener или поток» — локальный listener есть ваша сборочная деталь, не частная собственность библиотеки. (3) Полная сборка настоящего сервиса переадресации живёт в тестах и рантайме главы 127 — образец фиксирует факт ресурса «накладные расходы переадресации = один потолок FIFO».
128.4契约
- Две формы: direct-tcpip (открывает клиент, цель объявляется) / forwarded-tcpip (открывает обратно сервер, источник — его прослушивание).
- Глобальные запросы: tcpip-forward want-reply; токен из динамического ограниченного FIFO; порт 0 = динамическое выделение сервером (SuccessRead извлекает).
- Отмена: TcpipForwardCancel отменяет тот же удалённый адрес.
- Границы портов: семантический слой единообразно 0..65535; нелегальная запись не продвигается, нелегальное чтение — протокольная ошибка (нормализация поверх линейного uint32).
- Заимствование адресов: string дословно — без принудительного превращения в socket address; право разрешения цели и контроль доступа — слою политики сервера.
- Порядок confirm: read-транзакция зафиксирована → ответ → состояние канала зафиксировано — приложение не шлёт confirmation в callback Packet.
- Отказ по умолчанию: Packet принят без решения — ответ по умолчанию UNKNOWN_CHANNEL_TYPE — peer не ждёт впустую.
- Переиспользование плоскости данных: канальный I/O/противодавление/окна/закрытие — единый словарь главы 125, переадресация второй машины состояний не ставит.
- Ноль скрытого: локальный listener/потоки/ожидания не строятся — сборка сервиса наверху.
- Сообщения без выделений: построители общего префикса завершают за один проход; reader строго отвергает хвосты.
Типичные ошибки
Симптом: клиент пробует разрешить db.internal, падает — отказ строить туннель; или разрешает одноимённую машину со своей точки зрения — соединение не с той целью.
Причина: целевой адрес direct-tcpip — с точки зрения сервера (имя хоста/IP заимствуется дословно). Клиент не разрешает — право разрешения и контроля доступа на сервере.
if ( !xrtNetAddrParse(Target, &Addr) ) { fail(); /* клиент разрешает адрес сервера: бессмысленно и обычно провально */ }
/* адрес входит в сообщение дословно - разрешает и авторизует сервер */ xrtSshDirectTcpipOpenWrite(&Writer, Id, Win, MP, TargetHostView, TargetPort, SourceView, SourcePort);
Симптом: редкая порча состояния канала — ваш confirmation соревняется и дублирует автоматический.
Причина: контракт порядка — строго три шага клиента (зафиксировать read-транзакцию → отправить ответ → зафиксировать состояние канала); после разбора ForwardedTcpipAccept confirmation автоматичен. Досрочная отправка приложения ломает границу транзакции.
onPacket(...) { parse_forwarded(&Msg); send_confirmation(Chan); /* досрочно: гонка с автоматическим путём */ }
onPacket(...) { if ( xrtSshClientForwardedTcpipAccept(Client, &Msg, ...) ) { /* confirmation уже отправлен клиентом после фиксации read-транзакции */ register_tunnel(&Msg); /* приложение только регистрирует */ } }
Симптом: после отключения клиента сервер всё ещё слушает удалённый порт переадресации — порт занят, поверхность атаки осталась.
Причина: регистрация tcpip-forward на сервере постоянна; перед разрывом следует TcpipForwardCancel (при нормальном разрыве большинство серверов убирает, но явная отмена — детерминированный вежливый путь, особенно в долгоживущих пулах соединений с повторными переадресациями).
/* построили удалённую переадресацию и разорвались сразу *//* последовательность закрытия: сперва отменить переадресацию, потом закрыть соединение */ xrtSshClientTcpipForwardCancel(Client, BindAddr, Port, ...); drain_and_close(Client);
Практические задания
Построить/разобрать пять классов сообщений переадресации (forward/cancel/success/direct/forwarded) — включая обе границы портов, 0 и 65536. Критерии приёмки: все пять проходят круг с идентичными полями; 65536 отвергается при записи без продвижения.
Собрать: локальный listener (глава 67) → DirectTcpipOpen на каждое входящее → двунаправленный перенос (канальный I/O ↔ TCP) → распространение закрытия на обоих концах. Против реального SSH-сервера переадресовать на HTTP-порт его localhost. Критерии приёмки: curl нормально забирает через туннель; при медленном потреблении память постоянна на обоих концах; распространение полузакрытия (одна сторона EOF — другая всё равно завершает чисто).
TcpipForward(端口 0) (порт 0) берёт динамический порт → записать отображение → входящие ForwardedTcpipAccept диспетчеризировать по источнику на локальные цели → cancel для очистки. Множество переадресаций конкурентно. Критерии приёмки: динамические порты корректно извлечены и записаны; после cancel сервер прекращает слушать (повторное соединение отвергается); множество туннелей конкурентно без помех.
Итоги
| Тема | Кратко |
|---|---|
| Две формы | direct-tcpip (-L, открывает клиент) / forwarded-tcpip (-R, открывает обратно сервер) |
| Глобальные запросы | tcpip-forward want-reply; FIFO токенов; порт 0 = динамическое выделение |
| Получение динамического порта | TcpipForwardSuccessRead — uint32 в ответе успеха |
| Границы портов | семантический слой 0..65535; нелегальная запись не продвигается / нелегальное чтение — протокольная ошибка |
| Заимствование адресов | string дословно; разрешает и контролирует сервер — клиент не касается |
| Порядок confirm | фиксация чтения → ответ → состояние канала; приложение не шлёт досрочно |
| Отказ по умолчанию | Packet принят без решения → UNKNOWN_CHANNEL_TYPE |
| Плоскость данных | канальный I/O/окна/противодавление всё из главы 125 |
| Ноль скрытого | listener/потоки не строятся — сборка сервиса наверху |
| Сборочные детали | -L = listener + Direct + перенос; -R = Forward + listener + Accept |
📖 Справочники API в этой главе:xssh-ssh_client_forward (4 записей)xssh-ssh_forward_message (10 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки