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 设备]
        C --> D[netBindClient]
        D --> E[internet.Dialer]
    end
    E --> F[WireGuard 对端 UDP]

    subgraph "入站(服务器模式)"
        G[UDP 监听器] --> H[netBindServer]
        H --> I[wireguard-go 设备]
        I --> J[gVisor netstack]
        J --> K[TCP/UDP 转发器]
        K --> L[路由分发器]
    end

关键组件

组件文件用途
Handler(出站)proxy/wireguard/client.go出站 WireGuard 隧道
Server(入站)proxy/wireguard/server.go入站 WireGuard 端点
Tunnel 接口proxy/wireguard/tun.go抽象 TUN 设备
gvisorNetproxy/wireguard/tun.go基于 gVisor 的虚拟 TUN
netBindClientproxy/wireguard/bind.go客户端 UDP 传输
netBindServerproxy/wireguard/bind.go服务端 UDP 传输
gvisortunproxy/wireguard/gvisortun/gVisor 网络栈设置

隧道接口

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 配置字符串后,隧道为 TCP 和 UDP 提供标准的 Go net.Conn 接口。

gVisor 网络栈

主要实现使用 gVisor 的用户空间 TCP/IP 栈:

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 设备在首次使用时延迟初始化,当拨号器变更时重新创建:

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  // Already initialized with same dialer
    }
    // Create new bind and tunnel
    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. Initialize/reuse WireGuard tunnel
    h.processWireGuard(ctx, dialer)

    // 2. Resolve DNS if destination is a domain
    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. Dial through the virtual tunnel
    if command == protocol.RequestCommandTCP {
        conn, err := h.net.DialContextTCPAddrPort(ctx, addrPort)
        // Copy data bidirectionally
    } else {
        conn, err := h.net.DialUDPAddrPort(netip.AddrPort{}, addrPort)
        // Copy data bidirectionally
    }
}

源码: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 或 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
            // Feed packet to 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)
    // Bidirectional copy between tunnel conn and dispatched link
    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  // Server always uses gVisor
    }
    if c.NoKernelTun {
        return createGVisorTun
    }
    if kernelTunSupported, _ := KernelTunSupported(); !kernelTunSupported {
        return createGVisorTun
    }
    return createKernelTun  // Linux only
}

源码: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() 调用时初始化,并在后续连接中复用。如果拨号器发生变更(如不同的出站路由),隧道会被重新创建。

  2. Reserved 字节Reserved 配置字段在 WireGuard UDP 头部设置 3 个字节,某些提供商(如 Cloudflare WARP)用于路由。

源码:proxy/wireguard/client.go:129

  1. 线程安全:处理器使用 sync.MutexwgLock)保护隧道的创建和切换。多个并发连接共享同一个隧道。

  2. 连接生命周期:通过隧道的每个 TCP/UDP 连接使用标准 Xray 超时策略独立管理。隧道本身在连接之间持续存在。

  3. gVisor TCP 保活:通过服务端转发器的 TCP 连接启用了保活,以防止挂起的连接:

go
ep.SocketOptions().SetKeepAlive(true)

源码:proxy/wireguard/tun.go:168

  1. UDP 延迟关闭:服务端 UDP 端点有 15 秒的延迟关闭超时,以确保及时释放资源:
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 的设置反映了这一点。

用于重新实现目的的技术分析。