get_logs needs a unit from an allowlist (router-*, kea, unbound, networkd, nftables, ddclient, acme, sshd, kernel). API tokens are valid for 1, 7, 30 (default) or 90 days, or without limit. The overview and the Changes page say which token made the unapplied changes. options.json (router://options) no longer holds the options' source paths: they differ between builds of a release, so a router rebuilding itself offline had to build the file and failed (found by the update test). Web UI: panels flow into columns on wide screens and the width is no longer capped; on phones the navigation is a menu button; the Advanced page uses panels; VLAN dropdowns show their chosen names; the duplicate WireGuard heading is gone. basic: a subtest for wan -> local rules. SPECS.md is now the only document (decisions, how each part works, and what is not done yet); PLAN.md is gone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
230 lines
6.8 KiB
Go
230 lines
6.8 KiB
Go
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.<name>.mac), a VLAN device by its LAN (lans.<l>.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
|
|
}
|