XTLS/Xray-core · error

invalid xray.tun.fd: file descriptor is not a TUN device

Error message

invalid xray.tun.fd: file descriptor is not a TUN device

What it means

Xray validates a user-supplied TUN file descriptor (passed via the xray.tun.fd platform setting) by issuing the TUNGETIFF ioctl and checking the returned flags. If the IFF_TUN bit is not set, the fd is not a TUN interface (it may be a socket, pipe, or regular file), so startup aborts. This is a configuration/integration guard: the fd must come from opening /dev/net/tun and binding it with TUNSETIFF.

Source

Thrown at proxy/tun/tun_linux.go:97

	fd, err := strconv.Atoi(fdStr)
	if err != nil {
		return -1, nil, true, errors.New("invalid ", platform.TunFdKey).Base(err)
	}
	if fd < 3 {
		return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": file descriptor must be >= 3")
	}

	ifr, err := unix.NewIfreq("")
	if err != nil {
		return -1, nil, true, err
	}
	if err = unix.IoctlIfreq(fd, unix.TUNGETIFF, ifr); err != nil {
		return -1, nil, true, err
	}

	flags := ifr.Uint16()
	if flags&unix.IFF_TUN == 0 {
		return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": file descriptor is not a TUN device")
	}
	if flags&unix.IFF_NO_PI == 0 {
		return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": TUN device must use IFF_NO_PI")
	}

	actualName := ifr.Name()
	if expectedName != "" && actualName != expectedName {
		return -1, nil, true, errors.New("invalid ", platform.TunFdKey, ": TUN device name ", actualName, " does not match configured name ", expectedName)
	}

	tunLink, err := netlink.LinkByName(actualName)
	if err != nil {
		return -1, nil, true, err
	}

	if err = unix.SetNonblock(fd, true); err != nil {
		return -1, nil, true, err
	}

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Verify the fd with a TUNGETIFF ioctl before passing it to Xray (see validationCode)
  2. Create the TUN yourself via open("/dev/net/tun", O_RDWR) + ioctl(TUNSETIFF, IFF_TUN|IFF_NO_PI) and pass that fd
  3. Remove xray.tun.fd entirely and let Xray create and name the TUN device itself via the tun inbound config
  4. Double-check the fd number actually corresponds to /dev/net/tun in /proc/<pid>/fd (it should be a symlink to /dev/net/tun)

Example fix

// before: passing an arbitrary fd
export XRAY_TUN_FD=5  // fd 5 is a UDP socket

// after: create a real TUN fd first
fd, _ := unix.Open("/dev/net/tun", unix.O_RDWR, 0)
ifr, _ := unix.NewIfreq("tun0")
ifr.SetUint16(unix.IFF_TUN | unix.IFF_NO_PI)
_ = unix.IoctlIfreq(fd, unix.TUNSETIFF, ifr)
// now export/hand over this fd
Defensive patterns

Strategy: validation

Validate before calling

func isTunFD(fd int) bool {
	ifr, err := unix.NewIfreq("")
	if err != nil {
		return false
	}
	if err := unix.IoctlIfreq(fd, unix.TUNGETIFF, ifr); err != nil {
		return false
	}
	return ifr.Uint16()&unix.IFF_TUN != 0
}

// before starting xray with xray.tun.fd:
if !isTunFD(myFd) {
	log.Fatal("fd is not a TUN device")
}

Try / catch

if err := tun.Start(); err != nil {
	if strings.Contains(err.Error(), "not a TUN device") {
		// fix the fd source (open /dev/net/tun + TUNSETIFF) and retry once
	}
}

Prevention

When it happens

Trigger: Calling Xray with xray.tun.fd pointing at a UDP/TCP socket, an fd inherited from a parent process that is not /dev/net/tun, or a raw IP socket. Also opening /dev/net/tun but never calling TUNSETIFF before handing the fd over.

Common situations: Integration with an external TUN manager (e.g. a supervisor script or another VPN stack) that passes the wrong fd number; shell scripts that dup fds; Android/iOS handoff where the fd is actually a VpnService socket rather than a TUN; stale fd numbers after process restart.

Related errors


AI-assisted analysis of XTLS/Xray-core@7d214f8b09 (2026-08-15). Data as JSON: /api/errors/6207787d23fc5d28. Report an issue: GitHub.