В ProxyMapService появилась трансляция HTTP/1.1 → HTTP/2: клиент по-прежнему говорит на HTTP/1.1, а прокси общается с upstream-сервером по h2 и конвертирует ответы обратно в HTTP/1.1.
Зачем это нужно
Многие современные серверы (gRPC-шлюзы, Kestrel с h2-only, CDN) принимают или предпочитают HTTP/2. При этом клиентские программы, скрипты и старые библиотеки часто умеют только HTTP/1.1. Прокси закрывает этот разрыв: клиент ничего не меняет, а сервер получает современный протокол. Весь трафик по-прежнему проходит через прокси и логируется, в том числе h2-фреймы на стороне сервера.
Как включить
Флаг EnableTranslateH1ToH2 можно задать на трёх уровнях — глобально, для слушающего порта или для отдельного правила хоста:
{
"HTTP": {
"EnableTranslateH1ToH2": true
},
"ProxyMappings": [
{
"Listen": {
"Port": 8080,
"Action": "Bypass",
"DecryptSSL": true,
"EnableTranslateH1ToH2": true
}
}
],
"HostRules": {
"Items": [
{
"HostName": "api.example.com",
"DecryptSSL": true,
"EnableTranslateH1ToH2": true
}
]
}
}
Трансляция работает только для HTTPS с расшифровкой (MITM): нужно видеть запросы, чтобы перепаковать их в фреймы. Для прозрачного туннеля и обычного HTTP флаг не действует.
Важно: сам по себе флаг ничего не делает — upstream должен выбрать h2 на TLS-рукопожатии. Поэтому в SslClientOptions.ApplicationProtocols должен быть h2 (см. ниже).
Как это устроено и ограничения
Каждый запрос клиента парсится как HTTP/1.1, затем прокси отправляет на сервер HEADERS и DATA-фреймы, а ответ h2 собирает обратно в строку статуса, заголовки и тело. Правила преобразования:
- hop-by-hop заголовки (
Connection,Keep-Alive,Transfer-Encoding,Upgrade,Host) отбрасываются;Hostстановится псевдозаголовком:authority - метод, схема и путь уходят в
:method,:scheme,:path - ответ без
Content-Lengthотдаётся клиенту сTransfer-Encoding: chunked, трейлеры передаются - некорректные заголовки (CR/LF/NUL, конфликтующие
Content-Length, неоднозначныйTransfer-Encoding) отклоняются как защита от request smuggling
Ограничения:
- транслируется только направление h1 → h2; обратного h2 → h1 нет
- одновременно активен один h2-stream: pipelining клиента выполняется последовательно
CONNECTи WebSocket upgrade не транслируются и получают ответ501 Not Implemented- server push отключён (
ENABLE_PUSH=0) - если сервер присылает
RST_STREAM, клиент получит502
Роль ApplicationProtocols
Прокси делает два TLS-рукопожатия, и у каждого свой список ALPN. Для начала три обозначения:
- C —
SslClientOptions.ApplicationProtocols: что прокси предлагает upstream-серверу - S —
SslServerOptions.ApplicationProtocols: что прокси разрешает клиенту - K — ALPN из ClientHello самого клиента: что клиент реально умеет
Порядок рукопожатий обратный: сначала прокси договаривается с upstream, потом отвечает клиенту. Точнее, он заранее подглядывает ClientHello (SocketFlags.Peek) и уже потом выбирает, что предложить серверу.
Если список пуст (в K или в S), подразумевается только http/1.1. Пустой ALPN означает, что клиент ничего не согласовывает, и тогда поверх TLS идёт HTTP/1.1.
Решение принимается в три шага (T — включённая трансляция):
- Возможности клиента:
h1 = K ∩ S содержит http/1.1,h2 = K ∩ S содержит h2. - Что предложить upstream:
h2— если оно есть в C и (клиент умеет h2 или T включена и клиент умеет h1);http/1.1— если оно есть в C и клиент умеет h1. - После ответа upstream: h2 и клиент умеет h2 — прозрачный HTTP/2; h2 и клиент только h1 при включённой T — трансляция; upstream ответил http/1.1 и клиент умеет h1 — обычный HTTP/1.1. Всё остальное — ошибка согласования.
Результат прокси фиксирует: клиенту предлагается ровно один протокол, тот, на котором будет работать сессия.
Сценарии
Браузер присылает K = http/1.1, h2, если не сказано иначе. У curl и подобных клиентов — только http/1.1.
| C (SslClientOptions) | S (SslServerOptions) | K (клиент) | T | Результат |
|---|---|---|---|---|
| http/1.1 | http/1.1 | любой с h1 | любое | HTTP/1.1 с обеих сторон |
| http/1.1,h2 | http/1.1,h2 | http/1.1,h2 | любое | общается как сервер выбрал: h2 — прозрачный HTTP/2, иначе HTTP/1.1 |
| h2 | http/1.1 | http/1.1 | включена | трансляция h1 → h2 |
| h2 | http/1.1 | http/1.1 | выключена | ошибка согласования |
| h2 | http/1.1,h2 | http/1.1,h2 | любое | прозрачный HTTP/2, трансляция не нужна |
| h2 | http/1.1,h2 | http/1.1 (curl) | включена | трансляция h1 → h2 |
| h2 | http/1.1,h2 | http/1.1 (curl) | выключена | ошибка согласования |
| http/1.1,h2 | http/1.1 | http/1.1,h2 | включена | upstream выбрал h2 — трансляция; выбрал http/1.1 — обычный HTTP/1.1 |
| http/1.1,h2 | http/1.1 | http/1.1,h2 | выключена | HTTP/1.1 с обеих сторон (серверу h2 не предлагается) |
| http/1.1 | h2 | http/1.1,h2 | любое | ошибка: клиент умеет только h2, а upstream не предлагается h2 (обратной трансляции h2 → h1 нет) |
Последняя строка — частая ловушка: http/1.1 в C и жёсткое h2 в S не согласуются ни при каких настройках, потому что трансляция есть только в одну сторону.
Диагностика
При успешной трансляции в логе будет запись уровня Info:
HTTP/1.1 -> HTTP/2 translation: client 127.0.0.1:39461 speaks HTTP/1.1, upstream localhost:5112 negotiated h2
Рекомендация для старта: SslClientOptions.ApplicationProtocols = "http/1.1,h2", SslServerOptions.ApplicationProtocols = "http/1.1" и EnableTranslateH1ToH2 = true. Прокси предложит серверу h2, а клиент останется на HTTP/1.1.