From 2a65ce43e7e8cc38dc213fed287f1b8574e96ee3 Mon Sep 17 00:00:00 2001 From: Damyan Yordanov Date: Mon, 18 Nov 2024 20:34:11 +0100 Subject: [PATCH] Add prefix delegation configuration for `onmetal` (#213) * Make prefix delegation length configurable * Update readme * Fix some lint findings --- README.md | 9 +++-- example/config.yaml | 2 +- example/onmetal_config.yaml | 2 ++ internal/api/onmetal_config.go | 12 +++++++ plugins/onmetal/plugin.go | 52 +++++++++++++++++++++++---- plugins/onmetal/plugin_test.go | 64 ++++++++++++++++++++++++++++++---- 6 files changed, 125 insertions(+), 16 deletions(-) create mode 100644 example/onmetal_config.yaml create mode 100644 internal/api/onmetal_config.go diff --git a/README.md b/README.md index 7ce959a..c9bdbf9 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,14 @@ Further, as a second parameter, a comma-separated list of subnet names shall be - depends on [IPAM operator](https://github.com/ironcore-dev/ipam) ## OnMetal -The OnMetal plugin leases a [non temporary IPv6 address](https://datatracker.ietf.org/doc/html/rfc8415#section-6.2) to an in-band client, based on the algorithm described above. +The OnMetal plugin leases a [non temporary IPv6 address](https://datatracker.ietf.org/doc/html/rfc8415#section-6.2) to an in-band client, based on the algorithm described above. Additionally, when requested from the client, a prefix delegation with preconfigured length is leased. Currently multiple prefix delegations are not supported, client prefix delegation length proposals are ignored completely. The prefix delegation length should be in the range 1 <= length <= 127. ### Configuration -No configuration is needed +The onmetal configuration consists of the prefix delegation length only. +Providing the length in `onmetal_config.yaml` goes as follows: +```yaml +prefixDelegation: + length: 64 +``` ### Notes - supports only IPv6 - IPv6 relays are mandatory diff --git a/example/config.yaml b/example/config.yaml index f4a41e6..4c0b16d 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -12,7 +12,7 @@ server6: # add leased IPs to ironcore's IPAM - ipam: ipam-ns ipam-subnet1,ipam-subnet2,some-other-subnet # lease IPs based on /127 subnets coming from relays running on the switches - - onmetal: + - onmetal: onmetal_config.yaml # announce DNS servers per DHCP - dns: 2001:4860:4860::6464 2001:4860:4860::64 # implement (i)PXE boot diff --git a/example/onmetal_config.yaml b/example/onmetal_config.yaml new file mode 100644 index 0000000..7f72aa1 --- /dev/null +++ b/example/onmetal_config.yaml @@ -0,0 +1,2 @@ +prefixDelegation: + length: 64 \ No newline at end of file diff --git a/internal/api/onmetal_config.go b/internal/api/onmetal_config.go new file mode 100644 index 0000000..30de297 --- /dev/null +++ b/internal/api/onmetal_config.go @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: MIT + +package api + +type PrefixDelegation struct { + Length int `yaml:"length"` +} + +type OnMetalConfig struct { + PrefixDelegation PrefixDelegation `yaml:"prefixDelegation"` +} diff --git a/plugins/onmetal/plugin.go b/plugins/onmetal/plugin.go index 09fd29b..03d76ba 100644 --- a/plugins/onmetal/plugin.go +++ b/plugins/onmetal/plugin.go @@ -6,8 +6,12 @@ package onmetal import ( "fmt" "net" + "os" "time" + "github.com/ironcore-dev/fedhcp/internal/api" + "gopkg.in/yaml.v3" + "github.com/coredhcp/coredhcp/handler" "github.com/coredhcp/coredhcp/logger" "github.com/coredhcp/coredhcp/plugins" @@ -21,19 +25,52 @@ var Plugin = plugins.Plugin{ Setup6: setup6, } -var mask80 = net.CIDRMask(prefixLength, 128) +var prefixLength int const ( - preferredLifeTime = 24 * time.Hour - validLifeTime = 24 * time.Hour - prefixLength = 80 + preferredLifeTime = 24 * time.Hour + validLifeTime = 24 * time.Hour + prefixDelegationLengthMin = 1 + prefixDelegationLengthMax = 127 ) +// args[0] = path to config file +func parseArgs(args ...string) (string, error) { + if len(args) != 1 { + return "", fmt.Errorf("exactly one argument must be passed to the onmetal plugin, got %d", len(args)) + } + return args[0], nil +} + +func loadConfig(args ...string) (*api.OnMetalConfig, error) { + path, err := parseArgs(args...) + if err != nil { + return nil, fmt.Errorf("invalid configuration: %v", err) + } + + log.Debugf("Reading metal config file %s", path) + configData, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %v", err) + } + + config := &api.OnMetalConfig{} + if err = yaml.Unmarshal(configData, config); err != nil { + return nil, fmt.Errorf("failed to parse config file: %v", err) + } + return config, nil +} + func setup6(args ...string) (handler.Handler6, error) { - if len(args) != 0 { - return nil, fmt.Errorf("no arguments expected, got %d", len(args)) + onMetalConfig, err := loadConfig(args...) + if err != nil { + return nil, err + } + + prefixLength = onMetalConfig.PrefixDelegation.Length + if prefixLength < prefixDelegationLengthMin || prefixLength > prefixDelegationLengthMax { + return nil, fmt.Errorf("invalid prefix length: %d", prefixLength) } - log.Printf("loaded onmetal plugin for DHCPv6.") return handler6, nil } @@ -79,6 +116,7 @@ func handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) { optIAPD := m.Options.OneIAPD() T1 := preferredLifeTime T2 := validLifeTime + var mask80 = net.CIDRMask(prefixLength, 128) if optIAPD != nil { if optIAPD.T1 != 0 { diff --git a/plugins/onmetal/plugin_test.go b/plugins/onmetal/plugin_test.go index 40f6fe6..9161276 100644 --- a/plugins/onmetal/plugin_test.go +++ b/plugins/onmetal/plugin_test.go @@ -5,8 +5,12 @@ package onmetal import ( "net" + "os" "testing" + "github.com/ironcore-dev/fedhcp/internal/api" + "gopkg.in/yaml.v3" + "github.com/insomniacslk/dhcp/dhcpv6" ) @@ -17,13 +21,26 @@ const ( ) var ( - expectedIPAddress6 = net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2} - expectedIAID = [4]byte{1, 2, 3, 4} - expectedPrefix = "2001:db8:1111:2222:3333::/80" + expectedIAID = [4]byte{1, 2, 3, 4} ) func Init6() { - _, err := setup6() + data := api.OnMetalConfig{ + PrefixDelegation: api.PrefixDelegation{ + Length: 80, + }, + } + + configData, _ := yaml.Marshal(data) + + file, _ := os.CreateTemp("", "config.yaml") + defer func() { + _ = file.Close() + _ = os.Remove(file.Name()) + }() + _ = os.WriteFile(file.Name(), configData, 0644) + + _, err := setup6(file.Name()) if err != nil { log.Fatal(err) } @@ -31,9 +48,19 @@ func Init6() { /* parametrization */ func TestWrongNumberArgs(t *testing.T) { - _, err := setup6("not-needed-arg") + _, err := setup6() if err == nil { - t.Fatal("no error occurred when providing wrong number of args (1), but it should have") + t.Fatal("no error occurred when not providing a configuration file path, but it should have") + } + + _, err = setup6("non-existing.yaml") + if err == nil { + t.Fatal("no error occurred when providing non existing configuration path, but it should have") + } + + _, err = setup6("foo", "bar") + if err == nil { + t.Fatal("no error occurred when providing wrong number of args (2), but it should have") } } @@ -91,6 +118,7 @@ func TestIPAddressRequested6(t *testing.T) { t.Errorf("expected IAID %d, got %d", expectedIAID, iana.IaId) } + expectedIPAddress6 := net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2} if !addr.Equal(expectedIPAddress6) { t.Errorf("expected IPv6 address %v, got %v", expectedIPAddress6, iana.Options.OneAddress().IPv6Addr) } @@ -217,6 +245,7 @@ func TestPrefixDelegationRequested6(t *testing.T) { t.Errorf("expected IAID %d, got %d", expectedIAID, iapd.IaId) } + expectedPrefix := "2001:db8:1111:2222:3333::/80" if pref.String() != expectedPrefix { t.Errorf("expected prefix %v, got %v", expectedPrefix, pref) } @@ -257,3 +286,26 @@ func TestPrefixDelegationNotRequested6(t *testing.T) { t.Fatalf("Expected %d IAPD option, got %d: %v", optionDisabled, len(opts), opts) } } + +func TestPrefixDelegationNotRequested7(t *testing.T) { + prefixDelegationLengthOutOfBounds := 128 + data := api.OnMetalConfig{ + PrefixDelegation: api.PrefixDelegation{ + Length: prefixDelegationLengthOutOfBounds, + }, + } + + configData, _ := yaml.Marshal(data) + + file, _ := os.CreateTemp("", "config.yaml") + defer func() { + _ = file.Close() + _ = os.Remove(file.Name()) + }() + _ = os.WriteFile(file.Name(), configData, 0644) + + _, err := setup6(file.Name()) + if err == nil { + t.Fatal("no error occurred when providing wrong prefix delegation length, but it should have") + } +}