Add IPv6 support for Dnsmasq and NetworkManager

Signed-off-by: Timothy Middelkoop <tmiddelkoop@internet2.edu>
This commit is contained in:
Timothy Middelkoop
2025-11-27 01:56:18 +00:00
parent 07eee48a6a
commit 4f58845a9e
25 changed files with 417 additions and 106 deletions

View File

@@ -11,8 +11,8 @@ type DHCPConf struct {
Template string `yaml:"template,omitempty" default:"default"`
RangeStart string `yaml:"range start,omitempty"`
RangeEnd string `yaml:"range end,omitempty"`
Range6Start string `yaml:"range6 start,omitempty"`
Range6End string `yaml:"range6 end,omitempty"`
Range6Start string `yaml:"range6 start,omitempty"`
Range6End string `yaml:"range6 end,omitempty"`
SystemdName string `yaml:"systemd name,omitempty" default:"dhcpd"`
}

View File

@@ -8,10 +8,10 @@ package config
import (
"bytes"
"fmt"
"net"
"os"
"reflect"
"strconv"
"github.com/creasty/defaults"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
@@ -26,11 +26,11 @@ var cachedConf WarewulfYaml
type WarewulfYaml struct {
Comment string `yaml:"comment,omitempty"`
Ipaddr string `yaml:"ipaddr,omitempty"`
Ipaddr6 string `yaml:"ipaddr6,omitempty"`
Netmask string `yaml:"netmask,omitempty"`
Network string `yaml:"network,omitempty"`
IpCIDR6 string `yaml:"-"`
Fqdn string `yaml:"fqdn,omitempty"`
Ipaddr6 string `yaml:"ipaddr6,omitempty"`
PrefixLen6 string `yaml:"prefixlen6,omitempty"`
Warewulf *WarewulfConf `yaml:"warewulf,omitempty"`
API *APIConf `yaml:"api,omitempty"`
DHCP *DHCPConf `yaml:"dhcp,omitempty"`
@@ -140,18 +140,19 @@ func (conf *WarewulfYaml) Parse(data []byte, autodetect bool) error {
}
}
if conf.Ipaddr6 != "" {
if ip, _, err := net.ParseCIDR(conf.Ipaddr6); err == nil {
if ip.To4() != nil {
return fmt.Errorf("invalid ipv6 address: ip address is ipv4: %s", conf.Ipaddr6)
}
conf.IpCIDR6 = conf.Ipaddr6
if ip := net.ParseIP(conf.Ipaddr6); ip != nil {
if prefix, err := strconv.Atoi(conf.PrefixLen6); err == nil {
conf.Ipaddr6 = ip.String()
} else {
return fmt.Errorf("invalid ipv6 address: must use CIDR notation: %s", conf.Ipaddr6)
conf.PrefixLen6 = strconv.Itoa(prefix)
}
}
if ip, net, err := net.ParseCIDR(conf.Ipaddr6); err == nil {
conf.Ipaddr6 = ip.String()
prefixLen6, _ := net.Mask.Size()
conf.PrefixLen6 = strconv.Itoa(prefixLen6)
}
return nil
}
@@ -183,6 +184,28 @@ func (config *WarewulfYaml) IpCIDR() string {
return cidr.String()
}
func (config *WarewulfYaml) IpCIDR6() string {
if config.Ipaddr6 == "" || config.PrefixLen6 == "" {
return ""
}
ip := net.ParseIP(config.Ipaddr6)
if ip == nil || ip.To4() != nil {
return ""
}
prefix, err := strconv.Atoi(config.PrefixLen6)
if err != nil {
return ""
}
cidr := net.IPNet{
IP: ip,
Mask: net.CIDRMask(prefix, 128),
}
if cidr.IP == nil || cidr.Mask == nil {
return ""
}
return cidr.String()
}
// InitializedFromFile returns true if [WarewulfYaml] memory was read from
// a file, or false otherwise.
func (conf *WarewulfYaml) InitializedFromFile() bool {

View File

@@ -153,6 +153,7 @@ ipaddr6: "2001:db8::1/64"
`,
result: `
ipaddr6: "2001:db8::1"
prefixlen6: "64"
warewulf:
autobuild overlays: true
grubboot: false
@@ -421,3 +422,56 @@ func TestNetworkCIDR(t *testing.T) {
})
}
}
func TestIpCIDR6(t *testing.T) {
tests := map[string]struct {
ipaddr6 string
prefixlen6 string
cidr string
}{
"blank": {
ipaddr6: "",
prefixlen6: "",
cidr: "",
},
"ip only": {
ipaddr6: "2001:db8::1",
prefixlen6: "",
cidr: "",
},
"prefix only": {
ipaddr6: "",
prefixlen6: "64",
cidr: "",
},
"full": {
ipaddr6: "2001:db8::1",
prefixlen6: "64",
cidr: "2001:db8::1/64",
},
"invalid ip": {
ipaddr6: "asdf",
prefixlen6: "64",
cidr: "",
},
"invalid prefix": {
ipaddr6: "2001:db8::1",
prefixlen6: "asdf",
cidr: "",
},
"ipv4 address": {
ipaddr6: "192.168.0.1",
prefixlen6: "24",
cidr: "",
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
conf := New()
conf.Ipaddr6 = tt.ipaddr6
conf.PrefixLen6 = tt.prefixlen6
assert.Equal(t, tt.cidr, conf.IpCIDR6())
})
}
}

