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

package guestnet

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

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

const (
	FenceTable = "swcguest"

	fencePriority int32 = -300

	fenceAnswerWait = 5 * time.Second

	brPreRouting = 0
	brLocalIn    = 1

	verdictDrop   = 0
	verdictAccept = 1

	ethIPv4 = 0x0800
	ethIPv6 = 0x86dd
	ethARP  = 0x0806

	ipTCP    = 6
	ipUDP    = 17
	ipICMPv6 = 58

	icmpv6RouterSolicit   = 133
	icmpv6NeighborSolicit = 135
	icmpv6NeighborAdvert  = 136
)

type expr struct {
	name  string
	attrs []attr
}

type fenceRule struct {
	chain string
	exprs []expr
}

type nftMessage struct {
	typ   uint16
	flags uint16
	body  []byte
}

type ipFamily struct {
	match  []expr
	dstOff uint32
}

func (f ipFamily) to(p netip.Prefix) []expr {
	return slices.Concat(f.match, prefixIs(unix.NFT_PAYLOAD_NETWORK_HEADER, f.dstOff, p))
}

func Fence(w Wiring) error {
	host, err := netip.ParseAddr(HostAddress)
	if err != nil || !host.Is4() {
		return fmt.Errorf("guestnet: host address %q is not IPv4", HostAddress)
	}
	gateway, err := netip.ParseAddr(Gateway)
	if err != nil || !gateway.Is4() {
		return fmt.Errorf("guestnet: gateway %q is not IPv4", Gateway)
	}
	ports := HostPorts(w.HostPorts)
	if len(ports) != len(w.HostPorts) {
		return fmt.Errorf("guestnet: host ports %v carry a duplicate or a value outside 1-65535", w.HostPorts)
	}
	if len(w.HostAddresses) == 0 {
		return fmt.Errorf("guestnet: the wiring names none of the host's addresses")
	}
	own, err := ParsePrefixes(w.HostAddresses)
	if err != nil {
		return err
	}
	link, err := ParsePrefixes([]string{GuestLink})
	if err != nil {
		return err
	}
	blocked, err := ParsePrefixes(BlockedPrefixes)
	if err != nil {
		return err
	}
	lan, err := ParsePrefixes(LANUDPPrefixes)
	if err != nil {
		return err
	}
	fromGuest := metaIs(unix.NFT_META_IIFNAME, ifname(w.Tap))
	v4 := ipFamily{slices.Concat(fromGuest, metaIs(unix.NFT_META_PROTOCOL, be16(ethIPv4))), 16}
	v6 := ipFamily{slices.Concat(fromGuest, metaIs(unix.NFT_META_PROTOCOL, be16(ethIPv6))), 24}
	familyOf := func(p netip.Prefix) ipFamily {
		if p.Addr().Is4() {
			return v4
		}
		return v6
	}
	toHost := v4.to(netip.PrefixFrom(host, 32))
	var rules []fenceRule
	pre := func(parts ...[]expr) {
		rules = append(rules, fenceRule{"prerouting", slices.Concat(parts...)})
	}
	pre(fromGuest, metaLoad(unix.NFT_META_PROTOCOL),
		[]expr{cmpOp(unix.NFT_CMP_NEQ, be16(ethIPv4)), cmpOp(unix.NFT_CMP_NEQ, be16(ethIPv6)), cmpOp(unix.NFT_CMP_NEQ, be16(ethARP))},
		verdict(verdictDrop))
	for _, p := range ports {
		pre(toHost, l4Is(ipTCP), dport(cmpOp(unix.NFT_CMP_EQ, be16(uint16(p)))), verdict(verdictAccept))
	}
	pre(toHost, l4Is(ipTCP), rejectTCP())
	pre(toHost, verdict(verdictDrop))
	for _, proto := range []byte{ipUDP, ipTCP} {
		pre(v4.to(netip.PrefixFrom(gateway, 32)), l4Is(proto), dport(cmpOp(unix.NFT_CMP_EQ, be16(DNSPort))), verdict(verdictAccept))
	}
	for _, t := range []byte{icmpv6RouterSolicit, icmpv6NeighborSolicit, icmpv6NeighborAdvert} {
		pre(v6.match, l4Is(ipICMPv6), payloadLoad(unix.NFT_PAYLOAD_TRANSPORT_HEADER, 0, 1), []expr{cmpOp(unix.NFT_CMP_EQ, []byte{t})},
			verdict(verdictAccept))
	}
	highUDP := []expr{cmpOp(unix.NFT_CMP_GTE, be16(HighUDPMinPort))}
	for _, closed := range HighUDPClosedPorts {
		highUDP = append(highUDP, cmpOp(unix.NFT_CMP_NEQ, be16(uint16(closed))))
	}
	var fromWebRTC []expr
	if w.WebRTCUDPPort != 0 {
		if err := ValidWebRTCUDPPort(w.WebRTCUDPPort); err != nil {
			return err
		}
		fromWebRTC = slices.Concat(l4Is(ipUDP), sport(cmpOp(unix.NFT_CMP_EQ, be16(uint16(w.WebRTCUDPPort)))), dport(highUDP...))
	}
	if w.HostUDP {
		for _, p := range HostUDPPrefixes(own) {
			pre(familyOf(p).to(p), l4Is(ipUDP), dport(highUDP...), verdict(verdictAccept))
		}
	}
	if fromWebRTC != nil {
		for _, p := range HostUDPPrefixes(own) {
			pre(familyOf(p).to(p), fromWebRTC, verdict(verdictAccept))
		}
	}
	for _, p := range slices.Concat(own, link) {
		pre(familyOf(p).to(p), verdict(verdictDrop))
	}
	if fromWebRTC != nil {
		for _, p := range lan {
			pre(familyOf(p).to(p), fromWebRTC, verdict(verdictAccept))
		}
	}
	if w.LANUDP {
		for _, p := range lan {
			pre(familyOf(p).to(p), l4Is(ipUDP), dport(highUDP...), verdict(verdictAccept))
		}
	}
	for _, p := range blocked {
		pre(familyOf(p).to(p), verdict(verdictDrop))
	}
	rules = append(rules, fenceRule{"input", slices.Concat(fromGuest, verdict(verdictDrop))})
	msgs := []nftMessage{{typ: unix.NFT_MSG_NEWTABLE, flags: unix.NLM_F_CREATE | unix.NLM_F_EXCL,
		body: encodeAttrs([]attr{{unix.NFTA_TABLE_NAME, cstr(FenceTable)}})}}
	for _, c := range []struct {
		name string
		hook uint32
	}{{"prerouting", brPreRouting}, {"input", brLocalIn}} {
		hook := encodeAttrs([]attr{{unix.NFTA_HOOK_HOOKNUM, be32(c.hook)}, {unix.NFTA_HOOK_PRIORITY, beInt32(fencePriority)}})
		msgs = append(msgs, nftMessage{typ: unix.NFT_MSG_NEWCHAIN, flags: unix.NLM_F_CREATE | unix.NLM_F_EXCL, body: encodeAttrs([]attr{
			{unix.NFTA_CHAIN_TABLE, cstr(FenceTable)},
			{unix.NFTA_CHAIN_NAME, cstr(c.name)},
			{unix.NFTA_CHAIN_HOOK | unix.NLA_F_NESTED, hook},
			{unix.NFTA_CHAIN_POLICY, be32(verdictAccept)},
			{unix.NFTA_CHAIN_TYPE, cstr("filter")},
		})})
	}
	for _, r := range rules {
		list := make([]attr, 0, len(r.exprs))
		for _, e := range r.exprs {
			list = append(list, attr{unix.NFTA_LIST_ELEM | unix.NLA_F_NESTED, encodeAttrs([]attr{
				{unix.NFTA_EXPR_NAME, cstr(e.name)},
				{unix.NFTA_EXPR_DATA | unix.NLA_F_NESTED, encodeAttrs(e.attrs)},
			})})
		}
		msgs = append(msgs, nftMessage{typ: unix.NFT_MSG_NEWRULE, flags: unix.NLM_F_CREATE | unix.NLM_F_APPEND, body: encodeAttrs([]attr{
			{unix.NFTA_RULE_TABLE, cstr(FenceTable)},
			{unix.NFTA_RULE_CHAIN, cstr(r.chain)},
			{unix.NFTA_RULE_EXPRESSIONS | unix.NLA_F_NESTED, encodeAttrs(list)},
		})})
	}
	if err := nftBatch(msgs); err != nil {
		return fmt.Errorf("guestnet: fencing the guest off the host and the LAN with nf_tables: %w", err)
	}
	return nil
}

