diff --git a/example/pxeboot_config.yaml b/example/pxeboot_config.yaml index 172a0cc..af759b3 100644 --- a/example/pxeboot_config.yaml +++ b/example/pxeboot_config.yaml @@ -1,2 +1,14 @@ -tftpServer: tftp://[2001:db8::1]/ipxe/x86_64/ipxe -ipxeServer: http://[2001:db8::1]/ipxe/boot6 \ No newline at end of file +tftpAddress: + ipv4: + amd64: tftp://192.168.0.1/x86_64/amd64.efi + arm64: tftp://192.168.0.1/aarch64/arm64.efi + ipv6: + amd64: tftp://[2001:db8::1]/x86_64/amd64.efi + arm64: tftp://[2001:db8::1]/aarch64/arm64.efi +ipxeAddress: + ipv4: + amd64: http://192.168.0.2/ipxe/boot4.pxe + arm64: http://192.168.0.2/ipxe/boot4.pxe + ipv6: + amd64: http://[2001:db8::2]/ipxe/boot6.pxe + arm64: http://[2001:db8::2]/ipxe/boot6.pxe diff --git a/go.mod b/go.mod index c064773..9356e01 100644 --- a/go.mod +++ b/go.mod @@ -61,6 +61,7 @@ require ( github.com/google/gopacket v1.1.19 // indirect github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect github.com/google/uuid v1.6.0 // indirect + github.com/jinzhu/copier v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index bfa626c..ff81d6e 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,8 @@ github.com/ironcore-dev/ipam v0.2.4 h1:8dEbvggTmLfZFykhyBCPdN6oAQvHfnWPFM5p9Y2Wq github.com/ironcore-dev/ipam v0.2.4/go.mod h1:p5+URcAMcCEcYnNjPCnzu7btQcejU8veEZDFbkaTcps= github.com/ironcore-dev/metal-operator v0.1.0 h1:0GqpKgfH5hG5jULUESsTr1dUH73ip3YJYIqtV8kBqoY= github.com/ironcore-dev/metal-operator v0.1.0/go.mod h1:JMxYoN5PJ0jvKhWlpV6kR9fhBXdLy2NNtvzJXXN/Iuc= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= diff --git a/internal/api/pxeboot_config.go b/internal/api/pxeboot_config.go index c7f51e4..0caf0b8 100644 --- a/internal/api/pxeboot_config.go +++ b/internal/api/pxeboot_config.go @@ -3,7 +3,25 @@ package api -type PxebootConfig struct { - TFTPServer string `yaml:"tftpServer"` - IPXEServer string `yaml:"ipxeServer"` +type Arch string + +const ( + AMD64 Arch = "amd64" + ARM64 Arch = "arm64" + UnknownArch Arch = "unknown" +) + +type PxeBootConfig struct { + TFTPAddress Addresses `yaml:"tftpAddress"` + IPXEAddress Addresses `yaml:"ipxeAddress"` +} + +type Addresses struct { + IPv4 map[Arch]string `yaml:"ipv4"` + IPv6 map[Arch]string `yaml:"ipv6"` +} + +type Architectures struct { + Amd64 string `yaml:"amd64"` + Arm64 string `yaml:"arm64"` } diff --git a/plugins/pxeboot/plugin.go b/plugins/pxeboot/plugin.go index 12f7c06..57e0d4c 100644 --- a/plugins/pxeboot/plugin.go +++ b/plugins/pxeboot/plugin.go @@ -1,21 +1,6 @@ // SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors // SPDX-License-Identifier: MIT -// Package nbp implements handling of an NBP (Network Boot Program) using an -// URL, e.g. http://[fe80::abcd:efff:fe12:3456]/my-nbp or tftp://10.0.0.1/my-nbp . -// The NBP information is only added if it is requested by the client. -// -// For DHCPv6 OPT_BOOTFILE_URL (option 59) is used, and the value is passed -// unmodified. If the query string is specified and contains a "param" key, -// its value is also passed as OPT_BOOTFILE_PARAM (option 60), so it will be -// duplicated between option 59 and 60. -// -// Example usage: -// -// server6: -// - plugins: -// - pxeboot: pxeboot_config.yaml - package pxeboot import ( @@ -23,12 +8,12 @@ import ( "net/url" "os" + "github.com/ironcore-dev/fedhcp/internal/api" "github.com/ironcore-dev/fedhcp/internal/printer" + "gopkg.in/yaml.v3" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/iana" - "github.com/ironcore-dev/fedhcp/internal/api" - "gopkg.in/yaml.v2" "github.com/coredhcp/coredhcp/handler" "github.com/coredhcp/coredhcp/logger" @@ -45,9 +30,24 @@ var Plugin = plugins.Plugin{ Setup6: setup6, } +type TFTPOptionIPv4 struct { + TFTPServerNameOption *dhcpv4.Option + TFTPBootFileNameOption *dhcpv4.Option +} + +type BootOptionsIPv4 struct { + TFTPOptions map[api.Arch]*TFTPOptionIPv4 + IPXEOptions map[api.Arch]*dhcpv4.Option +} + +type BootOptionsIPv6 struct { + TFTPOptions map[api.Arch]dhcpv6.Option + IPXEOptions map[api.Arch]dhcpv6.Option +} + var ( - tftpOption, ipxeOption dhcpv6.Option - tftpBootFileOption, tftpServerNameOption, ipxeBootFileOption *dhcpv4.Option + bootOptsV4 *BootOptionsIPv4 + bootOptsV6 *BootOptionsIPv6 ) // args[0] = path to config file @@ -58,7 +58,7 @@ func parseArgs(args ...string) (string, error) { return args[0], nil } -func loadConfig(args ...string) (*api.PxebootConfig, error) { +func loadConfig(args ...string) (*api.PxeBootConfig, error) { path, err := parseArgs(args...) if err != nil { return nil, fmt.Errorf("invalid configuration: %v", err) @@ -70,7 +70,7 @@ func loadConfig(args ...string) (*api.PxebootConfig, error) { return nil, fmt.Errorf("failed to read config file: %v", err) } - config := &api.PxebootConfig{} + config := &api.PxeBootConfig{} if err = yaml.Unmarshal(configData, config); err != nil { return nil, fmt.Errorf("failed to parse config file: %v", err) } @@ -78,48 +78,81 @@ func loadConfig(args ...string) (*api.PxebootConfig, error) { return config, nil } -func parseConfig(args ...string) (*url.URL, *url.URL, error) { - pxebootConfig, err := loadConfig(args...) - if err != nil { - return nil, nil, err +func parseConfig(args ...string) error { + bootOptsV4 = &BootOptionsIPv4{ + TFTPOptions: map[api.Arch]*TFTPOptionIPv4{}, + IPXEOptions: map[api.Arch]*dhcpv4.Option{}, + } + bootOptsV6 = &BootOptionsIPv6{ + TFTPOptions: map[api.Arch]dhcpv6.Option{}, + IPXEOptions: map[api.Arch]dhcpv6.Option{}, } - tftp, err := url.Parse(pxebootConfig.TFTPServer) + pxeBootConfig, err := loadConfig(args...) if err != nil { - return nil, nil, fmt.Errorf("invalid tftp url: %v", err) + return err } - ipxe, err := url.Parse(pxebootConfig.IPXEServer) - if err != nil { - return nil, nil, fmt.Errorf("invalid ipxe url: %v", err) + for arch, addr := range pxeBootConfig.IPXEAddress.IPv4 { + ipxeAddress, err := url.Parse(addr) + if err != nil { + return err + } + if (ipxeAddress.Scheme != "http" && ipxeAddress.Scheme != "https") || ipxeAddress.Host == "" || ipxeAddress.Path == "" { + return fmt.Errorf("malformed iPXE parameter, should be a valid URL") + } + bfn := dhcpv4.OptBootFileName(ipxeAddress.String()) + bootOptsV4.IPXEOptions[arch] = &bfn + } + + for arch, addr := range pxeBootConfig.TFTPAddress.IPv4 { + tftpAddress, err := url.Parse(addr) + if err != nil { + return err + } + if tftpAddress.Scheme != "tftp" || tftpAddress.Host == "" || tftpAddress.Path == "" || tftpAddress.Path[0] != '/' || tftpAddress.Path[1:] == "" { + return fmt.Errorf("malformed TFTP parameter, should be a valid URL") + } + + sn := dhcpv4.OptTFTPServerName(tftpAddress.Host) + bfn := dhcpv4.OptBootFileName(tftpAddress.Path[1:]) + bootOptsV4.TFTPOptions[arch] = &TFTPOptionIPv4{ + TFTPServerNameOption: &sn, + TFTPBootFileNameOption: &bfn, + } } - if tftp.Scheme != "tftp" || tftp.Host == "" || tftp.Path == "" || tftp.Path[0] != '/' || tftp.Path[1:] == "" { - return nil, nil, fmt.Errorf("malformed TFTP parameter, should be a valid URL") + for arch, addr := range pxeBootConfig.IPXEAddress.IPv6 { + ipxeAddress, err := url.Parse(addr) + if err != nil { + return err + } + if (ipxeAddress.Scheme != "http" && ipxeAddress.Scheme != "https") || ipxeAddress.Host == "" || ipxeAddress.Path == "" { + return fmt.Errorf("malformed iPXE parameter, should be a valid URL") + } + bootOptsV6.IPXEOptions[arch] = dhcpv6.OptBootFileURL(ipxeAddress.String()) } - if (ipxe.Scheme != "http" && ipxe.Scheme != "https") || ipxe.Host == "" || ipxe.Path == "" { - return nil, nil, fmt.Errorf("malformed iPXE parameter, should be a valid URL") + for arch, addr := range pxeBootConfig.TFTPAddress.IPv6 { + tftpAddress, err := url.Parse(addr) + if err != nil { + return err + } + if tftpAddress.Scheme != "tftp" || tftpAddress.Host == "" || tftpAddress.Path == "" || tftpAddress.Path[0] != '/' || tftpAddress.Path[1:] == "" { + return fmt.Errorf("malformed TFTP parameter, should be a valid URL") + } + + bootOptsV6.TFTPOptions[arch] = dhcpv6.OptBootFileURL(tftpAddress.String()) } - return tftp, ipxe, nil + return nil } func setup4(args ...string) (handler.Handler4, error) { - tftp, ipxe, err := parseConfig(args...) - if err != nil { + if err := parseConfig(args...); err != nil { return nil, err } - opt1 := dhcpv4.OptBootFileName(tftp.Path[1:]) - tftpBootFileOption = &opt1 - - opt2 := dhcpv4.OptTFTPServerName(tftp.Host) - tftpServerNameOption = &opt2 - - opt3 := dhcpv4.OptBootFileName(ipxe.String()) - ipxeBootFileOption = &opt3 - log.Printf("loaded PXEBOOT plugin for DHCPv4.") return pxeBootHandler4, nil } @@ -133,30 +166,25 @@ func pxeBootHandler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { printer.VerboseRequest(req, log, printer.IPv4) defer printer.VerboseResponse(req, resp, log, printer.IPv4) - if tftpBootFileOption == nil || tftpServerNameOption == nil || ipxeBootFileOption == nil { - // nothing to do - return resp, false - } - if req.IsOptionRequested(dhcpv4.OptionBootfileName) { var opt, opt2 *dhcpv4.Option - // if iPXE request - if req.GetOneOption(dhcpv4.OptionUserClassInformation) != nil { - userClassInfo := req.GetOneOption(dhcpv4.OptionUserClassInformation) - log.Debugf("UserClassInformation: %s (%x)", string(userClassInfo), userClassInfo) - if len(userClassInfo) >= 4 && string(userClassInfo[0:4]) == "iPXE" { - opt = ipxeBootFileOption + tftp, arch := isTFTPRequested4(req) + ipxe := isIPXERequested4(req) + + if ipxe { + if !checkIPXEOptionsV4ForArchAreValid(arch) { + log.Infof("No IPXE address configured for DHCPv4") + return resp, false } - } else - // if TFTP request - if req.GetOneOption(dhcpv4.OptionClassIdentifier) != nil { - classID := req.GetOneOption(dhcpv4.OptionClassIdentifier) - log.Debugf("ClassIdentifier: %s (%x)", string(classID), classID) - if len(classID) >= 19 && string(classID[0:19]) == "PXEClient:Arch:0000" { - opt = tftpBootFileOption - opt2 = tftpServerNameOption + opt = bootOptsV4.IPXEOptions[arch] + } else if tftp { + if !checkTFTPOptionsV4ForArchAreValid(arch) { + log.Infof("No TFTP address configured for DHCPv4") + return resp, false } + opt = bootOptsV4.TFTPOptions[arch].TFTPBootFileNameOption + opt2 = bootOptsV4.TFTPOptions[arch].TFTPServerNameOption } if opt != nil { @@ -172,15 +200,73 @@ func pxeBootHandler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { return resp, false } +func checkTFTPOptionsV4ForArchAreValid(arch api.Arch) bool { + if bootOptsV4.TFTPOptions == nil { + return false + } + + v, exists := bootOptsV4.TFTPOptions[arch] + if !exists { + return false + } + + if v.TFTPServerNameOption == nil || + v.TFTPBootFileNameOption.String() == "" || + v.TFTPBootFileNameOption == nil || + v.TFTPBootFileNameOption.String() == "" { + return false + } + + return true +} + +func checkIPXEOptionsV4ForArchAreValid(arch api.Arch) bool { + if bootOptsV4.IPXEOptions == nil { + return false + } + + v, exists := bootOptsV4.IPXEOptions[arch] + if !exists || v.String() == "" { + return false + } + + return true +} + +func isTFTPRequested4(req *dhcpv4.DHCPv4) (bool, api.Arch) { + if req.GetOneOption(dhcpv4.OptionClassIdentifier) != nil { + classID := req.GetOneOption(dhcpv4.OptionClassIdentifier) + log.Debugf("ClassIdentifier: %s (%x)", string(classID), classID) + if len(classID) >= 20 && string(classID[0:19]) == "PXEClient:Arch:0000" { + return true, api.AMD64 + } else if len(classID) >= 20 && string(classID[0:20]) == "PXEClient:Arch:00011" { + return true, api.ARM64 + } else { + return true, api.UnknownArch + } + } + return false, api.UnknownArch +} + +func isIPXERequested4(req *dhcpv4.DHCPv4) bool { + if req.GetOneOption(dhcpv4.OptionUserClassInformation) != nil { + userClassInfo := req.GetOneOption(dhcpv4.OptionUserClassInformation) + log.Debugf("UserClassInformation: %s (%x)", string(userClassInfo), userClassInfo) + if len(userClassInfo) >= 4 && string(userClassInfo[0:4]) == "iPXE" { + return true + } else { + log.Warnf("Non-IPXE UserClass option set for DHCPv4") + } + } + + return false +} + func setup6(args ...string) (handler.Handler6, error) { - tftp, ipxe, err := parseConfig(args...) - if err != nil { + if err := parseConfig(args...); err != nil { return nil, err } - tftpOption = dhcpv6.OptBootFileURL(tftp.String()) - ipxeOption = dhcpv6.OptBootFileURL(ipxe.String()) - log.Printf("loaded PXEBOOT plugin for DHCPv6.") return pxeBootHandler6, nil } @@ -194,10 +280,6 @@ func pxeBootHandler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) { printer.VerboseRequest(req, log, printer.IPv6) defer printer.VerboseResponse(req, resp, log, printer.IPv6) - if tftpOption == nil || ipxeOption == nil { - // nothing to do - return resp, false - } decap, err := req.GetInnerMessage() if err != nil { log.Errorf("Could not decapsulate request: %v", err) @@ -206,31 +288,86 @@ func pxeBootHandler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) { } if decap.IsOptionRequested(dhcpv6.OptionBootfileURL) { - var opt *dhcpv6.Option - - // if TFTP request - if decap.GetOneOption(dhcpv6.OptionClientArchType) != nil { - optBytes := decap.GetOneOption(dhcpv6.OptionClientArchType).ToBytes() - log.Debugf("ClientArchType: %s (%x)", string(optBytes), optBytes) - if len(optBytes) == 2 && optBytes[0] == 0 && optBytes[1] == byte(iana.EFI_X86_64) { // 0x07 - opt = &tftpOption - } - } + var opt dhcpv6.Option + + tftp, arch := isTFTPRequested6(decap) + ipxe := isIPXERequested6(decap) - // if iPXE request - if decap.GetOneOption(dhcpv6.OptionUserClass) != nil { - userClass := decap.GetOneOption(dhcpv6.OptionUserClass).ToBytes() - log.Debugf("UserClass: %s (%x)", string(userClass), userClass) - if len(userClass) >= 5 && string(userClass[2:6]) == "iPXE" { - opt = &ipxeOption + if ipxe { + if !checkIPXEOptionsV6ForArchAreValid(arch) { + log.Infof("No IPXE address configured for DHCPv6") + return resp, false } + opt = bootOptsV6.IPXEOptions[arch] + } else if tftp { + if !checkTFTPOptionsV6ForArchAreValid(arch) { + log.Infof("No TFTP address configured for DHCPv6") + return resp, false + } + opt = bootOptsV6.TFTPOptions[arch] } if opt != nil { - resp.AddOption(*opt) - log.Debugf("Added option %s", *opt) + resp.AddOption(opt) + log.Debugf("Added option %s", opt) } } return resp, false } + +func isTFTPRequested6(req *dhcpv6.Message) (bool, api.Arch) { + if req.GetOneOption(dhcpv6.OptionClientArchType) != nil { + optBytes := req.GetOneOption(dhcpv6.OptionClientArchType).ToBytes() + log.Debugf("ClientArchType: %s (%x)", string(optBytes), optBytes) + if len(optBytes) == 2 && optBytes[0] == 0 && optBytes[1] == byte(iana.EFI_X86_64) { // 0x07 + return true, api.AMD64 + } else if len(optBytes) == 2 && optBytes[0] == 0 && optBytes[1] == byte(iana.EFI_ARM64) { // 0x0B + return true, api.ARM64 + } else { + return true, api.UnknownArch + } + } + return false, api.UnknownArch + +} + +func isIPXERequested6(req *dhcpv6.Message) bool { + if req.GetOneOption(dhcpv6.OptionUserClass) != nil { + userClass := req.GetOneOption(dhcpv6.OptionUserClass).ToBytes() + log.Debugf("UserClass: %s (%x)", string(userClass), userClass) + if len(userClass) >= 5 && string(userClass[2:6]) == "iPXE" { + return true + } else { + log.Warnf("Non-IPXE UserClass option set for DHCPv6") + return false + } + } + return false +} + +func checkTFTPOptionsV6ForArchAreValid(arch api.Arch) bool { + if bootOptsV6.TFTPOptions == nil { + return false + } + + v, exists := bootOptsV6.TFTPOptions[arch] + if !exists || v.String() == "" { + return false + } + + return true +} + +func checkIPXEOptionsV6ForArchAreValid(arch api.Arch) bool { + if bootOptsV6.IPXEOptions == nil { + return false + } + + v, exists := bootOptsV6.IPXEOptions[arch] + if !exists || v.String() == "" { + return false + } + + return true +} diff --git a/plugins/pxeboot/plugin_test.go b/plugins/pxeboot/plugin_test.go index 473bd8e..fac0024 100644 --- a/plugins/pxeboot/plugin_test.go +++ b/plugins/pxeboot/plugin_test.go @@ -10,7 +10,6 @@ import ( "testing" "github.com/insomniacslk/dhcp/dhcpv4" - "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/iana" @@ -19,20 +18,23 @@ import ( ) const ( - ipxePath = "http://[2001:db8::1]/boot.ipxe" - tftpPath = "tftp://[2001:db8::1]/boot.efi" + tftpAMDPath4 = "tftp://192.168.0.1/x86_64/amd64.efi" + tftpARMPath4 = "tftp://192.168.0.1/aarch64/arm64.efi" + tftpAMDPath6 = "tftp://[2001:db8::1]/x86_64/amd64.efi" + tftpARMPath6 = "tftp://[2001:db8::1]/aarch64/arm64.efi" + ipxePathAMD4 = "http://192.168.0.2/ipxe/x86_64/boot4.pxe" + ipxePathARM4 = "http://192.168.0.2/ipxe/aarch64/boot4.pxe" + ipxePathAMD6 = "http://[2001:db8::2]/ipxe/x86_64/boot6.pxe" + ipxePathARM6 = "http://[2001:db8::2]/ipxe/aarch64/boot6.pxe" ) var ( numberOptsBootFileURL int tempConfigFilePattern = "*-pxeboot_config.yaml" - validConfig = &api.PxebootConfig{ - TFTPServer: tftpPath, - IPXEServer: ipxePath, - } + validConfig *api.PxeBootConfig ) -func createTempConfig(config api.PxebootConfig, tempDir string) (string, error) { +func createTempConfig(config api.PxeBootConfig, tempDir string) (string, error) { configData, err := yaml.Marshal(config) if err != nil { return "", err @@ -56,7 +58,7 @@ func createTempConfig(config api.PxebootConfig, tempDir string) (string, error) return configFile, nil } -func Init4(config api.PxebootConfig, tempDir string) error { +func Init4(config api.PxeBootConfig, tempDir string) error { configFile, err := createTempConfig(config, tempDir) if err != nil { return err @@ -70,7 +72,7 @@ func Init4(config api.PxebootConfig, tempDir string) error { return nil } -func Init6(config api.PxebootConfig, tempDir string, numOptBoot int) error { +func Init6(config api.PxeBootConfig, tempDir string, numOptBoot int) error { numberOptsBootFileURL = numOptBoot configFile, err := createTempConfig(config, tempDir) @@ -86,8 +88,15 @@ func Init6(config api.PxebootConfig, tempDir string, numOptBoot int) error { return err } -/* parametrization */ +func TestMain(m *testing.M) { + initValidConfig() + defer initValidConfig() + exitCode := m.Run() + os.Exit(exitCode) +} + +/* parametrization */ func TestWrongNumberArgs(t *testing.T) { _, err := parseArgs("foo", "bar") if err == nil { @@ -100,81 +109,97 @@ func TestWrongNumberArgs(t *testing.T) { } } -func TestWrongArgs(t *testing.T) { - malformedTFTPPath := []string{"tftp://example.com", "tftp:/example.com/boot.efi", "foo://example.com/boot.efi"} - for _, wrongTFTP := range malformedTFTPPath { - config := &api.PxebootConfig{ - TFTPServer: wrongTFTP, - IPXEServer: ipxePath, - } - tempDir := t.TempDir() - err := Init4(*config, tempDir) - if err == nil { - t.Fatalf("no error occurred when providing wrong TFTP path %s, but it should have", wrongTFTP) - } - if tftpBootFileOption != nil { - t.Fatalf("TFTP boot file was set when providing wrong TFTP path %s, but it should be empty", wrongTFTP) - } - if tftpServerNameOption != nil { - t.Fatalf("TFTP server name was set when providing wrong TFTP path %s, but it should be empty", wrongTFTP) - } - if ipxeBootFileOption != nil { - t.Fatalf("IPXE boot file was set when providing wrong TFTP path %s, but it should be empty", wrongTFTP) - } +func TestWrongArgs4(t *testing.T) { + malformedTFTPPath := []string{"tftp://192.168.0.3", "tftp:/192.168.0.3/boot.efi", "foo://192.168.0.3/boot.efi"} + malformedIPXEPath := []string{"https://192.168.0.3", "http:/192.168.0.3/boot.ipxe", "foo://192.168.0.3/boot.ipxe"} - err = Init6(*config, tempDir, 0) - if err == nil { - t.Fatalf("no error occurred when providing wrong TFTP path %s, but it should have", wrongTFTP) - } - if tftpOption != nil { - t.Fatalf("TFTP boot file was set when providing wrong TFTP path %s, but it should be empty", wrongTFTP) - } - if ipxeOption != nil { - t.Fatalf("IPXE boot file was set when providing wrong TFTP path %s, but it should be empty", wrongTFTP) + for _, wrongTFTP := range malformedTFTPPath { + for _, arch := range []api.Arch{api.AMD64, api.ARM64} { + initValidConfig() + validConfig.TFTPAddress.IPv4[arch] = wrongTFTP + tempDir := t.TempDir() + err := Init4(*validConfig, tempDir) + if err == nil { + t.Fatalf("no error occurred when providing wrong TFTP path %s for arch %s, but it should have", wrongTFTP, arch) + } + + tftpo, exists := bootOptsV4.TFTPOptions[arch] + if exists && tftpo.TFTPBootFileNameOption != nil { + t.Fatalf("TFTP boot file was set when providing wrong TFTP path %s for arch %s, but it should be empty", wrongTFTP, arch) + } + + if exists && tftpo.TFTPServerNameOption != nil { + t.Fatalf("TFTP server name was set when providing wrong TFTP path %s for arch %s, but it should be empty", wrongTFTP, arch) + } } } - malformedIPXEPath := []string{"https://example.com", "http:/www.example.com/boot.ipxe", "foo://example.com/boot.ipxe"} for _, wrongIPXE := range malformedIPXEPath { - config := &api.PxebootConfig{ - TFTPServer: tftpPath, - IPXEServer: wrongIPXE, - } - tempDir := t.TempDir() - err := Init4(*config, tempDir) - if err == nil { - t.Fatalf("no error occurred when providing wrong IPXE path %s, but it should have", wrongIPXE) - } - err = Init4(*config, tempDir) - if err == nil { - t.Fatalf("no error occurred when providing wrong IPXE path %s, but it should have", wrongIPXE) - } - if tftpBootFileOption != nil { - t.Fatalf("TFTP boot file was set when providing wrong IPXE path %s, but it should be empty", wrongIPXE) - } - if tftpServerNameOption != nil { - t.Fatalf("TFTP server name set when providing wrong IPXE path %s, but it should be empty", wrongIPXE) - } - if ipxeBootFileOption != nil { - t.Fatalf("IPXE boot file was set when providing wrong IPXE path %s, but it should be empty", wrongIPXE) - } - - err = Init6(*config, tempDir, 0) - if err == nil { - t.Fatalf("no error occurred when providing wrong IPXE path %s, but it should have", wrongIPXE) - } - if tftpOption != nil { - t.Fatalf("TFTP boot file was set when providing wrong IPXE path %s, but it should be empty", wrongIPXE) - } - if ipxeOption != nil { - t.Fatalf("IPXE boot file was set when providing wrong IPXE path %s, but it should be empty", wrongIPXE) + for _, arch := range []api.Arch{api.AMD64, api.ARM64} { + initValidConfig() + validConfig.IPXEAddress.IPv4[arch] = wrongIPXE + tempDir := t.TempDir() + err := Init4(*validConfig, tempDir) + if err == nil { + t.Fatalf("no error occurred when providing wrong IPXE path %s for arch %s, but it should have", wrongIPXE, arch) + } + + ipxeo, exists := bootOptsV4.IPXEOptions[arch] + if exists && ipxeo.String() != "" { + t.Fatalf("IPXE boot file was set when providing wrong IPXE path %s for arch %s, but it should be empty", wrongIPXE, arch) + } } } } -/* IPv6 */ +func TestPXERequestedAMD6(t *testing.T) { + tempDir := t.TempDir() + _ = Init6(*validConfig, tempDir, 1) -func TestPXERequested6(t *testing.T) { + req, err := dhcpv6.NewMessage() + if err != nil { + t.Fatal(err) + } + req.MessageType = dhcpv6.MessageTypeRequest + req.AddOption(dhcpv6.OptRequestedOption(dhcpv6.OptionBootfileURL)) + optUserClass := dhcpv6.OptUserClass{} + buf := []byte{ + 0, 4, + 'i', 'P', 'X', 'E', + } + _ = optUserClass.FromBytes(buf) + req.UpdateOption(&optUserClass) + + req.AddOption(dhcpv6.OptRequestedOption(dhcpv6.OptionBootfileURL)) + optClientArchType := dhcpv6.OptClientArchType(iana.EFI_X86_64) + req.UpdateOption(optClientArchType) + + stub, err := dhcpv6.NewMessage() + if err != nil { + t.Fatal(err) + } + stub.MessageType = dhcpv6.MessageTypeReply + + resp, stop := pxeBootHandler6(req, stub) + if resp == nil { + t.Fatal("plugin did not return a message") + } + + if stop { + t.Error("plugin interrupted processing, but it shouldn't have") + } + opts := resp.GetOption(dhcpv6.OptionBootfileURL) + if len(opts) != numberOptsBootFileURL { + t.Fatalf("Expected %d BootFileUrl option, got %d: %v", numberOptsBootFileURL, len(opts), opts) + } + + bootFileURL := resp.(*dhcpv6.Message).Options.BootFileURL() + if bootFileURL != ipxePathAMD6 { + t.Errorf("Found BootFileURL %s, expected %s", bootFileURL, ipxePathAMD6) + } +} + +func TestPXERequestedARM6(t *testing.T) { tempDir := t.TempDir() _ = Init6(*validConfig, tempDir, 1) @@ -192,6 +217,10 @@ func TestPXERequested6(t *testing.T) { _ = optUserClass.FromBytes(buf) req.UpdateOption(&optUserClass) + req.AddOption(dhcpv6.OptRequestedOption(dhcpv6.OptionBootfileURL)) + optClientArchType := dhcpv6.OptClientArchType(iana.EFI_ARM64) + req.UpdateOption(optClientArchType) + stub, err := dhcpv6.NewMessage() if err != nil { t.Fatal(err) @@ -212,12 +241,12 @@ func TestPXERequested6(t *testing.T) { } bootFileURL := resp.(*dhcpv6.Message).Options.BootFileURL() - if bootFileURL != ipxePath { - t.Errorf("Found BootFileURL %s, expected %s", bootFileURL, ipxePath) + if bootFileURL != ipxePathARM6 { + t.Errorf("Found BootFileURL %s, expected %s", bootFileURL, ipxePathARM6) } } -func TestTFTPRequested6(t *testing.T) { +func TestTFTPRequestedAMD6(t *testing.T) { tempDir := t.TempDir() _ = Init6(*validConfig, tempDir, 1) @@ -250,8 +279,46 @@ func TestTFTPRequested6(t *testing.T) { } bootFileURL := resp.(*dhcpv6.Message).Options.BootFileURL() - if bootFileURL != tftpPath { - t.Errorf("Found BootFileURL %s, expected %s", bootFileURL, tftpPath) + if bootFileURL != tftpAMDPath6 { + t.Errorf("Found BootFileURL %s, expected %s", bootFileURL, tftpAMDPath6) + } +} + +func TestTFTPRequestedARM6(t *testing.T) { + tempDir := t.TempDir() + _ = Init6(*validConfig, tempDir, 1) + + req, err := dhcpv6.NewMessage() + if err != nil { + t.Fatal(err) + } + req.MessageType = dhcpv6.MessageTypeRequest + req.AddOption(dhcpv6.OptRequestedOption(dhcpv6.OptionBootfileURL)) + optClientArchType := dhcpv6.OptClientArchType(iana.EFI_ARM64) + req.UpdateOption(optClientArchType) + + stub, err := dhcpv6.NewMessage() + if err != nil { + t.Fatal(err) + } + stub.MessageType = dhcpv6.MessageTypeReply + + resp, stop := pxeBootHandler6(req, stub) + if resp == nil { + t.Fatal("plugin did not return a message") + } + + if stop { + t.Error("plugin interrupted processing, but it shouldn't have") + } + opts := resp.GetOption(dhcpv6.OptionBootfileURL) + if len(opts) != numberOptsBootFileURL { + t.Fatalf("Expected %d BootFileUrl option, got %d: %v", numberOptsBootFileURL, len(opts), opts) + } + + bootFileURL := resp.(*dhcpv6.Message).Options.BootFileURL() + if bootFileURL != tftpARMPath6 { + t.Errorf("Found BootFileURL %s, expected %s", bootFileURL, tftpARMPath6) } } @@ -390,7 +457,7 @@ func TestTFTPNotRequested6(t *testing.T) { /* IPV4 */ -func TestPXERequested4(t *testing.T) { +func TestPXERequestedAMD4(t *testing.T) { tempDir := t.TempDir() _ = Init4(*validConfig, tempDir) @@ -405,6 +472,9 @@ func TestPXERequested4(t *testing.T) { optUserClass := dhcpv4.OptUserClass("iPXE") req.UpdateOption(optUserClass) + optClassID := dhcpv4.OptClassIdentifier("PXEClient:Arch:00007") + req.UpdateOption(optClassID) + stub, err := dhcpv4.NewReplyFromRequest(req) if err != nil { t.Fatal(err) @@ -420,12 +490,50 @@ func TestPXERequested4(t *testing.T) { } bootFileURL := dhcpv4.GetString(dhcpv4.OptionBootfileName, resp.Options) - if bootFileURL != ipxePath { - t.Errorf("Found BootFileURL %s, expected %s", bootFileURL, ipxePath) + if bootFileURL != ipxePathAMD4 { + t.Errorf("Found BootFileURL %s, expected %s", bootFileURL, ipxePathAMD4) } } -func TestTFTPRequested4(t *testing.T) { +func TestPXERequestedARM4(t *testing.T) { + tempDir := t.TempDir() + _ = Init4(*validConfig, tempDir) + + req, err := dhcpv4.NewDiscovery(net.HardwareAddr{ + 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, + dhcpv4.WithRequestedOptions(dhcpv4.OptionBootfileName), + ) + if err != nil { + t.Fatal(err) + } + + optUserClass := dhcpv4.OptUserClass("iPXE") + req.UpdateOption(optUserClass) + + optClassID := dhcpv4.OptClassIdentifier("PXEClient:Arch:00011") + req.UpdateOption(optClassID) + + stub, err := dhcpv4.NewReplyFromRequest(req) + if err != nil { + t.Fatal(err) + } + + resp, stop := pxeBootHandler4(req, stub) + if resp == nil { + t.Fatal("plugin did not return a message") + } + + if stop { + t.Error("plugin interrupted processing, but it shouldn't have") + } + + bootFileURL := dhcpv4.GetString(dhcpv4.OptionBootfileName, resp.Options) + if bootFileURL != ipxePathARM4 { + t.Errorf("Found BootFileURL %s, expected %s", bootFileURL, ipxePathARM4) + } +} + +func TestTFTPRequestedAMD4(t *testing.T) { tempDir := t.TempDir() _ = Init4(*validConfig, tempDir) @@ -462,8 +570,50 @@ func TestTFTPRequested4(t *testing.T) { Host: tftpServerName, Path: bootFileName, }).String() - if combinedPath != tftpPath { - t.Errorf("Found TFTP path %s, expected %s", combinedPath, tftpPath) + if combinedPath != tftpAMDPath4 { + t.Errorf("Found TFTP path %s, expected %s", combinedPath, tftpAMDPath4) + } +} + +func TestTFTPRequestedARM4(t *testing.T) { + tempDir := t.TempDir() + _ = Init4(*validConfig, tempDir) + + req, err := dhcpv4.NewDiscovery(net.HardwareAddr{ + 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, + dhcpv4.WithRequestedOptions(dhcpv4.OptionBootfileName), + ) + if err != nil { + t.Fatal(err) + } + + optClassID := dhcpv4.OptClassIdentifier("PXEClient:Arch:00011") + req.UpdateOption(optClassID) + + stub, err := dhcpv4.NewReplyFromRequest(req) + if err != nil { + t.Fatal(err) + } + + resp, stop := pxeBootHandler4(req, stub) + if resp == nil { + t.Fatal("plugin did not return a message") + } + + if stop { + t.Error("plugin interrupted processing, but it shouldn't have") + } + + const protocol = "tftp" + tftpServerName := dhcpv4.GetString(dhcpv4.OptionTFTPServerName, resp.Options) + bootFileName := dhcpv4.GetString(dhcpv4.OptionBootfileName, resp.Options) + combinedPath := (&url.URL{ + Scheme: protocol, + Host: tftpServerName, + Path: bootFileName, + }).String() + if combinedPath != tftpARMPath4 { + t.Errorf("Found TFTP path %s, expected %s", combinedPath, tftpARMPath4) } } @@ -608,3 +758,28 @@ func TestWrongTFTPRequested4(t *testing.T) { t.Errorf("Found TFTP path %s, expected empty", bootFileName) } } + +func initValidConfig() { + validConfig = &api.PxeBootConfig{ + TFTPAddress: api.Addresses{ + IPv4: map[api.Arch]string{ + api.AMD64: tftpAMDPath4, + api.ARM64: tftpARMPath4, + }, + IPv6: map[api.Arch]string{ + api.AMD64: tftpAMDPath6, + api.ARM64: tftpARMPath6, + }, + }, + IPXEAddress: api.Addresses{ + IPv4: map[api.Arch]string{ + api.AMD64: ipxePathAMD4, + api.ARM64: ipxePathARM4, + }, + IPv6: map[api.Arch]string{ + api.AMD64: ipxePathAMD6, + api.ARM64: ipxePathARM6, + }, + }, + } +}