Skip to content

Путь пакета

На этой странице прослеживается полный жизненный цикл соединения через Xray-core — с момента подключения клиента до момента, когда данные достигают удалённого сервера.

Обзор

mermaid
flowchart TB
    Client([Клиентское приложение]) -->|"Подключение к<br/>порту прослушивания"| Listener

    subgraph Inbound["Входящий (app/proxyman/inbound)"]
        Listener["internet.Listener<br/>(TCP Hub)"]
        Worker["tcpWorker / udpWorker"]
        Proxy["proxy.Inbound.Process()<br/>(VLESS/VMess/Trojan/...)"]
    end

    subgraph Core["Основной конвейер"]
        Dispatcher["DefaultDispatcher.Dispatch()"]
        Sniff["Sniffer<br/>(HTTP/TLS/QUIC/FakeDNS)"]
        Router["Router.PickRoute()"]
    end

    subgraph Outbound["Исходящий (app/proxyman/outbound)"]
        OHandler["outbound.Handler.Dispatch()"]
        Mux["Mux ClientManager<br/>(если mux включён)"]
        OProxy["proxy.Outbound.Process()<br/>(VLESS/Freedom/...)"]
        Transport["internet.Dialer.Dial()<br/>(TCP/WS/gRPC/...)"]
    end

    Listener -->|stat.Connection| Worker
    Worker -->|"создание ctx + вызов"| Proxy
    Proxy -->|"dispatcher.Dispatch(ctx, dest)"| Dispatcher
    Dispatcher --> Sniff
    Sniff --> Router
    Router -->|тег исходящего| OHandler
    OHandler --> Mux
    Mux --> OProxy
    OProxy --> Transport
    Transport -->|"зашифрованное соединение"| Server([Удалённый/Целевой сервер])

Фаза 1: Приём соединения

TCP Worker (app/proxyman/inbound/worker.go)

Когда приходит TCP-соединение, срабатывает метод tcpWorker.callback():

go
func (w *tcpWorker) callback(conn stat.Connection) {
    ctx, cancel := context.WithCancel(w.ctx)
    sid := session.NewID()
    ctx = c.ContextWithID(ctx, sid)

    // Создание метаданных исходящего
    outbounds := []*session.Outbound{{}}

    // Для прозрачного прокси: получение исходного назначения
    if w.recvOrigDest {
        switch getTProxyType(w.stream) {
        case internet.SocketConfig_Redirect:
            dest, _ = tcp.GetOriginalDestination(conn)
        case internet.SocketConfig_TProxy:
            dest = net.DestinationFromAddr(conn.LocalAddr())
        }
        outbounds[0].Target = dest
    }
    ctx = session.ContextWithOutbounds(ctx, outbounds)

    // Прикрепление метаданных входящего
    ctx = session.ContextWithInbound(ctx, &session.Inbound{
        Source:  net.DestinationFromAddr(conn.RemoteAddr()),
        Gateway: net.TCPDestination(w.address, w.port),
        Tag:     w.tag,
        Conn:    conn,
    })

    // Прикрепление конфигурации перехвата
    content := new(session.Content)
    content.SniffingRequest = ... // из конфигурации
    ctx = session.ContextWithContent(ctx, content)

    // Передача обработчику протокола
    w.proxy.Process(ctx, net.Network_TCP, conn, w.dispatcher)
}

Ключевые значения контекста, устанавливаемые здесь:

  • session.Inbound — адрес источника, тег входящего обработчика, сырое соединение
  • session.Outbound — цель (заполняется для TProxy/redirect)
  • session.Content — конфигурация перехвата

UDP Worker

Для UDP udpWorker обрабатывает пакеты иначе:

  • Использует udp.Dispatcher для управления UDP-«соединениями» (с ключом по источнику)
  • Каждый уникальный источник получает виртуальное соединение, направляемое через прокси
  • Очистка по таймауту для неактивных UDP-сессий

