package amneziawg import ( "fmt" "sort" "strconv" "strings" ) // portSpec is a single port (start == end) or an inclusive range start..end. type portSpec struct { start int end int } // parseForwardedPorts splits a user-supplied string ("80, 443; 8000-8100") // into validated port specs. Tokens are separated by comma or semicolon; // whitespace is ignored. Invalid tokens are silently dropped — the input is // a free-form text field and validation is best-effort by design. func parseForwardedPorts(input string) []portSpec { if input == "" { return nil } input = strings.ReplaceAll(input, ";", ",") tokens := strings.Split(input, ",") var specs []portSpec seen := make(map[string]struct{}, len(tokens)) for _, tok := range tokens { tok = strings.TrimSpace(tok) if tok == "" { continue } spec, ok := parsePortToken(tok) if !ok { continue } key := fmt.Sprintf("%d-%d", spec.start, spec.end) if _, dup := seen[key]; dup { continue } seen[key] = struct{}{} specs = append(specs, spec) } return specs } func parsePortToken(tok string) (portSpec, bool) { if idx := strings.IndexByte(tok, '-'); idx >= 0 { start, ok1 := parsePortNumber(strings.TrimSpace(tok[:idx])) end, ok2 := parsePortNumber(strings.TrimSpace(tok[idx+1:])) if !ok1 || !ok2 || start > end { return portSpec{}, false } return portSpec{start: start, end: end}, true } p, ok := parsePortNumber(tok) if !ok { return portSpec{}, false } return portSpec{start: p, end: p}, true } func parsePortNumber(s string) (int, bool) { n, err := strconv.Atoi(s) if err != nil || n < 1 || n > 65535 { return 0, false } return n, true } // ForwardedPortsInclude reports whether port is covered by any spec in a raw // ForwardedPorts string (a single port or an inclusive range). Used for // save-time validation that a client isn't about to hijack the panel's own // port or another inbound's port -- see // internal/web/service/inbound_amneziawg.go's port-conflict checks. // // Per-client port-forwarding is implemented by internal/amneziawgnet's // listener supervisor (PortForwardSet), which dials directly into the // embedded gVisor netstack toward the peer's tunnel-internal address -- // the retired kernel-module architecture used PostUp/PostDown iptables DNAT // rules instead, which had no equivalent path once that architecture was // cut over; ExpandForwardedPorts below is what the supervisor uses to turn // a raw spec into the concrete ports it listens on. func ForwardedPortsInclude(forwardedPorts string, port int) bool { for _, spec := range parseForwardedPorts(forwardedPorts) { if port >= spec.start && port <= spec.end { return true } } return false } // MaxForwardedPorts caps how many unique ports a single client's // ForwardedPorts spec can expand to. internal/amneziawgnet's listener // supervisor opens up to two real sockets (TCP+UDP) per port, so this bounds // worst-case file descriptor usage to a fixed, sane amount regardless of how // large a stored spec claims to be -- a legacy or hand-edited "1-65535" // costs exactly the same as "1-100" once expansion stops at the cap. const MaxForwardedPorts = 100 // ExpandForwardedPorts parses forwardedPorts the same way // ForwardedPortsInclude does and returns every unique port it covers, in // ascending order, capped at MaxForwardedPorts. Expansion stops the instant // the cap is reached rather than expanding fully and truncating afterward, // so this is safe to call unconditionally against arbitrary -- including // pre-existing, pre-cap -- stored data. func ExpandForwardedPorts(forwardedPorts string) []int { return expandForwardedPorts(forwardedPorts, MaxForwardedPorts) } // ExceedsForwardedPortsCap reports whether forwardedPorts covers strictly // more than MaxForwardedPorts unique ports -- unlike comparing // len(ExpandForwardedPorts(...)) to the cap, which can never tell "exactly // at the cap" apart from "over it" since that expansion already truncates // there. func ExceedsForwardedPortsCap(forwardedPorts string) bool { return len(expandForwardedPorts(forwardedPorts, MaxForwardedPorts+1)) > MaxForwardedPorts } // expandForwardedPorts is ExpandForwardedPorts with an explicit stop-count, // so ExceedsForwardedPortsCap can probe one past the real cap without // expanding an arbitrarily large legacy spec in full. func expandForwardedPorts(forwardedPorts string, limit int) []int { seen := make(map[int]struct{}, limit) ports := make([]int, 0, limit) outer: for _, spec := range parseForwardedPorts(forwardedPorts) { for p := spec.start; p <= spec.end; p++ { if len(ports) >= limit { break outer } if _, dup := seen[p]; dup { continue } seen[p] = struct{}{} ports = append(ports, p) } } sort.Ints(ports) return ports }