1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 03:36:48 +00:00

fix(interfaces): a host interface can be created with DHCP or no IPv4, and every value is validated

The IPv4 address and gateway were required even for DHCP or no IPv4, so
such an interface could not be created; malformed addresses passed and a
missing prefix crashed the XML builder. They are validated like the IPv6
fields now (netmask notation accepted, IPv6 scope ids rejected), and a
static address without a gateway gets no empty route. Interface names
follow the Linux rule (up to 15 letters, digits, . _ -). A bridge member,
or the device an ethernet interface configures, must be a device of the
host (the page no longer falls back to made-up names), a bridge cannot
contain itself, and needs STP and a non-negative delay.
This commit is contained in:
catborise 2026-10-07 20:23:39 +03:00
parent 3c0d5efb18
commit 443b31d86b
5 changed files with 229 additions and 58 deletions

View file

@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
class AddInterface(forms.Form):
name = forms.CharField(max_length=10, required=True)
name = forms.CharField(max_length=15, required=True)
itype = forms.ChoiceField(
required=True, choices=(("bridge", "bridge"), ("ethernet", "ethernet"))
)
@ -19,7 +19,8 @@ class AddInterface(forms.Form):
required=True,
choices=(("dhcp", "dhcp"), ("static", "static"), ("none", "none")),
)
ipv4_addr = forms.CharField(max_length=18, required=False)
# room for a dotted netmask: 192.0.2.10/255.255.255.0
ipv4_addr = forms.CharField(max_length=31, required=False)
ipv4_gw = forms.CharField(max_length=15, required=False)
ipv6_type = forms.ChoiceField(
required=True,
@ -28,81 +29,92 @@ class AddInterface(forms.Form):
ipv6_addr = forms.CharField(max_length=100, required=False)
ipv6_gw = forms.CharField(max_length=100, required=False)
stp = forms.ChoiceField(required=False, choices=(("on", "on"), ("off", "off")))
delay = forms.IntegerField(required=False)
delay = forms.IntegerField(required=False, min_value=0)
def __init__(self, *args, netdevs=(), **kwargs):
# The host's network devices: a bridge member, or the device an
# ethernet interface configures, must be one of them.
super().__init__(*args, **kwargs)
self.netdevs = set(netdevs)
def clean_ipv4_addr(self):
ipv4_addr = self.cleaned_data["ipv4_addr"]
have_symbol = re.match("^[0-9./]+$", ipv4_addr)
if not have_symbol:
raise forms.ValidationError(
_("The IPv4 address must not contain any special characters")
)
elif len(ipv4_addr) > 20:
raise forms.ValidationError(
_("The IPv4 address must not exceed 20 characters")
)
return ipv4_addr
if not ipv4_addr:
return ipv4_addr
try:
# Always address/prefix, as the interface XML needs both
return str(ipaddress.IPv4Interface(ipv4_addr))
except ValueError:
raise forms.ValidationError(_("The IPv4 address is not valid (e.g. 192.0.2.10/24)"))
def clean_ipv4_gw(self):
ipv4_gw = self.cleaned_data["ipv4_gw"]
have_symbol = re.match("^[0-9.]+$", ipv4_gw)
if not have_symbol:
raise forms.ValidationError(
_("The IPv4 gateway must not contain any special characters")
)
elif len(ipv4_gw) > 20:
raise forms.ValidationError(
_("The IPv4 gateway must not exceed 20 characters")
)
return ipv4_gw
if not ipv4_gw:
return ipv4_gw
try:
return str(ipaddress.IPv4Address(ipv4_gw))
except ValueError:
raise forms.ValidationError(_("The IPv4 gateway is not valid"))
# libvirt's interface schema has no IPv6 scope id (fe80::1%eth0)
def clean_ipv6_addr(self):
ipv6_addr = self.cleaned_data["ipv6_addr"]
if not ipv6_addr:
return ipv6_addr
try:
# Always address/prefix, as the interface XML needs both
return str(ipaddress.IPv6Interface(ipv6_addr))
address = ipaddress.IPv6Interface(ipv6_addr)
except ValueError:
address = None
if address is None or address.scope_id is not None:
raise forms.ValidationError(_("The IPv6 address is not valid (e.g. 2001:db8::10/64)"))
# Always address/prefix, as the interface XML needs both
return str(address)
def clean_ipv6_gw(self):
ipv6_gw = self.cleaned_data["ipv6_gw"]
if not ipv6_gw:
return ipv6_gw
try:
return str(ipaddress.IPv6Address(ipv6_gw))
gateway = ipaddress.IPv6Address(ipv6_gw)
except ValueError:
gateway = None
if gateway is None or gateway.scope_id is not None:
raise forms.ValidationError(_("The IPv6 gateway is not valid"))
def clean(self):
cleaned = super().clean()
if cleaned.get("ipv6_type") == "static" and not cleaned.get("ipv6_addr") and "ipv6_addr" not in self.errors:
self.add_error("ipv6_addr", _("A static IPv6 configuration needs an address"))
return cleaned
return str(gateway)
def clean_name(self):
# A Linux interface name: up to 15 characters, no spaces or slashes
name = self.cleaned_data["name"]
have_symbol = re.match("^[a-z0-9.]+$", name)
if not have_symbol:
if not re.fullmatch(r"[A-Za-z0-9_.-]{1,15}", name) or name in (".", ".."):
raise forms.ValidationError(
_("The interface must not contain any special characters")
)
elif len(name) > 10:
raise forms.ValidationError(
_("The interface must not exceed 10 characters")
_("The interface name may have up to 15 letters, digits and . _ -")
)
return name
def clean_netdev(self):
netdev = self.cleaned_data["netdev"]
have_symbol = re.match("^[a-z0-9.:]+$", netdev)
if not have_symbol:
if not re.fullmatch(r"[A-Za-z0-9_.-]{1,15}", netdev) or netdev in (".", ".."):
raise forms.ValidationError(
_("The interface must not contain any special characters")
)
elif len(netdev) > 10:
raise forms.ValidationError(
_("The interface must not exceed 10 characters")
_("The device name may have up to 15 letters, digits and . _ -")
)
return netdev
def clean(self):
cleaned = super().clean()
for family in ("ipv4", "ipv6"):
addr = f"{family}_addr"
if cleaned.get(f"{family}_type") == "static" and not cleaned.get(addr) and addr not in self.errors:
self.add_error(addr, _("A static configuration needs an address"))
name, netdev = cleaned.get("name"), cleaned.get("netdev")
if cleaned.get("itype") == "bridge":
if not cleaned.get("stp") and "stp" not in self.errors:
self.add_error("stp", _("A bridge needs STP on or off"))
if cleaned.get("delay") is None and "delay" not in self.errors:
self.add_error("delay", _("A bridge needs a forward delay"))
if netdev and netdev not in self.netdevs:
self.add_error("netdev", _("The device is not on this host"))
elif netdev and netdev == name:
self.add_error("netdev", _("A bridge cannot contain itself"))
elif cleaned.get("itype") == "ethernet" and name and name not in self.netdevs:
self.add_error("name", _("An ethernet interface configures a device of this host"))
return cleaned