func metaLoad(key uint32) []expr {
	return []expr{{"meta", []attr{{unix.NFTA_META_DREG, be32(unix.NFT_REG_1)}, {unix.NFTA_META_KEY, be32(key)}}}}
}

func metaIs(key uint32, value []byte) []expr {
	return append(metaLoad(key), cmpOp(unix.NFT_CMP_EQ, value))
}

func l4Is(proto byte) []expr {
	return metaIs(unix.NFT_META_L4PROTO, []byte{proto})
}

func payloadLoad(base, offset, length uint32) []expr {
	return []expr{{"payload", []attr{
		{unix.NFTA_PAYLOAD_DREG, be32(unix.NFT_REG_1)},
		{unix.NFTA_PAYLOAD_BASE, be32(base)},
		{unix.NFTA_PAYLOAD_OFFSET, be32(offset)},
		{unix.NFTA_PAYLOAD_LEN, be32(length)},
	}}}
}

func prefixIs(base, offset uint32, p netip.Prefix) []expr {
	addr := p.Masked().Addr().AsSlice()
	load := payloadLoad(base, offset, uint32(len(addr)))
	if p.Bits() == len(addr)*8 {
		return append(load, cmpOp(unix.NFT_CMP_EQ, addr))
	}
	mask := make([]byte, len(addr))
	for i := 0; i < p.Bits(); i++ {
		mask[i/8] |= 0x80 >> (i % 8)
	}
	return append(load, bitwise(mask), cmpOp(unix.NFT_CMP_EQ, addr))
}

func dport(cmps ...expr) []expr {
	return append(payloadLoad(unix.NFT_PAYLOAD_TRANSPORT_HEADER, 2, 2), cmps...)
}

func sport(cmps ...expr) []expr {
	return append(payloadLoad(unix.NFT_PAYLOAD_TRANSPORT_HEADER, 0, 2), cmps...)
}

func bitwise(mask []byte) expr {
	return expr{"bitwise", []attr{
		{unix.NFTA_BITWISE_SREG, be32(unix.NFT_REG_1)},
		{unix.NFTA_BITWISE_DREG, be32(unix.NFT_REG_1)},
		{unix.NFTA_BITWISE_LEN, be32(uint32(len(mask)))},
		{unix.NFTA_BITWISE_MASK | unix.NLA_F_NESTED, encodeAttrs([]attr{{unix.NFTA_DATA_VALUE, mask}})},
		{unix.NFTA_BITWISE_XOR | unix.NLA_F_NESTED, encodeAttrs([]attr{{unix.NFTA_DATA_VALUE, make([]byte, len(mask))}})},
	}}
}

func cmpOp(op uint32, value []byte) expr {
	return expr{"cmp", []attr{
		{unix.NFTA_CMP_SREG, be32(unix.NFT_REG_1)},
		{unix.NFTA_CMP_OP, be32(op)},
		{unix.NFTA_CMP_DATA | unix.NLA_F_NESTED, encodeAttrs([]attr{{unix.NFTA_DATA_VALUE, value}})},
	}}
}

