package model import "fmt" // Chains rules are placed in. const ( // ChainEarly sees packets before connection tracking (prerouting, // priority raw): cheap drops, antispoofing. ChainEarly = "early" // ChainInput is traffic to the firewall itself. ChainInput = "input" // ChainForward is traffic through the firewall. ChainForward = "forward" // ChainOutput is traffic from the firewall itself. ChainOutput = "output" ) // Chains lists the rule chains in display order. var Chains = []string{ChainEarly, ChainInput, ChainForward, ChainOutput} // ChainOrder is a chain's place in Chains. func ChainOrder(c string) int { for i, x := range Chains { if x == c { return i } } return len(Chains) } // Rule actions. const ( ActionAccept = "accept" ActionDrop = "drop" ActionReject = "reject" // ActionLog only logs (and counts); the packet goes on to the next // rule. ActionLog = "log" ) // Actions lists the rule actions. var Actions = []string{ActionAccept, ActionDrop, ActionReject, ActionLog} // Families a rule can be limited to; "" is both. const ( FamilyIPv4 = "ipv4" FamilyIPv6 = "ipv6" ) // Zone is a group of interfaces (wildcards such as "incus*" allowed). // External zones face the internet: built-ins such as antispoofing and // MSS clamping apply to them. type Zone struct { ID int64 `json:"id"` Name string `json:"name"` Interfaces []string `json:"interfaces"` External bool `json:"external,omitempty"` Comment string `json:"comment,omitempty"` } func (z *Zone) GetID() int64 { return z.ID } func (z *Zone) Label() string { return z.Name } // AddressSet is a named list of IPv4 and IPv6 addresses, prefixes and // ranges. A dynamic set also takes entries at run time, each with a // timeout, through its own API (a captive portal's signed-in clients). type AddressSet struct { ID int64 `json:"id"` Name string `json:"name"` Entries []string `json:"entries"` Dynamic bool `json:"dynamic,omitempty"` // Timeout is a dynamic set's default entry lifetime in seconds (0: // entries stay until removed). Timeout int `json:"timeout,omitempty"` Comment string `json:"comment,omitempty"` } func (s *AddressSet) GetID() int64 { return s.ID } func (s *AddressSet) Label() string { return s.Name } // Service is a named list of ports ("tcp/22", "udp/60000-61000") and ICMP // types ("icmp/echo-request", "icmpv6/packet-too-big"). Rules name // services, or give such entries directly. type Service struct { ID int64 `json:"id"` Name string `json:"name"` Entries []string `json:"entries"` Comment string `json:"comment,omitempty"` // Builtin marks the catalog's services (never stored). Builtin bool `json:"builtin,omitempty"` } func (s *Service) GetID() int64 { return s.ID } func (s *Service) Label() string { return s.Name } // Match is what a rule, port forward, source NAT or redirect looks at. // Lists are ORed; the fields together are ANDed. type Match struct { // InZones and OutZones are zone names; with NotIn/NotOut, every zone // but those. InZones []string `json:"in_zones,omitempty"` NotIn bool `json:"not_in,omitempty"` OutZones []string `json:"out_zones,omitempty"` NotOut bool `json:"not_out,omitempty"` // Sources and Destinations are set names or literal addresses, // prefixes and ranges, IPv4 or IPv6. Sources []string `json:"sources,omitempty"` NotSources bool `json:"not_sources,omitempty"` Destinations []string `json:"destinations,omitempty"` NotDests bool `json:"not_destinations,omitempty"` // Services are service names or entries ("tcp/22", "icmp/echo-request"). Services []string `json:"services,omitempty"` // Family limits the rule to ipv4 or ipv6 ("": what its addresses // and services allow). Family string `json:"family,omitempty"` } // Rule is one line of a chain, checked in order. type Rule struct { ID int64 `json:"id"` Chain string `json:"chain"` Position float64 `json:"position"` Match // CtState: new, established, related, invalid, untracked. CtState []string `json:"ct_state,omitempty"` // CtStatus: dnat, snat (a connection that was NATed). CtStatus []string `json:"ct_status,omitempty"` // Days ("Monday", …) and hours ("08:00"-"17:30") the rule applies. Days []string `json:"days,omitempty"` HourFrom string `json:"hour_from,omitempty"` HourTo string `json:"hour_to,omitempty"` // Limit, if set, makes the rule match only packets within (or with // LimitOver, beyond) a rate. Limit *Limit `json:"limit,omitempty"` // ConnLimit matches when a source already has more than this many // connections (per source address, grouped as GroupV4/GroupV6 say). ConnLimit int `json:"conn_limit,omitempty"` // Quota, if set, is an allowance of bytes: the rule matches once it // is used up (Over), or while it lasts. Quota *Quota `json:"quota,omitempty"` // GroupV4 and GroupV6 group addresses for the per-address limits, // connection limit and quota: by prefix length (0: the default, each // IPv4 address alone, IPv6 by /64, since one device has several). GroupV4 int `json:"group_v4,omitempty"` GroupV6 int `json:"group_v6,omitempty"` // EveryPacket checks the rule against every packet, before established // connections are accepted, so a drop or reject also stops connections // already open, not just new ones. EveryPacket bool `json:"every_packet,omitempty"` Action string `json:"action"` // RejectWith: "" (ICMP port/admin unreachable as fits) or tcp-reset. RejectWith string `json:"reject_with,omitempty"` Log bool `json:"log,omitempty"` LogPrefix string `json:"log_prefix,omitempty"` // Raw, when set, is an nftables rule written by hand, used instead of // the fields above (the escape hatch for what Housecarl can't say). Raw string `json:"raw,omitempty"` Comment string `json:"comment,omitempty"` Disabled bool `json:"disabled,omitempty"` } func (r *Rule) GetID() int64 { return r.ID } func (r *Rule) Label() string { if r.Comment != "" { return fmt.Sprintf("%s #%d (%s)", r.Chain, r.ID, r.Comment) } return fmt.Sprintf("%s #%d", r.Chain, r.ID) } // Limit is a rate: Rate packets (or bytes, with Unit) per Per (second, // minute, hour, day), with a burst. type Limit struct { Rate int `json:"rate"` Per string `json:"per"` // Burst is in the rate's unit. Burst int `json:"burst,omitempty"` // Unit: "" for packets, or bytes, kbytes or mbytes for a byte rate // (bandwidth). Unit string `json:"unit,omitempty"` // Over matches packets beyond the rate instead of within it (to drop // floods). Over bool `json:"over,omitempty"` // PerSource counts each source address on its own (a meter), instead // of all traffic the rule sees together; PerDestination each // destination. PerSource bool `json:"per_source,omitempty"` PerDestination bool `json:"per_destination,omitempty"` } // Byte units, as nftables writes them. var ByteUnits = map[string]int64{"bytes": 1, "kbytes": 1 << 10, "mbytes": 1 << 20, "gbytes": 1 << 30} // Quota is an allowance of bytes. nftables counts it; Housecarl keeps the // count across changes and restarts, and resets it each period. type Quota struct { Bytes int64 `json:"bytes"` // Over matches once the allowance is used up (to drop); otherwise the // rule matches while it lasts (to accept). Over bool `json:"over,omitempty"` // PerSource gives each source address (grouped as the rule's // GroupV4/GroupV6 say) its own allowance; PerDestination each // destination. PerSource bool `json:"per_source,omitempty"` PerDestination bool `json:"per_destination,omitempty"` // Reset: "" (only by hand), daily, weekly (on Mondays) or monthly (on // Day, 1-31: a short month's last day when it has no such day), at // midnight. Reset string `json:"reset,omitempty"` Day int `json:"day,omitempty"` } // Quota resets. var QuotaResets = []string{"daily", "weekly", "monthly"} // PerAddress reports whether a quota (or limit) counts each address on its // own, and which: "saddr", "daddr" or "". func (q *Quota) PerAddress() string { switch { case q == nil: return "" case q.PerSource: return "saddr" case q.PerDestination: return "daddr" } return "" } // PerAddress: as Quota's. func (l *Limit) PerAddress() string { switch { case l == nil: return "" case l.PerSource: return "saddr" case l.PerDestination: return "daddr" } return "" } // CountsTraffic reports whether a rule measures traffic (a quota or a byte // rate): it must see every packet of a connection, so it is checked before // the built-in that accepts established connections. func (r *Rule) CountsTraffic() bool { if r.Raw != "" { return false } if r.Quota != nil { return true } if r.Limit == nil { return false } _, bytes := ByteUnits[r.Limit.Unit] return bytes } // ChecksEveryPacket reports whether a rule is checked before the built-in // that accepts established connections: it counts traffic, or asks to. func (r *Rule) ChecksEveryPacket() bool { return r.Raw == "" && (r.EveryPacket || r.CountsTraffic()) } // Groups returns a rule's address grouping, defaults applied. func (r *Rule) Groups() (v4, v6 int) { v4, v6 = r.GroupV4, r.GroupV6 if v4 == 0 { v4 = 32 } if v6 == 0 { v6 = 64 } return v4, v6 } // PortForward publishes a service on an inside host: incoming connections // to a public address and port are sent on (DNAT) to To[:ToPort], let // through the forward chain, and, with Hairpin, also work from inside. // To6 also opens the port to an IPv6 host (a pinhole; no translation). type PortForward struct { ID int64 `json:"id"` Position float64 `json:"position"` Name string `json:"name,omitempty"` // Protocols: tcp, udp (default tcp). Protocols []string `json:"protocols"` // Ports is the public port or range ("8096", "7777-7780"). Ports string `json:"ports"` // PublicAddresses are the IPv4 addresses (or set names) to match; // none means any address on the external zones' interfaces (and then // no hairpin). PublicAddresses []string `json:"public_addresses,omitempty"` // Sources limits who may connect (set names or addresses). Sources []string `json:"sources,omitempty"` To string `json:"to,omitempty"` // ToPort is the inside port ("" keeps the public one; for a range, the // first port of an equally long range). ToPort string `json:"to_port,omitempty"` To6 string `json:"to6,omitempty"` // Hairpin lets inside clients use the public address: their // connections are masqueraded so replies come back through the // firewall. HairpinSources limits that to some sources (none: all // inside traffic). Hairpin bool `json:"hairpin,omitempty"` HairpinSources []string `json:"hairpin_sources,omitempty"` Comment string `json:"comment,omitempty"` Disabled bool `json:"disabled,omitempty"` } func (f *PortForward) GetID() int64 { return f.ID } func (f *PortForward) Label() string { if f.Name != "" { return f.Name } return fmt.Sprintf("port forward #%d (%s)", f.ID, f.Ports) } // SourceNAT rewrites the source address of outgoing traffic: masquerade // (To empty: the outgoing interface's address) or SNAT to To. type SourceNAT struct { ID int64 `json:"id"` Position float64 `json:"position"` Match To string `json:"to,omitempty"` Comment string `json:"comment,omitempty"` Disabled bool `json:"disabled,omitempty"` } func (s *SourceNAT) GetID() int64 { return s.ID } func (s *SourceNAT) Label() string { if s.Comment != "" { return fmt.Sprintf("source NAT #%d (%s)", s.ID, s.Comment) } return fmt.Sprintf("source NAT #%d", s.ID) } // Redirect sends matching traffic to a port on the firewall itself (to // make every client use its DNS server, say). type Redirect struct { ID int64 `json:"id"` Position float64 `json:"position"` Match // ToPort is the local port (the services' first port when empty). ToPort string `json:"to_port,omitempty"` Comment string `json:"comment,omitempty"` Disabled bool `json:"disabled,omitempty"` } func (r *Redirect) GetID() int64 { return r.ID } func (r *Redirect) Label() string { if r.Comment != "" { return fmt.Sprintf("redirect #%d (%s)", r.ID, r.Comment) } return fmt.Sprintf("redirect #%d", r.ID) }