dns/ddclient - move checkip properties from general settings to account and implement interface selection using curl. closes https://github.com/opnsense/plugins/issues/2863

o adds migration taking previous use_interface toggle into account
o embed fetch address into new script /usr/local/opnsense/scripts/ddclient/checkip
o guess protocol as the result can't contain two addresses anyway (service determines protocol)
o add OVH, provided by @toxic0berliner
This commit is contained in:
Ad Schellevis
2022-03-13 17:38:28 +01:00
parent b0fcd82305
commit d408dbb648
10 changed files with 209 additions and 118 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
PLUGIN_NAME= ddclient
PLUGIN_VERSION= 1.2
PLUGIN_VERSION= 1.3
PLUGIN_DEPENDS= ddclient
PLUGIN_COMMENT= Dynamic DNS client
PLUGIN_MAINTAINER= ad@opnsense.org
+5
View File
@@ -6,6 +6,11 @@ WWW: https://github.com/ddclient/ddclient
Plugin Changelog
================
1.3
* Add checkip settings per account using selected source interface when provided
* Add OVH DynHost to the DynDNS providers
1.2
* Add Loopia (contributed by Johan Lilja)
@@ -45,10 +45,6 @@ class AccountsController extends ApiMutableModelControllerBase
"description"
);
foreach ($result['rows'] as &$row) {
if ($row['use_interface'] == "0") {
$row['interface'] = "";
}
unset($row['use_interface']);
if ($row['service'] == 'Custom') {
$row['service'] = 'Custom (' . $row['protocol'] . ')';
}
@@ -60,14 +60,20 @@
<help>Hostname to update</help>
</field>
<field>
<id>account.use_interface</id>
<label>Use interface IP</label>
<id>account.checkip</id>
<label>Check ip method</label>
<type>dropdown</type>
<help>How to determine the address to use for this host</help>
</field>
<field>
<id>account.force_ssl</id>
<label>Force SSL</label>
<type>checkbox</type>
<help>Use the IP of a specified interface for the update</help>
<help>Force update using HTTPS</help>
</field>
<field>
<id>account.interface</id>
<label>Interface</label>
<label>Interface to monitor</label>
<type>dropdown</type>
</field>
<field>
@@ -12,29 +12,10 @@
<advanced>true</advanced>
<help>Enable verbose logging</help>
</field>
<field>
<id>ddclient.general.force_ssl</id>
<label>Force SSL</label>
<type>checkbox</type>
<advanced>true</advanced>
<help>Force update using HTTPS</help>
</field>
<field>
<id>ddclient.general.daemon_delay</id>
<label>Interval</label>
<type>text</type>
<help>Interval in seconds to check for address changes</help>
</field>
<field>
<id>ddclient.general.checkip</id>
<label>Check ip method</label>
<type>dropdown</type>
<help>How to determine the address to use for this host</help>
</field>
<field>
<id>ddclient.general.interface</id>
<label>Interface</label>
<type>dropdown</type>
<style>optional_setting checkip_if</style>
</field>
</form>
@@ -1,6 +1,6 @@
<model>
<mount>//OPNsense/DynDNS</mount>
<version>1.1.0</version>
<version>1.2.0</version>
<description>
Dynamic DNS client
</description>
@@ -14,44 +14,12 @@
<default>0</default>
<Required>Y</Required>
</verbose>
<force_ssl type="BooleanField">
<default>1</default>
<Required>Y</Required>
</force_ssl>
<daemon_delay type="IntegerField">
<default>300</default>
<Required>Y</Required>
<MinimumValue>1</MinimumValue>
<MaximumValue>86400</MaximumValue>
</daemon_delay>
<checkip type="OptionField">
<Required>Y</Required>
<default>web_dyndns</default>
<ValidationMessage>An IP service type is required.</ValidationMessage>
<OptionValues>
<web_dyndns>dyndns</web_dyndns>
<web_freedns>freedns</web_freedns>
<web_googledomains>googledomains</web_googledomains>
<web_he>he</web_he>
<web_ip4only_me value="web_ip4only.me">ip4only.me</web_ip4only_me>
<web_ip6only_me value="web_ip6only.me">ip6only.me</web_ip6only_me>
<web_ipify_ipv4 value="web_ipify-ipv4">ipify-ipv4</web_ipify_ipv4>
<web_ipify_ipv6 value="web_ipify-ipv6">ipify-ipv6</web_ipify_ipv6>
<web_loopia>loopia</web_loopia>
<web_myonlineportal>myonlineportal</web_myonlineportal>
<web_noip_ipv4 value="web_noip-ipv4">noip-ipv4</web_noip_ipv4>
<web_noip_ipv6 value="web_noip-ipv6">noip-ipv6</web_noip_ipv6>
<web_nsupdate_info_ipv4 value="web_nsupdate.info-ipv4">nsupdate.info-ipv4</web_nsupdate_info_ipv4>
<web_nsupdate_info_ipv6 value="web_nsupdate.info-ipv6">nsupdate.info-ipv6</web_nsupdate_info_ipv6>
<web_zoneedit>zoneedit</web_zoneedit>
<if>Interface</if>
</OptionValues>
</checkip>
<interface type="InterfaceField">
<Required>N</Required>
<multiple>N</multiple>
<default>wan</default>
</interface>
</general>
<accounts>
<account type="ArrayField">
@@ -86,6 +54,7 @@
<spdyn>spDYN</spdyn>
<strato>STRATO</strato>
<zoneedit1>Zoneedit</zoneedit1>
<ovh>OVH DynHost</ovh>
<custom>Custom</custom>
</OptionValues>
</service>
@@ -127,10 +96,33 @@
<Required>N</Required>
<IpAllowed>N</IpAllowed>
</zone>
<use_interface type="BooleanField">
<default>0</default>
<checkip type="OptionField">
<Required>Y</Required>
</use_interface>
<default>web_dyndns</default>
<ValidationMessage>An IP service type is required.</ValidationMessage>
<OptionValues>
<web_dyndns>dyndns</web_dyndns>
<web_freedns>freedns</web_freedns>
<web_googledomains>googledomains</web_googledomains>
<web_he>he</web_he>
<web_ip4only_me value="web_ip4only.me">ip4only.me</web_ip4only_me>
<web_ip6only_me value="web_ip6only.me">ip6only.me</web_ip6only_me>
<web_ipify_ipv4 value="web_ipify-ipv4">ipify-ipv4</web_ipify_ipv4>
<web_ipify_ipv6 value="web_ipify-ipv6">ipify-ipv6</web_ipify_ipv6>
<web_loopia>loopia</web_loopia>
<web_myonlineportal>myonlineportal</web_myonlineportal>
<web_noip_ipv4 value="web_noip-ipv4">noip-ipv4</web_noip_ipv4>
<web_noip_ipv6 value="web_noip-ipv6">noip-ipv6</web_noip_ipv6>
<web_nsupdate_info_ipv4 value="web_nsupdate.info-ipv4">nsupdate.info-ipv4</web_nsupdate_info_ipv4>
<web_nsupdate_info_ipv6 value="web_nsupdate.info-ipv6">nsupdate.info-ipv6</web_nsupdate_info_ipv6>
<web_zoneedit>zoneedit</web_zoneedit>
<if>Interface</if>
</OptionValues>
</checkip>
<force_ssl type="BooleanField">
<default>1</default>
<Required>Y</Required>
</force_ssl>
<interface type="InterfaceField">
<Required>N</Required>
<multiple>N</multiple>
@@ -0,0 +1,70 @@
<?php
/*
* Copyright (C) 2022 Deciso B.V.
* 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.
*/
namespace OPNsense\DynDNS\Migrations;
use OPNsense\Base\BaseModelMigration;
use OPNsense\Core\Config;
class M1_2_0 extends BaseModelMigration
{
/**
* Migrate older models into shared model
* @param $model
*/
public function run($model)
{
$config = Config::getInstance()->object();
if (empty($config->OPNsense->DynDNS)) {
return;
}
// migration will move these settings, extract datapoints from raw config
$checkip = (string)$config->OPNsense->DynDNS->general->checkip;
$interface = $checkip == "if" ? (string)$config->OPNsense->DynDNS->general->interface : "";
$force_ssl = (string)$config->OPNsense->DynDNS->general->force_ssl;
$pre_account = [];
if (!empty($config->OPNsense->DynDNS->accounts->account)) {
foreach ($config->OPNsense->DynDNS->accounts->account as $account) {
$pre_account[(string)$account->attributes()['uuid']] = [
"checkip" => !empty($account->use_interface) ? "if" : $checkip,
"interface" => !empty($account->use_interface) ? (string)$account->interface : $interface
];
}
}
// update accounts
foreach ($model->accounts->account->iterateItems() as $account) {
$uuid = $account->getAttributes()['uuid'];
$account->checkip = $pre_account[$uuid]['checkip'];
$account->interface = $pre_account[$uuid]['interface'];
$account->force_ssl = $force_ssl;
}
}
}
@@ -67,19 +67,8 @@ POSSIBILITY OF SUCH DAMAGE.
}
});
});
$("#account\\.use_interface").change(function(){
if ($(this).is(':checked')) {
$("#account\\.interface").prop( "disabled", false );
$("#account\\.interface").closest("tr").show();
} else {
$("#account\\.interface").closest("tr").hide();
$("#account\\.interface").prop( "disabled", true );
}
$('#account\\.interface').selectpicker('refresh');
});
$('#DialogAccount').on('shown.bs.modal', function (e) {
$("#account\\.service").change();
$("#account\\.use_interface").change();
});
$("#ddclient\\.general\\.checkip").change(function(){
@@ -113,6 +102,7 @@ POSSIBILITY OF SUCH DAMAGE.
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="service" data-type="string">{{ lang._('Service') }}</th>
<th data-column-id="hostnames" data-type="string">{{ lang._('Hostnames') }}</th>
<th data-column-id="username" data-type="string">{{ lang._('Username') }}</th>
<th data-column-id="interface" data-type="string">{{ lang._('Interface') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
+84
View File
@@ -0,0 +1,84 @@
#!/usr/local/bin/python3
"""
Copyright (c) 2022 Ad Schellevis <ad@opnsense.org>
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 argparse
import subprocess
import re
import ipaddress
service_list = {
'dyndns': '%s://checkip.dyndns.org/',
'freedns': '%s://freedns.afraid.org/dynamic/check.php',
'googledomains': '%s://domains.google.com/checkip',
'he': '%s://checkip.dns.he.net/',
'ip4only.me': '%s://ip4only.me/api/',
'ip6only.me': '%s://ip6only.me/api/',
'ipify-ipv4': '%s://api.ipify.org/',
'ipify-ipv6': '%s://api6.ipify.org/',
'loopia': '%s://dns.loopia.se/checkip/checkip.php',
'myonlineportal': '%s://myonlineportal.net/checkip',
'noip-ipv4': '%s://ip1.dynupdate.no-ip.com/',
'noip-ipv6': '%s://ip1.dynupdate6.no-ip.com/',
'nsupdate.info-ipv4': '%s://ipv4.nsupdate.info/myip',
'nsupdate.info-ipv6': '%s://ipv6.nsupdate.info/myip',
'zoneedit': '%s://dynamic.zoneedit.com/checkip.html'
}
def extract_address(txt):
""" Extract first IPv4 or IPv6 address from provided string
:param txt: text blob
:return: str
"""
for regexp in [r'[^a-fA-F0-9\:]', r'[^F0-9\.]']:
for line in re.sub(regexp, ' ', txt).split():
if line.count('.') == 3 or line.count(':') > 4:
try:
ipaddress.ip_address(line)
return line
except ValueError:
pass
if __name__ == '__main__':
# handle parameters
parser = argparse.ArgumentParser()
parser.add_argument('-s', '--service', help='service name', choices=service_list.keys(), required=True)
parser.add_argument('-i', '--interface', help='interface', type=str, default='')
parser.add_argument('-t', '--tls', help='enforce tls', choices=['0', '1'], default='0')
inputargs = parser.parse_args()
# use curl to fetch data, so we can optionally use "--interface"
params = ['/usr/local/bin/curl']
if inputargs.interface.strip() != "":
params.append("--interface")
params.append(inputargs.interface)
proto = 'http' if inputargs.tls == "0" else 'https'
params.append(service_list[inputargs.service] % proto)
result = subprocess.run(params, capture_output=True, text=True).stdout
print (extract_address(result))
@@ -2,58 +2,22 @@
daemon={{OPNsense.DynDNS.general.daemon_delay|default('300')}}
syslog=yes # log update msgs to syslog
pid=/var/run/ddclient.pid # record PID in file.
{% if not helpers.empty('OPNsense.DynDNS.general.force_ssl') %}
ssl=yes
{% endif %}
{% if not helpers.empty('OPNsense.DynDNS.general.verbose') %}
verbose=yes
{% endif %}
#
# setup how we expect to retrieve an IP address
#
{% if not helpers.empty('OPNsense.DynDNS.general.checkip') %}
{% set checkip = OPNsense.DynDNS.general.checkip %}
{% if checkip == 'if' and OPNsense.DynDNS.general.interface|default('') != '' %}
use=if, if={{physical_interface(OPNsense.DynDNS.general.interface)}}
{% elif checkip == 'web_dyndns' %}
use=web, web=http://checkip.dyndns.org/, web-skip="Current IP Address:"
{% elif checkip == 'web_freedns' %}
use=web, web=https://freedns.afraid.org/dynamic/check.php
{% elif checkip == 'web_googledomains' %}
use=web, web=https://domains.google.com/checkip
{% elif checkip == 'web_he' %}
use=web, web=http://checkip.dns.he.net/
{% elif checkip == 'web_ip4only.me' %}
use=web, web=http://ip4only.me/api/
{% elif checkip == 'web_ip6only.me' %}
use=web, web=http://ip6only.me/api/
{% elif checkip == 'web_ipify-ipv4' %}
use=web, web=https://api.ipify.org/
{% elif checkip == 'web_ipify-ipv6' %}
use=web, web=https://api6.ipify.org/
{% elif checkip == 'web_loopia' %}
use=web, web=http://dns.loopia.se/checkip/checkip.php, web-skip="Current IP Address:"
{% elif checkip == 'web_myonlineportal' %}
use=web, web=https://myonlineportal.net/checkip
{% elif checkip == 'web_noip-ipv4' %}
use=web, web=http://ip1.dynupdate.no-ip.com/
{% elif checkip == 'web_noip-ipv6' %}
use=web, web=http://ip1.dynupdate6.no-ip.com/
{% elif checkip == 'web_nsupdate.info-ipv4' %}
use=web, web=https://ipv4.nsupdate.info/myip
{% elif checkip == 'web_nsupdate.info-ipv6' %}
use=web, web=https://ipv6.nsupdate.info/myip
{% elif checkip == 'web_zoneedit' %}
use=web, web=http://dynamic.zoneedit.com/checkip.html
{% endif %}
{% endif %}
{% if helpers.exists('OPNsense.DynDNS.accounts.account') %}
{% for account in helpers.toList('OPNsense.DynDNS.accounts.account') %}
{% if account.enabled|default('0') == '1' %}
{% if account.use_interface|default('0') == '1' %}
{% if account.checkip == 'if' %}
use=if, if={{physical_interface(account.interface)}}, \
{% elif account.checkip.startswith('web_') %}
{% if account.interface %}
use=cmd, cmd="/usr/local/opnsense/scripts/ddclient/checkip -i {{physical_interface(account.interface)}} -t {{account.force_ssl}} -s {{account.checkip[4:]}}",
{% else %}
use=cmd, cmd="/usr/local/opnsense/scripts/ddclient/checkip -t {{account.force_ssl}} -s {{account.checkip[4:]}}",
{% endif %}
{% endif %}
{% if account.service == 'custom' %}
protocol={{account.protocol}}, \
@@ -96,6 +60,9 @@ server=update.spdyn.de, \
{% elif account.service == 'strato' %}
protocol=dyndns2, \
server=dyndns.strato.com, \
{% elif account.service == 'ovh' %}
protocol=dyndns2, \
server=www.ovh.com, \
{% else %}
protocol={{account.service}}, \
{% endif %}