Skip to content

SOCKS5 и HTTP-прокси

Xray-core реализует протоколы SOCKS4/4a/5 и HTTP/HTTPS-прокси как входящие (локальный прокси-сервер), так и исходящие (клиент вышестоящего прокси) обработчики. Входящий обработчик SOCKS также может автоматически обнаруживать и перенаправлять HTTP-запросы встроенному HTTP-обработчику.

Протокол SOCKS5

Обзор

  • Направление: Входящий + Исходящий
  • Транспорт: TCP + UDP
  • Аутентификация: Нет или Имя пользователя/Пароль (RFC 1929)
  • Команды: CONNECT (TCP), UDP ASSOCIATE
  • Стандарты: RFC 1928 (SOCKS5), RFC 1929 (Аутентификация по имени пользователя/паролю)

Формат передачи данных

Рукопожатие SOCKS5

mermaid
sequenceDiagram
    participant C as Клиент
    participant S as Сервер

    C->>S: Version(1B) + NMethods(1B) + Methods(NB)
    S->>C: Version(1B) + ChosenMethod(1B)

    alt Аутентификация по имени пользователя/паролю
        C->>S: AuthVer(1B) + ULen(1B) + User + PLen(1B) + Pass
        S->>C: AuthVer(1B) + Status(1B)
    end

    C->>S: Version(1B) + Cmd(1B) + Rsv(1B) + Address
    S->>C: Version(1B) + Rep(1B) + Rsv(1B) + BndAddress

Согласование аутентификации

Client -> Server:
+-----+----------+----------+
| VER | NMETHODS | METHODS  |
| 1B  | 1B       | 1-255B   |
+-----+----------+----------+
  0x05

Server -> Client:
+-----+--------+
| VER | METHOD |
| 1B  | 1B     |
+-----+--------+
  0x05

Значения методов:

ЗначениеМетод
0x00Без аутентификации
0x02Имя пользователя/Пароль
0xFFНет допустимых методов

Исходный код: proxy/socks/protocol.go:27-33

Аутентификация по имени пользователя/паролю (RFC 1929)

Client -> Server:
+-----+------+----------+------+----------+
| VER | ULEN | UNAME    | PLEN | PASSWD   |
| 1B  | 1B   | 1-255B   | 1B   | 1-255B   |
+-----+------+----------+------+----------+
  0x01

Server -> Client:
+-----+--------+
| VER | STATUS |
| 1B  | 1B     |
+-----+--------+
  0x01  0x00=ok

Исходный код: proxy/socks/protocol.go:232-265

Запрос SOCKS5

+-----+-----+------+----------+----------+------+
| VER | CMD | RSV  | ATYP     | DST.ADDR | PORT |
| 1B  | 1B  | 1B   | 1B       | variable | 2B   |
+-----+-----+------+----------+----------+------+
  0x05              0x00

Команды:

ЗначениеКомандаПоддержка
0x01TCP CONNECTДа
0x02TCP BINDНет (возвращает 0x07)
0x03UDP ASSOCIATEДа (если включено)
0xF0Tor ResolveОбрабатывается как CONNECT
0xF1Tor Resolve PTRОбрабатывается как CONNECT

Исходный код: proxy/socks/protocol.go:15-22, proxy/socks/protocol.go:164-180

Ответ SOCKS5

+-----+-----+------+----------+----------+------+
| VER | REP | RSV  | ATYP     | BND.ADDR | PORT |
| 1B  | 1B  | 1B   | 1B       | variable | 2B   |
+-----+-----+------+----------+----------+------+
  0x05       0x00

Для UDP ASSOCIATE поля BND.ADDR и BND.PORT указывают конечную точку UDP-ретрансляции.

Исходный код: proxy/socks/protocol.go:300-310

UDP-датаграмма SOCKS5

+------+------+------+----------+----------+------+---------+
| RSV  | RSV  | FRAG | ATYP     | DST.ADDR | PORT | DATA    |
| 1B   | 1B   | 1B   | 1B       | variable | 2B   | ...     |
+------+------+------+----------+----------+------+---------+
  0x00   0x00

Исходный код: proxy/socks/protocol.go:324-363

Байт фрагмента должен быть 0x00 — фрагментированный UDP не поддерживается:

go
if packet.Byte(2) != 0 /* fragments */ {
    return nil, errors.New("discarding fragmented payload.")
}

