ipadm: Port-Forwarding hinzufügen (-pa/-pl/-pd) + nftables-Include verdrahten
- ipadm verwaltet jetzt auch WAN->LAN Port-Forwards, referenziert per Hostname aus der bestehenden Host-DB (folgt IP-Änderungen automatisch) - ipadm -u generiert zusätzlich /etc/nftables.d/portforward.conf, validiert via 'nft -c -f' und reloadet nftables (Rollback bei ungültiger Config, wie beim dnsmasq-Teil) - /etc/nftables.conf bindet dafür neu /etc/nftables.d/*.conf ein - Host-Store-Locking-Logik in generischen LineStore[T] extrahiert, von Host- und PortForward-Store gemeinsam genutzt
This commit is contained in:
+229
-31
@@ -1,8 +1,10 @@
|
||||
// ipadm - dhcp/dns management for the dnsmasq-based router setup on this host.
|
||||
//
|
||||
// Maintains a flat-file database of static hosts (hostname, IP, optional
|
||||
// MAC, comment) and, on `ipadm -u`, regenerates the dnsmasq include file
|
||||
// with matching host-record/dhcp-host entries and reloads dnsmasq.
|
||||
// MAC, comment) and of port-forwards (WAN port/proto -> host from that
|
||||
// database). On `ipadm -u`, regenerates the dnsmasq include file
|
||||
// (host-record/dhcp-host entries) and the nftables include file (DNAT
|
||||
// rules), then reloads both services.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -12,7 +14,7 @@ import (
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
const version = "1.0.0"
|
||||
const version = "1.1.0"
|
||||
|
||||
const usage = `ipadm - dhcp/dns management (dnsmasq backend) v` + version + `
|
||||
|
||||
@@ -24,17 +26,22 @@ usage: ipadm <hostname> add/edit host (inter
|
||||
ipadm -m <hostname> <mac address> change mac address
|
||||
ipadm -r <old hostname> <new hostname> rename host
|
||||
ipadm -d <hostname> delete host
|
||||
ipadm -u update (regenerate dnsmasq config + reload)
|
||||
ipadm -pa [-f] <hostname> <wan-port> [lan-port] [tcp|udp|both]
|
||||
add port-forward to a known host
|
||||
ipadm -pl list port-forwards
|
||||
ipadm -pd <wan-port> [tcp|udp|both] delete port-forward
|
||||
ipadm -u update (regenerate dnsmasq+nftables config, reload)
|
||||
ipadm -h this help
|
||||
`
|
||||
|
||||
// netConfig describes the LAN the tool validates static IPs against.
|
||||
// Overridable via env vars, mainly so this can be tested without touching
|
||||
// the real /etc files.
|
||||
// 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.
|
||||
type netConfig struct {
|
||||
lanCIDR string
|
||||
poolStart string
|
||||
poolEnd string
|
||||
wanIface string
|
||||
}
|
||||
|
||||
func envOr(key, def string) string {
|
||||
@@ -48,15 +55,24 @@ func dbPath() string {
|
||||
return envOr("IPADM_DB", "/etc/ipadm/hosts")
|
||||
}
|
||||
|
||||
func portFwdDBPath() string {
|
||||
return envOr("IPADM_PORTFWD_DB", "/etc/ipadm/portforwards")
|
||||
}
|
||||
|
||||
func dnsmasqHostsPath() string {
|
||||
return envOr("IPADM_DNSMASQ_HOSTS", "/etc/dnsmasq.d/hosts.conf")
|
||||
}
|
||||
|
||||
func nftPortFwdPath() string {
|
||||
return envOr("IPADM_NFT_PORTFWD", "/etc/nftables.d/portforward.conf")
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +109,13 @@ func main() {
|
||||
cmdRename(args[1:])
|
||||
case "-d":
|
||||
cmdDelete(args[1:])
|
||||
case "-pa":
|
||||
cmdPortFwdAdd(args[1:])
|
||||
case "-pl":
|
||||
requireArgs(args[1:], 0, "ipadm -pl")
|
||||
cmdPortFwdList()
|
||||
case "-pd":
|
||||
cmdPortFwdDelete(args[1:])
|
||||
default:
|
||||
if strings.HasPrefix(args[0], "-") {
|
||||
fail("unbekannte Option %q\n\n%s", args[0], usage)
|
||||
@@ -112,7 +135,7 @@ func requireArgs(rest []string, want int, form string) {
|
||||
// on success. fn returns the new host list plus a human-readable summary of
|
||||
// what changed (printed on success).
|
||||
func withStore(fn func(hosts []Host) ([]Host, string, error)) {
|
||||
s, err := openStore(dbPath())
|
||||
s, err := openHostStore(dbPath())
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
@@ -128,11 +151,49 @@ func withStore(fn func(hosts []Host) ([]Host, string, error)) {
|
||||
fail("%s", err)
|
||||
}
|
||||
|
||||
sortHosts(newHosts)
|
||||
if err := s.Save(newHosts); err != nil {
|
||||
fail("kann %s nicht schreiben: %s", dbPath(), err)
|
||||
}
|
||||
fmt.Println(summary)
|
||||
fmt.Println("Hinweis: `ipadm -u` ausführen, um dnsmasq zu aktualisieren.")
|
||||
fmt.Println("Hinweis: `ipadm -u` ausführen, um dnsmasq/nftables zu aktualisieren.")
|
||||
}
|
||||
|
||||
// withPortFwdStore mirrors withStore for the port-forward DB. It also loads
|
||||
// the (read-only) host DB so add/delete can validate against known hosts.
|
||||
func withPortFwdStore(fn func(hosts []Host, fwds []PortForward) ([]PortForward, string, error)) {
|
||||
hs, err := openHostStore(dbPath())
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
hosts, err := hs.Load()
|
||||
hs.Close()
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
|
||||
s, err := openPortFwdStore(portFwdDBPath())
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
fwds, err := s.Load()
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
|
||||
newFwds, summary, err := fn(hosts, fwds)
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
|
||||
sortPortFwds(newFwds)
|
||||
if err := s.Save(newFwds); err != nil {
|
||||
fail("kann %s nicht schreiben: %s", portFwdDBPath(), err)
|
||||
}
|
||||
fmt.Println(summary)
|
||||
fmt.Println("Hinweis: `ipadm -u` ausführen, um nftables zu aktualisieren.")
|
||||
}
|
||||
|
||||
func cmdInteractive(name string) {
|
||||
@@ -147,7 +208,7 @@ func cmdInteractive(name string) {
|
||||
}
|
||||
|
||||
func cmdList() {
|
||||
s, err := openStore(dbPath())
|
||||
s, err := openHostStore(dbPath())
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
@@ -160,9 +221,10 @@ func cmdList() {
|
||||
fmt.Println("keine Hosts eingetragen")
|
||||
return
|
||||
}
|
||||
sortHosts(hosts)
|
||||
tw := tabwriter.NewWriter(os.Stdout, 2, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "HOSTNAME\tIP\tMAC\tCOMMENT")
|
||||
for _, h := range sortedCopy(hosts) {
|
||||
for _, h := range hosts {
|
||||
mac := h.MAC
|
||||
if mac == "" {
|
||||
mac = "-"
|
||||
@@ -172,18 +234,6 @@ func cmdList() {
|
||||
tw.Flush()
|
||||
}
|
||||
|
||||
func sortedCopy(hosts []Host) []Host {
|
||||
out := make([]Host, len(hosts))
|
||||
copy(out, hosts)
|
||||
// Save() sorts on write, but list can run before the first save.
|
||||
for i := 1; i < len(out); i++ {
|
||||
for j := i; j > 0 && strings.ToLower(out[j-1].Name) > strings.ToLower(out[j].Name); j-- {
|
||||
out[j-1], out[j] = out[j], out[j-1]
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cmdAdd(args []string) {
|
||||
force := false
|
||||
if len(args) > 0 && args[0] == "-f" {
|
||||
@@ -336,20 +386,168 @@ func cmdDelete(args []string) {
|
||||
})
|
||||
}
|
||||
|
||||
func cmdUpdate() {
|
||||
s, err := openStore(dbPath())
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
func cmdPortFwdAdd(args []string) {
|
||||
force := false
|
||||
if len(args) > 0 && args[0] == "-f" {
|
||||
force = true
|
||||
args = args[1:]
|
||||
}
|
||||
hosts, err := s.Load()
|
||||
s.Close()
|
||||
if len(args) < 2 || len(args) > 4 {
|
||||
fail("usage: ipadm -pa [-f] <hostname> <wan-port> [lan-port] [tcp|udp|both]")
|
||||
}
|
||||
name := args[0]
|
||||
wanPort, err := parsePort(args[1])
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
|
||||
content := renderDnsmasqConfig(hosts)
|
||||
if err := applyDnsmasqConfig(dnsmasqHostsPath(), content); err != nil {
|
||||
withPortFwdStore(func(hosts []Host, fwds []PortForward) ([]PortForward, string, error) {
|
||||
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)
|
||||
}
|
||||
lanPort, proto, err := classifyPortFwdArgs(args[2:], wanPort)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if err := validatePort("wan-port", wanPort); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if err := validatePort("lan-port", lanPort); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
conflicts := findConflictingPortFwds(fwds, wanPort, proto, -1)
|
||||
if len(conflicts) > 0 && !force {
|
||||
return nil, "", fmt.Errorf("wan-port %d/%s kollidiert mit bestehendem Port-Forward (benutze -f zum Überschreiben)", wanPort, proto)
|
||||
}
|
||||
for i := len(conflicts) - 1; i >= 0; i-- {
|
||||
fwds = append(fwds[:conflicts[i]], fwds[conflicts[i]+1:]...)
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
func cmdPortFwdList() {
|
||||
hs, err := openHostStore(dbPath())
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
hosts, err := hs.Load()
|
||||
hs.Close()
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
ipByHost := map[string]string{}
|
||||
for _, h := range hosts {
|
||||
ipByHost[strings.ToLower(h.Name)] = h.IP
|
||||
}
|
||||
|
||||
s, err := openPortFwdStore(portFwdDBPath())
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
defer s.Close()
|
||||
fwds, err := s.Load()
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
if len(fwds) == 0 {
|
||||
fmt.Println("keine Port-Forwards eingetragen")
|
||||
return
|
||||
}
|
||||
sortPortFwds(fwds)
|
||||
tw := tabwriter.NewWriter(os.Stdout, 2, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "WAN-PORT\tPROTO\tHOST\tLAN-IP\tLAN-PORT")
|
||||
for _, f := range fwds {
|
||||
ip, ok := ipByHost[strings.ToLower(f.Host)]
|
||||
if !ok {
|
||||
ip = "??? (Host fehlt)"
|
||||
}
|
||||
fmt.Fprintf(tw, "%d\t%s\t%s\t%s\t%d\n", f.WanPort, f.Proto, f.Host, ip, f.LanPort)
|
||||
}
|
||||
tw.Flush()
|
||||
}
|
||||
|
||||
func cmdPortFwdDelete(args []string) {
|
||||
if len(args) < 1 || len(args) > 2 {
|
||||
fail("usage: ipadm -pd <wan-port> [tcp|udp|both]")
|
||||
}
|
||||
wanPort, err := parsePort(args[0])
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
proto := ""
|
||||
if len(args) == 2 {
|
||||
proto = args[1]
|
||||
if !validProtos[proto] {
|
||||
fail("ungültiges Protokoll %q (erwarte tcp/udp/both)", proto)
|
||||
}
|
||||
}
|
||||
|
||||
withPortFwdStore(func(hosts []Host, fwds []PortForward) ([]PortForward, string, error) {
|
||||
matches := findPortFwd(fwds, wanPort, proto)
|
||||
if len(matches) == 0 {
|
||||
return nil, "", fmt.Errorf("kein Port-Forward für wan-port %d gefunden", wanPort)
|
||||
}
|
||||
if len(matches) > 1 {
|
||||
var protos []string
|
||||
for _, i := range matches {
|
||||
protos = append(protos, fwds[i].Proto)
|
||||
}
|
||||
return nil, "", fmt.Errorf("wan-port %d ist mehrdeutig (Protokolle: %s) — bitte Protokoll angeben", wanPort, strings.Join(protos, ", "))
|
||||
}
|
||||
idx := matches[0]
|
||||
deleted := fwds[idx]
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
func parsePort(s string) (int, error) {
|
||||
var port int
|
||||
if _, err := fmt.Sscanf(s, "%d", &port); err != nil {
|
||||
return 0, fmt.Errorf("ungültiger Port: %q", s)
|
||||
}
|
||||
return port, nil
|
||||
}
|
||||
|
||||
func cmdUpdate() {
|
||||
cfg := loadNetConfig()
|
||||
|
||||
hs, err := openHostStore(dbPath())
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
hosts, err := hs.Load()
|
||||
hs.Close()
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
|
||||
dnsContent := renderDnsmasqConfig(hosts)
|
||||
if err := applyDnsmasqConfig(dnsmasqHostsPath(), dnsContent); err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
fmt.Printf("%s aktualisiert, dnsmasq neu geladen (%d Hosts).\n", dnsmasqHostsPath(), len(hosts))
|
||||
|
||||
fs, err := openPortFwdStore(portFwdDBPath())
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
fwds, err := fs.Load()
|
||||
fs.Close()
|
||||
if err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
|
||||
nftContent, warnings := renderPortForwards(hosts, fwds, cfg.wanIface)
|
||||
for _, w := range warnings {
|
||||
fmt.Fprintln(os.Stderr, "warnung:", w)
|
||||
}
|
||||
if err := applyPortForwards(nftPortFwdPath(), nftContent); err != nil {
|
||||
fail("%s", err)
|
||||
}
|
||||
fmt.Printf("%s aktualisiert, nftables neu geladen (%d Port-Forwards).\n", nftPortFwdPath(), len(fwds)-len(warnings))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user