How to Configure Transparent Proxy (TUN Mode) in Xray-core on Linux, Windows, macOS, FreeBSD, Android, and iOS
To configure transparent proxy TUN mode in Xray-core, define a tun inbound in your JSON configuration, instantiate the platform-specific virtual network interface via the internal drivers in proxy/tun/, and ensure Xray's own upstream traffic bypasses the TUN device to avoid routing loops.
Xray-core provides a TUN inbound that creates a virtual network interface to capture raw IP packets, enabling system-wide transparent proxying at layer 3. This article explains how to configure transparent proxy TUN mode in Xray-core across all supported platforms using the specific implementation files found in the XTLS/Xray-core repository. Unlike traditional TCP/UDP inbounds, TUN mode operates on IP packets directly, requiring platform-specific setup for interface creation and routing rules.
Core Architecture of TUN Mode
The TUN implementation in Xray-core abstracts platform differences behind a common configuration interface while using OS-specific drivers to create the virtual device.
Configuration Mapping
In infra/conf/tun.go, the TunConfig struct maps JSON configuration fields to the internal tun.Config used by the proxy engine. This configuration specifies interface names, MTU values, gateway addresses, and DNS servers that the TUN device will advertise.
Platform Abstraction Layer
The generic Tun interface defined in proxy/tun/tun.go provides the contract that all platform-specific implementations must satisfy. Each operating system provides its own driver file that creates the actual network device:
- Linux:
proxy/tun/tun_linux.gouses theTUNSETIFFioctl to allocatetunXdevices - Windows:
proxy/tun/tun_windows.gowraps the Wintun library and requireswintun.dll - macOS/iOS:
proxy/tun/tun_darwin.gocreatesutundevices via the kernel API - FreeBSD:
proxy/tun/tun_freebsd.gouses thetun(4)character device - Android:
proxy/tun/tun_android.goreceives a file descriptor from the AndroidVpnService
Detailed routing considerations and design limitations are documented in proxy/tun/README.md, which provides essential guidance for preventing traffic loops.
Linux TUN Configuration
On Linux, Xray-core uses the TUNSETIFF ioctl system call to create a TUN device, as implemented in proxy/tun/tun_linux.go.
After starting Xray with the TUN inbound configured, bring the interface online and configure routing:
Systemd-networkd configuration (/etc/systemd/network/90-xray0.network):
[Match]
Name = xray0
[Network]
KeepConfiguration = yes
[Link]
ActivationPolicy = manual
RequiredForOnline = no
[Route]
Table = 1001
Destination = 0.0.0.0/0
[RoutingPolicyRule]
From = 192.168.0.0/24
Table = 1001
Manual routing commands:
# Bring the interface up
ip link set dev xray0 up
# Prevent routing loops by excluding Xray's uplink IP (e.g., 203.0.113.5)
ip route add 203.0.113.5/32 via 192.168.0.1
# Route LAN traffic through the TUN table
ip rule add from 192.168.0.0/24 table 1001
Windows TUN Configuration
Windows implementations rely on the Wintun driver (proxy/tun/tun_windows.go). Place wintun.dll in the same directory as xray.exe before starting the service.
Once Xray initializes, a new network adapter appears with the configured name. Add an on-link route using the adapter's interface index:
rem Discover the adapter index (e.g., 47) via "route print"
route add 1.1.1.1 mask 255.255.255.0 0.0.0.0 if 47
Windows routes traffic at layer 3 through this adapter, but you must still ensure Xray's own outbound connections bypass the TUN interface to prevent loops.
macOS and iOS TUN Configuration
Both macOS and iOS use proxy/tun/tun_darwin.go to create utun devices via the kernel's utun API.
macOS setup:
# Create the interface (handled by Xray) then add routes
sudo route add -net 10.0.0.0/24 -iface utun3
iOS setup:
iOS requires obtaining the file descriptor from a NetworkExtension packetFlow and passing it to Xray via environment variables. The application embedding Xray must set XRAY_TUN_FD (or xray.tun.fd, as the name is case-insensitive) before launching the binary:
// Swift example using NetworkExtension
let fd = packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int32
setenv("XRAY_TUN_FD", String(fd), 1)
// Launch Xray-core via gomobile framework
FreeBSD TUN Configuration
FreeBSD utilizes the tun(4) driver as implemented in proxy/tun/tun_freebsd.go. After Xray creates the device node, assign addresses and routes:
# Assign IP to the tun device
ifconfig tun0 inet 169.254.10.1/30 up
# Add routes through the interface
route add -net 0.0.0.0/0 -iface tun0
Android TUN Configuration
Android implementations in proxy/tun/tun_android.go do not create the TUN device directly. Instead, the application must establish a VpnService, obtain the file descriptor, and communicate it to Xray-core via the process environment.
Kotlin implementation:
val tunInterface = vpnServiceBuilder.establish()
System.setProperty("XRAY_TUN_FD", tunInterface?.fd?.toString() ?: "")
// Launch Xray as subprocess or via JNI
The environment variable XRAY_TUN_FD (also recognized as xray.tun.fd) must contain the integer file descriptor value before Xray initializes its inbound handlers.
Routing and Loop Prevention
Because TUN mode operates at layer 3, traffic enters Xray as raw IP packets after the OS has performed DNS resolution. This creates two critical requirements:
-
DNS handling: DNS queries resolve through the system's normal resolver before packets enter the TUN device. Xray sees IP addresses, not domain names, for outbound routing decisions unless you use Xray's internal DNS for the initial connection.
-
Loop prevention: You must ensure Xray's upstream traffic (connections to your proxy servers) routes through the physical gateway, not the TUN interface. Common strategies include:
- Adding static host routes for proxy server IPs via the real gateway
- Using policy-based routing (Linux
ip-rule,iptablesmarks) to exclude Xray process traffic from the TUN table
Summary
- TUN mode creates a virtual network interface at layer 3 to capture all IP traffic system-wide, unlike TCP/UDP inbounds that listen on specific ports.
- Configuration uses the
tunprotocol ininfra/conf/tun.gowith settings for name, MTU, gateway, and DNS. - Platform drivers reside in
proxy/tun/and useTUNSETIFF(Linux), Wintun (Windows),utun(Darwin),tun(4)(FreeBSD), or file descriptors (Android/iOS). - Mobile platforms require passing the VPN file descriptor via the
XRAY_TUN_FDenvironment variable before starting Xray. - Routing safety demands that Xray's own upstream traffic bypass the TUN interface to prevent infinite loops.
Frequently Asked Questions
What is the difference between TUN mode and regular inbound proxies in Xray-core?
Regular inbounds like socks or http listen on TCP/UDP ports and handle application-layer traffic, requiring explicit proxy configuration in client applications. TUN mode operates at layer 3 via proxy/tun/tun.go, creating a virtual network interface that intercepts all IP packets transparently without application support, enabling system-wide proxying for UDP, TCP, and ICMP traffic.
How do I prevent routing loops when using TUN mode?
Routing loops occur when Xray's encrypted traffic to upstream servers re-enters the TUN interface. Prevent this by adding static routes for your proxy server IPs via your physical gateway (e.g., ip route add <server-ip>/32 via <gateway> on Linux), or use policy routing to mark and bypass TUN traffic originating from the Xray process itself, as detailed in proxy/tun/README.md.
Why does DNS resolution behave differently in TUN mode?
In TUN mode, DNS resolution happens outside Xray-core because the OS resolver performs lookups before IP packets enter the virtual interface. Xray receives only IP addresses, not domain names, which affects routing rules based on domains. To control DNS, configure the OS resolver or use Xray's internal DNS client for its own outbound connections, but expect that forwarded client traffic arrives pre-resolved.
Can I use TUN mode on iOS without jailbreaking?
Yes. Non-jailbroken iOS apps can use TUN mode through the NetworkExtension framework. Your app creates a NEPacketTunnelProvider, obtains the file descriptor from packetFlow, and passes it to Xray-core via the XRAY_TUN_FD environment variable before launching the engine through a gomobile-generated framework. This uses the same proxy/tun/tun_darwin.go implementation as macOS but requires proper entitlements from Apple.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →