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: