Compare commits

..

9 commits

Author SHA1 Message Date
Gitouche b70621b018 Add IPv4/IPv6 lookup services 2022-09-26 22:54:11 +02:00
Gitouche fcb18989b9 clean ipv6 support 2022-09-26 22:52:12 +02:00
Gitouche e0b7e598e0 add multidomain support - broken ipv6 2022-09-26 22:08:43 +02:00
David Ervideira d504a6a177
Update README.md 2020-01-21 11:26:55 +00:00
David Ervideira cf926700e4
Delete .pydevproject 2019-12-09 16:13:19 +00:00
David Ervideira 0714cbce8a
Delete .project 2019-12-09 16:13:09 +00:00
David Ervideira b55e96d78c The script it self can run forver now
* Add option for the script to repeat itself
  after N seconds

* Add a Dockerfile so that this could be run
  inside docker
2019-12-09 16:10:00 +00:00
David Ervideira d46419ef3d Fix config property name 2019-12-09 13:18:19 +00:00
David Ervideira c1fd36d383 IPv6 support and Python 3 conversion
This introduces support to Python 3, since Python 2 will
deprecated in January 1st 2020.
More info here: https://www.python.org/doc/sunset-python-2/

This Python 3 conversion breaks compatibility with Python 2
but since Python 2 will be deprecated, I see no issue with that.

Another update this is introducing is ipv6 support since I started
getting my ip in v6 format from the ifconfig providers.

There is a new config entry `subdomains6` for configuring the ipv6
domains that will be updated if the returned ip is ipv6.

With ipv6, the script will update either ipv6 or ipv4 depending on
the value returned by the provider.

The usage of the script remains the same.

Other changes made:

 * Use `print()` function instead of the `print` statement
 * Better readability by using f-string
 * Use the json object directly from `requests`instead of parsing the
