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 |