package main import ( "fmt" "os" "path/filepath" "regexp" "slices" "strconv" "strings" ) // Names for network cards and VLAN devices. A card is named by its MAC address // (router.interfaces..mac), a VLAN device by its LAN (lans..deviceName) // or the PPPoE session (wan.ipv4.pppoe.vlanName). Linux cannot rename a device // that is up, so the router only starts using a new name after a reboot; // router-apply stages such a change instead of activating it. // reservedNameRe matches the names the kernel and udev hand out themselves: // a rename onto one could collide with a card that gets it at boot. // Keep in step with chosenNameOk in modules/router/assertions.nix. var reservedNameRe = regexp.MustCompile(`^((eth|wlan|ppp|wg|veth|bond|tun|tap|dummy|ifb|docker|virbr|br|lo)[0-9]*|(en|wl|ww|sl)[opsx][0-9a-f].*)$`) // chosenName says what is wrong with a name given to a card or VLAN device. func chosenName(n string) string { switch { case !nameRe.MatchString(n): return "lower case letters, digits and dashes, starting with a letter, at most 15 characters" case reservedNameRe.MatchString(n): return "looks like a name the kernel hands out (eth0, wlan0, enp2s0, ...)" } return "" } // NIC is a physical network card as the kernel names it now. type NIC struct { Kernel string MAC string } // NICs lists the physical interfaces with their MAC addresses. func NICs() []NIC { var out []NIC for _, n := range Interfaces() { addr, _ := os.ReadFile(filepath.Join("/sys/class/net", n, "address")) out = append(out, NIC{Kernel: n, MAC: strings.ToLower(strings.TrimSpace(string(addr)))}) } return out } // cardNames returns router.interfaces as name -> MAC. func (s Settings) cardNames() map[string]string { out := map[string]string{} router, _ := s["router"].(map[string]any) cards, _ := router["interfaces"].(map[string]any) for name, v := range cards { if c, ok := v.(map[string]any); ok { out[name] = str(c, "mac") } } return out } // vlanNames returns the chosen VLAN device names, keyed "parent.id". func (s Settings) vlanNames() map[string]string { out := map[string]string{} router, _ := s["router"].(map[string]any) lans, _ := router["lans"].(map[string]any) for _, v := range lans { if l, ok := v.(map[string]any); ok && str(l, "deviceName") != "" { out[str(l, "interface")+"."+str(l, "vlan")] = str(l, "deviceName") } } if v := str(router, "wan", "ipv4", "pppoe", "vlanName"); v != "" && str(router, "wan", "ipv4", "type") == "pppoe" { out[str(router, "wan", "interface")+"."+str(router, "wan", "ipv4", "pppoe", "vlan")] = v } return out } // effectiveName is what the settings call a card: the name chosen for its MAC, // else the kernel's. func effectiveName(names map[string]string, n NIC) string { for name, mac := range names { if mac != "" && mac == n.MAC { return name } } return n.Kernel } // Rename is one edited row of the interface table. type Rename struct { Dev string // the device as the settings call it: "lan", or "lan.1002" for a VLAN MAC string // of a physical card; empty for a VLAN From string // the name shown before editing: the card's name, the VLAN device's custom name or "" To string // what the user typed; empty: back to the kernel's name / the default } // rename applies the edited rows to the settings. kernel gives the kernel's // name for a MAC address (to undo a rename). The changes take effect after a // reboot, so references to a card in the settings follow its new name at once. // It returns the references to rewrite in the UI's own preferences too: // old device name -> new. func (s Settings) rename(rows []Rename, kernel func(mac string) string) (map[string]string, error) { errs := FormErrors{} moved := map[string]string{} router := obj(s, "router") for i, r := range rows { field := fmt.Sprintf("name[%d]", i) to := strings.TrimSpace(r.To) if to == r.From { continue } if r.MAC == "" && !strings.Contains(r.Dev, ".") { errs[field] = "this interface is not on this machine" continue } if to != "" { if why := chosenName(to); why != "" { errs[field] = why continue } } if r.MAC != "" { s.renameCard(router, r, to, kernel, moved, errs, field) } else { s.renameVLAN(router, r, to, errs, field) } } if len(errs) > 0 { return nil, errs } // Two names must not be the same, whatever was typed. if dup := s.duplicateName(); dup != "" { return nil, FormErrors{"name": dup + " is used twice"} } return moved, nil } func (s Settings) renameCard(router map[string]any, r Rename, to string, kernel func(string) string, moved map[string]string, errs FormErrors, field string) { cards := obj(router, "interfaces") if to == "" { to = kernel(r.MAC) if to == "" { errs[field] = "the kernel's name for this card is unknown" return } delete(cards, r.Dev) } else { if c, ok := cards[to].(map[string]any); ok && str(c, "mac") != r.MAC { errs[field] = to + " is the name of another interface" return } delete(cards, r.Dev) // it may have a name already obj(cards, to)["mac"] = r.MAC } if len(cards) == 0 { delete(router, "interfaces") } moved[r.Dev] = to if str(router, "wan", "interface") == r.Dev { obj(router, "wan")["interface"] = to } lans, _ := router["lans"].(map[string]any) for _, v := range lans { if l, ok := v.(map[string]any); ok && str(l, "interface") == r.Dev { l["interface"] = to } } } func (s Settings) renameVLAN(router map[string]any, r Rename, to string, errs FormErrors, field string) { parent, id := splitVLAN(r.Dev) if id == 0 { errs[field] = "not a VLAN" return } used := false set := func(m map[string]any, key string) { used = true if to == "" { delete(m, key) } else { m[key] = to } } lans, _ := router["lans"].(map[string]any) for _, v := range lans { if l, ok := v.(map[string]any); ok && str(l, "interface") == parent && str(l, "vlan") == strconv.Itoa(id) { set(l, "deviceName") } } if str(router, "wan", "interface") == parent && str(router, "wan", "ipv4", "type") == "pppoe" && str(router, "wan", "ipv4", "pppoe", "vlan") == strconv.Itoa(id) { set(obj(obj(obj(router, "wan"), "ipv4"), "pppoe"), "vlanName") } if !used { errs[field] = "a VLAN that nothing uses has no device to name" } } // duplicateName returns a name that two cards or VLAN devices share. func (s Settings) duplicateName() string { var all []string for n := range s.cardNames() { all = append(all, n) } for _, n := range s.vlanNames() { all = append(all, n) } slices.Sort(all) for i := 1; i < len(all); i++ { if all[i] == all[i-1] { return all[i] } } return "" } // devLabel is how a device is shown in a list: a VLAN with a chosen name as // "dmz (lan.1002)", anything else as it is. func devLabel(devices []Device, name string) string { for _, d := range devices { if d.Name == name && d.Custom != "" { return d.Custom + " (" + name + ")" } } return name }