314 lines
11 KiB
Go
314 lines
11 KiB
Go
package firewall
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/vishvananda/netlink"
|
|
"github.com/vishvananda/netlink/nl"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// ipsetLayoutInstalled reports the ipset staging file and restore service a
|
|
// packaging uses, or empty strings when its persistence mechanism is not
|
|
// installed. The Debian layout restores sets through a netfilter-persistent
|
|
// plugin (proven by ipsetPlugin's presence); the RHEL and Arch layouts use a
|
|
// dedicated ipset service (proven by its unit or init.d script existing).
|
|
func ipsetLayoutInstalled(ctx context.Context, layout iptLayout) (path, service string) {
|
|
if layout.ipsetPath == "" {
|
|
return "", ""
|
|
}
|
|
if layout.ipsetPlugin != "" {
|
|
if matches, _ := filepath.Glob(layout.ipsetPlugin); len(matches) == 0 {
|
|
return "", ""
|
|
}
|
|
return layout.ipsetPath, layout.ipsetService
|
|
}
|
|
if !serviceInstalled(ctx, layout.ipsetService) {
|
|
return "", ""
|
|
}
|
|
return layout.ipsetPath, layout.ipsetService
|
|
}
|
|
|
|
// detectIPSetPersistence reports the first installed ipset persistence mechanism
|
|
// among the known packagings, independent of which one manages the rules save
|
|
// files. It serves backends that keep their rules elsewhere but stage their sets
|
|
// as plain ipsets — ufw, whose host commonly carries the ipset package without
|
|
// iptables-persistent, so probing by rules layout would find nothing.
|
|
func detectIPSetPersistence(ctx context.Context) (path, service string) {
|
|
for _, l := range iptLayouts {
|
|
if p, s := ipsetLayoutInstalled(ctx, l); p != "" {
|
|
return p, s
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
// ipsetLiveFamily reports the family of a live kernel ipset by name: the shared
|
|
// set-family source for backends whose address sets are plain ipsets created
|
|
// live (iptables, and ufw through its iptables set helper). It is a variable so
|
|
// tests can substitute a fake kernel.
|
|
var ipsetLiveFamily = netlinkIPSetFamily
|
|
|
|
// ipsetRefFamily resolves the single family of the set(s) a rule references for
|
|
// every backend whose address sets are kernel ipsets: declared — the backend's
|
|
// own staged sets, its config file or hook — is consulted first, because those
|
|
// backends stage set changes and activate them on Reload, so a set staged but
|
|
// not yet loaded is the family the rule will actually match once both are
|
|
// applied. An unrelated live ipset sharing the name must not shadow it. The live
|
|
// kernel is the fallback, covering a set that exists only in the kernel (a host
|
|
// with no persistence mechanism, where sets are created live). declared runs
|
|
// lazily, at most once per resolve. A live query failure (netlink blocked, no
|
|
// privileges) just means not found live. Backends whose sets are not kernel
|
|
// ipsets (nftables named sets, firewalld's D-Bus ipsets) must not resolve
|
|
// through here.
|
|
func ipsetRefFamily(source, destination string, declared func() ([]*AddressSet, error)) (Family, error) {
|
|
var sets []*AddressSet
|
|
loaded := false
|
|
return setRefFamilyFrom(func(name string) (Family, bool, error) {
|
|
if !loaded {
|
|
loaded = true
|
|
var err error
|
|
if sets, err = declared(); err != nil {
|
|
return FamilyAny, false, err
|
|
}
|
|
}
|
|
for _, s := range sets {
|
|
if s.Name == name {
|
|
return s.Family, true, nil
|
|
}
|
|
}
|
|
if fam, found, err := ipsetLiveFamily(name); err == nil && found {
|
|
return fam, true, nil
|
|
}
|
|
return FamilyAny, false, nil
|
|
}, source, destination)
|
|
}
|
|
|
|
// --- live kernel ipsets -----------------------------------------------------
|
|
//
|
|
// Every kernel-side set operation but the staged bulk load goes through netlink
|
|
// here. The `ipset` binary is kept only for that load (ipset restore), where the
|
|
// tool owns the file format, parses the entries and negotiates each type's
|
|
// revision; doing that over netlink would mean reimplementing all three.
|
|
//
|
|
// Reading these back is deliberately kept in step with the save-format decode in
|
|
// iptables_linux.go: the same address set must look the same whether it was read
|
|
// from a staging file or from the kernel.
|
|
|
|
// ipsetCall runs a netlink ipset operation, turning the library's panic on a
|
|
// kernel error it cannot type-assert to syscall.Errno into an ordinary error.
|
|
// Every call into the library must be wrapped: the assertion is unchecked, so
|
|
// any non-Errno failure (a closed socket, a short read) would otherwise take the
|
|
// caller's process down.
|
|
func ipsetCall(op string, fn func() error) (err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = fmt.Errorf("ipset %s failed: %v", op, r)
|
|
}
|
|
}()
|
|
return fn()
|
|
}
|
|
|
|
// ipsetNoSuchSet reports whether err is the kernel's answer for an operation on
|
|
// a set that does not exist, which every removal treats as already done.
|
|
func ipsetNoSuchSet(err error) bool {
|
|
return errors.Is(err, syscall.ENOENT)
|
|
}
|
|
|
|
// ipsetInUse reports whether err is the kernel refusing to destroy a set that a
|
|
// loaded rule still matches on. The staged model reloads the rules before the
|
|
// destroys it owes, so this means a rule outside the staging file holds the set.
|
|
func ipsetInUse(err error) bool {
|
|
var e nl.IPSetError
|
|
return errors.As(err, &e) && (int(e) == nl.IPSET_ERR_BUSY || int(e) == nl.IPSET_ERR_REFERENCED)
|
|
}
|
|
|
|
// ipsetUnavailable reports whether err means the host has no ipset support at
|
|
// all — the kernel module is absent and nfnetlink has no subsystem to hand the
|
|
// request to. Read as no sets rather than an error, matching a host where the
|
|
// feature was never installed.
|
|
func ipsetUnavailable(err error) bool {
|
|
return errors.Is(err, syscall.EOPNOTSUPP) || errors.Is(err, syscall.EPROTONOSUPPORT)
|
|
}
|
|
|
|
// ipsetNLFamily maps a family to the value the kernel's ipset API expects.
|
|
func ipsetNLFamily(f Family) uint8 {
|
|
if f == IPv6 {
|
|
return unix.AF_INET6
|
|
}
|
|
return unix.AF_INET
|
|
}
|
|
|
|
// ipsetEncodeEntry converts a set entry — a bare address or a CIDR — into the
|
|
// netlink form. Replace is set so add and delete carry the kernel's exist flag,
|
|
// the same idempotence `ipset -exist` gives.
|
|
func ipsetEncodeEntry(entry string) (*netlink.IPSetEntry, error) {
|
|
if strings.Contains(entry, "/") {
|
|
_, n, err := net.ParseCIDR(entry)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("address set entry %q is not a valid cidr: %w", entry, err)
|
|
}
|
|
ones, _ := n.Mask.Size()
|
|
return &netlink.IPSetEntry{IP: n.IP, CIDR: uint8(ones), Replace: true}, nil
|
|
}
|
|
ip := net.ParseIP(entry)
|
|
if ip == nil {
|
|
return nil, fmt.Errorf("address set entry %q is not an ip address or cidr", entry)
|
|
}
|
|
return &netlink.IPSetEntry{IP: ip, Replace: true}, nil
|
|
}
|
|
|
|
// ipsetDecodeEntry renders a kernel entry the way the save format writes it, so
|
|
// a set read live and the same set read from a staging file compare equal. A
|
|
// prefix covering the whole address is dropped, which is how `ipset save` emits
|
|
// a single host stored in a hash:net set.
|
|
func ipsetDecodeEntry(e netlink.IPSetEntry) string {
|
|
if e.IP == nil {
|
|
return ""
|
|
}
|
|
bits := 128
|
|
if e.IP.To4() != nil {
|
|
bits = 32
|
|
}
|
|
if e.CIDR == 0 || int(e.CIDR) == bits {
|
|
return e.IP.String()
|
|
}
|
|
return e.IP.String() + "/" + strconv.Itoa(int(e.CIDR))
|
|
}
|
|
|
|
// ipsetCreate creates a kernel set, tolerating one that already exists with the
|
|
// same definition. The type revision is left to the netlink library, which maps
|
|
// hash:ip to its revision 1 and falls through to the base revision 0 for
|
|
// hash:net: every kernel carrying hash:net registers 0, and the revisions above
|
|
// it only add features (ranges, nomatch, counters, comments, forceadd, skbinfo)
|
|
// this library never asks for.
|
|
func ipsetCreate(name string, family Family, t SetType) error {
|
|
return ipsetCall("create "+name, func() error {
|
|
return netlink.IpsetCreate(name, t.String(), netlink.IpsetCreateOptions{
|
|
Replace: true,
|
|
Family: ipsetNLFamily(family),
|
|
})
|
|
})
|
|
}
|
|
|
|
// ipsetDestroy removes a kernel set. A set that is already gone is success.
|
|
func ipsetDestroy(name string) error {
|
|
err := ipsetCall("destroy "+name, func() error { return netlink.IpsetDestroy(name) })
|
|
if err == nil || ipsetNoSuchSet(err) {
|
|
return nil
|
|
}
|
|
if ipsetInUse(err) {
|
|
return fmt.Errorf("address set %q is still referenced by a loaded rule: %w", name, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// ipsetFlush empties a kernel set. A set that is already gone is success.
|
|
func ipsetFlush(name string) error {
|
|
err := ipsetCall("flush "+name, func() error { return netlink.IpsetFlush(name) })
|
|
if err != nil && !ipsetNoSuchSet(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ipsetAddEntry adds an entry to a kernel set. An entry already present is
|
|
// success; a set that does not exist is reported, since the caller asked to add
|
|
// to something that is not there.
|
|
func ipsetAddEntry(name, entry string) error {
|
|
e, err := ipsetEncodeEntry(entry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := ipsetCall("add "+name, func() error { return netlink.IpsetAdd(name, e) }); err != nil {
|
|
if ipsetNoSuchSet(err) {
|
|
return fmt.Errorf("address set %q does not exist", name)
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ipsetDelEntry removes an entry from a kernel set. A missing entry, or a
|
|
// missing set, is success.
|
|
func ipsetDelEntry(name, entry string) error {
|
|
e, err := ipsetEncodeEntry(entry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = ipsetCall("del "+name, func() error { return netlink.IpsetDel(name, e) })
|
|
if err != nil && !ipsetNoSuchSet(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ipsetLiveSets reports every set the kernel holds, decoded into address sets.
|
|
// A host with no ipset support has no sets to report rather than an error, which
|
|
// keeps a Backup on such a host from failing over a feature it never had.
|
|
func ipsetLiveSets() ([]*AddressSet, error) {
|
|
var res []netlink.IPSetResult
|
|
err := ipsetCall("list", func() error {
|
|
var e error
|
|
res, e = netlink.IpsetListAll()
|
|
return e
|
|
})
|
|
if err != nil {
|
|
if ipsetUnavailable(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
sets := make([]*AddressSet, 0, len(res))
|
|
for _, r := range res {
|
|
if r.SetName == "" {
|
|
continue
|
|
}
|
|
set := &AddressSet{Name: r.SetName, Family: IPv4, Type: SetHashIP}
|
|
if r.Family == unix.NFPROTO_IPV6 {
|
|
set.Family = IPv6
|
|
}
|
|
// An unmodeled type reads as hash:ip, matching how the save-format decode
|
|
// treats a create line it does not recognize.
|
|
if r.TypeName == SetHashNet.String() {
|
|
set.Type = SetHashNet
|
|
}
|
|
for _, e := range r.Entries {
|
|
if s := ipsetDecodeEntry(e); s != "" {
|
|
set.Entries = append(set.Entries, s)
|
|
}
|
|
}
|
|
sets = append(sets, set)
|
|
}
|
|
return sets, nil
|
|
}
|
|
|
|
// netlinkIPSetFamily queries the kernel's ipset subsystem over netlink for a
|
|
// set's family. A set that does not exist reports found=false; any other
|
|
// failure (netlink unavailable, insufficient privileges) is an error the caller
|
|
// can fall back from.
|
|
func netlinkIPSetFamily(name string) (fam Family, found bool, err error) {
|
|
var res *netlink.IPSetResult
|
|
err = ipsetCall("list "+name, func() error {
|
|
var e error
|
|
res, e = netlink.IpsetList(name)
|
|
return e
|
|
})
|
|
if err != nil {
|
|
if ipsetNoSuchSet(err) {
|
|
return FamilyAny, false, nil
|
|
}
|
|
return FamilyAny, false, err
|
|
}
|
|
if res.Family == unix.NFPROTO_IPV6 {
|
|
return IPv6, true, nil
|
|
}
|
|
return IPv4, true, nil
|
|
}
|