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
+}