Skip to content

Транспорт TCP

Введение

TCP-транспорт является стандартным и наиболее фундаментальным транспортом в Xray-core. Он обеспечивает сырые TCP-соединения с опциональной безопасностью TLS/REALITY, обфускацией HTTP-заголовков и расширенной настройкой на уровне сокетов. В этом документе рассматриваются процессы dial/listen, поддержка параметров сокетов, механизм Happy Eyeballs для двухстекового соединения и поддержка прозрачного прокси (TProxy).

Регистрация протокола

TCP-транспорт регистрируется под именем "tcp" (transport/internet/tcp/tcp.go:3):

go
const protocolName = "tcp"

Регистрация происходит в трех функциях init():

  • Дайлер: tcp/dialer.go:110-112 -- RegisterTransportDialer("tcp", Dial)
  • Слушатель: tcp/hub.go:138-140 -- RegisterTransportListener("tcp", ListenTCP)
  • Создатель конфигурации: tcp/config.go:8-12 -- RegisterProtocolConfigCreator("tcp", ...)

Процесс Dial

Точка входа

tcp.Dial (tcp/dialer.go:20-108) вызывается уровнем диспетчеризации транспорта:

go
func Dial(ctx context.Context, dest net.Destination,
    streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) {
    conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
    if err != nil {
        return nil, err
    }
    // Применение TLS или REALITY...
    // Применение аутентификатора заголовков...
    return stat.Connection(conn), nil
}

Применение уровня безопасности

После установления сырого TCP-соединения безопасность применяется в порядке приоритета (tcp/dialer.go:27-93):

  1. TLS: Если tls.ConfigFromStreamSettings возвращает не nil, выполняется TLS-рукопожатие. Если настроен отпечаток, используется uTLS (tls.UClient), в противном случае -- стандартный Go TLS (tls.Client).
  2. REALITY: Если reality.ConfigFromStreamSettings возвращает не nil (и TLS не настроен), выполняется REALITY-рукопожатие через reality.UClient.

Обфускация заголовков

После применения безопасности опциональный ConnectionAuthenticator оборачивает соединение (tcp/dialer.go:95-106):

go
tcpSettings := streamSettings.ProtocolSettings.(*Config)
if tcpSettings.HeaderSettings != nil {
    headerConfig, _ := tcpSettings.HeaderSettings.GetInstance()
    auth, _ := internet.CreateConnectionAuthenticator(headerConfig)
    conn = auth.Client(conn)
}

Это позволяет использовать обфускацию HTTP-заголовков, при которой TCP-поток оборачивается так, чтобы выглядеть как обычный HTTP-трафик.

Процесс Listen

Точка входа

tcp.ListenTCP (tcp/hub.go:30-95) создает TCP-слушатель:

go
func ListenTCP(ctx context.Context, address net.Address, port net.Port,
    streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) {
    // ...
    listener, err = internet.ListenSystem(ctx, &net.TCPAddr{
        IP:   address.IP(),
        Port: int(port),
    }, streamSettings.SocketSettings)
    // Настройка TLS / REALITY / аутентификации заголовков
    go l.keepAccepting()
    return l, nil
}

Цикл приема соединений

Горутина keepAccepting (tcp/hub.go:97-126) принимает соединения и применяет безопасность:

go
func (v *Listener) keepAccepting() {
    for {
        conn, err := v.listener.Accept()
        // ...
        go func() {
            if v.tlsConfig != nil {
                conn = tls.Server(conn, v.tlsConfig)
            } else if v.realityConfig != nil {
                conn, err = reality.Server(conn, v.realityConfig)
            }
            if v.authConfig != nil {
                conn = v.authConfig.Server(conn)
            }
            v.addConn(stat.Connection(conn))
        }()
    }
}

Поддержка Unix Domain Socket

И dial, и listen поддерживают Unix domain sockets. Слушатель проверяет port == 0 для переключения в режим Unix (tcp/hub.go:44-55).

Протокол PROXY

TCP-слушатель объединяет прием PROXY-протокола из конфигурации TCP и настроек сокета (tcp/hub.go:37-41):

go
streamSettings.SocketSettings.AcceptProxyProtocol =
    l.config.AcceptProxyProtocol || streamSettings.SocketSettings.AcceptProxyProtocol

При включении internet.ListenSystem оборачивает слушатель в proxyproto.Listener (system_listener.go:170-173).

