Skip to content

Commit 4504dc1

Browse files
committed
[datapath] avoid repeated netlink socket leak on forced-buffer fallback
Signed-off-by: Jared Ledvina <jared.ledvina@datadoghq.com>
1 parent 80065ca commit 4504dc1

1 file changed

Lines changed: 42 additions & 19 deletions

File tree

pkg/datapath/linux/devices_controller.go

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"net/netip"
1414
"slices"
1515
"strings"
16+
"sync/atomic"
1617

1718
"github.com/cilium/hive/cell"
1819
"github.com/cilium/statedb"
@@ -802,6 +803,15 @@ func makeNetlinkFuncs(log *slog.Logger, neighborReceiveBufferSize int) (*netlink
802803
return nil, fmt.Errorf("getting current netns: %w", err)
803804
}
804805

806+
// neighForceBufferFailed records that forcing the neighbor subscription's
807+
// receive buffer has already failed for this process (e.g. missing
808+
// CAP_NET_ADMIN or a size rejected against net.core.rmem_max). Such failures
809+
// are effectively permanent, so once we have seen one we stop attempting the
810+
// forced subscription on subsequent restarts. This bounds the netlink socket
811+
// leaked by the vendored library on the setsockopt-failure path to a single
812+
// fd for the lifetime of the process instead of leaking one per restart.
813+
var neighForceBufferFailed atomic.Bool
814+
805815
return &netlinkFuncs{
806816
RouteSubscribe: func(ch chan<- netlink.RouteUpdate, done <-chan struct{}, errorCallback func(error)) error {
807817
h := vns.NsHandle(cur.FD())
@@ -831,28 +841,41 @@ func makeNetlinkFuncs(log *slog.Logger, neighborReceiveBufferSize int) (*netlink
831841
})
832842
},
833843
NeighSubscribe: func(ch chan<- netlink.NeighUpdate, done <-chan struct{}, errorCallback func(error)) error {
844+
// Only force the receive buffer if forcing hasn't already failed
845+
// for this process (see neighForceBufferFailed).
846+
forceBufferSize := neighborReceiveBufferSize
847+
if neighForceBufferFailed.Load() {
848+
forceBufferSize = 0
849+
}
850+
834851
h := vns.NsHandle(cur.FD())
835852
err := safenetlink.NeighSubscribeWithOptions(ch, done,
836-
neighSubscribeOptions(neighborReceiveBufferSize, &h, errorCallback))
837-
if err != nil && neighborReceiveBufferSize > 0 {
838-
// Forcing the receive buffer past net.core.rmem_max requires
839-
// CAP_NET_ADMIN and a size the kernel accepts. If that fails the
840-
// error is permanent, and without a fallback the controller would
841-
// wedge in the restartWaitDuration loop in run(), retrying it once
842-
// per second forever. Fall back once to the kernel-default buffer
843-
// so neighbor updates keep flowing, at the cost of more frequent
844-
// ENOBUFS-driven restarts under heavy churn. On the setsockopt
845-
// failure path the library returns before starting the goroutine
846-
// that closes ch, so ch is safe to reuse here.
847-
log.Warn("Failed to subscribe to neighbor updates with forced netlink receive buffer, falling back to kernel default buffer",
848-
logfields.Error, err,
849-
logfields.BufferSize, neighborReceiveBufferSize,
850-
)
851-
h = vns.NsHandle(cur.FD())
852-
return safenetlink.NeighSubscribeWithOptions(ch, done,
853-
neighSubscribeOptions(0, &h, errorCallback))
853+
neighSubscribeOptions(forceBufferSize, &h, errorCallback))
854+
if err == nil || forceBufferSize <= 0 {
855+
return err
854856
}
855-
return err
857+
858+
// Subscribing with a forced receive buffer failed. Forcing the
859+
// buffer past net.core.rmem_max requires CAP_NET_ADMIN and a size
860+
// the kernel accepts, so the most likely cause is a permanent
861+
// misconfiguration; without a fallback the controller would wedge in
862+
// the restartWaitDuration loop in run(), retrying the same failure
863+
// once per second forever. Fall back once to the kernel-default
864+
// buffer so neighbor updates keep flowing (at the cost of more
865+
// frequent ENOBUFS-driven restarts under heavy churn) and remember
866+
// the failure so we don't re-attempt (and re-leak a socket via the
867+
// vendored library) on every restart. The error may instead be an
868+
// unrelated socket-setup failure (e.g. fd exhaustion); in that case
869+
// the fallback re-hits and propagates it, so no genuine error is
870+
// masked. On the setsockopt-failure path the library returns before
871+
// starting the goroutine that closes ch, so ch is safe to reuse.
872+
neighForceBufferFailed.Store(true)
873+
log.Warn("Failed to subscribe to neighbor updates with a forced netlink receive buffer; retrying without forcing it. Neighbor updates may be dropped more often under high churn. This usually means the agent lacks CAP_NET_ADMIN or the configured size was rejected against net.core.rmem_max.",
874+
logfields.Error, err,
875+
logfields.BufferSize, neighborReceiveBufferSize,
876+
)
877+
return safenetlink.NeighSubscribeWithOptions(ch, done,
878+
neighSubscribeOptions(0, &h, errorCallback))
856879
},
857880
LinkList: func() ([]netlink.Link, error) {
858881
return safenetlink.WithRetryResult(func() ([]netlink.Link, error) {

0 commit comments

Comments
 (0)