~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
252 lines · 6.9 KB Raw History
//go:build linux

package guestnet

import (
	"encoding/binary"
	"fmt"
	"net"
	"net/netip"
	"os"
	"syscall"
	"unsafe"

	"golang.org/x/sys/unix"
)

func Wire(w Wiring) error {
	if err := w.Validate(); err != nil {
		return err
	}
	if err := Fence(w); err != nil {
		return err
	}
	uplink, err := pastaLink()
	if err != nil {
		return err
	}
	if err := createTap(w.Tap); err != nil {
		return err
	}
	nl, err := openNetlink()
	if err != nil {
		return err
	}
	defer nl.close()
	if err := nl.newBridge(w.Bridge); err != nil {
		return err
	}
	tap, err := net.InterfaceByName(w.Tap)
	if err != nil {
		return fmt.Errorf("guestnet: the tap %s: %w", w.Tap, err)
	}
	br, err := net.InterfaceByName(w.Bridge)
	if err != nil {
		return fmt.Errorf("guestnet: the bridge %s: %w", w.Bridge, err)
	}
	for _, step := range []struct {
		what string
		run  func() error
	}{
		{"bring up " + w.Bridge, func() error { return nl.setLink(br.Index, 0, true) }},
		{"bring up " + w.Tap, func() error { return nl.setLink(tap.Index, 0, true) }},
		{"enslave " + uplink.Name + " to " + w.Bridge, func() error { return nl.setLink(uplink.Index, br.Index, true) }},
		{"enslave " + w.Tap + " to " + w.Bridge, func() error { return nl.setLink(tap.Index, br.Index, true) }},
		{"pin the guest's neighbor entry", func() error { return nl.permanentNeighbor(br.Index, w.GuestAddress, w.GuestMAC) }},
		{"route the guest through " + w.Bridge, func() error { return nl.hostRoute(br.Index, w.GuestAddress) }},
	} {
		if err := step.run(); err != nil {
			return fmt.Errorf("guestnet: %s: %w", step.what, err)
		}
	}
	return nil
}

func pastaLink() (net.Interface, error) {
	ifaces, err := net.Interfaces()
	if err != nil {
		return net.Interface{}, fmt.Errorf("guestnet: listing this namespace's links: %w", err)
	}
	for _, i := range ifaces {
		if i.Flags&net.FlagLoopback != 0 {
			continue
		}
		addrs, err := i.Addrs()
		if err != nil {
			continue
		}
		for _, a := range addrs {
			if p, err := netip.ParsePrefix(a.String()); err == nil && p.Addr().Is4() {
				return i, nil
			}
		}
	}
	return net.Interface{}, fmt.Errorf("guestnet: no link but loopback carries an IPv4 address, so pasta has not configured this namespace and nothing is wired")
}

func createTap(name string) error {
	fd, err := unix.Open("/dev/net/tun", unix.O_RDWR|unix.O_CLOEXEC, 0)
	if err != nil {
		return fmt.Errorf("guestnet: opening /dev/net/tun: %w", err)
	}
	defer unix.Close(fd)
	ifr, err := unix.NewIfreq(name)
	if err != nil {
		return fmt.Errorf("guestnet: tap name %q: %w", name, err)
	}
	ifr.SetUint16(unix.IFF_TAP | unix.IFF_NO_PI)
	if err := unix.IoctlIfreq(fd, unix.TUNSETIFF, ifr); err != nil {
		return fmt.Errorf("guestnet: TUNSETIFF %s: %w", name, err)
	}
	if err := unix.IoctlSetInt(fd, unix.TUNSETPERSIST, 1); err != nil {
		return fmt.Errorf("guestnet: TUNSETPERSIST %s: %w", name, err)
	}
	return nil
}

type netlink struct {
	fd  int
	seq uint32
}

func openNetlink() (*netlink, error) {
	fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.NETLINK_ROUTE)
	if err != nil {
		return nil, fmt.Errorf("guestnet: netlink socket: %w", err)
	}
	if err := unix.Bind(fd, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil {
		unix.Close(fd)
		return nil, fmt.Errorf("guestnet: netlink bind: %w", err)
	}
	return &netlink{fd: fd}, nil
}

func (n *netlink) close() { unix.Close(n.fd) }

type attr struct {
	typ  uint16
	data []byte
}

func align4(n int) int { return (n + 3) &^ 3 }

func encodeAttrs(attrs []attr) []byte {
	var out []byte
	for _, a := range attrs {
		l := 4 + len(a.data)
		b := make([]byte, align4(l))
		binary.NativeEndian.PutUint16(b[0:], uint16(l))
		binary.NativeEndian.PutUint16(b[2:], a.typ)
		copy(b[4:], a.data)
		out = append(out, b...)
	}
	return out
}

