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..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 @@ -91,6 +91,13 @@ dropdown + + account.dynipv6host + + text + true + 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/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..7b600eb579 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['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 5618d9aeb6..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 @@ -67,11 +67,29 @@ def extract_address(host, txt): return "" -def checkip(service, proto='https', timeout='10', interface=None): +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 ipaddress.IPv4Address|ipaddress.IPv6Address + :raises ValueError: If the input can not be converted to an IPaddress + """ + 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): """ 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_'): @@ -84,8 +102,13 @@ def checkip(service, proto='https', timeout='10', interface=None): 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) + 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 @@ -94,7 +117,7 @@ def checkip(service, proto='https', timeout='10', interface=None): 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(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 }}",