Системный дайлер: параметры сокетов

DefaultSystemDialer.Dial (transport/internet/system_dialer.go:51-146) -- это место, где создаются сырые соединения на уровне ОС. Он настраивает:

  • Keep-Alive: Значения по умолчанию, аналогичные Chrome (45 секунд простоя, 45 секунд интервал) через net.KeepAliveConfig Go 1.24+ (system_dialer.go:91-117)
  • Multipath TCP: dialer.SetMultipathTCP(true) при установленном TcpMptcp (system_dialer.go:121-123)
  • Управление сокетом: Callback Control применяет платформо-зависимые параметры через applyOutboundSocketOptions (system_dialer.go:124-143)

Параметры сокетов Linux (sockopt_linux.go)

Реализация для Linux (transport/internet/sockopt_linux.go:43-138) поддерживает:

ПараметрСистемный вызовПоле конфигурации
SO_MARKSOL_SOCKET, SO_MARKconfig.Mark
SO_BINDTODEVICEBindToDeviceconfig.Interface
TCP_FASTOPEN_CONNECTSOL_TCP, TCP_FASTOPEN_CONNECTconfig.Tfo (исходящие)
TCP_FASTOPENSOL_TCP, TCP_FASTOPENconfig.Tfo (входящие)
TCP_CONGESTIONSOL_TCP, TCP_CONGESTIONconfig.TcpCongestion
TCP_WINDOW_CLAMPIPPROTO_TCP, TCP_WINDOW_CLAMPconfig.TcpWindowClamp
TCP_USER_TIMEOUTIPPROTO_TCP, TCP_USER_TIMEOUTconfig.TcpUserTimeout
TCP_MAXSEGIPPROTO_TCP, TCP_MAXSEGconfig.TcpMaxSeg
IP_TRANSPARENTSOL_IP, IP_TRANSPARENTconfig.Tproxy
IPV6_RECVORIGDSTADDRРазличныеconfig.ReceiveOriginalDestAddress
CustomПользовательские level/optconfig.CustomSockopt

Параметры сокетов macOS (sockopt_darwin.go)

Реализация для Darwin (transport/internet/sockopt_darwin.go:103-192) имеет платформо-зависимые отличия:

  • TCP_FASTOPEN использует платформенные константы: TCP_FASTOPEN_SERVER = 0x01, TCP_FASTOPEN_CLIENT = 0x02 (sockopt_darwin.go:19-21)
  • Привязка к интерфейсу использует IP_BOUND_IF / IPV6_BOUND_IF вместо SO_BINDTODEVICE (sockopt_darwin.go:136-151)
  • Прозрачный прокси использует /dev/pf и ioctl DIOCNATLOOK для определения исходного адреса назначения (sockopt_darwin.go:40-101)

Разбор значения TFO

Значение конфигурации TFO разбирается особым образом (transport/internet/sockopt.go:21-30):

go
func (v *SocketConfig) ParseTFOValue() int {
    if v.Tfo == 0 { return -1 }  // не задано
    tfo := int(v.Tfo)
    if tfo < 0 { tfo = 0 }       // явно отключено
    return tfo
}

На Linux для исходящих соединений любое положительное значение становится 1 для TCP_FASTOPEN_CONNECT. На Linux для входящих соединений значение передается напрямую в TCP_FASTOPEN как длина очереди.

Пользовательские параметры сокетов

Механизм CustomSockopt (sockopt_linux.go:93-129) позволяет выполнять произвольные вызовы setsockopt:

go
for _, custom := range config.CustomSockopt {
    if custom.System != "" && custom.System != runtime.GOOS { continue }
    if !strings.HasPrefix(network, custom.Network) { continue }
    // Применение int или string setsockopt
}

Поддерживается фильтрация по ОС и типу сети (например, "tcp" соответствует tcp4/tcp6).

Happy Eyeballs (RFC 8305)

Когда разрешено несколько IP-адресов и включен Happy Eyeballs, TcpRaceDial (transport/internet/happy_eyeballs.go:16-97) реализует RFC 8305:

Сортировка IP-адресов

sortIPs (happy_eyeballs.go:100-156) чередует адреса IPv4 и IPv6:

