Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<label>resourceId</label>
<type>text</type>
<advanced>true</advanced>
<style>optional_setting service_azure</style>
</field>
<field>
<id>account.username</id>
Expand All @@ -51,13 +52,34 @@
<type>password</type>
<help>Password associated with this account</help>
</field>
<field>
<id>account.token_secret</id>
<label>Token secret</label>
<type>password</type>
<style>optional_setting service_desec-v4 service_desec-v6</style>
<help>Token secret for the domain. This is not your deSEC account password.</help>
</field>
<field>
<id>account.wildcard</id>
<label>Wildcard</label>
<type>checkbox</type>
<style>optional_setting service_dyndns2 service_woima service_cloudflare service_easydns service_custom</style>
<help>add a DNS wildcard CNAME record that points to the configured host.</help>
</field>
<field>
<id>account.prune_a</id>
<label>Prune A</label>
<type>checkbox</type>
<style>optional_setting service_desec-v6</style>
<help>Delete existing A (IPv4) records when this IPv6 domain updates. Leave unchecked to preserve them.</help>
</field>
<field>
<id>account.prune_aaaa</id>
<label>Prune AAAA</label>
<type>checkbox</type>
<style>optional_setting service_desec-v4</style>
<help>Delete existing AAAA (IPv6) records when this IPv4 domain updates. Leave unchecked to preserve them.</help>
</field>
<field>
<id>account.zone</id>
<label>Zone</label>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<model>
<mount>//OPNsense/DynDNS</mount>
<version>1.5.1</version>
<version>1.5.2</version>
<description>Dynamic DNS client</description>
<items>
<general>
Expand Down Expand Up @@ -113,6 +113,16 @@
<Required>N</Required>
<Mask>/^[^\n]*$/</Mask>
</password>
<!--
Keep this as TextField to support cloning and to avoid
confusion about the state of migrated deSEC accounts. The
dialog still renders it as a protected password input.
Users who can access this GUI can trivially extract it anyway.
-->
<token_secret type="TextField">
<Required>N</Required>
<Mask>/^[^\n]*$/</Mask>
</token_secret>
<resourceId type="TextField">
<Required>N</Required>
<Mask>/^[^\n]*$/</Mask>
Expand All @@ -131,6 +141,14 @@
<Default>0</Default>
<Required>Y</Required>
</wildcard>
<prune_a type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</prune_a>
<prune_aaaa type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</prune_aaaa>
<zone type="HostnameField">
<Required>N</Required>
<IpAllowed>N</IpAllowed>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace OPNsense\DynDNS\Migrations;

use OPNsense\Base\BaseModelMigration;

