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("invalid line (expected 4 tab-separated fields): %q", line) } wanPort, err := strconv.Atoi(parts[0]) if err != nil { return PortForward{}, fmt.Errorf("invalid wan-port: %q", parts[0]) } lanPort, err := strconv.Atoi(parts[3]) if err != nil { return PortForward{}, fmt.Errorf("invalid lan-port: %q", parts[3]) } proto := parts[1] if !validProtos[proto] { return PortForward{}, fmt.Errorf("invalid protocol: %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("invalid %s: %d (allowed: 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("protocol given more than once: %q", a) } proto = a protoSet = true continue } if n, convErr := strconv.Atoi(a); convErr == nil { if lanPortSet { return 0, "", fmt.Errorf("lan-port given more than once: %q", a) } lanPort = n lanPortSet = true continue } return 0, "", fmt.Errorf("unrecognized argument %q (expected lan-port or tcp/udp/both)", a) } return lanPort, proto, nil }