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 |