Обзор архитектуры транспортного уровня
Введение
Транспортный уровень Xray-core предоставляет подключаемую абстракцию для сетевого взаимодействия. Каждый транспортный протокол (TCP, WebSocket, gRPC, HTTPUpgrade, SplitHTTP, mKCP) регистрируется в глобальном реестре при запуске, и ядро направляет вызовы dial/listen на основе конфигурации. Параллельный уровень безопасности (TLS, REALITY) оборачивает транспорты независимо. В этом документе рассматриваются паттерн реестра, основные интерфейсы, объект конфигурации MemoryStreamConfig и механизм выбора транспортов во время выполнения.
Ключевые интерфейсы
Интерфейс Dialer
Интерфейс Dialer (transport/internet/dialer.go:22-31) -- это высокоуровневая абстракция для исходящих соединений:
type Dialer interface {
Dial(ctx context.Context, destination net.Destination) (stat.Connection, error)
DestIpAddress() net.IP
SetOutboundGateway(ctx context.Context, ob *session.Outbound)
}Внутри транспортный уровень использует низкоуровневую сигнатуру функции для дайлеров каждого протокола:
// 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) представляет серверный слушатель:
type Listener interface {
Close() error
Addr() net.Addr
}Каждый транспорт также регистрирует ListenFunc (tcp_hub.go:23):
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) -- это абстракция самого низкого уровня, которая устанавливает фактические соединения на уровне ОС:
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):
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:
func init() {
common.Must(internet.RegisterTransportDialer(protocolName, Dial))
}Реестр транспортных слушателей
Аналогично слушатели регистрируются через RegisterTransportListener (tcp_hub.go:13-19):
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):
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.go | tcp/hub.go |
| WebSocket | "websocket" | websocket/dialer.go | websocket/hub.go |
| gRPC | "grpc" | grpc/dial.go | grpc/hub.go |
| HTTPUpgrade | "httpupgrade" | httpupgrade/dialer.go | httpupgrade/hub.go |
| SplitHTTP | "splithttp" | splithttp/dialer.go | splithttp/hub.go |
| mKCP | "mkcp" | kcp/dialer.go | kcp/listener.go |
MemoryStreamConfig
MemoryStreamConfig (transport/internet/memory_settings.go:9-19) -- это разобранная, хранимая в памяти форма protobuf StreamConfig. Она позволяет избежать повторной десериализации protobuf во время выполнения:
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:
func ToMemoryStreamConfig(s *StreamConfig) (*MemoryStreamConfig, error) {
ets, err := s.GetEffectiveTransportSettings()
// ... строит MemoryStreamConfig из полей StreamConfig
}Процесс выбора транспорта
Исходящие соединения (Dial)
Основная точка входа -- internet.Dial (dialer.go:48-75):
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):
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":
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).
Диаграмма архитектуры
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 --> OSDomainStrategy и разрешение DNS
Перед установлением соединений на системном уровне DialSystem (dialer.go:227-283) выполняет разрешение DNS в соответствии с настроенной DomainStrategy. Таблица стратегий (config.go:15-28) описывает 11 вариантов:
| Стратегия | Поведение | Предпочтение | Резервный вариант |
|---|---|---|---|
| AsIs | Без разрешения | - | - |
| UseIP | Разрешение обоих | IPv4+IPv6 | Нет |
| UseIPv4 | Разрешение v4 | IPv4 | Нет |
| UseIPv6 | Разрешение v6 | IPv6 | Нет |
| UseIPv4v6 | Разрешение v4, резерв v6 | IPv4 | IPv6 |
| UseIPv6v4 | Разрешение v6, резерв v4 | IPv6 | IPv4 |
| 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).