Expand LAN to 10.0.0.0/22 with static/reserved/dynamic/spare zones

Splits the LAN into four /24 zones: 10.0.0.0/24 static (no DHCP, DNS
only), 10.0.1.0/24 fixed DHCP reservations by MAC, 10.0.2.0/24 dynamic
DHCP pool, 10.0.3.0/24 spare/unused. ipadm now derives the required
subnet from whether a host has a MAC, auto-assigns free IPs, and
auto-migrates a host's IP when its MAC is added/removed. Migrated the
existing archerc80 reservation from 10.0.0.2 to 10.0.1.2.

Also fixes a latent bug found while doing this: dnsmasq's SIGHUP
(`systemctl reload`) only re-reads /etc/hosts, not the conf-dir files
ipadm writes to, so config changes were silently not applied on
reload. ipadm now restarts dnsmasq instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 08:57:58 +02:00
co-authored by Claude Sonnet 5
parent 78beedeabc
commit c135f205fa
10 changed files with 364 additions and 112 deletions
+21 -11
View File
@@ -40,19 +40,11 @@ func interactiveEdit(hosts []Host, name string, cfg netConfig) ([]Host, error) {
fmt.Printf("Editing existing host %q (press enter to keep a value)\n", name)
} else {
cur = Host{Name: name}
fmt.Printf("New host %q (press enter to leave MAC/comment empty)\n", name)
}
for {
ip := prompt("IP address", cur.IP)
if err := validateIP(ip, cfg.lanCIDR, cfg.poolStart, cfg.poolEnd); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
continue
}
cur.IP = ip
break
fmt.Printf("New host %q (press enter to accept defaults)\n", name)
}
// MAC decides which subnet the host belongs in (see netConfig), so it's
// asked first and used to suggest a matching free IP below.
for {
mac := prompt("MAC address (optional, '-' to clear)", macOrDash(cur.MAC))
if mac == "-" {
@@ -67,6 +59,24 @@ func interactiveEdit(hosts []Host, name string, cfg netConfig) ([]Host, error) {
break
}
cidr := cfg.cidrFor(cur.MAC)
suggested := cur.IP
if suggested == "" || validateIP(suggested, cidr, cfg.gatewayIP) != nil {
if freeIP, err := nextFreeIP(hosts, cidr, idx, cfg.gatewayIP); err == nil {
suggested = freeIP
}
}
for {
ip := prompt(fmt.Sprintf("IP address (%s)", cidr), suggested)
if err := validateIP(ip, cidr, cfg.gatewayIP); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
continue
}
cur.IP = ip
break
}
cur.Comment = prompt("Comment", cur.Comment)
if other := findByIP(hosts, cur.IP, idx); other >= 0 {