Skip to content

Commit 8a66470

Browse files
committed
Add management plugin that doesn't need IPAM
1 parent 60adafd commit 8a66470

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed

plugins/management/plugin.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: MIT
3+
4+
package management
5+
6+
import (
7+
"net"
8+
"time"
9+
10+
"github.com/ironcore-dev/fedhcp/internal/printer"
11+
12+
"github.com/coredhcp/coredhcp/handler"
13+
"github.com/coredhcp/coredhcp/logger"
14+
"github.com/coredhcp/coredhcp/plugins"
15+
"github.com/insomniacslk/dhcp/dhcpv6"
16+
"github.com/insomniacslk/dhcp/iana"
17+
"github.com/mdlayher/netx/eui64"
18+
)
19+
20+
var log = logger.GetLogger("plugins/managament")
21+
22+
var Plugin = plugins.Plugin{
23+
Name: "management",
24+
Setup6: setup6,
25+
}
26+
27+
const (
28+
preferredLifeTime = 24 * time.Hour
29+
validLifeTime = 24 * time.Hour
30+
)
31+
32+
func setup6(_ ...string) (handler.Handler6, error) {
33+
return handler6, nil
34+
}
35+
36+
func handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
37+
if req == nil {
38+
log.Error("Received nil DHCPv6 request")
39+
return nil, true
40+
}
41+
42+
printer.VerboseRequest(req, log, printer.IPv6)
43+
defer printer.VerboseResponse(req, resp, log, printer.IPv6)
44+
45+
if !req.IsRelay() {
46+
log.Printf("Received non-relay DHCPv6 request, dropping.")
47+
return nil, true
48+
}
49+
50+
relayMsg := req.(*dhcpv6.RelayMessage)
51+
52+
if len(relayMsg.LinkAddr) != 16 {
53+
log.Errorf("Received malformed link address of length %d, dropping.", len(relayMsg.LinkAddr))
54+
return nil, true
55+
}
56+
57+
mac, err := getMAC(relayMsg)
58+
if err != nil {
59+
log.Errorf("Failed to obtain MAC, dropping: %s", err.Error())
60+
return nil, true
61+
}
62+
63+
if len(mac) != 6 {
64+
log.Errorf("Received malformed MAC address of length %d, dropping.", len(mac))
65+
return nil, true
66+
}
67+
68+
ipaddr := make(net.IP, len(relayMsg.LinkAddr))
69+
copy(ipaddr, relayMsg.LinkAddr)
70+
71+
feEUI64(ipaddr, mac)
72+
73+
msg, err := req.GetInnerMessage()
74+
if err != nil {
75+
log.Errorf("BUG: could not decapsulate: %v", err)
76+
return nil, true
77+
}
78+
79+
if msg.Options.OneIANA() == nil {
80+
log.Debug("No address requested")
81+
return resp, false
82+
}
83+
84+
iana := &dhcpv6.OptIANA{
85+
IaId: msg.Options.OneIANA().IaId,
86+
Options: dhcpv6.IdentityOptions{
87+
Options: []dhcpv6.Option{
88+
&dhcpv6.OptIAAddress{
89+
IPv6Addr: ipaddr,
90+
PreferredLifetime: preferredLifeTime,
91+
ValidLifetime: validLifeTime,
92+
},
93+
},
94+
},
95+
}
96+
resp.AddOption(iana)
97+
log.Infof("Client %s, added option IA address %s", mac.String(), iana.String())
98+
99+
return resp, false
100+
}
101+
102+
func getMAC(relayMsg *dhcpv6.RelayMessage) (net.HardwareAddr, error) {
103+
hwType, mac := relayMsg.Options.ClientLinkLayerAddress()
104+
if hwType == iana.HWTypeEthernet {
105+
return mac, nil
106+
}
107+
108+
log.Infof("failed to retrieve client link layer address, falling back to EUI64 (%s)", relayMsg.PeerAddr.String())
109+
_, mac, err := eui64.ParseIP(relayMsg.PeerAddr)
110+
if err != nil {
111+
log.Errorf("Could not parse peer address: %s", err)
112+
return nil, err
113+
}
114+
115+
return mac, nil
116+
}
117+
118+
// feEUI64 adjusts the given IP address in-place by overwriting the host part
119+
// using an adaptation of the EUI64 scheme. The two middle bytes are set to 0xfe
120+
// and the first and last three bytes consist of the corresponding first and
121+
// last three bytes of the MAC address. Any pre-existing host bits will be
122+
// overwritten.
123+
//
124+
// Example:
125+
//
126+
// ipaddr=2001:db8::
127+
// mac=01:23:45:67:89:ab
128+
// result=2001:db8::0123:45fe:fe67:89ab
129+
func feEUI64(ipaddr net.IP, mac net.HardwareAddr) {
130+
// 128 bit == 16 byte, 0-7 are left as-is, 8-15 are modified.
131+
// 11, 12 get set to 0xfe (EUI64 would use 0xff 0xfe)
132+
ipaddr[11] = 0xfe
133+
ipaddr[12] = 0xfe
134+
135+
copy(ipaddr[8:11], mac[0:3])
136+
copy(ipaddr[13:16], mac[3:6])
137+
// TODO: should we flip the 7th bit as EUI64 does it? To me that is just
138+
// confusing so I won't do it for now.
139+
}

plugins/management/plugin_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package management
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"slices"
7+
"testing"
8+
)
9+
10+
func TestFeEUI64(t *testing.T) {
11+
tests := []struct {
12+
ip net.IP
13+
mac net.HardwareAddr
14+
want net.IP
15+
}{{
16+
net.ParseIP("2001:db8::"),
17+
parseMAC("aa:bb:cc:dd:ee:ff"),
18+
net.ParseIP("2001:db8::aabb:ccfe:fedd:eeff"),
19+
}, {
20+
net.ParseIP("2001:db8::"),
21+
parseMAC("01:23:45:67:89:ab"),
22+
net.ParseIP("2001:db8::0123:45fe:fe67:89ab"),
23+
}, {
24+
net.ParseIP("2001:db8::dead:beef"),
25+
parseMAC("aa:bb:cc:dd:ee:ff"),
26+
net.ParseIP("2001:db8::aabb:ccfe:fedd:eeff"),
27+
}}
28+
29+
for ti, tt := range tests {
30+
t.Run(fmt.Sprintf("#%d", ti), func(t *testing.T) {
31+
feEUI64(tt.ip, tt.mac)
32+
if !slices.Equal(tt.ip, tt.want) {
33+
t.Errorf("got=%s != want=%s", tt.ip.String(), tt.want.String())
34+
}
35+
})
36+
}
37+
}
38+
39+
func parseMAC(s string) net.HardwareAddr {
40+
a, err := net.ParseMAC(s)
41+
if err != nil {
42+
panic(err.Error())
43+
}
44+
45+
return a
46+
}

0 commit comments

Comments
 (0)