Фаза 2: Обработка протокола (входящий)

Каждый прокси-протокол реализует интерфейс proxy.Inbound:

go
type Inbound interface {
    Network() []net.Network
    Process(ctx context.Context, network net.Network,
        conn stat.Connection, dispatcher routing.Dispatcher) error
}

Обработчик протокола:

  1. Читает и декодирует заголовок протокола из conn
  2. Извлекает целевое назначение (адрес + порт)
  3. Аутентифицирует пользователя (если применимо)
  4. Вызывает dispatcher.Dispatch(ctx, destination) для получения пары pipe
  5. Двунаправленно копирует данные между conn и pipe

Пример: входящий VLESS (упрощённо)

go
func (h *Handler) Process(ctx, network, connection, dispatch) error {
    // Чтение первых байтов
    first := buf.FromBytes(make([]byte, buf.Size))
    first.ReadFrom(connection)

    // Декодирование заголовка VLESS
    userSentID, request, requestAddons, err :=
        encoding.DecodeRequestHeader(first, reader, h.validator)

    // Установка пользователя в контексте
    ctx = session.ContextWithInbound(ctx, &session.Inbound{
        User: user,
        ...
    })

    // Отправка в маршрутизацию
    link, _ := dispatch.Dispatch(ctx, request.Destination())

    // Двунаправленное копирование
    // Загрузка: connection -> link.Writer (к исходящему)
    // Скачивание: link.Reader -> connection (к клиенту)
    task.Run(ctx, requestDone, responseDone)
}

Фаза 3: Диспетчеризация

DefaultDispatcher.Dispatch() — центральный узел (app/dispatcher/default.go):

go
func (d *DefaultDispatcher) Dispatch(ctx, destination) (*transport.Link, error) {
    // Установка цели в метаданных исходящего
    ob.OriginalTarget = destination
    ob.Target = destination

    // Создание пары pipe
    inbound, outbound := d.getLink(ctx)

    if sniffingRequest.Enabled {
        go func() {
            // Оборачивание reader с кешированием
            cReader := &cachedReader{reader: outbound.Reader}
            outbound.Reader = cReader

            // Перехват первых байтов
            result, err := sniffer(ctx, cReader, ...)

            // Переопределение назначения при совпадении перехвата
            if d.shouldOverride(ctx, result, ...) {
                destination.Address = net.ParseAddress(result.Domain())
                ob.Target = destination // или ob.RouteTarget для RouteOnly
            }

            d.routedDispatch(ctx, outbound, destination)
        }()
    } else {
        go d.routedDispatch(ctx, outbound, destination)
    }

    return inbound, nil  // возвращается входящему прокси
}

Пара pipe

getLink() создаёт две связанные пары pipe:

Клиент <-> [InboundLink] <-> Pipe <-> [OutboundLink] <-> Сервер

InboundLink:                    OutboundLink:
  Reader = downlinkReader         Reader = uplinkReader
  Writer = uplinkWriter           Writer = downlinkWriter

Клиент пишет -> uplinkWriter -> uplinkReader -> Сервер читает
Сервер пишет -> downlinkWriter -> downlinkReader -> Клиент читает

Если статистика включена, вставляются обёртки SizeStatWriter для подсчёта байтов.

Фаза 4: Маршрутизация

routedDispatch() выбирает исходящий обработчик:

go
func (d *DefaultDispatcher) routedDispatch(ctx, link, destination) {
    // 1. Проверка принудительного тега исходящего (из платформы/API)
    if forcedTag := session.GetForcedOutboundTagFromContext(ctx); forcedTag != "" {
        handler = d.ohm.GetHandler(forcedTag)
    }
    // 2. Запрос к маршрутизатору для выбора маршрута
    else if route, err := d.router.PickRoute(routingCtx); err == nil {
        handler = d.ohm.GetHandler(route.GetOutboundTag())
    }
    // 3. Откат к исходящему обработчику по умолчанию
    else {
        handler = d.ohm.GetDefaultHandler()
    }

    // Отправка выбранному исходящему обработчику
    handler.Dispatch(ctx, link)
}

