// Package tap provides Linux TAP (Layer 2) device management. // // A TAP device is a virtual Ethernet interface that allows userspace programs // to read and write raw Ethernet frames. SoftEther VPN operates at Layer 2, // so it bridges Ethernet frames between the TAP device and the VPN tunnel. // // This uses the Linux kernel's TUN/TAP driver via /dev/net/tun with the // IFF_TAP | IFF_NO_PI flags (TAP mode, no packet info header). // // See: https://www.kernel.org/doc/html/latest/networking/tuntap.html package tap import ( "fmt" "net" "os" "unsafe" "golang.org/x/sys/unix" ) const ( tunDevice = "/dev/net/tun" ifnameSize = 16 ) // Device represents an open TAP network interface. type Device struct { file *os.File Name string // Kernel-assigned interface name (e.g. "tap0") } // Open creates a new TAP device with the given name. // If name is empty, the kernel assigns one automatically (tap0, tap1, ...). // Requires CAP_NET_ADMIN or root privileges. func Open(name string) (*Device, error) { fd, err := unix.Open(tunDevice, unix.O_RDWR|unix.O_CLOEXEC, 0) if err != nil { return nil, fmt.Errorf("open %s: %w", tunDevice, err) } var ifr [unix.IFNAMSIZ + 64]byte // IFF_TAP: Layer 2 (Ethernet frames), IFF_NO_PI: no 4-byte packet info header flags := uint16(unix.IFF_TAP | unix.IFF_NO_PI) ifr[ifnameSize] = byte(flags) ifr[ifnameSize+1] = byte(flags >> 8) if name != "" { copy(ifr[:ifnameSize], name) } _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), unix.TUNSETIFF, uintptr(unsafe.Pointer(&ifr[0]))) if errno != 0 { unix.Close(fd) return nil, fmt.Errorf("ioctl TUNSETIFF: %w", errno) } // Read back the kernel-assigned name assignedName := string(ifr[:ifnameSize]) for i, b := range assignedName { if b == 0 { assignedName = assignedName[:i] break } } return &Device{ file: os.NewFile(uintptr(fd), tunDevice), Name: assignedName, }, nil } // Read reads a single Ethernet frame from the TAP device. func (d *Device) Read(buf []byte) (int, error) { return d.file.Read(buf) } // Write writes a single Ethernet frame to the TAP device. func (d *Device) Write(buf []byte) (int, error) { return d.file.Write(buf) } // Close closes the TAP device file descriptor, which also removes the interface. func (d *Device) Close() error { return d.file.Close() } // MAC returns the hardware (MAC) address of the TAP interface. func (d *Device) MAC() (net.HardwareAddr, error) { sock, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) if err != nil { return nil, fmt.Errorf("socket: %w", err) } defer unix.Close(sock) var ifr [40]byte copy(ifr[:ifnameSize], d.Name) _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(sock), unix.SIOCGIFHWADDR, uintptr(unsafe.Pointer(&ifr[0]))) if errno != 0 { return nil, fmt.Errorf("SIOCGIFHWADDR: %w", errno) } // MAC starts at offset ifnameSize+2 (sa_family is 2 bytes) mac := make(net.HardwareAddr, 6) copy(mac, ifr[ifnameSize+2:ifnameSize+8]) return mac, nil } // SetMAC sets the hardware (MAC) address of the TAP interface. func (d *Device) SetMAC(mac net.HardwareAddr) error { sock, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) if err != nil { return fmt.Errorf("socket: %w", err) } defer unix.Close(sock) var ifr [40]byte copy(ifr[:ifnameSize], d.Name) ifr[ifnameSize] = 1 // sa_family = ARPHRD_ETHER ifr[ifnameSize+1] = 0 copy(ifr[ifnameSize+2:ifnameSize+8], mac) _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(sock), unix.SIOCSIFHWADDR, uintptr(unsafe.Pointer(&ifr[0]))) if errno != 0 { return fmt.Errorf("SIOCSIFHWADDR: %w", errno) } return nil } // SetUp brings the TAP interface up (equivalent to `ip link set up`). func (d *Device) SetUp() error { sock, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) if err != nil { return fmt.Errorf("socket: %w", err) } defer unix.Close(sock) var ifr [40]byte copy(ifr[:ifnameSize], d.Name) // SIOCGIFFLAGS: get current interface flags _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(sock), unix.SIOCGIFFLAGS, uintptr(unsafe.Pointer(&ifr[0]))) if errno != 0 { return fmt.Errorf("SIOCGIFFLAGS: %w", errno) } // Set IFF_UP flag flags := uint16(ifr[ifnameSize]) | uint16(ifr[ifnameSize+1])<<8 flags |= unix.IFF_UP ifr[ifnameSize] = byte(flags) ifr[ifnameSize+1] = byte(flags >> 8) // SIOCSIFFLAGS: apply updated flags _, _, errno = unix.Syscall(unix.SYS_IOCTL, uintptr(sock), unix.SIOCSIFFLAGS, uintptr(unsafe.Pointer(&ifr[0]))) if errno != 0 { return fmt.Errorf("SIOCSIFFLAGS: %w", errno) } return nil }