From efd63aecd4dbc31c0f2b95fc04ffb6f2c5577cb3 Mon Sep 17 00:00:00 2001 From: Dacian Reece-Stremtan Date: Fri, 1 Aug 2025 13:43:57 -0500 Subject: [PATCH] IPv6 iPXE support * Parse address and port with netip w/ tests * Add Authority to provisioning templates based on remote IP family (name based on RFC 3986) * Add IPv6 support to the default iPXE (tested) * Update `grub.cfg.ww` to use Authority (untested) Co-authored-by: Dacian Reece-Stremtan Co-authored-by: Timothy Middelkoop Signed-off-by: Timothy Middelkoop --- CHANGELOG.md | 4 ++ CONTRIBUTORS.md | 1 + etc/grub/grub.cfg.ww | 3 +- etc/ipxe/default.ipxe | 3 +- internal/pkg/warewulfd/parser.go | 11 +++-- internal/pkg/warewulfd/parser_test.go | 53 ++++++++++++++++++++++++ internal/pkg/warewulfd/provision.go | 39 +++++++++++++++++ internal/pkg/warewulfd/provision_test.go | 15 ++++--- 8 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 internal/pkg/warewulfd/parser_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b586938d..66af5359 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added `DELETE /api/overlays/{name}/file?path={path}` - Added `wwctl configure warewulfd` - Added troubleshooting documentation regarding NUMA and tmpfs +- Added support for IPv6 when booting with iPXE. #1852 ### Changed - Restore default idempotency of `PUT /api/nodes/{id}` - `DELETE /api/overlays/{name}?force=true` can delete overlays that are in use - `warewulfd` overlay autobuild rebuilds overlays after node discovery. #1468 + +### Fixed + - Improved netplan support. #1873 ### Fixed diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index e831e8f3..7d75a19e 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -47,3 +47,4 @@ * Stephen Simpson [@ssimpson89](https://github.com/ssimpson89) * Rafael Lopez @rafalop * Arian Cabrera [@acabrera86](https://github.com/acabrera86) +* Dacian Reece-Stremtan [@dacianstremtan](https://github.com/dacianstremtan) diff --git a/etc/grub/grub.cfg.ww b/etc/grub/grub.cfg.ww index 0d39cba2..7b88eaf8 100644 --- a/etc/grub/grub.cfg.ww +++ b/etc/grub/grub.cfg.ww @@ -4,6 +4,7 @@ echo echo "Warewulf Server:" echo "* Ipaddr: {{.Ipaddr}}" echo "* Port: {{.Port}}" +echo "* Authority: {{.Authority}}" echo echo "This node:" echo "* Fqdn: {{.Fqdn}}" @@ -28,7 +29,7 @@ reboot echo "Reading asset key..." smbios --type 3 --get-string 8 --set assetkey -uri="(http,{{.Ipaddr}}:{{.Port}})/provision/${net_default_mac}?assetkey=${assetkey}" +uri="(http,{{.Authority}})/provision/${net_default_mac}?assetkey=${assetkey}" kernel="${uri}&stage=kernel" set default={{ or .Tags.GrubMenuEntry "single-stage" }} diff --git a/etc/ipxe/default.ipxe b/etc/ipxe/default.ipxe index 69bf0848..872bdd24 100644 --- a/etc/ipxe/default.ipxe +++ b/etc/ipxe/default.ipxe @@ -15,7 +15,7 @@ sleep 30 reboot {{- end }} -set baseuri http://{{.Ipaddr}}:{{.Port}}/provision/{{.Hwaddr}} +set baseuri http://{{.Authority}}/provision/{{.Hwaddr}} set uri ${baseuri}?assetkey=${asset}&uuid=${uuid} echo Downloading kernel image... @@ -115,6 +115,7 @@ goto menu echo Warewulf Server: echo * Ipaddr: {{.Ipaddr}} echo * Port: {{.Port}} +echo * Authority: {{.Authority}} echo echo This node: echo * Fqdn: {{.Fqdn}} diff --git a/internal/pkg/warewulfd/parser.go b/internal/pkg/warewulfd/parser.go index eea3c190..bd29e80e 100644 --- a/internal/pkg/warewulfd/parser.go +++ b/internal/pkg/warewulfd/parser.go @@ -2,7 +2,7 @@ package warewulfd import ( "net/http" - "strconv" + "net/netip" "strings" "github.com/pkg/errors" @@ -44,9 +44,12 @@ func parseReq(req *http.Request) (parserInfo, error) { ret.efifile = path_parts[2] } ret.hwaddr = hwaddr - ret.ipaddr = strings.Split(req.RemoteAddr, ":")[0] - ret.remoteport, _ = strconv.Atoi(strings.Split(req.RemoteAddr, ":")[1]) - + remoteAddrPort, err := netip.ParseAddrPort(req.RemoteAddr) + if err != nil { + return ret, errors.New("could not parse remote address") + } + ret.ipaddr = remoteAddrPort.Addr().String() + ret.remoteport = int(remoteAddrPort.Port()) if len(req.URL.Query()["assetkey"]) > 0 { ret.assetkey = req.URL.Query()["assetkey"][0] } diff --git a/internal/pkg/warewulfd/parser_test.go b/internal/pkg/warewulfd/parser_test.go new file mode 100644 index 00000000..da595575 --- /dev/null +++ b/internal/pkg/warewulfd/parser_test.go @@ -0,0 +1,53 @@ +package warewulfd + +import ( + "net/http" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" +) + +var parseReqTests = []struct { + description string + url string + remoteAddr string + result parserInfo +}{ + { + description: "basic ipv4 request", + url: "/provision/00:00:00:ff:ff:ff", + remoteAddr: "10.5.1.1:9873", + result: parserInfo{ + hwaddr: "00:00:00:ff:ff:ff", + ipaddr: "10.5.1.1", + remoteport: 9873, + stage: "ipxe", + }, + }, + { + description: "basic ipv6 request", + url: "/provision/00:00:00:ff:ff:ff", + remoteAddr: "[fd00:5::1:1]:9873", + result: parserInfo{ + hwaddr: "00:00:00:ff:ff:ff", + ipaddr: "fd00:5::1:1", + remoteport: 9873, + stage: "ipxe", + }, + }, +} + +func Test_ParseReq(t *testing.T) { + for _, tt := range parseReqTests { + t.Run(tt.description, func(t *testing.T) { + req := &http.Request{ + URL: &url.URL{Path: tt.url}, + RemoteAddr: tt.remoteAddr, + } + result, err := parseReq(req) + assert.NoError(t, err) + assert.Equal(t, tt.result, result) + }) + } +} diff --git a/internal/pkg/warewulfd/provision.go b/internal/pkg/warewulfd/provision.go index 09c678ac..0c75f925 100644 --- a/internal/pkg/warewulfd/provision.go +++ b/internal/pkg/warewulfd/provision.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "net/netip" "path" "path/filepath" "strconv" @@ -32,7 +33,9 @@ type templateVars struct { Ipxe string Hwaddr string Ipaddr string + Ipaddr6 string Port string + Authority string KernelArgs string KernelVersion string Root string @@ -115,12 +118,30 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) { kernelVersion = kernel_.Version() } } + authority := fmt.Sprintf("%s:%d", conf.Ipaddr, conf.Warewulf.Port) + ipaddr6 := "" + if confIpaddr6, err := netip.ParsePrefix(conf.Ipaddr6); err == nil { + ipaddr6 = confIpaddr6.Addr().String() + } + if rinfoIpaddr, err := netip.ParseAddr(rinfo.ipaddr); err == nil { + if rinfoIpaddr.Is6() { + if ipaddr6 != "" { + authority = fmt.Sprintf("[%s]:%d", ipaddr6, conf.Warewulf.Port) + } else { + wwlog.Error("No valid IPv6 address configured, but request is IPv6") + } + } + } else { + wwlog.Error("Could not parse request IP address: %s", rinfo.ipaddr) + } tmpl_data = &templateVars{ Id: remoteNode.Id(), Cluster: remoteNode.ClusterName, Fqdn: remoteNode.Id(), Ipaddr: conf.Ipaddr, + Ipaddr6: ipaddr6, Port: strconv.Itoa(conf.Warewulf.Port), + Authority: authority, Hostname: remoteNode.Id(), Hwaddr: rinfo.hwaddr, ImageName: remoteNode.ImageName, @@ -203,12 +224,30 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) { kernelVersion = kernel_.Version() } } + authority := fmt.Sprintf("%s:%d", conf.Ipaddr, conf.Warewulf.Port) + ipaddr6 := "" + if confIpaddr6, err := netip.ParsePrefix(conf.Ipaddr6); err == nil { + ipaddr6 = confIpaddr6.Addr().String() + } + if rinfoIpaddr, err := netip.ParseAddr(rinfo.ipaddr); err == nil { + if rinfoIpaddr.Is6() { + if ipaddr6 != "" { + authority = fmt.Sprintf("[%s]:%d", ipaddr6, conf.Warewulf.Port) + } else { + wwlog.Error("No valid IPv6 address configured, but request is IPv6") + } + } + } else { + wwlog.Error("Could not parse request IP address: %s", rinfo.ipaddr) + } tmpl_data = &templateVars{ Id: remoteNode.Id(), Cluster: remoteNode.ClusterName, Fqdn: remoteNode.Id(), Ipaddr: conf.Ipaddr, + Ipaddr6: ipaddr6, Port: strconv.Itoa(conf.Warewulf.Port), + Authority: authority, Hostname: remoteNode.Id(), Hwaddr: rinfo.hwaddr, ImageName: remoteNode.ImageName, diff --git a/internal/pkg/warewulfd/provision_test.go b/internal/pkg/warewulfd/provision_test.go index 9833b139..6078fa44 100644 --- a/internal/pkg/warewulfd/provision_test.go +++ b/internal/pkg/warewulfd/provision_test.go @@ -23,22 +23,23 @@ var provisionSendTests = []struct { }{ {"system overlay", "/overlay-system/00:00:00:ff:ff:ff", "system overlay", 200, "10.10.10.10:9873"}, {"runtime overlay", "/overlay-runtime/00:00:00:ff:ff:ff", "runtime overlay", 200, "10.10.10.10:9873"}, - {"fake overlay", "/overlay-system/00:00:00:ff:ff:ff?overlay=fake", "", 404, "10.10.10.10:9873:9873"}, + {"fake overlay", "/overlay-system/00:00:00:ff:ff:ff?overlay=fake", "", 404, "10.10.10.10:9873"}, {"specific overlay", "/overlay-system/00:00:00:ff:ff:ff?overlay=o1", "specific overlay", 200, "10.10.10.10:9873"}, {"find shim", "/efiboot/shim.efi", "", 200, "10.10.10.10:9873"}, {"find shim", "/efiboot/shim.efi", "", 404, "10.10.10.11:9873"}, {"find grub", "/efiboot/grub.efi", "", 200, "10.10.10.10:9873"}, {"find grub", "/efiboot/grub.efi", "", 404, "10.10.10.11:9873"}, {"find initramfs", "/provision/00:00:00:ff:ff:ff?stage=initramfs", "", 200, "10.10.10.10:9873"}, - {"ipxe test with NetDevs and KernelVersion", "/provision/00:00:00:00:00:ff?stage=ipxe", "1.1.1 ifname=net:00:00:00:00:00:ff ", 200, "10.10.10.12:9873"}, - {"find grub.cfg", "/efiboot/grub.cfg", "dracut", 200, "10.10.10.11:9873"}, + {"ipxe test with NetDevs, KernelVersion, and Authority", "/provision/00:00:00:00:00:ff?stage=ipxe", "1.1.1 ifname=net:00:00:00:00:00:ff 10.10.0.1 fd00:10::1 10.10.0.1:9873", 200, "10.10.10.12:9873"}, + {"ipxe ipv6", "/provision/00:00:00:00:00:ff?stage=ipxe", "1.1.1 ifname=net:00:00:00:00:00:ff 10.10.0.1 fd00:10::1 [fd00:10::1]:9873", 200, "[fd00:10::10:12]:9873"}, + {"find grub.cfg", "/efiboot/grub.cfg", "dracut 10.10.0.1:9873", 200, "10.10.10.11:9873"}, } func Test_ProvisionSend(t *testing.T) { env := testenv.New(t) defer env.RemoveAll() - env.WriteFile("etc/warewulf/nodes.conf", `nodeprofiles: + env.WriteFile("/etc/warewulf/nodes.conf", `nodeprofiles: default: image name: suse nodes: @@ -78,8 +79,8 @@ nodes: env.CreateFile("/var/lib/warewulf/chroots/suse/rootfs/usr/lib64/efi/shim.efi") env.CreateFile("/var/lib/warewulf/chroots/suse/rootfs/usr/share/efi/x86_64/grub.efi") env.CreateFile("/var/lib/warewulf/chroots/suse/rootfs/boot/initramfs-1.1.0.img") - env.WriteFile("/etc/warewulf/ipxe/test.ipxe", "{{.KernelVersion}}{{range $devname, $netdev := .NetDevs}}{{if and $netdev.Hwaddr $netdev.Device}} ifname={{$netdev.Device}}:{{$netdev.Hwaddr}} {{end}}{{end}}") - env.WriteFile("/etc/warewulf/grub/grub.cfg.ww", "{{ .Tags.GrubMenuEntry }}") + env.WriteFile("/etc/warewulf/ipxe/test.ipxe", "{{.KernelVersion}}{{range $devname, $netdev := .NetDevs}}{{if and $netdev.Hwaddr $netdev.Device}} ifname={{$netdev.Device}}:{{$netdev.Hwaddr}} {{end}}{{end}} {{.Ipaddr}} {{.Ipaddr6}} {{.Authority}}") + env.WriteFile("/etc/warewulf/grub/grub.cfg.ww", "{{ .Tags.GrubMenuEntry }} {{ .Authority }}") dbErr := LoadNodeDB() assert.NoError(t, dbErr) @@ -87,6 +88,8 @@ nodes: conf := warewulfconf.Get() secureFalse := false conf.Warewulf.SecureP = &secureFalse + conf.Ipaddr = "10.10.0.1" + conf.Ipaddr6 = "fd00:10::1/64" assert.NoError(t, os.MkdirAll(path.Join(conf.Paths.OverlayProvisiondir(), "n1"), 0700)) assert.NoError(t, os.WriteFile(path.Join(conf.Paths.OverlayProvisiondir(), "n1", "__SYSTEM__.img"), []byte("system overlay"), 0600)) assert.NoError(t, os.WriteFile(path.Join(conf.Paths.OverlayProvisiondir(), "n1", "__RUNTIME__.img"), []byte("runtime overlay"), 0600))