Description
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?