733 lines
22 KiB
Go
733 lines
22 KiB
Go
package firewall
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Action is the firewall action taken on a rule's matching packets.
|
|
type Action uint8
|
|
|
|
const (
|
|
// ActionInvalid is the zero value of Action, meaning no action; it is
|
|
// rejected when authoring a rule or policy.
|
|
ActionInvalid Action = iota
|
|
// Accept permits matching packets through.
|
|
Accept
|
|
// Reject refuses matching packets with an error response to the sender.
|
|
Reject
|
|
// Drop silently discards matching packets.
|
|
Drop
|
|
)
|
|
|
|
// String returns the canonical lower-case name of the action.
|
|
func (t Action) String() string {
|
|
switch t {
|
|
case Accept:
|
|
return "accept"
|
|
case Reject:
|
|
return "reject"
|
|
case Drop:
|
|
return "drop"
|
|
}
|
|
return "invalid"
|
|
}
|
|
|
|
// ParseAction parses a caller-supplied action token (case-insensitive),
|
|
// accepting only the concrete actions Accept, Reject and Drop. The sentinel
|
|
// "invalid" (ActionInvalid) is rejected here so callers cannot author a rule or
|
|
// policy with no real action; backup decoding round-trips it separately in
|
|
// Action.UnmarshalJSON.
|
|
func ParseAction(s string) (Action, error) {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "accept":
|
|
return Accept, nil
|
|
case "reject":
|
|
return Reject, nil
|
|
case "drop":
|
|
return Drop, nil
|
|
}
|
|
return 0, fmt.Errorf("unknown action %q", s)
|
|
}
|
|
|
|
// Family is the IP family a rule targets.
|
|
type Family uint8
|
|
|
|
const (
|
|
// FamilyAny targets both IPv4 and IPv6.
|
|
FamilyAny Family = iota
|
|
// IPv4 targets IPv4 traffic only.
|
|
IPv4
|
|
// IPv6 targets IPv6 traffic only.
|
|
IPv6
|
|
)
|
|
|
|
// String returns the canonical lower-case name of the family.
|
|
func (t Family) String() string {
|
|
switch t {
|
|
case IPv4:
|
|
return "ipv4"
|
|
case IPv6:
|
|
return "ipv6"
|
|
}
|
|
return "any"
|
|
}
|
|
|
|
// ParseFamily parses a family token (case-insensitive), accepting the canonical
|
|
// name emitted by Family.String plus the common aliases (v4/inet4, v6/inet6).
|
|
// An unknown value is an error.
|
|
func ParseFamily(s string) (Family, error) {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "any":
|
|
return FamilyAny, nil
|
|
case "ipv4", "v4", "inet4":
|
|
return IPv4, nil
|
|
case "ipv6", "v6", "inet6":
|
|
return IPv6, nil
|
|
}
|
|
return 0, fmt.Errorf("unknown family %q", s)
|
|
}
|
|
|
|
// Protocol is the network protocol a rule matches.
|
|
type Protocol uint8
|
|
|
|
const (
|
|
// ProtocolAny matches every IP protocol and is the zero value.
|
|
ProtocolAny Protocol = iota
|
|
// UDP is the connectionless transport protocol.
|
|
UDP
|
|
// TCP is the connection-oriented transport protocol.
|
|
TCP
|
|
// ICMP and ICMPv6 are the control-message protocols. ICMP implies IPv4 and
|
|
// ICMPv6 implies IPv6.
|
|
ICMP
|
|
ICMPv6
|
|
// SCTP is a transport protocol that, like TCP and UDP, carries ports.
|
|
SCTP
|
|
// GRE, ESP and AH are portless IP protocols (tunneling and IPsec). A rule
|
|
// carrying one of these cannot also match a port.
|
|
GRE
|
|
ESP
|
|
AH
|
|
// TCPUDP matches TCP and UDP together.
|
|
TCPUDP
|
|
)
|
|
|
|
// String returns the canonical lower-case name of the protocol.
|
|
func (t Protocol) String() string {
|
|
switch t {
|
|
case UDP:
|
|
return "udp"
|
|
case TCP:
|
|
return "tcp"
|
|
case TCPUDP:
|
|
return "tcpudp"
|
|
case ICMP:
|
|
return "icmp"
|
|
case ICMPv6:
|
|
return "icmpv6"
|
|
case SCTP:
|
|
return "sctp"
|
|
case GRE:
|
|
return "gre"
|
|
case ESP:
|
|
return "esp"
|
|
case AH:
|
|
return "ah"
|
|
}
|
|
return "any"
|
|
}
|
|
|
|
// IsICMP reports whether the protocol is ICMP or ICMPv6.
|
|
func (t Protocol) IsICMP() bool {
|
|
return t == ICMP || t == ICMPv6
|
|
}
|
|
|
|
// HasPorts reports whether the protocol carries layer-4 ports (TCP, UDP, SCTP or
|
|
// the merged TCPUDP). A port match is only meaningful and only valid for these
|
|
// protocols.
|
|
func (t Protocol) HasPorts() bool {
|
|
return t == TCP || t == UDP || t == SCTP || t == TCPUDP
|
|
}
|
|
|
|
// oppositeProtocol returns the other transport of the TCP/UDP pair a TCPUDP rule
|
|
// fans out to: UDP for TCP and vice versa. Every other protocol has no twin and
|
|
// returns ProtocolAny (the sentinel meaning "no pair"). It is the protocol analog of
|
|
// oppositeFamily, and supports the dual-row split on removal.
|
|
func oppositeProtocol(p Protocol) Protocol {
|
|
switch p {
|
|
case TCP:
|
|
return UDP
|
|
case UDP:
|
|
return TCP
|
|
default:
|
|
return ProtocolAny
|
|
}
|
|
}
|
|
|
|
// Ptr returns a pointer to v. It is a convenience for setting optional rule
|
|
// fields such as ICMPType, e.g. firewall.Ptr[uint8](8).
|
|
func Ptr[T any](v T) *T {
|
|
return &v
|
|
}
|
|
|
|
// icmpNameToNum maps the ICMP type names various tools accept (and their common
|
|
// aliases) to their numeric type. It is used when reading a rule whose ICMP type
|
|
// is written by name; rules this library writes always emit the number, which
|
|
// every backend accepts.
|
|
var icmpNameToNum = map[string]uint8{
|
|
"echo-reply": 0,
|
|
"pong": 0,
|
|
"destination-unreachable": 3,
|
|
"source-quench": 4,
|
|
"redirect": 5,
|
|
"echo-request": 8,
|
|
"ping": 8,
|
|
"router-advertisement": 9,
|
|
"router-solicitation": 10,
|
|
"time-exceeded": 11,
|
|
"ttl-exceeded": 11,
|
|
"parameter-problem": 12,
|
|
"timestamp-request": 13,
|
|
"timestamp-reply": 14,
|
|
"info-request": 15,
|
|
"info-reply": 16,
|
|
"address-mask-request": 17,
|
|
"address-mask-reply": 18,
|
|
"traceroute": 30,
|
|
}
|
|
|
|
// icmpv6NameToNum maps the ICMPv6 type names nftables, ip6tables and ufw print
|
|
// to their numeric type (both the nftables nd-* spellings and the ip6tables
|
|
// long forms such as router-solicitation). ICMPv6 reuses several names from
|
|
// ICMPv4 (echo-request, destination-unreachable, ...) for *different* numbers,
|
|
// so a name read from an ICMPv6 rule must be resolved through this table rather
|
|
// than icmpNameToNum.
|
|
var icmpv6NameToNum = map[string]uint8{
|
|
"destination-unreachable": 1,
|
|
"packet-too-big": 2,
|
|
"time-exceeded": 3,
|
|
"ttl-exceeded": 3,
|
|
"parameter-problem": 4,
|
|
"echo-request": 128,
|
|
"ping": 128,
|
|
"echo-reply": 129,
|
|
"pong": 129,
|
|
"mld-listener-query": 130,
|
|
"mld-listener-report": 131,
|
|
"mld-listener-done": 132,
|
|
"mld-listener-reduction": 132,
|
|
"nd-router-solicit": 133,
|
|
"router-solicitation": 133,
|
|
"nd-router-advert": 134,
|
|
"router-advertisement": 134,
|
|
"nd-neighbor-solicit": 135,
|
|
"neighbor-solicitation": 135,
|
|
"neighbour-solicitation": 135,
|
|
"nd-neighbor-advert": 136,
|
|
"neighbor-advertisement": 136,
|
|
"neighbour-advertisement": 136,
|
|
"nd-redirect": 137,
|
|
"redirect": 137,
|
|
"router-renumbering": 138,
|
|
"ind-neighbor-solicit": 141,
|
|
"ind-neighbor-advert": 142,
|
|
"mld2-listener-report": 143,
|
|
}
|
|
|
|
// parseICMPType parses an ICMP type token as either a number (0-255) or one of
|
|
// the well-known IPv4 names in icmpNameToNum.
|
|
func parseICMPType(tok string) (uint8, bool) {
|
|
return parseICMPTypeFamily(tok, false)
|
|
}
|
|
|
|
// parseICMPTypeFamily parses an ICMP type token like parseICMPType, but resolves
|
|
// names through the ICMPv6 table when v6 is true. Numbers parse identically in
|
|
// either family (and rules this library writes always emit the number), so only
|
|
// the name path is family-dependent.
|
|
func parseICMPTypeFamily(tok string, v6 bool) (uint8, bool) {
|
|
tok = strings.TrimSpace(tok)
|
|
if n, err := strconv.ParseUint(tok, 10, 8); err == nil {
|
|
return uint8(n), true
|
|
}
|
|
if v6 {
|
|
if n, ok := icmpv6NameToNum[strings.ToLower(tok)]; ok {
|
|
return n, true
|
|
}
|
|
return 0, false
|
|
}
|
|
if n, ok := icmpNameToNum[strings.ToLower(tok)]; ok {
|
|
return n, true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// ParseICMPType parses an ICMP type token as either a number (0-255) or a
|
|
// well-known type name, resolving names through the ICMPv6 table when v6 is true
|
|
// (the same name maps to a different number under ICMPv6 — e.g. echo-request is 8
|
|
// for ICMPv4 but 128 for ICMPv6). It is the exported form of the resolution the
|
|
// backends use internally, so a caller or CLI authoring a rule by name accepts
|
|
// exactly the spellings the library itself emits and reads back.
|
|
func ParseICMPType(tok string, v6 bool) (uint8, bool) {
|
|
return parseICMPTypeFamily(tok, v6)
|
|
}
|
|
|
|
// GetProtocol converts a string to the network protocol. The common spellings
|
|
// each backend emits for ICMPv6 (icmpv6, ipv6-icmp, icmp6) are all recognized.
|
|
// An unknown token resolves to ProtocolAny (the widest match), so a caller that
|
|
// must distinguish an unknown protocol from a genuine "any" checks the token
|
|
// itself, as the save-file parsers do.
|
|
func GetProtocol(proto string) Protocol {
|
|
switch {
|
|
case strings.EqualFold("udp", proto):
|
|
return UDP
|
|
case strings.EqualFold("tcp", proto):
|
|
return TCP
|
|
case strings.EqualFold("tcpudp", proto):
|
|
return TCPUDP
|
|
case strings.EqualFold("icmp", proto):
|
|
return ICMP
|
|
case strings.EqualFold("icmpv6", proto),
|
|
strings.EqualFold("ipv6-icmp", proto),
|
|
strings.EqualFold("icmp6", proto):
|
|
return ICMPv6
|
|
case strings.EqualFold("sctp", proto):
|
|
return SCTP
|
|
case strings.EqualFold("gre", proto):
|
|
return GRE
|
|
case strings.EqualFold("esp", proto),
|
|
strings.EqualFold("ipsec-esp", proto):
|
|
return ESP
|
|
case strings.EqualFold("ah", proto),
|
|
strings.EqualFold("ipsec-ah", proto):
|
|
return AH
|
|
}
|
|
return ProtocolAny
|
|
}
|
|
|
|
// PortRange is an inclusive range of ports. A single port is represented with
|
|
// End equal to Start (or End left zero, which normalizes to Start).
|
|
type PortRange struct {
|
|
// Start is the first port in the inclusive range.
|
|
Start uint16
|
|
// End is the last port in the inclusive range.
|
|
End uint16
|
|
}
|
|
|
|
// normalized returns the range with a zero or inverted End collapsed to a single
|
|
// port at Start.
|
|
func (pr PortRange) normalized() PortRange {
|
|
if pr.End == 0 || pr.End < pr.Start {
|
|
pr.End = pr.Start
|
|
}
|
|
return pr
|
|
}
|
|
|
|
// String renders the range as "80" for a single port or "80-90" for a span.
|
|
func (pr PortRange) String() string {
|
|
pr = pr.normalized()
|
|
if pr.Start == pr.End {
|
|
return strconv.FormatUint(uint64(pr.Start), 10)
|
|
}
|
|
return fmt.Sprintf("%d-%d", pr.Start, pr.End)
|
|
}
|
|
|
|
// ParsePortRange parses a single "80" or "80-90"/"80:90" token into a PortRange.
|
|
func ParsePortRange(s string) (PortRange, error) {
|
|
s = strings.TrimSpace(s)
|
|
sep := "-"
|
|
if strings.Contains(s, ":") {
|
|
sep = ":"
|
|
}
|
|
lo, hi, isRange := strings.Cut(s, sep)
|
|
start, err := strconv.ParseUint(strings.TrimSpace(lo), 10, 16)
|
|
if err != nil {
|
|
return PortRange{}, fmt.Errorf("invalid port %q", lo)
|
|
}
|
|
pr := PortRange{Start: uint16(start), End: uint16(start)}
|
|
if isRange {
|
|
end, err := strconv.ParseUint(strings.TrimSpace(hi), 10, 16)
|
|
if err != nil {
|
|
return PortRange{}, fmt.Errorf("invalid port %q", hi)
|
|
}
|
|
pr.End = uint16(end)
|
|
if pr.End < pr.Start {
|
|
return PortRange{}, fmt.Errorf("port range end %d is below start %d", pr.End, pr.Start)
|
|
}
|
|
}
|
|
return pr, nil
|
|
}
|
|
|
|
// ParsePortRanges parses a separated list such as "80,443,1000-2000" into a slice
|
|
// of PortRange values. sep is the separator between entries (typically ",").
|
|
func ParsePortRanges(s, sep string) ([]PortRange, error) {
|
|
var out []PortRange
|
|
for _, tok := range strings.Split(s, sep) {
|
|
tok = strings.TrimSpace(tok)
|
|
if tok == "" {
|
|
continue
|
|
}
|
|
pr, err := ParsePortRange(tok)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, pr)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// FormatPortRanges renders a slice of ranges as a separated list.
|
|
func FormatPortRanges(prs []PortRange, sep string) string {
|
|
parts := make([]string, len(prs))
|
|
for i, pr := range prs {
|
|
parts[i] = pr.String()
|
|
}
|
|
return strings.Join(parts, sep)
|
|
}
|
|
|
|
// ConnState is a set of connection-tracking states to match, combined as a
|
|
// bitmask (e.g. StateEstablished|StateRelated). The zero value matches no
|
|
// particular state (i.e. the rule is stateless).
|
|
type ConnState uint8
|
|
|
|
const (
|
|
// StateNew matches packets starting a new connection.
|
|
StateNew ConnState = 1 << iota
|
|
// StateEstablished matches packets belonging to an existing connection.
|
|
StateEstablished
|
|
// StateRelated matches packets starting a connection related to an existing
|
|
// one.
|
|
StateRelated
|
|
// StateInvalid matches packets the tracker cannot associate with a connection.
|
|
StateInvalid
|
|
)
|
|
|
|
// connStateNames lists the states in canonical rendering order.
|
|
var connStateNames = []struct {
|
|
bit ConnState
|
|
name string
|
|
}{
|
|
{StateNew, "new"},
|
|
{StateEstablished, "established"},
|
|
{StateRelated, "related"},
|
|
{StateInvalid, "invalid"},
|
|
}
|
|
|
|
// Strings returns the set states as lower-case names in canonical order.
|
|
func (s ConnState) Strings() []string {
|
|
var out []string
|
|
for _, cs := range connStateNames {
|
|
if s&cs.bit != 0 {
|
|
out = append(out, cs.name)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// String renders the state set as a comma-separated list (e.g.
|
|
// "established,related"), or the empty string when no state is set.
|
|
func (s ConnState) String() string {
|
|
return strings.Join(s.Strings(), ",")
|
|
}
|
|
|
|
// ParseConnState parses state names (case-insensitive) into a ConnState bitmask.
|
|
// Each token may itself be a comma-separated list. An unknown name is an error.
|
|
func ParseConnState(tokens ...string) (ConnState, error) {
|
|
var state ConnState
|
|
for _, tok := range tokens {
|
|
for _, name := range strings.Split(tok, ",") {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
matched := false
|
|
for _, cs := range connStateNames {
|
|
if strings.EqualFold(name, cs.name) {
|
|
state |= cs.bit
|
|
matched = true
|
|
break
|
|
}
|
|
}
|
|
if !matched {
|
|
return 0, fmt.Errorf("unknown connection state %q", name)
|
|
}
|
|
}
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
// RateUnit is the time unit a RateLimit is expressed over.
|
|
type RateUnit uint8
|
|
|
|
const (
|
|
// PerSecond expresses a rate per second.
|
|
PerSecond RateUnit = iota
|
|
// PerMinute expresses a rate per minute.
|
|
PerMinute
|
|
// PerHour expresses a rate per hour.
|
|
PerHour
|
|
// PerDay expresses a rate per day.
|
|
PerDay
|
|
)
|
|
|
|
// String returns the canonical (nftables-style) unit name.
|
|
func (u RateUnit) String() string {
|
|
switch u {
|
|
case PerMinute:
|
|
return "minute"
|
|
case PerHour:
|
|
return "hour"
|
|
case PerDay:
|
|
return "day"
|
|
}
|
|
return "second"
|
|
}
|
|
|
|
// ParseRateUnit parses a rate-unit token, accepting the long, short and
|
|
// single-letter spellings the various backends emit (e.g. second/sec/s).
|
|
func ParseRateUnit(s string) (RateUnit, error) {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "s", "sec", "second", "seconds":
|
|
return PerSecond, nil
|
|
case "m", "min", "minute", "minutes":
|
|
return PerMinute, nil
|
|
case "h", "hour", "hours":
|
|
return PerHour, nil
|
|
case "d", "day", "days":
|
|
return PerDay, nil
|
|
}
|
|
return 0, fmt.Errorf("unknown rate unit %q", s)
|
|
}
|
|
|
|
// RateLimit caps the rate at which a rule matches packets: up to Rate packets
|
|
// per Unit, with an optional Burst allowance. A nil *RateLimit on a Rule means
|
|
// no rate limiting. Backends that cannot express a rate limit reject a rule
|
|
// carrying one rather than applying it unlimited.
|
|
type RateLimit struct {
|
|
// Rate is the maximum number of matching packets allowed per Unit.
|
|
Rate uint
|
|
// Unit is the time window Rate is counted over.
|
|
Unit RateUnit
|
|
// Burst is an optional allowance for bursts above Rate. 0 leaves the burst
|
|
// at the backend default.
|
|
Burst uint
|
|
}
|
|
|
|
// String renders the limit as "<rate>/<unit>" (e.g. "10/minute").
|
|
func (rl RateLimit) String() string {
|
|
return fmt.Sprintf("%d/%s", rl.Rate, rl.Unit)
|
|
}
|
|
|
|
// parseRateToken parses a "<rate>/<unit>" token (e.g. "10/minute") into its
|
|
// numeric rate and unit. Backends use it when decoding a rule.
|
|
func parseRateToken(tok string) (uint, RateUnit, error) {
|
|
num, unitStr, ok := strings.Cut(strings.TrimSpace(tok), "/")
|
|
if !ok {
|
|
return 0, 0, fmt.Errorf("invalid rate %q", tok)
|
|
}
|
|
n, err := strconv.ParseUint(strings.TrimSpace(num), 10, 32)
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("invalid rate %q", tok)
|
|
}
|
|
unit, err := ParseRateUnit(unitStr)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
return uint(n), unit, nil
|
|
}
|
|
|
|
// ConnLimit caps the number of concurrent connections a rule matches. When
|
|
// PerSource is set the cap is applied per source address; otherwise it is a
|
|
// single global cap. A nil *ConnLimit means no connection limiting.
|
|
type ConnLimit struct {
|
|
// Count is the maximum number of concurrent connections the rule matches.
|
|
Count uint
|
|
// PerSource, when set, applies Count per source address rather than as a
|
|
// single global cap.
|
|
PerSource bool
|
|
}
|
|
|
|
// netfilterDefaultBurst is the burst the kernel's xt_limit applies when a rule
|
|
// names none (5). nft and iptables always print it back, and their read paths
|
|
// collapse it to 0 (unset), so a caller that sets Burst=5 is asking for exactly
|
|
// that default; normBurst folds the two spellings together.
|
|
const netfilterDefaultBurst = 5
|
|
|
|
// normBurst folds an explicit burst of the netfilter default (5) to 0 (unset)
|
|
// so a rule that names Burst=5 matches its own read-back, which reports the
|
|
// default as 0.
|
|
func normBurst(b uint) uint {
|
|
if b == netfilterDefaultBurst {
|
|
return 0
|
|
}
|
|
return b
|
|
}
|
|
|
|
// eqRateLimit reports whether two optional rate limits are equal, treating nil
|
|
// as a distinct "unset" value. The burst is compared through normBurst so an
|
|
// explicit default burst (5) and an unset burst (0) count as the same limit.
|
|
func eqRateLimit(a, b *RateLimit) bool {
|
|
if a == nil || b == nil {
|
|
return a == b
|
|
}
|
|
return a.Rate == b.Rate && a.Unit == b.Unit && normBurst(a.Burst) == normBurst(b.Burst)
|
|
}
|
|
|
|
// eqConnLimit reports whether two optional connection limits are equal, treating
|
|
// nil as a distinct "unset" value.
|
|
func eqConnLimit(a, b *ConnLimit) bool {
|
|
if a == nil || b == nil {
|
|
return a == b
|
|
}
|
|
return *a == *b
|
|
}
|
|
|
|
// Direction names the traffic direction a default policy or rule applies to.
|
|
type Direction uint8
|
|
|
|
const (
|
|
// DirInput is the inbound (input) direction. It must remain the zero value so
|
|
// a rule with no explicit direction is an input rule.
|
|
DirInput Direction = iota
|
|
// DirOutput is the outbound (output) direction.
|
|
DirOutput
|
|
// DirForward is the routing (forward) direction, where a backend models it.
|
|
DirForward
|
|
// DirAny applies to both the input and output directions. It is the direction
|
|
// analog of FamilyAny: a backend that can store a bidirectional rule as one
|
|
// object reads it back as DirAny, while one that cannot fans it into a concrete
|
|
// input row plus a role-swapped output row on write (expandDirections). It never
|
|
// covers DirForward (a routed rule has no input/output twin) and must be declared
|
|
// last so DirInput stays the zero value.
|
|
DirAny
|
|
)
|
|
|
|
// String returns the canonical lower-case name of the direction.
|
|
func (d Direction) String() string {
|
|
switch d {
|
|
case DirOutput:
|
|
return "output"
|
|
case DirForward:
|
|
return "forward"
|
|
case DirAny:
|
|
return "any"
|
|
}
|
|
return "input"
|
|
}
|
|
|
|
// ParseDirection parses a direction token (case-insensitive), accepting the
|
|
// canonical name emitted by Direction.String. An unknown value is an error.
|
|
func ParseDirection(s string) (Direction, error) {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "input", "in":
|
|
return DirInput, nil
|
|
case "output", "out":
|
|
return DirOutput, nil
|
|
case "forward", "fwd":
|
|
return DirForward, nil
|
|
case "any", "both":
|
|
return DirAny, nil
|
|
}
|
|
return 0, fmt.Errorf("unknown direction %q", s)
|
|
}
|
|
|
|
// DefaultPolicy describes the default action a firewall applies to packets that
|
|
// match no rule, per direction. A field left as ActionInvalid has backend-
|
|
// defined meaning: on Get it means the backend does not expose that direction,
|
|
// and on Set it means the direction should be left unchanged.
|
|
type DefaultPolicy struct {
|
|
// Input is the default action for inbound packets.
|
|
Input Action
|
|
// Output is the default action for outbound packets.
|
|
Output Action
|
|
// Forward is the default action for routed packets.
|
|
Forward Action
|
|
}
|
|
|
|
// get returns the action for a direction on a DefaultPolicy.
|
|
func (p *DefaultPolicy) get(d Direction) Action {
|
|
switch d {
|
|
case DirOutput:
|
|
return p.Output
|
|
case DirForward:
|
|
return p.Forward
|
|
}
|
|
return p.Input
|
|
}
|
|
|
|
// set assigns the action for a direction on a DefaultPolicy.
|
|
func (p *DefaultPolicy) set(d Direction, a Action) {
|
|
switch d {
|
|
case DirOutput:
|
|
p.Output = a
|
|
case DirForward:
|
|
p.Forward = a
|
|
default:
|
|
p.Input = a
|
|
}
|
|
}
|
|
|
|
// SetType names the kind of entries an AddressSet holds.
|
|
type SetType uint8
|
|
|
|
const (
|
|
// SetHashIP is a set of individual IP addresses.
|
|
SetHashIP SetType = iota
|
|
// SetHashNet is a set of CIDR network ranges.
|
|
SetHashNet
|
|
)
|
|
|
|
// String returns the ipset-style name of the set type.
|
|
func (t SetType) String() string {
|
|
switch t {
|
|
case SetHashNet:
|
|
return "hash:net"
|
|
}
|
|
return "hash:ip"
|
|
}
|
|
|
|
// ParseSetType parses a set-type token (case-insensitive), accepting the
|
|
// canonical name emitted by SetType.String ("hash:ip"/"hash:net") plus the short
|
|
// aliases "ip"/"net". An unknown value is an error.
|
|
func ParseSetType(s string) (SetType, error) {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "hash:ip", "ip":
|
|
return SetHashIP, nil
|
|
case "hash:net", "net":
|
|
return SetHashNet, nil
|
|
}
|
|
return 0, fmt.Errorf("unknown set type %q", s)
|
|
}
|
|
|
|
// AddressSet is a named collection of addresses (an ipset, an nftables set or a
|
|
// pf table) that rules can match against. It is managed separately from filter
|
|
// and NAT rules through the Manager's address-set methods.
|
|
type AddressSet struct {
|
|
// Name of the set. Backends that namespace sets (nftables table, pf anchor)
|
|
// keep it within their own container.
|
|
Name string
|
|
// Family restricts the set to an IP family. Some backends require a concrete
|
|
// family (nftables inet sets carry a single address type); FamilyAny is
|
|
// resolved to IPv4 by those backends.
|
|
Family Family
|
|
// Type is the entry kind, defaulting to SetHashIP when zero.
|
|
Type SetType
|
|
// Entries are the addresses or CIDRs in the set.
|
|
Entries []string
|
|
}
|
|
|
|
// ruleLine is one line a rule materializes into in a csf.allow/csf.deny or apf
|
|
// allow_hosts/deny_hosts file, paired with the rule that line reads back as. A rule
|
|
// spanning a family or transport axis the native line cannot carry has no single
|
|
// form, so it fans out into one line per cell. EditIPList marks the lines the file
|
|
// already holds as it scans and writes only the rest, so a partially present fan-out
|
|
// — one family written by an earlier single-family add, or a line lost to a manual
|
|
// edit — is completed rather than left half open or duplicated wholesale.
|
|
type ruleLine struct {
|
|
// line is the exact text written to the list file.
|
|
line string
|
|
// read is the rule that line parses back to, which is what an existing line in
|
|
// the file is compared against to decide whether the line is already present.
|
|
read *Rule
|
|
}
|