Configurable count and interval for gratuitous ARP/NA messages

The default is to send 3 messages at 1s intervals.

That can be overridden in "docker network create" using:
  -o com.docker.network.advertise_addr_nmsgs=3
  -o com.docker.network.advertise_addr_ms=1000

Or, in daemon.json for each driver:
  "default-network-opts": {
    "bridge": {
      "com.docker.network.advertise_addr_nmsgs": "3",
      "com.docker.network.advertise_addr_ms": "1000"
    }
  }

The allowed range is 0-3 for the number of messages, and
100-2000ms for the interval. Setting nmsgs to 0 disables the
gratuitous ARP/NA messages.

The default bridge will always use the built-in defaults,
it cannot be configured.

Signed-off-by: Rob Murray <rob.murray@docker.com>
This commit is contained in:
Rob Murray
2024-11-18 15:53:12 +00:00
parent eaa84bc8f4
commit 522016a842
7 changed files with 181 additions and 10 deletions

View File

@@ -39,6 +39,14 @@ const (
// DriverMTU constant represents the MTU size for the network driver
DriverMTU = DriverPrefix + ".mtu"
// AdvertiseAddrNMsgs is the number of unsolicited ARP/NA messages that will be sent to
// advertise an interface's IP and MAC addresses.
AdvertiseAddrNMsgs = Prefix + ".advertise_addr_nmsgs"
// AdvertiseAddrIntervalMs is the minimum interval between ARP/NA advertisements for
// an interface's IP and MAC addresses (in milliseconds).
AdvertiseAddrIntervalMs = Prefix + ".advertise_addr_ms"
// OverlayVxlanIDList constant represents a list of VXLAN Ids as csv
OverlayVxlanIDList = DriverPrefix + ".overlay.vxlanid_list"

View File

@@ -6,6 +6,7 @@ package libnetwork
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/netip"
@@ -364,7 +365,11 @@ func (n *Network) validateConfiguration() error {
"[ ingress | internal | attachable | scope ] are not supported.")
}
}
if n.configFrom != "" {
if n.configFrom == "" {
if err := n.validateAdvertiseAddrConfig(); err != nil {
return err
}
} else {
if n.configOnly {
return types.ForbiddenErrorf("a configuration network cannot depend on another configuration network")
}
@@ -533,6 +538,35 @@ func (n *Network) getEpCnt() *endpointCnt {
return n.epCnt
}
func (n *Network) validateAdvertiseAddrConfig() error {
var errs []error
_, err := n.validatedAdvertiseAddrNMsgs()
errs = append(errs, err)
_, err = n.validatedAdvertiseAddrInterval()
errs = append(errs, err)
return errors.Join(errs...)
}
func (n *Network) advertiseAddrNMsgs() (int, bool) {
v, err := n.validatedAdvertiseAddrNMsgs()
if err != nil || v == nil {
// On Linux, config was validated before network creation. This
// path is for un-set values and unsupported platforms.
return 0, false
}
return *v, true
}
func (n *Network) advertiseAddrInterval() (time.Duration, bool) {
v, err := n.validatedAdvertiseAddrInterval()
if err != nil || v == nil {
// On Linux, config was validated before network creation. This
// path is for un-set values and unsupported platforms.
return 0, false
}
return *v, true
}
// TODO : Can be made much more generic with the help of reflection (but has some golang limitations)
func (n *Network) MarshalJSON() ([]byte, error) {
netMap := make(map[string]interface{})

View File

@@ -4,6 +4,12 @@ package libnetwork
import (
"context"
"fmt"
"strconv"
"time"
"github.com/docker/docker/libnetwork/netlabel"
"github.com/docker/docker/libnetwork/osl"
"github.com/docker/docker/libnetwork/ipams/defaultipam"
)
@@ -32,3 +38,36 @@ func deleteEpFromResolver(epName string, epIface *EndpointInterface, resolvers [
func defaultIpamForNetworkType(networkType string) string {
return defaultipam.DriverName
}
func (n *Network) validatedAdvertiseAddrNMsgs() (*int, error) {
nMsgsStr, ok := n.DriverOptions()[netlabel.AdvertiseAddrNMsgs]
if !ok {
return nil, nil
}
nMsgs, err := strconv.Atoi(nMsgsStr)
if err != nil {
return nil, fmt.Errorf("value for option "+netlabel.AdvertiseAddrNMsgs+" %q must be an integer", nMsgsStr)
}
if nMsgs < osl.AdvertiseAddrNMsgsMin || nMsgs > osl.AdvertiseAddrNMsgsMax {
return nil, fmt.Errorf(netlabel.AdvertiseAddrNMsgs+" must be in the range %d to %d",
osl.AdvertiseAddrNMsgsMin, osl.AdvertiseAddrNMsgsMax)
}
return &nMsgs, nil
}
func (n *Network) validatedAdvertiseAddrInterval() (*time.Duration, error) {
intervalStr, ok := n.DriverOptions()[netlabel.AdvertiseAddrIntervalMs]
if !ok {
return nil, nil
}
msecs, err := strconv.Atoi(intervalStr)
if err != nil {
return nil, fmt.Errorf("value for option "+netlabel.AdvertiseAddrIntervalMs+" %q must be integer milliseconds", intervalStr)
}
interval := time.Duration(msecs) * time.Millisecond
if interval < osl.AdvertiseAddrIntervalMin || interval > osl.AdvertiseAddrIntervalMax {
return nil, fmt.Errorf(netlabel.AdvertiseAddrIntervalMs+" must be in the range %d to %d",
osl.AdvertiseAddrIntervalMin/time.Millisecond, osl.AdvertiseAddrIntervalMax/time.Millisecond)
}
return &interval, nil
}

View File

@@ -240,3 +240,11 @@ func defaultIpamForNetworkType(networkType string) string {
}
return defaultipam.DriverName
}
func (n *Network) validatedAdvertiseAddrNMsgs() (*int, error) {
return nil, nil
}
func (n *Network) validatedAdvertiseAddrInterval() (*time.Duration, error) {
return nil, nil
}

View File

@@ -25,14 +25,45 @@ import (
"golang.org/x/sys/unix"
)
const (
// AdvertiseAddrNMsgsMin defines the minimum number of ARP/NA messages sent when an
// interface is configured.
// Zero can be used to disable unsolicited ARP/NA.
AdvertiseAddrNMsgsMin = 0
// AdvertiseAddrNMsgsMax defines the maximum number of ARP/NA messages sent when an
// interface is configured. It's three, to match RFC-5227 Section 1.1
// // ("PROBE_NUM=3") and RFC-4861 MAX_NEIGHBOR_ADVERTISEMENT.
AdvertiseAddrNMsgsMax = 3
// advertiseAddrNMsgsDefault is the default number of ARP/NA messages sent when
// an interface is configured.
advertiseAddrNMsgsDefault = 3
// AdvertiseAddrIntervalMin defines the minimum interval between ARP/NA messages
// sent when an interface is configured. The min defined here is nonstandard,
// RFC-5227 PROBE_MIN and the default for RetransTimer in RFC-4861 are one
// second. But, faster resends may be useful in a bridge network (where packets
// are not transmitted on a real network).
AdvertiseAddrIntervalMin = 100 * time.Millisecond
// AdvertiseAddrIntervalMax defines the maximum interval between ARP/NA messages
// sent when an interface is configured. The max of 2s matches RFC-5227
// PROBE_MAX.
AdvertiseAddrIntervalMax = 2 * time.Second
// advertiseAddrIntervalDefault is the default interval between ARP/NA messages
// sent when and interface is configured.
// One second matches RFC-5227 PROBE_MIN and the default for RetransTimer in RFC-4861.
advertiseAddrIntervalDefault = time.Second
)
// newInterface creates a new interface in the given namespace using the
// provided options.
func newInterface(ns *Namespace, srcName, dstPrefix string, options ...IfaceOption) (*Interface, error) {
i := &Interface{
stopCh: make(chan struct{}),
srcName: srcName,
dstName: dstPrefix,
ns: ns,
stopCh: make(chan struct{}),
srcName: srcName,
dstName: dstPrefix,
advertiseAddrNMsgs: advertiseAddrNMsgsDefault,
advertiseAddrInterval: advertiseAddrIntervalDefault,
ns: ns,
}
for _, opt := range options {
if opt != nil {
@@ -69,7 +100,13 @@ type Interface struct {
routes []*net.IPNet
bridge bool
sysctls []string
ns *Namespace
// advertiseAddrNMsgs is the number of unsolicited ARP/NA messages that will be sent to
// advertise the interface's addresses. No messages will be sent if this is zero.
advertiseAddrNMsgs int
// advertiseAddrInterval is the interval between unsolicited ARP/NA messages sent to
// advertise the interface's addresses.
advertiseAddrInterval time.Duration
ns *Namespace
}
// SrcName returns the name of the interface in the origin network namespace.
@@ -382,7 +419,7 @@ func waitForIfUpped(ctx context.Context, ns netns.NsHandle, ifIndex int) (bool,
}
}
// advertiseAddrs triggers send gratuitous ARP and Neighbour Advertisement
// advertiseAddrs triggers send unsolicited ARP and Neighbour Advertisement
// messages, so that caches are updated with the MAC address currently associated
// with the interface's IP addresses.
//
@@ -414,6 +451,10 @@ func (n *Namespace) advertiseAddrs(ctx context.Context, ifIndex int, i *Interfac
log.G(ctx).Debug("No MAC address to advertise")
return nil
}
if i.advertiseAddrNMsgs == 0 {
log.G(ctx).Debug("Unsolicited ARP/NA is disabled")
return nil
}
arpSender, naSender := n.prepAdvertiseAddrs(ctx, i, ifIndex)
if arpSender == nil && naSender == nil {
@@ -464,6 +505,9 @@ func (n *Namespace) advertiseAddrs(ctx context.Context, ifIndex int, i *Interfac
if err := send(ctx); err != nil {
return err
}
if i.advertiseAddrNMsgs == 1 {
return nil
}
// Don't clean up on return from this function, there are more ARPs/NAs to send.
stillSending = true
@@ -472,9 +516,9 @@ func (n *Namespace) advertiseAddrs(ctx context.Context, ifIndex int, i *Interfac
defer cleanup()
ctx, span := otel.Tracer("").Start(context.WithoutCancel(ctx), "libnetwork.osl.advertiseAddrs")
defer span.End()
ticker := time.NewTicker(time.Second)
ticker := time.NewTicker(i.advertiseAddrInterval)
defer ticker.Stop()
for c := range 2 {
for c := range i.advertiseAddrNMsgs - 1 {
select {
case <-i.stopCh:
log.G(ctx).Debug("Unsolicited ARP/NA sends cancelled")

View File

@@ -1,6 +1,10 @@
package osl
import "net"
import (
"fmt"
"net"
"time"
)
func (nh *neigh) processNeighOptions(options ...NeighOption) {
for _, opt := range options {
@@ -89,3 +93,29 @@ func WithSysctls(sysctls []string) IfaceOption {
return nil
}
}
// WithAdvertiseAddrNMsgs sets the number of unsolicited ARP/NA messages that will
// be sent to advertise a network interface's addresses.
func WithAdvertiseAddrNMsgs(nMsgs int) IfaceOption {
return func(i *Interface) error {
if nMsgs < AdvertiseAddrNMsgsMin || nMsgs > AdvertiseAddrNMsgsMax {
return fmt.Errorf("AdvertiseAddrNMsgs %d is not in the range %d to %d",
nMsgs, AdvertiseAddrNMsgsMin, AdvertiseAddrNMsgsMax)
}
i.advertiseAddrNMsgs = nMsgs
return nil
}
}
// WithAdvertiseAddrInterval sets the interval between unsolicited ARP/NA messages
// sent to advertise a network interface's addresses.
func WithAdvertiseAddrInterval(interval time.Duration) IfaceOption {
return func(i *Interface) error {
if interval < AdvertiseAddrIntervalMin || interval > AdvertiseAddrIntervalMax {
return fmt.Errorf("AdvertiseAddrNMsgs %d is not in the range %v to %v milliseconds",
interval, AdvertiseAddrIntervalMin, AdvertiseAddrIntervalMax)
}
i.advertiseAddrInterval = interval
return nil
}
}

View File

@@ -340,6 +340,14 @@ func (sb *Sandbox) populateNetworkResources(ctx context.Context, ep *Endpoint) e
if sysctls := ep.getSysctls(); len(sysctls) > 0 {
ifaceOptions = append(ifaceOptions, osl.WithSysctls(sysctls))
}
if n := ep.getNetwork(); n != nil {
if nMsgs, ok := n.advertiseAddrNMsgs(); ok {
ifaceOptions = append(ifaceOptions, osl.WithAdvertiseAddrNMsgs(nMsgs))
}
if interval, ok := n.advertiseAddrInterval(); ok {
ifaceOptions = append(ifaceOptions, osl.WithAdvertiseAddrInterval(interval))
}
}
if err := sb.osSbox.AddInterface(ctx, i.srcName, i.dstPrefix, ifaceOptions...); err != nil {
return fmt.Errorf("failed to add interface %s to sandbox: %v", i.srcName, err)