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

254 lines
6.0 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 address
import (
"context"
"fmt"
"log"
"net"
"strings"
"time"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
"github.com/talos-systems/go-procfs/procfs"
"golang.org/x/sys/unix"
"github.com/talos-systems/talos/pkg/machinery/config"
"github.com/talos-systems/talos/pkg/machinery/constants"
)
const dhcpReceivedRouteMetric uint32 = 1024
// DHCP implements the Addressing interface.
type DHCP struct {
Ack *dhcpv4.DHCPv4
NetIf *net.Interface
DHCPOptions config.DHCPOptions
Mtu int
RouteList []config.Route
}
// Name returns back the name of the address method.
func (d *DHCP) Name() string {
return "dhcp"
}
// Link returns the underlying net.Interface that this address
// method is configured for.
func (d *DHCP) Link() *net.Interface {
return d.NetIf
}
// Discover handles the DHCP client exchange stores the DHCP Ack.
func (d *DHCP) Discover(ctx context.Context, link *net.Interface) error {
d.NetIf = link
ack, err := d.discover(ctx)
if ack != nil {
d.Ack = ack
}
return err
}
// Address returns back the IP address from the received DHCP offer.
func (d *DHCP) Address() *net.IPNet {
return &net.IPNet{
IP: d.Ack.YourIPAddr,
Mask: d.Mask(),
}
}
// Mask returns the netmask from the DHCP offer.
func (d *DHCP) Mask() net.IPMask {
return d.Ack.SubnetMask()
}
// MTU returs the MTU size from the DHCP offer.
func (d *DHCP) MTU() uint32 {
mtuReturn := uint32(d.NetIf.MTU)
if d.Ack != nil {
// TODO do we need to implement dhcpv4.GetUint32 upstream?
mtu, err := dhcpv4.GetUint16(dhcpv4.OptionInterfaceMTU, d.Ack.Options)
if err == nil {
mtuReturn = uint32(mtu)
}
}
// override with any non-zero Mtu value passed into the dhcp object
if uint32(d.Mtu) > 0 {
mtuReturn = uint32(d.Mtu)
}
return mtuReturn
}
// TTL denotes how long a DHCP offer is valid for.
func (d *DHCP) TTL() time.Duration {
if d.Ack == nil {
return 0
}
return d.Ack.IPAddressLeaseTime(time.Minute * 30)
}
// Family qualifies the address as ipv4 or ipv6.
func (d *DHCP) Family() int {
if d.Ack.YourIPAddr.To4() != nil {
return unix.AF_INET
}
return unix.AF_INET6
}
// Scope sets the address scope.
func (d *DHCP) Scope() uint8 {
return unix.RT_SCOPE_UNIVERSE
}
// Valid denotes if this address method should be used.
func (d *DHCP) Valid() bool {
return d.Ack != nil
}
// Routes aggregates all Routers and ClasslessStaticRoutes retrieved from
// the DHCP offer.
// rfc3442:
// If the DHCP server returns both a Classless Static Routes option and
// a Router option, the DHCP client MUST ignore the Router option.
func (d *DHCP) Routes() (routes []*Route) {
metric := dhcpReceivedRouteMetric
if d.DHCPOptions != nil && d.DHCPOptions.RouteMetric() != 0 {
metric = d.DHCPOptions.RouteMetric()
}
defRoute := &net.IPNet{
IP: net.IPv4zero,
Mask: net.IPv4Mask(0, 0, 0, 0),
}
for _, router := range d.Ack.Router() {
routes = append(routes, &Route{
Destination: defRoute,
Gateway: router,
Metric: metric,
})
}
// overwrite router option if classless routes were provided.
if len(d.Ack.ClasslessStaticRoute()) > 0 {
routes = []*Route{}
for _, dhcpRoute := range d.Ack.ClasslessStaticRoute() {
routes = append(routes, &Route{
Destination: dhcpRoute.Dest,
Gateway: dhcpRoute.Router,
Metric: metric,
})
}
}
// append any routes that were provided in config
for _, route := range d.RouteList {
_, ipnet, err := net.ParseCIDR(route.Network())
if err != nil {
// TODO: we should at least log this failure
continue
}
routes = append(routes, &Route{
Destination: ipnet,
Gateway: net.ParseIP(route.Gateway()),
Metric: staticRouteDefaultMetric,
})
}
return routes
}
// Resolvers returns the DNS resolvers from the DHCP offer.
func (d *DHCP) Resolvers() []net.IP {
return d.Ack.DNS()
}
// Hostname returns the hostname from the DHCP offer.
func (d *DHCP) Hostname() (hostname string) {
if d.Ack.HostName() == "" {
hostname = fmt.Sprintf("%s-%s", "talos", strings.ReplaceAll(d.Address().IP.String(), ".", "-"))
} else {
hostname = d.Ack.HostName()
}
if d.Ack.DomainName() != "" {
hostname = fmt.Sprintf("%s.%s", strings.Split(hostname, ".")[0], d.Ack.DomainName())
}
return hostname
}
// discover handles the actual DHCP conversation.
func (d *DHCP) discover(ctx context.Context) (*dhcpv4.DHCPv4, error) {
opts := []dhcpv4.OptionCode{
dhcpv4.OptionClasslessStaticRoute,
dhcpv4.OptionDomainNameServer,
dhcpv4.OptionDNSDomainSearchList,
dhcpv4.OptionHostName,
// TODO: handle these options
dhcpv4.OptionNTPServers,
dhcpv4.OptionDomainName,
}
// <3 azure
// When including dhcp.OptionInterfaceMTU we don't get a dhcp offer back on azure.
// So we'll need to explicitly exclude adding this option for azure.
if p := procfs.ProcCmdline().Get(constants.KernelParamPlatform).First(); p != nil {
if *p != "azure" {
opts = append(opts, dhcpv4.OptionInterfaceMTU)
}
}
mods := []dhcpv4.Modifier{dhcpv4.WithRequestedOptions(opts...)}
// TODO expose this ( nclient4.WithDebugLogger() ) with some
// debug logging option
cli, err := nclient4.New(d.NetIf.Name)
if err != nil {
return nil, err
}
// nolint: errcheck
defer cli.Close()
lease, err := cli.Request(ctx, mods...)
if err != nil {
// TODO: Make this a well defined error so we can make it not fatal
log.Printf("failed dhcp request for %q: %v", d.NetIf.Name, err)
return nil, err
}
log.Printf("DHCP ACK on %q: %s", d.NetIf.Name, collapseSummary(lease.ACK.Summary()))
return lease.ACK, err
}
func collapseSummary(summary string) string {
lines := strings.Split(summary, "\n")[1:]
for i := range lines {
lines[i] = strings.TrimSpace(lines[i])
}
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
return strings.Join(lines, ", ")
}