Исходный код: proxy/socks/protocol.go:334-336

Поддержка SOCKS4/4a

Входящий обработчик также обрабатывает SOCKS4/4a на основе байта версии:

go
switch version {
case socks4Version:  // 0x04
    return s.handshake4(cmd, reader, writer)
case socks5Version:  // 0x05
    return s.handshake5(cmd, reader, writer)
}

Исходный код: proxy/socks/protocol.go:222-230

SOCKS4a расширяет SOCKS4 поддержкой доменных имён: если IP начинается с 0x00, доменное имя следует после нулевого терминатора идентификатора пользователя.

Исходный код: proxy/socks/protocol.go:72-78

Входящий (сервер)

Файл: proxy/socks/server.go

Автоопределение: Сервер читает первый байт для различения SOCKS от HTTP:

go
if firstbyte[0] != 5 && firstbyte[0] != 4 {
    // Не SOCKS, попробовать HTTP
    return s.httpServer.ProcessWithFirstbyte(ctx, network, conn, dispatcher, firstbyte...)
}

Исходный код: proxy/socks/server.go:92-95

Поток UDP Associate:

  1. Клиент отправляет CONNECT с командой 0x03
  2. Сервер отвечает адресом/портом UDP-ретрансляции
  3. Клиент отправляет UDP-датаграммы на этот адрес
  4. Сервер принимает данные по UDP-сети, декодирует заголовок SOCKS5 UDP, выполняет диспетчеризацию
  5. TCP-соединение остаётся открытым как сигнал поддержания связи
go
func (*Server) handleUDP(c io.Reader) error {
    // Ожидание закрытия TCP-соединения клиентом
    return common.Error2(io.Copy(buf.DiscardBytes, c))
}

Исходный код: proxy/socks/server.go:182-186

UDP-фильтр: Когда аутентификация включена, только клиенты, установившие TCP-соединение UDP ASSOCIATE, могут отправлять UDP-пакеты:

go
if s.udpFilter != nil && !s.udpFilter.Check(conn.RemoteAddr()) {
    return nil
}

Исходный код: proxy/socks/server.go:189-192

Исходящий (клиент)

Файл: proxy/socks/client.go

Клиент выполняет полное рукопожатие SOCKS5 с вышестоящим сервером:

go
func ClientHandshake(request *RequestHeader, reader io.Reader, writer io.Writer) (*RequestHeader, error) {
    // 1. Отправка выбора метода аутентификации
    // 2. Чтение выбранного сервером метода
    // 3. Если аутентификация по паролю, отправка учётных данных
    // 4. Отправка запроса CONNECT или UDP ASSOCIATE
    // 5. Чтение ответа
}

Исходный код: proxy/socks/protocol.go:421-515

Для UDP клиент открывает отдельное UDP-соединение на адрес, возвращённый ответом SOCKS5 UDP ASSOCIATE.

Исходный код: proxy/socks/client.go:146-163


Протокол HTTP-прокси

Обзор

  • Направление: Входящий + Исходящий
  • Транспорт: TCP, UNIX-сокет
  • Аутентификация: HTTP Basic Auth (заголовок Proxy-Authorization)
  • Методы: CONNECT (туннель), Plain HTTP (прокси)
  • UDP: Не поддерживается

HTTP CONNECT (туннелирование)

mermaid
sequenceDiagram
    participant C as Клиент
    participant P as Прокси
    participant S as Сервер

    C->>P: CONNECT host:port HTTP/1.1
    P->>S: TCP-подключение к host:port
    S->>P: Подключено
    P->>C: HTTP/1.1 200 Connection established
    C->>P: [необработанные байты]
    P->>S: [необработанные байты]
    S->>P: [необработанные байты]
    P->>C: [необработанные байты]

Исходный код: proxy/http/server.go:176-202

Обычное HTTP-проксирование

Для не-CONNECT запросов сервер:

  1. Парсит полный HTTP-запрос с помощью Go http.ReadRequest()
  2. Извлекает назначение из заголовка Host или URL
  3. Передаёт запрос исходящему обработчику
  4. Поддерживает keep-alive для последовательных запросов на одном соединении
go
keepAlive := strings.TrimSpace(strings.ToLower(
    request.Header.Get("Proxy-Connection"))) == "keep-alive"