Маршрутизатор последовательно оценивает правила (см. Движок маршрутизации).

Фаза 5: Обработка исходящего

Исходящий обработчик (app/proxyman/outbound/handler.go)

Обёртка исходящего обработчика:

go
func (h *Handler) Dispatch(ctx, link) {
    // Проверка mux
    if h.mux != nil && shouldUseMux(ctx) {
        h.mux.Dispatch(ctx, link)
        return
    }

    // Прямая обработка прокси
    h.proxy.Process(ctx, link, h)  // h реализует internet.Dialer
}

Установка транспортного соединения

Когда proxy.Process() вызывает dialer.Dial(ctx, dest):

  1. Поиск настроек потока для исходящего
  2. Выбор транспортного дайлера (TCP/WS/gRPC и т. д.)
  3. Установка сырого соединения
  4. Применение уровня безопасности (TLS/REALITY/без шифрования)
  5. Возврат stat.Connection

Обработка исходящего прокси

Исходящий прокси кодирует свой протокол и копирует данные:

go
func (h *Handler) Process(ctx, link, dialer) error {
    // Установка транспортного соединения
    conn, _ := dialer.Dial(ctx, serverAddress)

    // Кодирование заголовка протокола
    encoding.EncodeRequestHeader(conn, request, addons)

    // Двунаправленное копирование
    // Загрузка: link.Reader -> conn (к серверу)
    // Скачивание: conn -> link.Writer (к клиенту через pipe)
    task.Run(ctx, postRequest, getResponse)
}

Полная диаграмма последовательности

mermaid
sequenceDiagram
    participant C as Клиент
    participant TW as tcpWorker
    participant PI as Входящий прокси
    participant D as Диспетчер
    participant R as Маршрутизатор
    participant PO as Исходящий прокси
    participant T as Транспорт
    participant S as Удалённый сервер

    C->>TW: TCP-подключение
    TW->>TW: Создание контекста сессии
    TW->>PI: Process(ctx, conn, dispatcher)
    PI->>PI: Декодирование заголовка протокола
    PI->>D: Dispatch(ctx, destination)
    D->>D: Создание пары pipe
    D-->>PI: возврат inboundLink
    Note over D: асинхронная горутина:
    D->>D: Перехват первых байтов
    D->>R: PickRoute(ctx)
    R-->>D: тег исходящего
    D->>PO: handler.Dispatch(ctx, outboundLink)
    PO->>T: dialer.Dial(ctx, server)
    T->>S: Транспортное подключение + TLS
    T-->>PO: conn
    PO->>S: Кодирование заголовка + полезная нагрузка

    par Загрузка (клиент -> сервер)
        PI->>D: pipe.Write (данные клиента)
        D->>PO: pipe.Read -> conn.Write
    and Скачивание (сервер -> клиент)
        S->>PO: conn.Read
        PO->>D: pipe.Write (данные сервера)
        D->>PI: pipe.Read -> conn.Write
        PI->>C: данные ответа
    end

Заметки по реализации

При повторной реализации критически важными являются следующие части:

  1. Контекст сессии — содержит все метаданные; должен передаваться через каждый вызов
  2. Пара pipe — асинхронный мост между входящим и исходящим; необходимо обратное давление
  3. Перехват — должен выполняться на первых байтах до маршрутизации; потреблённые байты необходимо кешировать
  4. Двунаправленное копирование — две горутины (загрузка + скачивание) с общей отменой
  5. Таймер активности — сбрасывается при каждой передаче данных; запускает закрытие при истечении таймаута бездействия

Паттерн task.Run(ctx, postRequest, task.OnSuccess(getResponse, task.Close(writer))) используется повсеместно: сначала выполняется загрузка, затем при успехе начинается скачивание, writer закрывается по завершении скачивания.

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