validation.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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 text(v, "src_port") != "" || text(v, "dst_port") != "" || text(v, "any_port") != "" || text(v, "to_ports") != "" {
  233. _, protocol := plain(text(v, "protocol"))
  234. if protocol != "tcp" && protocol != "udp" {
  235. return fmt.Errorf("NAT ports require an explicit TCP or UDP protocol; create separate rules for both")
  236. }
  237. }
  238. if action == "netmap" && text(v, "to_ports") != "" {
  239. return fmt.Errorf("netmap translates addresses only; use src-nat/dst-nat to translate ports")
  240. }
  241. if (action == "src-nat" || action == "dst-nat" || action == "netmap") && text(v, "to_addresses") == "" {
  242. return fmt.Errorf("to-addresses is required for %s", action)
  243. }
  244. if s := text(v, "to_addresses"); s != "" {
  245. if action == "netmap" {
  246. if _, _, err := net.ParseCIDR(s); err != nil {
  247. return fmt.Errorf("netmap requires a destination CIDR")
  248. }
  249. } else {
  250. for _, ip := range strings.Split(s, "-") {
  251. if net.ParseIP(ip) == nil {
  252. return fmt.Errorf("invalid to-addresses")
  253. }
  254. }
  255. }
  256. }
  257. if s := text(v, "to_ports"); s != "" {
  258. if err := validatePorts(s); err != nil {
  259. return fmt.Errorf("to-ports: %w", err)
  260. }
  261. _, p := plain(text(v, "protocol"))
  262. if p != "tcp" && p != "udp" {
  263. return fmt.Errorf("to-ports requires TCP or UDP")
  264. }
  265. }
  266. }
  267. if kind == "mangle" {
  268. 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"}
  269. if key := required[action]; key != "" && text(v, key) == "" {
  270. return fmt.Errorf("%s is required", key)
  271. }
  272. if action == "change-dscp" {
  273. n, _ := v["new_dscp"].(float64)
  274. if n < 0 || n > 63 {
  275. return fmt.Errorf("DSCP must be 0–63")
  276. }
  277. }
  278. if action == "change-mss" {
  279. _, p := plain(text(v, "protocol"))
  280. if p != "" && p != "all" && p != "tcp" {
  281. return fmt.Errorf("change-mss requires TCP")
  282. }
  283. s := text(v, "new_mss")
  284. if s != "clamp-to-pmtu" {
  285. if err := intRange(s, 1, 65535); err != nil {
  286. return fmt.Errorf("new-mss: %w", err)
  287. }
  288. }
  289. }
  290. if action == "set-priority" {
  291. if !strings.EqualFold(chain, "postrouting") {
  292. return fmt.Errorf("set-priority requires the postrouting chain")
  293. }
  294. if err := intRange(text(v, "new_priority"), 0, 65535); err != nil {
  295. return err
  296. }
  297. }
  298. }
  299. for _, key := range []string{"connection_state", "connection_nat_state"} {
  300. if items, ok := v[key].([]any); ok {
  301. for _, item := range items {
  302. state, _ := item.(string)
  303. allowed := map[string]bool{"new": true, "established": true, "related": true, "invalid": true, "untracked": true}
  304. if key == "connection_nat_state" {
  305. allowed = map[string]bool{"srcnat": true, "dstnat": true, "snat": true, "dnat": true}
  306. }
  307. if !allowed[strings.ToLower(state)] {
  308. return fmt.Errorf("invalid %s", key)
  309. }
  310. }
  311. }
  312. }
  313. if action == "netmap" && v["ip_version"] == float64(6) {
  314. return fmt.Errorf("netmap is IPv4-only")
  315. }
  316. for _, key := range []string{"src_geoip", "dst_geoip"} {
  317. if s := text(v, key); s != "" {
  318. _, s = plain(s)
  319. s = strings.TrimPrefix(s, "geo:")
  320. s = strings.TrimPrefix(s, "geo_")
  321. s = strings.ToUpper(s)
  322. if !regexp.MustCompile(`^[A-Z]{2}$`).MatchString(s) && s != "EU" && s != "BALKANS" && s != "BLOCKLIST" && s != "HIGHRISK" {
  323. return fmt.Errorf("invalid GeoIP country or preset")
  324. }
  325. if v["ip_version"] == float64(6) {
  326. return fmt.Errorf("GeoIP is IPv4-only")
  327. }
  328. }
  329. }
  330. if action == "reject" {
  331. if s := text(v, "reject_with"); s != "" {
  332. 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")
  333. if v["ip_version"] == float64(6) {
  334. allowed = strings.Fields("icmp6-no-route icmp6-adm-prohibited icmp6-addr-unreachable icmp6-port-unreachable tcp-reset")
  335. }
  336. valid := false
  337. for _, option := range allowed {
  338. valid = valid || s == option
  339. }
  340. if !valid {
  341. return fmt.Errorf("reject type is not supported for this IP family")
  342. }
  343. }
  344. }
  345. return validateAdvanced(v)
  346. }
  347. func validateListName(s string) error {
  348. if strings.TrimSpace(s) != s || len(s) > 128 || s == "" || strings.HasPrefix(s, "geo:") || strings.HasPrefix(s, "geo_") || strings.ContainsAny(s, "\n\r\x00") {
  349. return fmt.Errorf("invalid or reserved address list name %q", s)
  350. }
  351. return nil
  352. }
  353. func ValidateRule(kind string, rule any) error {
  354. if kind != "filter" && kind != "nat" && kind != "mangle" {
  355. return fmt.Errorf("unknown firewall rule kind")
  356. }
  357. rule = normalizeNativeRule(kind, rule)
  358. if err := validateRule(kind, rule); err != nil {
  359. return err
  360. }
  361. if err := validateNewPortProtocol(rule); err != nil {
  362. return err
  363. }
  364. return validateChainPlacement(kind, rule)
  365. }
  366. func validateNewPortProtocol(rule any) error {
  367. v := values(rule)
  368. if text(v, "src_port") == "" && text(v, "dst_port") == "" && text(v, "any_port") == "" && text(v, "to_ports") == "" {
  369. return nil
  370. }
  371. inv, protocol := plain(strings.ToLower(text(v, "protocol")))
  372. if inv != "" || (protocol != "tcp" && protocol != "udp") {
  373. return fmt.Errorf("port matches require explicit TCP or UDP without protocol inversion; use separate rules for TCP and UDP")
  374. }
  375. return nil
  376. }
  377. // Accept native target names at the API boundary while preserving the storage
  378. // format used by existing installations and older clients.
  379. func normalizeNativeRule(kind string, rule any) any {
  380. if r, ok := rule.(FilterRule); ok && kind == "filter" {
  381. r.Action = strings.ToLower(r.Action)
  382. return r
  383. }
  384. if r, ok := rule.(MangleRule); ok && kind == "mangle" {
  385. r.Action = strings.ToLower(r.Action)
  386. switch r.Action {
  387. case "mark":
  388. r.Action = "mark-packet"
  389. case "connmark":
  390. r.Action = "mark-connection"
  391. case "tcpmss":
  392. r.Action = "change-mss"
  393. case "dscp":
  394. r.Action = "change-dscp"
  395. case "ttl", "hl":
  396. r.Action = "change-ttl"
  397. case "classify":
  398. r.Action = "set-priority"
  399. }
  400. return r
  401. }
  402. if kind == "nat" {
  403. if r, ok := rule.(NATRule); ok {
  404. switch strings.ToLower(r.Action) {
  405. case "dnat":
  406. r.Action = "dst-nat"
  407. case "snat":
  408. r.Action = "src-nat"
  409. default:
  410. r.Action = strings.ToLower(r.Action)
  411. }
  412. return r
  413. }
  414. }
  415. return rule
  416. }
  417. // Validate new and edited rules without changing the interpretation of saved rules.
  418. func validateChainPlacement(kind string, rule any) error {
  419. v := values(rule)
  420. chain := strings.ToLower(strings.TrimSpace(text(v, "chain")))
  421. if text(v, "action") == "jump" {
  422. target := strings.ToLower(strings.TrimSpace(text(v, "jump_target")))
  423. if target == chain || target == "input" || target == "output" || target == "forward" || target == "prerouting" || target == "postrouting" || target == "srcnat" || target == "dstnat" {
  424. return fmt.Errorf("jump requires a different user-defined chain")
  425. }
  426. }
  427. if kind != "nat" && (chain == "srcnat" || chain == "dstnat") {
  428. return fmt.Errorf("chain %s is available in the nat table only", chain)
  429. }
  430. if kind == "nat" {
  431. switch chain {
  432. case "dstnat":
  433. chain = "prerouting"
  434. case "srcnat":
  435. chain = "postrouting"
  436. }
  437. }
  438. reserved := map[string]bool{"input": true, "output": true, "forward": true, "prerouting": true, "postrouting": true}
  439. if !reserved[chain] {
  440. return nil
  441. } // User chains are reached through an explicit jump.
  442. if kind == "filter" && (chain == "prerouting" || chain == "postrouting") || kind == "nat" && chain == "forward" {
  443. return fmt.Errorf("chain %s is not available in the %s table", chain, kind)
  444. }
  445. if (chain == "output" || chain == "postrouting") && (text(v, "in_interface") != "" || text(v, "in_interface_list") != "" || text(v, "in_bridge_port") != "" || text(v, "src_mac_address") != "") {
  446. return fmt.Errorf("chain %s cannot match an incoming interface or source MAC", chain)
  447. }
  448. if (chain == "input" || chain == "prerouting") && (text(v, "out_interface") != "" || text(v, "out_interface_list") != "" || text(v, "out_bridge_port") != "") {
  449. return fmt.Errorf("chain %s cannot match an outgoing interface", chain)
  450. }
  451. if kind == "nat" {
  452. switch text(v, "action") {
  453. case "dst-nat", "redirect":
  454. if chain != "prerouting" && chain != "output" {
  455. return fmt.Errorf("%s requires prerouting/dstnat or output", text(v, "action"))
  456. }
  457. case "masquerade":
  458. if chain != "postrouting" {
  459. return fmt.Errorf("masquerade requires postrouting/srcnat")
  460. }
  461. case "src-nat":
  462. if chain != "postrouting" && chain != "input" {
  463. return fmt.Errorf("src-nat requires postrouting/srcnat or input")
  464. }
  465. }
  466. }
  467. return nil
  468. }