WireGuard
Xray-core интегрирует WireGuard как исходящий и входящий протокол, используя реализацию wireguard-go в пространстве пользователя и сетевой стек gVisor (netstack) для создания виртуальных туннельных интерфейсов полностью в пользовательском пространстве. Это позволяет организовать WireGuard-туннелирование без привилегий для TUN-устройства на уровне ОС (на большинстве платформ).
Обзор
- Направление: Входящий + Исходящий
- Транспорт: UDP (протокол WireGuard работает поверх UDP)
- Шифрование: Фреймворк протокола Noise (Curve25519, ChaCha20-Poly1305, BLAKE2s)
- Аутентификация: Пары публичного/приватного ключей + опциональный предварительно распределённый ключ
- UDP: Полная поддержка (через сетевой стек в пространстве пользователя)
- TCP: Полная поддержка (через сетевой стек в пространстве пользователя)
- Ядро TUN: Поддерживается на Linux (опционально, по умолчанию выключено для входящих)
Архитектура
graph TD
subgraph "Процесс Xray-core"
A[Исходящий обработчик] --> B[gVisor netstack]
B --> C[wireguard-go Device]
C --> D[netBindClient]
D --> E[internet.Dialer]
end
E --> F[WireGuard Peer UDP]
subgraph "Входящий (серверный режим)"
G[UDP-слушатель] --> H[netBindServer]
H --> I[wireguard-go Device]
I --> J[gVisor netstack]
J --> K[TCP/UDP Forwarder]
K --> L[Диспетчер маршрутизации]
endКлючевые компоненты
| Компонент | Файл | Назначение |
|---|---|---|
Handler (исходящий) | proxy/wireguard/client.go | Исходящий WireGuard-туннель |
Server (входящий) | proxy/wireguard/server.go | Входящая точка WireGuard |
Интерфейс Tunnel | proxy/wireguard/tun.go | Абстрактное TUN-устройство |
gvisorNet | proxy/wireguard/tun.go | Виртуальный TUN на основе gVisor |
netBindClient | proxy/wireguard/bind.go | UDP-транспорт на стороне клиента |
netBindServer | proxy/wireguard/bind.go | UDP-транспорт на стороне сервера |
Пакет gvisortun | proxy/wireguard/gvisortun/ | Настройка сетевого стека gVisor |
Интерфейс Tunnel
type Tunnel interface {
BuildDevice(ipc string, bind conn.Bind) error
DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (net.Conn, error)
DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error)
Close() error
}Исходный код: proxy/wireguard/tun.go:33-38
Интерфейс Tunnel абстрагирует виртуальное сетевое устройство. После вызова BuildDevice() со строкой IPC-конфигурации туннель предоставляет стандартные Go-интерфейсы net.Conn для TCP и UDP.
Сетевой стек gVisor
Основная реализация использует TCP/IP-стек gVisor в пространстве пользователя:
func createGVisorTun(localAddresses []netip.Addr, mtu int,
handler promiscuousModeHandler) (Tunnel, error) {
tun, n, stack, err := gvisortun.CreateNetTUN(localAddresses, mtu, handler != nil)
// ...
}Исходный код: proxy/wireguard/tun.go:139-144
Промискуитетный режим (сервер)
Когда предоставлена функция handler (серверный режим), gVisor настраивает TCP- и UDP-форвардеры, перехватывающие все входящие пакеты:
// TCP Forwarder
tcpForwarder := tcp.NewForwarder(stack, 0, 65535, func(r *tcp.ForwarderRequest) {
ep, err := r.CreateEndpoint(&wq)
r.Complete(false)
ep.SocketOptions().SetKeepAlive(true)
handler(net.TCPDestination(...), gonet.NewTCPConn(&wq, ep))
})
stack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket)
// UDP Forwarder
udpForwarder := udp.NewForwarder(stack, func(r *udp.ForwarderRequest) {
ep, _ := r.CreateEndpoint(&wq)
ep.SocketOptions().SetLinger(tcpip.LingerOption{Enabled: true, Timeout: 15 * time.Second})
handler(net.UDPDestination(...), gonet.NewUDPConn(&wq, ep))
})
stack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)Исходный код: proxy/wireguard/tun.go:150-201
Исходящий обработчик
Файл: proxy/wireguard/client.go
Инициализация
func New(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
endpoints, hasIPv4, hasIPv6, err := parseEndpoints(conf)
return &Handler{
conf: conf,
dns: d,
endpoints: endpoints,
hasIPv4: hasIPv4,
hasIPv6: hasIPv6,
}, nil
}Исходный код: proxy/wireguard/client.go:62-78
WireGuard-устройство инициализируется лениво при первом использовании и пересоздаётся при смене dialer'а:
func (h *Handler) processWireGuard(ctx context.Context, dialer internet.Dialer) error {
h.wgLock.Lock()
defer h.wgLock.Unlock()
if h.bind != nil && h.bind.dialer == dialer && h.net != nil {
return nil // Уже инициализирован с тем же dialer'ом
}
// Создание нового bind и туннеля
h.bind = &netBindClient{
netBind: netBind{dns: h.dns, ...},
ctx: ctx,
dialer: dialer,
reserved: h.conf.Reserved,
}
h.net, err = h.makeVirtualTun()
}Исходный код: proxy/wireguard/client.go:94-142
Обработка соединений
func (h *Handler) Process(ctx context.Context, link *transport.Link,
dialer internet.Dialer) error {
// 1. Инициализация/повторное использование WireGuard-туннеля
h.processWireGuard(ctx, dialer)
// 2. Разрешение DNS, если назначение — домен
if addr.Family().IsDomain() {
ips, _, err = h.dns.LookupIP(addr.Domain(), dns.IPOption{
IPv4Enable: h.hasIPv4 && h.conf.preferIP4(),
IPv6Enable: h.hasIPv6 && h.conf.preferIP6(),
})
}
// 3. Подключение через виртуальный туннель
if command == protocol.RequestCommandTCP {
conn, err := h.net.DialContextTCPAddrPort(ctx, addrPort)
// Двунаправленное копирование данных
} else {
conn, err := h.net.DialUDPAddrPort(netip.AddrPort{}, addrPort)
// Двунаправленное копирование данных
}
}Исходный код: proxy/wireguard/client.go:145-252
IPC-конфигурация
WireGuard-устройство настраивается через строку IPC-запроса (тот же формат, что и wg set):
func (h *Handler) createIPCRequest() string {
var request strings.Builder
request.WriteString(fmt.Sprintf("private_key=%s\n", h.conf.SecretKey))
for _, peer := range h.conf.Peers {
request.WriteString(fmt.Sprintf("public_key=%s\n", peer.PublicKey))
if peer.PreSharedKey != "" {
request.WriteString(fmt.Sprintf("preshared_key=%s\n", peer.PreSharedKey))
}
request.WriteString(fmt.Sprintf("endpoint=%s:%s\n", addr, port))
for _, ip := range peer.AllowedIps {
request.WriteString(fmt.Sprintf("allowed_ip=%s\n", ip))
}
if peer.KeepAlive != 0 {
request.WriteString(fmt.Sprintf("persistent_keepalive_interval=%d\n", peer.KeepAlive))
}
}
return request.String()
}Исходный код: proxy/wireguard/client.go:272-338
DNS-разрешение конечной точки: Если конечная точка пира — домен, он разрешается с использованием либо предварительно разрешённого IP dialer'а, либо DNS-клиента:
if addr.Family().IsDomain() {
dialerIp := h.bind.dialer.DestIpAddress()
if dialerIp != nil {
addr = net.ParseAddress(dialerIp.String())
} else {
ips, _, _ = h.dns.LookupIP(addr.Domain(), ...)
addr = net.IPAddress(ips[dice.Roll(len(ips))])
}
}Исходный код: proxy/wireguard/client.go:296-321
Входящий обработчик (сервер)
Файл: proxy/wireguard/server.go
Поддержка сетей
Сервер принимает только UDP-соединения (транспортный протокол WireGuard):
func (*Server) Network() []net.Network {
return []net.Network{net.Network_UDP}
}Исходный код: proxy/wireguard/server.go:75-77
Обработка пакетов
Сервер читает UDP-пакеты с транспортного уровня и передаёт их WireGuard-устройству:
func (s *Server) Process(ctx context.Context, network net.Network,
conn stat.Connection, dispatcher routing.Dispatcher) error {
reader := buf.NewPacketReader(conn)
for {
mpayload, err := reader.ReadMultiBuffer()
for _, payload := range mpayload {
v, ok := <-s.bindServer.readQueue
// Передача пакета в wireguard-go
v.bytes = payload.Read(v.buff)
v.endpoint = nep
v.waiter.Done()
}
}
}Исходный код: proxy/wireguard/server.go:80-120
Перенаправление соединений
Когда стек gVisor получает расшифрованные TCP/UDP-соединения из туннеля, обработчик forwardConnection выполняет их диспетчеризацию:
func (s *Server) forwardConnection(dest net.Destination, conn net.Conn) {
defer conn.Close()
link, err := s.info.dispatcher.Dispatch(ctx, dest)
// Двунаправленное копирование между туннельным соединением и диспетчеризованным каналом
task.Run(ctx, requestDone, responseDone)
}Исходный код: proxy/wireguard/server.go:122-190
Выбор режима TUN
Файл: proxy/wireguard/config.go
Обработчик выбирает между ядерным TUN и gVisor TUN:
func (c *DeviceConfig) createTun() tunCreator {
if !c.IsClient {
return createGVisorTun // Сервер всегда использует gVisor
}
if c.NoKernelTun {
return createGVisorTun
}
if kernelTunSupported, _ := KernelTunSupported(); !kernelTunSupported {
return createGVisorTun
}
return createKernelTun // Только Linux
}Исходный код: proxy/wireguard/config.go:33-54
Ядерный TUN (Linux)
Файл: proxy/wireguard/tun_linux.go
На Linux с соответствующими привилегиями может использоваться реальное ядерное TUN-устройство для лучшей производительности. Ядро обрабатывает TCP/IP нативно.
TUN по умолчанию (другие платформы)
Файл: proxy/wireguard/tun_default.go
На не-Linux платформах доступен только gVisor TUN.
Стратегия домена
WireGuard поддерживает стратегии DNS-разрешения с запасным вариантом:
| Стратегия | Предпочтение | Запасной вариант |
|---|---|---|
FORCE_IP | IPv4 + IPv6 | Нет |
FORCE_IP4 | Только IPv4 | Нет |
FORCE_IP6 | Только IPv6 | Нет |
FORCE_IP46 | IPv4 | IPv6 |
FORCE_IP64 | IPv6 | IPv4 |
Исходный код: proxy/wireguard/config.go:9-31
Примечания по реализации
Ленивая инициализация: WireGuard-туннель не создаётся при конфигурации. Он инициализируется при первом вызове
Process()и повторно используется для последующих соединений. Если dialer меняется (например, другая исходящая маршрутизация), туннель пересоздаётся.Зарезервированные байты: Поле
Reservedв конфигурации устанавливает 3 байта в UDP-заголовке WireGuard, используемые некоторыми провайдерами (например, Cloudflare WARP) для маршрутизации.
Исходный код: proxy/wireguard/client.go:129
Потокобезопасность: Обработчик использует
sync.Mutex(wgLock) для защиты создания и переключения туннеля. Множественные одновременные соединения разделяют один туннель.Жизненный цикл соединений: Каждое TCP/UDP-соединение через туннель управляется независимо с использованием стандартных политик тайм-аутов Xray. Сам туннель сохраняется между соединениями.
TCP keep-alive в gVisor: TCP-соединения через серверный форвардер имеют включённый keep-alive для предотвращения зависших соединений:
ep.SocketOptions().SetKeepAlive(true)Исходный код: proxy/wireguard/tun.go:168
- UDP linger: Серверные UDP-конечные точки имеют 15-секундный тайм-аут linger для своевременного освобождения ресурсов:
ep.SocketOptions().SetLinger(tcpip.LingerOption{Enabled: true, Timeout: 15 * time.Second})Исходный код: proxy/wireguard/tun.go:191-193
- Нет шифрования на уровне Xray: WireGuard обрабатывает всё шифрование внутренне через протокол Noise. Xray просто передаёт расшифрованные IP-пакеты через виртуальный сетевой стек. Настройка
CanSpliceCopy = 3отражает это.