How WireGuard Inbound Works in Xray-core: Complete Implementation Guide

The Xray-core WireGuard inbound creates a virtual TUN interface, decrypts UDP packets from WireGuard peers, and forwards clear-text traffic into Xray's standard routing pipeline.

Xray-core's WireGuard inbound implementation allows you to receive encrypted WireGuard traffic and process it through Xray's powerful routing, DNS, and policy engine. This article explains how the WireGuard inbound works internally and provides complete configuration guidance based on the actual source code in XTLS/Xray-core.

Architecture Overview

The WireGuard inbound in Xray-core operates through a layered architecture that bridges WireGuard's encryption with Xray's proxy pipeline.

Core Components

Component Role Source Location
DeviceConfig Holds all WireGuard settings including peers, endpoints, MTU, and domain strategy proxy/wireguard/config.proto
NewServer Entry point that parses endpoints, selects TUN implementation, and builds the WireGuard device proxy/wireguard/server.go
Server.Process Handles incoming UDP packets and pushes them to the read queue proxy/wireguard/server.go
forwardConnection Creates Xray sub-contexts and dispatches decrypted traffic to the routing pipeline proxy/wireguard/server.go
TUN creator Decides between kernel-mode TUN (createKernelTun) or userspace gVisor TUN (createGVisorTun) proxy/wireguard/config.go
netBindServer Implements the bind side of the WireGuard device, handling packet rewriting and queue management proxy/wireguard/server.go

Traffic Flow Through the System

  1. Initialization: Xray reads the inbound configuration and calls wireguard.NewServer in proxy/wireguard/server.go

  2. Endpoint parsing: The parseEndpoints function extracts IPv4/IPv6 endpoints and determines address families for DNS lookups

  3. TUN creation: conf.createTun() returns a tunCreator. The system checks IsClient, NoKernelTun, and KernelTunSupported() to choose between kernel TUN or gVisor-based TUN

  4. Device building: tun.BuildDevice(createIPCRequest(conf), server.bindServer) creates the WireGuard device and attaches the netBindServer

  5. Packet reception: When UDP datagrams arrive, Server.Process parses the remote endpoint, wraps the connection into a buf.PacketReader, and pushes packets onto bindServer.readQueue

  6. Connection forwarding: For each outgoing clear-text connection, forwardConnection builds a sub-context with inbound name "wireguard", sets inbound.Source to the peer's address, and calls dispatcher.DispatchLink to hand control to Xray's routing, DNS, and policy engine

Configuration Reference

The WireGuard inbound configuration is defined in proxy/wireguard/config.proto at lines 17-34. Understanding these fields is essential for proper setup.

Configuration Schema

Field Type Description
secret_key string Server's private key (keep secure)
peers repeated PeerConfig List of allowed peers
endpoint repeated string Local listening addresses
mtu int32 Interface MTU (default 1420)
num_workers int32 Worker goroutines for bind server
domain_strategy enum IP version handling strategy
is_client bool Client mode flag (false for inbound)
no_kernel_tun bool Force gVisor TUN instead of kernel

Peer Configuration

Each peer in the peers array uses PeerConfig from proxy/wireguard/config.proto:

Field Description
public_key Peer's public key
pre_shared_key Optional pre-shared key for additional security
endpoint Peer's external endpoint (IP:port)
allowed_ips CIDR ranges this peer can route
keep_alive Persistent keepalive interval in seconds

Complete Configuration Examples

Minimal Server Configuration (YAML)

This configuration sets up a basic WireGuard inbound in Xray-core:

inbounds:
  - tag: wg-in
    type: wireguard
    listen: 0.0.0.0:51820
    domain_strategy: FORCE_IP46
    mtu: 1420
    num_workers: 2
    is_client: false
    no_kernel_tun: false
    peers:
      - public_key: "YOUR_PEER_PUBLIC_KEY_BASE64"
        pre_shared_key: ""
        endpoint: "203.0.113.5:51820"
        allowed_ips:
          - "0.0.0.0/0"
          - "::/0"
        keep_alive: 25
    secret_key: "YOUR_SERVER_PRIVATE_KEY_BASE64"

Key configuration points:

  • type: wireguard activates the inbound implementation in proxy/wireguard/server.go
  • is_client: false puts the inbound in server mode (required for inbound operation)
  • no_kernel_tun: false allows automatic selection between kernel and gVisor TUN

Multiple Peers Configuration

inbounds:
  - tag: wg-in-multi
    type: wireguard
    listen: 0.0.0.0:51820
    domain_strategy: FORCE_IP46
    mtu: 1420
    num_workers: 4
    is_client: false
    no_kernel_tun: false
    peers:
      - public_key: "PEER1_PUBLIC_KEY"
        allowed_ips:
          - "10.0.0.2/32"
        keep_alive: 25
      - public_key: "PEER2_PUBLIC_KEY"
        allowed_ips:
          - "10.0.0.3/32"
        keep_alive: 25
      - public_key: "PEER3_PUBLIC_KEY"
        allowed_ips:
          - "10.0.0.0/24"
        keep_alive: 60
    secret_key: "SERVER_PRIVATE_KEY"

Programmatic Configuration (Go)

For direct integration with Xray-core's Go API:

package main

import (
    "context"
    
    "github.com/xtls/xray-core/proxy/wireguard"
)

