Andrey Smirnov 360d887967 fix: prevent endless loop with DHCP requests in networkd
There were two problems:

* `configureInterfaces` was always failing if interface is already set
up, as the routes already exist

* `renew` was halving the renew interval each time `configureInterface`
fails, which starts at (LeaseTime/2) and goes effectively to zero

This was leading to high networkd CPU usage, storm of DHCP requests on
the network.

Signed-off-by: Andrey Smirnov <smirnov.andrey@gmail.com>
2020-12-01 08:12:12 -08:00

448 lines
11 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package nic provides a way to describe and configure a network interface.
package nic
import (
"context"
"errors"
"fmt"
"log"
"net"
"os"
"strconv"
"syscall"
"time"
"github.com/hashicorp/go-multierror"
"github.com/jsimonetti/rtnetlink"
"github.com/jsimonetti/rtnetlink/rtnl"
"github.com/mdlayher/netlink"
"github.com/talos-systems/go-procfs/procfs"
"github.com/talos-systems/go-retry/retry"
"golang.org/x/sys/unix"
"google.golang.org/protobuf/proto"
"github.com/talos-systems/talos/internal/app/networkd/pkg/address"
"github.com/talos-systems/talos/pkg/machinery/constants"
)
const (
// ref: https://tools.ietf.org/html/rfc791
// MinimumMTU is the lowest allowed MTU for an interface.
MinimumMTU = 68
// MaximumMTU is the highest allowed MTU for an interface.
MaximumMTU = 65536
)
// NetworkInterface provides an abstract configuration representation for a
// network interface.
type NetworkInterface struct {
Name string
Type int
Ignore bool
Dummy bool
Bonded bool
MTU uint32
Link *net.Interface
SubInterfaces []*net.Interface
AddressMethod []address.Addressing
BondSettings *netlink.AttributeEncoder
Vlans []*Vlan
rtConn *rtnetlink.Conn
rtnlConn *rtnl.Conn
}
// New returns a NetworkInterface with all of the given setter options applied.
func New(setters ...Option) (*NetworkInterface, error) {
// Default interface setup
iface := defaultOptions()
// Configure interface with any specified options
var result *multierror.Error
for _, setter := range setters {
result = multierror.Append(result, setter(iface))
}
// TODO: May need to look at switching this around to filter by Interface.HardwareAddr
// Ensure we have an interface name defined
if iface.Name == "" {
result = multierror.Append(result, errors.New("interface must have a name"))
}
// If no addressing methods have been configured, default to DHCP.
// If VLANs exist do not force DHCP on master device
if len(iface.AddressMethod) == 0 && len(iface.Vlans) == 0 {
iface.AddressMethod = append(iface.AddressMethod, &address.DHCP{})
}
// Handle netlink connection
conn, err := rtnl.Dial(nil)
if err != nil {
result = multierror.Append(result, err)
return nil, result.ErrorOrNil()
}
iface.rtnlConn = conn
// Need rtnetlink for MTU and bond settings
nlConn, err := rtnetlink.Dial(nil)
if err != nil {
result = multierror.Append(result, err)
return nil, result.ErrorOrNil()
}
iface.rtConn = nlConn
return iface, result.ErrorOrNil()
}
// IsIgnored checks the network interface to see if it should be ignored and not configured.
func (n *NetworkInterface) IsIgnored() bool {
if n.Ignore || procfs.ProcCmdline().Get(constants.KernelParamNetworkInterfaceIgnore).Contains(n.Name) {
return true
}
return false
}
// Create creates the underlying link if it does not already exist.
func (n *NetworkInterface) Create() error {
var info *rtnetlink.LinkInfo
iface, err := net.InterfaceByName(n.Name)
if err == nil {
n.Link = iface
return err
}
if n.Bonded {
info = &rtnetlink.LinkInfo{Kind: "bond"}
}
if n.Dummy {
info = &rtnetlink.LinkInfo{Kind: "dummy"}
}
if err = n.createLink(n.Name, info); err != nil {
return err
}
iface, err = net.InterfaceByName(n.Name)
if err != nil {
return err
}
n.Link = iface
return nil
}
// CreateSub create VLAN devices that belongs to a master device.
func (n *NetworkInterface) CreateSub() error {
var info *rtnetlink.LinkInfo
// Create all the VLAN devices
for _, vlan := range n.Vlans {
name := n.Name + "." + strconv.Itoa(int(vlan.ID))
log.Printf("setting up %s", name)
iface, err := net.InterfaceByName(name)
if err == nil {
vlan.Link = iface
continue
}
data, err := vlan.VlanSettings.Encode()
if err != nil {
log.Println("failed to encode vlan link parameters: " + err.Error())
continue
}
// Vlan devices needs the master link index
masterIdx := uint32(n.Link.Index)
info = &rtnetlink.LinkInfo{Kind: "vlan", Data: data}
if err = n.createSubLink(name, info, &masterIdx); err != nil {
log.Println("failed to create vlan link " + err.Error())
return err
}
iface, err = net.InterfaceByName(name)
if err != nil {
log.Println("failed to get vlan interface ")
return err
}
vlan.Link = iface
}
return nil
}
// Configure is used to set the link state and configure any necessary
// bond settings ( ex, mode ).
func (n *NetworkInterface) Configure() (err error) {
if n.IsIgnored() {
return err
}
if n.Bonded {
if err = n.configureBond(n.Link.Index, n.BondSettings); err != nil {
return err
}
bondIndex := proto.Uint32(uint32(n.Link.Index))
// TODO: Add check if link is already part of a bond
if err = n.enslaveLink(bondIndex, n.SubInterfaces...); err != nil {
return err
}
}
if err = n.rtnlConn.LinkUp(n.Link); err != nil {
return err
}
if err = n.waitForLinkToBeUp(n.Link); err != nil {
return fmt.Errorf("failed to bring up interface %q: %w", n.Link.Name, err)
}
// Create all the VLAN devices
for _, vlan := range n.Vlans {
if err = n.rtnlConn.LinkUp(vlan.Link); err != nil {
return err
}
if err = n.waitForLinkToBeUp(vlan.Link); err != nil {
return fmt.Errorf("failed to bring up interface %q: %w", vlan.Link.Name, err)
}
}
return nil
}
func (n *NetworkInterface) waitForLinkToBeUp(linkDev *net.Interface) error {
// Wait for link to report up
var link rtnetlink.LinkMessage
err := retry.Constant(30*time.Second, retry.WithUnits(250*time.Millisecond), retry.WithJitter(50*time.Millisecond)).Retry(func() error {
var err error
link, err = n.rtConn.Link.Get(uint32(linkDev.Index))
if err != nil {
return retry.UnexpectedError(err)
}
if link.Flags&unix.IFF_UP != unix.IFF_UP {
return retry.ExpectedError(fmt.Errorf("link is not up %s", n.Link.Name))
}
if link.Flags&unix.IFF_RUNNING != unix.IFF_RUNNING {
return retry.ExpectedError(fmt.Errorf("link is not ready %s", n.Link.Name))
}
return nil
})
return err
}
// Addressing handles the address method for a configured interface ( dhcp/static ).
// This is inclusive of the address itself as well as any defined routes.
func (n *NetworkInterface) Addressing() error {
if n.IsIgnored() {
return nil
}
for _, method := range n.AddressMethod {
if err := n.configureInterface(method, n.Link); err != nil {
// Treat as non fatal error when failing to configure an interface
continue
}
}
return nil
}
// AddressingSub handles the address method for a configured sub interface ( dhcp/static ).
// This is inclusive of the address itself as well as any defined routes.
func (n *NetworkInterface) AddressingSub() error {
if n.IsIgnored() {
return nil
}
for _, vlan := range n.Vlans {
for _, method := range vlan.AddressMethod {
if err := n.configureInterface(method, vlan.Link); err != nil {
log.Println("failed to configure address on vlan link: " + err.Error())
// Treat as non fatal error when failing to configure an interface
continue
}
}
}
return nil
}
// Renew is the mechanism for keeping a dhcp lease active.
func (n *NetworkInterface) Renew() {
for _, method := range n.AddressMethod {
if method.TTL() == 0 {
continue
}
go n.renew(method)
}
}
// renew sets up the looping to ensure we keep the addressing information
// up to date. We attempt to do our first reconfiguration halfway through
// address TTL. If that fails, we'll continue to attempt to retry every
// halflife.
func (n *NetworkInterface) renew(method address.Addressing) {
const minRenewDuration = 5 * time.Second // protect from renewing too often
renewDuration := method.TTL() / 2
var err error
for {
time.Sleep(renewDuration)
if err = n.configureInterface(method, n.Link); err != nil {
log.Printf("failure to renew address for %q: %s", n.Name, err)
renewDuration = (renewDuration / 2)
} else {
renewDuration = method.TTL() / 2
}
if renewDuration < minRenewDuration {
renewDuration = minRenewDuration
}
}
}
// configureInterface handles the actual address discovery mechanism and
// netlink interaction to configure the interface.
// nolint: gocyclo
func (n *NetworkInterface) configureInterface(method address.Addressing, link *net.Interface) error {
var err error
discoverErr := method.Discover(context.Background(), link)
// Set link MTU in any case
if err = n.setMTU(method.Link().Index, method.MTU()); err != nil {
return fmt.Errorf("error setting MTU %d on %q: %w", method.MTU(), n.Name, err)
}
if discoverErr != nil {
return discoverErr
}
if method.Address() != nil {
// Check to see if we need to configure the address
var addrs []*net.IPNet
addrs, err = n.rtnlConn.Addrs(method.Link(), method.Family())
if err != nil {
return err
}
addressExists := false
for _, addr := range addrs {
if method.Address().String() == addr.String() {
addressExists = true
break
}
}
if !addressExists && method.Address() != nil {
if err = n.rtnlConn.AddrAdd(method.Link(), method.Address()); err != nil {
switch err := err.(type) {
case *netlink.OpError:
if !os.IsExist(err.Err) && err.Err != syscall.ESRCH {
return fmt.Errorf("error adding address %s on %q: %w", method.Address(), n.Name, err)
}
default:
return fmt.Errorf("failed to add address (already exists) %+v to %s: %v", method.Address(), method.Link().Name, err)
}
}
}
}
// Add any routes
for _, r := range method.Routes() {
// If gateway/router is 0.0.0.0 we'll set to nil so route scope decision will be correct
gw := r.Gateway
if net.IPv4zero.Equal(gw) || net.IPv6zero.Equal(gw) {
gw = nil
}
src := method.Address()
// if destination is the ipv6 default route,and gateway is LL do not pass a src address to set the default geteway
if net.IPv6zero.Equal(r.Destination.IP) && gw.IsLinkLocalUnicast() {
src = nil
}
attr := rtnetlink.RouteAttributes{
Priority: r.Metric,
}
if gw != nil {
attr.Gateway = gw
}
err = n.rtnlConn.RouteAdd(method.Link(), *r.Destination, gw, rtnl.WithRouteSrc(src), rtnl.WithRouteAttrs(attr))
if err != nil {
// ignore "EEXIST" errors for routes which are already present
if opErr, ok := err.(*netlink.OpError); !ok || !os.IsExist(opErr.Err) {
return fmt.Errorf("error adding route %s %s on %q: %s", *r.Destination, gw, n.Name, err)
}
}
}
return nil
}
// Reset removes addressing configuration from a given link.
func (n *NetworkInterface) Reset() {
var (
err error
link *net.Interface
nets []*net.IPNet
)
link, err = net.InterfaceByName(n.Name)
if err != nil {
return
}
if nets, err = n.rtnlConn.Addrs(link, 0); err != nil {
return
}
for _, ipnet := range nets {
if err = n.rtnlConn.AddrDel(link, ipnet); err != nil {
continue
}
}
// nolint: errcheck
n.rtnlConn.LinkDown(link)
}