package render import ( "fmt" "net/netip" "strings" "housecarl/internal/model" ) // Ban is an active ban, for rendering (Options.Bans): its list, the // address banned (an IPv4 address, or an IPv6 prefix of the list's // grouping), and for a ban of one port its protocol and port. Timeout is // the seconds it has left (0: it doesn't end). type Ban struct { List int64 Addr netip.Prefix Proto string // "tcp", "udp", or "" for every port Port int Timeout int } // BanSet is the name of a ban list's set for a family, and with svc // true its set of bans of one port. func BanSet(list int64, v6, svc bool) string { name := fmt.Sprintf("ban_%d_v4", list) if v6 { name = fmt.Sprintf("ban_%d_v6", list) } if svc { name += "_svc" } return name } // BanMark is the mark (comment "hc:ban3") of a ban list's rules. func BanMark(list int64) string { return fmt.Sprintf("ban%d", list) } // BanElement is the set element of a ban: "203.0.113.9", or // "203.0.113.9 . tcp . 22". func BanElement(b Ban) string { e := b.Addr.Addr().String() if b.Proto != "" { e += fmt.Sprintf(" . %s . %d", b.Proto, b.Port) } return e } // IsBanSet reports whether a set is a ban list's. func IsBanSet(name string) bool { return strings.HasPrefix(name, "ban_") } // banSets declares the ban lists' sets. func (r *renderer) banSets() { for _, b := range r.st.BanLists { if b.Disabled { continue } obj := "ban list " + b.Name for _, v6 := range []bool{false, true} { r.w.emit(obj, "\tset %s { type %s; flags timeout; }", BanSet(b.ID, v6, false), addrType(v6)) r.w.emit(obj, "\tset %s { type %s . inet_proto . inet_service; flags timeout; }", BanSet(b.ID, v6, true), addrType(v6)) } } } // banRules writes the drops of the ban lists that cover a chain, ahead // of everything that accepts: a banned address's established connections // are cut too. func (r *renderer) banRules(chain string) { for _, b := range r.st.BanLists { if b.Disabled || !b.Covers(chain) { continue } obj := "ban list " + b.Name d4, d6 := r.st.SplitRefs(b.Hosts) for _, fam := range []string{"v4", "v6"} { dsts := []string{""} if b.Scope == model.BanScopeHosts { refs := d4 if fam == "v6" { refs = d6 } var ok bool if dsts, ok = addrAlts(fam, "daddr", refs, true, false); !ok { continue } } key := ipKeyword(fam) + " saddr" if fam == "v6" && b.V6Group() < 128 { key += " and " + v6Mask(b.V6Group()) } v6 := fam == "v6" for _, d := range dsts { r.banDrop(obj, b.ID, strings.Join(nonEmpty([]string{d, key + " @" + BanSet(b.ID, v6, false)}), " ")) r.banDrop(obj, b.ID, strings.Join(nonEmpty([]string{d, key + " . meta l4proto . th dport @" + BanSet(b.ID, v6, true)}), " ")) } } } } // banDrop writes one drop of a ban list, or in shadow mode counts it. func (r *renderer) banDrop(obj string, list int64, match string) { if r.opt.Shadow { r.w.emit(obj, "\t\t%s counter accept comment \"hc:%s %s\"", match, BanMark(list), Would("drop")) return } r.w.emit(obj, "\t\t%s counter drop comment \"hc:%s\"", match, BanMark(list)) } // v6Mask is the mask of an IPv6 prefix length: 64 → ffff:ffff:ffff:ffff::. func v6Mask(bits int) string { mask := make([]byte, 16) for i := 0; i < bits; i++ { mask[i/8] |= 0x80 >> (i % 8) } m, _ := netip.AddrFromSlice(mask) return m.String() } // banElements adds the active bans, with the time each has left, so a // reload keeps them. func (r *renderer) banElements() { for _, ban := range r.opt.Bans { var list *model.BanList for _, b := range r.st.BanLists { if b.ID == ban.List && !b.Disabled { list = b } } if list == nil || !ban.Addr.IsValid() { continue } timeout := "" if ban.Timeout > 0 { timeout = fmt.Sprintf(" timeout %ds", ban.Timeout) } r.w.emit("ban list "+list.Name, "add element inet %s %s { %s%s }", Table, BanSet(list.ID, ban.Addr.Addr().Is6(), ban.Proto != ""), BanElement(ban), timeout) } }