func cstr(s string) []byte { return append([]byte(s), 0) }

func u32(v uint32) []byte {
	b := make([]byte, 4)
	binary.NativeEndian.PutUint32(b, v)
	return b
}

func (n *netlink) do(typ uint16, flags uint16, body []byte) error {
	n.seq++
	hdr := unix.NlMsghdr{
		Len:   uint32(unix.SizeofNlMsghdr + len(body)),
		Type:  typ,
		Flags: unix.NLM_F_REQUEST | unix.NLM_F_ACK | flags,
		Seq:   n.seq,
	}
	msg := make([]byte, unix.SizeofNlMsghdr, int(hdr.Len))
	*(*unix.NlMsghdr)(unsafe.Pointer(&msg[0])) = hdr
	msg = append(msg, body...)
	if err := unix.Sendto(n.fd, msg, 0, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil {
		return err
	}
	buf := make([]byte, os.Getpagesize())
	for {
		nr, _, err := unix.Recvfrom(n.fd, buf, 0)
		if err != nil {
			return err
		}
		msgs, err := syscall.ParseNetlinkMessage(buf[:nr])
		if err != nil {
			return err
		}
		for _, m := range msgs {
			if m.Header.Seq != n.seq || m.Header.Type != unix.NLMSG_ERROR {
				continue
			}
			if len(m.Data) < 4 {
				return fmt.Errorf("short netlink error message")
			}
			if code := int32(binary.NativeEndian.Uint32(m.Data[:4])); code != 0 {
				return unix.Errno(-code)
			}
			return nil
		}
	}
}

func ifinfo(index int, flags, change uint32) []byte {
	b := make([]byte, unix.SizeofIfInfomsg)
	*(*unix.IfInfomsg)(unsafe.Pointer(&b[0])) = unix.IfInfomsg{Family: unix.AF_UNSPEC, Index: int32(index), Flags: flags, Change: change}
	return b
}

func (n *netlink) newBridge(name string) error {
	info := encodeAttrs([]attr{{unix.IFLA_INFO_KIND, cstr("bridge")}})
	body := append(ifinfo(0, 0, 0), encodeAttrs([]attr{
		{unix.IFLA_IFNAME, cstr(name)},
		{unix.IFLA_LINKINFO | unix.NLA_F_NESTED, info},
	})...)
	if err := n.do(unix.RTM_NEWLINK, unix.NLM_F_CREATE|unix.NLM_F_EXCL, body); err != nil {
		return fmt.Errorf("guestnet: creating the bridge %s: %w", name, err)
	}
	return nil
}

func (n *netlink) setLink(index, master int, up bool) error {
	var flags, change uint32
	if up {
		flags, change = unix.IFF_UP, unix.IFF_UP
	}
	body := ifinfo(index, flags, change)
	if master > 0 {
		body = append(body, encodeAttrs([]attr{{unix.IFLA_MASTER, u32(uint32(master))}})...)
	}
	return n.do(unix.RTM_NEWLINK, 0, body)
}

func (n *netlink) permanentNeighbor(index int, address, mac string) error {
	ip, err := netip.ParseAddr(address)
	if err != nil {
		return err
	}
	hw, err := ParseMAC(mac)
	if err != nil {
		return err
	}
	nd := make([]byte, unix.SizeofNdMsg)
	*(*unix.NdMsg)(unsafe.Pointer(&nd[0])) = unix.NdMsg{Family: unix.AF_INET, Ifindex: int32(index), State: unix.NUD_PERMANENT}
	v4 := ip.As4()
	body := append(nd, encodeAttrs([]attr{{unix.NDA_DST, v4[:]}, {unix.NDA_LLADDR, hw[:]}})...)
	return n.do(unix.RTM_NEWNEIGH, unix.NLM_F_CREATE|unix.NLM_F_REPLACE, body)
}

func (n *netlink) hostRoute(index int, address string) error {
	ip, err := netip.ParseAddr(address)
	if err != nil {
		return err
	}
	rt := make([]byte, unix.SizeofRtMsg)
	*(*unix.RtMsg)(unsafe.Pointer(&rt[0])) = unix.RtMsg{
		Family: unix.AF_INET, Dst_len: 32, Table: unix.RT_TABLE_MAIN,
		Protocol: unix.RTPROT_BOOT, Scope: unix.RT_SCOPE_LINK, Type: unix.RTN_UNICAST,
	}
	v4 := ip.As4()
	body := append(rt, encodeAttrs([]attr{{unix.RTA_DST, v4[:]}, {unix.RTA_OIF, u32(uint32(index))}})...)
	return n.do(unix.RTM_NEWROUTE, unix.NLM_F_CREATE|unix.NLM_F_REPLACE, body)
}