package render import ( "fmt" "strings" "housecarl/internal/model" ) // FallbackMark starts the comment of every fallback rule ("hc:fallback-…"): // how housecarld tells the fallback is in force. const FallbackMark = "fallback" // Fallback builds the ruleset housecarl-ruleset.service loads at boot when // the saved one fails to: nothing new gets in from the external zones' // interfaces, everything else is let through, and IPv4 leaving an // external zone is masqueraded (when any source NAT is configured), so the // inside keeps working until housecarld puts the real ruleset back. With // no external zone it can't tell which interface faces the internet, and // lets nothing but loopback through. // // It is deliberately small: no sets, flowtable, meters or quotas, and // interfaces by name only, so it loads before they exist. func Fallback(st *model.State) string { var b strings.Builder line := func(format string, args ...any) { fmt.Fprintf(&b, format, args...) b.WriteByte('\n') } // rule writes a counted rule: what it matches, then its verdict. rule := func(name, match, verdict string) { line("\t\t%s counter %s comment \"hc:%s-%s\"", match, verdict, FallbackMark, name) } ext := st.ExternalInterfaces() line("# Housecarl's fallback: loaded at boot only if ruleset.nft fails to load,") line("# until housecarld starts and puts the real ruleset back.") line("table inet %s {}", Table) line("delete table inet %s", Table) line("table inet %s {", Table) if len(ext) == 0 { // Nothing says which interface faces the internet: close it all. for _, c := range []string{"input", "forward"} { line("\tchain %s {", c) line("\t\ttype filter hook %s priority filter; policy drop;", c) line("\t\tiifname \"lo\" accept") line("\t\tcounter comment \"hc:%s-%s\"", FallbackMark, c) line("\t}") } line("}") return b.String() } e := ifaces(ext) line("\tchain input {") line("\t\ttype filter hook input priority filter; policy accept;") line("\t\tiifname \"lo\" accept") line("\t\tct state established,related accept") rule("icmpv6", fmt.Sprintf("iifname %s icmpv6 type { %s, echo-request, echo-reply }", e, icmpv6Errors), "accept") rule("icmpv6-nd", fmt.Sprintf("iifname %s icmpv6 type { %s } ip6 hoplimit 255", e, icmpv6ND), "accept") rule("icmpv6-mld", fmt.Sprintf("iifname %s ip6 saddr fe80::/10 icmpv6 type { %s }", e, icmpv6MLD), "accept") rule("dhcpv6-client", fmt.Sprintf("iifname %s ip6 saddr fe80::/10 udp dport 546", e), "accept") rule("input", "iifname "+e, "drop") line("\t}") line("\tchain forward {") line("\t\ttype filter hook forward priority filter; policy accept;") if st.Settings.Builtins.MSSClamp { line("\t\toifname %s tcp flags syn tcp option maxseg size set rt mtu", e) } line("\t\tct state established,related accept") rule("forward", "iifname "+e, "drop") line("\t}") if masquerades(st) { line("\tchain srcnat {") line("\t\ttype nat hook postrouting priority srcnat; policy accept;") rule("masquerade", "meta nfproto ipv4 oifname "+e, "masquerade") line("\t}") } line("}") return b.String() } // masquerades reports whether any source NAT is configured. func masquerades(st *model.State) bool { for _, x := range st.SNATs { if !x.Disabled { return true } } return false }