treewide: move //net to //osbase/net

The net package contains the utility to dump a network configuration in
proto format. It should be in osbase.

Change-Id: I4d25d9c7d600f4a04b9b79bd1ba98286bf9daec3
Reviewed-on: https://review.monogon.dev/c/monogon/+/3313
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/osbase/net/dump/BUILD.bazel b/osbase/net/dump/BUILD.bazel
new file mode 100644
index 0000000..3175e4e
--- /dev/null
+++ b/osbase/net/dump/BUILD.bazel
@@ -0,0 +1,16 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "dump",
+    srcs = [
+        "hwaddr_compat.go",
+        "netdump.go",
+    ],
+    importpath = "source.monogon.dev/osbase/net/dump",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//osbase/net/proto",
+        "@com_github_vishvananda_netlink//:netlink",
+        "@org_golang_x_sys//unix",
+    ],
+)
diff --git a/osbase/net/dump/cli/BUILD.bazel b/osbase/net/dump/cli/BUILD.bazel
new file mode 100644
index 0000000..acd4bdb
--- /dev/null
+++ b/osbase/net/dump/cli/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "cli_lib",
+    srcs = ["main.go"],
+    importpath = "source.monogon.dev/osbase/net/dump/cli",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//osbase/net/dump",
+        "@org_golang_google_protobuf//encoding/prototext",
+    ],
+)
+
+go_binary(
+    name = "cli",
+    embed = [":cli_lib"],
+    visibility = ["//visibility:public"],
+)
diff --git a/osbase/net/dump/cli/main.go b/osbase/net/dump/cli/main.go
new file mode 100644
index 0000000..c2de81a
--- /dev/null
+++ b/osbase/net/dump/cli/main.go
@@ -0,0 +1,18 @@
+package main
+
+import (
+	"fmt"
+	"log"
+
+	"google.golang.org/protobuf/encoding/prototext"
+
+	netdump "source.monogon.dev/osbase/net/dump"
+)
+
+func main() {
+	netconf, _, err := netdump.Dump()
+	if err != nil {
+		log.Fatalf("failed to dump network configuration: %v", err)
+	}
+	fmt.Println(prototext.Format(netconf))
+}
diff --git a/osbase/net/dump/hwaddr_compat.go b/osbase/net/dump/hwaddr_compat.go
new file mode 100644
index 0000000..07067bf
--- /dev/null
+++ b/osbase/net/dump/hwaddr_compat.go
@@ -0,0 +1,83 @@
+package netdump
+
+import (
+	"errors"
+	"fmt"
+	"net"
+	"runtime"
+	"unsafe"
+
+	"golang.org/x/sys/unix"
+)
+
+// @linux//include/uapi/linux:if.h
+type ifreq struct {
+	ifname [16]byte
+	data   uintptr
+}
+
+// @linux//include/uapi/linux:ethtool.h ethtool_perm_addr
+type ethtoolPermAddr struct {
+	Cmd  uint32
+	Size uint32
+	// Make this an array for memory layout reasons (see
+	// comment on the kernel struct)
+	Data [32]byte
+}
+
+var errNoPermenentHWAddr = errors.New("no permanent hardware address available")
+
+func isAllZeroes(data []byte) bool {
+	for _, b := range data {
+		if b != 0 {
+			return false
+		}
+	}
+	return true
+}
+
+// Get permanent hardware address on Linux kernels older than 5.6. On newer
+// kernels this is available via normal netlink. Returns errNoPermanentHWAddr
+// in case no such address is available.
+func getPermanentHWAddrLegacy(ifName string) (net.HardwareAddr, error) {
+	fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
+	if err != nil {
+		fd, err = unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW, unix.NETLINK_GENERIC)
+		if err != nil {
+			return nil, err
+		}
+	}
+	defer unix.Close(fd)
+
+	var ioctlPins runtime.Pinner
+	defer ioctlPins.Unpin()
+
+	var data ethtoolPermAddr
+	data.Cmd = unix.ETHTOOL_GPERMADDR
+	data.Size = uint32(len(data.Data))
+
+	var req ifreq
+	copy(req.ifname[:], ifName)
+	ioctlPins.Pin(&data)
+	req.data = uintptr(unsafe.Pointer(&data))
+	for {
+		_, _, err := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), unix.SIOCETHTOOL, uintptr(unsafe.Pointer(&req)))
+		if err == unix.EINTR {
+			continue
+		}
+		if err != 0 {
+			return nil, fmt.Errorf("ioctl(SIOETHTOOL) failed: %w", err)
+		}
+		break
+	}
+	runtime.KeepAlive(req)
+	runtime.KeepAlive(data)
+	// This kernel API is rather old and can indicate the absence of a permanent
+	// hardware MAC in two ways: a size of zero (in case the driver does not
+	// implement a permanent hardware address at all) or an all-zero value in
+	// case the driver has support for returning one but hasn't populated it.
+	if data.Size == 0 || isAllZeroes(data.Data[:data.Size]) {
+		return nil, errNoPermenentHWAddr
+	}
+	return data.Data[:data.Size], nil
+}
diff --git a/osbase/net/dump/netdump.go b/osbase/net/dump/netdump.go
new file mode 100644
index 0000000..a7d8023
--- /dev/null
+++ b/osbase/net/dump/netdump.go
@@ -0,0 +1,255 @@
+package netdump
+
+import (
+	"bytes"
+	"fmt"
+	"math"
+	"os"
+	"sort"
+	"strconv"
+	"strings"
+
+	"github.com/vishvananda/netlink"
+	"golang.org/x/sys/unix"
+
+	netapi "source.monogon.dev/osbase/net/proto"
+)
+
+var vlanProtoMap = map[netlink.VlanProtocol]netapi.VLAN_Protocol{
+	netlink.VLAN_PROTOCOL_8021Q:  netapi.VLAN_CVLAN,
+	netlink.VLAN_PROTOCOL_8021AD: netapi.VLAN_SVLAN,
+}
+
+// From iproute2's rt_protos
+const (
+	protoUnspec = 0
+	protoKernel = 2
+	protoBoot   = 3
+	protoStatic = 4
+)
+
+// Dump dumps the network configuration of the current network namespace into
+// a osbase.net.proto.Net structure. This is currently only expected to work for
+// systems which do not use a dynamic routing protocol to establish basic
+// internet connectivity.
+// The second return value is a list of warnings, i.e. things which might be
+// problematic, but might still result in a working (though less complete)
+// configuration. The Net in the first return value is set to a non-nil value
+// even if there are warnings. The third return value is for a hard error,
+// the Net value will be nil in that case.
+func Dump() (*netapi.Net, []error, error) {
+	var n netapi.Net
+	var warnings []error
+	links, err := netlink.LinkList()
+	if err != nil {
+		return nil, nil, fmt.Errorf("failed to list network links: %w", err)
+	}
+	// Map interface index -> interface pointer
+	ifIdxMap := make(map[int]*netapi.Interface)
+	// Map interface index -> names of children
+	ifChildren := make(map[int][]string)
+	// Map interface index -> number of reverse dependencies
+	ifNRevDeps := make(map[int]int)
+	for _, link := range links {
+		linkAttrs := link.Attrs()
+		// Ignore loopback interfaces. The default one will always be
+		// created, and we don't have support for additional loopbacks.
+		if linkAttrs.EncapType == "loopback" {
+			continue
+		}
+		// Gather interface-type-specific data into a netapi interface.
+		var iface netapi.Interface
+		switch l := link.(type) {
+		case *netlink.Device:
+			mac := link.Attrs().PermHardwareAddr
+			if len(mac) == 0 {
+				// Try legacy method for old kernels
+				mac, err = getPermanentHWAddrLegacy(l.Name)
+				// Errors are expected, not all interfaces support this.
+				// If a permanent hardware address could not be obtained, fall
+				// back to the configured hardware address.
+				if err != nil {
+					mac = link.Attrs().HardwareAddr
+				}
+			}
+			iface.Type = &netapi.Interface_Device{Device: &netapi.Device{
+				HardwareAddress: mac.String(),
+			}}
+		case *netlink.Bond:
+			bond := netapi.Bond{
+				MinLinks:           int32(l.MinLinks),
+				TransmitHashPolicy: netapi.Bond_TransmitHashPolicy(l.XmitHashPolicy),
+			}
+			switch l.Mode {
+			case netlink.BOND_MODE_802_3AD:
+				lacp := netapi.Bond_LACP{
+					Rate:                netapi.Bond_LACP_Rate(l.LacpRate),
+					ActorSystemPriority: int32(l.AdActorSysPrio),
+					UserPortKey:         int32(l.AdUserPortKey),
+					SelectionLogic:      netapi.Bond_LACP_SelectionLogic(l.AdSelect),
+				}
+				if len(bytes.TrimLeft(l.AdActorSystem, "\x00")) != 0 {
+					lacp.ActorSystemMac = l.AdActorSystem.String()
+				}
+				bond.Mode = &netapi.Bond_Lacp{Lacp: &lacp}
+			default:
+			}
+			iface.Type = &netapi.Interface_Bond{Bond: &bond}
+		case *netlink.Vlan:
+			parentLink, err := netlink.LinkByIndex(l.ParentIndex)
+			if err != nil {
+				warnings = append(warnings, fmt.Errorf("unable to get parent for VLAN interface %q, interface ignored: %w", iface.Name, err))
+				continue
+			}
+			iface.Type = &netapi.Interface_Vlan{Vlan: &netapi.VLAN{
+				Id:       int32(l.VlanId),
+				Protocol: vlanProtoMap[l.VlanProtocol],
+				Parent:   parentLink.Attrs().Name,
+			}}
+		default:
+			continue
+		}
+		// Append common interface data to netapi interface.
+		iface.Name = linkAttrs.Name
+		iface.Mtu = int32(linkAttrs.MTU)
+		// Collect addresses into interface.
+		addrs, err := netlink.AddrList(link, netlink.FAMILY_ALL)
+		if err != nil {
+			warnings = append(warnings, fmt.Errorf("unable to get addresses for interface %q, interface ignored: %w", iface.Name, err))
+			continue
+		}
+		for _, a := range addrs {
+			// Ignore IPv6 link-local addresses
+			if a.IP.IsLinkLocalUnicast() && a.IP.To4() == nil {
+				continue
+			}
+			// Sadly it's not possible to reliably determine if a DHCP client is
+			// running. Good clients usually either don't set the permanent flag
+			// and/or a lifetime.
+			if a.Flags&unix.IFA_F_PERMANENT == 0 || (a.ValidLft > 0 && a.ValidLft < math.MaxUint32) {
+				if a.IP.To4() == nil {
+					// Enable IPv6 Autoconfig
+					if iface.Ipv6Autoconfig == nil {
+						iface.Ipv6Autoconfig = &netapi.IPv6Autoconfig{}
+						iface.Ipv6Autoconfig.Privacy, err = getIPv6IfaceAutoconfigPrivacy(linkAttrs.Name)
+						if err != nil {
+							warnings = append(warnings, err)
+						}
+					}
+				} else {
+					if iface.Ipv4Autoconfig == nil {
+						iface.Ipv4Autoconfig = &netapi.IPv4Autoconfig{}
+					}
+				}
+				// Dynamic address, ignore
+				continue
+			}
+			if a.Peer != nil {
+				// Add an interface route for the peer
+				iface.Route = append(iface.Route, &netapi.Interface_Route{
+					Destination: a.Peer.String(),
+					SourceIp:    a.IP.String(),
+				})
+			}
+			iface.Address = append(iface.Address, a.IPNet.String())
+		}
+		if linkAttrs.MasterIndex != 0 {
+			ifChildren[linkAttrs.MasterIndex] = append(ifChildren[linkAttrs.MasterIndex], iface.Name)
+			ifNRevDeps[linkAttrs.Index]++
+		}
+		if linkAttrs.ParentIndex != 0 {
+			ifNRevDeps[linkAttrs.ParentIndex]++
+		}
+		ifIdxMap[link.Attrs().Index] = &iface
+	}
+	routes, err := netlink.RouteList(nil, netlink.FAMILY_ALL)
+	if err != nil {
+		return nil, nil, fmt.Errorf("failed to list routes: %w", err)
+	}
+	// Collect all routes into routes assigned to exact netapi interfaces.
+	for _, r := range routes {
+		if r.Family != netlink.FAMILY_V4 && r.Family != netlink.FAMILY_V6 {
+			continue
+		}
+		var route netapi.Interface_Route
+		// Ignore all dynamic routes
+		if r.Protocol != protoUnspec && r.Protocol != protoBoot &&
+			r.Protocol != protoStatic {
+			continue
+		}
+		if r.LinkIndex == 0 {
+			// Only for "exotic" routes like "unreachable" which are not
+			// necessary for connectivity, skip for now
+			continue
+		}
+		if r.Dst == nil {
+			switch r.Family {
+			case netlink.FAMILY_V4:
+				route.Destination = "0.0.0.0/0"
+			case netlink.FAMILY_V6:
+				route.Destination = "::/0"
+			default:
+				// Switch is complete, all other families get ignored at the start
+				// of the loop.
+				panic("route family changed under us")
+			}
+		} else {
+			route.Destination = r.Dst.String()
+		}
+		if !r.Gw.IsUnspecified() && len(r.Gw) != 0 {
+			route.GatewayIp = r.Gw.String()
+		}
+		if !r.Src.IsUnspecified() && len(r.Src) != 0 {
+			route.SourceIp = r.Src.String()
+		}
+		// Linux calls the metric RTA_PRIORITY even though it behaves as lower-
+		// is-better. Note that RTA_METRICS is NOT the metric.
+		route.Metric = int32(r.Priority)
+		iface, ok := ifIdxMap[r.LinkIndex]
+		if !ok {
+			continue
+		}
+
+		iface.Route = append(iface.Route, &route)
+	}
+	// Finally, gather all interface into a list, filtering out unused ones.
+	for ifIdx, iface := range ifIdxMap {
+		switch i := iface.Type.(type) {
+		case *netapi.Interface_Bond:
+			// Add children here, as now they are all known
+			i.Bond.MemberInterface = ifChildren[ifIdx]
+		case *netapi.Interface_Device:
+			// Drop physical interfaces from the config if they have no IPs and
+			// no reverse dependencies.
+			if len(iface.Address) == 0 && iface.Ipv4Autoconfig == nil &&
+				iface.Ipv6Autoconfig == nil && ifNRevDeps[ifIdx] == 0 {
+				continue
+			}
+		}
+		n.Interface = append(n.Interface, iface)
+	}
+	// Make the output stable
+	sort.Slice(n.Interface, func(i, j int) bool { return n.Interface[i].Name < n.Interface[j].Name })
+	return &n, warnings, nil
+}
+
+func getIPv6IfaceAutoconfigPrivacy(name string) (netapi.IPv6Autoconfig_Privacy, error) {
+	useTempaddrRaw, err := os.ReadFile(fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/use_tempaddr", name))
+	if err != nil {
+		return netapi.IPv6Autoconfig_DISABLE, fmt.Errorf("failed to read use_tempaddr sysctl for interface %q: %w", name, err)
+	}
+	useTempaddr, err := strconv.ParseInt(strings.TrimSpace(string(useTempaddrRaw)), 10, 64)
+	if err != nil {
+		return netapi.IPv6Autoconfig_DISABLE, fmt.Errorf("failed to parse use_tempaddr sysctl for interface %q: %w", name, err)
+	}
+	switch {
+	case useTempaddr <= 0:
+		return netapi.IPv6Autoconfig_DISABLE, nil
+	case useTempaddr == 1:
+		return netapi.IPv6Autoconfig_AVOID, nil
+	case useTempaddr > 1:
+		return netapi.IPv6Autoconfig_PREFER, nil
+	default:
+		panic("switch is complete but hit default case")
+	}
+}
diff --git a/osbase/net/proto/BUILD.bazel b/osbase/net/proto/BUILD.bazel
new file mode 100644
index 0000000..cdc115e
--- /dev/null
+++ b/osbase/net/proto/BUILD.bazel
@@ -0,0 +1,23 @@
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+
+proto_library(
+    name = "net_proto_proto",
+    srcs = ["net.proto"],
+    visibility = ["//visibility:public"],
+)
+
+go_proto_library(
+    name = "net_proto_go_proto",
+    importpath = "source.monogon.dev/osbase/net/proto",
+    proto = ":net_proto_proto",
+    visibility = ["//visibility:public"],
+)
+
+go_library(
+    name = "proto",
+    embed = [":net_proto_go_proto"],
+    importpath = "source.monogon.dev/osbase/net/proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/osbase/net/proto/gomod-generated-placeholder.go b/osbase/net/proto/gomod-generated-placeholder.go
new file mode 100644
index 0000000..92256db
--- /dev/null
+++ b/osbase/net/proto/gomod-generated-placeholder.go
@@ -0,0 +1 @@
+package proto
diff --git a/osbase/net/proto/net.proto b/osbase/net/proto/net.proto
new file mode 100644
index 0000000..1fd46aa
--- /dev/null
+++ b/osbase/net/proto/net.proto
@@ -0,0 +1,216 @@
+syntax = "proto3";
+
+// This package provides a configuration format for configuring IP-based
+// networking on Linux. This is going to be used in cases where automatic
+// configuration by Monogon's network stack is infeasible or network
+// configuration information needs to be provided to non-Monogon systems.
+// It's kept human-readable as it may be written by humans directly when
+// configuring Monogon systems.
+package osbase.net.proto;
+option go_package = "source.monogon.dev/osbase/net/proto";
+
+// Device references one or more network adapters, i.e. network devices which
+// connect this kernel to an outside system.
+// All conditions which are set in the message are ANDed together.
+message Device {
+  // Matches the permanent hardware address of the interface. The raw address
+  // is hex-encoded and colons are inserted between every byte boundary.
+  // This is the MAC address on Ethernet interfaces.
+  string hardware_address = 1;
+  // Matches the Linux driver of the network interface
+  string driver = 2;
+  // In case of multiple matches, use the n-th interface instead of the first.
+  int32 index = 3;
+}
+// Bond defines an aggregate of physical layer 2 links which behave as one
+// virtual layer 2 link. This includes active-passive as well as active-
+// active configurations with two or more links.
+message Bond {
+  // List of interface names which are a member of this bond. It's recommended
+  // to only use Device-type interfaces here as others might behave
+  // unexpectedly (Bond on VLAN interfaces) or fail to be configured entirely
+  // (Bond on Bond). All interface names listed here must exist as part of the
+  // same Net message as this Bond interface.
+  repeated string member_interface = 1;
+  // Minimum number of links to be up to consider the bond to be up.
+  // Can be used in case expected bandwith is more than a single interface
+  // can take in which case it might be preferable to not pass any traffic
+  // over causing significant packet loss.
+  int32 min_links = 2;
+  message CarrierMonitor {
+    // Interval at which the PCS is polled for physical link status if Linux's
+    // carrier monitoring is not available.
+    int32 polling_interval_ms = 1;
+    // Disable the use of Linux's carrier monitoring which can use interrupts
+    // and force polling in all cases.
+    bool force_polling = 4;
+    // Amount of time to delay marking the link as down in the bond after the
+    // carrier has been lost. Should be a multiple of polling_interval_ms.
+    int32 down_delay_ms = 2;
+    // Amount of time to delay marking the link as up in the bond after the
+    // carrier is available. Should be a multiple of polling_interval_ms.
+    int32 up_delay_ms = 3;
+  }
+  oneof link_monitor {
+    CarrierMonitor carrier_monitor = 3;
+    // TODO(#186): Support ARP monitor for other modes
+  }
+  enum TransmitHashPolicy {
+    // Layer 2 MAC address
+    LAYER2 = 0;
+    // IP address, protocol and port
+    LAYER3_4 = 1;
+    // MAC address and IP address
+    LAYER2_3 = 2;
+    // Encapsulated MAC address and IP address
+    ENCAP_LAYER2_3 = 3;
+    // Encapsulated IP address, protocol and port
+    ENCAP_LAYER3_4 = 4;
+    // VLAN ID and source MAC
+    VLAN_SRCMAC = 5;
+  }
+  TransmitHashPolicy transmit_hash_policy = 4;
+  // Use the Link Aggregation Control Protocol to automatically use the
+  // available links as best as possible.
+  message LACP {
+    enum Rate {
+      // LACP slow rate, one packet every 30s
+      SLOW = 0;
+      // LACP fast rate, one packet every 1s
+      FAST = 1;
+    }
+    Rate rate = 1;
+    enum SelectionLogic {
+      STABLE = 0;
+      BANDWIDTH = 1;
+      COUNT = 2;
+    }
+    SelectionLogic selection_logic = 2;
+    int32 actor_system_priority = 3;
+    int32 user_port_key = 4;
+    string actor_system_mac = 5;
+  }
+  message ActiveBackup {
+    // TODO(#186): More settings
+  }
+  oneof mode {
+    LACP lacp = 5;
+    ActiveBackup active_backup = 6;
+  }
+}
+
+message VLAN {
+  // Name of the parent interface passing tagged packets. The interface
+  // referenced here must exist in the same Net message as this VLAN
+  // interface.
+  string parent = 1;
+  // VLAN ID (1-4094)
+  int32 id = 2;
+  enum Protocol {
+    // C-VLAN, also known as "standard" VLAN inserts a header with the
+    // VLAN ID (VID) right before the EtherType.
+    CVLAN = 0;
+    // S-VLAN, also known as QinQ or 802.1ad (obsolete) inserts a second VLAN ID
+    // before the C-VLAN header. This allows stacking two VLANs. The ID
+    // specified here is just for the outer VLAN, the inner one can be set by
+    // creating another VLAN interface and setting this one to be its parent.
+    SVLAN = 1;
+  }
+  Protocol protocol = 3;
+}
+
+// IPv4Autoconfig contains settings for the automatic configuration of IPv4
+// addresses, routes and further network information via DHCPv4.
+message IPv4Autoconfig {}
+
+// IPv6Autoconfig contains settings for the automatic configuration of IPv6
+// addreses, routes and further network information via ICMPv6 Router
+// Advertisements and optionally DHCPv6 if indicated by the Router
+// Advertisement.
+message IPv6Autoconfig {
+  enum Privacy {
+    // Do not generate privacy addresses.
+    DISABLE = 0;
+    // Generate privacy addresses, but prefer non-privacy addresses.
+    AVOID = 1;
+    // Generate privacy addresses and use them over other non-privacy
+    // addresses.
+    PREFER = 2;
+  }
+  // privacy controls if and how privacy addresses (see RFC 4941) are used if
+  // DHCPv6 is not used for addressing. If DHCPv6 is used for addressing
+  // any privacy considerations lie with the DHCPv6 server.
+  Privacy privacy = 1;
+}
+
+message Interface {
+  // Name of the interface. Used as a reference in this config as well as for
+  // the name of the kernel interface. Must not be empty, less than 16 UTF-8
+  // bytes long and cannot contain spaces, forward slashes, colons or percent
+  // signs. The UTF-8 encoding can also not include 0xa0 which is interpreted
+  // as a space by Linux since its ctype database is based on Latin1.
+  string name = 1;
+
+  // Type of interface
+  oneof type {
+    Device device = 3;
+    Bond bond = 4;
+    VLAN vlan = 5;
+  }
+  // Enable automatic IPv4 network configuration via DHCPv4.
+  IPv4Autoconfig ipv4_autoconfig = 10;
+
+  // Enable automatic IPv6 network configuration via router advertisements and
+  // DHCPv6.
+  IPv6Autoconfig ipv6_autoconfig = 11;
+
+  // IP addresses to be statically configured. These can either be single
+  // IP addresses (both IPv4 and IPv6) as well as CIDR-style networks for
+  // which a corresponding route is automatically added. If single IP addreses
+  // are used, a corresponding route must be added, otherwise no traffic will
+  // be routed out of the interface.
+  repeated string address = 12;
+
+  message Route {
+    // Destination in CIDR form or as a single IP.
+    string destination = 1;
+
+    // If set, the destination network is not directly on-link, but reachable
+    // via a gateway which is on-link. On point-to-point networks without
+    // ARP/NDP this doesn't do anything should never be set.
+    // Note that here, different from other network configs, the gateway IP (if
+    // configured) is assumed to be on-link for the interface it's configured
+    // under. Configuring a route with a gateway IP which is routed to another
+    // interface is invalid.
+    string gateway_ip = 2;
+
+    // An optional hint to the kernel which source address to prefer when using
+    // this route.
+    string source_ip = 3;
+
+    // Metric of this interface route. A lower metric route wins over one with a
+    // higher metric. If unset, defaults to 0 which is the default metric in
+    // Linux.
+    int32 metric = 4;
+  }
+  // List of routes which direct traffic into this interface.
+  repeated Route route = 14;
+
+  // Maximum transmission unit of the interface. If unset it will be
+  // automatically configured by DHCP or LLDP or left at the interface default
+  // value. Minimum value is 1280 bytes as required by IPv6.
+  int32 mtu = 13;
+}
+
+message Nameserver {
+  // The IP address of the nameserver in string form.
+  string ip = 1;
+}
+
+// Net contains a network configuration for a single network namespace.
+//
+// This is effectively the top-level configuration message for a machine.
+message Net {
+  repeated Interface interface = 1;
+  repeated Nameserver nameserver = 3;
+}