Skip to content

Commit 80065ca

Browse files
committed
[datapath] harden neighbor netlink receive buffer handling
Signed-off-by: Jared Ledvina <jared.ledvina@datadoghq.com>
1 parent 5baa1b8 commit 80065ca

5 files changed

Lines changed: 80 additions & 6 deletions

File tree

Documentation/cmdref/cilium-agent.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Documentation/cmdref/cilium-agent_hive.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Documentation/cmdref/cilium-agent_hive_dot-graph.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/datapath/linux/devices_controller.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ func newDevicesController(lc cell.Lifecycle, p devicesControllerParams) (*device
147147
func (dc *devicesController) Start(startCtx cell.HookContext) error {
148148
if dc.params.NetlinkFuncs == nil {
149149
var err error
150-
dc.params.NetlinkFuncs, err = makeNetlinkFuncs(dc.params.Config.NeighborNetlinkBufferSize)
150+
dc.params.NetlinkFuncs, err = makeNetlinkFuncs(dc.log, dc.params.Config.NeighborNetlinkBufferSize)
151151
if err != nil {
152152
return err
153153
}
@@ -762,7 +762,16 @@ type netlinkFuncs struct {
762762
// past net.core.rmem_max: cilium-agent runs with CAP_NET_ADMIN, and a dropped
763763
// neighbor update is more costly than the extra kernel memory. A
764764
// non-positive receiveBufferSize leaves the kernel default in place.
765+
//
766+
// The underlying library only skips the setsockopt call when the size is
767+
// exactly 0, so negative values are normalized to 0 here to honor the
768+
// "non-positive means kernel default" contract; otherwise a negative value
769+
// would be passed to setsockopt(SO_RCVBUF) and clamped by the kernel rather
770+
// than leaving the default untouched.
765771
func neighSubscribeOptions(receiveBufferSize int, ns *vns.NsHandle, errorCallback func(error)) netlink.NeighSubscribeOptions {
772+
if receiveBufferSize < 0 {
773+
receiveBufferSize = 0
774+
}
766775
return netlink.NeighSubscribeOptions{
767776
ListExisting: false,
768777
ErrorCallback: errorCallback,
@@ -779,7 +788,10 @@ func neighSubscribeOptions(receiveBufferSize int, ns *vns.NsHandle, errorCallbac
779788
// the netlink socket used for the neighbor subscription. See
780789
// defaults.NeighborNetlinkBufferSize for rationale. A value <= 0 leaves the
781790
// kernel default in place.
782-
func makeNetlinkFuncs(neighborReceiveBufferSize int) (*netlinkFuncs, error) {
791+
//
792+
// log is used to report the one-time fallback to the kernel-default receive
793+
// buffer if forcing the configured size fails (see the NeighSubscribe closure).
794+
func makeNetlinkFuncs(log *slog.Logger, neighborReceiveBufferSize int) (*netlinkFuncs, error) {
783795
netlinkHandle, err := safenetlink.NewHandle(&safenetlink.HandleConfig{NLFamilies: []int{unix.NETLINK_ROUTE}})
784796
if err != nil {
785797
return nil, fmt.Errorf("creating netlink handle: %w", err)
@@ -820,8 +832,27 @@ func makeNetlinkFuncs(neighborReceiveBufferSize int) (*netlinkFuncs, error) {
820832
},
821833
NeighSubscribe: func(ch chan<- netlink.NeighUpdate, done <-chan struct{}, errorCallback func(error)) error {
822834
h := vns.NsHandle(cur.FD())
823-
return safenetlink.NeighSubscribeWithOptions(ch, done,
835+
err := safenetlink.NeighSubscribeWithOptions(ch, done,
824836
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))
854+
}
855+
return err
825856
},
826857
LinkList: func() ([]netlink.Link, error) {
827858
return safenetlink.WithRetryResult(func() ([]netlink.Link, error) {

pkg/datapath/linux/devices_controller_test.go

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/cilium/cilium/pkg/datapath/tables"
3434
"github.com/cilium/cilium/pkg/defaults"
3535
"github.com/cilium/cilium/pkg/hive"
36+
"github.com/cilium/cilium/pkg/option"
3637
"github.com/cilium/cilium/pkg/testutils"
3738
)
3839

@@ -63,9 +64,9 @@ func TestPrivilegedDevicesControllerScript(t *testing.T) {
6364

6465
h := hive.New(
6566
DevicesControllerCell,
66-
cell.Provide(func() (*netlinkFuncs, error) {
67+
cell.Provide(func(log *slog.Logger) (*netlinkFuncs, error) {
6768
// Provide the normal netlink interface, restricted to the test network namespace.
68-
return makeNetlinkFuncs(defaults.NeighborNetlinkBufferSize)
69+
return makeNetlinkFuncs(log, defaults.NeighborNetlinkBufferSize)
6970
}),
7071
)
7172

@@ -110,11 +111,50 @@ func TestNeighSubscribeOptions(t *testing.T) {
110111
assert.False(t, opts.ListExisting)
111112
})
112113

113-
t.Run("non-positive size leaves kernel default in place", func(t *testing.T) {
114+
t.Run("zero size leaves kernel default in place", func(t *testing.T) {
114115
opts := neighSubscribeOptions(0, &ns, errorCallback)
115116
assert.Equal(t, 0, opts.ReceiveBufferSize)
116117
assert.False(t, opts.ReceiveBufferForceSize)
117118
})
119+
120+
t.Run("negative size is normalized to kernel default", func(t *testing.T) {
121+
// A negative value must be normalized to 0 so the library skips the
122+
// setsockopt call entirely; otherwise setsockopt(SO_RCVBUF, <negative>)
123+
// would be issued and clamped by the kernel rather than left at default.
124+
opts := neighSubscribeOptions(-1, &ns, errorCallback)
125+
assert.Equal(t, 0, opts.ReceiveBufferSize)
126+
assert.False(t, opts.ReceiveBufferForceSize)
127+
})
128+
}
129+
130+
// TestNeighborNetlinkBufferSize_Config verifies that the
131+
// neighbor-netlink-buffer-size flag is registered and that its value is
132+
// populated into DevicesConfig.NeighborNetlinkBufferSize via hive's
133+
// field-name<->flag-name convention (the actual wiring the devices controller
134+
// relies on at Start).
135+
func TestNeighborNetlinkBufferSize_Config(t *testing.T) {
136+
newConfig := func(t *testing.T, args ...string) DevicesConfig {
137+
var got DevicesConfig
138+
h := hive.New(
139+
cell.Config(DevicesConfig{}),
140+
cell.Invoke(func(cfg DevicesConfig) { got = cfg }),
141+
)
142+
flags := pflag.NewFlagSet("", pflag.ContinueOnError)
143+
h.RegisterFlags(flags)
144+
require.NoError(t, flags.Parse(args))
145+
require.NoError(t, h.Populate(hivetest.Logger(t)))
146+
return got
147+
}
148+
149+
t.Run("default is applied", func(t *testing.T) {
150+
cfg := newConfig(t)
151+
assert.Equal(t, defaults.NeighborNetlinkBufferSize, cfg.NeighborNetlinkBufferSize)
152+
})
153+
154+
t.Run("flag overrides default", func(t *testing.T) {
155+
cfg := newConfig(t, "--"+option.NeighborNetlinkBufferSize+"=1024")
156+
assert.Equal(t, 1024, cfg.NeighborNetlinkBufferSize)
157+
})
118158
}
119159

120160
func TestDevicesController_Restarts(t *testing.T) {

0 commit comments

Comments
 (0)