validation.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. package firewall
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net"
  6. "reflect"
  7. "regexp"
  8. "strconv"
  9. "strings"
  10. "time"
  11. )
  12. var safeIdentifier = regexp.MustCompile(`^[A-Za-z0-9_.:-]+$`)
  13. var safeChain = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]{0,15}$`)
  14. var commonFields = strings.Fields(`ip_version id order chain action protocol src_address dst_address src_port dst_port any_port in_interface out_interface in_interface_list out_interface_list packet_mark connection_mark routing_mark connection_type connection_state connection_nat_state src_address_list dst_address_list src_geoip dst_geoip comment disabled packets bytes content src_mac_address in_bridge_port out_bridge_port ipsec_policy dscp tcp_mss tos packet_size random tcp_flags icmp_options ttl time days limit nth connection_limit src_address_type dst_address_type ip_fragment log log_prefix jump_target address_list address_list_timeout`)
  15. // Capabilities defines the options actually implemented by this Linux backend.
  16. func Capabilities() map[string]any {
  17. actions := map[string][]string{
  18. "filter": strings.Fields("accept drop reject return jump log passthrough add-src-to-address-list add-dst-to-address-list"),
  19. "nat": strings.Fields("accept return jump log passthrough masquerade src-nat dst-nat redirect netmap add-src-to-address-list add-dst-to-address-list"),
  20. "mangle": strings.Fields("accept drop return jump log passthrough mark-connection mark-routing mark-packet change-mss change-dscp change-ttl set-priority add-src-to-address-list add-dst-to-address-list"),
  21. }
  22. fields := map[string][]string{}
  23. for _, kind := range []string{"filter", "nat", "mangle"} {
  24. var model any
  25. switch kind {
  26. case "filter":
  27. model = FilterRule{}
  28. case "nat":
  29. model = NATRule{}
  30. case "mangle":
  31. model = MangleRule{}
  32. }
  33. present := map[string]bool{}
  34. typ := reflect.TypeOf(model)
  35. for i := 0; i < typ.NumField(); i++ {
  36. present[strings.Split(typ.Field(i).Tag.Get("json"), ",")[0]] = true
  37. }
  38. for _, field := range commonFields {
  39. if present[field] {
  40. fields[kind] = append(fields[kind], field)
  41. }
  42. }
  43. }
  44. fields["filter"] = append(fields["filter"], "reject_with")
  45. fields["nat"] = append(fields["nat"], "to_addresses", "to_ports")
  46. fields["mangle"] = append(fields["mangle"], strings.Fields("new_connection_mark new_routing_mark new_packet_mark new_mss new_dscp new_ttl new_priority passthrough")...)
  47. return map[string]any{"actions": actions, "fields": fields, "time_zone": "UTC", "geoip_ipv6": false}
  48. }
  49. func values(rule any) map[string]any {
  50. b, _ := json.Marshal(rule)
  51. var v map[string]any
  52. _ = json.Unmarshal(b, &v)
  53. return v
  54. }
  55. func text(v map[string]any, k string) string { s, _ := v[k].(string); return s }
  56. func populated(v any) bool {
  57. switch x := v.(type) {
  58. case nil:
  59. return false
  60. case string:
  61. return x != ""
  62. case bool:
  63. return x
  64. case float64:
  65. return x != 0
  66. case []any:
  67. return len(x) > 0
  68. }
  69. return false
  70. }
  71. func plain(s string) (string, string) {
  72. if strings.HasPrefix(s, "!") {
  73. return "! ", strings.TrimSpace(s[1:])
  74. }
  75. return "", strings.TrimSpace(s)
  76. }
  77. func quote(s string) string { return strconv.Quote(s) }
  78. func intRange(s string, min, max int) error {
  79. n, e := strconv.Atoi(s)
  80. if e != nil || n < min || n > max {
  81. return fmt.Errorf("expected a number from %d to %d", min, max)
  82. }
  83. return nil
  84. }
  85. func portValue(s string) string { return strings.ReplaceAll(s, "-", ":") }
  86. func validatePorts(s string) error {
  87. _, s = plain(s)
  88. for _, p := range strings.Split(portValue(s), ",") {
  89. bounds := strings.Split(p, ":")
  90. if len(bounds) > 2 {
  91. return fmt.Errorf("invalid port range")
  92. }
  93. for _, n := range bounds {
  94. if err := intRange(n, 0, 65535); err != nil {
  95. return err
  96. }
  97. }
  98. if len(bounds) == 2 {
  99. a, _ := strconv.Atoi(bounds[0])
  100. b, _ := strconv.Atoi(bounds[1])
  101. if a > b {
  102. return fmt.Errorf("reversed port range")
  103. }
  104. }
  105. }
  106. if len(strings.Split(s, ",")) > 15 {
  107. return fmt.Errorf("at most 15 ports per rule")
  108. }
  109. return nil
  110. }
  111. func timeoutSeconds(s string) (int, error) {
  112. if s == "" || s == "0" || s == "none-dynamic" || s == "none-static" {
  113. return 0, nil
  114. }
  115. if strings.HasSuffix(s, "d") {
  116. n, e := strconv.ParseFloat(strings.TrimSuffix(s, "d"), 64)
  117. if e != nil || n <= 0 || n > 24855 {
  118. return 0, fmt.Errorf("invalid address list timeout")
  119. }
  120. return int(n * 86400), nil
  121. }
  122. d, e := time.ParseDuration(s)
  123. if e != nil || d < time.Second || d/time.Second > 2147483 {
  124. return 0, fmt.Errorf("timeout must be 0 or a duration such as 30s, 5m, 1h, 1d")
  125. }
  126. return int(d / time.Second), nil
  127. }
  128. func validateRule(kind string, rule any) error {
  129. v := values(rule)
  130. if disabled, _ := v["disabled"].(bool); disabled {
  131. return nil
  132. }
  133. if family, _ := v["ip_version"].(float64); family != 0 && family != 4 && family != 6 {
  134. return fmt.Errorf("IP family must be 4 or 6")
  135. }
  136. cap := Capabilities()
  137. allowed := map[string]bool{}
  138. for _, k := range cap["fields"].(map[string][]string)[kind] {
  139. allowed[k] = true
  140. }
  141. for key, value := range v {
  142. if s, ok := value.(string); ok && strings.ContainsAny(s, "\n\r\x00") {
  143. return fmt.Errorf("%s: control characters are not allowed", key)
  144. }
  145. }
  146. for _, key := range []string{"id", "jump_target"} {
  147. s := text(v, key)
  148. if s != "" && !safeIdentifier.MatchString(s) {
  149. return fmt.Errorf("invalid %s", key)
  150. }
  151. }
  152. chain := text(v, "chain")
  153. if chain == "" || !safeChain.MatchString(chain) {
  154. return fmt.Errorf("a valid chain is required (1–16 characters)")
  155. }
  156. if disabled, _ := v["disabled"].(bool); disabled {
  157. return nil
  158. }
  159. for key, value := range v {
  160. if !allowed[key] && populated(value) {
  161. return fmt.Errorf("%s is not supported by the Linux firewall; clear this legacy setting", key)
  162. }
  163. }
  164. action := text(v, "action")
  165. valid := false
  166. for _, a := range cap["actions"].(map[string][]string)[kind] {
  167. if action == a {
  168. valid = true
  169. }
  170. }
  171. if !valid {
  172. return fmt.Errorf("action %q is not supported in %s", action, kind)
  173. }
  174. for _, key := range []string{"src_address", "dst_address"} {
  175. _, s := plain(text(v, key))
  176. if s != "" {
  177. if net.ParseIP(s) == nil {
  178. if _, _, err := net.ParseCIDR(s); err != nil {
  179. return fmt.Errorf("invalid %s", key)
  180. }
  181. }
  182. }
  183. }
  184. for _, key := range []string{"src_port", "dst_port", "any_port"} {
  185. if s := text(v, key); s != "" {
  186. if err := validatePorts(s); err != nil {
  187. return fmt.Errorf("%s: %w", key, err)
  188. }
  189. }
  190. }
  191. for _, key := range []string{"in_interface", "out_interface", "in_interface_list", "out_interface_list", "in_bridge_port", "out_bridge_port"} {
  192. _, s := plain(text(v, key))
  193. if s != "" && !safeIdentifier.MatchString(s) {
  194. return fmt.Errorf("invalid %s", key)
  195. }
  196. }
  197. for _, pair := range [][2]string{{"in_interface", "in_interface_list"}, {"out_interface", "out_interface_list"}} {
  198. if text(v, pair[0]) != "" && text(v, pair[1]) != "" {
  199. return fmt.Errorf("choose %s or %s, not both", pair[0], pair[1])
  200. }
  201. }
  202. for _, key := range []string{"in_interface_list", "out_interface_list"} {
  203. _, s := plain(text(v, key))
  204. if strings.HasPrefix(text(v, key), "!") {
  205. return fmt.Errorf("negated interface lists are not supported; use an explicit negated interface")
  206. }
  207. if s != "" && len(resolveInterfaceList(s)) == 0 {
  208. return fmt.Errorf("interface list %q has no members", s)
  209. }
  210. }
  211. for _, key := range []string{"src_address_list", "dst_address_list", "address_list"} {
  212. _, s := plain(text(v, key))
  213. if s != "" {
  214. if err := validateListName(s); err != nil {
  215. return err
  216. }
  217. }
  218. }
  219. if text(v, "packet_mark") != "" && text(v, "routing_mark") != "" && text(v, "packet_mark") != text(v, "routing_mark") {
  220. return fmt.Errorf("packet mark and routing mark use the same Linux mark; choose one criterion")
  221. }
  222. if action == "jump" && !safeChain.MatchString(text(v, "jump_target")) {
  223. return fmt.Errorf("jump target must be a valid custom chain")
  224. }
  225. if strings.HasPrefix(action, "add-") && text(v, "address_list") == "" {
  226. return fmt.Errorf("address list is required")
  227. }
  228. if _, err := timeoutSeconds(text(v, "address_list_timeout")); err != nil {
  229. return err
  230. }
  231. if kind == "nat" {
  232. if action == "netmap" && text(v, "to_ports") != "" {
  233. return fmt.Errorf("netmap translates addresses only; use src-nat/dst-nat to translate ports")
  234. }
  235. if (action == "src-nat" || action == "dst-nat" || action == "netmap") && text(v, "to_addresses") == "" {
  236. return fmt.Errorf("to-addresses is required for %s", action)
  237. }
  238. if s := text(v, "to_addresses"); s != "" {
  239. if action == "netmap" {
  240. if _, _, err := net.ParseCIDR(s); err != nil {
  241. return fmt.Errorf("netmap requires a destination CIDR")
  242. }
  243. } else {
  244. for _, ip := range strings.Split(s, "-") {
  245. if net.ParseIP(ip) == nil {
  246. return fmt.Errorf("invalid to-addresses")
  247. }
  248. }
  249. }
  250. }
  251. if s := text(v, "to_ports"); s != "" {
  252. if err := validatePorts(s); err != nil {
  253. return fmt.Errorf("to-ports: %w", err)
  254. }
  255. _, p := plain(text(v, "protocol"))
  256. if p != "tcp" && p != "udp" {
  257. return fmt.Errorf("to-ports requires TCP or UDP")
  258. }
  259. }
  260. }
  261. if kind == "mangle" {
  262. required := map[string]string{"mark-connection": "new_connection_mark", "mark-routing": "new_routing_mark", "mark-packet": "new_packet_mark", "change-mss": "new_mss", "change-ttl": "new_ttl", "set-priority": "new_priority"}
  263. if key := required[action]; key != "" && text(v, key) == "" {
  264. return fmt.Errorf("%s is required", key)
  265. }
  266. if action == "change-dscp" {
  267. n, _ := v["new_dscp"].(float64)
  268. if n < 0 || n > 63 {
  269. return fmt.Errorf("DSCP must be 0–63")
  270. }
  271. }
  272. if action == "change-mss" {
  273. _, p := plain(text(v, "protocol"))
  274. if p != "" && p != "all" && p != "tcp" {
  275. return fmt.Errorf("change-mss requires TCP")
  276. }
  277. s := text(v, "new_mss")
  278. if s != "clamp-to-pmtu" {
  279. if err := intRange(s, 1, 65535); err != nil {
  280. return fmt.Errorf("new-mss: %w", err)
  281. }
  282. }
  283. }
  284. if action == "set-priority" {
  285. if chain != "postrouting" {
  286. return fmt.Errorf("set-priority requires the postrouting chain")
  287. }
  288. if err := intRange(text(v, "new_priority"), 0, 65535); err != nil {
  289. return err
  290. }
  291. }
  292. }
  293. for _, key := range []string{"connection_state", "connection_nat_state"} {
  294. if items, ok := v[key].([]any); ok {
  295. for _, item := range items {
  296. state, _ := item.(string)
  297. allowed := map[string]bool{"new": true, "established": true, "related": true, "invalid": true, "untracked": true}
  298. if key == "connection_nat_state" {
  299. allowed = map[string]bool{"srcnat": true, "dstnat": true, "snat": true, "dnat": true}
  300. }
  301. if !allowed[strings.ToLower(state)] {
  302. return fmt.Errorf("invalid %s", key)
  303. }
  304. }
  305. }
  306. }
  307. if action == "netmap" && v["ip_version"] == float64(6) {
  308. return fmt.Errorf("netmap is IPv4-only")
  309. }
  310. for _, key := range []string{"src_geoip", "dst_geoip"} {
  311. if s := text(v, key); s != "" {
  312. _, s = plain(s)
  313. s = strings.TrimPrefix(s, "geo:")
  314. s = strings.TrimPrefix(s, "geo_")
  315. s = strings.ToUpper(s)
  316. if !regexp.MustCompile(`^[A-Z]{2}$`).MatchString(s) && s != "EU" && s != "BALKANS" && s != "BLOCKLIST" && s != "HIGHRISK" {
  317. return fmt.Errorf("invalid GeoIP country or preset")
  318. }
  319. if v["ip_version"] == float64(6) {
  320. return fmt.Errorf("GeoIP is IPv4-only")
  321. }
  322. }
  323. }
  324. if action == "reject" {
  325. if s := text(v, "reject_with"); s != "" {
  326. allowed := strings.Fields("icmp-net-unreachable icmp-network-unreachable icmp-host-unreachable icmp-proto-unreachable icmp-port-unreachable icmp-net-prohibited icmp-host-prohibited icmp-admin-prohibited tcp-reset")
  327. if v["ip_version"] == float64(6) {
  328. allowed = strings.Fields("icmp6-no-route icmp6-adm-prohibited icmp6-addr-unreachable icmp6-port-unreachable tcp-reset")
  329. }
  330. valid := false
  331. for _, option := range allowed {
  332. valid = valid || s == option
  333. }
  334. if !valid {
  335. return fmt.Errorf("reject type is not supported for this IP family")
  336. }
  337. }
  338. }
  339. return validateAdvanced(v)
  340. }
  341. func validateListName(s string) error {
  342. if strings.TrimSpace(s) != s || len(s) > 128 || s == "" || strings.HasPrefix(s, "geo:") || strings.HasPrefix(s, "geo_") || strings.ContainsAny(s, "\n\r\x00") {
  343. return fmt.Errorf("invalid or reserved address list name %q", s)
  344. }
  345. return nil
  346. }
  347. func ValidateRule(kind string, rule any) error {
  348. if kind != "filter" && kind != "nat" && kind != "mangle" {
  349. return fmt.Errorf("unknown firewall rule kind")
  350. }
  351. return validateRule(kind, rule)
  352. }