| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- package firewall
- import (
- "encoding/json"
- "fmt"
- "net"
- "reflect"
- "regexp"
- "strconv"
- "strings"
- "time"
- )
- var safeIdentifier = regexp.MustCompile(`^[A-Za-z0-9_.:-]+$`)
- var safeChain = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]{0,15}$`)
- 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`)
- // Capabilities defines the options actually implemented by this Linux backend.
- func Capabilities() map[string]any {
- actions := map[string][]string{
- "filter": strings.Fields("accept drop reject return jump log passthrough add-src-to-address-list add-dst-to-address-list"),
- "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"),
- "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"),
- }
- fields := map[string][]string{}
- for _, kind := range []string{"filter", "nat", "mangle"} {
- var model any
- switch kind {
- case "filter":
- model = FilterRule{}
- case "nat":
- model = NATRule{}
- case "mangle":
- model = MangleRule{}
- }
- present := map[string]bool{}
- typ := reflect.TypeOf(model)
- for i := 0; i < typ.NumField(); i++ {
- present[strings.Split(typ.Field(i).Tag.Get("json"), ",")[0]] = true
- }
- for _, field := range commonFields {
- if present[field] {
- fields[kind] = append(fields[kind], field)
- }
- }
- }
- fields["filter"] = append(fields["filter"], "reject_with")
- fields["nat"] = append(fields["nat"], "to_addresses", "to_ports")
- 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")...)
- return map[string]any{"actions": actions, "fields": fields, "time_zone": "UTC", "geoip_ipv6": false}
- }
- func values(rule any) map[string]any {
- b, _ := json.Marshal(rule)
- var v map[string]any
- _ = json.Unmarshal(b, &v)
- return v
- }
- func text(v map[string]any, k string) string { s, _ := v[k].(string); return s }
- func populated(v any) bool {
- switch x := v.(type) {
- case nil:
- return false
- case string:
- return x != ""
- case bool:
- return x
- case float64:
- return x != 0
- case []any:
- return len(x) > 0
- }
- return false
- }
- func plain(s string) (string, string) {
- if strings.HasPrefix(s, "!") {
- return "! ", strings.TrimSpace(s[1:])
- }
- return "", strings.TrimSpace(s)
- }
- func quote(s string) string { return strconv.Quote(s) }
- func intRange(s string, min, max int) error {
- n, e := strconv.Atoi(s)
- if e != nil || n < min || n > max {
- return fmt.Errorf("expected a number from %d to %d", min, max)
- }
- return nil
- }
- func portValue(s string) string { return strings.ReplaceAll(s, "-", ":") }
- func validatePorts(s string) error {
- _, s = plain(s)
- for _, p := range strings.Split(portValue(s), ",") {
- bounds := strings.Split(p, ":")
- if len(bounds) > 2 {
- return fmt.Errorf("invalid port range")
- }
- for _, n := range bounds {
- if err := intRange(n, 0, 65535); err != nil {
- return err
- }
- }
- if len(bounds) == 2 {
- a, _ := strconv.Atoi(bounds[0])
- b, _ := strconv.Atoi(bounds[1])
- if a > b {
- return fmt.Errorf("reversed port range")
- }
- }
- }
- if len(strings.Split(s, ",")) > 15 {
- return fmt.Errorf("at most 15 ports per rule")
- }
- return nil
- }
- func timeoutSeconds(s string) (int, error) {
- if s == "" || s == "0" || s == "none-dynamic" || s == "none-static" {
- return 0, nil
- }
- if strings.HasSuffix(s, "d") {
- n, e := strconv.ParseFloat(strings.TrimSuffix(s, "d"), 64)
- if e != nil || n <= 0 || n > 24855 {
- return 0, fmt.Errorf("invalid address list timeout")
- }
- return int(n * 86400), nil
- }
- d, e := time.ParseDuration(s)
- if e != nil || d < time.Second || d/time.Second > 2147483 {
- return 0, fmt.Errorf("timeout must be 0 or a duration such as 30s, 5m, 1h, 1d")
- }
- return int(d / time.Second), nil
- }
- func validateRule(kind string, rule any) error {
- v := values(rule)
- if disabled, _ := v["disabled"].(bool); disabled {
- return nil
- }
- if family, _ := v["ip_version"].(float64); family != 0 && family != 4 && family != 6 {
- return fmt.Errorf("IP family must be 4 or 6")
- }
- cap := Capabilities()
- allowed := map[string]bool{}
- for _, k := range cap["fields"].(map[string][]string)[kind] {
- allowed[k] = true
- }
- for key, value := range v {
- if s, ok := value.(string); ok && strings.ContainsAny(s, "\n\r\x00") {
- return fmt.Errorf("%s: control characters are not allowed", key)
- }
- }
- for _, key := range []string{"id", "jump_target"} {
- s := text(v, key)
- if s != "" && !safeIdentifier.MatchString(s) {
- return fmt.Errorf("invalid %s", key)
- }
- }
- chain := text(v, "chain")
- if chain == "" || !safeChain.MatchString(chain) {
- return fmt.Errorf("a valid chain is required (1–16 characters)")
- }
- if disabled, _ := v["disabled"].(bool); disabled {
- return nil
- }
- for key, value := range v {
- if !allowed[key] && populated(value) {
- return fmt.Errorf("%s is not supported by the Linux firewall; clear this legacy setting", key)
- }
- }
- action := text(v, "action")
- valid := false
- for _, a := range cap["actions"].(map[string][]string)[kind] {
- if action == a {
- valid = true
- }
- }
- if !valid {
- return fmt.Errorf("action %q is not supported in %s", action, kind)
- }
- for _, key := range []string{"src_address", "dst_address"} {
- _, s := plain(text(v, key))
- if s != "" {
- if net.ParseIP(s) == nil {
- if _, _, err := net.ParseCIDR(s); err != nil {
- return fmt.Errorf("invalid %s", key)
- }
- }
- }
- }
- for _, key := range []string{"src_port", "dst_port", "any_port"} {
- if s := text(v, key); s != "" {
- if err := validatePorts(s); err != nil {
- return fmt.Errorf("%s: %w", key, err)
- }
- }
- }
- for _, key := range []string{"in_interface", "out_interface", "in_interface_list", "out_interface_list", "in_bridge_port", "out_bridge_port"} {
- _, s := plain(text(v, key))
- if s != "" && !safeIdentifier.MatchString(s) {
- return fmt.Errorf("invalid %s", key)
- }
- }
- for _, pair := range [][2]string{{"in_interface", "in_interface_list"}, {"out_interface", "out_interface_list"}} {
- if text(v, pair[0]) != "" && text(v, pair[1]) != "" {
- return fmt.Errorf("choose %s or %s, not both", pair[0], pair[1])
- }
- }
- for _, key := range []string{"in_interface_list", "out_interface_list"} {
- _, s := plain(text(v, key))
- if strings.HasPrefix(text(v, key), "!") {
- return fmt.Errorf("negated interface lists are not supported; use an explicit negated interface")
- }
- if s != "" && len(resolveInterfaceList(s)) == 0 {
- return fmt.Errorf("interface list %q has no members", s)
- }
- }
- for _, key := range []string{"src_address_list", "dst_address_list", "address_list"} {
- _, s := plain(text(v, key))
- if s != "" {
- if err := validateListName(s); err != nil {
- return err
- }
- }
- }
- if text(v, "packet_mark") != "" && text(v, "routing_mark") != "" && text(v, "packet_mark") != text(v, "routing_mark") {
- return fmt.Errorf("packet mark and routing mark use the same Linux mark; choose one criterion")
- }
- if action == "jump" && !safeChain.MatchString(text(v, "jump_target")) {
- return fmt.Errorf("jump target must be a valid custom chain")
- }
- if strings.HasPrefix(action, "add-") && text(v, "address_list") == "" {
- return fmt.Errorf("address list is required")
- }
- if _, err := timeoutSeconds(text(v, "address_list_timeout")); err != nil {
- return err
- }
- if kind == "nat" {
- if text(v, "src_port") != "" || text(v, "dst_port") != "" || text(v, "any_port") != "" || text(v, "to_ports") != "" {
- _, protocol := plain(text(v, "protocol"))
- if protocol != "tcp" && protocol != "udp" {
- return fmt.Errorf("NAT ports require an explicit TCP or UDP protocol; create separate rules for both")
- }
- }
- if action == "netmap" && text(v, "to_ports") != "" {
- return fmt.Errorf("netmap translates addresses only; use src-nat/dst-nat to translate ports")
- }
- if (action == "src-nat" || action == "dst-nat" || action == "netmap") && text(v, "to_addresses") == "" {
- return fmt.Errorf("to-addresses is required for %s", action)
- }
- if s := text(v, "to_addresses"); s != "" {
- if action == "netmap" {
- if _, _, err := net.ParseCIDR(s); err != nil {
- return fmt.Errorf("netmap requires a destination CIDR")
- }
- } else {
- for _, ip := range strings.Split(s, "-") {
- if net.ParseIP(ip) == nil {
- return fmt.Errorf("invalid to-addresses")
- }
- }
- }
- }
- if s := text(v, "to_ports"); s != "" {
- if err := validatePorts(s); err != nil {
- return fmt.Errorf("to-ports: %w", err)
- }
- _, p := plain(text(v, "protocol"))
- if p != "tcp" && p != "udp" {
- return fmt.Errorf("to-ports requires TCP or UDP")
- }
- }
- }
- if kind == "mangle" {
- 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"}
- if key := required[action]; key != "" && text(v, key) == "" {
- return fmt.Errorf("%s is required", key)
- }
- if action == "change-dscp" {
- n, _ := v["new_dscp"].(float64)
- if n < 0 || n > 63 {
- return fmt.Errorf("DSCP must be 0–63")
- }
- }
- if action == "change-mss" {
- _, p := plain(text(v, "protocol"))
- if p != "" && p != "all" && p != "tcp" {
- return fmt.Errorf("change-mss requires TCP")
- }
- s := text(v, "new_mss")
- if s != "clamp-to-pmtu" {
- if err := intRange(s, 1, 65535); err != nil {
- return fmt.Errorf("new-mss: %w", err)
- }
- }
- }
- if action == "set-priority" {
- if chain != "postrouting" {
- return fmt.Errorf("set-priority requires the postrouting chain")
- }
- if err := intRange(text(v, "new_priority"), 0, 65535); err != nil {
- return err
- }
- }
- }
- for _, key := range []string{"connection_state", "connection_nat_state"} {
- if items, ok := v[key].([]any); ok {
- for _, item := range items {
- state, _ := item.(string)
- allowed := map[string]bool{"new": true, "established": true, "related": true, "invalid": true, "untracked": true}
- if key == "connection_nat_state" {
- allowed = map[string]bool{"srcnat": true, "dstnat": true, "snat": true, "dnat": true}
- }
- if !allowed[strings.ToLower(state)] {
- return fmt.Errorf("invalid %s", key)
- }
- }
- }
- }
- if action == "netmap" && v["ip_version"] == float64(6) {
- return fmt.Errorf("netmap is IPv4-only")
- }
- for _, key := range []string{"src_geoip", "dst_geoip"} {
- if s := text(v, key); s != "" {
- _, s = plain(s)
- s = strings.TrimPrefix(s, "geo:")
- s = strings.TrimPrefix(s, "geo_")
- s = strings.ToUpper(s)
- if !regexp.MustCompile(`^[A-Z]{2}$`).MatchString(s) && s != "EU" && s != "BALKANS" && s != "BLOCKLIST" && s != "HIGHRISK" {
- return fmt.Errorf("invalid GeoIP country or preset")
- }
- if v["ip_version"] == float64(6) {
- return fmt.Errorf("GeoIP is IPv4-only")
- }
- }
- }
- if action == "reject" {
- if s := text(v, "reject_with"); s != "" {
- 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")
- if v["ip_version"] == float64(6) {
- allowed = strings.Fields("icmp6-no-route icmp6-adm-prohibited icmp6-addr-unreachable icmp6-port-unreachable tcp-reset")
- }
- valid := false
- for _, option := range allowed {
- valid = valid || s == option
- }
- if !valid {
- return fmt.Errorf("reject type is not supported for this IP family")
- }
- }
- }
- return validateAdvanced(v)
- }
- func validateListName(s string) error {
- if strings.TrimSpace(s) != s || len(s) > 128 || s == "" || strings.HasPrefix(s, "geo:") || strings.HasPrefix(s, "geo_") || strings.ContainsAny(s, "\n\r\x00") {
- return fmt.Errorf("invalid or reserved address list name %q", s)
- }
- return nil
- }
- func ValidateRule(kind string, rule any) error {
- if kind != "filter" && kind != "nat" && kind != "mangle" {
- return fmt.Errorf("unknown firewall rule kind")
- }
- return validateRule(kind, rule)
- }
|