From b4ba5898eec1623ab8ea1b1b291ca4e7098cfec8 Mon Sep 17 00:00:00 2001 From: SaarLAN-Pissbeutel <183202051+SaarLAN-Pissbeutel@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:06:31 +0100 Subject: [PATCH 1/3] Add support for altering IPv6 addresses in ddclient plugin --- .../controllers/OPNsense/DynDNS/forms/dialogAccount.xml | 7 +++++++ .../src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.xml | 5 +++++ .../src/opnsense/scripts/ddclient/lib/account/__init__.py | 3 ++- dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py | 6 +++++- .../service/templates/OPNsense/ddclient/ddclient.json | 1 + 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/dialogAccount.xml b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/dialogAccount.xml index b47b38ddac..c045d5e163 100644 --- a/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/dialogAccount.xml +++ b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/dialogAccount.xml @@ -91,6 +91,13 @@ dropdown + + account.dynipv6host + + text + true + Swap the host part of the ipv6 address with the given partial ipv6 address + account.checkip_timeout diff --git a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.xml b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.xml index 0888c87ace..600e17517b 100644 --- a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.xml +++ b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.xml @@ -165,6 +165,11 @@ + + N + /^::(([0-9a-fA-F]{1,4}:){0,3}[0-9a-fA-F]{1,4})?$/u + Entry is not a valid partial ipv6 address definition (e.g. ::1000). + 10 Y diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/__init__.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/__init__.py index 4c3716b95e..fd81e32d93 100755 --- a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/__init__.py +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/__init__.py @@ -118,7 +118,8 @@ def execute(self): service = self.settings.get('checkip'), proto = 'https' if self.settings.get('force_ssl', False) else 'http', timeout = str(self.settings.get('checkip_timeout', '10')), - interface = self.settings['interface'] if self.settings.get('interface' ,'').strip() != '' else None + interface = self.settings['interface'] if self.settings.get('interface' ,'').strip() != '' else None, + dynipv6host = self.settings.get('dynipv6host') ) if self._current_address == None: diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py index 5618d9aeb6..e952f7df26 100755 --- a/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py @@ -67,11 +67,12 @@ def extract_address(host, txt): return "" -def checkip(service, proto='https', timeout='10', interface=None): +def checkip(service, proto='https', timeout='10', interface=None, dynipv6host=None): """ find ip address using external services defined in checkip_service_list :param proto: protocol :param timeout: timeout in seconds :param interface: bind to interface + :param dynipv6host: optional partial ipv6 address :return: str """ if service.startswith('web_'): @@ -95,6 +96,9 @@ def checkip(service, proto='https', timeout='10', interface=None): if (parts[0] == 'inet' and service == 'if') or (parts[0] == 'inet6' and service == 'if6'): try: address = ipaddress.ip_address(parts[1]) + # alter dynamic ipv6 address + if dynipv6host and isinstance(address, ipaddress.IPv6Address): + address = ipaddress.ip_address(':'.join(parts[1].split(':')[:4]) + ':' + ':'.join(str(ipaddress.ip_address(dynipv6host).exploded).split(':')[4:])) if address.is_global: return str(address) except ValueError: diff --git a/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/ddclient.json b/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/ddclient.json index 79cf6fbbd1..4a2cc7a852 100644 --- a/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/ddclient.json +++ b/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/ddclient.json @@ -22,6 +22,7 @@ "zone": "{{ account.zone }}", "checkip": "{{ account.checkip }}", "interface": "{% if account.interface %}{{physical_interface(account.interface)}}{% endif %}", + "dynipv6host": "{{ account.dynipv6host }}", "checkip_timeout": {{ account.checkip_timeout }}, "force_ssl": {{ "true" if account.force_ssl == '1' else "false"}}, "ttl": "{{ account.ttl }}", From 20b4786b69aefb4ec863aae7c5dcbb2f95955ab9 Mon Sep 17 00:00:00 2001 From: Marc Philippi Date: Wed, 22 Jan 2025 14:20:28 +0100 Subject: [PATCH 2/3] Refactoring of checkip --- .../OPNsense/DynDNS/forms/dialogAccount.xml | 4 +-- .../scripts/ddclient/lib/account/__init__.py | 2 +- .../opnsense/scripts/ddclient/lib/address.py | 27 +++++++++++++++---- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/dialogAccount.xml b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/dialogAccount.xml index c045d5e163..a3661651c5 100644 --- a/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/dialogAccount.xml +++ b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/dialogAccount.xml @@ -93,10 +93,10 @@ account.dynipv6host - + text true - Swap the host part of the ipv6 address with the given partial ipv6 address + Swap the interface identifier of the ipv6 address with the given partial ipv6 address account.checkip_timeout diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/__init__.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/__init__.py index fd81e32d93..7b600eb579 100755 --- a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/__init__.py +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/__init__.py @@ -119,7 +119,7 @@ def execute(self): proto = 'https' if self.settings.get('force_ssl', False) else 'http', timeout = str(self.settings.get('checkip_timeout', '10')), interface = self.settings['interface'] if self.settings.get('interface' ,'').strip() != '' else None, - dynipv6host = self.settings.get('dynipv6host') + dynipv6host = self.settings['dynipv6host'] if self.settings.get('dynipv6host' ,'').strip() != '' else None ) if self._current_address == None: diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py index e952f7df26..5d9866ccfa 100755 --- a/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py @@ -67,6 +67,24 @@ def extract_address(host, txt): return "" +def transform_ip(ip, ipv6host=None): + """ Changes ipv6 addresses if interface identifier is given + :param ip: ip address + :param ipv6host: 64 bit interface identifier + :return str + """ + if ( + ipv6host and + isinstance(ip, ipaddress.IPv6Address) + ): + # extract 64 bit long prefix + prefix = ':'.join(str(ip).split(':')[:4]) + # normalize 64 bit interface identifier + host = ':'.join(str(ipaddress.ip_address(ipv6host).exploded).split(':')[4:]) + ip = ipaddress.ip_address(f"{prefix}:{host}") + return ip + + def checkip(service, proto='https', timeout='10', interface=None, dynipv6host=None): """ find ip address using external services defined in checkip_service_list :param proto: protocol @@ -85,8 +103,10 @@ def checkip(service, proto='https', timeout='10', interface=None, dynipv6host=No params.append(interface) url = checkip_service_list[service] % proto params.append(url) - return extract_address(urlparse(url).hostname, + extracted_address = extract_address(urlparse(url).hostname, subprocess.run(params, capture_output=True, text=True).stdout) + address = ipaddress.ip_address(extracted_address) + return str(transform_ip(address, dynipv6host)) elif service in ['if', 'if6'] and interface is not None: # return first non private IPv[4|6] interface address ifcfg = subprocess.run(['/sbin/ifconfig', interface], capture_output=True, text=True).stdout @@ -96,11 +116,8 @@ def checkip(service, proto='https', timeout='10', interface=None, dynipv6host=No if (parts[0] == 'inet' and service == 'if') or (parts[0] == 'inet6' and service == 'if6'): try: address = ipaddress.ip_address(parts[1]) - # alter dynamic ipv6 address - if dynipv6host and isinstance(address, ipaddress.IPv6Address): - address = ipaddress.ip_address(':'.join(parts[1].split(':')[:4]) + ':' + ':'.join(str(ipaddress.ip_address(dynipv6host).exploded).split(':')[4:])) if address.is_global: - return str(address) + return str(transform_ip(address, dynipv6host)) except ValueError: continue else: From a3b405ca64acf5fce9bbb1f085bcc427ced9beb8 Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Wed, 22 Jan 2025 20:43:57 +0100 Subject: [PATCH 3/3] dns/ddclient - minor cleanups for https://github.com/opnsense/plugins/pull/4491 * simplify network / host concat a bit * add try...except for the curl fetch in case the other end doesn't return a valid address * extend form help text for "Dynamic ipv6 host" a bit --- .../OPNsense/DynDNS/forms/dialogAccount.xml | 2 +- .../opnsense/scripts/ddclient/lib/address.py | 34 ++++++++++--------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/dialogAccount.xml b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/dialogAccount.xml index a3661651c5..eb0fb768e4 100644 --- a/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/dialogAccount.xml +++ b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/dialogAccount.xml @@ -96,7 +96,7 @@ text true - Swap the interface identifier of the ipv6 address with the given partial ipv6 address + Swap the interface identifier of the ipv6 address with the given partial ipv6 address (the least significant 64 bits of the address) account.checkip_timeout diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py index 5d9866ccfa..d1dc1c6ab1 100755 --- a/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py @@ -1,5 +1,5 @@ """ - Copyright (c) 2022-2023 Ad Schellevis + Copyright (c) 2022-2025 Ad Schellevis All rights reserved. Redistribution and use in source and binary forms, with or without @@ -71,18 +71,17 @@ def transform_ip(ip, ipv6host=None): """ Changes ipv6 addresses if interface identifier is given :param ip: ip address :param ipv6host: 64 bit interface identifier - :return str + :return ipaddress.IPv4Address|ipaddress.IPv6Address + :raises ValueError: If the input can not be converted to an IPaddress """ - if ( - ipv6host and - isinstance(ip, ipaddress.IPv6Address) - ): - # extract 64 bit long prefix - prefix = ':'.join(str(ip).split(':')[:4]) - # normalize 64 bit interface identifier - host = ':'.join(str(ipaddress.ip_address(ipv6host).exploded).split(':')[4:]) - ip = ipaddress.ip_address(f"{prefix}:{host}") - return ip + if ipv6host and ip.find(':') > 0: + # extract 64 bit long prefix and add ipv6host [64]bits + return ipaddress.ip_address( + ipaddress.ip_network("%s/64" % ip, strict=False).network_address.exploded[0:19] + + ipaddress.ip_address(ipv6host).exploded[19:] + ) + else: + return ipaddress.ip_address(ip) def checkip(service, proto='https', timeout='10', interface=None, dynipv6host=None): @@ -105,8 +104,11 @@ def checkip(service, proto='https', timeout='10', interface=None, dynipv6host=No params.append(url) extracted_address = extract_address(urlparse(url).hostname, subprocess.run(params, capture_output=True, text=True).stdout) - address = ipaddress.ip_address(extracted_address) - return str(transform_ip(address, dynipv6host)) + try: + return str(transform_ip(extracted_address, dynipv6host)) + except ValueError: + # invalid address + return "" elif service in ['if', 'if6'] and interface is not None: # return first non private IPv[4|6] interface address ifcfg = subprocess.run(['/sbin/ifconfig', interface], capture_output=True, text=True).stdout @@ -115,9 +117,9 @@ def checkip(service, proto='https', timeout='10', interface=None, dynipv6host=No parts = line.split() if (parts[0] == 'inet' and service == 'if') or (parts[0] == 'inet6' and service == 'if6'): try: - address = ipaddress.ip_address(parts[1]) + address = transform_ip(parts[1], dynipv6host) if address.is_global: - return str(transform_ip(address, dynipv6host)) + return str(address) except ValueError: continue else: