Транспорт HTTPUpgrade
Введение
HTTPUpgrade -- это легковесный транспорт, который имитирует рукопожатие WebSocket (HTTP/1.1 Upgrade: websocket), но не использует фреймирование WebSocket. После завершения рукопожатия обновления соединение становится сырым TCP-потоком. Это делает его проще и эффективнее полного транспорта WebSocket, при этом позволяя проходить через промежуточные устройства, ожидающие HTTP Upgrade. Он поддерживает TLS, отпечатки uTLS, протокол PROXY и ранние данные.
Регистрация протокола
Зарегистрирован как "httpupgrade" (transport/internet/httpupgrade/httpupgrade.go:3):
const protocolName = "httpupgrade"- Дайлер:
httpupgrade/dialer.go:134-136 - Слушатель:
httpupgrade/hub.go:165-167 - Конфигурация:
httpupgrade/config.go:19-23
Процесс Dial
Рукопожатие
dialhttpUpgrade (httpupgrade/dialer.go:46-115) выполняет ручное HTTP Upgrade:
func dialhttpUpgrade(ctx context.Context, dest net.Destination,
streamSettings *internet.MemoryStreamConfig) (net.Conn, error) {
// 1. Dial сырого TCP через internet.DialSystem
pconn, _ := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
// 2. Опциональная обертка TLS
if tConfig != nil {
// Применение TLS с отпечатками uTLS или стандартным Go TLS
conn = tls.UClient(pconn, tlsConfig, fingerprint) // или tls.Client
}
// 3. Построение HTTP-запроса
req := &http.Request{
Method: http.MethodGet,
URL: &requestURL,
Header: make(http.Header),
}
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "websocket")
// 4. Запись запроса напрямую в соединение
req.Write(conn)
// 5. Обертка ConnRF для чтения ответа
connRF := &ConnRF{Conn: conn, Req: req, First: true}
return connRF, nil
}В отличие от транспорта WebSocket, здесь НЕ используется gorilla/websocket. HTTP-запрос записывается напрямую в TCP-поток, а ответ разбирается вручную.
Чтение ответа (ConnRF)
Структура ConnRF (httpupgrade/dialer.go:19-44) перехватывает первый вызов Read для разбора HTTP-ответа:
type ConnRF struct {
net.Conn
Req *http.Request
First bool
}
func (c *ConnRF) Read(b []byte) (int, error) {
if c.First {
c.First = false
reader := bufio.NewReaderSize(c.Conn, len(b))
resp, err := http.ReadResponse(reader, c.Req)
if resp.Status != "101 Switching Protocols" ||
strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" ||
strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
return 0, errors.New("unrecognized reply")
}
// Чтение буферизованных байтов из bufreader
return reader.Read(b[:reader.Buffered()])
}
return c.Conn.Read(b)
}Ключевое решение: bufio.Reader имеет размер точно len(b), чтобы гарантировать, что любые данные после HTTP-заголовков ответа (которые могут прийти в том же TCP-сегменте) будут захвачены и возвращены при этом первом чтении.
Ранние данные
Когда Ed == 0 (по умолчанию), ответ читается сразу при dial для подтверждения успешного обновления (dialer.go:107-112). Когда Ed > 0, чтение ответа откладывается до первого вызова Read приложением, что позволяет быстрее завершить dial.
Обработка заголовков
Пользовательские заголовки добавляются через функцию AddHeader (dialer.go:120-122), которая обходит каноникализацию MIME-заголовков Go:
func AddHeader(header http.Header, key, value string) {
header[key] = append(header[key], value)
}Это сохраняет точный регистр имен заголовков (например, "Web*S*ocket" вместо "Websocket").
Процесс Listen
Архитектура сервера
ListenHTTPUpgrade (httpupgrade/hub.go:115-163) создает сырой TCP-слушатель (не HTTP-сервер):
func ListenHTTPUpgrade(ctx context.Context, address net.Address, port net.Port,
streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) {
// Создание TCP/Unix-слушателя через internet.ListenSystem
// Опциональная обертка TLS
serverInstance := &server{
config: transportConfiguration,
addConn: addConn,
innnerListener: listener,
}
go serverInstance.keepAccepting()
return serverInstance, nil
}В отличие от транспорта WebSocket (который использует http.Server), HTTPUpgrade принимает сырые соединения и вручную разбирает HTTP-запросы.
Обработка соединений
server.Handle (httpupgrade/hub.go:34-42) и server.upgrade (hub.go:45-103):
func (s *server) upgrade(conn net.Conn) (stat.Connection, error) {
connReader := bufio.NewReader(conn)
req, _ := http.ReadRequest(connReader)
// Проверка хоста и пути
if len(s.config.Host) > 0 && !internet.IsValidHTTPHost(host, s.config.Host) {
return nil, errors.New("bad host")
}
if req.URL.Path != path {
return nil, errors.New("bad path")
}
// Проверка заголовков обновления
if connection != "upgrade" || upgrade != "websocket" {
return nil, errors.New("unrecognized request")
}
// Отправка ответа 101
resp := &http.Response{
Status: "101 Switching Protocols",
StatusCode: 101,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{},
}
resp.Header.Set("Connection", "Upgrade")
resp.Header.Set("Upgrade", "websocket")
resp.Write(conn)
return stat.Connection(newConnection(conn, remoteAddr)), nil
}X-Forwarded-For
Реальный IP клиента извлекается из заголовков переадресации (hub.go:83-100) с учетом настройки TrustedXForwardedFor в параметрах сокета.
Формат данных на проводе
Клиент -> Сервер:
GET /path HTTP/1.1
Host: example.com
Connection: Upgrade
Upgrade: websocket
User-Agent: Mozilla/5.0 ...
[произвольные заголовки]
Сервер -> Клиент:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
[сырой двунаправленный TCP-поток]После рукопожатия данные передаются как сырые байты -- без фреймирования WebSocket, без префиксов длины, без маскирования. Это ключевое отличие от полного транспорта WebSocket.
sequenceDiagram
participant Client as Клиент
participant Server as Сервер
Client->>Server: GET /path HTTP/1.1\r\nUpgrade: websocket\r\n...
Server->>Client: HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n
Note over Client,Server: Начинается сырой TCP-поток
Client->>Server: [байты данных прокси]
Server->>Client: [байты данных прокси]Обертка соединения
Структура connection (httpupgrade/connection.go:5-19) минимальна:
type connection struct {
net.Conn
remoteAddr net.Addr
}
func (c *connection) RemoteAddr() net.Addr {
return c.remoteAddr
}Она переопределяет только RemoteAddr() для поддержки X-Forwarded-For. Все остальные методы делегируются базовому net.Conn.
Поддержка протокола PROXY
И клиент, и сервер поддерживают протокол PROXY:
- Сервер: Наследуется из настроек сокета. Когда
AcceptProxyProtocolравен true, базовыйinternet.ListenSystemоборачивает слушатель (hub.go:117-122,hub.go:145-147). - Объединение конфигурации:
AcceptProxyProtocolиз конфигурации транспорта ИЛИ настроек сокета активирует протокол PROXY (hub.go:121).
Параметры конфигурации
Из httpupgrade/config.go:
- Path: URL-путь, нормализованный с ведущим
/(config.go:8-17) - Host: Ожидаемый заголовок Host для серверной проверки
- Header: Произвольные HTTP-заголовки (карта)
- AcceptProxyProtocol: Включение протокола PROXY на слушателе
- Ed: Размер ранних данных. При ненулевом значении разбор ответа откладывается.
Примечания по реализации
- Без фреймирования WebSocket: После ответа
101 Switching Protocolsданные отправляются как сырые байты. Это устраняет накладные расходы WebSocket (2-14 байт на фрейм) и требование маскирования. - Без зависимости от gorilla: В отличие от транспорта WebSocket, HTTPUpgrade не использует
gorilla/websocket. Он формирует и разбирает HTTP-сообщения напрямую. - Сервер без состояния: Каждое соединение обрабатывается независимо. Сервер не поддерживает состояние сессии.
- Сохранение регистра заголовков: Функция
AddHeaderобходитtextproto.CanonicalMIMEHeaderKeyGo, позволяя использовать точный регистр имен заголовков. Это может помочь соответствовать ожиданиям конкретных CDN или промежуточных устройств. - Трюк с размером bufio:
ConnRF.Readсоздаетbufio.ReaderSize(conn, len(b)), ограниченный размером буфера вызывающего. Это гарантирует, что буферизованный reader никогда не прочитает больше, чем может быть возвращено, предотвращая потерю данных из-за двойной буферизации. - TLS на слушателе: В отличие от gRPC (который использует систему учетных данных gRPC), HTTPUpgrade оборачивает net.Listener напрямую через
tls.NewListener(hub.go:149-153). - Сравнение с WebSocket: HTTPUpgrade строго проще и эффективнее для проксирования. Транспорт WebSocket следует предпочитать только тогда, когда нужны специфические функции WebSocket (heartbeat-пинги, посообщенное сжатие, браузерный дайлер).