Files
moby/daemon/libnetwork/drivers/overlay/encryption_nft_linux.go
Cory Snider 558df8d6de d/libn/i/nftables: make Table a reference type
Table.Close() had a value receiver, so setting t.t = nil only modified
the callee's copy of the handle. The caller's Table was left looking
valid:

	t, _ := nftables.NewTable(...)
	t.Close()
	t.IsValid() // true

Worse, Close() only dropped the nftables handle, and nftApply() opens a
new one whenever it finds none. So Apply() and Reload() on a closed table
silently reopened a handle and carried on updating the ruleset.

At the root of it, a Table looked like a plain value but behaved like a
reference, and nothing stopped it from being copied. So embed table in
Table by value and hand out *Table instead. The Table/table split is
still needed - table's fields have to be exported for text/template -
but reference semantics are now visible at every call site, and they're
enforced: because table contains a sync.Mutex, "go vet" reports both a
copy of a Table and a method or function that takes one by value, so the
shape of this bug is no longer expressible.

Close() therefore can't invalidate the table by clearing a pointer, it
has to record the state. Add a closed flag, and refuse to open a new
nftables handle for a table that's been closed.

Apply() checked neither for a closed table nor for a nil *Table, which
would have panicked. It now reports an error, checking the closed state
with applyLock held so that it can't race with Close(), and before the
in-memory table is touched so that a rejected update isn't recorded as
applied.

The invalid table is now a nil *Table rather than a zero-value Table,
which also removes the need for consumers to return an empty Table
alongside an error. That made it obvious that the nftabler was leaking
the table it had just created when it gave up on setting up IPv6, so
close it.

Signed-off-by: Cory Snider <csnider@mirantis.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 10:52:59 -04:00

143 lines
3.6 KiB
Go

//go:build linux
package overlay
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/containerd/log"
"github.com/moby/moby/v2/daemon/libnetwork/drivers/overlay/overlayutils"
"github.com/moby/moby/v2/daemon/libnetwork/internal/nftables"
)
const (
nftOverlayTable = "docker-overlay"
nftEncOutChainName = "enc-out"
nftEncInChainName = "enc-in"
nftEncVNSetName = "encrypted-vnis"
nftEncVNIExpr = "@th,96,24"
)
// ensureOverlayEncNftTable returns the overlay encryption nft table, running one-time setup on first use.
func (d *driver) ensureOverlayEncNftTable(ctx context.Context) (*nftables.Table, error) {
d.overlayEncNftInitMu.Lock()
defer d.overlayEncNftInitMu.Unlock()
if d.overlayEncNftTable.IsValid() {
return d.overlayEncNftTable, nil
}
v6, err := d.isIPv6Transport()
if err != nil {
return nil, err
}
fam := nftables.IPv4
if v6 {
fam = nftables.IPv6
}
t, err := nftables.NewTable(fam, nftOverlayTable)
if err != nil {
return nil, err
}
tm := nftables.Modifier{}
tm.Create(nftables.Set{
Name: nftEncVNSetName,
ElementType: nftables.Typeof(nftEncVNIExpr),
})
tm.Create(nftables.BaseChain{
Name: nftEncOutChainName,
ChainType: nftables.BaseChainTypeRoute,
Hook: nftables.BaseChainHookOutput,
Priority: nftables.BaseChainPriorityMangle,
Policy: nftables.BaseChainPolicyAccept,
})
tm.Create(nftables.BaseChain{
Name: nftEncInChainName,
ChainType: nftables.BaseChainTypeFilter,
Hook: nftables.BaseChainHookInput,
Priority: nftables.BaseChainPriorityRaw,
Policy: nftables.BaseChainPolicyAccept,
})
port := strconv.FormatUint(uint64(overlayutils.VXLANUDPPort()), 10)
tm.Create(nftables.Rule{
Chain: nftEncOutChainName,
Rule: []string{
"udp dport", port,
nftEncVNIExpr,
"@" + nftEncVNSetName,
"counter",
"meta mark set", fmt.Sprintf("0x%x", mark),
},
})
tm.Create(nftables.Rule{
Chain: nftEncInChainName,
Rule: []string{
"meta secpath missing",
"udp dport", port,
nftEncVNIExpr,
"@" + nftEncVNSetName,
"counter",
"drop",
},
})
if err := t.Apply(ctx, tm); err != nil {
_ = t.Close()
return nil, err
}
d.overlayEncNftTable = t
return t, nil
}
func (d *driver) programOverlayEncVNINft(ctx context.Context, vni uint32, encrypted bool) error {
// Attempt to clean up stale iptables rules from an old incarnation of
// the daemon which could clash with the nftables ruleset.
cleanupErr := errors.Join(d.programInput(vni, false), d.programMangle(vni, false))
if cleanupErr != nil {
log.G(ctx).WithError(cleanupErr).Infof("Failed to clean up stale iptables rules for VNI %d", vni)
}
t, err := d.ensureOverlayEncNftTable(ctx)
if err != nil {
return err
}
tm := nftables.Modifier{}
se := nftables.SetElement{
SetName: nftEncVNSetName,
Element: fmt.Sprintf("0x%06x", vni&0xffffff),
Idempotent: true,
}
if encrypted {
tm.Create(se)
} else {
tm.Delete(se)
}
return t.Apply(ctx, tm)
}
// cleanupNft deletes all nftables rules created by the driver. It's intended to
// be used during startup, to clean up rules created by an old incarnation of
// the daemon after switching to a different firewall backend.
func (d *driver) cleanupNft(ctx context.Context) {
v6, err := d.isIPv6Transport()
if err != nil {
log.G(ctx).WithError(err).Error("Deleting overlay encryption nftables rules")
return
}
fam := nftables.IPv4
if v6 {
fam = nftables.IPv6
}
if err := nftables.RunCmd(ctx, fmt.Appendf(nil, "delete table %s %s", fam, nftOverlayTable)); err != nil {
log.G(ctx).WithError(err).Info("Deleting overlay encryption nftables rules")
return
}
log.G(ctx).Info("Deleted overlay encryption nftables rules")
}