Skip to content

Обзор архитектуры транспортного уровня

Введение

Транспортный уровень Xray-core предоставляет подключаемую абстракцию для сетевого взаимодействия. Каждый транспортный протокол (TCP, WebSocket, gRPC, HTTPUpgrade, SplitHTTP, mKCP) регистрируется в глобальном реестре при запуске, и ядро направляет вызовы dial/listen на основе конфигурации. Параллельный уровень безопасности (TLS, REALITY) оборачивает транспорты независимо. В этом документе рассматриваются паттерн реестра, основные интерфейсы, объект конфигурации MemoryStreamConfig и механизм выбора транспортов во время выполнения.

Ключевые интерфейсы

Интерфейс Dialer

Интерфейс Dialer (transport/internet/dialer.go:22-31) -- это высокоуровневая абстракция для исходящих соединений:

go
type Dialer interface {
    Dial(ctx context.Context, destination net.Destination) (stat.Connection, error)
    DestIpAddress() net.IP
    SetOutboundGateway(ctx context.Context, ob *session.Outbound)
}

Внутри транспортный уровень использует низкоуровневую сигнатуру функции для дайлеров каждого протокола:

go
// dialer.go:34
type dialFunc func(ctx context.Context, dest net.Destination, streamSettings *MemoryStreamConfig) (stat.Connection, error)

Интерфейс Listener

Интерфейс Listener (transport/internet/tcp_hub.go:25-28) представляет серверный слушатель:

go
type Listener interface {
    Close() error
    Addr() net.Addr
}

Каждый транспорт также регистрирует ListenFunc (tcp_hub.go:23):

go
type ListenFunc func(ctx context.Context, address net.Address, port net.Port,
    settings *MemoryStreamConfig, handler ConnHandler) (Listener, error)

Где ConnHandler -- это просто func(stat.Connection).

Интерфейс SystemDialer

SystemDialer (transport/internet/system_dialer.go:18-21) -- это абстракция самого низкого уровня, которая устанавливает фактические соединения на уровне ОС:

go
type SystemDialer interface {
    Dial(ctx context.Context, source net.Address, destination net.Destination,
        sockopt *SocketConfig) (net.Conn, error)
    DestIpAddress() net.IP
}

Реализация по умолчанию (DefaultSystemDialer) вызывает net.Dialer.DialContext из Go с применением параметров сокета через RawConn.Control.

Паттерн реестра

Реестр транспортных дайлеров

Каждый транспорт регистрирует свой дайлер в функции init() с помощью RegisterTransportDialer (dialer.go:39-45):

go
var transportDialerCache = make(map[string]dialFunc)

func RegisterTransportDialer(protocol string, dialer dialFunc) error {
    if _, found := transportDialerCache[protocol]; found {
        return errors.New(protocol, " dialer already registered").AtError()
    }
    transportDialerCache[protocol] = dialer
    return nil
}

Пример из transport/internet/tcp/dialer.go:110-112:

go
func init() {
    common.Must(internet.RegisterTransportDialer(protocolName, Dial))
}

Реестр транспортных слушателей

Аналогично слушатели регистрируются через RegisterTransportListener (tcp_hub.go:13-19):

go
var transportListenerCache = make(map[string]ListenFunc)

func RegisterTransportListener(protocol string, listener ListenFunc) error {
    if _, found := transportListenerCache[protocol]; found {
        return errors.New(protocol, " listener already registered.").AtError()
    }
    transportListenerCache[protocol] = listener
    return nil
}

Реестр создателей конфигураций

Каждый транспорт также регистрирует фабрику конфигурации (config.go:32-38):

go
var globalTransportConfigCreatorCache = make(map[string]ConfigCreator)

func RegisterProtocolConfigCreator(name string, creator ConfigCreator) error {
    if _, found := globalTransportConfigCreatorCache[name]; found {
        return errors.New("protocol ", name, " is already registered").AtError()
    }
    globalTransportConfigCreatorCache[name] = creator
    return nil
}

Это вызывается из функции init() в файле config.go каждого транспорта, например transport/internet/tcp/config.go:8-12.

Зарегистрированные имена протоколов

