Skip to content

Commit 6c98f4c

Browse files
committed
feat: implement new DHCP network configuration
Fixes #11661 Fixes #10958 This also implement proper client identifier handling. Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
1 parent da92a75 commit 6c98f4c

File tree

43 files changed

+2671
-650
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2671
-650
lines changed

api/lock.binpb

680 Bytes
Binary file not shown.

api/resource/definitions/enums/enums.proto

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ enum NethelpersBondXmitHashPolicy {
118118
BOND_XMIT_POLICY_ENCAP34 = 4;
119119
}
120120

121+
// NethelpersClientIdentifier is a DHCP client identifier.
122+
enum NethelpersClientIdentifier {
123+
CLIENT_IDENTIFIER_NONE = 0;
124+
CLIENT_IDENTIFIER_MAC = 1;
125+
CLIENT_IDENTIFIER_DUID = 2;
126+
}
127+
121128
// NethelpersConntrackState is a conntrack state.
122129
enum NethelpersConntrackState {
123130
NETHELPERS_CONNTRACKSTATE_UNSPECIFIED = 0;

api/resource/definitions/network/network.proto

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,24 @@ message BridgeVLANSpec {
8787
bool filtering_enabled = 1;
8888
}
8989

90+
// ClientIdentifierSpec is a shared DHCP4/DHCP6 client identifier spec.
91+
message ClientIdentifierSpec {
92+
talos.resource.definitions.enums.NethelpersClientIdentifier client_identifier = 1;
93+
string duid_raw_hex = 2;
94+
}
95+
9096
// DHCP4OperatorSpec describes DHCP4 operator options.
9197
message DHCP4OperatorSpec {
9298
uint32 route_metric = 1;
9399
bool skip_hostname_request = 2;
100+
ClientIdentifierSpec client_identifier = 3;
94101
}
95102

96103
// DHCP6OperatorSpec describes DHCP6 operator options.
97104
message DHCP6OperatorSpec {
98-
string duid = 1;
99105
uint32 route_metric = 2;
100106
bool skip_hostname_request = 3;
107+
ClientIdentifierSpec client_identifier = 4;
101108
}
102109

103110
// DNSResolveCacheSpec describes DNS servers status.

cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/common.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,9 @@ func (m *Maker[T]) applyOmniConfigs() error {
274274

275275
func (m *Maker[T]) finalizeMachineConfigs() (*bundle.Bundle, error) {
276276
// These options needs to be generated after the implementing maker has made changes to the cluster request.
277-
m.GenOps = slices.Concat(m.GenOps, m.Provisioner.GenOptions(m.ClusterRequest.Network, m.VersionContract))
277+
provisionGenOps, provisionBundleOps := m.Provisioner.GenOptions(m.ClusterRequest.Network, m.VersionContract)
278+
m.GenOps = slices.Concat(m.GenOps, provisionGenOps)
279+
m.ConfigBundleOps = slices.Concat(m.ConfigBundleOps, provisionBundleOps)
278280
m.GenOps = slices.Concat(m.GenOps, []generate.Option{generate.WithEndpointList(m.Endpoints)})
279281

280282
m.ConfigBundleOps = append(m.ConfigBundleOps,

cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/common_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/cluster/create/clusterops"
1515
"github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers"
1616
"github.com/siderolabs/talos/pkg/machinery/config"
17+
"github.com/siderolabs/talos/pkg/machinery/config/bundle"
1718
"github.com/siderolabs/talos/pkg/machinery/config/encoder"
1819
"github.com/siderolabs/talos/pkg/machinery/config/generate"
1920
"github.com/siderolabs/talos/pkg/machinery/config/generate/secrets"
@@ -24,8 +25,8 @@ type testProvisioner struct {
2425
provision.Provisioner
2526
}
2627

27-
func (p testProvisioner) GenOptions(r provision.NetworkRequest, _ *config.VersionContract) []generate.Option {
28-
return []generate.Option{func(o *generate.Options) error { return nil }}
28+
func (p testProvisioner) GenOptions(r provision.NetworkRequest, _ *config.VersionContract) ([]generate.Option, []bundle.Option) {
29+
return []generate.Option{func(o *generate.Options) error { return nil }}, nil
2930
}
3031

3132
func (p testProvisioner) GetTalosAPIEndpoints(provision.NetworkRequest) []string {

go.mod

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,6 @@ replace (
2323
gopkg.in/yaml.v3 => github.com/unix4ever/yaml v0.0.0-20220527175918-f17b0f05cf2c
2424
)
2525

26-
// fd-leak related replacements: https://github.com/siderolabs/talos/issues/9412
27-
// https://github.com/insomniacslk/dhcp/pull/550
28-
replace github.com/insomniacslk/dhcp => github.com/smira/dhcp v0.0.0-20250407153013-99942baa5d59
29-
3026
// deadcode elimination fix replacement: https://github.com/siderolabs/talos/issues/11296
3127
// upstream PR: https://github.com/containerd/containerd/pull/12175
3228
// this a fork with containerd 2.1 branch + the commit from the PR above
@@ -106,7 +102,7 @@ require (
106102
github.com/hashicorp/go-getter/v2 v2.2.3
107103
github.com/hashicorp/go-multierror v1.1.1
108104
github.com/hetznercloud/hcloud-go/v2 v2.25.1
109-
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905
105+
github.com/insomniacslk/dhcp v0.0.0-20251007151141-da879a2c3546
110106
github.com/jeromer/syslogparser v1.1.0
111107
github.com/jsimonetti/rtnetlink/v2 v2.0.5
112108
github.com/jxskiss/base62 v1.1.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ github.com/hugelgupf/vmtest v0.0.0-20240307030256-5d9f3d34a58d h1:nP8SfQJqruIVSW
371371
github.com/hugelgupf/vmtest v0.0.0-20240307030256-5d9f3d34a58d/go.mod h1:B63hDJMhTupLWCHwopAyEo7wRFowx9kOc8m8j1sfOqE=
372372
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
373373
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
374+
github.com/insomniacslk/dhcp v0.0.0-20251007151141-da879a2c3546 h1:zj09ZbgBgcPaft1b/RftIAT+ALx85M2yzJyOvliRbGQ=
375+
github.com/insomniacslk/dhcp v0.0.0-20251007151141-da879a2c3546/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo=
374376
github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k=
375377
github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
376378
github.com/jeromer/syslogparser v1.1.0 h1:HES0EviO9iPvCu56LjVFVhbM3o0BckDlIbQfkkaRJAw=
@@ -667,8 +669,6 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
667669
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
668670
github.com/smira/containerd/v2 v2.0.0-20250806103510-dcf2fc86e156 h1:vxHt7VLqjFtY3c80Al/RTPAxxu7XVQuTeTNkRZb2AOQ=
669671
github.com/smira/containerd/v2 v2.0.0-20250806103510-dcf2fc86e156/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM=
670-
github.com/smira/dhcp v0.0.0-20250407153013-99942baa5d59 h1:lBQLOP8ZZI6mbePwGX/bqXi3srwhYB/zcwqrnX+JJIc=
671-
github.com/smira/dhcp v0.0.0-20250407153013-99942baa5d59/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
672672
github.com/smira/kobject v0.0.0-20240304111826-49c8d4613389 h1:f/5NRv5IGZxbjBhc5MnlbNmyuXBPxvekhBAUzyKWyLY=
673673
github.com/smira/kobject v0.0.0-20240304111826-49c8d4613389/go.mod h1:+SexPO1ZvdbbWUdUnyXEWv3+4NwHZjKhxOmQqHY4Pqc=
674674
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=

internal/app/machined/pkg/controllers/network/link_config.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ func (ctrl *LinkConfigController) processDevicesConfiguration(
407407
}
408408
}
409409

410+
//nolint:gocyclo
410411
func (ctrl *LinkConfigController) processLinkConfigs(logger *zap.Logger, linkMap map[string]*network.LinkSpecSpec, cfg *config.MachineConfig, linkNameResolver *network.LinkResolver) {
411412
if cfg == nil {
412413
return
@@ -446,6 +447,20 @@ func (ctrl *LinkConfigController) processLinkConfigs(logger *zap.Logger, linkMap
446447
logger.Error("unknown link config type", zap.String("linkName", linkName), zap.String("type", fmt.Sprintf("%T", specificLinkConfig)))
447448
}
448449
}
450+
451+
// if we have DHCP config, bring up the link implicitly if it hasn't been configured yet
452+
for _, dhcpConfig := range cfg.Config().NetworkDHCPConfigs() {
453+
linkName := dhcpConfig.Name()
454+
linkName = linkNameResolver.Resolve(linkName)
455+
456+
if _, exists := linkMap[linkName]; !exists {
457+
linkMap[linkName] = &network.LinkSpecSpec{
458+
Name: linkName,
459+
Up: true,
460+
ConfigLayer: network.ConfigMachineConfiguration,
461+
}
462+
}
463+
}
449464
}
450465

451466
type vlaner interface {
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package operator
6+
7+
import (
8+
"context"
9+
"encoding/hex"
10+
"fmt"
11+
"net"
12+
13+
"github.com/cosi-project/runtime/pkg/safe"
14+
"github.com/cosi-project/runtime/pkg/state"
15+
"github.com/insomniacslk/dhcp/dhcpv4"
16+
"github.com/insomniacslk/dhcp/dhcpv6"
17+
"github.com/insomniacslk/dhcp/iana"
18+
"go.uber.org/zap"
19+
20+
"github.com/siderolabs/talos/pkg/machinery/nethelpers"
21+
"github.com/siderolabs/talos/pkg/machinery/resources/network"
22+
)
23+
24+
// GetDHCPv6ClientIdentifier returns the DHCPv6 client identifier to use.
25+
func GetDHCPv6ClientIdentifier(ctx context.Context, st state.State, logger *zap.Logger, linkName string, spec network.ClientIdentifierSpec) ([]dhcpv6.Modifier, error) {
26+
switch spec.ClientIdentifier {
27+
case nethelpers.ClientIdentifierNone:
28+
return nil, nil //nolint:nilerr
29+
case nethelpers.ClientIdentifierMAC:
30+
link, err := safe.StateGetByID[*network.LinkStatus](ctx, st, linkName)
31+
if err != nil {
32+
return nil, fmt.Errorf("error getting link %q: %w", linkName, err)
33+
}
34+
35+
if len(link.TypedSpec().HardwareAddr) == 0 {
36+
return nil, fmt.Errorf("link %q has no hardware address", linkName)
37+
}
38+
39+
duid := dhcpv6.DUIDLL{
40+
HWType: iana.HWTypeEthernet,
41+
LinkLayerAddr: net.HardwareAddr(link.TypedSpec().HardwareAddr),
42+
}
43+
44+
return []dhcpv6.Modifier{dhcpv6.WithClientID(&duid)}, nil
45+
case nethelpers.ClientIdentifierDUID:
46+
if spec.DUIDRawHex == "" {
47+
return nil, fmt.Errorf("duidRawHex must be set when clientIdentifier is DUID")
48+
}
49+
50+
duidBin, err := hex.DecodeString(spec.DUIDRawHex)
51+
if err != nil {
52+
logger.Error("failed to parse DUID, ignored", zap.String("link", linkName))
53+
54+
return nil, nil //nolint:nilerr
55+
}
56+
57+
duid, err := dhcpv6.DUIDFromBytes(duidBin)
58+
if err != nil {
59+
logger.Error("failed to parse DUID, ignored", zap.String("link", linkName))
60+
61+
return nil, nil //nolint:nilerr
62+
}
63+
64+
return []dhcpv6.Modifier{dhcpv6.WithClientID(duid)}, nil
65+
default:
66+
return nil, fmt.Errorf("unknown client identifier %d", spec.ClientIdentifier)
67+
}
68+
}
69+
70+
// GetDHCP4ClientIdentifier returns the DHCP client identifier to use.
71+
func GetDHCP4ClientIdentifier(ctx context.Context, st state.State, logger *zap.Logger, linkName string, spec network.ClientIdentifierSpec) ([]dhcpv4.Modifier, error) {
72+
switch spec.ClientIdentifier {
73+
case nethelpers.ClientIdentifierNone:
74+
return nil, nil //nolint:nilerr
75+
case nethelpers.ClientIdentifierMAC:
76+
link, err := safe.StateGetByID[*network.LinkStatus](ctx, st, linkName)
77+
if err != nil {
78+
return nil, fmt.Errorf("error getting link %q: %w", linkName, err)
79+
}
80+
81+
if len(link.TypedSpec().HardwareAddr) == 0 {
82+
return nil, fmt.Errorf("link %q has no hardware address", linkName)
83+
}
84+
85+
// per RFC 2132, section 9.14
86+
identifier := append([]byte{byte(iana.HWTypeEthernet)}, link.TypedSpec().HardwareAddr...)
87+
88+
return []dhcpv4.Modifier{dhcpv4.WithOption(dhcpv4.OptClientIdentifier(identifier))}, nil
89+
case nethelpers.ClientIdentifierDUID:
90+
if spec.DUIDRawHex == "" {
91+
return nil, fmt.Errorf("duidRawHex must be set when clientIdentifier is DUID")
92+
}
93+
94+
duidBin, err := hex.DecodeString(spec.DUIDRawHex)
95+
if err != nil {
96+
logger.Error("failed to parse DUID, ignored", zap.String("link", linkName))
97+
98+
return nil, nil //nolint:nilerr
99+
}
100+
101+
_, err = dhcpv6.DUIDFromBytes(duidBin)
102+
if err != nil {
103+
logger.Error("failed to parse DUID, ignored", zap.String("link", linkName))
104+
105+
return nil, nil //nolint:nilerr
106+
}
107+
108+
return []dhcpv4.Modifier{dhcpv4.WithOption(dhcpv4.OptClientIdentifier(duidBin))}, nil
109+
default:
110+
return nil, fmt.Errorf("unknown client identifier %d", spec.ClientIdentifier)
111+
}
112+
}

internal/app/machined/pkg/controllers/network/operator/dhcp4.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type DHCP4 struct {
3535

3636
linkName string
3737
routeMetric uint32
38+
clientIdentifier network.ClientIdentifierSpec
3839
skipHostnameRequest bool
3940
requestMTU bool
4041

@@ -57,6 +58,7 @@ func NewDHCP4(logger *zap.Logger, linkName string, config network.DHCP4OperatorS
5758
linkName: linkName,
5859
routeMetric: config.RouteMetric,
5960
skipHostnameRequest: config.SkipHostnameRequest,
61+
clientIdentifier: config.ClientIdentifier,
6062
// <3 azure
6163
// When including dhcp.OptionInterfaceMTU we don't get a dhcp offer back on azure.
6264
// So we'll need to explicitly exclude adding this option for azure.
@@ -522,7 +524,7 @@ func (d *DHCP4) requestRenew(ctx context.Context, hostname network.HostnameStatu
522524
opts = append(opts, dhcpv4.OptionHostName, dhcpv4.OptionDomainName)
523525
}
524526

525-
mods := []dhcpv4.Modifier{dhcpv4.WithRequestedOptions(opts...), WithNumSecunds(secs)}
527+
mods := []dhcpv4.Modifier{dhcpv4.WithRequestedOptions(opts...), WithNumSeconds(secs)}
526528

527529
if !sendHostnameRequest {
528530
// If the node has a hostname, always send it to the DHCP
@@ -536,6 +538,13 @@ func (d *DHCP4) requestRenew(ctx context.Context, hostname network.HostnameStatu
536538
}
537539
}
538540

541+
clientIdentifierModes, err := GetDHCP4ClientIdentifier(ctx, d.state, d.logger, d.linkName, d.clientIdentifier)
542+
if err != nil {
543+
return 0, fmt.Errorf("failed to get client identifier: %w", err)
544+
}
545+
546+
mods = append(mods, clientIdentifierModes...)
547+
539548
client, err := d.newClient()
540549
if err != nil {
541550
return 0, err
@@ -594,8 +603,8 @@ func collapseSummary(summary string) string {
594603
return strings.Join(lines, ", ")
595604
}
596605

597-
// WithNumSecunds sets the secs field of a DHCPv4 packet.
598-
func WithNumSecunds(secs uint16) dhcpv4.Modifier {
606+
// WithNumSeconds sets the secs field of a DHCPv4 packet.
607+
func WithNumSeconds(secs uint16) dhcpv4.Modifier {
599608
return func(d *dhcpv4.DHCPv4) {
600609
d.NumSeconds = secs
601610
}

0 commit comments

Comments
 (0)