content with the `json` module
 * Add function to check for ipv6
 * Add `subdomains6`` to config got ipv6 subdomains
 * Incresed verbosity option
2019-12-09 11:57:26 +00:00
8 changed files with 300 additions and 96 deletions

132
.gitignore vendored
View file

@ -1,2 +1,134 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# Custom
src/config.py src/config.py
src/config.pyc src/config.pyc

View file

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>gandi_live_dns</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/${PROJECT_DIR_NAME}/src</path>
</pydev_pathproperty>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
</pydev_project>

13
Dockerfile Normal file
View file

@ -0,0 +1,13 @@
FROM python:3.7-buster
LABEL maintainer=david@dme.ninja
LABEL version="0.1"
# Copy scripts and requirements.txt
COPY src/ /gandi-live-dns
COPY requirements.txt /requirements.txt
# Install script requirements.txt
RUN pip install -r /requirements.txt
CMD ["python", "/gandi-live-dns/gandi-live-dns.py", "-v", "-r", "3600"]

View file

@ -4,7 +4,9 @@ gandi-live-dns
This is a simple dynamic DNS updater for the This is a simple dynamic DNS updater for the
[Gandi](https://www.gandi.net) registrar. It uses their [LiveDNS REST API](http://doc.livedns.gandi.net/) to update the zone file for a subdomain of a domain to point at the external IPv4 address of the computer it has been run from. [Gandi](https://www.gandi.net) registrar. It uses their [LiveDNS REST API](http://doc.livedns.gandi.net/) to update the zone file for a subdomain of a domain to point at the external IPv4 address of the computer it has been run from.
It has been developed on Debian 8 Jessie and tested on Debian 9 Stretch GNU/Linux using Python 2.7. ~~It has been developed on Debian 8 Jessie and tested on Debian 9 Stretch GNU/Linux using Python 2.7.~~
This has been update to work with Python 3 (Python 3.6 at Ubuntu 18.04). This will not work with Python 2 since it ~~will be deprecated in~~ is deprecated since 2020-01-01.
With the new v5 Website, Gandi has also launched a new REST API which makes it easier to communicate via bash/curl or python/requests. With the new v5 Website, Gandi has also launched a new REST API which makes it easier to communicate via bash/curl or python/requests.
@ -22,7 +24,10 @@ https://account.gandi.net/en/ and apply for (at least) the production API
key by following their directions. key by following their directions.
#### A DNS Record #### A DNS Record
Create the DNS A Records in the GANDI Webinterface which you want to update if your IP changes. Create the DNS A Records in the GANDI Webinterface which you want to update if your IPv4 changes.
#### AAAA DNS Record (only needed if ipv6 is in use)
Create the DNS AAAA Records for ipv6 in the GANDI Webinterface which you want to update if your IPv6 changes.
#### Git Clone or Download the Script #### Git Clone or Download the Script
Download the Script from here as [zip](https://github.com/cavebeat/gandi-live-dns/archive/master.zip)/[tar.gz](https://github.com/cavebeat/gandi-live-dns/archive/master.tar.gz) and extract it. Download the Script from here as [zip](https://github.com/cavebeat/gandi-live-dns/archive/master.zip)/[tar.gz](https://github.com/cavebeat/gandi-live-dns/archive/master.tar.gz) and extract it.
@ -56,10 +61,16 @@ Your domain for the subdomains to be updated
##### subdomains ##### subdomains
All subdomains which should be updated. They get created if they do not yet exist. All subdomains which should be updated. They get created if they do not yet exist.
* `subdomains` for ipv4
* `subdomains6` for ipv6
``` ```
subdomains = ["subdomain1", "subdomain2", "subdomain3"] subdomains = ["subdomain1", "subdomain2", "subdomain3"]
subdomains6 = ["subdomain1v6", "subdomain2v6", "subdomain3v6"]
``` ```
The first subdomain is used to find out the actual IP in the Zone Records.
The first subdomain is used to find out the actual IP in the Zone Records.
If the returnded ip from is ipv6, it will use the first from subdomains6.
#### Run the script #### Run the script
And run the script: And run the script:
@ -88,12 +99,14 @@ Status Code: 201 , DNS Record Created , IP updated for subdomain3
``` ```
root@dyndns:~/gandi-live-dns-master/src# ./gandi-live-dns.py -h root@dyndns:~/gandi-live-dns-master/src# ./gandi-live-dns.py -h
usage: gandi-live-dns.py [-h] [-f] usage: gandi-live-dns.py [-h] [-f] [-v] [-r]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-f, --force force an update/create -v, --verbose increase output verbosity
-f, --force force an update/create
-r REPEAT, --repeat REPEAT
keep running and repeat every N seconds
``` ```
The force option runs the script, even when no IP change has been detected. The force option runs the script, even when no IP change has been detected.
@ -124,12 +137,17 @@ Run the script every five minutes.
``` ```
*/5 * * * * /root/gandi-live-dns-master/src/gandi-live-dns.py >/dev/null 2>&1 */5 * * * * /root/gandi-live-dns-master/src/gandi-live-dns.py >/dev/null 2>&1
``` ```
### Run with Docker
Use the docker file to build the image. With docker, the script will run every 3600 seconds. (This value can be changed in the Dockerfile.
### Limitations ### Limitations
The XML-RPC API has a limit of 30 requests per 2 seconds, so i guess it's safe to update 25 subdomains at once with the REST API. The XML-RPC API has a limit of 30 requests per 2 seconds, so i guess it's safe to update 25 subdomains at once with the REST API.
### Upcoming Features ### Upcoming Features
* command line Argument for verbose mode * ~~command line Argument for verbose mode~~ Aditional verbosity implemented.
### Inspiration ### Inspiration

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
requests

View file

@ -20,12 +20,22 @@ https://dns.api.gandi.net/api/v5/
''' '''
api_endpoint = 'https://dns.api.gandi.net/api/v5' api_endpoint = 'https://dns.api.gandi.net/api/v5'
#your domain with the subdomains in the zone file/UUID
domain = 'mydomain.tld'
#enter all subdomains to be updated, subdomains must already exist to be updated
#your domain and subdomains to be updated, subdomains must already exist to be updated #your domain and subdomains to be updated, subdomains must already exist to be updated
dnsentries = { dnsentries = {
"mydomain.tld": ["subdomain1", "subdomain2"], "mydomain.tld": {
"myotherdomain.tld": ["subdomain3"], "ipv4": ["subdomain1", "subdomain2"],
"ipv6": ["subdomain3v6"],
},
"myotherdomain.tld": {
"ipv4": ["subdomain4"],
},
} }
#300 seconds = 5 minutes #300 seconds = 5 minutes
ttl = '300' ttl = '300'
@ -36,10 +46,15 @@ run your own external IP provider:
+ <?php $ip = $_SERVER['REMOTE_ADDR']; ?> + <?php $ip = $_SERVER['REMOTE_ADDR']; ?>
<?php print $ip; ?> <?php print $ip; ?>
e.g. e.g.
+ https://ifconfig.co + https://api[4|6].ipify.org
+ https://ifconfig.co/ip
+ http://ifconfig.me/ip + http://ifconfig.me/ip
+ http://whatismyip.akamai.com/ + http://whatismyip.akamai.com/
+ http://ipinfo.io/ip + http://ipinfo.io/ip
+ many more ... + many more ...
''' '''
ifconfig = 'choose_from_above_or_run_your_own' ifconfig = 'choose_from_above_or_run_your_own'
ifconfig = {
"ipv4": 'choose_from_above_or_run_your_own_ipv4_lookup_service',
"ipv6": 'choose_from_above_or_run_your_own_ipv6_lookup_service',
}

View file

@ -1,70 +1,91 @@
#!/usr/bin/env python #!/usr/bin/env python3
# encoding: utf-8 # encoding: utf-8
''' '''
Gandi v5 LiveDNS - DynDNS Update via REST API and CURL/requests Gandi v5 LiveDNS - DynDNS Update via REST API and CURL/requests
@author: cave @author: cave
@author: dvdme
License GPLv3 License GPLv3
https://www.gnu.org/licenses/gpl-3.0.html https://www.gnu.org/licenses/gpl-3.0.html
Created on 13 Aug 2017 Created on 13 Aug 2017
http://doc.livedns.gandi.net/ Forked on 08 Dec 2019
http://doc.livedns.gandi.net/
http://doc.livedns.gandi.net/#api-endpoint -> https://dns.gandi.net/api/v5/ http://doc.livedns.gandi.net/#api-endpoint -> https://dns.gandi.net/api/v5/
''' '''
import requests, json import json
import requests
import ipaddress
import config import config
import argparse import argparse
import threading as th
from pprint import pprint
def get_dynip(ifconfig_provider): def check_is_ipv6(ip_address, verbose=False):
return ipaddress.ip_address(ip_address).version == 6
def get_dynip(ifconfig_provider, verbose=False):
''' find out own IPv4 at home <-- this is the dynamic IP which changes more or less frequently ''' find out own IPv4 at home <-- this is the dynamic IP which changes more or less frequently
similar to curl ifconfig.me/ip, see example.config.py for details to ifconfig providers similar to curl ifconfig.me/ip, see example.config.py for details to ifconfig providers
''' '''
if verbose:
print(f'Using {ifconfig_provider}')
r = requests.get(ifconfig_provider) r = requests.get(ifconfig_provider)
print('Checking dynamic IP: ' , r.text.strip('\n')) print (f'Checking dynamic IP: {r.text.strip()}')
return r.text.strip('\n') return r.text.strip()
def get_uuid(): def get_uuid(verbose=False):
''' '''
find out ZONE UUID from domain find out ZONE UUID from domain
Info on domain "DOMAIN" Info on domain "DOMAIN"
GET /domains/<DOMAIN>: GET /domains/<DOMAIN>:
''' '''
url = config.api_endpoint + '/domains/' + config.domain url = config.api_endpoint + '/domains/' + config.domain
u = requests.get(url, headers={"X-Api-Key":config.api_secret}) u = requests.get(url, headers={"X-Api-Key":config.api_secret})
json_object = json.loads(u._content) json_object = u.json()
if verbose:
pprint(json_object)
if u.status_code == 200: if u.status_code == 200:
return json_object['zone_uuid'] return json_object['zone_uuid']
else: else:
print('Error: HTTP Status Code ', u.status_code, 'when trying to get Zone UUID') print(f'Error: HTTP Status Code {u.status_code} when trying to get Zone UUID')
print(json_object['message']) pprint(u.json())
exit() exit()
def get_dnsip(uuid): def get_dnsip(uuid, is_ipv6=False, verbose=False):
''' find out IP from first Subdomain DNS-Record ''' find out IP from first Subdomain DNS-Record
List all records with name "NAME" and type "TYPE" in the zone UUID List all records with name "NAME" and type "TYPE" in the zone UUID
GET /zones/<UUID>/records/<NAME>/<TYPE>: GET /zones/<UUID>/records/<NAME>/<TYPE>:
The first subdomain from config.subdomain will be used to get The first subdomain from config.subdomain will be used to get
the actual DNS Record IP the actual DNS Record IP
''' '''
if is_ipv6:
record_type = '/AAAA'
else:
record_type = '/A'
url = config.api_endpoint+ '/zones/' + uuid + '/records/' + config.subdomains[0] + '/A' subdomain = config.subdomains[0]
headers = {"X-Api-Key":config.api_secret} url = config.api_endpoint+ '/zones/' + uuid + '/records/' + subdomain + record_type
headers = {'X-Api-Key':config.api_secret}
u = requests.get(url, headers=headers) u = requests.get(url, headers=headers)
if u.status_code == 200: if u.status_code == 200:
json_object = json.loads(u._content) json_object = u.json()
print('Checking IP from DNS Record' , config.subdomains[0], ':', json_object['rrset_values'][0].encode('ascii','ignore').decode().strip('\n')) if verbose:
return json_object['rrset_values'][0].encode('ascii','ignore').decode().strip('\n') pprint(json_object)
dnsip = json_object['rrset_values'][0].strip()
print (f'Checking IP from DNS Record {subdomain}: {dnsip}')
return dnsip
else: else:
print('Error: HTTP Status Code ', u.status_code, 'when trying to get IP from subdomain', config.subdomains[0]) print('Error: HTTP Status Code ', u.status_code, 'when trying to get IP from subdomain', subdomain)
print (json_object['message']) pprint(u.json())
exit() exit()
def update_records(uuid, dynIP, subdomain): def update_records(uuid, dynIP, subdomain, is_ipv6=False, verbose=False):
''' update DNS Records for Subdomains ''' update DNS Records for Subdomains
Change the "NAME"/"TYPE" record from the zone UUID Change the "NAME"/"TYPE" record from the zone UUID
PUT /zones/<UUID>/records/<NAME>/<TYPE>: PUT /zones/<UUID>/records/<NAME>/<TYPE>:
curl -X PUT -H "Content-Type: application/json" \ curl -X PUT -H "Content-Type: application/json" \
@ -73,54 +94,83 @@ def update_records(uuid, dynIP, subdomain):
"rrset_values": ["<VALUE>"]}' \ "rrset_values": ["<VALUE>"]}' \
https://dns.gandi.net/api/v5/zones/<UUID>/records/<NAME>/<TYPE> https://dns.gandi.net/api/v5/zones/<UUID>/records/<NAME>/<TYPE>
''' '''
url = config.api_endpoint+ '/zones/' + uuid + '/records/' + subdomain + '/A' if is_ipv6:
payload = {"rrset_ttl": config.ttl, "rrset_values": [dynIP]} record_type = '/AAAA'
headers = {"Content-Type": "application/json", "X-Api-Key":config.api_secret} else:
record_type = '/A'
url = config.api_endpoint+ '/zones/' + uuid + '/records/' + subdomain + record_type
payload = {'rrset_ttl': config.ttl, "rrset_values": [dynIP]}
headers = {'Content-Type': 'application/json', 'X-Api-Key':config.api_secret}
u = requests.put(url, data=json.dumps(payload), headers=headers) u = requests.put(url, data=json.dumps(payload), headers=headers)
json_object = json.loads(u._content) json_object = u.json()
if u.status_code == 201: if u.status_code == 201:
print('Status Code:', u.status_code, ',', json_object['message'], ', IP updated for', subdomain) print (f'Status Code: {u.status_code}, {json_object["message"]}, IP updated for {subdomain}')
return True return True
else: else:
print('Error: HTTP Status Code ', u.status_code, 'when trying to update IP from subdomain', subdomain) print (f'Error: HTTP Status Code {u.status_code} when trying to update IP from subdomain {subdomain}')
print(json_object['message']) print (json_object['message'])
exit() exit()
def main(force_update, verbosity): def main(force_update, verbosity, repeat):
if verbosity: if verbosity:
print("verbosity turned on - not implemented by now") print('verbosity turned on')
verbose = True
else:
verbose =False
for key, value in config.dnsentries.items(): if repeat and verbose:
config.domain = key print(f'repeat turned on, will repeat every {repeat} seconds')
config.subdomains = value
#get zone ID from Account for domain, content in config.dnsentries.items():
uuid = get_uuid() config.domain = domain
for key, value in content.items():
#compare dynIP and DNS IP afi = key
dynIP = get_dynip(config.ifconfig) config.subdomains = value
dnsIP = get_dnsip(uuid)
#get zone ID from Account
if force_update: uuid = get_uuid()
print("Going to update/create the DNS Records for the subdomains")
for sub in config.subdomains: #compare dynIP and DNS IP
update_records(uuid, dynIP, sub) dynIP = get_dynip(config.ifconfig[afi], verbose)
else:
if dynIP == dnsIP: if check_is_ipv6(dynIP, verbose):
print("IP Address Match - no further action") is_ipv6 = True
print('Detected ipv6')
else: else:
print("IP Address Mismatch - going to update the DNS Records for the subdomains with new IP", dynIP) print('Detected ipv4')
for sub in config.subdomains: is_ipv6 = False
update_records(uuid, dynIP, sub)
if __name__ == "__main__": dnsIP = get_dnsip(uuid, is_ipv6, verbose)
subdomains = config.subdomains
if force_update:
print ('Going to update/create the DNS Records for the subdomains')
for sub in subdomains:
update_records(uuid, dynIP, sub, is_ipv6, verbose)
else:
if verbose:
print(f'dynIP: {dynIP}')
print(f'dnsIP: {dnsIP}')
if dynIP == dnsIP:
print ('IP Address Match - no further action')
else:
print (f'IP Address Mismatch - going to update the DNS Records for the subdomains with new IP {dynIP}')
for sub in subdomains:
update_records(uuid, dynIP, sub, is_ipv6, verbose)
if repeat:
if verbosity:
print(f'Repeating in {repeat} seconds')
th.Timer(repeat, main, [force_update, verbosity, repeat]).start()
if __name__ == '__main__':
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('-v', '--verbose', help="increase output verbosity", action="store_true") parser.add_argument('-v', '--verbose', help='increase output verbosity', action='store_true')
parser.add_argument('-f', '--force', help="force an update/create", action="store_true") parser.add_argument('-f', '--force', help='force an update/create', action='store_true')
parser.add_argument('-r', '--repeat', type=int, help='keep running and repeat every N seconds')
args = parser.parse_args() args = parser.parse_args()
main(args.force, args.verbose, args.repeat)
main(args.force, args.verbose)