class M1_5_2 extends BaseModelMigration
{
public function run($model)
{
foreach ($model->accounts->account->iterateItems() as $account) {
$service = (string)$account->service;
if ($service == 'desec-v4' || $service == 'desec-v6') {
/*
* Older deSEC entries used "password" to store the token secret;
* the deSEC account password was never supported. Copy the value
* to the explicit field, but leave the old value so unchanged
* migrated entries can roll back. Entries created after this
* migration only use token_secret.
*/
$legacy_token = $account->password->getValue();
if ((string)$account->token_secret == '' && $legacy_token != '') {
$account->token_secret = $legacy_token;
}
/*
* deSEC never used "username" as an account login. If set at
* all, it could only mirror Hostname(s), which is already the
* authoritative update target.
*/
$account->username = '';
/*
* The original deSEC path deleted the opposite address family
* when "preserve" was omitted. Existing accounts keep that
* behavior; new accounts get the model defaults and preserve.
*/
if ($service == 'desec-v4') {
$account->prune_aaaa = '1';
} else {
$account->prune_a = '1';
}
}
}
}
}
20 changes: 18 additions & 2 deletions dns/ddclient/src/opnsense/mvc/app/views/OPNsense/DynDNS/index.volt
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ POSSIBILITY OF SUCH DAMAGE.
updateServiceControlUI('dyndns');
}
});
$("#account\\.service").change(function(){
let service = $(this).val();
function updateAccountServiceControls() {
let service = $("#account\\.service").val();
$("#frm_DialogAccount .optional_setting").each(function(){
let this_item = $(this);
if (this_item.hasClass("service_"+service)) {
Expand All @@ -69,6 +69,22 @@ POSSIBILITY OF SUCH DAMAGE.
this_item.prop( "disabled", true );
}
});
let is_desec = ['desec-v4', 'desec-v6'].includes(service);
// deSEC's dynDNS "username" was never an account login; the only
// useful value was a duplicate of Hostname(s), so migration clears
// it. Use Hostname(s) plus Token secret and keep legacy Password
// hidden for rollback without exposing or rewriting it.
$("#account\\.username, #account\\.password")
.prop("disabled", is_desec)
.closest("tr")
.toggle(!is_desec);
}

$("#account\\.service").change(updateAccountServiceControls);
$(document).ajaxComplete(function(event, xhr, settings) {
if (settings.url && settings.url.indexOf('/api/dyndns/accounts/get_item/') !== -1) {
updateAccountServiceControls();
}
});
$('#DialogAccount').on('shown.bs.modal', function (e) {
$("#account\\.service").change();
Expand Down
97 changes: 97 additions & 0 deletions dns/ddclient/src/opnsense/scripts/ddclient/lib/account/desec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import syslog
import requests
from . import BaseAccount


class DeSEC(BaseAccount):
_checked_values = {'1', 'true', 'yes', 'on'}
_preserve_value = 'preserve'
_user_agent = 'OPNsense-dyndns'

_services = {
'desec-v4': {
'label': 'deSEC (IPv4)',
'server': 'update.dedyn.io',
'address_param': 'myipv4',
'other_param': 'myipv6',
'prune_setting': 'prune_aaaa'
},
'desec-v6': {
'label': 'deSEC (IPv6)',
'server': 'update6.dedyn.io',
'address_param': 'myipv6',
'other_param': 'myipv4',
'prune_setting': 'prune_a'
}
}

@classmethod
def known_services(cls):
return {key: item['label'] for key, item in cls._services.items()}

@classmethod
def match(cls, account):
return account.get('service') in cls._services

@classmethod
def _is_checked(cls, value):
return value is True or str(value).lower() in cls._checked_values

def _token_secret(self):
# Legacy deSEC accounts stored the token secret in "password"; a deSEC
# account password was never accepted by this backend.
return self.settings.get('token_secret') or self.settings.get('password') or ''

def _address_parameters(self, service_settings):
# deSEC prunes the other address family when its parameter is empty.
# New accounts preserve by default; migrated accounts may set prune_* to
# keep the historic behavior.
other_address = (
''
if self._is_checked(self.settings.get(service_settings['prune_setting'], False))
else self._preserve_value
)
return {
'hostname': self.settings.get('hostnames'),
service_settings['address_param']: str(self.current_address),
service_settings['other_param']: other_address
}

def _request_options(self, service_settings):
uri_proto = 'https' if self.settings.get('force_ssl', False) else 'http'
# deSEC's dynDNS "username" is the domain being updated, not an
# account login. We already send that via the hostname parameter, so use
# token authentication and keep the generic Username field irrelevant.
return {
'url': f"{uri_proto}://{service_settings['server']}/nic/update",
'params': self._address_parameters(service_settings),
'headers': {
'User-Agent': self._user_agent,
'Authorization': f"Token {self._token_secret()}"
}
}

def execute(self):
if not super().execute():
return False

service_settings = self._services[self.settings.get('service')]
req = requests.get(**self._request_options(service_settings))

if 200 <= req.status_code < 300:
if self.is_verbose:
syslog.syslog(
syslog.LOG_NOTICE,
"Account %s set new ip %s [%s]" % (self.description, self.current_address, req.text.strip())
)

self.update_state(address=self.current_address, status=req.text.split()[0] if req.text else '')
return True

syslog.syslog(
syslog.LOG_ERR,
"Account %s failed to set new ip %s [%d - %s]" % (
self.description, self.current_address, req.status_code, req.text.replace('\n', '')
)
)
return False
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ class DynDNS2(BaseAccount):

_services = {
'dyndns2': 'members.dyndns.org',
'desec-v4': 'update.dedyn.io',
'desec-v6': 'update6.dedyn.io',
'dns-o-matic': 'updates.dnsomatic.com',
'dynu': 'api.dynu.com',
'he-net': 'dyn.dns.he.net',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
"resourceId": {{ account.resourceId | default('') | tojson }},
"username": {{ account.username | default('') | tojson }},
"password": {{ account.password | default('') | tojson }},
"token_secret": {{ account.token_secret | default('') | tojson }},
"hostnames": "{{ account.hostnames }}",
"wildcard": {{ "true" if account.wildcard == '1' else "false"}},
"prune_a": {{ "true" if account.prune_a|default('0') == '1' else "false"}},
"prune_aaaa": {{ "true" if account.prune_aaaa|default('0') == '1' else "false"}},
"zone": "{{ account.zone }}",
"checkip": "{{ account.checkip }}",
"interface": "{% if account.interface %}{{physical_interface(account.interface)}}{% endif %}",
Expand Down