Исходный код: proxy/http/server.go:163

Аутентификация

Базовая аутентификация проверяется через заголовок Proxy-Authorization:

go
func parseBasicAuth(auth string) (username, password string, ok bool) {
    const prefix = "Basic "
    c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
    cs := string(c)
    s := strings.IndexByte(cs, ':')
    return cs[:s], cs[s+1:], true
}

Исходный код: proxy/http/server.go:63-78

Неудачная аутентификация возвращает 407 Proxy Authentication Required с заголовком Proxy-Authenticate: Basic realm="proxy".

Исходный код: proxy/http/server.go:128

Поддержка HTTP/2 (исходящий)

Исходящий HTTP-клиент поддерживает как HTTP/1.1, так и HTTP/2 при подключении через вышестоящий HTTP-прокси:

go
switch nextProto {
case "", "http/1.1":
    return connectHTTP1(rawConn)
case "h2":
    t := http2.Transport{}
    h2clientConn, err := t.NewClientConn(rawConn)
    return connectHTTP2(rawConn, h2clientConn)
}

Исходный код: proxy/http/client.go:320-332

Соединения H2 кешируются для каждого назначения для мультиплексирования:

go
var cachedH2Conns map[net.Destination]h2Conn

Исходный код: proxy/http/client.go:47-48

Обработка ответов 1xx

Сервер корректно обрабатывает промежуточные ответы HTTP 1xx (например, 100 Continue), перенаправляя их клиенту перед чтением фактического ответа:

go
if strings.HasPrefix(status, "1") {
    // Чтение до \r\n\r\n, перенаправление клиенту
    writer.Write(ResponseHeader1xx)
}
return http.ReadResponse(r, req)

Исходный код: proxy/http/server.go:316-344

Входящий (сервер)

Файл: proxy/http/server.go

Ключевые особенности поведения:

  • Поддерживает режим AllowTransparent для прозрачного HTTP-проксирования (абсолютный URL не требуется)
  • Удаляет hop-by-hop заголовки согласно спецификации HTTP
  • Устанавливает User-Agent в пустую строку, если не указан (предотвращает значение по умолчанию Go)
  • Обрабатывает keep-alive соединения в цикле (goto Start)

Исходящий (клиент)

Файл: proxy/http/client.go

Клиент:

  1. Считывает первую полезную нагрузку из читателя канала
  2. Устанавливает HTTP CONNECT туннель к вышестоящему прокси
  3. Отправляет первую полезную нагрузку сразу после установки туннеля
  4. Поддерживает пользовательские заголовки через конфигурацию (с поддержкой Go-шаблонов для Source/Target)
go
data := struct {
    Source net.Destination
    Target net.Destination
}{...}
tmpl.Execute(&buf, data)

Исходный код: proxy/http/client.go:180-203

Примечания по реализации

  1. Автоопределение SOCKS/HTTP: Входящий обработчик SOCKS читает первый байт. Если это не 0x04 и не 0x05, он перенаправляет соединение встроенному HTTP-серверу. Это позволяет одному порту обслуживать как SOCKS5, так и HTTP-прокси.

Исходный код: proxy/socks/server.go:92-95

  1. Поведение CanSpliceCopy: И SOCKS, и HTTP изначально устанавливают CanSpliceCopy = 2. После рукопожатия протокола (для TCP CONNECT) значение повышается до 1, включая нулевое копирование на необработанном TCP-потоке. Если на транспорте обнаружен TLS, начальное значение — 3.

  2. Отклонение аутентификации SOCKS4: SOCKS4 не поддерживает аутентификацию по паролю. Если сервер требует аутентификацию, соединения SOCKS4 немедленно отклоняются.

Исходный код: proxy/socks/protocol.go:50-53

  1. UDP не поддерживается в HTTP-прокси: Исходящий HTTP явно отклоняет UDP-назначения.

Исходный код: proxy/http/client.go:80-82

  1. DispatchLink vs Dispatch: Для TCP CONNECT и SOCKS TCP входящий обработчик использует dispatcher.DispatchLink(), который блокирует до завершения соединения, вместо Dispatch(), который возвращает Link для ручного копирования. Это упрощает управление жизненным циклом.

Исходный код: proxy/socks/server.go:163-169

Технический анализ для целей повторной реализации.