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>
254 lines
6.0 KiB
Go
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, ", ")
|
|
}
|