go
func sortIPs(ips []net.IP, prioritizeIPv6 bool, interleave uint32) []net.IP {
    // Разделение на массивы ip4 и ip6
    // Чередование: попеременное ip4/ip6 на основе значения interleave
    // prioritizeIPv6 определяет, какое семейство идет первым
}

При значении interleave=1 по умолчанию результат выглядит так: [v6, v4, v6, v4, ...] (или [v4, v6, ...], если IPv6 не приоритизирован).

Алгоритм Race Dial

mermaid
sequenceDiagram
    participant C as TcpRaceDial
    participant T as Timer
    participant G1 as Горутина 1
    participant G2 as Горутина 2
    participant G3 as Горутина 3

    C->>T: Старт (задержка 0 мс для первого)
    T->>G1: tcpTryDial(IP[0])
    T->>T: Reset(tryDelayMs)
    T->>G2: tcpTryDial(IP[1])
    G1-->>C: result{conn, nil}
    C->>C: Отмена контекста (остановка остальных)
    C->>C: Ожидание активных горутин
    C-->>C: Возврат выигравшего соединения

Ключевые параметры из конфигурации HappyEyeballs:

  • TryDelayMs: Задержка между началом каждой новой попытки (по умолчанию 250 мс согласно RFC 8305)
  • MaxConcurrentTry: Максимальное количество одновременных попыток соединения
  • PrioritizeIpv6: Должен ли IPv6 идти первым в чередующемся списке
  • Interleave: Сколько адресов одного семейства перед переключением

Первое успешное соединение побеждает. Все остальные закрываются. Алгоритм корректно обрабатывает отмену и неудачи (happy_eyeballs.go:35-96).

Условия активации

Happy Eyeballs используется только когда выполнены все условия (dialer.go:263):

  • Конфигурация HappyEyeballs не nil
  • TryDelayMs > 0 и MaxConcurrentTry > 0
  • Разрешено минимум 2 IP-адреса
  • DialerProxy не настроен
  • Назначение -- TCP

Прозрачный прокси (TProxy)

Linux

На Linux TProxy работает путем установки IP_TRANSPARENT на сокетах (sockopt_linux.go:131-135), что позволяет процессу привязываться к нелокальным адресам. Восстановление исходного адреса назначения использует SO_ORIGINAL_DST (tcp/sockopt_linux.go:18-52):

go
func GetOriginalDestination(conn stat.Connection) (net.Destination, error) {
    // Использует getsockopt(SO_ORIGINAL_DST) для восстановления исходного адреса
    // назначения из перенаправленного соединения (через iptables REDIRECT/TPROXY)
}

macOS

На macOS исходный адрес назначения восстанавливается через PF (sockopt_darwin.go:40-101) с использованием ioctl DIOCNATLOOK на /dev/pf.

Системный слушатель

DefaultListener.Listen (transport/internet/system_listener.go:78-175) создает слушатели на уровне ОС с:

  • Управление сокетом: Применяет applyInboundSocketOptions через getControlFunc (system_listener.go:24-42)
  • SO_REUSEPORT: Всегда устанавливается на слушателях (system_listener.go:39)
  • Unix domain sockets: Поддерживает абстрактные сокеты (префикс @ в Linux), права доступа к файлам и блокировку файлов (system_listener.go:115-166)
  • Протокол PROXY: Оборачивается proxyproto.Listener при включении (system_listener.go:170-173)
  • Multipath TCP: lc.SetMultipathTCP(true) при настройке (system_listener.go:111-113)

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

  • Протокол по умолчанию: Когда транспорт не указан, используется TCP (config.go:59-63).
  • Обертка UDP: Для UDP-назначений DefaultSystemDialer создает PacketConnWrapper, реализующий net.Conn поверх net.PacketConn (system_dialer.go:54-89).
  • Значения keep-alive по умолчанию: Дайлер повторяет поведение keep-alive Chrome: 45 секунд простоя + 45 секунд интервал (system_dialer.go:91-96). Слушатели по умолчанию имеют keep-alive отключенным (system_listener.go:92).
  • FakePacketConn: Используется для обертки TCP-соединений как PacketConn в сценариях QUIC-поверх-TCP (system_dialer.go:258-283).
  • Таймаут дайлера: Жестко задан как 16 секунд (system_dialer.go:113).
  • REALITY на слушателе: Когда настроен REALITY, слушатель запускает горутину для DetectPostHandshakeRecordsLens (tcp/hub.go:78).

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