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:
+66
-25
@@ -14,7 +14,7 @@ import (
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
const version = "1.1.0"
|
||||
const version = "1.2.0"
|
||||
|
||||
type usageRow struct {
|
||||
left string
|
||||
@@ -25,7 +25,7 @@ type usageRow struct {
|
||||
var usageRows = []usageRow{
|
||||
{"ipadm <hostname>", "add/edit host (interactive)", false},
|
||||
{"ipadm -l", "list hosts", false},
|
||||
{"ipadm -a [-f] <hostname> <ip address> [mac address]", "add host", false},
|
||||
{"ipadm -a [-f] <hostname> [ip address] [mac address]", "add host (IP auto-assigned if omitted)", false},
|
||||
{"ipadm -c <hostname> <comment>", "set comment", false},
|
||||
{"ipadm -i <hostname> <ip address>", "change ip address", false},
|
||||
{"ipadm -m <hostname> <mac address>", "change mac address", false},
|
||||
@@ -34,7 +34,7 @@ var usageRows = []usageRow{
|
||||
{"ipadm -pa [-f] <hostname> <wan-port> [lan-port] [tcp|udp|both]", "add port-forward to a known host", true},
|
||||
{"ipadm -pl", "list port-forwards", false},
|
||||
{"ipadm -pd <wan-port> [tcp|udp|both]", "delete port-forward", false},
|
||||
{"ipadm -u", "update (regenerate dnsmasq+nftables config, reload)", false},
|
||||
{"ipadm -u", "update (regenerate dnsmasq+nftables config, apply)", false},
|
||||
{"ipadm -h", "this help", false},
|
||||
}
|
||||
|
||||
@@ -73,11 +73,25 @@ var usage = buildUsage()
|
||||
// netConfig describes the LAN/WAN the tool validates against. Overridable
|
||||
// via env vars, mainly so this can be tested without touching the real
|
||||
// /etc files.
|
||||
//
|
||||
// The LAN is a /22 split into four /24s: staticCIDR for hosts without a MAC
|
||||
// (manually configured on the device, DNS only), reservedCIDR for hosts with
|
||||
// a MAC (DHCP static reservation), plus a dynamic DHCP pool and a reserved,
|
||||
// currently unused /24 that ipadm doesn't need to know about.
|
||||
type netConfig struct {
|
||||
lanCIDR string
|
||||
poolStart string
|
||||
poolEnd string
|
||||
wanIface string
|
||||
staticCIDR string
|
||||
reservedCIDR string
|
||||
gatewayIP string
|
||||
wanIface string
|
||||
}
|
||||
|
||||
// cidrFor returns the subnet a host with the given MAC (possibly empty)
|
||||
// belongs in.
|
||||
func (c netConfig) cidrFor(mac string) string {
|
||||
if mac == "" {
|
||||
return c.staticCIDR
|
||||
}
|
||||
return c.reservedCIDR
|
||||
}
|
||||
|
||||
func envOr(key, def string) string {
|
||||
@@ -105,10 +119,10 @@ func nftPortFwdPath() string {
|
||||
|
||||
func loadNetConfig() netConfig {
|
||||
return netConfig{
|
||||
lanCIDR: envOr("IPADM_LAN_CIDR", "10.0.0.0/24"),
|
||||
poolStart: envOr("IPADM_POOL_START", "10.0.0.100"),
|
||||
poolEnd: envOr("IPADM_POOL_END", "10.0.0.200"),
|
||||
wanIface: envOr("IPADM_WAN_IFACE", "enp3s0"),
|
||||
staticCIDR: envOr("IPADM_STATIC_CIDR", "10.0.0.0/24"),
|
||||
reservedCIDR: envOr("IPADM_RESERVED_CIDR", "10.0.1.0/24"),
|
||||
gatewayIP: envOr("IPADM_GATEWAY_IP", "10.0.0.1"),
|
||||
wanIface: envOr("IPADM_WAN_IFACE", "enp3s0"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,13 +290,13 @@ func cmdAdd(args []string) {
|
||||
force = true
|
||||
args = args[1:]
|
||||
}
|
||||
if len(args) < 2 || len(args) > 3 {
|
||||
fail("usage: ipadm -a [-f] <hostname> <ip address> [mac address]")
|
||||
if len(args) < 1 || len(args) > 3 {
|
||||
fail("usage: ipadm -a [-f] <hostname> [ip address] [mac address]")
|
||||
}
|
||||
name, ip := args[0], args[1]
|
||||
mac := ""
|
||||
if len(args) == 3 {
|
||||
mac = args[2]
|
||||
name := args[0]
|
||||
ip, mac, err := classifyHostArgs(args[1:])
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
cfg := loadNetConfig()
|
||||
|
||||
@@ -290,9 +304,6 @@ func cmdAdd(args []string) {
|
||||
if err := validateHostname(name); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if err := validateIP(ip, cfg.lanCIDR, cfg.poolStart, cfg.poolEnd); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
normMac, err := normalizeMAC(mac)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
@@ -302,6 +313,18 @@ func cmdAdd(args []string) {
|
||||
if idx >= 0 && !force {
|
||||
return nil, "", fmt.Errorf("host %q already exists (use -f to overwrite, or `ipadm %s` to edit it)", name, name)
|
||||
}
|
||||
|
||||
cidr := cfg.cidrFor(normMac)
|
||||
if ip == "" {
|
||||
freeIP, err := nextFreeIP(hosts, cidr, idx, cfg.gatewayIP)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
ip = freeIP
|
||||
} else if err := validateIP(ip, cidr, cfg.gatewayIP); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if other := findByIP(hosts, ip, idx); other >= 0 {
|
||||
return nil, "", fmt.Errorf("IP %s is already assigned to host %q", ip, hosts[other].Name)
|
||||
}
|
||||
@@ -312,10 +335,10 @@ func cmdAdd(args []string) {
|
||||
h := Host{Name: name, IP: ip, MAC: normMac}
|
||||
if idx >= 0 {
|
||||
hosts[idx] = h
|
||||
return hosts, fmt.Sprintf("Host %q overwritten (-f).", name), nil
|
||||
return hosts, fmt.Sprintf("Host %q overwritten (-f), IP %s.", name, ip), nil
|
||||
}
|
||||
hosts = append(hosts, h)
|
||||
return hosts, fmt.Sprintf("Host %q added.", name), nil
|
||||
return hosts, fmt.Sprintf("Host %q added with IP %s.", name, ip), nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -346,7 +369,8 @@ func cmdChangeIP(args []string) {
|
||||
if idx < 0 {
|
||||
return nil, "", fmt.Errorf("host %q not found", name)
|
||||
}
|
||||
if err := validateIP(ip, cfg.lanCIDR, cfg.poolStart, cfg.poolEnd); err != nil {
|
||||
cidr := cfg.cidrFor(hosts[idx].MAC)
|
||||
if err := validateIP(ip, cidr, cfg.gatewayIP); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if other := findByIP(hosts, ip, idx); other >= 0 {
|
||||
@@ -365,6 +389,7 @@ func cmdChangeMAC(args []string) {
|
||||
if mac == "-" {
|
||||
mac = ""
|
||||
}
|
||||
cfg := loadNetConfig()
|
||||
withStore(func(hosts []Host) ([]Host, string, error) {
|
||||
idx := findHost(hosts, name)
|
||||
if idx < 0 {
|
||||
@@ -377,12 +402,28 @@ func cmdChangeMAC(args []string) {
|
||||
if other := findByMAC(hosts, normMac, idx); other >= 0 {
|
||||
return nil, "", fmt.Errorf("MAC %s is already assigned to host %q", normMac, hosts[other].Name)
|
||||
}
|
||||
|
||||
hosts[idx].MAC = normMac
|
||||
label := normMac
|
||||
if label == "" {
|
||||
label = "(removed)"
|
||||
}
|
||||
return hosts, fmt.Sprintf("MAC of %q changed to %s.", name, label), nil
|
||||
summary := fmt.Sprintf("MAC of %q changed to %s.", name, label)
|
||||
|
||||
// The subnet a host belongs in depends on whether it has a MAC
|
||||
// (see netConfig). If that changed, move it to a free IP in the
|
||||
// now-correct subnet instead of leaving it in the wrong one.
|
||||
targetCIDR := cfg.cidrFor(normMac)
|
||||
if validateIP(hosts[idx].IP, targetCIDR, cfg.gatewayIP) != nil {
|
||||
newIP, err := nextFreeIP(hosts, targetCIDR, idx, cfg.gatewayIP)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("MAC changed, but host must move to %s and no free IP is left there: %w", targetCIDR, err)
|
||||
}
|
||||
oldIP := hosts[idx].IP
|
||||
hosts[idx].IP = newIP
|
||||
summary += fmt.Sprintf(" Host moved from %s to %s (subnet %s).", oldIP, newIP, targetCIDR)
|
||||
}
|
||||
return hosts, summary, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -566,7 +607,7 @@ func cmdUpdate() {
|
||||
if err := applyDnsmasqConfig(dnsmasqHostsPath(), dnsContent); err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
fmt.Printf("%s updated, dnsmasq reloaded (%d hosts).\n", dnsmasqHostsPath(), len(hosts))
|
||||
fmt.Printf("%s updated, dnsmasq restarted (%d hosts).\n", dnsmasqHostsPath(), len(hosts))
|
||||
|
||||
fs, err := openPortFwdStore(portFwdDBPath())
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user