From 45c5b47a57c0c7efdc126f24f880238b9aec9781 Mon Sep 17 00:00:00 2001 From: Dennis Marttinen Date: Mon, 27 Mar 2023 22:54:42 +0300 Subject: [PATCH] feat: dhcpv4: send current hostname, fix spec compliance of renewals This adds support for automatically registering node hostnames in DNS by sending the current hostname to DHCP via option 12. If the current hostname is updated, issue a new DISCOVER to propagate the update to DHCP (updating the hostname on lease renewals is not universally supported by DHCP servers). This addition maintains the previous functionality where the node can also request its hostname from the DHCP server. The received hostname will be processed and prioritized as usual by the `network.HostnameSpecController`. This change set also contains fixes to make DHCP renewals compliant with RFC 2131, specifically avoiding sending the server identifier and requested IP address when issuing renewals using a previous offer. This also uncovered issues and missing features in the upstream `insomniacslk/dhcp` library, the fixes and improvements for which are now finally merged. Sending hostname updates have been tested against `dnsmasq` and the built-in DHCP + DNS services in Windows Server. Hostname retrieval from DHCP and edge cases with overridden hostnames from different configuration layers have been extensively tested against `dnsmasq`. Signed-off-by: Dennis Marttinen Signed-off-by: Andrey Smirnov --- .../definitions/network/network.proto | 4 +- go.mod | 2 +- go.sum | 4 +- .../pkg/controllers/network/operator/dhcp4.go | 348 ++++++++++++++---- .../pkg/controllers/network/operator_spec.go | 4 +- .../definitions/network/network.pb.go | 4 +- pkg/machinery/resources/network/condition.go | 2 +- .../resources/network/hostname_spec.go | 2 +- .../resources/network/hostname_status.go | 2 +- .../resources/network/operator_spec.go | 8 + pkg/provision/providers/vm/dhcpd.go | 16 +- website/content/v1.4/reference/api.md | 4 +- 12 files changed, 315 insertions(+), 85 deletions(-) diff --git a/api/resource/definitions/network/network.proto b/api/resource/definitions/network/network.proto index 083e95620e..b9575e5d50 100755 --- a/api/resource/definitions/network/network.proto +++ b/api/resource/definitions/network/network.proto @@ -96,14 +96,14 @@ message HardwareAddrSpec { bytes hardware_addr = 2; } -// HostnameSpecSpec describes node nostname. +// HostnameSpecSpec describes node hostname. message HostnameSpecSpec { string hostname = 1; string domainname = 2; talos.resource.definitions.enums.NetworkConfigLayer config_layer = 3; } -// HostnameStatusSpec describes node nostname. +// HostnameStatusSpec describes node hostname. message HostnameStatusSpec { string hostname = 1; string domainname = 2; diff --git a/go.mod b/go.mod index f0962c5a84..d25f8e7ad2 100644 --- a/go.mod +++ b/go.mod @@ -65,7 +65,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.6.0 github.com/hetznercloud/hcloud-go v1.41.0 - github.com/insomniacslk/dhcp v0.0.0-20230307103557-e252950ab961 + github.com/insomniacslk/dhcp v0.0.0-20230327135226-74ae03f2425e github.com/jsimonetti/rtnetlink v1.3.1 github.com/jxskiss/base62 v1.1.0 github.com/martinlindhe/base36 v1.1.1 diff --git a/go.sum b/go.sum index 46f46538e1..a03281c46e 100644 --- a/go.sum +++ b/go.sum @@ -813,8 +813,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/insomniacslk/dhcp v0.0.0-20230307103557-e252950ab961 h1:x/YtdDlmypenG1te/FfH6LVM+3krhXk5CFV8VYNNX5M= -github.com/insomniacslk/dhcp v0.0.0-20230307103557-e252950ab961/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI= +github.com/insomniacslk/dhcp v0.0.0-20230327135226-74ae03f2425e h1:8ChxkWKTVYg7LKBvYNLNRnlobgbPrzzossZUoST2T7o= +github.com/insomniacslk/dhcp v0.0.0-20230327135226-74ae03f2425e/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= diff --git a/internal/app/machined/pkg/controllers/network/operator/dhcp4.go b/internal/app/machined/pkg/controllers/network/operator/dhcp4.go index e0fff4eb0f..eb2677340e 100644 --- a/internal/app/machined/pkg/controllers/network/operator/dhcp4.go +++ b/internal/app/machined/pkg/controllers/network/operator/dhcp4.go @@ -14,8 +14,11 @@ import ( "sync" "time" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/nclient4" + "github.com/siderolabs/gen/channel" "github.com/siderolabs/gen/slices" "go.uber.org/zap" "go4.org/netipx" @@ -28,13 +31,14 @@ import ( // DHCP4 implements the DHCPv4 network operator. type DHCP4 struct { logger *zap.Logger + state state.State linkName string routeMetric uint32 skipHostnameRequest bool requestMTU bool - offer *dhcpv4.DHCPv4 + lease *nclient4.Lease mu sync.Mutex addresses []network.AddressSpecSpec @@ -46,9 +50,10 @@ type DHCP4 struct { } // NewDHCP4 creates DHCPv4 operator. -func NewDHCP4(logger *zap.Logger, linkName string, config network.DHCP4OperatorSpec, platform runtime.Platform) *DHCP4 { +func NewDHCP4(logger *zap.Logger, linkName string, config network.DHCP4OperatorSpec, platform runtime.Platform, state state.State) *DHCP4 { return &DHCP4{ logger: logger, + state: state, linkName: linkName, routeMetric: config.RouteMetric, skipHostnameRequest: config.SkipHostnameRequest, @@ -64,24 +69,129 @@ func (d *DHCP4) Prefix() string { return fmt.Sprintf("dhcp4/%s", d.linkName) } +// extractHostname extracts a hostname from the given resource if it is a valid network.HostnameStatus. +func extractHostname(res resource.Resource) network.HostnameStatusSpec { + if res, ok := res.(*network.HostnameStatus); ok { + return *res.TypedSpec() + } + + return network.HostnameStatusSpec{} +} + +// setupHostnameWatch returns the initial hostname and a channel that outputs all events related to hostname changes. +func (d *DHCP4) setupHostnameWatch(ctx context.Context) (network.HostnameStatusSpec, <-chan state.Event, error) { + hostnameWatchCh := make(chan state.Event) + if err := d.state.Watch(ctx, resource.NewMetadata( + network.NamespaceName, + network.HostnameStatusType, + network.HostnameID, + resource.VersionUndefined, + ), hostnameWatchCh); err != nil { + return network.HostnameStatusSpec{}, nil, err + } + + return extractHostname((<-hostnameWatchCh).Resource), hostnameWatchCh, nil +} + +// knownHostname checks if the given hostname has been defined by this operator. +func (d *DHCP4) knownHostname(hostname network.HostnameStatusSpec) bool { + for i := range d.hostname { + if d.hostname[i].FQDN() == hostname.FQDN() { + return true + } + } + + return false +} + +// waitForNetworkReady waits for the network to be ready and the leased address to +// be assigned to the associated so that unicast operations can bind successfully. +func (d *DHCP4) waitForNetworkReady(ctx context.Context) error { + // If an IP address has been registered, wait for the address association to be ready + if len(d.addresses) > 0 { + _, err := d.state.WatchFor(ctx, + resource.NewMetadata( + network.NamespaceName, + network.AddressStatusType, + network.AddressID(d.linkName, d.addresses[0].Address), + resource.VersionUndefined, + ), + state.WithPhases(resource.PhaseRunning), + ) + if err != nil { + return fmt.Errorf("failed to wait for the address association to be ready: %w", err) + } + } + + // Wait for the network (address and connectivity) to be ready + if err := network.NewReadyCondition(d.state, network.AddressReady, network.ConnectivityReady).Wait(ctx); err != nil { + return fmt.Errorf("failed to wait for the network address and connectivity to be ready: %w", err) + } + + return nil +} + // Run the operator loop. // -//nolint:gocyclo,dupl +//nolint:gocyclo,cyclop,dupl func (d *DHCP4) Run(ctx context.Context, notifyCh chan<- struct{}) { - const minRenewDuration = 5 * time.Second // protect from renewing too often + const minRenewDuration = 5 * time.Second // Protect from renewing too often renewInterval := minRenewDuration + hostname, hostnameWatchCh, err := d.setupHostnameWatch(ctx) + if err != nil && !errors.Is(err, context.Canceled) { + d.logger.Warn("failed to watch for hostname changes", zap.Error(err)) + } + for { - leaseTime, err := d.renew(ctx) + // Track if we need to acquire a new lease + newLease := d.lease == nil + + // Perform a lease request or renewal + leaseTime, err := d.requestRenew(ctx, hostname) if err != nil && !errors.Is(err, context.Canceled) { - d.logger.Warn("renew failed", zap.Error(err), zap.String("link", d.linkName)) + d.logger.Warn("request/renew failed", zap.Error(err), zap.String("link", d.linkName)) } - if err == nil { - select { - case notifyCh <- struct{}{}: - case <-ctx.Done(): + if err == nil && newLease { + // Notify the underlying controller about the new lease + if !channel.SendWithContext(ctx, notifyCh, struct{}{}) { + return + } + + if !d.skipHostnameRequest { + // Wait for networking to be established before transitioning to unicast operations + if err = d.waitForNetworkReady(ctx); err != nil && !errors.Is(err, context.Canceled) { + d.logger.Warn("failed to wait for networking to become ready", zap.Error(err)) + } + } + } + + // DHCP hostname parroting protection: if, e.g., `dnsmasq` receives a request that both + // sends a hostname and requests one, it will "parrot" the sent hostname back if no other + // name has been defined for the requesting host. This causes update anomalies, since + // removing a hostname defined previously by, e.g., the configuration layer, causes a copy + // of that hostname to live on in a spec defined by this operator, even though it isn't + // sourced from DHCP. + // + // To avoid this issue, never send and request a hostname in the same operation. When + // negotiating a new lease, first send the current hostname when acquiring the lease, and + // then follow up with a dedicated INFORM request asking the server for a DHCP-defined + // hostname. When renewing a lease, we're free to always request a hostname with an INFORM + // (to detect server-side changes), since any changes to the node hostname will cause a + // lease invalidation and re-start the negotiation process. More details below. + if err == nil && !d.skipHostnameRequest { + // Request the node hostname from the DHCP server + err = d.requestHostname(ctx) + if err != nil && !errors.Is(err, context.Canceled) { + d.logger.Warn("hostname request failed", zap.Error(err), zap.String("link", d.linkName)) + } + } + + // Notify the underlying controller about the received hostname and/or renewed lease + if err == nil && (!d.skipHostnameRequest || !newLease) { + if !channel.SendWithContext(ctx, notifyCh, struct{}{}) { return } } @@ -96,10 +206,64 @@ func (d *DHCP4) Run(ctx context.Context, notifyCh chan<- struct{}) { renewInterval = minRenewDuration } - select { - case <-ctx.Done(): - return - case <-time.After(renewInterval): + for { + select { + case <-ctx.Done(): + return + case <-time.After(renewInterval): + case event := <-hostnameWatchCh: + // Attempt to drain the hostname watch channel coalescing multiple events into a single + // change to the DHCP. + drainLoop: + for { + select { + case event = <-hostnameWatchCh: + case <-ctx.Done(): + return + case <-time.After(time.Second): + break drainLoop + } + } + + // If the hostname resource was deleted entirely, we must still inform the DHCP + // server that the node has no hostname anymore. `extractHostname` will return a + // blank hostname for a Tombstone resource generated by a deletion event. + oldHostname := hostname + hostname = extractHostname(event.Resource) + + d.logger.Debug("detected hostname change", + zap.String("old", oldHostname.FQDN()), + zap.String("new", hostname.FQDN()), + ) + + // If, on first invocation, the DHCP server has given a new hostname for the node, + // and the `network.HostnameSpecController` decides to apply it as a preferred + // hostname, this operator would unnecessarily drop the lease and restart DHCP + // discovery. Thus, if the selected hostname has been sourced from this operator, + // we don't need to do anything. + if (oldHostname == network.HostnameStatusSpec{} && d.knownHostname(hostname)) || oldHostname == hostname { + continue + } + + // While updating the hostname together with a RENEW request works with dnsmasq, it + // doesn't work with the Windows Server DHCP + DNS. A hostname update via an + // INIT-REBOOT request also gets ignored. Thus, the only reliable way to update the + // hostname seems to be to forget the old release and initiate a new DISCOVER flow + // with the new hostname. RFC 2131 doesn't define any better way to do this, and, + // as a DISCOVER request cannot be targeted at the previous lessor according to the + // spec, the node may switch DHCP servers on hostname change. However, this is not + // a major concern, since a single network should not host multiple competing DHCP + // servers in the first place. + d.lease = nil + + d.logger.Debug("restarting DHCP sequence due to hostname change", + zap.Strings("dhcp_hostname", slices.Map(d.hostname, func(spec network.HostnameSpecSpec) string { + return spec.Hostname + })), + ) + } + + break } } } @@ -152,8 +316,31 @@ func (d *DHCP4) TimeServerSpecs() []network.TimeServerSpecSpec { return d.timeservers } +func (d *DHCP4) parseHostnameFromAck(ack *dhcpv4.DHCPv4) { + d.mu.Lock() + defer d.mu.Unlock() + + d.hostname = nil + + if ack.HostName() != "" { + spec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigOperator, + } + + if err := spec.ParseFQDN(ack.HostName()); err == nil { + if ack.DomainName() != "" { + spec.Domainname = ack.DomainName() + } + + d.hostname = []network.HostnameSpecSpec{ + spec, + } + } + } +} + //nolint:gocyclo -func (d *DHCP4) parseAck(ack *dhcpv4.DHCPv4) { +func (d *DHCP4) parseNetworkConfigFromAck(ack *dhcpv4.DHCPv4) { d.mu.Lock() defer d.mu.Unlock() @@ -228,7 +415,7 @@ func (d *DHCP4) parseAck(ack *dhcpv4.DHCPv4) { }) if !addr.Contains(gw) { - // add an interface route for the gateway if it's not in the same network + // Add an interface route for the gateway if it's not in the same network d.routes = append(d.routes, network.RouteSpecSpec{ Family: nethelpers.FamilyInet4, Destination: netip.PrefixFrom(gw, gw.BitLen()), @@ -266,26 +453,6 @@ func (d *DHCP4) parseAck(ack *dhcpv4.DHCPv4) { d.resolvers = nil } - if ack.HostName() != "" && !d.skipHostnameRequest { - spec := network.HostnameSpecSpec{ - ConfigLayer: network.ConfigOperator, - } - - if err = spec.ParseFQDN(ack.HostName()); err == nil { - if ack.DomainName() != "" { - spec.Domainname = ack.DomainName() - } - - d.hostname = []network.HostnameSpecSpec{ - spec, - } - } else { - d.hostname = nil - } - } else { - d.hostname = nil - } - if len(ack.NTPServers()) > 0 { convertIP := func(ip net.IP) string { result, _ := netipx.FromStdIP(ip) @@ -304,17 +471,71 @@ func (d *DHCP4) parseAck(ack *dhcpv4.DHCPv4) { } } -func (d *DHCP4) renew(ctx context.Context) (time.Duration, error) { +func (d *DHCP4) newClient() (*nclient4.Client, error) { + var clientOpts []nclient4.ClientOpt + + // We have an existing lease, target the server with unicast + if d.lease != nil { + // RFC 2131, section 4.3.2: + // DHCPREQUEST generated during RENEWING state: + // ... This message will be unicast, so no relay + // agents will be involved in its transmission. + clientOpts = append(clientOpts, + nclient4.WithServerAddr(&net.UDPAddr{ + IP: d.lease.ACK.ServerIPAddr, + Port: nclient4.ServerPort, + }), + // WithUnicast must be specified manually, WithServerAddr is not enough + nclient4.WithUnicast(&net.UDPAddr{ + IP: d.lease.ACK.YourIPAddr, + Port: nclient4.ClientPort, + }), + ) + } + + // Create a new client, the caller is responsible for closing it + return nclient4.New(d.linkName, clientOpts...) +} + +// requestHostname uses an INFORM request to request a hostname from the DHCP server, as requesting +// it during a DISCOVER, when simultaneously sending the local hostname, is not reliable (see above). +func (d *DHCP4) requestHostname(ctx context.Context) error { + opts := []dhcpv4.OptionCode{ + dhcpv4.OptionHostName, + dhcpv4.OptionDomainName, + } + + client, err := d.newClient() + if err != nil { + return err + } + + //nolint:errcheck + defer client.Close() + + d.logger.Debug("DHCP INFORM", zap.String("link", d.linkName)) + + // Send an INFORM request for querying a hostname from the DHCP server + ack, err := client.Inform(ctx, d.lease.ACK.YourIPAddr, dhcpv4.WithRequestedOptions(opts...)) + if err != nil { + return err + } + + d.logger.Debug("DHCP ACK", zap.String("link", d.linkName), zap.String("dhcp", collapseSummary(ack.Summary()))) + + // Parse the hostname from the response + d.parseHostnameFromAck(ack) + + return nil +} + +func (d *DHCP4) requestRenew(ctx context.Context, hostname network.HostnameStatusSpec) (time.Duration, error) { opts := []dhcpv4.OptionCode{ dhcpv4.OptionClasslessStaticRoute, dhcpv4.OptionDomainNameServer, + // TODO(twelho): This is unused until network.ResolverSpec supports search domains dhcpv4.OptionDNSDomainSearchList, dhcpv4.OptionNTPServers, - dhcpv4.OptionDomainName, - } - - if !d.skipHostnameRequest { - opts = append(opts, dhcpv4.OptionHostName) } if d.requestMTU { @@ -322,50 +543,45 @@ func (d *DHCP4) renew(ctx context.Context) (time.Duration, error) { } mods := []dhcpv4.Modifier{dhcpv4.WithRequestedOptions(opts...)} - clientOpts := []nclient4.ClientOpt{} - - if d.offer != nil { - // do not use broadcast, but send the packet to DHCP server directly - addr, err := net.ResolveUDPAddr("udp", d.offer.ServerIPAddr.String()+":67") - if err != nil { - return 0, err - } - // by default it's set to 0.0.0.0 which actually breaks lease renew - d.offer.ClientIPAddr = d.offer.YourIPAddr + // If the node has a hostname, always send it to the DHCP + // server with option 12 during lease acquisition and renewal + if len(hostname.Hostname) > 0 { + mods = append(mods, dhcpv4.WithOption(dhcpv4.OptHostName(hostname.Hostname))) + } - clientOpts = append(clientOpts, nclient4.WithServerAddr(addr)) + if len(hostname.Domainname) > 0 { + mods = append(mods, dhcpv4.WithOption(dhcpv4.OptDomainName(hostname.Domainname))) } - cli, err := nclient4.New(d.linkName, clientOpts...) + client, err := d.newClient() if err != nil { return 0, err } //nolint:errcheck - defer cli.Close() - - var lease *nclient4.Lease + defer client.Close() - if d.offer != nil { - lease, err = cli.RequestFromOffer(ctx, d.offer, mods...) + if d.lease != nil { + d.logger.Debug("DHCP RENEW", zap.String("link", d.linkName)) + d.lease, err = client.Renew(ctx, d.lease, mods...) } else { - lease, err = cli.Request(ctx, mods...) + d.logger.Debug("DHCP REQUEST", zap.String("link", d.linkName)) + d.lease, err = client.Request(ctx, mods...) } if err != nil { - // clear offer if request fails to start with discover sequence next time - d.offer = nil + // explicitly clear the lease on failure to start with the discovery sequence next time + d.lease = nil return 0, err } - d.logger.Debug("DHCP ACK", zap.String("link", d.linkName), zap.String("dhcp", collapseSummary(lease.ACK.Summary()))) + d.logger.Debug("DHCP ACK", zap.String("link", d.linkName), zap.String("dhcp", collapseSummary(d.lease.ACK.Summary()))) - d.offer = lease.Offer - d.parseAck(lease.ACK) + d.parseNetworkConfigFromAck(d.lease.ACK) - return lease.ACK.IPAddressLeaseTime(time.Minute * 30), nil + return d.lease.ACK.IPAddressLeaseTime(time.Minute * 30), nil } func collapseSummary(summary string) string { diff --git a/internal/app/machined/pkg/controllers/network/operator_spec.go b/internal/app/machined/pkg/controllers/network/operator_spec.go index 984e79538b..91c06db96c 100644 --- a/internal/app/machined/pkg/controllers/network/operator_spec.go +++ b/internal/app/machined/pkg/controllers/network/operator_spec.go @@ -234,7 +234,7 @@ func (ctrl *OperatorSpecController) reconcileOperators(ctx context.Context, r co // stop operator ctrl.operators[id].Stop() delete(ctrl.operators, id) - } else if *shouldRun[id] != ctrl.operators[id].Spec { + } else if !ctrl.operators[id].Spec.Equal(*shouldRun[id]) { logger.Debug("replacing operator", zap.String("operator", id)) // stop operator @@ -423,7 +423,7 @@ func (ctrl *OperatorSpecController) newOperator(logger *zap.Logger, spec *networ case network.OperatorDHCP4: logger = logger.With(zap.String("operator", "dhcp4")) - return operator.NewDHCP4(logger, spec.LinkName, spec.DHCP4, ctrl.V1alpha1Platform) + return operator.NewDHCP4(logger, spec.LinkName, spec.DHCP4, ctrl.V1alpha1Platform, ctrl.State) case network.OperatorDHCP6: logger = logger.With(zap.String("operator", "dhcp6")) diff --git a/pkg/machinery/api/resource/definitions/network/network.pb.go b/pkg/machinery/api/resource/definitions/network/network.pb.go index e4e32e6c4d..43f5e3d909 100644 --- a/pkg/machinery/api/resource/definitions/network/network.pb.go +++ b/pkg/machinery/api/resource/definitions/network/network.pb.go @@ -801,7 +801,7 @@ func (x *HardwareAddrSpec) GetHardwareAddr() []byte { return nil } -// HostnameSpecSpec describes node nostname. +// HostnameSpecSpec describes node hostname. type HostnameSpecSpec struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -865,7 +865,7 @@ func (x *HostnameSpecSpec) GetConfigLayer() enums.NetworkConfigLayer { return enums.NetworkConfigLayer(0) } -// HostnameStatusSpec describes node nostname. +// HostnameStatusSpec describes node hostname. type HostnameStatusSpec struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache diff --git a/pkg/machinery/resources/network/condition.go b/pkg/machinery/resources/network/condition.go index e12c5169ad..93a691c083 100644 --- a/pkg/machinery/resources/network/condition.go +++ b/pkg/machinery/resources/network/condition.go @@ -19,7 +19,7 @@ type ReadyCondition struct { checks []StatusCheck } -// NewReadyCondition builds a coondition which waits for the network to be ready. +// NewReadyCondition builds a condition which waits for the network to be ready. func NewReadyCondition(state state.State, checks ...StatusCheck) *ReadyCondition { return &ReadyCondition{ state: state, diff --git a/pkg/machinery/resources/network/hostname_spec.go b/pkg/machinery/resources/network/hostname_spec.go index d9ba855578..4776e35443 100644 --- a/pkg/machinery/resources/network/hostname_spec.go +++ b/pkg/machinery/resources/network/hostname_spec.go @@ -25,7 +25,7 @@ type HostnameSpec = typed.Resource[HostnameSpecSpec, HostnameSpecExtension] // HostnameID is the ID of the singleton instance. const HostnameID resource.ID = "hostname" -// HostnameSpecSpec describes node nostname. +// HostnameSpecSpec describes node hostname. // //gotagsrewrite:gen type HostnameSpecSpec struct { diff --git a/pkg/machinery/resources/network/hostname_status.go b/pkg/machinery/resources/network/hostname_status.go index 9dcf3236e6..f35d7335a1 100644 --- a/pkg/machinery/resources/network/hostname_status.go +++ b/pkg/machinery/resources/network/hostname_status.go @@ -19,7 +19,7 @@ const HostnameStatusType = resource.Type("HostnameStatuses.net.talos.dev") // HostnameStatus resource holds node hostname. type HostnameStatus = typed.Resource[HostnameStatusSpec, HostnameStatusExtension] -// HostnameStatusSpec describes node nostname. +// HostnameStatusSpec describes node hostname. // //gotagsrewrite:gen type HostnameStatusSpec struct { diff --git a/pkg/machinery/resources/network/operator_spec.go b/pkg/machinery/resources/network/operator_spec.go index d3fdd06ab2..bb3a6635fe 100644 --- a/pkg/machinery/resources/network/operator_spec.go +++ b/pkg/machinery/resources/network/operator_spec.go @@ -36,6 +36,14 @@ type OperatorSpecSpec struct { ConfigLayer ConfigLayer `yaml:"layer" protobuf:"7"` } +// Equal implements equality check for OperatorSpecSpec. +func (spec OperatorSpecSpec) Equal(other OperatorSpecSpec) bool { + // config layer is not important for equality check + spec.ConfigLayer = other.ConfigLayer + + return spec == other +} + // DHCP4OperatorSpec describes DHCP4 operator options. // //gotagsrewrite:gen diff --git a/pkg/provision/providers/vm/dhcpd.go b/pkg/provision/providers/vm/dhcpd.go index d6a4336042..1544e85cac 100644 --- a/pkg/provision/providers/vm/dhcpd.go +++ b/pkg/provision/providers/vm/dhcpd.go @@ -30,7 +30,7 @@ import ( //nolint:gocyclo func handlerDHCP4(serverIP net.IP, statePath string) server4.Handler { return func(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) { - log.Printf("DHCPv6: got %s", m.Summary()) + log.Printf("DHCPv4: got %s", m.Summary()) if m.OpCode != dhcpv4.OpcodeBootRequest { return @@ -62,15 +62,19 @@ func handlerDHCP4(serverIP net.IP, statePath string) server4.Handler { } modifiers := []dhcpv4.Modifier{ + dhcpv4.WithServerIP(serverIP), dhcpv4.WithNetmask(net.CIDRMask(int(match.Netmask), match.IP.BitLen())), dhcpv4.WithYourIP(match.IP.AsSlice()), - dhcpv4.WithOption(dhcpv4.OptDNS(netipAddrsToIPs(match.Nameservers)...)), dhcpv4.WithOption(dhcpv4.OptRouter(match.Gateway.AsSlice())), dhcpv4.WithOption(dhcpv4.OptIPAddressLeaseTime(5 * time.Minute)), dhcpv4.WithOption(dhcpv4.OptServerIdentifier(serverIP)), } - if match.Hostname != "" { + if m.IsOptionRequested(dhcpv4.OptionDomainNameServer) { + modifiers = append(modifiers, dhcpv4.WithOption(dhcpv4.OptDNS(netipAddrsToIPs(match.Nameservers)...))) + } + + if match.Hostname != "" && m.IsOptionRequested(dhcpv4.OptionHostName) { modifiers = append(modifiers, dhcpv4.WithOption(dhcpv4.OptHostName(match.Hostname)), ) @@ -97,12 +101,14 @@ func handlerDHCP4(serverIP net.IP, statePath string) server4.Handler { } } - resp.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionInterfaceMTU, dhcpv4.Uint16(match.MTU).ToBytes())) + if m.IsOptionRequested(dhcpv4.OptionInterfaceMTU) { + resp.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionInterfaceMTU, dhcpv4.Uint16(match.MTU).ToBytes())) + } switch mt := m.MessageType(); mt { //nolint:exhaustive case dhcpv4.MessageTypeDiscover: resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer)) - case dhcpv4.MessageTypeRequest: + case dhcpv4.MessageTypeRequest, dhcpv4.MessageTypeInform: resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) default: log.Printf("unhandled message type: %v", mt) diff --git a/website/content/v1.4/reference/api.md b/website/content/v1.4/reference/api.md index 78bd02a047..6b06aadba2 100644 --- a/website/content/v1.4/reference/api.md +++ b/website/content/v1.4/reference/api.md @@ -2567,7 +2567,7 @@ HardwareAddrSpec describes spec for the link. ### HostnameSpecSpec -HostnameSpecSpec describes node nostname. +HostnameSpecSpec describes node hostname. | Field | Type | Label | Description | @@ -2584,7 +2584,7 @@ HostnameSpecSpec describes node nostname. ### HostnameStatusSpec -HostnameStatusSpec describes node nostname. +HostnameStatusSpec describes node hostname. | Field | Type | Label | Description |