View File

@@ -75,18 +75,19 @@ type KernelConf struct {
}
type NetDev struct {
Type string `yaml:"type,omitempty" json:"type,omitempty" lopt:"type" sopt:"T" comment:"Set device type of given network"`
OnBoot wwtype.WWbool `yaml:"onboot,omitempty" json:"onbot,omitempty" lopt:"onboot" comment:"Enable/disable network device (true/false)"`
Device string `yaml:"device,omitempty" json:"device,omitempty" lopt:"netdev" sopt:"N" comment:"Set the device for given network"`
Hwaddr string `yaml:"hwaddr,omitempty" json:"hwaddr,omitempty" lopt:"hwaddr" sopt:"H" comment:"Set the device's HW address for given network" type:"MAC"`
Ipaddr net.IP `yaml:"ipaddr,omitempty" json:"ipaddr,omitempty" lopt:"ipaddr" sopt:"I" comment:"IPv4 address in given network" type:"IP"`
Ipaddr6 net.IP `yaml:"ip6addr,omitempty" json:"ip6addr,omitempty" lopt:"ipaddr6" comment:"IPv6 address" type:"IP"`
Prefix net.IP `yaml:"prefix,omitempty" json:"prefix,omitempty"`
Netmask net.IP `yaml:"netmask,omitempty" json:"netmask,omitempty" lopt:"netmask" sopt:"M" comment:"Set the networks netmask" type:"IP"`
Gateway net.IP `yaml:"gateway,omitempty" json:"gateway,omitempty" lopt:"gateway" sopt:"G" comment:"Set the node's network device gateway" type:"IP"`
MTU string `yaml:"mtu,omitempty" json:"mtu,omitempty" lopt:"mtu" comment:"Set the mtu" type:"uint"`
Tags map[string]string `yaml:"tags,omitempty" json:"tags,omitempty"`
primary bool
Type string `yaml:"type,omitempty" json:"type,omitempty" lopt:"type" sopt:"T" comment:"Set device type of given network"`
OnBoot wwtype.WWbool `yaml:"onboot,omitempty" json:"onbot,omitempty" lopt:"onboot" comment:"Enable/disable network device (true/false)"`
Device string `yaml:"device,omitempty" json:"device,omitempty" lopt:"netdev" sopt:"N" comment:"Set the device for given network"`
Hwaddr string `yaml:"hwaddr,omitempty" json:"hwaddr,omitempty" lopt:"hwaddr" sopt:"H" comment:"Set the device's HW address for given network" type:"MAC"`
Ipaddr net.IP `yaml:"ipaddr,omitempty" json:"ipaddr,omitempty" lopt:"ipaddr" sopt:"I" comment:"IPv4 address in given network" type:"IP"`
Netmask net.IP `yaml:"netmask,omitempty" json:"netmask,omitempty" lopt:"netmask" sopt:"M" comment:"Set the networks netmask" type:"IP"`
Gateway net.IP `yaml:"gateway,omitempty" json:"gateway,omitempty" lopt:"gateway" sopt:"G" comment:"Set the node's IPv4 network device gateway" type:"IP"`
Ipaddr6 net.IP `yaml:"ipaddr6,omitempty" json:"ipaddr6,omitempty" lopt:"ipaddr6" comment:"IPv6 address in given network" type:"IP"`
PrefixLen6 string `yaml:"prefixlen6,omitempty" json:"prefixlen6,omitempty" lopt:"prefixlen6" comment:"Set the networks IPv6 prefix length" type:"uint"`
Gateway6 net.IP `yaml:"gateway6,omitempty" json:"gateway6,omitempty" lopt:"gateway6" comment:"Set the node's IPv6 network device gateway" type:"IP"`
MTU string `yaml:"mtu,omitempty" json:"mtu,omitempty" lopt:"mtu" comment:"Set the mtu" type:"uint"`
Tags map[string]string `yaml:"tags,omitempty" json:"tags,omitempty"`
primary bool
}
/*

View File

@@ -191,10 +191,11 @@ func Test_listFields(t *testing.T) {
"NetDevs[default].Device",
"NetDevs[default].Hwaddr",
"NetDevs[default].Ipaddr",
"NetDevs[default].Ipaddr6",
"NetDevs[default].Prefix",
"NetDevs[default].Netmask",
"NetDevs[default].Gateway",
"NetDevs[default].Ipaddr6",
"NetDevs[default].PrefixLen6",
"NetDevs[default].Gateway6",
"NetDevs[default].MTU",
"NetDevs[default].Tags[nettag]",
"Tags[tag]",
@@ -245,10 +246,11 @@ func Test_listFields(t *testing.T) {
"NetDevs[default].Device",
"NetDevs[default].Hwaddr",
"NetDevs[default].Ipaddr",
"NetDevs[default].Ipaddr6",
"NetDevs[default].Prefix",
"NetDevs[default].Netmask",
"NetDevs[default].Gateway",
"NetDevs[default].Ipaddr6",
"NetDevs[default].PrefixLen6",
"NetDevs[default].Gateway6",
"NetDevs[default].MTU",
"NetDevs[default].Tags[nettag]",
"Tags[tag]",

View File

@@ -4,6 +4,7 @@ import (
"net"
"reflect"
"sort"
"strconv"
"strings"
"github.com/warewulf/warewulf/internal/pkg/util"
@@ -426,6 +427,28 @@ func (netdev *NetDev) IpCIDR() string {
return ipCIDR.String()
}
/*
Return the ipv6 address with prefix CIDR format. Aimed for the use in
template, symmetric with IpCIDR.
*/
func (netdev *NetDev) IpCIDR6() string {
if netdev.Ipaddr6 == nil || netdev.Ipaddr6.IsUnspecified() || netdev.PrefixLen6 == "" {
return ""
}
if netdev.Ipaddr6.To4() != nil {
return ""
}
prefix, err := strconv.Atoi(netdev.PrefixLen6)
if err != nil || prefix < 0 || prefix > 128 {
return ""
}
ipCIDR6 := net.IPNet{
IP: netdev.Ipaddr6,
Mask: net.CIDRMask(prefix, 128),
}
return ipCIDR6.String()
}
var unsetValues = []string{"UNSET", "UNDEF"}
func isUnsetValue(value string) bool {

View File

@@ -45,6 +45,59 @@ func Test_IpCIDR(t *testing.T) {
}
}
func Test_IpCIDR6(t *testing.T) {
tests := map[string]struct {
ipaddr net.IP
prefix string
cidr string
}{
"nil": {
ipaddr: nil,
prefix: "",
cidr: "",
},
"ip only": {
ipaddr: net.ParseIP("fd00:10::1"),
prefix: "",
cidr: "",
},
"netmask only": {
ipaddr: nil,
prefix: "64",
cidr: "",
},
"ipv4": {
ipaddr: net.ParseIP("10.0.0.1"),
prefix: "64",
cidr: "",
},
"invalid prefix type": {
ipaddr: net.ParseIP("fd00:10::1"),
prefix: "string",
cidr: "",
},
"invalid prefix": {
ipaddr: net.ParseIP("fd00:10::1"),
prefix: "129",
cidr: "",
},
"working": {
ipaddr: net.ParseIP("fd00:10::1"),
prefix: "64",
cidr: "fd00:10::1/64",
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
n := new(NetDev)
n.Ipaddr6 = tt.ipaddr
n.PrefixLen6 = tt.prefix
assert.Equal(t, tt.cidr, n.IpCIDR6())
})
}
}
func Test_Empty(t *testing.T) {
var netdev NetDev
var netdevPtr *NetDev

View File

@@ -25,12 +25,13 @@ type TemplateStruct struct {
BuildSource string
Ipaddr string
IpCIDR string
Ipaddr6 string
IpCIDR6 string
Netmask string
Network string
NetworkCIDR string
Overlay string
Ipaddr6 string
IpCIDR6 string
PrefixLen6 string
Ipv6 bool
Dhcp warewulfconf.DHCPConf
Nfs warewulfconf.NFSConf
@@ -70,14 +71,15 @@ func InitStruct(overlayName string, nodeData node.Node, allNodes []node.Node) (T
tstruct.Warewulf = *controller.Warewulf
tstruct.Ipaddr = controller.Ipaddr
tstruct.IpCIDR = controller.IpCIDR()
tstruct.Ipaddr6 = controller.Ipaddr6
tstruct.IpCIDR6 = controller.IpCIDR6
if controller.Ipaddr6 != "" && controller.IpCIDR6 != "" {
tstruct.Ipv6 = true
}
tstruct.Netmask = controller.Netmask
tstruct.Network = controller.Network
tstruct.NetworkCIDR = controller.NetworkCIDR()
tstruct.Ipaddr6 = controller.Ipaddr6
tstruct.IpCIDR6 = controller.IpCIDR6()
tstruct.PrefixLen6 = controller.PrefixLen6
if controller.Ipaddr6 != "" && controller.PrefixLen6 != "" {
tstruct.Ipv6 = true
}
// init some convenience vars
tstruct.Id = nodeData.Id()
tstruct.Hostname = nodeData.Id()

View File

@@ -633,21 +633,24 @@ func (legacy *KernelConf) Upgrade(imageName string) (upgraded *node.KernelConf)
}
type NetDev struct {
Default string `yaml:"default"`
Device string `yaml:"device,omitempty"`
Gateway string `yaml:"gateway,omitempty"`
Hwaddr string `yaml:"hwaddr,omitempty"`
IpCIDR string `yaml:"ipcidr,omitempty"`
Ipaddr string `yaml:"ipaddr,omitempty"`
Ipaddr6 string `yaml:"ip6addr,omitempty"`
MTU string `yaml:"mtu,omitempty"`
Netmask string `yaml:"netmask,omitempty"`
OnBoot string `yaml:"onboot,omitempty"`
Prefix string `yaml:"prefix,omitempty"`
Primary string `yaml:"primary,omitempty"`
Tags map[string]string `yaml:"tags,omitempty"`
TagsDel []string `yaml:"tagsdel,omitempty"`
Type string `yaml:"type,omitempty"`
Default string `yaml:"default"`
Device string `yaml:"device,omitempty"`
Gateway string `yaml:"gateway,omitempty"`
Gateway6 string `yaml:"gateway6,omitempty"`
Hwaddr string `yaml:"hwaddr,omitempty"`
IpCIDR string `yaml:"ipcidr,omitempty"`
Ip6addr string `yaml:"ip6addr,omitempty"`
Ipaddr string `yaml:"ipaddr,omitempty"`
Ipaddr6 string `yaml:"ipaddr6,omitempty"`
MTU string `yaml:"mtu,omitempty"`
Netmask string `yaml:"netmask,omitempty"`
OnBoot string `yaml:"onboot,omitempty"`
Prefix string `yaml:"prefix,omitempty"`
PrefixLen6 string `yaml:"prefixlen6,omitempty"`
Primary string `yaml:"primary,omitempty"`
Tags map[string]string `yaml:"tags,omitempty"`
TagsDel []string `yaml:"tagsdel,omitempty"`
Type string `yaml:"type,omitempty"`
}
func (legacy *NetDev) Upgrade(addDefaults bool) (upgraded *node.NetDev) {
@@ -655,9 +658,17 @@ func (legacy *NetDev) Upgrade(addDefaults bool) (upgraded *node.NetDev) {
upgraded.Tags = make(map[string]string)
upgraded.Device = legacy.Device
upgraded.Gateway = net.ParseIP(legacy.Gateway)
upgraded.Gateway6 = net.ParseIP(legacy.Gateway6)
upgraded.Hwaddr = legacy.Hwaddr
upgraded.Ipaddr = net.ParseIP(legacy.Ipaddr)
upgraded.Ipaddr6 = net.ParseIP(legacy.Ipaddr6)
if legacy.Ip6addr != "" {
if legacy.Ipaddr6 != legacy.Ip6addr {
wwlog.Error("Ipaddr6 (%s) and Ip6addr (%s) are both set and not the same, remove Ip6addr", legacy.Ipaddr6, legacy.Ip6addr)
}
upgraded.Ipaddr6 = net.ParseIP(legacy.Ip6addr)
} else {
upgraded.Ipaddr6 = net.ParseIP(legacy.Ipaddr6)
}
upgraded.MTU = legacy.MTU
upgraded.Netmask = net.ParseIP(legacy.Netmask)
if legacy.IpCIDR != "" {
@@ -676,7 +687,10 @@ func (legacy *NetDev) Upgrade(addDefaults bool) (upgraded *node.NetDev) {
if legacy.OnBoot != "" {
warnError(upgraded.OnBoot.Set(legacy.OnBoot))
}
upgraded.Prefix = net.ParseIP(legacy.Prefix)
if legacy.Prefix != "" {
logIgnore("Prefix", legacy.Prefix, "obsolete")
}
upgraded.PrefixLen6 = legacy.PrefixLen6
if legacy.Tags != nil {
for key, value := range legacy.Tags {
upgraded.Tags[key] = value

View File

@@ -220,6 +220,31 @@ nodes:
interface: lanplus
escapechar: "~"
write: "true"
`,
},
{
name: "Rename ip6addr to ipaddr6",
addDefaults: false,
replaceOverlays: false,
legacyYaml: `
nodeprofiles: {}
nodes:
n1:
network devices:
default:
device: wwnet0
hwaddr: e6:92:39:49:7b:03
ip6addr: 2001:db8::111
`,
upgradedYaml: `
nodeprofiles: {}
nodes:
n1:
network devices:
default:
device: wwnet0
hwaddr: e6:92:39:49:7b:03
ipaddr6: 2001:db8::111
`,
},
{

View File

@@ -120,8 +120,8 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) {
}
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 confIpaddr6, err := netip.ParseAddr(conf.Ipaddr6); err == nil {
ipaddr6 = confIpaddr6.String()
}
if rinfoIpaddr, err := netip.ParseAddr(rinfo.ipaddr); err == nil {
if rinfoIpaddr.Is6() {
@@ -226,8 +226,8 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) {
}
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 confIpaddr6, err := netip.ParseAddr(conf.Ipaddr6); err == nil {
ipaddr6 = confIpaddr6.String()
}
if rinfoIpaddr, err := netip.ParseAddr(rinfo.ipaddr); err == nil {
if rinfoIpaddr.Is6() {

View File

@@ -89,7 +89,7 @@ nodes:
secureFalse := false
conf.Warewulf.SecureP = &secureFalse
conf.Ipaddr = "10.10.0.1"
conf.Ipaddr6 = "fd00:10::1/64"
conf.Ipaddr6 = "fd00:10::1"
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))