Skip to content

Commit

Permalink
dns/ddclient: Add support for altering IPv6 addresses in ddclient plu…
Browse files Browse the repository at this point in the history
…gin (#4497)

* Add support for altering IPv6 addresses in ddclient plugin

* Refactoring of checkip

* dns/ddclient - minor cleanups for #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

---------

Co-authored-by: SaarLAN-Pissbeutel <[email protected]>
Co-authored-by: Marc Philippi <[email protected]>
  • Loading branch information
3 people authored Jan 23, 2025
1 parent 2b17488 commit 8606b35
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@
<label>Interface to monitor</label>
<type>dropdown</type>
</field>
<field>
<id>account.dynipv6host</id>
<label>Dynamic ipv6 host</label>
<type>text</type>
<advanced>true</advanced>
<help>Swap the interface identifier of the ipv6 address with the given partial ipv6 address (the least significant 64 bits of the address)</help>
</field>
<field>
<id>account.checkip_timeout</id>
<label>Check ip timeout</label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@
</check001>
</Constraints>
</checkip>
<dynipv6host type="TextField">
<Required>N</Required>
<mask>/^::(([0-9a-fA-F]{1,4}:){0,3}[0-9a-fA-F]{1,4})?$/u</mask>
<ValidationMessage>Entry is not a valid partial ipv6 address definition (e.g. ::1000).</ValidationMessage>
</dynipv6host>
<checkip_timeout type="IntegerField">
<Default>10</Default>
<Required>Y</Required>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
31 changes: 27 additions & 4 deletions dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Copyright (c) 2022-2023 Ad Schellevis <[email protected]>
Copyright (c) 2022-2025 Ad Schellevis <[email protected]>
All rights reserved.
Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -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_'):
Expand All @@ -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
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}",
Expand Down

0 comments on commit 8606b35

Please sign in to comment.