ТранспортИмя протоколаФайл дайлераФайл слушателя
TCP"tcp"tcp/dialer.gotcp/hub.go
WebSocket"websocket"websocket/dialer.gowebsocket/hub.go
gRPC"grpc"grpc/dial.gogrpc/hub.go
HTTPUpgrade"httpupgrade"httpupgrade/dialer.gohttpupgrade/hub.go
SplitHTTP"splithttp"splithttp/dialer.gosplithttp/hub.go
mKCP"mkcp"kcp/dialer.gokcp/listener.go

MemoryStreamConfig

MemoryStreamConfig (transport/internet/memory_settings.go:9-19) -- это разобранная, хранимая в памяти форма protobuf StreamConfig. Она позволяет избежать повторной десериализации protobuf во время выполнения:

go
type MemoryStreamConfig struct {
    Destination      *net.Destination
    ProtocolName     string
    ProtocolSettings interface{}
    SecurityType     string
    SecuritySettings interface{}
    TcpmaskManager   *finalmask.TcpmaskManager
    UdpmaskManager   *finalmask.UdpmaskManager
    SocketSettings   *SocketConfig
    DownloadSettings *MemoryStreamConfig
}

Ключевые поля:

  • ProtocolName: например "tcp", "websocket", "grpc" -- определяет, какой зарегистрированный дайлер/слушатель используется.
  • ProtocolSettings: Конфигурация, специфичная для транспорта (например, *tcp.Config, *websocket.Config). Приводится через утверждение типа.
  • SecurityType / SecuritySettings: "tls" или "reality" с соответствующим объектом конфигурации.
  • SocketSettings: Низкоуровневые параметры сокета (protobuf SocketConfig).
  • DownloadSettings: Используется SplitHTTP для отдельной конфигурации потока загрузки.
  • TcpmaskManager / UdpmaskManager: Уровень обфускации пакетов (finalmask).

Функция преобразования ToMemoryStreamConfig (memory_settings.go:22-78) разбирает protobuf StreamConfig:

go
func ToMemoryStreamConfig(s *StreamConfig) (*MemoryStreamConfig, error) {
    ets, err := s.GetEffectiveTransportSettings()
    // ... строит MemoryStreamConfig из полей StreamConfig
}

Процесс выбора транспорта

Исходящие соединения (Dial)

Основная точка входа -- internet.Dial (dialer.go:48-75):

go
func Dial(ctx context.Context, dest net.Destination, streamSettings *MemoryStreamConfig) (stat.Connection, error) {
    if dest.Network == net.Network_TCP {
        if streamSettings == nil {
            s, _ := ToMemoryStreamConfig(nil)
            streamSettings = s
        }
        protocol := streamSettings.ProtocolName
        dialer := transportDialerCache[protocol]
        if dialer == nil {
            return nil, errors.New(protocol, " dialer not registered")
        }
        return dialer(ctx, dest, streamSettings)
    }
    if dest.Network == net.Network_UDP {
        udpDialer := transportDialerCache["udp"]
        // ...
    }
}

Когда streamSettings равен nil, ToMemoryStreamConfig(nil) создает конфигурацию по умолчанию с ProtocolName = "tcp" (из config.go:58-64).

Входящие соединения (Listen)

Прослушивание осуществляется через ListenTCP или ListenUnix (tcp_hub.go:52-79):

go
func ListenTCP(ctx context.Context, address net.Address, port net.Port,
    settings *MemoryStreamConfig, handler ConnHandler) (Listener, error) {
    // ...
    protocol := settings.ProtocolName
    listenFunc := transportListenerCache[protocol]
    listener, err := listenFunc(ctx, address, port, settings, handler)
    return listener, nil
}

Разрешение конфигурации

Метод StreamConfig.GetEffectiveProtocol (config.go:58-64) по умолчанию возвращает "tcp":

go
func (c *StreamConfig) GetEffectiveProtocol() string {
    if c == nil || len(c.ProtocolName) == 0 {
        return "tcp"
    }
    return c.ProtocolName
}

Настройки, специфичные для транспорта, извлекаются через GetTransportSettingsFor (config.go:71-81), который перебирает список TransportSettings в поисках совпадающего имени протокола. Если совпадение не найдено, создается конфигурация по умолчанию через зарегистрированный ConfigCreator.

