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
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Команды:
| Значение | Команда | Поддержка |
|---|---|---|
0x01 | TCP CONNECT | Да |
0x02 | TCP BIND | Нет (возвращает 0x07) |
0x03 | UDP ASSOCIATE | Да (если включено) |
0xF0 | Tor Resolve | Обрабатывается как CONNECT |
0xF1 | Tor 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 не поддерживается:
if packet.Byte(2) != 0 /* fragments */ {
return nil, errors.New("discarding fragmented payload.")
}Исходный код: proxy/socks/protocol.go:334-336
Поддержка SOCKS4/4a
Входящий обработчик также обрабатывает SOCKS4/4a на основе байта версии:
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:
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:
- Клиент отправляет CONNECT с командой 0x03
- Сервер отвечает адресом/портом UDP-ретрансляции
- Клиент отправляет UDP-датаграммы на этот адрес
- Сервер принимает данные по UDP-сети, декодирует заголовок SOCKS5 UDP, выполняет диспетчеризацию
- TCP-соединение остаётся открытым как сигнал поддержания связи
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-пакеты:
if s.udpFilter != nil && !s.udpFilter.Check(conn.RemoteAddr()) {
return nil
}Исходный код: proxy/socks/server.go:189-192
Исходящий (клиент)
Файл: proxy/socks/client.go
Клиент выполняет полное рукопожатие SOCKS5 с вышестоящим сервером:
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 (туннелирование)
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 запросов сервер:
- Парсит полный HTTP-запрос с помощью Go
http.ReadRequest() - Извлекает назначение из заголовка
Hostили URL - Передаёт запрос исходящему обработчику
- Поддерживает keep-alive для последовательных запросов на одном соединении
keepAlive := strings.TrimSpace(strings.ToLower(
request.Header.Get("Proxy-Connection"))) == "keep-alive"Исходный код: proxy/http/server.go:163
Аутентификация
Базовая аутентификация проверяется через заголовок Proxy-Authorization:
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-прокси:
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 кешируются для каждого назначения для мультиплексирования:
var cachedH2Conns map[net.Destination]h2ConnИсходный код: proxy/http/client.go:47-48
Обработка ответов 1xx
Сервер корректно обрабатывает промежуточные ответы HTTP 1xx (например, 100 Continue), перенаправляя их клиенту перед чтением фактического ответа:
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
Клиент:
- Считывает первую полезную нагрузку из читателя канала
- Устанавливает HTTP CONNECT туннель к вышестоящему прокси
- Отправляет первую полезную нагрузку сразу после установки туннеля
- Поддерживает пользовательские заголовки через конфигурацию (с поддержкой Go-шаблонов для
Source/Target)
data := struct {
Source net.Destination
Target net.Destination
}{...}
tmpl.Execute(&buf, data)Исходный код: proxy/http/client.go:180-203
Примечания по реализации
- Автоопределение SOCKS/HTTP: Входящий обработчик SOCKS читает первый байт. Если это не
0x04и не0x05, он перенаправляет соединение встроенному HTTP-серверу. Это позволяет одному порту обслуживать как SOCKS5, так и HTTP-прокси.
Исходный код: proxy/socks/server.go:92-95
Поведение CanSpliceCopy: И SOCKS, и HTTP изначально устанавливают
CanSpliceCopy = 2. После рукопожатия протокола (для TCP CONNECT) значение повышается до1, включая нулевое копирование на необработанном TCP-потоке. Если на транспорте обнаружен TLS, начальное значение —3.Отклонение аутентификации SOCKS4: SOCKS4 не поддерживает аутентификацию по паролю. Если сервер требует аутентификацию, соединения SOCKS4 немедленно отклоняются.
Исходный код: proxy/socks/protocol.go:50-53
- UDP не поддерживается в HTTP-прокси: Исходящий HTTP явно отклоняет UDP-назначения.
Исходный код: proxy/http/client.go:80-82
- DispatchLink vs Dispatch: Для TCP CONNECT и SOCKS TCP входящий обработчик использует
dispatcher.DispatchLink(), который блокирует до завершения соединения, вместоDispatch(), который возвращаетLinkдля ручного копирования. Это упрощает управление жизненным циклом.
Исходный код: proxy/socks/server.go:163-169