portfwd.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. package amneziawg
  2. import (
  3. "fmt"
  4. "sort"
  5. "strconv"
  6. "strings"
  7. )
  8. // portSpec is a single port (start == end) or an inclusive range start..end.
  9. type portSpec struct {
  10. start int
  11. end int
  12. }
  13. // parseForwardedPorts splits a user-supplied string ("80, 443; 8000-8100")
  14. // into validated port specs. Tokens are separated by comma or semicolon;
  15. // whitespace is ignored. Invalid tokens are silently dropped — the input is
  16. // a free-form text field and validation is best-effort by design.
  17. func parseForwardedPorts(input string) []portSpec {
  18. if input == "" {
  19. return nil
  20. }
  21. input = strings.ReplaceAll(input, ";", ",")
  22. tokens := strings.Split(input, ",")
  23. var specs []portSpec
  24. seen := make(map[string]struct{}, len(tokens))
  25. for _, tok := range tokens {
  26. tok = strings.TrimSpace(tok)
  27. if tok == "" {
  28. continue
  29. }
  30. spec, ok := parsePortToken(tok)
  31. if !ok {
  32. continue
  33. }
  34. key := fmt.Sprintf("%d-%d", spec.start, spec.end)
  35. if _, dup := seen[key]; dup {
  36. continue
  37. }
  38. seen[key] = struct{}{}
  39. specs = append(specs, spec)
  40. }
  41. return specs
  42. }
  43. func parsePortToken(tok string) (portSpec, bool) {
  44. if idx := strings.IndexByte(tok, '-'); idx >= 0 {
  45. start, ok1 := parsePortNumber(strings.TrimSpace(tok[:idx]))
  46. end, ok2 := parsePortNumber(strings.TrimSpace(tok[idx+1:]))
  47. if !ok1 || !ok2 || start > end {
  48. return portSpec{}, false
  49. }
  50. return portSpec{start: start, end: end}, true
  51. }
  52. p, ok := parsePortNumber(tok)
  53. if !ok {
  54. return portSpec{}, false
  55. }
  56. return portSpec{start: p, end: p}, true
  57. }
  58. func parsePortNumber(s string) (int, bool) {
  59. n, err := strconv.Atoi(s)
  60. if err != nil || n < 1 || n > 65535 {
  61. return 0, false
  62. }
  63. return n, true
  64. }
  65. // ForwardedPortsInclude reports whether port is covered by any spec in a raw
  66. // ForwardedPorts string (a single port or an inclusive range). Used for
  67. // save-time validation that a client isn't about to hijack the panel's own
  68. // port or another inbound's port -- see
  69. // internal/web/service/inbound_amneziawg.go's port-conflict checks.
  70. //
  71. // Per-client port-forwarding is implemented by internal/amneziawgnet's
  72. // listener supervisor (PortForwardSet), which dials directly into the
  73. // embedded gVisor netstack toward the peer's tunnel-internal address --
  74. // the retired kernel-module architecture used PostUp/PostDown iptables DNAT
  75. // rules instead, which had no equivalent path once that architecture was
  76. // cut over; ExpandForwardedPorts below is what the supervisor uses to turn
  77. // a raw spec into the concrete ports it listens on.
  78. func ForwardedPortsInclude(forwardedPorts string, port int) bool {
  79. for _, spec := range parseForwardedPorts(forwardedPorts) {
  80. if port >= spec.start && port <= spec.end {
  81. return true
  82. }
  83. }
  84. return false
  85. }
  86. // MaxForwardedPorts caps how many unique ports a single client's
  87. // ForwardedPorts spec can expand to. internal/amneziawgnet's listener
  88. // supervisor opens up to two real sockets (TCP+UDP) per port, so this bounds
  89. // worst-case file descriptor usage to a fixed, sane amount regardless of how
  90. // large a stored spec claims to be -- a legacy or hand-edited "1-65535"
  91. // costs exactly the same as "1-100" once expansion stops at the cap.
  92. const MaxForwardedPorts = 100
  93. // ExpandForwardedPorts parses forwardedPorts the same way
  94. // ForwardedPortsInclude does and returns every unique port it covers, in
  95. // ascending order, capped at MaxForwardedPorts. Expansion stops the instant
  96. // the cap is reached rather than expanding fully and truncating afterward,
  97. // so this is safe to call unconditionally against arbitrary -- including
  98. // pre-existing, pre-cap -- stored data.
  99. func ExpandForwardedPorts(forwardedPorts string) []int {
  100. return expandForwardedPorts(forwardedPorts, MaxForwardedPorts)
  101. }
  102. // ExceedsForwardedPortsCap reports whether forwardedPorts covers strictly
  103. // more than MaxForwardedPorts unique ports -- unlike comparing
  104. // len(ExpandForwardedPorts(...)) to the cap, which can never tell "exactly
  105. // at the cap" apart from "over it" since that expansion already truncates
  106. // there.
  107. func ExceedsForwardedPortsCap(forwardedPorts string) bool {
  108. return len(expandForwardedPorts(forwardedPorts, MaxForwardedPorts+1)) > MaxForwardedPorts
  109. }
  110. // expandForwardedPorts is ExpandForwardedPorts with an explicit stop-count,
  111. // so ExceedsForwardedPortsCap can probe one past the real cap without
  112. // expanding an arbitrarily large legacy spec in full.
  113. func expandForwardedPorts(forwardedPorts string, limit int) []int {
  114. seen := make(map[int]struct{}, limit)
  115. ports := make([]int, 0, limit)
  116. outer:
  117. for _, spec := range parseForwardedPorts(forwardedPorts) {
  118. for p := spec.start; p <= spec.end; p++ {
  119. if len(ports) >= limit {
  120. break outer
  121. }
  122. if _, dup := seen[p]; dup {
  123. continue
  124. }
  125. seen[p] = struct{}{}
  126. ports = append(ports, p)
  127. }
  128. }
  129. sort.Ints(ports)
  130. return ports
  131. }