- 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
132 lines
3.7 KiB
Go
132 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// PortForward maps a WAN port/protocol to a port on a host that must exist
|
|
// in the ipadm host database (referenced by name, so the forward keeps
|
|
// working if the host's IP changes later).
|
|
type PortForward struct {
|
|
WanPort int
|
|
Proto string // "tcp", "udp", or "both"
|
|
Host string
|
|
LanPort int
|
|
}
|
|
|
|
var validProtos = map[string]bool{"tcp": true, "udp": true, "both": true}
|
|
|
|
const portFwdHeader = "# ipadm port-forward database - managed with `ipadm`, do not edit while ipadm is running\n" +
|
|
"# wanport\tproto\thost\tlanport\n"
|
|
|
|
// One line per forward, tab-separated: wanport\tproto\thost\tlanport
|
|
func parsePortFwdLine(line string) (PortForward, error) {
|
|
parts := strings.Split(line, "\t")
|
|
if len(parts) != 4 {
|
|
return PortForward{}, fmt.Errorf("ungültige Zeile (erwarte 4 Tab-getrennte Felder): %q", line)
|
|
}
|
|
wanPort, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return PortForward{}, fmt.Errorf("ungültiger wan-port: %q", parts[0])
|
|
}
|
|
lanPort, err := strconv.Atoi(parts[3])
|
|
if err != nil {
|
|
return PortForward{}, fmt.Errorf("ungültiger lan-port: %q", parts[3])
|
|
}
|
|
proto := parts[1]
|
|
if !validProtos[proto] {
|
|
return PortForward{}, fmt.Errorf("ungültiges Protokoll: %q", proto)
|
|
}
|
|
return PortForward{WanPort: wanPort, Proto: proto, Host: parts[2], LanPort: lanPort}, nil
|
|
}
|
|
|
|
func formatPortFwdLine(p PortForward) string {
|
|
return fmt.Sprintf("%d\t%s\t%s\t%d", p.WanPort, p.Proto, p.Host, p.LanPort)
|
|
}
|
|
|
|
func openPortFwdStore(path string) (*LineStore[PortForward], error) {
|
|
return openLineStore(path, portFwdHeader, parsePortFwdLine, formatPortFwdLine)
|
|
}
|
|
|
|
func sortPortFwds(fwds []PortForward) {
|
|
sort.Slice(fwds, func(i, j int) bool {
|
|
if fwds[i].WanPort != fwds[j].WanPort {
|
|
return fwds[i].WanPort < fwds[j].WanPort
|
|
}
|
|
return fwds[i].Proto < fwds[j].Proto
|
|
})
|
|
}
|
|
|
|
func validatePort(label string, port int) error {
|
|
if port < 1 || port > 65535 {
|
|
return fmt.Errorf("ungültiger %s: %d (erlaubt: 1-65535)", label, port)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// protosOverlap reports whether two port-forward entries on the same WAN
|
|
// port would both try to handle the same traffic (e.g. "tcp" and "both").
|
|
func protosOverlap(a, b string) bool {
|
|
if a == b {
|
|
return true
|
|
}
|
|
return a == "both" || b == "both"
|
|
}
|
|
|
|
// findConflictingPortFwds returns the indexes of entries that occupy the
|
|
// same (wanport, proto) as the given one, excluding exceptIdx.
|
|
func findConflictingPortFwds(fwds []PortForward, wanPort int, proto string, exceptIdx int) []int {
|
|
var out []int
|
|
for i, f := range fwds {
|
|
if i == exceptIdx {
|
|
continue
|
|
}
|
|
if f.WanPort == wanPort && protosOverlap(f.Proto, proto) {
|
|
out = append(out, i)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func findPortFwd(fwds []PortForward, wanPort int, proto string) []int {
|
|
var out []int
|
|
for i, f := range fwds {
|
|
if f.WanPort == wanPort && (proto == "" || f.Proto == proto) {
|
|
out = append(out, i)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// classifyPortFwdArgs sorts the optional trailing arguments of `ipadm -pa`
|
|
// (lan-port and/or protocol, in any order) into their fields. Returns an
|
|
// error on unrecognized or duplicate tokens.
|
|
func classifyPortFwdArgs(args []string, wanPort int) (lanPort int, proto string, err error) {
|
|
lanPort = wanPort
|
|
proto = "tcp"
|
|
lanPortSet, protoSet := false, false
|
|
for _, a := range args {
|
|
if validProtos[a] {
|
|
if protoSet {
|
|
return 0, "", fmt.Errorf("Protokoll mehrfach angegeben: %q", a)
|
|
}
|
|
proto = a
|
|
protoSet = true
|
|
continue
|
|
}
|
|
if n, convErr := strconv.Atoi(a); convErr == nil {
|
|
if lanPortSet {
|
|
return 0, "", fmt.Errorf("lan-port mehrfach angegeben: %q", a)
|
|
}
|
|
lanPort = n
|
|
lanPortSet = true
|
|
continue
|
|
}
|
|
return 0, "", fmt.Errorf("unbekanntes Argument %q (erwarte lan-port oder tcp/udp/both)", a)
|
|
}
|
|
return lanPort, proto, nil
|
|
}
|