From 84a7323b42a130895fff77836d64bdb9635d9e78 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Fri, 28 Jan 2022 07:33:02 -0500 Subject: [PATCH] dynamicdns: Replace ez-ipupdate Add Python implementation of GnuDIP client. Tests: - In testing container, configure Dynamic DNS with a (previously offlined) freedombox.rocks account. FreedomBox interface shows that the address has been updated. GnuDIP server also shows the correct IP address. - Running "gnudip update" and "dynamicdns update" actions produce the expected results. --- actions/dynamicdns | 41 +++++--------- actions/gnudip | 77 +++++++++++++++++++++++++++ plinth/modules/dynamicdns/__init__.py | 4 -- plinth/modules/dynamicdns/gnudip.py | 39 ++++++++++++++ 4 files changed, 130 insertions(+), 31 deletions(-) create mode 100755 actions/gnudip create mode 100644 plinth/modules/dynamicdns/gnudip.py diff --git a/actions/dynamicdns b/actions/dynamicdns index 9e19b38a7..2746967da 100755 --- a/actions/dynamicdns +++ b/actions/dynamicdns @@ -31,7 +31,7 @@ UPDATEMINUTES=5 # a "blind" update UPDATEMINUTESUNKNOWN=3600 TOOLNAME=ez-ipupdate -UPDATE_TOOL=$(which ${TOOLNAME}) +GNUDIP_ACTION=/usr/share/plinth/actions/gnudip DISABLED_STRING='disabled' ENABLED_STRING='enabled' @@ -157,13 +157,6 @@ doWriteCFG() else # we are directly connected echo "NAT no" >> ${HELPERCFG} - # if this file is added ez-ipupdate will take ip form this interface - { - echo "interface=${default_interface}" - # if this line is added to config file, ez-ipupdate will be launched on startup via init.d - echo "daemon" - echo "execute=${0} success" - } >> ${out_file} fi fi } @@ -296,7 +289,7 @@ doGetWANIP() fi } -# actualy do the update (using wget or ez-ipupdate or even both) +# actualy do the update (using wget or gnudip action or even both) # this function is called via cronjob doUpdate() { @@ -309,7 +302,11 @@ doUpdate() return fi if [ ! -z "${server}" ];then - start-stop-daemon -S -x "${UPDATE_TOOL}" -m -p "${PIDFILE}" -- -c "${cfgfile}" + if "${GNUDIP_ACTION}" update; then + ${0} success ${wanip} + else + ${0} failed + fi fi if [ ! -z "${updateurl}" ];then doReplaceVars @@ -351,24 +348,14 @@ case ${cmd} in ;; start) doGetWANIP - if [ "$(grep ^NAT ${HELPERCFG} | awk '{print $2}')" = "no" ];then - #if we are not behind a NAT device and we use gnudip, start the daemon tool - gnudipServer=$(grep ^server= ${cfgfile} 2> /dev/null | cut -d = -f 2- |grep -v ^\'\') - if [ ! -f ${CFG} -a ! -z "${gnudipServer}" ];then - mv ${CFG_disabled} ${CFG} - /etc/init.d/${TOOLNAME} start - fi - # if we are not behind a NAT device and we use update-URL, add a cronjob - # (daemon tool does not support update-URL feature) - if [ ! -z "$(grep ^POSTURL $HELPERCFG | awk '{print $2}')" ];then - echo "*/${UPDATEMINUTES} * * * * root ${0} update" > ${CRONJOB} - $0 update - fi - else - # if we are behind a NAT device, add a cronjob (daemon tool cannot monitor WAN IP changes) - echo "*/${UPDATEMINUTES} * * * * root ${0} update" > $CRONJOB - $0 update + # if we use gnudip, enable gnudip. + gnudipServer=$(grep ^server= ${cfgfile} 2> /dev/null | cut -d = -f 2- |grep -v ^\'\') + if [ ! -f ${CFG} -a ! -z "${gnudipServer}" ];then + mv ${CFG_disabled} ${CFG} fi + # add a cronjob + echo "*/${UPDATEMINUTES} * * * * root ${0} update" > $CRONJOB + $0 update ;; get-nat) NAT=$(grep ^NAT $HELPERCFG 2> /dev/null | awk '{print $2}') diff --git a/actions/gnudip b/actions/gnudip new file mode 100755 index 000000000..fe112d429 --- /dev/null +++ b/actions/gnudip @@ -0,0 +1,77 @@ +#!/usr/bin/python3 +# -*- mode: python -*- +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +GnuDIP client for updating Dynamic DNS records. +""" + +import argparse +import logging +import pathlib +import sys + +from plinth.modules.dynamicdns import gnudip + +logger = logging.getLogger(__name__) + +CONFIG_FILE = pathlib.Path('/etc/ez-ipupdate/ez-ipupdate.conf') + + +def parse_arguments(): + """Return parsed command line arguments as dictionary.""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') + + subparsers.add_parser('update', help='Update Dynamic DNS record') + + subparsers.required = True + return parser.parse_args() + + +def subcommand_update(_): + """Update Dynamic DNS record. + + Uses settings from ez-ipupdate config file. + """ + if not CONFIG_FILE.is_file(): + logger.info('GnuDIP configuration not found.') + return + + config = {} + with CONFIG_FILE.open() as cf: + lines = cf.readlines() + for line in lines: + if '=' in line: + items = line.split('=') + key = items[0].strip() + value = items[1].strip() + else: + key = line.strip() + value = True + + config[key] = value + + if not all(['host' in config, 'server' in config, 'user' in config]): + logger.warn('GnuDIP configuration is not complete.') + return + + username, password = config['user'].split(':') + result, new_ip = gnudip.update(config['server'], config['host'], username, + password) + if result == 0 and new_ip: + print(new_ip) + + return result + + +def main(): + """Parse arguments and perform all duties.""" + arguments = parse_arguments() + + subcommand = arguments.subcommand.replace('-', '_') + subcommand_method = globals()['subcommand_' + subcommand] + sys.exit(subcommand_method(arguments)) + + +if __name__ == '__main__': + main() diff --git a/plinth/modules/dynamicdns/__init__.py b/plinth/modules/dynamicdns/__init__.py index 86efbc4da..613ad7c6f 100644 --- a/plinth/modules/dynamicdns/__init__.py +++ b/plinth/modules/dynamicdns/__init__.py @@ -11,7 +11,6 @@ from plinth import cfg, menu from plinth.modules.backups.components import BackupRestore from plinth.modules.names.components import DomainType from plinth.modules.users.components import UsersAndGroups -from plinth.package import Packages from plinth.signals import domain_added from plinth.utils import format_lazy @@ -58,9 +57,6 @@ class DynamicDNSApp(app_module.App): 'dynamicdns:index', parent_url_name='system') self.add(menu_item) - packages = Packages('packages-dynamicdns', ['ez-ipupdate']) - self.add(packages) - domain_type = DomainType('domain-type-dynamic', _('Dynamic Domain Name'), 'dynamicdns:index', can_have_certificate=True) diff --git a/plinth/modules/dynamicdns/gnudip.py b/plinth/modules/dynamicdns/gnudip.py new file mode 100644 index 000000000..58ad15045 --- /dev/null +++ b/plinth/modules/dynamicdns/gnudip.py @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +GnuDIP client for updating Dynamic DNS records. +""" + +import hashlib +import logging +import socket + +BUF_SIZE = 50 +GNUDIP_PORT = 3495 + +logger = logging.getLogger(__name__) + + +def update(server, domain, username, password): + """Update Dynamic DNS record.""" + domain = domain.removeprefix(username + '.') + password_digest = hashlib.md5(password.encode()).hexdigest() + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + logger.debug('Connecting to %s:%d', server, GNUDIP_PORT) + s.connect((server, GNUDIP_PORT)) + salt = s.recv(BUF_SIZE).decode().strip() + salted_digest = password_digest + '.' + salt + final_digest = hashlib.md5(salted_digest.encode()).hexdigest() + + update_request = username + ':' + final_digest + ':' + domain + ':2\n' + s.sendall(update_request.encode()) + + response = s.recv(BUF_SIZE).decode().strip() + result, new_ip = response.split(':') + result = int(result) + if result == 0: + logger.info('GnuDIP update success: %s', new_ip) + else: + logger.warn('GnuDIP update error: %s', response) + + return result, new_ip