Путь пакета
На этой странице прослеживается полный жизненный цикл соединения через Xray-core — с момента подключения клиента до момента, когда данные достигают удалённого сервера.
Обзор
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():
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:
type Inbound interface {
Network() []net.Network
Process(ctx context.Context, network net.Network,
conn stat.Connection, dispatcher routing.Dispatcher) error
}Обработчик протокола:
- Читает и декодирует заголовок протокола из
conn - Извлекает целевое назначение (адрес + порт)
- Аутентифицирует пользователя (если применимо)
- Вызывает
dispatcher.Dispatch(ctx, destination)для получения пары pipe - Двунаправленно копирует данные между
connи pipe
Пример: входящий VLESS (упрощённо)
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):
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() выбирает исходящий обработчик:
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)
Обёртка исходящего обработчика:
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):
- Поиск настроек потока для исходящего
- Выбор транспортного дайлера (TCP/WS/gRPC и т. д.)
- Установка сырого соединения
- Применение уровня безопасности (TLS/REALITY/без шифрования)
- Возврат
stat.Connection
Обработка исходящего прокси
Исходящий прокси кодирует свой протокол и копирует данные:
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)
}Полная диаграмма последовательности
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Заметки по реализации
При повторной реализации критически важными являются следующие части:
- Контекст сессии — содержит все метаданные; должен передаваться через каждый вызов
- Пара pipe — асинхронный мост между входящим и исходящим; необходимо обратное давление
- Перехват — должен выполняться на первых байтах до маршрутизации; потреблённые байты необходимо кешировать
- Двунаправленное копирование — две горутины (загрузка + скачивание) с общей отменой
- Таймер активности — сбрасывается при каждой передаче данных; запускает закрытие при истечении таймаута бездействия
Паттерн task.Run(ctx, postRequest, task.OnSuccess(getResponse, task.Close(writer))) используется повсеместно: сначала выполняется загрузка, затем при успехе начинается скачивание, writer закрывается по завершении скачивания.