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

Самое будничное применение 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概念

Две формы и потоки данных

1Локальная переадресация (-L)локальный listener принимает соединение -> на каждое открывается канал direct-tcpip
2Удалённая переадресация (-R)глобальный запрос TcpipForward(адрес прослушивания, порт) -> сервер поднимает listener
3Плоскость данныхединый канальный I/O (окна/противодавление/закрытие главы 125) - переадресация своего не ставит

Слой сообщений: пять классов нагрузки и семантика заимствования

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: в callback Packet или во время 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 канала локальной переадресации:

extlibs/xssh/examples/forward_message/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXSSH_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xssh.h impl.c extlibs/xssh/examples/forward_message/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(输出 direct-tcpip open 报文构建的自检结果)

Что только что произошло. (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 — заявление ресурсов слоя композиции:

extlibs/xssh/examples/client_forward/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXSSH_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xssh.h impl.c extlibs/xssh/examples/client_forward/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(输出全局回复 FIFO 上限的配置自检结果)

Что только что произошло. (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 строго отвергает хвосты.

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

Ловушка 1: целевой адрес разбирается как локальный:Ловушка 1: целевой адрес разбирается как локальный

Симптом: клиент пробует разрешить db.internal, падает — отказ строить туннель; или разрешает одноимённую машину со своей точки зрения — соединение не с той целью.

Причина: целевой адрес direct-tcpip — с точки зрения сервера (имя хоста/IP заимствуется дословно). Клиент не разрешает — право разрешения и контроля доступа на сервере.

错误写法
if ( !xrtNetAddrParse(Target, &Addr) ) {
	fail();   /* клиент разрешает адрес сервера: бессмысленно и обычно провально */
}
正确写法
/* адрес входит в сообщение дословно - разрешает и авторизует сервер */
xrtSshDirectTcpipOpenWrite(&Writer, Id, Win, MP,
	TargetHostView, TargetPort, SourceView, SourcePort);
Ловушка 2: досрочный confirmation в callback Packet:Ловушка 2: досрочный confirmation в callback Packet

Симптом: редкая порча состояния канала — ваш confirmation соревняется и дублирует автоматический.

Причина: контракт порядка — строго три шага клиента (зафиксировать read-транзакцию → отправить ответ → зафиксировать состояние канала); после разбора ForwardedTcpipAccept confirmation автоматичен. Досрочная отправка приложения ломает границу транзакции.

错误写法
onPacket(...) {
	parse_forwarded(&Msg);
	send_confirmation(Chan);   /* досрочно: гонка с автоматическим путём */
}
正确写法
onPacket(...) {
	if ( xrtSshClientForwardedTcpipAccept(Client, &Msg, ...) ) {
		/* confirmation уже отправлен клиентом после фиксации read-транзакции */
		register_tunnel(&Msg);   /* приложение только регистрирует */
	}
}
Ловушка 3: разрыв без cancel (утечка listener на сервере):Ловушка 3: разрыв без cancel (утечка listener на сервере)

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

Причина: регистрация 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 записей)

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