Настройки безопасности разрешаются аналогично через GetEffectiveSecuritySettings (config.go:83-90).

Диаграмма архитектуры

mermaid
flowchart TD
    subgraph "Уровень приложения"
        OUT[Обработчик исходящих]
        IN[Обработчик входящих]
    end

    subgraph "Диспетчеризация транспорта"
        DIAL["internet.Dial()"]
        LISTEN["internet.ListenTCP()"]
        MSC[MemoryStreamConfig]
    end

    subgraph "Реестр (map[string]func)"
        DR[transportDialerCache]
        LR[transportListenerCache]
        CR[globalTransportConfigCreatorCache]
    end

    subgraph "Реализации транспортов"
        TCP["tcp.Dial / tcp.ListenTCP"]
        WS["websocket.Dial / websocket.ListenWS"]
        GRPC["grpc.Dial / grpc.Listen"]
        HU["httpupgrade.Dial / httpupgrade.ListenHTTPUpgrade"]
        SH["splithttp.Dial / splithttp.ListenXH"]
        KCP["kcp.DialKCP / kcp.ListenKCP"]
    end

    subgraph "Уровень безопасности"
        TLS["tls.Client / tls.Server"]
        REALITY["reality.UClient / reality.Server"]
    end

    subgraph "Системный уровень"
        SD["internet.DialSystem"]
        SL["internet.ListenSystem"]
        OS["OS net.Dialer / net.ListenConfig"]
    end

    OUT --> DIAL
    IN --> LISTEN
    DIAL --> MSC --> DR
    LISTEN --> MSC --> LR

    DR --> TCP & WS & GRPC & HU & SH & KCP
    LR --> TCP & WS & GRPC & HU & SH & KCP

    TCP --> TLS & REALITY
    TCP --> SD
    WS --> TLS
    WS --> SD
    GRPC --> TLS & REALITY
    GRPC --> SD
    HU --> TLS
    HU --> SD
    SH --> TLS & REALITY
    SH --> SD
    KCP --> TLS
    KCP --> SL

    SD --> OS
    SL --> OS

DomainStrategy и разрешение DNS

Перед установлением соединений на системном уровне DialSystem (dialer.go:227-283) выполняет разрешение DNS в соответствии с настроенной DomainStrategy. Таблица стратегий (config.go:15-28) описывает 11 вариантов:

СтратегияПоведениеПредпочтениеРезервный вариант
AsIsБез разрешения--
UseIPРазрешение обоихIPv4+IPv6Нет
UseIPv4Разрешение v4IPv4Нет
UseIPv6Разрешение v6IPv6Нет
UseIPv4v6Разрешение v4, резерв v6IPv4IPv6
UseIPv6v4Разрешение v6, резерв v4IPv6IPv4
ForceIP/v4/v6/v4v6/v6v4То же, но ошибка при отсутствии результата......

Когда включен Happy Eyeballs и разрешено несколько IP-адресов, вызывается TcpRaceDial для одновременных попыток соединения (см. Транспорт TCP).

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

  • Потокобезопасность: Карты реестра (transportDialerCache, transportListenerCache, globalTransportConfigCreatorCache) записываются только во время init(), поэтому мьютекс для чтения во время выполнения не нужен.
  • Nil StreamConfig: Передача nil в Dial или ListenTCP автоматически создает конфигурацию TCP по умолчанию.
  • DialerProxy: DialSystem (dialer.go:271-280) поддерживает цепочку через другой обработчик исходящих соединений через sockopt.DialerProxy, что позволяет строить топологии прокси-через-прокси.
  • AddressPortStrategy: DialSystem может переопределять адрес/порт назначения, используя DNS-записи SRV или TXT (dialer.go:139-224).
  • Замена SystemDialer: UseAlternativeSystemDialer (system_dialer.go:232-237) позволяет заменить весь системный дайлер, что используется на платформах вроде Android.
  • PacketConnWrapper: UDP-"соединения" на самом деле представляют собой ListenPacket + WriteTo, обернутые в интерфейс net.Conn (system_dialer.go:152-204).

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