mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-23 23:51:11 +02:00
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>
448 lines
11 KiB
Go
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)
|
|
}
|