| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475 |
- 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
- }
|