m/n/c/network: add support for static network configuration

For certain network configurations autoconfiguration doesn't work or
is not appropriate, so for these a static configuration needs to be
used. The monorepo has recently gained net.proto, a Protobuf-based
network specification. This implements support for using this instead of
autoconfiguration in the Monogon network service.

Change-Id: Ifaec4e98b5a871308bde94c26fc09a7f0bcfd064
Reviewed-on: https://review.monogon.dev/c/monogon/+/1364
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/metropolis/node/core/main.go b/metropolis/node/core/main.go
index 721482b..bd7f475 100644
--- a/metropolis/node/core/main.go
+++ b/metropolis/node/core/main.go
@@ -107,7 +107,8 @@
 		logger.Warningf("Failed to initialize TPM 2.0, attempting fallback to untrusted: %v", err)
 	}
 
-	networkSvc := network.New()
+	networkSvc := network.New(nil)
+	networkSvc.DHCPVendorClassID = "dev.monogon.metropolis.node.v1"
 	timeSvc := timesvc.New()
 
 	// This function initializes a headless Delve if this is a debug build or
diff --git a/metropolis/node/core/network/BUILD.bazel b/metropolis/node/core/network/BUILD.bazel
index 86ffda8..1500eff 100644
--- a/metropolis/node/core/network/BUILD.bazel
+++ b/metropolis/node/core/network/BUILD.bazel
@@ -2,19 +2,26 @@
 
 go_library(
     name = "network",
-    srcs = ["main.go"],
+    srcs = [
+        "main.go",
+        "static.go",
+    ],
     importpath = "source.monogon.dev/metropolis/node/core/network",
     visibility = ["//:__subpackages__"],
     deps = [
+        "//go/algorithm/toposort",
         "//metropolis/node/core/network/dhcp4c",
         "//metropolis/node/core/network/dhcp4c/callback",
         "//metropolis/node/core/network/dns",
         "//metropolis/pkg/event",
         "//metropolis/pkg/event/memory",
+        "//metropolis/pkg/logtree",
         "//metropolis/pkg/supervisor",
+        "//net/proto",
         "@com_github_google_nftables//:nftables",
         "@com_github_google_nftables//expr",
         "@com_github_insomniacslk_dhcp//dhcpv4",
         "@com_github_vishvananda_netlink//:netlink",
+        "@org_golang_x_sys//unix",
     ],
 )
diff --git a/metropolis/node/core/network/main.go b/metropolis/node/core/network/main.go
index 6fb01eb..466c076 100644
--- a/metropolis/node/core/network/main.go
+++ b/metropolis/node/core/network/main.go
@@ -22,6 +22,7 @@
 	"net"
 	"os"
 	"path"
+	"strconv"
 	"strings"
 
 	"github.com/google/nftables"
@@ -35,6 +36,7 @@
 	"source.monogon.dev/metropolis/pkg/event"
 	"source.monogon.dev/metropolis/pkg/event/memory"
 	"source.monogon.dev/metropolis/pkg/supervisor"
+	netpb "source.monogon.dev/net/proto"
 )
 
 // Service is the network service for this node. It maintains all
@@ -43,6 +45,13 @@
 // via New, it can be started and restarted arbitrarily, but the service object
 // itself must be long-lived.
 type Service struct {
+	// If set, use the given static network configuration instead of relying on
+	// autoconfiguration.
+	StaticConfig *netpb.Net
+
+	// Vendor Class identifier of the system
+	DHCPVendorClassID string
+
 	dnsReg chan *dns.ExtraDirective
 	dnsSvc *dns.Service
 
@@ -58,12 +67,16 @@
 	status memory.Value[*Status]
 }
 