func createWireGuardInbound() (*wireguard.Server, error) {
    conf := &wireguard.DeviceConfig{
        SecretKey: "SERVER_PRIVATE_KEY_BASE64",
        Endpoint:  []string{"0.0.0.0:51820"},
        Peers: []*wireguard.PeerConfig{
            {
                PublicKey:    "PEER_PUBLIC_KEY_BASE64",
                PreSharedKey: "",
                Endpoint:     "203.0.113.5:51820",
                AllowedIps:   []string{"0.0.0.0/0", "::/0"},
                KeepAlive:    25,
            },
        },
        Mtu:            1420,
        NumWorkers:     2,
        DomainStrategy: wireguard.DeviceConfig_FORCE_IP46,
        IsClient:       false,
        NoKernelTun:    false,
    }

    srv, err := wireguard.NewServer(context.Background(), conf)
    if err != nil {
        return nil, err
    }
    
    return srv, nil
}

The NewServer function in proxy/wireguard/server.go performs all initialization steps: endpoint parsing, TUN selection, and device building.

TUN Implementation Selection

The WireGuard inbound in Xray-core supports two TUN implementations, selected automatically or forced via configuration.

Selection Logic

The decision happens in proxy/wireguard/config.go lines 33-53:

func (c *DeviceConfig) createTun() tunCreator {
    // Check if we should use kernel TUN
    if !c.IsClient && !c.NoKernelTun && KernelTunSupported() {
        return createKernelTun
    }
    // Fall back to gVisor userspace netstack
    return createGVisorTun
}

Kernel TUN (createKernelTun)

  • Uses the operating system's native TUN interface
  • Better performance, lower latency
  • Requires appropriate permissions (usually root or CAP_NET_ADMIN)
  • Platform-dependent: Linux supports it fully; other platforms may vary

gVisor TUN (createGVisorTun)

  • Pure userspace implementation using Google's gVisor netstack
  • No special permissions required
  • More portable across platforms
  • Slightly higher overhead than kernel TUN

Configuration Control

Setting Effect
no_kernel_tun: true Forces gVisor TUN regardless of kernel support
no_kernel_tun: false Allows automatic selection (default)
is_client: true In client mode, typically uses gVisor TUN

Key Source Files Reference

Understanding the WireGuard inbound implementation requires familiarity with these files in the Xray-core repository:

File Purpose Lines of Interest
proxy/wireguard/config.proto Protobuf schema for DeviceConfig and PeerConfig 17-34
proxy/wireguard/server.go Core inbound implementation: NewServer, Process, forwardConnection 36-70, 77-124, 26-71
proxy/wireguard/config.go TUN selection logic and configuration helpers 33-53
proxy/wireguard/tun_linux.go Linux-specific kernel TUN implementation Full file
proxy/wireguard/tun_default.go Fallback TUN implementations for other platforms Full file
testing/scenarios/wireguard_test.go End-to-end integration tests Full file

Summary

  • WireGuard inbound in Xray-core creates a virtual TUN interface that receives encrypted UDP packets, decrypts them, and forwards clear-text traffic into Xray's standard routing pipeline.

  • Core implementation resides in proxy/wireguard/server.go, with NewServer handling initialization and Server.Process managing packet reception.

  • TUN selection automatically chooses between kernel-mode TUN (better performance) and gVisor userspace TUN (better portability) based on IsClient, NoKernelTun, and KernelTunSupported() in proxy/wireguard/config.go.

  • Configuration uses DeviceConfig and PeerConfig defined in proxy/wireguard/config.proto, supporting YAML, JSON, and programmatic Go configuration.

  • Traffic forwarding uses forwardConnection in proxy/wireguard/server.go to create properly tagged sub-contexts that integrate with Xray's routing.Dispatcher.

Frequently Asked Questions

What is the difference between kernel TUN and gVisor TUN in Xray-core's WireGuard inbound?

Kernel TUN uses the operating system's native virtual network interface, providing better performance and lower latency but requiring appropriate permissions (typically root or CAP_NET_ADMIN). GVisor TUN is a pure userspace implementation using Google's gVisor netstack, offering broader platform compatibility without special permissions but with slightly higher overhead. The automatic selection logic in proxy/wireguard/config.go prefers kernel TUN for server mode on Linux when available.

How do I force Xray-core to use gVisor TUN instead of kernel TUN?

Set no_kernel_tun: true in your WireGuard inbound configuration. This forces the createTun() function in proxy/wireguard/config.go to return createGVisorTun regardless of whether kernel TUN support is available. This is useful when running without root privileges or on platforms where kernel TUN is unreliable.

Can I use the same WireGuard configuration for both client and server in Xray-core?

No, the is_client flag fundamentally changes the TUN implementation selection and traffic direction. For inbound operation (receiving WireGuard connections), you must set is_client: false. The server mode enables the full packet processing pipeline in Server.Process and forwardConnection. Client mode uses a different code path optimized for outbound connections.

What happens to decrypted traffic after it leaves the WireGuard TUN?

Decrypted traffic passes through forwardConnection in proxy/wireguard/server.go, which creates a new Xray sub-context with inbound tag "wireguard", sets the source address to the original peer's address, and calls dispatcher.DispatchLink. This integrates the traffic into Xray's normal routing pipeline, where it can be processed by rules, DNS, outbound handlers, and other Xray features just like any other inbound traffic.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →