package render import ( "fmt" "strings" "housecarl/internal/model" ) // effects are the words of a raw rule's statements that act on packets // (or jump away). In shadow mode a raw rule with any of them is left out, // unless its only one is a final drop or reject, which becomes an accept. var effects = map[string]bool{ "drop": true, "reject": true, "queue": true, "dnat": true, "snat": true, "masquerade": true, "redirect": true, "tproxy": true, "notrack": true, "dup": true, "fwd": true, "synproxy": true, "flow": true, "offload": true, "set": true, "reset": true, "jump": true, "goto": true, "return": true, } // ShadowRaw rewrites a raw rule for shadow mode: as written when it only // matches, counts, logs or accepts; with its final drop or reject made an // accept (marked "[would drop]" in its comment); ok is false when it does // anything else to packets (NAT, setting a mark or header field, a jump, // a verdict map…), and must be left out. func ShadowRaw(raw string) (out string, ok bool) { toks := tokens(raw) verdict := -1 for i := 0; i < len(toks); i++ { w := word(toks[i]) if w == "comment" { i++ // its text continue } if !effects[w] { continue } if verdict >= 0 || (w != "drop" && w != "reject") { return "", false } verdict = i // What follows a verdict, up to the comment, must be a reject's // "with …". j := i + 1 for j < len(toks) && word(toks[j]) != "comment" { j++ } if j > i+1 && (w == "drop" || word(toks[i+1]) != "with") { return "", false } i = j - 1 } if verdict < 0 { return raw, true } mark := Would(word(toks[verdict])) head := append(append([]string{}, toks[:verdict]...), "accept") for i := verdict + 1; i < len(toks); i++ { if word(toks[i]) != "comment" { continue } if i+1 < len(toks) { text := strings.Trim(toks[i+1], `"`) return strings.Join(append(head, "comment", quote(strings.TrimSpace(mark+" "+text))), " "), true } } return strings.Join(append(head, "comment", quote(mark)), " "), true } // tokens splits a rule into words, a quoted string being one. func tokens(s string) []string { var out []string var cur strings.Builder quoted := false for _, c := range s { switch { case c == '"': quoted = !quoted cur.WriteRune(c) case !quoted && (c == ' ' || c == '\t'): if cur.Len() > 0 { out = append(out, cur.String()) cur.Reset() } default: cur.WriteRune(c) } } if cur.Len() > 0 { out = append(out, cur.String()) } return out } // word is a token as a keyword: without the punctuation of sets and maps // around it; a quoted string is no keyword. func word(t string) string { if strings.HasPrefix(t, `"`) { return "" } return strings.ToLower(strings.Trim(t, ",{}:()")) } // ShadowLeftOut lists the raw rules shadow mode leaves out (ShadowRaw). func ShadowLeftOut(st *model.State) []*model.Rule { var out []*model.Rule for _, r := range st.Rules { if r.Raw == "" || r.Disabled { continue } if _, ok := ShadowRaw(r.Raw); !ok { out = append(out, r) } } return out } // ShadowFallback is the boot fallback in shadow mode: the other firewall // protects the machine, so it is an empty table that filters nothing, // with a rule outside every hook that tells housecarld it is in force. func ShadowFallback() string { var b strings.Builder fmt.Fprintf(&b, "# Housecarl's fallback in shadow mode: loaded at boot only if ruleset.nft\n") fmt.Fprintf(&b, "# fails to load. It filters nothing; the other firewall decides.\n") fmt.Fprintf(&b, "table inet %s {}\ndelete table inet %s\n", Table, Table) fmt.Fprintf(&b, "table inet %s {\n\tchain fallback {\n\t\tcounter comment \"hc:%s-shadow\"\n\t}\n}\n", Table, FallbackMark) return b.String() }