-func New() *Service {
+// New instantiates a new network service. If autoconfiguration is desired,
+// staticConfig must be set to nil. If staticConfig is set to a non-nil value,
+// it will be used instead of autoconfiguration.
+func New(staticConfig *netpb.Net) *Service {
 	dnsReg := make(chan *dns.ExtraDirective)
 	dnsSvc := dns.New(dnsReg)
 	return &Service{
-		dnsReg: dnsReg,
-		dnsSvc: dnsSvc,
+		dnsReg:       dnsReg,
+		dnsSvc:       dnsSvc,
+		StaticConfig: staticConfig,
 	}
 }
 
@@ -134,7 +147,7 @@
 	if err != nil {
 		return fmt.Errorf("failed to create DHCP client on interface %v: %w", iface.Attrs().Name, err)
 	}
-	s.dhcp.VendorClassIdentifier = "dev.monogon.metropolis.node.v1"
+	s.dhcp.VendorClassIdentifier = s.DHCPVendorClassID
 	s.dhcp.RequestedOptions = []dhcpv4.OptionCode{dhcpv4.OptionRouter, dhcpv4.OptionDomainNameServer, dhcpv4.OptionClasslessStaticRoute}
 	s.dhcp.LeaseCallback = dhcpcb.Compose(dhcpcb.ManageIP(iface), dhcpcb.ManageRoutes(iface), s.statusCallback, func(old, new *dhcp4c.Lease) error {
 		if old == nil || !old.AssignedIP.Equal(new.AssignedIP) {
@@ -190,10 +203,38 @@
 	return nil
 }
 
+// RFC2474 Section 4.2.2.1 with reference to RFC791 Section 3.1 (Network
+// Control Precedence)
+const dscpCS7 = 0x7 << 3
+
 func (s *Service) Run(ctx context.Context) error {
 	logger := supervisor.Logger(ctx)
 	supervisor.Run(ctx, "dns", s.dnsSvc.Run)
-	supervisor.Run(ctx, "interfaces", s.runInterfaces)
+
+	earlySysctlOpts := sysctlOptions{
+		// Enable strict reverse path filtering on all interfaces (important
+		// for spoofing prevention from Pods with CAP_NET_ADMIN)
+		"net.ipv4.conf.all.rp_filter": "1",
+		// Disable source routing
+		"net.ipv4.conf.all.accept_source_route": "0",
+		// By default no interfaces should accept router advertisements.
+		// This will be selectively enabled on the appropriate interfaces.
+		"net.ipv6.conf.all.accept_ra": "0",
+		// Make static IPs stick around, otherwise we have to configure them
+		// again after carrier loss events.
+		"net.ipv6.conf.all.keep_addr_on_down": "1",
+		// Make neighbor discovery use DSCP CS7 without ECN
+		"net.ipv6.conf.all.ndisc_tclass": strconv.Itoa(dscpCS7 << 2),
+	}
+	if err := earlySysctlOpts.apply(); err != nil {
+		logger.Fatalf("Error configuring early sysctl options: %v", err)
+	}
+	// Choose between autoconfig and static config runnables
+	if s.StaticConfig == nil {
+		supervisor.Run(ctx, "dynamic", s.runDynamicConfig)
+	} else {
+		supervisor.Run(ctx, "static", s.runStaticConfig)
+	}
 
 	s.natTable = s.nftConn.AddTable(&nftables.Table{
 		Family: nftables.TableFamilyIPv4,
@@ -214,11 +255,6 @@
 	sysctlOpts := sysctlOptions{
 		// Enable IP forwarding for our pods
 		"net.ipv4.ip_forward": "1",
-		// Enable strict reverse path filtering on all interfaces (important
-		// for spoofing prevention from Pods with CAP_NET_ADMIN)
-		"net.ipv4.conf.all.rp_filter": "1",
-		// Disable source routing
-		"net.ipv4.conf.all.accept_source_route": "0",
 
 		// Increase Linux socket kernel buffer sizes to 16MiB (needed for fast
 		// datacenter networks)
@@ -236,7 +272,7 @@
 	return nil
 }
 
-func (s *Service) runInterfaces(ctx context.Context) error {
+func (s *Service) runDynamicConfig(ctx context.Context) error {
 	logger := supervisor.Logger(ctx)
 	logger.Info("Starting network interface management")
 
diff --git a/metropolis/node/core/network/static.go b/metropolis/node/core/network/static.go
new file mode 100644
index 0000000..45469df
--- /dev/null
+++ b/metropolis/node/core/network/static.go
@@ -0,0 +1,417 @@
+package network
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"net"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+
+	"github.com/insomniacslk/dhcp/dhcpv4"
+	"github.com/vishvananda/netlink"
+	"golang.org/x/sys/unix"
+
+	"source.monogon.dev/go/algorithm/toposort"
+	"source.monogon.dev/metropolis/node/core/network/dhcp4c"
+	dhcpcb "source.monogon.dev/metropolis/node/core/network/dhcp4c/callback"
+	"source.monogon.dev/metropolis/pkg/logtree"
+	"source.monogon.dev/metropolis/pkg/supervisor"
+	netpb "source.monogon.dev/net/proto"
+)
+
+var vlanProtoMap = map[netpb.VLAN_Protocol]netlink.VlanProtocol{
+	netpb.VLAN_CVLAN: netlink.VLAN_PROTOCOL_8021Q,
+	netpb.VLAN_SVLAN: netlink.VLAN_PROTOCOL_8021AD,
+}
+
+func (s *Service) runStaticConfig(ctx context.Context) error {
+	l := supervisor.Logger(ctx)
+	sortedInterfaces, err := getSortedIfaces(s)
+	if err != nil {
+		return err
+	}
+
+	hostDevices, err := listHostDeviceIfaces()
+	if err != nil {
+		return err
+	}
+
+	nameLinkMap := make(map[string]netlink.Link)
+
+	// interface name -> parent interface name
+	nameParentMap := make(map[string]string)
+
+	for _, i := range sortedInterfaces {
+		var newLink netlink.Link
+		var err error
+		switch it := i.Type.(type) {
+		case *netpb.Interface_Device:
+			newLink, err = deviceIfaceFromSpec(it, hostDevices, l)
+		case *netpb.Interface_Bond:
+			for _, m := range it.Bond.MemberInterface {
+				nameParentMap[m] = i.Name
+			}
+			newLink, err = bondIfaceFromSpec(it, i)
+		case *netpb.Interface_Vlan:
+			newLink = &netlink.Vlan{
+				VlanId:       int(it.Vlan.Id),
+				VlanProtocol: vlanProtoMap[it.Vlan.Protocol],
+				LinkAttrs:    netlink.NewLinkAttrs(),
+			}
+			newLink.Attrs().ParentIndex = nameLinkMap[it.Vlan.Parent].Attrs().Index
+		}
+		if err != nil {
+			return fmt.Errorf("interface %q: %w", i.Name, err)
+		}
+		newLink.Attrs().Name = i.Name
+		// Set link administratively up
+		newLink.Attrs().Flags |= unix.IFF_UP
+		if i.Mtu != 0 {
+			newLink.Attrs().MTU = int(i.Mtu)
+		}
+		if nameParentMap[i.Name] != "" {
+			newLink.Attrs().MasterIndex = nameLinkMap[nameParentMap[i.Name]].Attrs().Index
+		}
+		if newLink.Attrs().Index == -1 {
+			if err := netlink.LinkAdd(newLink); err != nil {
+				return fmt.Errorf("failed to add link %q: %w", i.Name, err)
+			}
+		} else {
+			if err := netlink.LinkModify(newLink); err != nil {
+				return fmt.Errorf("failed to modify link %q: %w", i.Name, err)
+			}
+		}
+		nameLinkMap[i.Name] = newLink
+		if i.Ipv4Autoconfig != nil {
+			if err := s.runDHCPv4(ctx, newLink); err != nil {
+				return fmt.Errorf("error enabling DHCPv4 on %q: %w", newLink.Attrs().Name, err)
+			}
+		}
+		if i.Ipv6Autoconfig != nil {
+			err := sysctlOptions{
+				"net.ipv6.conf." + newLink.Attrs().Name + ".accept_ra": "1",
+			}.apply()
+			if err != nil {
+				return fmt.Errorf("failed enabling accept_ra for interface %q: %w", newLink.Attrs().Name, err)
+			}
+			// TODO(lorenz): Actually implement DHCPv6/Managed flag
+		}
+		for _, a := range i.Address {
+			err := addAddrFromSpec(a, newLink)
+			if err != nil {
+				return fmt.Errorf("failed adding address %q to link: %w", a, err)
+			}
+		}
+		for _, r := range i.Route {
+			err := routeFromSpec(r, newLink)
+			if err != nil {
+				return fmt.Errorf("failed creating route on interface %q: %w", i.Name, err)
+			}
+		}
+		l.Infof("Configured interface %q", i.Name)
+	}
+	supervisor.Signal(ctx, supervisor.SignalHealthy)
+	supervisor.Signal(ctx, supervisor.SignalDone)
+	return nil
+}
+
+func (s *Service) runDHCPv4(ctx context.Context, lnk netlink.Link) error {
+	c, err := dhcp4c.NewClient(netlinkLinkToNetInterface(lnk))
+	if err != nil {
+		return fmt.Errorf("failed creating DHCPv4 client: %w", err)
+	}
+	c.RequestedOptions = []dhcpv4.OptionCode{dhcpv4.OptionRouter, dhcpv4.OptionDomainNameServer, dhcpv4.OptionClasslessStaticRoute}
+	c.LeaseCallback = dhcpcb.Compose(dhcpcb.ManageIP(lnk), dhcpcb.ManageRoutes(lnk), s.statusCallback)
+	return supervisor.Run(ctx, "dhcp-"+lnk.Attrs().Name, c.Run)
+}
+
+// getSortedIfaces returns a list of all interfaces to be configured in
+// an order which is valid to configure them in, ie. parent interfaces get
+// configured before child interfaces. It also validates that all interfaces
+// referenced do in fact exist in the configuration.
+func getSortedIfaces(s *Service) ([]*netpb.Interface, error) {
+	var depGraph toposort.Graph[string]
+	ifMap := make(map[string]*netpb.Interface)
+	for _, iface := range s.StaticConfig.Interface {
+		if err := isValidDevName(iface.Name); err != nil {
+			return nil, fmt.Errorf("invalid interface name %q: %w", iface.Name, err)
+		}
+		ifMap[iface.Name] = iface
+		depGraph.AddNode(iface.Name)
+		switch it := iface.Type.(type) {
+		case *netpb.Interface_Bond:
+			for _, depIf := range it.Bond.MemberInterface {
+				// Bond interfaces are set up with no children, their children
+				// are then added when they are configured. Thus this needs a
+				// reverse dependency.
+				depGraph.AddEdge(depIf, iface.Name)
+			}
+		case *netpb.Interface_Vlan:
+			depGraph.AddEdge(iface.Name, it.Vlan.Parent)
+		}
+	}
+	badRefs := depGraph.ImplicitNodeReferences()
+	if len(badRefs) > 0 {
+		var errMsgs []string
+		for n, refs := range badRefs {
+			var strRefs []string
+			for ref := range refs {
+				strRefs = append(strRefs, ref)
+			}
+			errMsgs = append(errMsgs, fmt.Sprintf("reference to undefined interface %q from interfaces %s", n, strings.Join(strRefs, ", ")))
+		}
+		return nil, errors.New(strings.Join(errMsgs, "; "))
+	}
+	interfaceOrder, err := depGraph.TopologicalOrder()
+	if err != nil {
+		return nil, fmt.Errorf("unable to calculate interface setup order: %w", err)
+	}
+	var sortedInterfaces []*netpb.Interface
+	for _, ifname := range interfaceOrder {
+		sortedInterfaces = append(sortedInterfaces, ifMap[ifname])
+	}
+	return sortedInterfaces, nil
+}
+
+type deviceIfData struct {
+	dev    *netlink.Device
+	driver string
+}
+
+func listHostDeviceIfaces() ([]deviceIfData, error) {
+	links, err := netlink.LinkList()
+	if err != nil {
+		return nil, fmt.Errorf("failed to list network links: %w", err)
+	}
+
+	var hostDevices []deviceIfData
+
+	for _, link := range links {
+		if link.Attrs().EncapType == "loopback" {
+			continue
+		}
+		d, ok := link.(*netlink.Device)
+		if !ok {
+			continue
+		}
+		var driver string
+		driverPath, err := os.Readlink("/sys/class/net/" + d.Name + "/device/driver")
+		if err == nil {
+			driver = filepath.Base(driverPath)
+		}
+		hostDevices = append(hostDevices, deviceIfData{
+			dev:    d,
+			driver: driver,
+		})
+	}
+	return hostDevices, nil
+}
+
+func deviceIfaceFromSpec(it *netpb.Interface_Device, hostDevices []deviceIfData, l logtree.LeveledLogger) (*netlink.Device, error) {
+	var matchedDevices []*netlink.Device
+	var err error
+	var parsedHWAddr net.HardwareAddr
+	if it.Device.HardwareAddress != "" {
+		parsedHWAddr, err = net.ParseMAC(it.Device.HardwareAddress)
+		if err != nil {
+			return nil, fmt.Errorf("unable to parse hardware address %q: %w", it.Device.HardwareAddress, err)
+		}
+	}
+
+	// This is O(N^2), but is bounded by the amount of physical NICs in the
+	// system. At this point we can reasonably assume N < 100.
+	for _, d := range hostDevices {
+		if len(parsedHWAddr) != 0 {
+			// If device has a permanent hardware address, it must match,
+			// otherwise the standard hardware address must match
+			if len(d.dev.PermHardwareAddr) > 0 {
+				if !bytes.Equal(d.dev.PermHardwareAddr, parsedHWAddr) {
+					l.V(1).Infof("mismatched perm hw addr %q: %s %s\n", d.dev.Name, d.dev.PermHardwareAddr, parsedHWAddr)
+					continue
+				}
+			} else if !bytes.Equal(d.dev.HardwareAddr, parsedHWAddr) {
+				l.V(1).Infof("mismatched fallback hw addr %q: %s %s\n", d.dev.Name, d.dev.HardwareAddr, parsedHWAddr)
+				continue
+			}
+		}
+		if it.Device.Driver != "" {
+			if it.Device.Driver != d.driver {
+				l.V(1).Infof("mismatched driver %q: %s %s\n", d.dev.Name, it.Device.Driver, d.driver)
+				continue
+			}
+		}
+		matchedDevices = append(matchedDevices, d.dev)
+	}
+	if len(matchedDevices) <= int(it.Device.Index) || it.Device.Index < 0 {
+		return nil, fmt.Errorf("there are %d matching host devices but requested device index is %d", len(matchedDevices), it.Device.Index)
+	}
+	dev := &netlink.Device{
+		LinkAttrs: netlink.NewLinkAttrs(),
+	}
+	dev.Index = matchedDevices[it.Device.Index].Index
+	return dev, nil
+}
+
+var lacpRateMap = map[netpb.Bond_LACP_Rate]netlink.BondLacpRate{
+	netpb.Bond_LACP_SLOW: netlink.BOND_LACP_RATE_SLOW,
+	netpb.Bond_LACP_FAST: netlink.BOND_LACP_RATE_FAST,
+}
+
+var lacpAdSelectMap = map[netpb.Bond_LACP_SelectionLogic]netlink.BondAdSelect{
+	netpb.Bond_LACP_STABLE:    netlink.BOND_AD_SELECT_STABLE,
+	netpb.Bond_LACP_BANDWIDTH: netlink.BOND_AD_SELECT_BANDWIDTH,
+	netpb.Bond_LACP_COUNT:     netlink.BOND_AD_SELECT_COUNT,
+}
+
+var xmitHashPolicyMap = map[netpb.Bond_TransmitHashPolicy]netlink.BondXmitHashPolicy{
+	netpb.Bond_LAYER2:         netlink.BOND_XMIT_HASH_POLICY_LAYER2,
+	netpb.Bond_LAYER2_3:       netlink.BOND_XMIT_HASH_POLICY_LAYER2_3,
+	netpb.Bond_LAYER3_4:       netlink.BOND_XMIT_HASH_POLICY_LAYER3_4,
+	netpb.Bond_ENCAP_LAYER2_3: netlink.BOND_XMIT_HASH_POLICY_ENCAP2_3,
+	netpb.Bond_ENCAP_LAYER3_4: netlink.BOND_XMIT_HASH_POLICY_ENCAP3_4,
+	// TODO(vishvananda/netlink#860): constant not in netlink yet
+	netpb.Bond_VLAN_SRCMAC: 5,
+}
+
+func bondIfaceFromSpec(it *netpb.Interface_Bond, i *netpb.Interface) (*netlink.Bond, error) {
+	newBond := netlink.NewLinkBond(netlink.NewLinkAttrs())
+	newBond.MinLinks = int(it.Bond.MinLinks)
+	newBond.XmitHashPolicy = xmitHashPolicyMap[it.Bond.TransmitHashPolicy]
+	switch bt := it.Bond.Mode.(type) {
+	case *netpb.Bond_Lacp:
+		newBond.Mode = netlink.BOND_MODE_802_3AD
+		newBond.LacpRate = lacpRateMap[bt.Lacp.Rate]
+		newBond.AdSelect = lacpAdSelectMap[bt.Lacp.SelectionLogic]
+		if bt.Lacp.ActorSystemMac != "" {
+			mac, err := net.ParseMAC(bt.Lacp.ActorSystemMac)
+			if err != nil {
+				return nil, fmt.Errorf("malformed LACP actor_system_mac for bond %q: %w", i.Name, err)
+			}
+			newBond.AdActorSystem = mac
+		}
+		if bt.Lacp.ActorSystemPriority != 0 {
+			newBond.AdActorSysPrio = int(bt.Lacp.ActorSystemPriority)
+		}
+		if bt.Lacp.UserPortKey != 0 {
+			newBond.AdUserPortKey = int(bt.Lacp.UserPortKey)
+		}
+	case *netpb.Bond_ActiveBackup_:
+		newBond.Mode = netlink.BOND_MODE_ACTIVE_BACKUP
+	default:
+		return nil, fmt.Errorf("unknown bond type %T", bt)
+	}
+	return newBond, nil
+}
+
+func addAddrFromSpec(a string, link netlink.Link) error {
+	var addr netlink.Addr
+	ipNet, err := addressOrPrefix(a)
+	if err != nil {
+		// Error already contains original string and enough wrapping
+		// is already done, so pass through directly.
+		return err
+	}
+	addr.IPNet = ipNet
+	if ones, size := addr.Mask.Size(); ones == size {
+		// If this is a single host IP, do not add a prefix route as it is
+		// not routable without a separate inteface route.
+		addr.Flags |= unix.IFA_F_NOPREFIXROUTE
+	}
+	addr.Flags |= unix.IFA_F_PERMANENT
+	// Kernel will add the on-link prefix for us in the routing
+	// table if required.
+	if err := netlink.AddrAdd(link, &addr); err != nil {
+		return fmt.Errorf("failed to add to kernel interface: %w", err)
+	}
+	return nil
+}
+
+func routeFromSpec(r *netpb.Interface_Route, link netlink.Link) error {
+	var route netlink.Route
+	dst, err := addressOrPrefix(r.Destination)
+	if err != nil {
+		return fmt.Errorf("destination invalid: %w", err)
+	}
+	if !dst.IP.Mask(dst.Mask).Equal(dst.IP) {
+		return fmt.Errorf("destination %v has bits in the mask set", r.Destination)
+	}
+	route.Dst = dst
+	route.Protocol = unix.RTPROT_STATIC
+	route.LinkIndex = link.Attrs().Index
+	route.Priority = int(r.Metric)
+	if r.SourceIp != "" {
+		srcIP := net.ParseIP(r.SourceIp)
+		if srcIP == nil {
+			return fmt.Errorf("failed parsing %q as IP", r.SourceIp)
+		}
+		route.Src = srcIP
+	}
+	if r.GatewayIp != "" {
+		gwIP := net.ParseIP(r.GatewayIp)
+		if gwIP == nil {
+			return fmt.Errorf("failed parsing %q as IP", r.GatewayIp)
+		}
+		route.Gw = gwIP
+
+		// These are all interface routes, if a gateway is present
+		// it is always treated as on-link.
+		route.Flags |= int(netlink.FLAG_ONLINK)
+	}
+	if err := netlink.RouteAdd(&route); err != nil {
+		return fmt.Errorf("failed creating kernel route %q: %w", r.Destination, err)
+	}
+	return nil
+}
+
+func addressOrPrefix(s string) (*net.IPNet, error) {
+	if strings.ContainsRune(s, '/') {
+		ip, prefix, err := net.ParseCIDR(s)
+		if err != nil {
+			// err already contains original string, no need to wrap
+			return nil, err
+		}
+		return &net.IPNet{IP: ip, Mask: prefix.Mask}, nil
+	} else {
+		ip := net.ParseIP(s)
+		if ip == nil {
+			return nil, fmt.Errorf("invalid IP: %v", ip)
+		}
+		var mask net.IPMask
+		if ip.To4() == nil {
+			mask = net.CIDRMask(128, 128) // IPv6 /128
+		} else {
+			mask = net.CIDRMask(32, 32) // IPv4 /32
+		}
+		return &net.IPNet{IP: ip, Mask: mask}, nil
+	}
+}
+
+func netlinkLinkToNetInterface(lnk netlink.Link) *net.Interface {
+	attrs := lnk.Attrs()
+	return &net.Interface{
+		Index:        attrs.Index,
+		MTU:          attrs.MTU,
+		Name:         attrs.Name,
+		HardwareAddr: attrs.HardwareAddr,
+		Flags:        attrs.Flags,
+	}
+}
+
+var validDevNameRegexp = regexp.MustCompile("^[^/:[:space:]]{1,15}$")
+
+func isValidDevName(name string) error {
+	if name == "." || name == ".." {
+		return errors.New("cannot be \".\" or \"..\"")
+	}
+	if strings.ContainsRune(name, '%') {
+		return errors.New("contains \"%\" sign which dynamically allocate names, this is disallowed")
+	}
+	if !validDevNameRegexp.MatchString(name) {
+		return errors.New("too short, too long or contains forward slashes, colons or spaces")
+	}
+	return nil
+}