package firewall import ( "fmt" "net" "regexp" "strconv" "strings" "time" ) func inferProtocol(v map[string]any) string { inv, p := plain(strings.ToLower(text(v, "protocol"))) family, _ := v["ip_version"].(float64) if p == "6" { p = "tcp" } if p == "17" { p = "udp" } if p == "1" { p = "icmp" } if p == "58" || p == "icmpv6" { p = "ipv6-icmp" } if family == 6 && p == "icmp" { p = "ipv6-icmp" } if p == "" || p == "all" { if text(v, "icmp_options") != "" { p = "icmp" if family == 6 { p = "ipv6-icmp" } } else if text(v, "src_port") != "" || text(v, "dst_port") != "" || text(v, "any_port") != "" || text(v, "tcp_flags") != "" || text(v, "tcp_mss") != "" || text(v, "action") == "change-mss" { p = "tcp" } } if inv != "" { return "!" + p } return p } func parseRange(s string, max int) (string, error) { s = portValue(s) parts := strings.Split(s, ":") if len(parts) > 2 { return "", fmt.Errorf("invalid range") } for _, p := range parts { if e := intRange(p, 0, max); e != nil { return "", e } } if len(parts) == 2 { a, _ := strconv.Atoi(parts[0]) b, _ := strconv.Atoi(parts[1]) if a > b { return "", fmt.Errorf("reversed range") } } return s, nil } func tcpFlags(s string) (string, string, error) { var mask, on []string for _, part := range strings.Split(strings.ToUpper(s), ",") { inv, p := plain(strings.TrimSpace(part)) switch p { case "SYN", "ACK", "FIN", "RST", "URG", "PSH": default: return "", "", fmt.Errorf("unsupported TCP flag %q", p) } mask = append(mask, p) if inv == "" { on = append(on, p) } } set := "NONE" if len(on) > 0 { set = strings.Join(on, ",") } return strings.Join(mask, ","), set, nil } func ttlParts(s string) (string, string, error) { op := "eq" n := s if i := strings.IndexByte(s, ':'); i >= 0 { n = s[i+1:] switch s[:i] { case "equal": op = "eq" case "less-than": op = "lt" case "greater-than": op = "gt" default: return "", "", fmt.Errorf("TTL expects equal:N, less-than:N, or greater-than:N") } } if err := intRange(n, 0, 255); err != nil { return "", "", err } return op, n, nil } var ratePattern = regexp.MustCompile(`^([0-9]+(?:\.[0-9]+)?)(?:/([0-9]*)(s|sec|second|m|min|minute|h|hour))?$`) func rateParts(s string) (string, string, error) { s = strings.TrimSuffix(s, ":packet") parts := strings.Split(s, ",") if len(parts) > 2 { return "", "", fmt.Errorf("limit expects rate[,burst], for example 100/second,5") } m := ratePattern.FindStringSubmatch(parts[0]) if m == nil { return "", "", fmt.Errorf("invalid packet rate") } rate, _ := strconv.ParseFloat(m[1], 64) period := 1.0 if m[2] != "" { period, _ = strconv.ParseFloat(m[2], 64) } switch m[3] { case "m", "min", "minute": period *= 60 case "h", "hour": period *= 3600 } if period <= 0 || rate <= 0 { return "", "", fmt.Errorf("rate must be positive") } burst := "5" if len(parts) == 2 { burst = parts[1] if e := intRange(burst, 1, 2147483647); e != nil { return "", "", e } } return strconv.FormatFloat(rate/period, 'f', 6, 64) + "/second", burst, nil } var weekdays = map[string]string{"sun": "Sun", "mon": "Mon", "tue": "Tue", "wed": "Wed", "thu": "Thu", "fri": "Fri", "sat": "Sat"} func clock(s string) (string, error) { layout := "15:04:05" if len(s) == 5 { layout = "15:04" } t, e := time.Parse(layout, s) if e != nil { return "", fmt.Errorf("time must use HH:MM:SS") } return t.Format("15:04:05"), nil } func validateAdvanced(v map[string]any) error { p := inferProtocol(v) _, proto := plain(p) if proto == "ipv6-icmp" && v["ip_version"] != float64(6) { return fmt.Errorf("ICMPv6 requires IPv6") } if p != "" && p != "all" && !safeIdentifier.MatchString(proto) { return fmt.Errorf("invalid protocol") } if strings.HasPrefix(p, "!") && proto == "all" { return fmt.Errorf("cannot invert the all-protocols selection") } ports := text(v, "src_port") != "" || text(v, "dst_port") != "" || text(v, "any_port") != "" if ports && (strings.HasPrefix(p, "!") || (proto != "tcp" && proto != "udp")) { return fmt.Errorf("port matching requires positive TCP or UDP protocol") } if text(v, "tcp_flags") != "" || text(v, "tcp_mss") != "" { if p != "tcp" { return fmt.Errorf("TCP flags/MSS matching requires TCP") } } if s := text(v, "tcp_flags"); s != "" { if _, _, e := tcpFlags(s); e != nil { return e } } if text(v, "action") == "change-mss" && text(v, "tcp_flags") != "" { _, on, _ := tcpFlags(text(v, "tcp_flags")) found := false for _, flag := range strings.Split(on, ",") { found = found || flag == "SYN" } if !found { return fmt.Errorf("MSS rewriting requires SYN in the TCP flags") } } if len(text(v, "log_prefix")) > 29 { return fmt.Errorf("log prefix is limited to 29 bytes") } if len(text(v, "content")) > 128 { return fmt.Errorf("content matching is limited to 128 bytes") } for _, key := range []string{"tcp_mss", "packet_size"} { if s := text(v, key); s != "" { if _, e := parseRange(s, 65535); e != nil { return fmt.Errorf("%s: %w", key, e) } } } if s := text(v, "src_mac_address"); s != "" { _, s = plain(s) if _, e := net.ParseMAC(s); e != nil { return fmt.Errorf("invalid source MAC") } } if s := text(v, "dscp"); s != "" { if e := intRange(s, 0, 63); e != nil { return e } } if s := text(v, "tos"); s != "" { s = normalizeTOS(s) parts := strings.Split(s, "/") if len(parts) > 2 { return fmt.Errorf("invalid TOS") } for _, n := range parts { if _, e := strconv.ParseUint(n, 0, 8); e != nil { return fmt.Errorf("invalid TOS byte or mask") } } } if s := text(v, "connection_type"); s != "" && !safeIdentifier.MatchString(s) { return fmt.Errorf("invalid connection helper") } if s := text(v, "ttl"); s != "" { if _, _, e := ttlParts(s); e != nil { return e } } if text(v, "action") == "change-ttl" { if e := intRange(strings.TrimLeft(text(v, "new_ttl"), "+-"), 0, 255); e != nil { return fmt.Errorf("invalid new TTL") } } if s := text(v, "random"); s != "" { n, e := strconv.ParseFloat(strings.TrimSuffix(s, "%"), 64) if e != nil || n < 0 || n > 100 { return fmt.Errorf("random must be 0–100 percent") } } if s := text(v, "icmp_options"); s != "" { if p != "icmp" && p != "ipv6-icmp" { return fmt.Errorf("ICMP options require ICMP protocol") } _, s = plain(s) for _, n := range strings.Split(s, ":") { if e := intRange(n, 0, 255); e != nil { return fmt.Errorf("invalid ICMP type/code") } } if len(strings.Split(s, ":")) > 2 { return fmt.Errorf("invalid ICMP options") } } if s := text(v, "ipsec_policy"); s != "" { parts := strings.Split(strings.ReplaceAll(s, ":", ","), ",") if len(parts) != 2 || (parts[0] != "in" && parts[0] != "out") || (parts[1] != "ipsec" && parts[1] != "none") { return fmt.Errorf("IPsec policy must be in,ipsec / out,ipsec / in,none / out,none") } } if s := text(v, "limit"); s != "" { if _, _, e := rateParts(s); e != nil { return e } } if s := text(v, "time"); s != "" { parts := strings.Split(s, "-") if len(parts) != 2 { return fmt.Errorf("time expects start-stop in UTC") } for _, part := range parts { if _, e := clock(part); e != nil { return e } } } if s := text(v, "days"); s != "" { for _, day := range strings.Split(s, ",") { if weekdays[strings.ToLower(day)] == "" { return fmt.Errorf("invalid weekday %q", day) } } } if s := text(v, "nth"); s != "" { parts := strings.Split(s, ",") if len(parts) != 2 { return fmt.Errorf("nth expects every,packet (packet starts at 1)") } every, e := strconv.Atoi(parts[0]) packet, e2 := strconv.Atoi(parts[1]) if e != nil || e2 != nil || every < 1 || packet < 1 || packet > every { return fmt.Errorf("invalid nth values") } } if s := text(v, "connection_limit"); s != "" { parts := strings.Split(s, ",") if len(parts) != 2 { return fmt.Errorf("connection limit expects count,mask") } if e := intRange(parts[0], 1, 2147483647); e != nil { return e } max := 32 if v["ip_version"] == float64(6) { max = 128 } if e := intRange(parts[1], 0, max); e != nil { return e } } for _, key := range []string{"src_address_type", "dst_address_type"} { s := text(v, key) if s != "" { switch strings.ToUpper(s) { case "LOCAL", "UNICAST", "BROADCAST", "MULTICAST", "ANYCAST", "BLACKHOLE", "UNREACHABLE", "PROHIBIT": default: return fmt.Errorf("unsupported address type") } } } if s := text(v, "ip_fragment"); s != "" { if s != "yes" && s != "no" { return fmt.Errorf("fragment expects yes/no") } if v["ip_version"] == float64(6) { return fmt.Errorf("fragment matching is IPv4-only") } } return nil } func appendAdvanced(args []string, rule any) []string { v := values(rule) if s := text(v, "any_port"); s != "" { inv, s := plain(s) args = append(args, "-m multiport "+inv+"--ports "+portValue(s)) } if s := text(v, "packet_mark"); s != "" { inv, s := plain(s) args = append(args, fmt.Sprintf("-m mark %s--mark %d", inv, MarkNameToID(s))) } if s := text(v, "connection_type"); s != "" { args = append(args, "-m helper --helper "+s) } if items, ok := v["connection_nat_state"].([]any); ok && len(items) > 0 { var states []string for _, i := range items { state := strings.ToUpper(i.(string)) state = strings.ReplaceAll(state, "SRCNAT", "SNAT") state = strings.ReplaceAll(state, "DSTNAT", "DNAT") states = append(states, state) } args = append(args, "-m conntrack --ctstate "+strings.Join(states, ",")) } if s := text(v, "content"); s != "" { args = append(args, "-m string --algo bm --string "+quote(s)) } if s := text(v, "src_mac_address"); s != "" { inv, s := plain(s) args = append(args, "-m mac "+inv+"--mac-source "+s) } for _, direction := range []string{"in", "out"} { if s := text(v, direction+"_bridge_port"); s != "" { inv, s := plain(s) args = append(args, "-m physdev "+inv+"--physdev-"+direction+" "+s) } } if s := text(v, "ipsec_policy"); s != "" { p := strings.Split(strings.ReplaceAll(s, ":", ","), ",") args = append(args, "-m policy --dir "+p[0]+" --pol "+p[1]) } if s := text(v, "dscp"); s != "" { args = append(args, "-m dscp --dscp "+s) } if s := text(v, "tcp_mss"); s != "" { r, _ := parseRange(s, 65535) args = append(args, "-m tcpmss --mss "+r) } if s := text(v, "tos"); s != "" { args = append(args, "-m tos --tos "+normalizeTOS(s)) } if s := text(v, "packet_size"); s != "" { r, _ := parseRange(s, 65535) args = append(args, "-m length --length "+r) } if s := text(v, "random"); s != "" { n, _ := strconv.ParseFloat(strings.TrimSuffix(s, "%"), 64) args = append(args, fmt.Sprintf("-m statistic --mode random --probability %.8f", n/100)) } if s := text(v, "tcp_flags"); s != "" { mask, on, _ := tcpFlags(s) args = append(args, "--tcp-flags "+mask+" "+on) } if s := text(v, "icmp_options"); s != "" { inv, s := plain(s) flag := "--icmp-type" if inferProtocol(v) == "ipv6-icmp" { flag = "--icmpv6-type" } args = append(args, inv+flag+" "+strings.ReplaceAll(s, ":", "/")) } if s := text(v, "ttl"); s != "" { op, n, _ := ttlParts(s) module := "ttl" if v["ip_version"] == float64(6) { module = "hl" } args = append(args, "-m "+module+" --"+module+"-"+op+" "+n) } if s := text(v, "limit"); s != "" { rate, burst, _ := rateParts(s) args = append(args, "-m limit --limit "+rate+" --limit-burst "+burst) } if text(v, "time") != "" || text(v, "days") != "" { args = append(args, "-m time") if s := text(v, "time"); s != "" { p := strings.Split(s, "-") start, _ := clock(p[0]) stop, _ := clock(p[1]) args = append(args, "--timestart "+start+" --timestop "+stop) if start > stop { args = append(args, "--contiguous") } } if s := text(v, "days"); s != "" { var days []string for _, d := range strings.Split(s, ",") { days = append(days, weekdays[strings.ToLower(d)]) } args = append(args, "--weekdays "+strings.Join(days, ",")) } } if s := text(v, "nth"); s != "" { p := strings.Split(s, ",") packet, _ := strconv.Atoi(p[1]) args = append(args, fmt.Sprintf("-m statistic --mode nth --every %s --packet %d", p[0], packet-1)) } if s := text(v, "connection_limit"); s != "" { p := strings.Split(s, ",") args = append(args, "-m connlimit --connlimit-above "+p[0]+" --connlimit-mask "+p[1]) } for _, direction := range []string{"src", "dst"} { if s := text(v, direction+"_address_type"); s != "" { args = append(args, "-m addrtype --"+direction+"-type "+strings.ToUpper(s)) } } if s := text(v, "ip_fragment"); s != "" { if s == "yes" { args = append(args, "-f") } else { args = append(args, "! -f") } } return args } func normalizeTOS(s string) string { switch s { case "min-delay": return "0x10/0x1e" case "max-throughput": return "0x08/0x1e" case "max-reliability": return "0x04/0x1e" case "min-cost": return "0x02/0x1e" } return s }