@@ -147,7 +147,7 @@ func newDevicesController(lc cell.Lifecycle, p devicesControllerParams) (*device
147147func (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.
765771func 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 ) {
0 commit comments