From ce656d076bdebb2d53688f877472b1c0e915c5db Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Wed, 26 May 2021 18:20:16 +0200 Subject: [PATCH] Firewall / Aliases - add "Dynamic IPv6 Host" type. closes https://github.com/opnsense/core/issues/4923 --- .../app/models/OPNsense/Firewall/Alias.xml | 16 ++++ .../Firewall/FieldTypes/AliasContentField.php | 26 +++++++ .../app/views/OPNsense/Firewall/alias.volt | 24 ++++++ src/opnsense/scripts/filter/lib/alias.py | 6 ++ src/opnsense/scripts/filter/lib/interface.py | 75 +++++++++++++++++++ .../OPNsense/Filter/filter_tables.conf | 5 ++ 6 files changed, 152 insertions(+) create mode 100755 src/opnsense/scripts/filter/lib/interface.py diff --git a/src/opnsense/mvc/app/models/OPNsense/Firewall/Alias.xml b/src/opnsense/mvc/app/models/OPNsense/Firewall/Alias.xml index cb707ad26..4f1a88657 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Firewall/Alias.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/Alias.xml @@ -37,7 +37,13 @@ Network group MAC address External (advanced) + Dynamic IPv6 Host + + + interface.check001 + + Y @@ -46,6 +52,16 @@ IPv6 + + + + IPv6 Dynamic Host require an interface to track. + SetIfConstraint + type + dynipv6host + + + 0 diff --git a/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php b/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php index 2cba1668b..7c8f3f6b4 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php +++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php @@ -225,6 +225,26 @@ class AliasContentField extends BaseField return $messages; } + /** + * Validate partial ipv6 network definition + * @param array $data to validate + * @return bool|Callback + * @throws \OPNsense\Base\ModelException + */ + private function validatePartialIPv6Network($data) + { + $messages = array(); + foreach ($this->getItems($data) as $pnetwork) { + if (!Util::isIpAddress("0000" . $pnetwork)) { + $messages[] = sprintf( + gettext('Entry "%s" is not a valid partial ipv6 address definition (e.g. ::1000).'), + $pnetwork + ); + } + } + return $messages; + } + /** * Validate host options * @param array $data to validate @@ -307,6 +327,12 @@ class AliasContentField extends BaseField } ]); break; + case "dynipv6host": + $validators[] = new CallbackValidator(["callback" => function ($data) { + return $this->validatePartialIPv6Network($data); + } + ]); + break; default: break; } diff --git a/src/opnsense/mvc/app/views/OPNsense/Firewall/alias.volt b/src/opnsense/mvc/app/views/OPNsense/Firewall/alias.volt index 9cf7a5846..96ecc9ede 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Firewall/alias.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Firewall/alias.volt @@ -206,6 +206,7 @@ $("#alias\\.type").change(function(){ $(".alias_type").hide(); $("#row_alias\\.updatefreq").hide(); + $("#row_alias\\.interface").hide(); $("#copy-paste").hide(); switch ($(this).val()) { case 'geoip': @@ -218,6 +219,11 @@ $("#alias_type_networkgroup").show(); $("#alias\\.proto").selectpicker('hide'); break; + case 'dynipv6host': + $("#row_alias\\.interface").show(); + $("#alias\\.proto").selectpicker('hide'); + $("#alias_type_default").show(); + break; case 'urltable': $("#row_alias\\.updatefreq").show(); /* FALLTROUGH */ @@ -500,6 +506,7 @@ + @@ -721,6 +728,23 @@ + + +
+ + {{lang._('Interface')}} +
+ + + + + + + + +
diff --git a/src/opnsense/scripts/filter/lib/alias.py b/src/opnsense/scripts/filter/lib/alias.py index 70ffa6a56..24c80def0 100755 --- a/src/opnsense/scripts/filter/lib/alias.py +++ b/src/opnsense/scripts/filter/lib/alias.py @@ -37,6 +37,7 @@ from hashlib import md5 from . import geoip from . import net_wildcard_iterator, AsyncDNSResolver from .arpcache import ArpCache +from .interface import InterfaceParser class Alias(object): def __init__(self, elem, known_aliases=[], ttl=-1, ssl_no_verify=False, timeout=120): @@ -56,6 +57,7 @@ class Alias(object): self._timeout = timeout self._name = None self._type = None + self._interface = None self._proto = 'IPv4,IPv6' self._items = list() self._resolve_content = set() @@ -66,6 +68,8 @@ class Alias(object): self._proto = subelem.text elif subelem.tag == 'name': self._name = subelem.text + elif subelem.tag == 'interface': + self._interface = subelem.text elif subelem.tag == 'ttl': tmp = subelem.text.strip() if len(tmp.split('.')) <= 2 and tmp.replace('.', '').isdigit(): @@ -263,6 +267,8 @@ class Alias(object): return self._fetch_url elif self._type == 'geoip': return self._fetch_geo + elif self._type == 'dynipv6host': + return InterfaceParser(self._interface).iter_dynipv6host elif self._type == 'mac': return ArpCache().iter_addresses else: diff --git a/src/opnsense/scripts/filter/lib/interface.py b/src/opnsense/scripts/filter/lib/interface.py new file mode 100755 index 000000000..88ce8c9ba --- /dev/null +++ b/src/opnsense/scripts/filter/lib/interface.py @@ -0,0 +1,75 @@ +""" + Copyright (c) 2021 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +""" +import ipaddress +import subprocess + + +class InterfaceParser: + """ Interface address parser + """ + _ipv6_networks = dict() + + @classmethod + def _update(cls): + this_interface = None + for line in subprocess.run(['/sbin/ifconfig'], capture_output=True, text=True).stdout.split('\n'): + if not line.startswith("\t") and line.find(':') > -1: + this_interface = line.strip().split(':')[0] + elif this_interface is not None and line.startswith("\tinet6"): + parts = line.strip().split() + addr = None + mask = None + for i in range(len(parts)): + if parts[i] == 'inet6': + addr = parts[i+1].split("%")[0] + elif parts[i] == 'prefixlen': + mask = parts[i+1] + if this_interface not in cls._ipv6_networks: + cls._ipv6_networks[this_interface] = [] + if mask and addr: + cls._ipv6_networks[this_interface].append({"addr": ipaddress.IPv6Address(addr), "mask": mask}) + + def __init__(self, interface): + self._interface = interface + # collect addresses on class init (singleton) + if len(self._ipv6_networks) == 0: + self._update() + + def iter_dynipv6host(self, pattern): + if self._interface in self._ipv6_networks: + for network in self._ipv6_networks[self._interface]: + # only global addresses apply + if network["addr"].is_global: + base_mask = int(network["mask"]) + base_size=int((128-base_mask)/16) + offset_address = ipaddress.IPv6Address('0' + pattern.split("/")[0]) + calculated_address = ':'.join( + network["addr"].exploded.split(':')[:8-base_size] + + offset_address.exploded.split(':')[8-base_size:] + ) + calculated_mask = pattern.split("/")[1] if pattern.find("/") > -1 else "128" + yield "%s/%s" % (calculated_address, calculated_mask) diff --git a/src/opnsense/service/templates/OPNsense/Filter/filter_tables.conf b/src/opnsense/service/templates/OPNsense/Filter/filter_tables.conf index 4deb0b013..a28123ac9 100644 --- a/src/opnsense/service/templates/OPNsense/Filter/filter_tables.conf +++ b/src/opnsense/service/templates/OPNsense/Filter/filter_tables.conf @@ -5,6 +5,7 @@ {% endif %} {% set new_style_aliases = 0 %} +{% from 'OPNsense/Macros/interface.macro' import physical_interface %} {% if helpers.exists('OPNsense.Firewall.Alias.aliases.alias') %} {% set new_style_aliases = OPNsense.Firewall.Alias.aliases.alias|length %} {% for alias in helpers.toList('OPNsense.Firewall.Alias.aliases.alias') %} @@ -21,6 +22,8 @@ {{ alias.content|e|encode_idna }} {% elif alias.content %}
{{ alias.content|e|encode_idna }}
+{% endif %}{% if alias.interface and alias.type == 'dynipv6host' %} + {{ physical_interface(alias.interface)|default('LAN')}} {% endif %}{% if alias.proto %} {{ alias.proto|e }} {% endif %}{% if alias.updatefreq %} @@ -31,6 +34,8 @@ {{ system.aliasesresolveinterval|default('300') }} {% elif alias.type == 'mac' %} 30 +{% elif alias.type == 'dynipv6host' %} + 1 {% endif %} {% endif %}