Skip to content

WireGuard

Xray-core интегрирует WireGuard как исходящий и входящий протокол, используя реализацию wireguard-go в пространстве пользователя и сетевой стек gVisor (netstack) для создания виртуальных туннельных интерфейсов полностью в пользовательском пространстве. Это позволяет организовать WireGuard-туннелирование без привилегий для TUN-устройства на уровне ОС (на большинстве платформ).

Обзор

  • Направление: Входящий + Исходящий
  • Транспорт: UDP (протокол WireGuard работает поверх UDP)
  • Шифрование: Фреймворк протокола Noise (Curve25519, ChaCha20-Poly1305, BLAKE2s)
  • Аутентификация: Пары публичного/приватного ключей + опциональный предварительно распределённый ключ
  • UDP: Полная поддержка (через сетевой стек в пространстве пользователя)
  • TCP: Полная поддержка (через сетевой стек в пространстве пользователя)
  • Ядро TUN: Поддерживается на Linux (опционально, по умолчанию выключено для входящих)

Архитектура

mermaid
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
Интерфейс Tunnelproxy/wireguard/tun.goАбстрактное TUN-устройство
gvisorNetproxy/wireguard/tun.goВиртуальный TUN на основе gVisor
netBindClientproxy/wireguard/bind.goUDP-транспорт на стороне клиента
netBindServerproxy/wireguard/bind.goUDP-транспорт на стороне сервера
Пакет gvisortunproxy/wireguard/gvisortun/Настройка сетевого стека gVisor

Интерфейс Tunnel

go
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 в пространстве пользователя:

go
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-форвардеры, перехватывающие все входящие пакеты:

go
// 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

Инициализация

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'а:

go
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

Обработка соединений

go
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):

go
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-клиента:

go
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):

go
func (*Server) Network() []net.Network {
    return []net.Network{net.Network_UDP}
}

Исходный код: proxy/wireguard/server.go:75-77

Обработка пакетов

Сервер читает UDP-пакеты с транспортного уровня и передаёт их WireGuard-устройству:

go
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 выполняет их диспетчеризацию:

go
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:

go
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_IPIPv4 + IPv6Нет
FORCE_IP4Только IPv4Нет
FORCE_IP6Только IPv6Нет
FORCE_IP46IPv4IPv6
FORCE_IP64IPv6IPv4

Исходный код: proxy/wireguard/config.go:9-31

Примечания по реализации

  1. Ленивая инициализация: WireGuard-туннель не создаётся при конфигурации. Он инициализируется при первом вызове Process() и повторно используется для последующих соединений. Если dialer меняется (например, другая исходящая маршрутизация), туннель пересоздаётся.

  2. Зарезервированные байты: Поле Reserved в конфигурации устанавливает 3 байта в UDP-заголовке WireGuard, используемые некоторыми провайдерами (например, Cloudflare WARP) для маршрутизации.

Исходный код: proxy/wireguard/client.go:129

  1. Потокобезопасность: Обработчик использует sync.Mutex (wgLock) для защиты создания и переключения туннеля. Множественные одновременные соединения разделяют один туннель.

  2. Жизненный цикл соединений: Каждое TCP/UDP-соединение через туннель управляется независимо с использованием стандартных политик тайм-аутов Xray. Сам туннель сохраняется между соединениями.

  3. TCP keep-alive в gVisor: TCP-соединения через серверный форвардер имеют включённый keep-alive для предотвращения зависших соединений:

go
ep.SocketOptions().SetKeepAlive(true)

Исходный код: proxy/wireguard/tun.go:168

  1. UDP linger: Серверные UDP-конечные точки имеют 15-секундный тайм-аут linger для своевременного освобождения ресурсов:
go
ep.SocketOptions().SetLinger(tcpip.LingerOption{Enabled: true, Timeout: 15 * time.Second})

Исходный код: proxy/wireguard/tun.go:191-193

  1. Нет шифрования на уровне Xray: WireGuard обрабатывает всё шифрование внутренне через протокол Noise. Xray просто передаёт расшифрованные IP-пакеты через виртуальный сетевой стек. Настройка CanSpliceCopy = 3 отражает это.

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