Обзор TUN и IP-стека
TUN inbound создаёт виртуальный сетевой интерфейс и использует пользовательский IP-стек для преобразования необработанных IP-пакетов в соединения прикладного уровня, которые Xray-core может маршрутизировать.
Исходный код: proxy/tun/
Архитектура
flowchart LR
App([Приложение]) -->|"IP-пакеты"| TUN["TUN-интерфейс<br/>(ядро)"]
TUN -->|"необработанные L3-кадры"| EP["Link Endpoint<br/>(мост gVisor)"]
EP -->|"L3→L4"| Stack["IP-стек gVisor<br/>(TCP/UDP)"]
Stack -->|"TCP: gonet.TCPConn"| Handler["Обработчик TUN"]
Stack -->|"UDP: необработанные пакеты"| UDPH["Обработчик UDP<br/>(Full-Cone)"]
Handler -->|"DispatchLink()"| Dispatcher["Диспетчер Xray"]
UDPH -->|"HandleConnection()"| Handler
Dispatcher -->|"через маршрутизацию"| Outbound["Исходящее соединение<br/>(Freedom/VLESS/...)"]Компоненты
TUN-интерфейс (tun.go, tun_*.go)
Платформозависимое создание TUN-устройства:
type Tun interface {
Start() error // begin reading/writing
Close() error
// Platform-specific: creates fd, sets MTU, etc.
}
type TunOptions struct {
Name string // e.g., "utun5" (macOS), "tun0" (Linux)
MTU uint32 // default: 1500
}Каждая платформа имеет собственный NewTun():
- Linux (
tun_linux.go): Используетioctlдля создания TUN-устройства - macOS (
tun_darwin.go): Использует системный интерфейсutun - Windows (
tun_windows.go): Использует драйвер Wintun - Android (
tun_android.go): Использует fd, переданный из Java-слоя
IP-стек gVisor (stack_gvisor.go)
Пользовательский TCP/IP-стек обрабатывает необработанные IP-пакеты:
type stackGVisor struct {
ctx context.Context
tun GVisorTun // bridge to TUN device
idleTimeout time.Duration
handler *Handler // connection handler
stack *stack.Stack // gVisor network stack
endpoint stack.LinkEndpoint // link to TUN
}Обработчик (handler.go)
Обработчик связывает соединения gVisor с диспетчером Xray:
type Handler struct {
ctx context.Context
config *Config
stack Stack
policyManager policy.Manager
dispatcher routing.Dispatcher
tag string
sniffingRequest session.SniffingRequest
}Жизненный цикл
sequenceDiagram
participant Config
participant Handler as Обработчик
participant TUN as TUN-устройство
participant Stack as Стек gVisor
participant Dispatcher as Диспетчер
Config->>Handler: Init(ctx, pm, dispatcher)
Handler->>TUN: NewTun(options)
TUN-->>Handler: tunInterface
Handler->>Stack: NewStack(ctx, options, handler)
Stack-->>Handler: stackGVisor
Handler->>Stack: Start()
Stack->>Stack: Создание стека gVisor<br/>(IPv4, IPv6, TCP, UDP)
Stack->>Stack: Установка TCP-форвардера
Stack->>Stack: Установка UDP-форвардера
Handler->>TUN: Start()
TUN->>TUN: Начало чтения пакетов
Note over TUN,Stack: Работа...
TUN->>Stack: Необработанный IP-пакет
Stack->>Stack: Разбор заголовков L3/L4
alt TCP
Stack->>Stack: TCP-рукопожатие
Stack->>Handler: HandleConnection(tcpConn, dest)
else UDP
Stack->>Handler: HandlePacket(src, dst, data)
end
Handler->>Dispatcher: DispatchLink(ctx, dest, link)Обработка TCP
TCP-форвардер gVisor перехватывает все TCP SYN-пакеты:
tcpForwarder := tcp.NewForwarder(ipStack, 0, 65535, func(r *tcp.ForwarderRequest) {
go func(r *tcp.ForwarderRequest) {
var wq waiter.Queue
var id = r.ID()
// Complete TCP 3-way handshake
ep, err := r.CreateEndpoint(&wq)
// Configure socket options
options := ep.SocketOptions()
options.SetKeepAlive(false)
options.SetReuseAddress(true)
// Create Go net.Conn from gVisor endpoint
conn := gonet.NewTCPConn(&wq, ep)
// Dispatch to Xray routing
handler.HandleConnection(conn,
net.TCPDestination(
net.IPAddress(id.LocalAddress.AsSlice()),
net.Port(id.LocalPort),
))
ep.Close()
r.Complete(false)
}(r)
})«Локальный адрес» на стороне gVisor — это адрес назначения исходного пакета (сервер, к которому приложение хотело подключиться).
Обработка UDP (Full-Cone NAT)
Для UDP используется собственный обработчик вместо форвардера gVisor, чтобы обеспечить поддержку Full-Cone NAT. Подробнее см. UDP Full-Cone NAT.
HandleConnection (handler.go:104)
Каждое соединение (TCP или UDP) проходит через эту функцию:
func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) {
defer conn.Close()
ctx := context.WithCancel(t.ctx)
ctx = session.ContextWithInbound(ctx, &session.Inbound{
Name: "tun",
Tag: t.tag,
CanSpliceCopy: 3, // TUN cannot splice
Source: net.DestinationFromAddr(conn.RemoteAddr()),
})
ctx = session.ContextWithContent(ctx, &session.Content{
SniffingRequest: t.sniffingRequest,
})
// Create transport link from connection
link := &transport.Link{
Reader: buf.NewReader(conn),
Writer: buf.NewWriter(conn),
}
// Synchronous dispatch (blocks until transfer completes)
t.dispatcher.DispatchLink(ctx, destination, link)
}Ключевое отличие от других inbound: TUN использует DispatchLink() (синхронный вызов) вместо Dispatch() (асинхронный), поскольку соединение уже установлено gVisor.
Конфигурация стека gVisor
func createStack(ep stack.LinkEndpoint) (*stack.Stack, error) {
opts := stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{
ipv4.NewProtocol,
ipv6.NewProtocol,
},
TransportProtocols: []stack.TransportProtocolFactory{
tcp.NewProtocol,
udp.NewProtocol,
},
HandleLocal: false,
}
gStack := stack.New(opts)
// Create NIC (Network Interface Card)
gStack.CreateNIC(defaultNIC, ep)
// Route ALL traffic through this NIC
gStack.SetRouteTable([]tcpip.Route{
{Destination: header.IPv4EmptySubnet, NIC: defaultNIC},
{Destination: header.IPv6EmptySubnet, NIC: defaultNIC},
})
// Enable spoofing (accept any destination IP)
gStack.SetSpoofing(defaultNIC, true)
// Enable promiscuous (accept any destination MAC)
gStack.SetPromiscuousMode(defaultNIC, true)
// TCP tuning
gStack.SetTransportProtocolOption(tcp.ProtocolNumber,
&tcpip.CongestionControlOption("cubic"))
gStack.SetTransportProtocolOption(tcp.ProtocolNumber,
&tcpip.TCPSACKEnabled(true))
// ... buffer sizes, etc.
}Spoofing + Promiscuous необходимы: без них gVisor принимал бы только пакеты, адресованные его собственному IP, но TUN-трафик имеет произвольные IP-адреса назначения.
Link Endpoint (stack_gvisor_endpoint.go)
Link endpoint связывает обработку пакетов gVisor с файловым дескриптором TUN:
type tunEndpoint struct {
tun Tun
dispatcher stack.NetworkDispatcher
mtu uint32
}- Входящие: Чтение необработанных пакетов из TUN fd → доставка в стек gVisor
- Исходящие: gVisor генерирует ответные пакеты → запись в TUN fd
Замечания по реализации
gVisor — тяжёлая зависимость: Это полноценный пользовательский TCP/IP-стек (~100K+ строк). Можно рассмотреть альтернативы вроде lwIP, smoltcp или платформозависимых API.
TUN — платформозависимый: У каждой ОС свои API для создания TUN. На Android fd передаётся из VPN-сервиса.
Нет слушающего порта: TUN inbound объявляет
Network() []net.Network { return []net.Network{} }— он не слушает ни один порт. Трафик поступает с TUN-устройства.DispatchLink — синхронный: В отличие от
Dispatch(),DispatchLink()блокирует выполнение. Горутина создаётся для каждого соединения (форвардером gVisor).Размеры TCP-буферов имеют значение: Размеры TCP-буферов gVisor (8 МБ RX, 6 МБ TX) настроены для высокой пропускной способности. Слишком маленькие → низкая производительность. Слишком большие → расход памяти.
RACK/TLP отключены:
TCPRecovery(0)отключает восстановление при потерях RACK/TLP, чтобы исправить зависания соединений под высокой нагрузкой — известная особенность gVisor.