ipadm: translate to English, fix help alignment

- All ipadm-internal messages (usage, prompts, errors) are now English
- Usage text is built programmatically from a (left, desc) row list with
  computed column width instead of hand-aligned spaces, so it can't drift
  out of alignment again when rows are added/changed
- README's literal usage block updated to match; rest of the repo docs
  stay German
This commit is contained in:
2026-08-18 11:49:47 +02:00
parent 8591223aca
commit 1a39f54858
9 changed files with 137 additions and 99 deletions
+7 -5
View File
@@ -44,7 +44,7 @@ Installiert auf dem Server unter `/usr/local/bin/ipadm`.
## Usage ## Usage
``` ```
ipadm <hostname> add/edit host (interaktiv) ipadm <hostname> add/edit host (interactive)
ipadm -l list hosts ipadm -l list hosts
ipadm -a [-f] <hostname> <ip address> [mac address] add host ipadm -a [-f] <hostname> <ip address> [mac address] add host
ipadm -c <hostname> <comment> set comment ipadm -c <hostname> <comment> set comment
@@ -53,13 +53,15 @@ ipadm -m <hostname> <mac address> change mac address
ipadm -r <old hostname> <new hostname> rename host ipadm -r <old hostname> <new hostname> rename host
ipadm -d <hostname> delete host ipadm -d <hostname> delete host
ipadm -pa [-f] <hostname> <wan-port> [lan-port] [tcp|udp|both] ipadm -pa [-f] <hostname> <wan-port> [lan-port] [tcp|udp|both]
add port-forward zu bekanntem Host add port-forward to a known host
ipadm -pl list port-forwards ipadm -pl list port-forwards
ipadm -pd <wan-port> [tcp|udp|both] delete port-forward ipadm -pd <wan-port> [tcp|udp|both] delete port-forward
ipadm -u update (dnsmasq+nftables-Config regenerieren + reload) ipadm -u update (regenerate dnsmasq+nftables config, reload)
ipadm -h this help ipadm -h this help
``` ```
(Output/messages of the tool itself are English; this README stays German like the rest of the repo.)
Beispiel: Beispiel:
```sh ```sh
+4 -4
View File
@@ -45,11 +45,11 @@ func applyDnsmasqConfig(path string, content string) error {
backup = b backup = b
hadFile = true hadFile = true
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
return fmt.Errorf("kann bestehende %s nicht lesen: %w", path, err) return fmt.Errorf("cannot read existing %s: %w", path, err)
} }
if err := os.WriteFile(path, []byte(content), 0644); err != nil { if err := os.WriteFile(path, []byte(content), 0644); err != nil {
return fmt.Errorf("kann %s nicht schreiben: %w", path, err) return fmt.Errorf("cannot write %s: %w", path, err)
} }
if err := runDnsmasqTest(); err != nil { if err := runDnsmasqTest(); err != nil {
@@ -59,11 +59,11 @@ func applyDnsmasqConfig(path string, content string) error {
} else { } else {
os.Remove(path) os.Remove(path)
} }
return fmt.Errorf("dnsmasq-Konfiguration ungültig, Änderung zurückgerollt: %w", err) return fmt.Errorf("dnsmasq config invalid, change rolled back: %w", err)
} }
if err := reloadDnsmasq(); err != nil { if err := reloadDnsmasq(); err != nil {
return fmt.Errorf("%s geschrieben, aber Reload fehlgeschlagen: %w", path, err) return fmt.Errorf("%s written, but reload failed: %w", path, err)
} }
return nil return nil
} }
+1 -1
View File
@@ -22,7 +22,7 @@ const hostHeader = "# ipadm host database - managed with `ipadm`, do not edit wh
func parseHostLine(line string) (Host, error) { func parseHostLine(line string) (Host, error) {
parts := strings.SplitN(line, "\t", 4) parts := strings.SplitN(line, "\t", 4)
if len(parts) < 3 { if len(parts) < 3 {
return Host{}, fmt.Errorf("ungültige Zeile (erwarte mind. 3 Tab-getrennte Felder): %q", line) return Host{}, fmt.Errorf("invalid line (expected at least 3 tab-separated fields): %q", line)
} }
mac := parts[2] mac := parts[2]
if mac == "-" { if mac == "-" {
+9 -9
View File
@@ -37,16 +37,16 @@ func interactiveEdit(hosts []Host, name string, cfg netConfig) ([]Host, error) {
var cur Host var cur Host
if idx >= 0 { if idx >= 0 {
cur = hosts[idx] cur = hosts[idx]
fmt.Printf("Bearbeite bestehenden Host %q (Enter = Wert behalten)\n", name) fmt.Printf("Editing existing host %q (press enter to keep a value)\n", name)
} else { } else {
cur = Host{Name: name} cur = Host{Name: name}
fmt.Printf("Neuer Host %q (Enter bei MAC/Comment = leer lassen)\n", name) fmt.Printf("New host %q (press enter to leave MAC/comment empty)\n", name)
} }
for { for {
ip := prompt("IP-Adresse", cur.IP) ip := prompt("IP address", cur.IP)
if err := validateIP(ip, cfg.lanCIDR, cfg.poolStart, cfg.poolEnd); err != nil { if err := validateIP(ip, cfg.lanCIDR, cfg.poolStart, cfg.poolEnd); err != nil {
fmt.Fprintln(os.Stderr, "Fehler:", err) fmt.Fprintln(os.Stderr, "error:", err)
continue continue
} }
cur.IP = ip cur.IP = ip
@@ -54,26 +54,26 @@ func interactiveEdit(hosts []Host, name string, cfg netConfig) ([]Host, error) {
} }
for { for {
mac := prompt("MAC-Adresse (optional, '-' zum Löschen)", macOrDash(cur.MAC)) mac := prompt("MAC address (optional, '-' to clear)", macOrDash(cur.MAC))
if mac == "-" { if mac == "-" {
mac = "" mac = ""
} }
norm, err := normalizeMAC(mac) norm, err := normalizeMAC(mac)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "Fehler:", err) fmt.Fprintln(os.Stderr, "error:", err)
continue continue
} }
cur.MAC = norm cur.MAC = norm
break break
} }
cur.Comment = prompt("Kommentar", cur.Comment) cur.Comment = prompt("Comment", cur.Comment)
if other := findByIP(hosts, cur.IP, idx); other >= 0 { if other := findByIP(hosts, cur.IP, idx); other >= 0 {
return nil, fmt.Errorf("IP %s ist bereits Host %q zugewiesen", cur.IP, hosts[other].Name) return nil, fmt.Errorf("IP %s is already assigned to host %q", cur.IP, hosts[other].Name)
} }
if other := findByMAC(hosts, cur.MAC, idx); other >= 0 { if other := findByMAC(hosts, cur.MAC, idx); other >= 0 {
return nil, fmt.Errorf("MAC %s ist bereits Host %q zugewiesen", cur.MAC, hosts[other].Name) return nil, fmt.Errorf("MAC %s is already assigned to host %q", cur.MAC, hosts[other].Name)
} }
if idx >= 0 { if idx >= 0 {
+92 -56
View File
@@ -16,23 +16,59 @@ import (
const version = "1.1.0" const version = "1.1.0"
const usage = `ipadm - dhcp/dns management (dnsmasq backend) v` + version + ` type usageRow struct {
left string
desc string
wrap bool // put desc on its own line, indented to the description column
}
usage: ipadm <hostname> add/edit host (interactive) var usageRows = []usageRow{
ipadm -l list hosts {"ipadm <hostname>", "add/edit host (interactive)", false},
ipadm -a [-f] <hostname> <ip address> [mac address] add host {"ipadm -l", "list hosts", false},
ipadm -c <hostname> <comment> set comment {"ipadm -a [-f] <hostname> <ip address> [mac address]", "add host", false},
ipadm -i <hostname> <ip address> change ip address {"ipadm -c <hostname> <comment>", "set comment", false},
ipadm -m <hostname> <mac address> change mac address {"ipadm -i <hostname> <ip address>", "change ip address", false},
ipadm -r <old hostname> <new hostname> rename host {"ipadm -m <hostname> <mac address>", "change mac address", false},
ipadm -d <hostname> delete host {"ipadm -r <old hostname> <new hostname>", "rename host", false},
ipadm -pa [-f] <hostname> <wan-port> [lan-port] [tcp|udp|both] {"ipadm -d <hostname>", "delete host", false},
add port-forward to a known host {"ipadm -pa [-f] <hostname> <wan-port> [lan-port] [tcp|udp|both]", "add port-forward to a known host", true},
ipadm -pl list port-forwards {"ipadm -pl", "list port-forwards", false},
ipadm -pd <wan-port> [tcp|udp|both] delete port-forward {"ipadm -pd <wan-port> [tcp|udp|both]", "delete port-forward", false},
ipadm -u update (regenerate dnsmasq+nftables config, reload) {"ipadm -u", "update (regenerate dnsmasq+nftables config, reload)", false},
ipadm -h this help {"ipadm -h", "this help", false},
` }
// buildUsage lays out usageRows in two aligned columns. Rows too long to
// share a line with their description (marked wrap) get the description on
// the next line instead, so one oversized entry can't drag every other
// line's alignment along with it.
func buildUsage() string {
const gap = 3
width := 0
for _, r := range usageRows {
if !r.wrap && len(r.left) > width {
width = len(r.left)
}
}
var b strings.Builder
fmt.Fprintf(&b, "ipadm - dhcp/dns management (dnsmasq backend) v%s\n\n", version)
for i, r := range usageRows {
prefix := " "
if i == 0 {
prefix = "usage: "
}
if r.wrap {
fmt.Fprintf(&b, "%s%s\n", prefix, r.left)
fmt.Fprintf(&b, "%s%s%s\n", prefix, strings.Repeat(" ", width+gap), r.desc)
} else {
fmt.Fprintf(&b, "%s%-*s%s%s\n", prefix, width, r.left, strings.Repeat(" ", gap), r.desc)
}
}
return b.String()
}
var usage = buildUsage()
// netConfig describes the LAN/WAN the tool validates against. Overridable // netConfig describes the LAN/WAN the tool validates against. Overridable
// via env vars, mainly so this can be tested without touching the real // via env vars, mainly so this can be tested without touching the real
@@ -118,7 +154,7 @@ func main() {
cmdPortFwdDelete(args[1:]) cmdPortFwdDelete(args[1:])
default: default:
if strings.HasPrefix(args[0], "-") { if strings.HasPrefix(args[0], "-") {
fail("unbekannte Option %q\n\n%s", args[0], usage) fail("unknown option %q\n\n%s", args[0], usage)
} }
requireArgs(args[1:], 0, "ipadm <hostname>") requireArgs(args[1:], 0, "ipadm <hostname>")
cmdInteractive(args[0]) cmdInteractive(args[0])
@@ -127,7 +163,7 @@ func main() {
func requireArgs(rest []string, want int, form string) { func requireArgs(rest []string, want int, form string) {
if len(rest) != want { if len(rest) != want {
fail("falsche Anzahl Argumente für %q\n\n%s", form, usage) fail("wrong number of arguments for %q\n\n%s", form, usage)
} }
} }
@@ -153,10 +189,10 @@ func withStore(fn func(hosts []Host) ([]Host, string, error)) {
sortHosts(newHosts) sortHosts(newHosts)
if err := s.Save(newHosts); err != nil { if err := s.Save(newHosts); err != nil {
fail("kann %s nicht schreiben: %s", dbPath(), err) fail("cannot write %s: %s", dbPath(), err)
} }
fmt.Println(summary) fmt.Println(summary)
fmt.Println("Hinweis: `ipadm -u` ausführen, um dnsmasq/nftables zu aktualisieren.") fmt.Println("Note: run `ipadm -u` to apply this to dnsmasq/nftables.")
} }
// withPortFwdStore mirrors withStore for the port-forward DB. It also loads // withPortFwdStore mirrors withStore for the port-forward DB. It also loads
@@ -190,10 +226,10 @@ func withPortFwdStore(fn func(hosts []Host, fwds []PortForward) ([]PortForward,
sortPortFwds(newFwds) sortPortFwds(newFwds)
if err := s.Save(newFwds); err != nil { if err := s.Save(newFwds); err != nil {
fail("kann %s nicht schreiben: %s", portFwdDBPath(), err) fail("cannot write %s: %s", portFwdDBPath(), err)
} }
fmt.Println(summary) fmt.Println(summary)
fmt.Println("Hinweis: `ipadm -u` ausführen, um nftables zu aktualisieren.") fmt.Println("Note: run `ipadm -u` to apply this to nftables.")
} }
func cmdInteractive(name string) { func cmdInteractive(name string) {
@@ -203,7 +239,7 @@ func cmdInteractive(name string) {
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
return newHosts, fmt.Sprintf("Host %q gespeichert.", name), nil return newHosts, fmt.Sprintf("Host %q saved.", name), nil
}) })
} }
@@ -218,7 +254,7 @@ func cmdList() {
fail("%s", err) fail("%s", err)
} }
if len(hosts) == 0 { if len(hosts) == 0 {
fmt.Println("keine Hosts eingetragen") fmt.Println("no hosts configured")
return return
} }
sortHosts(hosts) sortHosts(hosts)
@@ -264,22 +300,22 @@ func cmdAdd(args []string) {
idx := findHost(hosts, name) idx := findHost(hosts, name)
if idx >= 0 && !force { if idx >= 0 && !force {
return nil, "", fmt.Errorf("Host %q existiert bereits (benutze -f zum Überschreiben oder `ipadm %s` zum Bearbeiten)", name, name) return nil, "", fmt.Errorf("host %q already exists (use -f to overwrite, or `ipadm %s` to edit it)", name, name)
} }
if other := findByIP(hosts, ip, idx); other >= 0 { if other := findByIP(hosts, ip, idx); other >= 0 {
return nil, "", fmt.Errorf("IP %s ist bereits Host %q zugewiesen", ip, hosts[other].Name) return nil, "", fmt.Errorf("IP %s is already assigned to host %q", ip, hosts[other].Name)
} }
if other := findByMAC(hosts, normMac, idx); other >= 0 { if other := findByMAC(hosts, normMac, idx); other >= 0 {
return nil, "", fmt.Errorf("MAC %s ist bereits Host %q zugewiesen", normMac, hosts[other].Name) return nil, "", fmt.Errorf("MAC %s is already assigned to host %q", normMac, hosts[other].Name)
} }
h := Host{Name: name, IP: ip, MAC: normMac} h := Host{Name: name, IP: ip, MAC: normMac}
if idx >= 0 { if idx >= 0 {
hosts[idx] = h hosts[idx] = h
return hosts, fmt.Sprintf("Host %q überschrieben (-f).", name), nil return hosts, fmt.Sprintf("Host %q overwritten (-f).", name), nil
} }
hosts = append(hosts, h) hosts = append(hosts, h)
return hosts, fmt.Sprintf("Host %q hinzugefügt.", name), nil return hosts, fmt.Sprintf("Host %q added.", name), nil
}) })
} }
@@ -292,10 +328,10 @@ func cmdComment(args []string) {
withStore(func(hosts []Host) ([]Host, string, error) { withStore(func(hosts []Host) ([]Host, string, error) {
idx := findHost(hosts, name) idx := findHost(hosts, name)
if idx < 0 { if idx < 0 {
return nil, "", fmt.Errorf("Host %q nicht gefunden", name) return nil, "", fmt.Errorf("host %q not found", name)
} }
hosts[idx].Comment = comment hosts[idx].Comment = comment
return hosts, fmt.Sprintf("Kommentar von %q gesetzt.", name), nil return hosts, fmt.Sprintf("Comment set for %q.", name), nil
}) })
} }
@@ -308,16 +344,16 @@ func cmdChangeIP(args []string) {
withStore(func(hosts []Host) ([]Host, string, error) { withStore(func(hosts []Host) ([]Host, string, error) {
idx := findHost(hosts, name) idx := findHost(hosts, name)
if idx < 0 { if idx < 0 {
return nil, "", fmt.Errorf("Host %q nicht gefunden", name) return nil, "", fmt.Errorf("host %q not found", name)
} }
if err := validateIP(ip, cfg.lanCIDR, cfg.poolStart, cfg.poolEnd); err != nil { if err := validateIP(ip, cfg.lanCIDR, cfg.poolStart, cfg.poolEnd); err != nil {
return nil, "", err return nil, "", err
} }
if other := findByIP(hosts, ip, idx); other >= 0 { if other := findByIP(hosts, ip, idx); other >= 0 {
return nil, "", fmt.Errorf("IP %s ist bereits Host %q zugewiesen", ip, hosts[other].Name) return nil, "", fmt.Errorf("IP %s is already assigned to host %q", ip, hosts[other].Name)
} }
hosts[idx].IP = ip hosts[idx].IP = ip
return hosts, fmt.Sprintf("IP von %q auf %s geändert.", name, ip), nil return hosts, fmt.Sprintf("IP of %q changed to %s.", name, ip), nil
}) })
} }
@@ -332,21 +368,21 @@ func cmdChangeMAC(args []string) {
withStore(func(hosts []Host) ([]Host, string, error) { withStore(func(hosts []Host) ([]Host, string, error) {
idx := findHost(hosts, name) idx := findHost(hosts, name)
if idx < 0 { if idx < 0 {
return nil, "", fmt.Errorf("Host %q nicht gefunden", name) return nil, "", fmt.Errorf("host %q not found", name)
} }
normMac, err := normalizeMAC(mac) normMac, err := normalizeMAC(mac)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
if other := findByMAC(hosts, normMac, idx); other >= 0 { if other := findByMAC(hosts, normMac, idx); other >= 0 {
return nil, "", fmt.Errorf("MAC %s ist bereits Host %q zugewiesen", normMac, hosts[other].Name) return nil, "", fmt.Errorf("MAC %s is already assigned to host %q", normMac, hosts[other].Name)
} }
hosts[idx].MAC = normMac hosts[idx].MAC = normMac
label := normMac label := normMac
if label == "" { if label == "" {
label = "(entfernt)" label = "(removed)"
} }
return hosts, fmt.Sprintf("MAC von %q auf %s geändert.", name, label), nil return hosts, fmt.Sprintf("MAC of %q changed to %s.", name, label), nil
}) })
} }
@@ -361,13 +397,13 @@ func cmdRename(args []string) {
} }
idx := findHost(hosts, oldName) idx := findHost(hosts, oldName)
if idx < 0 { if idx < 0 {
return nil, "", fmt.Errorf("Host %q nicht gefunden", oldName) return nil, "", fmt.Errorf("host %q not found", oldName)
} }
if existing := findHost(hosts, newName); existing >= 0 && existing != idx { if existing := findHost(hosts, newName); existing >= 0 && existing != idx {
return nil, "", fmt.Errorf("Host %q existiert bereits", newName) return nil, "", fmt.Errorf("host %q already exists", newName)
} }
hosts[idx].Name = newName hosts[idx].Name = newName
return hosts, fmt.Sprintf("Host %q umbenannt in %q.", oldName, newName), nil return hosts, fmt.Sprintf("Host %q renamed to %q.", oldName, newName), nil
}) })
} }
@@ -379,10 +415,10 @@ func cmdDelete(args []string) {
withStore(func(hosts []Host) ([]Host, string, error) { withStore(func(hosts []Host) ([]Host, string, error) {
idx := findHost(hosts, name) idx := findHost(hosts, name)
if idx < 0 { if idx < 0 {
return nil, "", fmt.Errorf("Host %q nicht gefunden", name) return nil, "", fmt.Errorf("host %q not found", name)
} }
hosts = append(hosts[:idx], hosts[idx+1:]...) hosts = append(hosts[:idx], hosts[idx+1:]...)
return hosts, fmt.Sprintf("Host %q gelöscht.", name), nil return hosts, fmt.Sprintf("Host %q deleted.", name), nil
}) })
} }
@@ -403,7 +439,7 @@ func cmdPortFwdAdd(args []string) {
withPortFwdStore(func(hosts []Host, fwds []PortForward) ([]PortForward, string, error) { withPortFwdStore(func(hosts []Host, fwds []PortForward) ([]PortForward, string, error) {
if findHost(hosts, name) < 0 { if findHost(hosts, name) < 0 {
return nil, "", fmt.Errorf("Host %q ist nicht in der ipadm-Datenbank (erst mit `ipadm %s` oder `ipadm -a` anlegen)", name, name) return nil, "", fmt.Errorf("host %q is not in the ipadm database (add it first with `ipadm %s` or `ipadm -a`)", name, name)
} }
lanPort, proto, err := classifyPortFwdArgs(args[2:], wanPort) lanPort, proto, err := classifyPortFwdArgs(args[2:], wanPort)
if err != nil { if err != nil {
@@ -418,14 +454,14 @@ func cmdPortFwdAdd(args []string) {
conflicts := findConflictingPortFwds(fwds, wanPort, proto, -1) conflicts := findConflictingPortFwds(fwds, wanPort, proto, -1)
if len(conflicts) > 0 && !force { if len(conflicts) > 0 && !force {
return nil, "", fmt.Errorf("wan-port %d/%s kollidiert mit bestehendem Port-Forward (benutze -f zum Überschreiben)", wanPort, proto) return nil, "", fmt.Errorf("wan-port %d/%s conflicts with an existing port-forward (use -f to overwrite)", wanPort, proto)
} }
for i := len(conflicts) - 1; i >= 0; i-- { for i := len(conflicts) - 1; i >= 0; i-- {
fwds = append(fwds[:conflicts[i]], fwds[conflicts[i]+1:]...) fwds = append(fwds[:conflicts[i]], fwds[conflicts[i]+1:]...)
} }
fwds = append(fwds, PortForward{WanPort: wanPort, Proto: proto, Host: name, LanPort: lanPort}) fwds = append(fwds, PortForward{WanPort: wanPort, Proto: proto, Host: name, LanPort: lanPort})
return fwds, fmt.Sprintf("Port-Forward %d/%s -> %s:%d angelegt.", wanPort, proto, name, lanPort), nil return fwds, fmt.Sprintf("Port-forward %d/%s -> %s:%d added.", wanPort, proto, name, lanPort), nil
}) })
} }
@@ -454,7 +490,7 @@ func cmdPortFwdList() {
fail("%s", err) fail("%s", err)
} }
if len(fwds) == 0 { if len(fwds) == 0 {
fmt.Println("keine Port-Forwards eingetragen") fmt.Println("no port-forwards configured")
return return
} }
sortPortFwds(fwds) sortPortFwds(fwds)
@@ -463,7 +499,7 @@ func cmdPortFwdList() {
for _, f := range fwds { for _, f := range fwds {
ip, ok := ipByHost[strings.ToLower(f.Host)] ip, ok := ipByHost[strings.ToLower(f.Host)]
if !ok { if !ok {
ip = "??? (Host fehlt)" ip = "??? (host missing)"
} }
fmt.Fprintf(tw, "%d\t%s\t%s\t%s\t%d\n", f.WanPort, f.Proto, f.Host, ip, f.LanPort) fmt.Fprintf(tw, "%d\t%s\t%s\t%s\t%d\n", f.WanPort, f.Proto, f.Host, ip, f.LanPort)
} }
@@ -482,33 +518,33 @@ func cmdPortFwdDelete(args []string) {
if len(args) == 2 { if len(args) == 2 {
proto = args[1] proto = args[1]
if !validProtos[proto] { if !validProtos[proto] {
fail("ungültiges Protokoll %q (erwarte tcp/udp/both)", proto) fail("invalid protocol %q (expected tcp/udp/both)", proto)
} }
} }
withPortFwdStore(func(hosts []Host, fwds []PortForward) ([]PortForward, string, error) { withPortFwdStore(func(hosts []Host, fwds []PortForward) ([]PortForward, string, error) {
matches := findPortFwd(fwds, wanPort, proto) matches := findPortFwd(fwds, wanPort, proto)
if len(matches) == 0 { if len(matches) == 0 {
return nil, "", fmt.Errorf("kein Port-Forward für wan-port %d gefunden", wanPort) return nil, "", fmt.Errorf("no port-forward found for wan-port %d", wanPort)
} }
if len(matches) > 1 { if len(matches) > 1 {
var protos []string var protos []string
for _, i := range matches { for _, i := range matches {
protos = append(protos, fwds[i].Proto) protos = append(protos, fwds[i].Proto)
} }
return nil, "", fmt.Errorf("wan-port %d ist mehrdeutig (Protokolle: %s) — bitte Protokoll angeben", wanPort, strings.Join(protos, ", ")) return nil, "", fmt.Errorf("wan-port %d is ambiguous (protocols: %s) — please specify a protocol", wanPort, strings.Join(protos, ", "))
} }
idx := matches[0] idx := matches[0]
deleted := fwds[idx] deleted := fwds[idx]
fwds = append(fwds[:idx], fwds[idx+1:]...) fwds = append(fwds[:idx], fwds[idx+1:]...)
return fwds, fmt.Sprintf("Port-Forward %d/%s (-> %s:%d) gelöscht.", deleted.WanPort, deleted.Proto, deleted.Host, deleted.LanPort), nil return fwds, fmt.Sprintf("Port-forward %d/%s (-> %s:%d) deleted.", deleted.WanPort, deleted.Proto, deleted.Host, deleted.LanPort), nil
}) })
} }
func parsePort(s string) (int, error) { func parsePort(s string) (int, error) {
var port int var port int
if _, err := fmt.Sscanf(s, "%d", &port); err != nil { if _, err := fmt.Sscanf(s, "%d", &port); err != nil {
return 0, fmt.Errorf("ungültiger Port: %q", s) return 0, fmt.Errorf("invalid port: %q", s)
} }
return port, nil return port, nil
} }
@@ -530,7 +566,7 @@ func cmdUpdate() {
if err := applyDnsmasqConfig(dnsmasqHostsPath(), dnsContent); err != nil { if err := applyDnsmasqConfig(dnsmasqHostsPath(), dnsContent); err != nil {
fail("%s", err) fail("%s", err)
} }
fmt.Printf("%s aktualisiert, dnsmasq neu geladen (%d Hosts).\n", dnsmasqHostsPath(), len(hosts)) fmt.Printf("%s updated, dnsmasq reloaded (%d hosts).\n", dnsmasqHostsPath(), len(hosts))
fs, err := openPortFwdStore(portFwdDBPath()) fs, err := openPortFwdStore(portFwdDBPath())
if err != nil { if err != nil {
@@ -544,10 +580,10 @@ func cmdUpdate() {
nftContent, warnings := renderPortForwards(hosts, fwds, cfg.wanIface) nftContent, warnings := renderPortForwards(hosts, fwds, cfg.wanIface)
for _, w := range warnings { for _, w := range warnings {
fmt.Fprintln(os.Stderr, "warnung:", w) fmt.Fprintln(os.Stderr, "warning:", w)
} }
if err := applyPortForwards(nftPortFwdPath(), nftContent); err != nil { if err := applyPortForwards(nftPortFwdPath(), nftContent); err != nil {
fail("%s", err) fail("%s", err)
} }
fmt.Printf("%s aktualisiert, nftables neu geladen (%d Port-Forwards).\n", nftPortFwdPath(), len(fwds)-len(warnings)) fmt.Printf("%s updated, nftables reloaded (%d port-forwards).\n", nftPortFwdPath(), len(fwds)-len(warnings))
} }
+6 -6
View File
@@ -25,7 +25,7 @@ func renderPortForwards(hosts []Host, fwds []PortForward, wanIface string) (cont
for _, f := range sorted { for _, f := range sorted {
ip, ok := ipByHost[strings.ToLower(f.Host)] ip, ok := ipByHost[strings.ToLower(f.Host)]
if !ok { if !ok {
warnings = append(warnings, fmt.Sprintf("Port-Forward %d/%s -> %q übersprungen: Host nicht (mehr) in der ipadm-Datenbank", f.WanPort, f.Proto, f.Host)) warnings = append(warnings, fmt.Sprintf("port-forward %d/%s -> %q skipped: host no longer in the ipadm database", f.WanPort, f.Proto, f.Host))
continue continue
} }
switch f.Proto { switch f.Proto {
@@ -58,14 +58,14 @@ func applyPortForwards(path string, content string) error {
backup = b backup = b
hadFile = true hadFile = true
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
return fmt.Errorf("kann bestehende %s nicht lesen: %w", path, err) return fmt.Errorf("cannot read existing %s: %w", path, err)
} }
if err := os.MkdirAll(dirOf(path), 0755); err != nil { if err := os.MkdirAll(dirOf(path), 0755); err != nil {
return fmt.Errorf("kann Verzeichnis für %s nicht anlegen: %w", path, err) return fmt.Errorf("cannot create directory for %s: %w", path, err)
} }
if err := os.WriteFile(path, []byte(content), 0644); err != nil { if err := os.WriteFile(path, []byte(content), 0644); err != nil {
return fmt.Errorf("kann %s nicht schreiben: %w", path, err) return fmt.Errorf("cannot write %s: %w", path, err)
} }
if err := runNftTest(); err != nil { if err := runNftTest(); err != nil {
@@ -74,11 +74,11 @@ func applyPortForwards(path string, content string) error {
} else { } else {
os.Remove(path) os.Remove(path)
} }
return fmt.Errorf("nftables-Konfiguration ungültig, Änderung zurückgerollt: %w", err) return fmt.Errorf("nftables config invalid, change rolled back: %w", err)
} }
if err := reloadNftables(); err != nil { if err := reloadNftables(); err != nil {
return fmt.Errorf("%s geschrieben, aber Reload fehlgeschlagen: %w", path, err) return fmt.Errorf("%s written, but reload failed: %w", path, err)
} }
return nil return nil
} }
+8 -8
View File
@@ -26,19 +26,19 @@ const portFwdHeader = "# ipadm port-forward database - managed with `ipadm`, do
func parsePortFwdLine(line string) (PortForward, error) { func parsePortFwdLine(line string) (PortForward, error) {
parts := strings.Split(line, "\t") parts := strings.Split(line, "\t")
if len(parts) != 4 { if len(parts) != 4 {
return PortForward{}, fmt.Errorf("ungültige Zeile (erwarte 4 Tab-getrennte Felder): %q", line) return PortForward{}, fmt.Errorf("invalid line (expected 4 tab-separated fields): %q", line)
} }
wanPort, err := strconv.Atoi(parts[0]) wanPort, err := strconv.Atoi(parts[0])
if err != nil { if err != nil {
return PortForward{}, fmt.Errorf("ungültiger wan-port: %q", parts[0]) return PortForward{}, fmt.Errorf("invalid wan-port: %q", parts[0])
} }
lanPort, err := strconv.Atoi(parts[3]) lanPort, err := strconv.Atoi(parts[3])
if err != nil { if err != nil {
return PortForward{}, fmt.Errorf("ungültiger lan-port: %q", parts[3]) return PortForward{}, fmt.Errorf("invalid lan-port: %q", parts[3])
} }
proto := parts[1] proto := parts[1]
if !validProtos[proto] { if !validProtos[proto] {
return PortForward{}, fmt.Errorf("ungültiges Protokoll: %q", proto) return PortForward{}, fmt.Errorf("invalid protocol: %q", proto)
} }
return PortForward{WanPort: wanPort, Proto: proto, Host: parts[2], LanPort: lanPort}, nil return PortForward{WanPort: wanPort, Proto: proto, Host: parts[2], LanPort: lanPort}, nil
} }
@@ -62,7 +62,7 @@ func sortPortFwds(fwds []PortForward) {
func validatePort(label string, port int) error { func validatePort(label string, port int) error {
if port < 1 || port > 65535 { if port < 1 || port > 65535 {
return fmt.Errorf("ungültiger %s: %d (erlaubt: 1-65535)", label, port) return fmt.Errorf("invalid %s: %d (allowed: 1-65535)", label, port)
} }
return nil return nil
} }
@@ -111,7 +111,7 @@ func classifyPortFwdArgs(args []string, wanPort int) (lanPort int, proto string,
for _, a := range args { for _, a := range args {
if validProtos[a] { if validProtos[a] {
if protoSet { if protoSet {
return 0, "", fmt.Errorf("Protokoll mehrfach angegeben: %q", a) return 0, "", fmt.Errorf("protocol given more than once: %q", a)
} }
proto = a proto = a
protoSet = true protoSet = true
@@ -119,13 +119,13 @@ func classifyPortFwdArgs(args []string, wanPort int) (lanPort int, proto string,
} }
if n, convErr := strconv.Atoi(a); convErr == nil { if n, convErr := strconv.Atoi(a); convErr == nil {
if lanPortSet { if lanPortSet {
return 0, "", fmt.Errorf("lan-port mehrfach angegeben: %q", a) return 0, "", fmt.Errorf("lan-port given more than once: %q", a)
} }
lanPort = n lanPort = n
lanPortSet = true lanPortSet = true
continue continue
} }
return 0, "", fmt.Errorf("unbekanntes Argument %q (erwarte lan-port oder tcp/udp/both)", a) return 0, "", fmt.Errorf("unrecognized argument %q (expected lan-port or tcp/udp/both)", a)
} }
return lanPort, proto, nil return lanPort, proto, nil
} }
+3 -3
View File
@@ -21,15 +21,15 @@ type LineStore[T any] struct {
func openLineStore[T any](path, header string, parse func(string) (T, error), format func(T) string) (*LineStore[T], error) { func openLineStore[T any](path, header string, parse func(string) (T, error), format func(T) string) (*LineStore[T], error) {
if err := os.MkdirAll(dirOf(path), 0755); err != nil { if err := os.MkdirAll(dirOf(path), 0755); err != nil {
return nil, fmt.Errorf("kann Verzeichnis für %s nicht anlegen: %w", path, err) return nil, fmt.Errorf("cannot create directory for %s: %w", path, err)
} }
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644) f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil { if err != nil {
return nil, fmt.Errorf("kann %s nicht öffnen: %w", path, err) return nil, fmt.Errorf("cannot open %s: %w", path, err)
} }
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {
f.Close() f.Close()
return nil, fmt.Errorf("kann Lock auf %s nicht setzen: %w", path, err) return nil, fmt.Errorf("cannot lock %s: %w", path, err)
} }
return &LineStore[T]{path: path, file: f, header: header, parse: parse, format: format}, nil return &LineStore[T]{path: path, file: f, header: header, parse: parse, format: format}, nil
} }
+7 -7
View File
@@ -10,7 +10,7 @@ var hostnameRe = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])
func validateHostname(name string) error { func validateHostname(name string) error {
if !hostnameRe.MatchString(name) { if !hostnameRe.MatchString(name) {
return fmt.Errorf("ungültiger Hostname %q (nur Buchstaben, Ziffern, Bindestrich, kein führender/abschließender Bindestrich, keine Domain)", name) return fmt.Errorf("invalid hostname %q (letters, digits, hyphen only; no leading/trailing hyphen; no domain)", name)
} }
return nil return nil
} }
@@ -26,23 +26,23 @@ func ip4ToUint32(ip net.IP) uint32 {
func validateIP(ip, lanCIDR, poolStart, poolEnd string) error { func validateIP(ip, lanCIDR, poolStart, poolEnd string) error {
parsed := net.ParseIP(ip) parsed := net.ParseIP(ip)
if parsed == nil || parsed.To4() == nil { if parsed == nil || parsed.To4() == nil {
return fmt.Errorf("ungültige IPv4-Adresse: %q", ip) return fmt.Errorf("invalid IPv4 address: %q", ip)
} }
_, cidr, err := net.ParseCIDR(lanCIDR) _, cidr, err := net.ParseCIDR(lanCIDR)
if err != nil { if err != nil {
return fmt.Errorf("internes Problem: ungültiges LAN-CIDR %q: %w", lanCIDR, err) return fmt.Errorf("internal error: invalid LAN CIDR %q: %w", lanCIDR, err)
} }
if !cidr.Contains(parsed) { if !cidr.Contains(parsed) {
return fmt.Errorf("IP %s liegt nicht im LAN-Netz %s", ip, lanCIDR) return fmt.Errorf("IP %s is not inside LAN network %s", ip, lanCIDR)
} }
start := net.ParseIP(poolStart) start := net.ParseIP(poolStart)
end := net.ParseIP(poolEnd) end := net.ParseIP(poolEnd)
if start == nil || end == nil { if start == nil || end == nil {
return fmt.Errorf("internes Problem: ungültiger DHCP-Pool %q-%q", poolStart, poolEnd) return fmt.Errorf("internal error: invalid DHCP pool %q-%q", poolStart, poolEnd)
} }
v, s, e := ip4ToUint32(parsed), ip4ToUint32(start), ip4ToUint32(end) v, s, e := ip4ToUint32(parsed), ip4ToUint32(start), ip4ToUint32(end)
if v >= s && v <= e { if v >= s && v <= e {
return fmt.Errorf("IP %s liegt im dynamischen DHCP-Pool (%s-%s) und ist für statische Reservierungen gesperrt", ip, poolStart, poolEnd) return fmt.Errorf("IP %s is inside the dynamic DHCP pool (%s-%s) and is reserved, not allowed for static assignments", ip, poolStart, poolEnd)
} }
return nil return nil
} }
@@ -55,7 +55,7 @@ func normalizeMAC(mac string) (string, error) {
} }
hw, err := net.ParseMAC(mac) hw, err := net.ParseMAC(mac)
if err != nil { if err != nil {
return "", fmt.Errorf("ungültige MAC-Adresse %q: %w", mac, err) return "", fmt.Errorf("invalid MAC address %q: %w", mac, err)
} }
return hw.String(), nil return hw.String(), nil
} }