From 0b6fee5649025d27d344115ebfa8dc43f7da4807 Mon Sep 17 00:00:00 2001 From: Maximilian Moehl Date: Fri, 14 Nov 2025 15:20:17 +0100 Subject: [PATCH] Add management plugin that doesn't need IPAM --- plugins/management/plugin.go | 139 ++++++++++++++++++++++++++++++ plugins/management/plugin_test.go | 49 +++++++++++ 2 files changed, 188 insertions(+) create mode 100644 plugins/management/plugin.go create mode 100644 plugins/management/plugin_test.go diff --git a/plugins/management/plugin.go b/plugins/management/plugin.go new file mode 100644 index 0000000..a3ddbe7 --- /dev/null +++ b/plugins/management/plugin.go @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: MIT + +package management + +import ( + "net" + "time" + + "github.com/ironcore-dev/fedhcp/internal/printer" + + "github.com/coredhcp/coredhcp/handler" + "github.com/coredhcp/coredhcp/logger" + "github.com/coredhcp/coredhcp/plugins" + "github.com/insomniacslk/dhcp/dhcpv6" + "github.com/insomniacslk/dhcp/iana" + "github.com/mdlayher/netx/eui64" +) + +var log = logger.GetLogger("plugins/managament") + +var Plugin = plugins.Plugin{ + Name: "management", + Setup6: setup6, +} + +const ( + preferredLifeTime = 24 * time.Hour + validLifeTime = 24 * time.Hour +) + +func setup6(_ ...string) (handler.Handler6, error) { + return handler6, nil +} + +func handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) { + if req == nil { + log.Error("Received nil DHCPv6 request") + return nil, true + } + + printer.VerboseRequest(req, log, printer.IPv6) + defer printer.VerboseResponse(req, resp, log, printer.IPv6) + + if !req.IsRelay() { + log.Printf("Received non-relay DHCPv6 request, dropping.") + return nil, true + } + + relayMsg := req.(*dhcpv6.RelayMessage) + + if len(relayMsg.LinkAddr) != 16 { + log.Errorf("Received malformed link address of length %d, dropping.", len(relayMsg.LinkAddr)) + return nil, true + } + + mac, err := getMAC(relayMsg) + if err != nil { + log.Errorf("Failed to obtain MAC, dropping: %s", err.Error()) + return nil, true + } + + if len(mac) != 6 { + log.Errorf("Received malformed MAC address of length %d, dropping.", len(mac)) + return nil, true + } + + ipaddr := make(net.IP, len(relayMsg.LinkAddr)) + copy(ipaddr, relayMsg.LinkAddr) + + feEUI64(ipaddr, mac) + + msg, err := req.GetInnerMessage() + if err != nil { + log.Errorf("BUG: could not decapsulate: %v", err) + return nil, true + } + + if msg.Options.OneIANA() == nil { + log.Debug("No address requested") + return resp, false + } + + iana := &dhcpv6.OptIANA{ + IaId: msg.Options.OneIANA().IaId, + Options: dhcpv6.IdentityOptions{ + Options: []dhcpv6.Option{ + &dhcpv6.OptIAAddress{ + IPv6Addr: ipaddr, + PreferredLifetime: preferredLifeTime, + ValidLifetime: validLifeTime, + }, + }, + }, + } + resp.AddOption(iana) + log.Infof("Client %s, added option IA address %s", mac.String(), iana.String()) + + return resp, false +} + +func getMAC(relayMsg *dhcpv6.RelayMessage) (net.HardwareAddr, error) { + hwType, mac := relayMsg.Options.ClientLinkLayerAddress() + if hwType == iana.HWTypeEthernet { + return mac, nil + } + + log.Infof("failed to retrieve client link layer address, falling back to EUI64 (%s)", relayMsg.PeerAddr.String()) + _, mac, err := eui64.ParseIP(relayMsg.PeerAddr) + if err != nil { + log.Errorf("Could not parse peer address: %s", err) + return nil, err + } + + return mac, nil +} + +// feEUI64 adjusts the given IP address in-place by overwriting the host part +// using an adaptation of the EUI64 scheme. The two middle bytes are set to 0xfe +// and the first and last three bytes consist of the corresponding first and +// last three bytes of the MAC address. Any pre-existing host bits will be +// overwritten. +// +// Example: +// +// ipaddr=2001:db8:: +// mac=01:23:45:67:89:ab +// result=2001:db8::0123:45fe:fe67:89ab +func feEUI64(ipaddr net.IP, mac net.HardwareAddr) { + // 128 bit == 16 byte, 0-7 are left as-is, 8-15 are modified. + // 11, 12 get set to 0xfe (EUI64 would use 0xff 0xfe) + ipaddr[11] = 0xfe + ipaddr[12] = 0xfe + + copy(ipaddr[8:11], mac[0:3]) + copy(ipaddr[13:16], mac[3:6]) + // TODO: should we flip the 7th bit as EUI64 does it? To me that is just + // confusing so I won't do it for now. +} diff --git a/plugins/management/plugin_test.go b/plugins/management/plugin_test.go new file mode 100644 index 0000000..5c5ecff --- /dev/null +++ b/plugins/management/plugin_test.go @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: MIT + +package management + +import ( + "fmt" + "net" + "slices" + "testing" +) + +func TestFeEUI64(t *testing.T) { + tests := []struct { + ip net.IP + mac net.HardwareAddr + want net.IP + }{{ + net.ParseIP("2001:db8::"), + parseMAC("aa:bb:cc:dd:ee:ff"), + net.ParseIP("2001:db8::aabb:ccfe:fedd:eeff"), + }, { + net.ParseIP("2001:db8::"), + parseMAC("01:23:45:67:89:ab"), + net.ParseIP("2001:db8::0123:45fe:fe67:89ab"), + }, { + net.ParseIP("2001:db8::dead:beef"), + parseMAC("aa:bb:cc:dd:ee:ff"), + net.ParseIP("2001:db8::aabb:ccfe:fedd:eeff"), + }} + + for ti, tt := range tests { + t.Run(fmt.Sprintf("#%d", ti), func(t *testing.T) { + feEUI64(tt.ip, tt.mac) + if !slices.Equal(tt.ip, tt.want) { + t.Errorf("got=%s != want=%s", tt.ip.String(), tt.want.String()) + } + }) + } +} + +func parseMAC(s string) net.HardwareAddr { + a, err := net.ParseMAC(s) + if err != nil { + panic(err.Error()) + } + + return a +}