From: Victor Wagner Date: Thu, 12 Sep 2019 06:17:50 +0000 (+0300) Subject: Fix conflict which arises from editing example conf without updating working copy... X-Git-Url: http://www.wagner.pp.ru/gitweb/?a=commitdiff_plain;h=65240923f667eeb18fb688c4591ca22495491d90;hp=d3eaa42a7888d9a95f7d64eb528dd8f84b0a777b;p=oss%2Fdyngo.git Fix conflict which arises from editing example conf without updating working copy first --- diff --git a/README.md b/README.md index 9b5f951..721fe2f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ remembered name. But it is not always a case in the world of NATs and VPNs. If machine always have public IP address, than using some dynamic DNS -service may help (short of neccessity to use full domain name. But +service may help (short of necessity to use full domain name. But ${HOME}/.ssh/config may help). But what if you log into your office workstation via corporate VPN, and want ssh back home? @@ -26,5 +26,40 @@ to specified URL. See [manual page](dyngo.md) for more info. +PRROTOCOL +========= + +**dyngo** uses protocol compatible with dyndns.org, as described +pn [help.dyn.com](https://help.dyn.com/remote-access-api/perform-update/ +) +As different dynamic DNS servers use different path components of URL, +you should specify URL with path component (up to question mark) in +the *server* parameter of configuraton section. + +INSTALLATION +============ + +**dyngo** is simple script which doesn't require anything but +python3 with standard library and **ip** utility from iproute (or +iproute2 on Debian) package, which presents on every modern Linux +system. + +I've even preferred to use +[urllib](https://docs.python.org/3/library/urllib.request.html#module-urllib.request) +to [requests](http://docs.python-requests.org/) to eliminate extra +external dependency. + +So, you can just drop **dyngo** somewhere in your filesystem, say in +**/usr/local/sbin** and put config into */etc* But **dyngo** should run +as a service, so you might want to use provided service file. + +**dyngo** should run as unprivileged user. But its configuration file +should, readable for this user, contains somewhat sensitive information +— your dyndns passwords. So, don't use common account many untrusted +code run as such as **nobody** or **www-data**. Better to create special +user dyngo. + +There should be writable directory, writable for this user for +persistent state database. By default it is /var/lib/dyngo. diff --git a/dyngo b/dyngo old mode 100644 new mode 100755 index 431e8cf..0b02240 --- a/dyngo +++ b/dyngo @@ -1,6 +1,8 @@ #!/usr/bin/env python3 import ipaddress, subprocess, sys, getopt,dbm from configparser import ConfigParser +import urllib.request,urllib.parse +import logging,os.path,time class DyndnsNetwork(object): """ This class represents dynamic DNS server and network. @@ -10,7 +12,7 @@ class DyndnsNetwork(object): self.hostname = config_section['hostname'] self.network = ipaddress.ip_network(config_section['network']) self.server = config_section['server'] - if user in config_section: + if 'user' in config_section: self.user=config_section["user"] self.password=config_section["password"] @@ -21,11 +23,30 @@ class DyndnsNetwork(object): if self.network.version != address.version: return False return address in self.network - def nsupdate(self, address): - raise NotImplementedError + def nsupdate(self, address): + """ + Sends a get query to specified server. + Raises HTTPError if something goes wrong + """ + #construct opener + logging.debug("Going to send message to %s",self.server) + if hasattr(self,'user'): + password_mgr =urllib.request.HTTPPasswordMgrWithDefaultRealm() + password_mgr.add_password(None,self.server,self.user,self.password) + handler=urllib.request.HTTPBasicAuthHandler(password_mgr) + opener = urllib.request.build_opener(handler) + else: + opener = urllib.request.build_opener() + with opener.open('%s?%s'%(self.server, + urllib.parse.urlencode({'hostname':self.hostname, + 'myip':str(address)}))) as req: + req.read().decode("utf-8") + + def get_current_addresses(): result=[] + logging.debug("Acquire current IPs") for line in subprocess.run(['ip','-o','addr','show'],check=True,stdout=subprocess.PIPE).stdout.decode('utf-8').split("\n"): if not len(line): continue @@ -34,6 +55,7 @@ def get_current_addresses(): if address.is_loopback or address.is_link_local: continue result.append(address) + logging.debug("Got following addresses: %s",repr(result)) return result def check_for_update(): addrlist=get_current_addresses() @@ -46,40 +68,64 @@ def check_for_update(): old_addr=ipaddress.ip_address(database[name].decode("utf-8")) if old_addr==a: # Nothing changed go, to next net + logging.debug("Address for net %s not changed",name) break # address changed - net.nsupdate(a) - database[name]=str(a) + try: + logging.info("Doing update for net %s with address %s",name, str(a)) + net.nsupdate(a) + database[name]=str(a) + except urllib.error.HTTPError as e: + logging.exception("Http error doing nsupdate code %s",e.code) + break if not found: + logging.info("Address for net %s no more found",name) del data[name] - - - -config=ConfigPaser() -config['dyngo']={'interval':'60','database','/var/lib/dyngo/dyngo.db'} +config=ConfigParser() +config['dyngo']={'interval':'60','database':'/var/lib/dyngo/dyngo.db', + 'ca':'/etc/ssl/certs','loglevel':'WARNING'} options=dict(getopt.getopt(sys.argv,"f:")[0]) if not '-f' in options: options["-f"]="/etc/dyngo.conf" if len(config.read(options["-f"]))!=1: print("Cannot read config %s"%options["-f"],file=sys.stderr) sys.exit(1) - conf=config['dyngo'] +if conf['loglevel'].isdigit(): + level=int(conf['loglevel']) +else: + try: + logging.NOTICE=25 + level=getattr(logging,conf['loglevel'].upper()) + except AttributeError: + print("Invalid logleevel '%s'"%conf('loglevel')) + sys.exit(1) + +logging.basicConfig(format="%(asctime)s %(message)s", + level=level, stream=sys.stderr) interval=int(conf['interval']) database=dbm.open(conf['database'],"c") +https_params={} +if 'ca' in conf and len(conf['ca']): + path=conf['ca'] + if os.path.isdir(path): + https_params['capath']=path + else: + http_params['cafile']=path # Convert all other config sections to DyndnsNetwork objects networks={} for sect in config.sections(): - if sect == 'dyngo' or sect= 'DEFAULT': + if sect == 'dyngo' or sect == 'DEFAULT': continue + logging.debug("Processing network %s",sect) networks[sect]=DyndnsNetwork(config[sect]) # Remove stale items from persistent database, which are no more # mentioned in the config file -for i in set([x.decode("utf-8") for x database.keys()])-set(network.keys()): +for i in set([x.decode("utf-8") for x in database.keys()])-set(networks.keys()): + logging.info("Removing from persistent state network %s which is no more in config",i) del database[i] - while True: check_for_update() time.sleep(interval) diff --git a/dyngo.md b/dyngo.md index d7ed659..94e58ff 100644 --- a/dyngo.md +++ b/dyngo.md @@ -40,6 +40,25 @@ Configuration file **dyngo.conf** is ini-style file. It contains section Names of server-description sections are arbitrary, but should be unique, because they are used as keys into persistent database. - +Parameters of **dyngo** section +------------------------------- + +* interval - number of seconds between rescans of network interfaces +* database - path to persistent state database +* ca - path to trusted certificate store in openssl compatible format. +* loglevel - minimum log message level which shoud to console + +Parameters of network section +----------------------------- + +* **hostname** - fully qualified domain name of your host to register in DNS +* **network** - network in *address*/*bits* notation which this host +should belong to. If we see address from this network on one of our +interfaces, we would send request to corresponding server. If net is +not privatte, but include private ranges, i.e. 0.0.0.0/0 or ::/0 private +addresses are not considered part of it. +* **server** - full url (without query string) of the dyndns web handler. +* **user** - user name for HTTP basic authentication +* **password** - password for HTTP basic authentcircation diff --git a/dyngo.service b/dyngo.service new file mode 100644 index 0000000..5177ad1 --- /dev/null +++ b/dyngo.service @@ -0,0 +1,14 @@ +[Unit] +Description=Dyngo multiple dyndns client +After=network.target +Documentation=https://github.com/vbwagner/dyngo + +[Service] +Type=simple +ExecStart=/usr/local/sbin/dyngo +KillSignal=SIGTERM +WorkingDirectory=~ +User=dyngo +StandardError=journal +[Install] +WantedBy=multi-user.target