mirror of
https://github.com/slackhq/nebula.git
synced 2025-11-23 00:44:25 +01:00
checkpt
This commit is contained in:
232
overlay/tuntap/device.go
Normal file
232
overlay/tuntap/device.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package tuntap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/hetznercloud/virtio-go/virtio"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Documentation:
|
||||
// https://docs.kernel.org/networking/tuntap.html
|
||||
// Also worth a read:
|
||||
// https://blog.cloudflare.com/virtual-networking-101-understanding-tap/
|
||||
|
||||
// Device represents a TUN/TAP device.
|
||||
type Device struct {
|
||||
name string
|
||||
ifindex uint32
|
||||
mac net.HardwareAddr
|
||||
file *os.File
|
||||
}
|
||||
|
||||
// NewDevice creates a new TUN/TAP device, brings it up, and returns a [Device]
|
||||
// instance providing access to it.
|
||||
//
|
||||
// There are multiple options that can be passed to this constructor to
|
||||
// influence device creation:
|
||||
// - [WithName]
|
||||
// - [WithDeviceType]
|
||||
// - [WithVirtioNetHdr]
|
||||
// - [WithInterfaceFlags]
|
||||
//
|
||||
// Remember to call [Device.Close] after use to free up resources.
|
||||
func NewDevice(options ...Option) (_ *Device, err error) {
|
||||
opts := optionDefaults
|
||||
opts.apply(options)
|
||||
if err = opts.validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid options: %w", err)
|
||||
}
|
||||
|
||||
// Get a file descriptor. The device will exist as long as we keep this
|
||||
// file descriptor open.
|
||||
fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("access tuntap driver: %w", err)
|
||||
}
|
||||
|
||||
// Create an interface request. When the name is empty, the kernel will
|
||||
// auto-select one.
|
||||
ifreq, err := unix.NewIfreq(opts.name)
|
||||
if err != nil {
|
||||
_ = unix.Close(fd)
|
||||
return nil, fmt.Errorf("new ifreq: %w", err)
|
||||
}
|
||||
|
||||
// Create the new device.
|
||||
ifreq.SetUint16(opts.ifreqFlags())
|
||||
if err = unix.IoctlIfreq(fd, unix.TUNSETIFF, ifreq); err != nil {
|
||||
_ = unix.Close(fd)
|
||||
return nil, fmt.Errorf("create device: %w", err)
|
||||
}
|
||||
|
||||
dev := Device{
|
||||
// The TUNSETIFF ioctl writes the actual name that was chosen for the
|
||||
// device back to the request, so use that.
|
||||
name: ifreq.Name(),
|
||||
}
|
||||
|
||||
// Make the file descriptor of the device non-blocking. This enables us to
|
||||
// cancel reads after a timeout when no packets are arriving.
|
||||
// This, and the call to NewFile has to happen after creating the device:
|
||||
// https://github.com/golang/go/issues/30426#issuecomment-470330742
|
||||
// NewFile will recognize that the file descriptor is non-blocking and will
|
||||
// configure polling for it.
|
||||
if err = unix.SetNonblock(fd, true); err != nil {
|
||||
_ = unix.Close(fd)
|
||||
return nil, fmt.Errorf("make file descriptor non-blocking: %w", err)
|
||||
}
|
||||
|
||||
// By wrapping the file descriptor as an os.File, we not only have a
|
||||
// convenient way to read and write, but also register a finalizer that
|
||||
// closes the file descriptor when it's being garbage collected.
|
||||
dev.file = os.NewFile(uintptr(fd), dev.name)
|
||||
|
||||
// Make sure the device is removed when one of the following initialization
|
||||
// steps fails.
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = dev.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if opts.virtioNetHdr {
|
||||
// Tell the device which size we use for our virtio_net_hdr.
|
||||
err = unix.IoctlSetPointerInt(fd, unix.TUNSETVNETHDRSZ, virtio.NetHdrSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("set vnethdr size: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the device which offloads are supported.
|
||||
err = unix.IoctlSetInt(fd, unix.TUNSETOFFLOAD, opts.offloads)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("set offloads: %w", err)
|
||||
}
|
||||
|
||||
// For the following ioctls we need just any AF_INET socket, so create one.
|
||||
inet, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open inet socket: %w", err)
|
||||
}
|
||||
defer func() { _ = unix.Close(inet) }()
|
||||
|
||||
// Set the interface flags to bring it up.
|
||||
ifreq.SetUint16(unix.IFF_UP | opts.interfaceFlags)
|
||||
if err = unix.IoctlIfreq(inet, unix.SIOCSIFFLAGS, ifreq); err != nil {
|
||||
return nil, fmt.Errorf("set interface flags: %w", err)
|
||||
}
|
||||
|
||||
// Get the interface index.
|
||||
if err = unix.IoctlIfreq(inet, unix.SIOCGIFINDEX, ifreq); err != nil {
|
||||
return nil, fmt.Errorf("get interface index: %w", err)
|
||||
}
|
||||
dev.ifindex = ifreq.Uint32()
|
||||
|
||||
// Get the MAC address.
|
||||
// This ioctl writes a sockaddr into the data ifru section of the interface
|
||||
// request struct. The MAC address is in the beginning of the
|
||||
// sockaddr.sa_data section.
|
||||
if err = unix.IoctlIfreq(inet, unix.SIOCGIFHWADDR, ifreq); err != nil {
|
||||
return nil, fmt.Errorf("get mac address: %w", err)
|
||||
}
|
||||
dev.mac = unsafe.Slice((*byte)(unsafe.Pointer(ifreq)), 32)[16+2 : 16+8]
|
||||
|
||||
return &dev, nil
|
||||
}
|
||||
|
||||
// Close closes the file descriptor behind this device. This will cause the
|
||||
// TUN/TAP device to be removed.
|
||||
func (dev *Device) Close() error {
|
||||
if err := dev.file.Close(); err != nil {
|
||||
return fmt.Errorf("close file descriptor: %w", err)
|
||||
}
|
||||
dev.file = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the name of this device.
|
||||
func (dev *Device) Name() string {
|
||||
dev.ensureInitialized()
|
||||
return dev.name
|
||||
}
|
||||
|
||||
// Ifindex returns the interface index of this device.
|
||||
func (dev *Device) Ifindex() uint32 {
|
||||
dev.ensureInitialized()
|
||||
return dev.ifindex
|
||||
}
|
||||
|
||||
// MAC returns the hardware address of this device.
|
||||
func (dev *Device) MAC() net.HardwareAddr {
|
||||
dev.ensureInitialized()
|
||||
return dev.mac
|
||||
}
|
||||
|
||||
// File returns the [os.File] that is used to communicate with this device.
|
||||
// If you access it directly, please be careful to not interfere with this
|
||||
// implementation.
|
||||
func (dev *Device) File() *os.File {
|
||||
dev.ensureInitialized()
|
||||
return dev.file
|
||||
}
|
||||
|
||||
// WritePacket writes the given packet to the TUN/TAP device.
|
||||
// When the [WithVirtioNetHdr] option was enabled, then the caller is
|
||||
// responsible to prepend the packet with a [virtio.NetHdr].
|
||||
func (dev *Device) WritePacket(packet []byte) error {
|
||||
dev.ensureInitialized()
|
||||
|
||||
_, err := dev.file.Write(packet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write %d bytes: %w", len(packet), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadPacket reads the next available packet from the TUN/TAP device into the
|
||||
// given buffer. Make sure that the buffer is large enough, otherwise only a
|
||||
// part of the packet may be read. The number of read bytes will be returned.
|
||||
//
|
||||
// When the [WithVirtioNetHdr] option was enabled, then the read packet will be
|
||||
// prepended with a [virtio.NetHdr]. The caller is responsible to handle it
|
||||
// accordingly.
|
||||
//
|
||||
// A timeout can be given to limit the time this operation blocks. If no packet
|
||||
// arrives within the given timeout, the read is canceled and an error that
|
||||
// wraps [os.ErrDeadlineExceeded] is returned. Pass a timeout of zero to make
|
||||
// this operation block infinitely.
|
||||
func (dev *Device) ReadPacket(buf []byte, timeout time.Duration) (int, error) {
|
||||
dev.ensureInitialized()
|
||||
|
||||
// Make sure the read times out. This only works for files that support
|
||||
// polling (see above).
|
||||
// When no timeout is desired, passing the zero time removes the deadline.
|
||||
var deadline time.Time
|
||||
if timeout > 0 {
|
||||
deadline = time.Now().Add(timeout)
|
||||
}
|
||||
if err := dev.file.SetReadDeadline(deadline); err != nil {
|
||||
return 0, fmt.Errorf("set deadline: %w", err)
|
||||
}
|
||||
|
||||
n, err := dev.file.Read(buf)
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("read up to %d bytes: %w", len(buf), err)
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// ensureInitialized is used as a guard to prevent methods to be called on an
|
||||
// uninitialized instance.
|
||||
func (dev *Device) ensureInitialized() {
|
||||
if dev.file == nil {
|
||||
panic("device is not initialized")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user