Skip to content

os, internal/poll, runtime: how to use /dev/net/tun on Linux #30426

Closed
@zx2c4

Description

@zx2c4

Go 1.12 brought Sysconn() for os.File. In theory that should let us OpenFile on /dev/net/tun, and then use Sysconn() to do all of the TUN-specific ioctls for setting up the device and giving it a name and setting some properties and such. From then out, it's supposed to be a matter of Read, Write, and Close. Since we don't need to call Fd() on the os.File at any point, we gain the benefits of using netpoll (which is epoll behind the scenes).

In addition to allowing the scheduler to make better decisions and not allocating an OS thread for every IO operation, netpoll also lets us call Read in one Go routine and Close in another, and the currently running Read will return immediately with an error saying that it's been closed. This is terrific for shutting down gracefully. To illustrate here's something that does not work as a consequence of using Fd:

        fd, err := os.OpenFile("/dev/net/tun", os.O_RDWR, 0)
        if err != nil {
                log.Fatal(err)
        }
        
        var ifr [unix.IFNAMSIZ + 64]byte
        copy(ifr[:], []byte("cheese"))
        *(*uint16)(unsafe.Pointer(&ifr[unix.IFNAMSIZ])) = unix.IFF_TUN
        
        _, _, errno := unix.Syscall(
                unix.SYS_IOCTL,
                uintptr(fd.Fd()),
                uintptr(unix.TUNSETIFF),
                uintptr(unsafe.Pointer(&ifr[0])),
        )
        if errno != 0 {
                log.Fatal(errno)
        }

        wait := sync.WaitGroup{}
        wait.Add(1)
        go func() {
                var err error
                for {   
                        _, err := fd.Read(b[:])
                        if err != nil {
                                break
                        }
                }
                log.Print("Read errored: ", err)
                wait.Done()
        }()
        time.Sleep(time.Second * 3)
        log.Print("Closing")
        err = fd.Close()
        if err != nil {
                log.Print("Close errored: " , err)
        }
        wait.Wait()
        log.Print("Exiting")

The problem with the above code is that fd.Read(b[:]) never returns after fd.Close() executes, and so the program hangs forever. Thanks to Sysconn in Go 1.12, we can fix that problem like this:

        fd, err := os.OpenFile("/dev/net/tun", os.O_RDWR, 0)
        if err != nil {
                log.Fatal(err)
        }
        
        var ifr [unix.IFNAMSIZ + 64]byte
        copy(ifr[:], []byte("cheese"))
        *(*uint16)(unsafe.Pointer(&ifr[unix.IFNAMSIZ])) = unix.IFF_TUN
        
        var errno syscall.Errno
        s, _ := fd.SyscallConn()
        s.Control(func(fd uintptr) {
                _, _, errno = unix.Syscall(
                        unix.SYS_IOCTL,
                        fd,
                        uintptr(unix.TUNSETIFF),
                        uintptr(unsafe.Pointer(&ifr[0])),
                )
        })
        if errno != 0 {
                log.Fatal(errno)
        }

        wait := sync.WaitGroup{}
        wait.Add(1)
        go func() {
                var err error
                for {   
                        _, err := fd.Read(b[:])
                        if err != nil {
                                break
                        }
                }
                log.Print("Read errored: ", err)
                wait.Done()
        }()
        time.Sleep(time.Second * 3)
        log.Print("Closing")
        err = fd.Close()
        if err != nil {
                log.Print("Close errored: " , err)
        }
        wait.Wait()
        log.Print("Exiting")

This works as expected with regards to that fd.Read(b[:]) getting cancelled. (In Go 1.11, I previously worked around this by manually polling on a cancellation pipe and the tun fd with some pretty gnarly ugliness. I've been eagerly awaiting the Go 1.12 release to stop having to play those games.)

There's a big problem, however: netpoll's use of epoll doesn't seem to agree with the the Linux tun driver's tun_chr_poll. Consider the following program:

package main

import "log"
import "os"
import "unsafe"
import "time"
import "syscall"
import "os/exec"
import "sync"
import "golang.org/x/sys/unix"

func main() {
	fd, err := os.OpenFile("/dev/net/tun", os.O_RDWR, 0)
	if err != nil {
		log.Fatal(err)
	}

	var ifr [unix.IFNAMSIZ + 64]byte
	copy(ifr[:], []byte("cheese"))
	*(*uint16)(unsafe.Pointer(&ifr[unix.IFNAMSIZ])) = unix.IFF_TUN

	var errno syscall.Errno
	s, _ := fd.SyscallConn()
	s.Control(func(fd uintptr) {
		_, _, errno = unix.Syscall(
			unix.SYS_IOCTL,
			fd,
			uintptr(unix.TUNSETIFF),
			uintptr(unsafe.Pointer(&ifr[0])),
		)
	})
	if errno != 0 {
		log.Fatal(errno)
	}

	wait := sync.WaitGroup{}
	wait.Add(1)
	go func() {
		var err error
		c := exec.Command("sh", "-c", "ip link set up cheese && ip a a 192.168.9.2/24 dev cheese")
		c.Start()
		c.Wait()
		exec.Command("sh", "-c", "ping -c 4 -f 192.168.9.1; ip link set down cheese; ip a f dev cheese").Start()
		b := [2000]byte{}
		for {
			n, err := fd.Read(b[:])
			if err != nil {
				break
			}
			log.Printf("Read %d bytes", n)
		}
		log.Print("Read errored: ", err)
		wait.Done()
	}()
	time.Sleep(time.Second * 15)
	log.Print("Closing")
	err = fd.Close()
	if err != nil {
		log.Print("Close errored: ", err)
	}
	wait.Wait()
	log.Print("Exiting")
}

This is supposed to work, but actually the call to Read winds up blocking and not returning any data, and only ever returns upon the call to Close. The above program can be "fixed" by adding fd.Fd() just above the go func() { line, in order to remove fd from netpoll. This, however, incurs the pre-Sysconn-era problem of Close not being cancelable and loosing the nice other benefits of netpoll.

Anybody familiar with netpoll's particular use of epoll interested in taking a look under the hood?

Metadata

Metadata

Assignees

No one assigned

    Labels

    FrozenDueToAgeNeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.OS-Linux

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions