~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
268 lines · 6.8 KB Raw History
package guestnet

import (
	"fmt"
	"net"
	"net/netip"
	"slices"
	"strconv"
	"strings"
)

const (
	PastaAddress = "172.30.0.0"
	Gateway      = "172.30.0.1"
	GuestAddress = "172.30.0.2"
	Prefix       = 30
	GuestMAC     = "06:00:ac:1e:00:02"
	HostAddress  = "192.0.2.1"
	Tap          = "fctap0"
	Bridge       = "br0"
	Interface    = "eth0"
	Shell        = "/bin/sh"
	GuestLink    = "172.30.0.0/30"
	DNSPort      = 53

	HighUDPMinPort = 1024
)

var (
	BlockedPrefixes = []string{
		"0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12",
		"192.0.0.0/24", "192.168.0.0/16", "198.18.0.0/15", "224.0.0.0/4", "255.255.255.255/32",
		"::1/128", "fc00::/7", "fe80::/10", "ff00::/8",
	}

	LANUDPPrefixes = []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fc00::/7"}

	HighUDPClosedPorts = []int{1900, 5353, 5355}
)

type Wiring struct {
	Tap          string `json:"tap"`
	Bridge       string `json:"bridge"`
	GuestAddress string `json:"guestAddress"`
	GuestMAC     string `json:"guestMAC"`
	HostPorts    []int  `json:"hostPorts,omitempty"`

	LANUDP        bool     `json:"lanUDP"`
	HostUDP       bool     `json:"hostUDP"`
	HostAddresses []string `json:"hostAddresses"`

	WebRTCUDPPort int `json:"webrtcUDPPort,omitempty"`
}

type VMExec struct {
	Wiring  Wiring   `json:"wiring"`
	VMM     string   `json:"vmm"`
	Argv    []string `json:"argv"`
	Console string   `json:"console"`
}

func Plan(hostPorts []int, lanUDP, hostUDP bool, hostAddresses []string, webrtcUDPPort int) Wiring {
	return Wiring{Tap: Tap, Bridge: Bridge, GuestAddress: GuestAddress, GuestMAC: GuestMAC, HostPorts: HostPorts(hostPorts),
		LANUDP: lanUDP, HostUDP: hostUDP, HostAddresses: slices.Clone(hostAddresses), WebRTCUDPPort: webrtcUDPPort}
}

func LANAddresses(hostAddresses []string) []string {
	lan, err := ParsePrefixes(LANUDPPrefixes)
	if err != nil {
		return nil
	}
	own, err := ParsePrefixes(hostAddresses)
	if err != nil {
		return nil
	}
	var out []string
	for _, p := range HostUDPPrefixes(own) {
		a := p.Addr()
		if slices.ContainsFunc(lan, func(l netip.Prefix) bool { return l.Contains(a) }) && !slices.Contains(out, a.String()) {
			out = append(out, a.String())
		}
	}
	slices.Sort(out)
	return out
}

func HostLANAddresses() ([]string, error) {
	own, err := HostAddresses()
	if err != nil {
		return nil, err
	}
	return LANAddresses(own), nil
}

func ValidWebRTCUDPPort(port int) error {
	if port < HighUDPMinPort || port > 65535 || slices.Contains(HighUDPClosedPorts, port) {
		return fmt.Errorf("guestnet: webrtc UDP port %d is not a UDP port from %d to 65535 outside %v", port, HighUDPMinPort, HighUDPClosedPorts)
	}
	return nil
}

func PastaUDPForwards(port int, lanAddresses []string) []string {
	if port == 0 {
		return nil
	}
	var out []string
	for _, a := range lanAddresses {
		out = append(out, "-u", a+"/"+strconv.Itoa(port))
	}
	return out
}

func HostUDPPrefixes(own []netip.Prefix) []netip.Prefix {
	host, _ := netip.ParseAddr(HostAddress)
	link := netip.MustParsePrefix(GuestLink)
	var out []netip.Prefix
	for _, p := range own {
		a := p.Addr()
		if a.IsLoopback() || a.IsLinkLocalUnicast() || a.IsMulticast() || a.IsUnspecified() || a == host || link.Contains(a) {
			continue
		}
		out = append(out, p)
	}
	return out
}

func HostAddresses() ([]string, error) {
	addrs, err := net.InterfaceAddrs()
	if err != nil {
		return nil, fmt.Errorf("guestnet: listing the host's addresses: %w", err)
	}
	var out []string
	for _, a := range addrs {
		p, err := netip.ParsePrefix(a.String())
		if err != nil {
			return nil, fmt.Errorf("guestnet: the host address %q: %w", a.String(), err)
		}
		if s := p.Addr().Unmap().String(); !slices.Contains(out, s) {
			out = append(out, s)
		}
	}
	if len(out) == 0 {
		return nil, fmt.Errorf("guestnet: the host has no address to fence the guest off")
	}
	slices.Sort(out)
	return out, nil
}

func ParsePrefixes(list []string) ([]netip.Prefix, error) {
	out := make([]netip.Prefix, 0, len(list))
	for _, s := range list {
		p, err := netip.ParsePrefix(s)
		if err != nil {
			a, aerr := netip.ParseAddr(s)
			if aerr != nil {
				return nil, fmt.Errorf("guestnet: %q is neither an address nor a prefix", s)
			}
			a = a.Unmap()
			p = netip.PrefixFrom(a, a.BitLen())
		}
		out = append(out, p.Masked())
	}
	return out, nil
}

func HostPorts(ports []int) []int {
	var out []int
	for _, p := range ports {
		if p > 0 && p <= 65535 && !slices.Contains(out, p) {
			out = append(out, p)
		}
	}
	slices.Sort(out)
	return out
}

func Netmask(prefix int) string {
	var mask uint32
	if prefix > 0 && prefix <= 32 {
		mask = ^uint32(0) << uint(32-prefix)
	}
	return fmt.Sprintf("%d.%d.%d.%d", byte(mask>>24), byte(mask>>16), byte(mask>>8), byte(mask))
}

func PastaArgv(pasta string, udpForwards []string, child ...string) []string {
	udp := []string{"-u", "none"}
	if len(udpForwards) > 0 {
		udp = udpForwards
	}
	argv := []string{
		pasta, "--config-net", "-q", "-f",
		"-a", PastaAddress,
		"-n", Netmask(Prefix),
		"-g", Gateway,
		"--dns-forward", Gateway,
		"--map-host-loopback", HostAddress,
		"--no-map-gw",
		"-t", "none",
	}
	argv = append(argv, udp...)
	argv = append(argv, "-T", "none", "-U", "none", "--", Shell, "-c", `exec "$0" "$@"`)
	return append(argv, child...)
}

func NetworkConfig() string {
	lines := []string{
		"version: 2",
		"ethernets:",
		"  " + Interface + ":",
		"    match:",
		"      macaddress: \"" + GuestMAC + "\"",
		"    set-name: " + Interface,
		"    dhcp4: false",
		"    dhcp6: false",
		"    addresses: [" + GuestAddress + "/" + strconv.Itoa(Prefix) + "]",
		"    routes:",
		"      - to: default",
		"        via: " + Gateway,
		"    nameservers:",
		"      addresses: [" + Gateway + "]",
	}
	return strings.Join(lines, "\n") + "\n"
}

func (w Wiring) Validate() error {
	if w.Tap == "" || w.Bridge == "" {
		return fmt.Errorf("guestnet: the wiring names tap %q and bridge %q; both are needed", w.Tap, w.Bridge)
	}
	if _, err := netip.ParseAddr(w.GuestAddress); err != nil {
		return fmt.Errorf("guestnet: guest address %q: %w", w.GuestAddress, err)
	}
	if _, err := ParseMAC(w.GuestMAC); err != nil {
		return err
	}
	for _, p := range w.HostPorts {
		if p <= 0 || p > 65535 {
			return fmt.Errorf("guestnet: host port %d is not a TCP port", p)
		}
	}
	if len(w.HostAddresses) == 0 {
		return fmt.Errorf("guestnet: the wiring names none of the host's addresses, so the fence could not keep the guest off them")
	}
	if w.WebRTCUDPPort != 0 {
		if err := ValidWebRTCUDPPort(w.WebRTCUDPPort); err != nil {
			return err
		}
	}
	if _, err := ParsePrefixes(w.HostAddresses); err != nil {
		return err
	}
	return nil
}

func ParseMAC(s string) ([6]byte, error) {
	var mac [6]byte
	parts := strings.Split(s, ":")
	if len(parts) != 6 {
		return mac, fmt.Errorf("guestnet: %q is not a MAC address", s)
	}
	for i, p := range parts {
		v, err := strconv.ParseUint(p, 16, 8)
		if err != nil {
			return mac, fmt.Errorf("guestnet: %q is not a MAC address", s)
		}
		mac[i] = byte(v)
	}
	return mac, nil
}