201 lines
7.9 KiB
Go
201 lines
7.9 KiB
Go
package firewall
|
|
|
|
import (
|
|
"net"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// Container runtimes and CNI plugins do not merely add firewall rules, they own
|
|
// and continuously reconcile them: dockerd, podman/netavark, kube-proxy and the
|
|
// CNI plugins each rewrite their rules on daemon start, network create and
|
|
// container start. Rewriting or deleting one of those rules severs live container
|
|
// networking for services the consumer never asked this library to touch, and the
|
|
// owning daemon reinstates it on its own schedule, so the change does not even
|
|
// hold. Every read path therefore treats such a rule as out of scope: it never
|
|
// reaches a caller, so it never enters a desired set, and the file-rewrite paths
|
|
// preserve its line verbatim.
|
|
//
|
|
// This is not the ownership filter the package deliberately avoids. The library
|
|
// still manages foreign rules — a hand-written system rule is reconciled like any
|
|
// other. The distinction is a live, self-reconciling owner, not authorship.
|
|
|
|
// containerRuntimeIfaces names interfaces a container runtime or CNI plugin
|
|
// creates and manages. A rule matching on one of these is that runtime's.
|
|
var containerRuntimeIfaces = map[string]bool{
|
|
// Docker.
|
|
"docker0": true,
|
|
"docker_gwbridge": true,
|
|
// Podman.
|
|
"cni-podman0": true,
|
|
// Kubernetes CNI plugins.
|
|
"cni0": true,
|
|
"flannel.1": true,
|
|
"tunl0": true,
|
|
"vxlan.calico": true,
|
|
"cilium_host": true,
|
|
"cilium_net": true,
|
|
"cilium_vxlan": true,
|
|
"weave": true,
|
|
"kube-bridge": true,
|
|
"kube-ipvs0": true,
|
|
"nodelocaldns": true,
|
|
}
|
|
|
|
// containerRuntimeIfacePrefixes names interface prefixes a container runtime or
|
|
// CNI plugin allocates per network or per container, where the suffix is a
|
|
// generated index or identifier.
|
|
var containerRuntimeIfacePrefixes = []string{
|
|
"podman", // podman0, podman1, ... per podman network.
|
|
"cali", // Calico's per-workload interfaces; matches its own `cali+` rules.
|
|
"lxc", // Cilium's per-endpoint interfaces, and LXC's container interfaces.
|
|
}
|
|
|
|
// dockerBridgeRe matches the bridge Docker creates for a user-defined network:
|
|
// "br-" followed by the first 12 hex digits of the network ID. The pattern is
|
|
// deliberately exact rather than a "br-" prefix so an operator's own bridge —
|
|
// br-lan, br-wan, br-100 — is still managed normally.
|
|
var dockerBridgeRe = regexp.MustCompile(`^br-[0-9a-f]{12}$`)
|
|
|
|
// isDockerBridge reports whether name is a bridge Docker generated for a
|
|
// user-defined network. Beyond the name pattern it requires at least one numeral,
|
|
// which rules out a hand-named bridge that happens to be spelled in hex letters
|
|
// (br-deadbeefcafe) while costing Docker nothing: a network ID is random hex, so
|
|
// the odds of its first 12 digits holding no numeral are about 1 in 195,000.
|
|
//
|
|
// This stays a heuristic. A bridge renamed through the network's
|
|
// com.docker.network.bridge.name option is not detectable by name at all, and a
|
|
// bridge deliberately named as 12 hex digits including a numeral is a false
|
|
// positive. Both are accepted: the pattern covers the default every
|
|
// docker compose project produces, which is the case that actually occurs.
|
|
func isDockerBridge(name string) bool {
|
|
return dockerBridgeRe.MatchString(name) && strings.ContainsAny(name, "0123456789")
|
|
}
|
|
|
|
// containerRuntimeChains names the iptables/nftables chains a container runtime
|
|
// or CNI plugin creates and reconciles. A trailing "-" or "_" marks a prefix
|
|
// whose remainder is generated (a network name, a service hash, an index).
|
|
var containerRuntimeChains = []string{
|
|
// Docker: DOCKER itself, plus DOCKER-USER, DOCKER-INGRESS, DOCKER-ISOLATION-*
|
|
// and the DOCKER-FORWARD/BRIDGE/CT/INTERNAL set Docker 28 introduced.
|
|
"DOCKER", "DOCKER-",
|
|
// Podman: netavark, and the CNI stack older releases used.
|
|
"NETAVARK", "NETAVARK_", "NETAVARK-", "CNI-",
|
|
// Kubernetes kube-proxy and kubelet.
|
|
"KUBE-",
|
|
// CNI plugins.
|
|
"CALI-", "cali-", "CILIUM_", "FLANNEL-", "WEAVE", "WEAVE-",
|
|
}
|
|
|
|
// containerRuntimeTables names the nftables tables a container runtime or CNI
|
|
// plugin owns outright. Every rule in one of these is the runtime's, whatever
|
|
// chain it sits in — Docker's native nftables backend, for example, uses generic
|
|
// chain names (filter-forward-in, nat-postrouting-out) that carry no marker of
|
|
// their own.
|
|
var containerRuntimeTables = map[string]bool{
|
|
"docker-bridges": true, // dockerd with firewall-backend=nftables.
|
|
"netavark": true, // podman/netavark.
|
|
"kube-proxy": true,
|
|
"kube-router": true,
|
|
"calico": true,
|
|
"cilium": true,
|
|
}
|
|
|
|
// isContainerRuntimeIface reports whether name is an interface a container
|
|
// runtime or CNI plugin manages.
|
|
func isContainerRuntimeIface(name string) bool {
|
|
if name == "" {
|
|
return false
|
|
}
|
|
// An interface match may be negated; the underlying name still identifies the
|
|
// runtime, and a negated match is just as much its rule.
|
|
name = strings.TrimPrefix(name, "!")
|
|
name = strings.TrimSpace(name)
|
|
if containerRuntimeIfaces[name] {
|
|
return true
|
|
}
|
|
if isDockerBridge(name) {
|
|
return true
|
|
}
|
|
// An iptables interface match may carry a trailing "+" wildcard (cali+).
|
|
bare := strings.TrimSuffix(name, "+")
|
|
for _, p := range containerRuntimeIfacePrefixes {
|
|
if strings.HasPrefix(bare, p) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isContainerRuntimeChain reports whether name is a chain a container runtime or
|
|
// CNI plugin creates and reconciles.
|
|
func isContainerRuntimeChain(name string) bool {
|
|
if name == "" {
|
|
return false
|
|
}
|
|
for _, c := range containerRuntimeChains {
|
|
// A generated-suffix entry matches by prefix; an exact entry must match whole
|
|
// so a user chain merely starting with one is still managed.
|
|
if strings.HasSuffix(c, "-") || strings.HasSuffix(c, "_") {
|
|
if strings.HasPrefix(name, c) {
|
|
return true
|
|
}
|
|
continue
|
|
}
|
|
if name == c {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isContainerRuntimeTable reports whether an nftables table is one a container
|
|
// runtime or CNI plugin owns outright. It accepts either a bare table name or the
|
|
// "family name" form the ruleset listing produces (e.g. "ip docker-bridges").
|
|
func isContainerRuntimeTable(name string) bool {
|
|
if name == "" {
|
|
return false
|
|
}
|
|
if i := strings.LastIndex(name, " "); i >= 0 {
|
|
name = name[i+1:]
|
|
}
|
|
return containerRuntimeTables[name]
|
|
}
|
|
|
|
// isContainerRuntime reports whether the rule belongs to a container runtime or
|
|
// CNI plugin, by the interfaces it matches on. Chain and table membership are
|
|
// checked by the backends that can see them, before a rule is ever built.
|
|
func (r *Rule) isContainerRuntime() bool {
|
|
if r == nil {
|
|
return false
|
|
}
|
|
return isContainerRuntimeIface(r.InInterface) || isContainerRuntimeIface(r.OutInterface)
|
|
}
|
|
|
|
// isHairpinMasquerade reports whether the NAT rule is a container runtime's
|
|
// hairpin (loopback) masquerade: the row Docker and podman add per published port
|
|
// so a container reaching its own published address is NATed back to itself. It
|
|
// carries no interface match, so it is the one container NAT rule the interface
|
|
// signal cannot see, and it is identified by its shape instead — a masquerade
|
|
// whose source and destination are the same single host. Nothing else generates
|
|
// that: masquerading a host's traffic to itself is meaningless outside a
|
|
// port-publishing loopback.
|
|
func (r *NATRule) isHairpinMasquerade() bool {
|
|
if r.Kind != Masquerade || r.Source == "" || !addrEqual(r.Source, r.Destination) {
|
|
return false
|
|
}
|
|
// canonAddr collapses a host prefix (/32, /128) onto the bare address, so a
|
|
// source that canonicalizes back to a plain IP is a single host, not a network.
|
|
// A masquerade of a whole subnet onto itself is not the hairpin shape.
|
|
c, ok := canonAddr(r.Source)
|
|
return ok && net.ParseIP(strings.TrimPrefix(c, "!")) != nil
|
|
}
|
|
|
|
// isContainerRuntime reports whether the NAT rule belongs to a container runtime
|
|
// or CNI plugin. It is the NATRule counterpart of Rule.isContainerRuntime.
|
|
func (r *NATRule) isContainerRuntime() bool {
|
|
if r == nil {
|
|
return false
|
|
}
|
|
return isContainerRuntimeIface(r.Interface) || r.isHairpinMasquerade()
|
|
}
|