func verdict(code uint32) []expr {
	v := encodeAttrs([]attr{{unix.NFTA_VERDICT_CODE, be32(code)}})
	return []expr{{"immediate", []attr{
		{unix.NFTA_IMMEDIATE_DREG, be32(unix.NFT_REG_VERDICT)},
		{unix.NFTA_IMMEDIATE_DATA | unix.NLA_F_NESTED, encodeAttrs([]attr{{unix.NFTA_DATA_VERDICT | unix.NLA_F_NESTED, v}})},
	}}}
}

func rejectTCP() []expr {
	return []expr{{"reject", []attr{{unix.NFTA_REJECT_TYPE, be32(unix.NFT_REJECT_TCP_RST)}, {unix.NFTA_REJECT_ICMP_CODE, []byte{0}}}}}
}

func ifname(name string) []byte {
	b := make([]byte, unix.IFNAMSIZ)
	copy(b, name)
	return b
}

func be16(v uint16) []byte {
	return binary.BigEndian.AppendUint16(nil, v)
}

func be32(v uint32) []byte {
	return binary.BigEndian.AppendUint32(nil, v)
}

func beInt32(v int32) []byte {
	return binary.BigEndian.AppendUint32(nil, uint32(v))
}

func nftBatch(msgs []nftMessage) error {
	fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.NETLINK_NETFILTER)
	if err != nil {
		return fmt.Errorf("netfilter netlink socket: %w", err)
	}
	defer unix.Close(fd)
	if err := unix.Bind(fd, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil {
		return fmt.Errorf("netfilter netlink bind: %w", err)
	}
	tv := unix.NsecToTimeval(fenceAnswerWait.Nanoseconds())
	if err := unix.SetsockoptTimeval(fd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv); err != nil {
		return err
	}
	var buf []byte
	seq := uint32(1)
	frame := func(typ, flags uint16, family uint8, resID uint16, body []byte) {
		l := unix.SizeofNlMsghdr + 4 + len(body)
		msg := make([]byte, unix.SizeofNlMsghdr, align4(l))
		*(*unix.NlMsghdr)(unsafe.Pointer(&msg[0])) = unix.NlMsghdr{Len: uint32(l), Type: typ, Flags: unix.NLM_F_REQUEST | flags, Seq: seq}
		msg = append(msg, family, unix.NFNETLINK_V0, byte(resID>>8), byte(resID))
		msg = append(msg, body...)
		buf = append(buf, msg[:align4(l)]...)
		seq++
	}
	frame(unix.NFNL_MSG_BATCH_BEGIN, 0, unix.AF_UNSPEC, unix.NFNL_SUBSYS_NFTABLES, nil)
	first := seq
	for _, m := range msgs {
		frame(unix.NFNL_SUBSYS_NFTABLES<<8|m.typ, m.flags|unix.NLM_F_ACK, unix.NFPROTO_BRIDGE, 0, m.body)
	}
	end := seq
	frame(unix.NFNL_MSG_BATCH_END, 0, unix.AF_UNSPEC, unix.NFNL_SUBSYS_NFTABLES, nil)
	if err := unix.Sendto(fd, buf, 0, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil {
		return err
	}
	pending := map[uint32]bool{}
	for s := first; s < end; s++ {
		pending[s] = true
	}
	rx := make([]byte, 4*os.Getpagesize())
	for len(pending) > 0 {
		n, _, err := unix.Recvfrom(fd, rx, 0)
		if err != nil {
			return fmt.Errorf("waiting for the kernel's answer: %w", err)
		}
		parsed, err := syscall.ParseNetlinkMessage(rx[:n])
		if err != nil {
			return err
		}
		for _, m := range parsed {
			if m.Header.Type != unix.NLMSG_ERROR || !pending[m.Header.Seq] {
				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 fmt.Errorf("message %d of %d: %w", m.Header.Seq-first+1, end-first, unix.Errno(-code))
			}
			delete(pending, m.Header.Seq)
		}
	}
	return nil
}