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 设备]
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 设备 |
gvisorNet | proxy/wireguard/tun.go | 基于 gVisor 的虚拟 TUN |
netBindClient | proxy/wireguard/bind.go | 客户端 UDP 传输 |
netBindServer | proxy/wireguard/bind.go | 服务端 UDP 传输 |
gvisortun 包 | proxy/wireguard/gvisortun/ | gVisor 网络栈设置 |
隧道接口
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 栈:
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 设备在首次使用时延迟初始化,当拨号器变更时重新创建:
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
处理连接
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 格式相同):
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 客户端进行解析:
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
// 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 处理器进行分发:
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 之间选择:
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_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()调用时初始化,并在后续连接中复用。如果拨号器发生变更(如不同的出站路由),隧道会被重新创建。Reserved 字节:
Reserved配置字段在 WireGuard UDP 头部设置 3 个字节,某些提供商(如 Cloudflare WARP)用于路由。
源码:proxy/wireguard/client.go:129
线程安全:处理器使用
sync.Mutex(wgLock)保护隧道的创建和切换。多个并发连接共享同一个隧道。连接生命周期:通过隧道的每个 TCP/UDP 连接使用标准 Xray 超时策略独立管理。隧道本身在连接之间持续存在。
gVisor TCP 保活:通过服务端转发器的 TCP 连接启用了保活,以防止挂起的连接:
ep.SocketOptions().SetKeepAlive(true)源码:proxy/wireguard/tun.go:168
- UDP 延迟关闭:服务端 UDP 端点有 15 秒的延迟关闭超时,以确保及时释放资源:
ep.SocketOptions().SetLinger(tcpip.LingerOption{Enabled: true, Timeout: 15 * time.Second})源码:proxy/wireguard/tun.go:191-193
- Xray 层无加密:WireGuard 通过 Noise 协议在内部处理所有加密。Xray 只是将解密后的 IP 数据包通过虚拟网络栈传输。
CanSpliceCopy = 3的设置反映了这一点。