diff --git a/mddclient/README.md b/mddclient/README.md new file mode 100644 index 0000000..a23a7fe --- /dev/null +++ b/mddclient/README.md @@ -0,0 +1,36 @@ +# MDDCLIENT +It's a client for dynamic DNS services like Infomaniak's. +Like [ddclient](https://github.com/ddclient/ddclient), but supports sending multiple updates for different domains. +I developed this because [Infomaniak's DynDns APIS](https://www.infomaniak.com/en/support/faq/2376/dyndns-updating-a-dynamic-dns-via-the-api) doesn't support multiple domains updates on the same call. So, even if ddclient was supported for a single domain, it was needed to setup multiple instances to update multiple domains (or subdomains) on the same machine. + +## Compatibility +I wrote this to solve the multiple domain update problem on Infomainak, but should work for every other provider supporting the original ddclient. + +## Use case +Let's say we have our Nextcloud instance on `https://mysite.cloud`. As self hosters, we run this instance on our server at home, behind our fiber or DSL provider with a dynamic IP. We need to use ddclient to keep the dynamic DNS updated, so the requests to `https://mysite.cloud` are sent to our current IP, that may change in any moment. +Now we decide to host a Matrix chat instance on `https://matrix.mysite.cloud` and a Mastodon social instance on `https://mastodon.mysite.cloud`. We configure ddclient adding the new subdomains in `/etc/ddclient.conf`, but the requests begin to fail with a 400 http code. This happens because our Dynamic DNS provider doesn't support multiple updates on the same request. + +So we need a script that issues an update request for every (sub)domain. This is mddclient. + +## Setup +Copy the script and the config file into the system to check: +``` +cp mddclient.py /usr/local/bin/mddclient.py +cp mddclient.cfg.example /usr/local/etc/mddclient.cfg +``` +Make the script executable: +``` +chmod +x /usr/local/bin/mddclient.py +``` +Edit `/usr/local/etc/mddclient.cfg` setting up the server url, username, password and the main domain to update. If you have other domains or subdomains running on the same fiber/dsl, configure them in the subsections as shown in the config example. +Run `/usr/local/bin/mddclient.py /usr/local/etc/mddclient.cfg` to check it is working. +Now copy the cron file: +``` +cp mddclient.cron.example /etc/cron.d/mddclient +``` +For increased safety, edit the cron file placing your email address in MAILTO var to be notified in case of mddclient.py catastrophic failure. + +Setup is now complete: the cron runs the script every minute and updates the dns. + +## Optimization +If multiple subdomains are served from the same public url (the same fiber/DSL account) it is possible to optimize the domain updates: when the main one results already up-to-date, the others are not updated. This feature is enabled by default, but can be disabled setting OPTIMIZE_API_CALLS to false in the config. diff --git a/mddclient/mddclient.cfg.example b/mddclient/mddclient.cfg.example new file mode 100644 index 0000000..ae83097 --- /dev/null +++ b/mddclient/mddclient.cfg.example @@ -0,0 +1,30 @@ +# The DEFAULT section contains the main domain config. +[DEFAULT] + +# Dynamic DNS Server +SERVER=infomaniak.com +LOGIN=myUserName +PASSWORD=mySuperSecretPassword + +# Main domain +DOMAIN=mysite.cloud + +# If multiple subdomains are served from the same public url +# (the same fiber/DSL account) it is possible to optimize the +# domain updates: when the main one results already up-to-date, +# the others are not updated. +OPTIMIZE_API_CALLS=True + +[matrix] +# The matrix.mysite.cloud subdomain +# This will be updated only if the main domain needed update +# (or the OPTIMIZE_API_CALLS setting is disabled) +DISABLED=True +DOMAIN=matrix.mysite.cloud + +[mastodon] +# The mastodon.mysite.cloud subdomain +# This will be updated only if the main domain needed update +# (or the OPTIMIZE_API_CALLS setting is disabled) +DISABLED=True +DOMAIN=mastodon.mysite.cloud diff --git a/mddclient/mddclient.cron.example b/mddclient/mddclient.cron.example new file mode 100644 index 0000000..3217a71 --- /dev/null +++ b/mddclient/mddclient.cron.example @@ -0,0 +1,6 @@ +# Cron to execute mddclient +# As a security measure, the address in MAILTO will be notified +# if the mddclient.py script crashes. + +MAILTO="your-email-address" +* * * * * root /usr/local/bin/mddclient.py /usr/local/etc/mddclient.cfg -q diff --git a/mddclient/mddclient.py b/mddclient/mddclient.py new file mode 100755 index 0000000..ef2f174 --- /dev/null +++ b/mddclient/mddclient.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 + +""" @package docstring +Mddclient + +A DynamicDns client like ddclient, but supporting multiple (sub)domains, +every one in its autonomous request. + +Installation: +- Copy mddclient.cfg in /usr/local/etc/mddclient.cfg and customize it +- Copy mddclient.py in /usr/local/bin/mddclient.py + +Usage: +Place a cron entry like this one: + +* * * * * root python3 /usr/local/bin/mddclient.py /usr/local/etc/mddclient.cfg + +@author Daniele Verducci + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import os +import sys +import logging +import configparser +import traceback + + +NAME = 'mddclient' +VERSION = '0.1' +DESCRIPTION = 'A DynamicDns client like ddclient, but supporting multiple (sub)domains' + +class Main: + + def __init__(self, configPath): + ''' Reads the config ''' + self._log = logging.getLogger('main') + + if not os.path.exists(configPath) or not os.path.isfile(configPath): + raise ValueError('configPath must be a file') + + self.config = configparser.ConfigParser() + self.config.read(configPath) + + def run(self): + ''' Makes the update requests ''' + + for section in self.config: + if section == 'DEFAULT': + # Main domain + # TODO + continue + + s = Settings(section, self.config) + if s.disabled: + self._log.info('Ignoring disabled domain "{}"'.format(section)) + continue + + self._log.info('Updating "{}"'.format(section)) + + # TODO: subdomain update request + + +class Settings: + ''' Represents settings for a check ''' + + EMAIL_LIST_SEP = ',' + + def __init__(self, name, config): + self.config = config + + ## Section name + self.name = name + + ## Settings + self.disabled = self.getBoolean(name, 'DISABLED', False) + self.optimize = self.getBoolean(name, 'OPTIMIZE_API_CALLS', True) + + ## DynDNS server data + self.ddserver = self.getStr(name, 'SERVER', None) + self.dduser = self.getStr(name, 'LOGIN', None) + self.ddpass = self.getStr(name, 'PASSWORD', None) + + ## Domain to update + self.domain = self.getStr(name, 'DOMAIN', False) + + def getStr(self, name, key, defaultValue): + try: + return self.config.get(name, key) + except configparser.NoOptionError: + return defaultValue + + def getBoolean(self, name, key, defaultValue): + try: + return self.config.getboolean(name, key) + except configparser.NoOptionError: + return defaultValue + + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser( + prog = NAME + '.py', + description = NAME + ' ' + VERSION + '\n' + DESCRIPTION, + formatter_class = argparse.RawTextHelpFormatter + ) + parser.add_argument('configFile', help="configuration file path") + parser.add_argument('-q', '--quiet', action='store_true', help="suppress non-essential output") + args = parser.parse_args() + + if args.quiet: + level = logging.WARNING + else: + level = logging.INFO + logging.basicConfig(level=level) + + try: + main = Main(args.configFile) + main.run() + except Exception as e: + logging.critical(traceback.format_exc()) + print('ERROR: {}'.format(e)) + sys.exit(1) + + sys.exit(0)