Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IPv6 support #19

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
65 changes: 38 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ gandi-live-dns
----

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 and/or IPv6 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.

Expand All @@ -17,25 +17,25 @@ You want your homeserver to be always available at `dynamic_subdomain.mydomain.t
`apt-get update && apt-get upgrade && apt-get install unzip python-requests python-args python-simplejson`

#### API Key
First, you must apply for an API key with Gandi. Visit
https://account.gandi.net/en/ and apply for (at least) the production API
First, you must apply for an API key with Gandi. Visit
https://account.gandi.net/en/ and apply for (at least) the production API
key by following their directions.

#### A DNS Record
Create the DNS A Records in the GANDI Webinterface which you want to update if your IP changes.
#### A or AAAA DNS Record
Create the DNS Records in the GANDI Webinterface which you want to update if your IP changes. Use an A Record for IPv4 and AAAA Record for IPv6.

#### 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.

or clone from git

`git clone https://github.com/cavebeat/gandi-live-dns.git`
`git clone https://github.com/cavebeat/gandi-live-dns.git`

#### Script Configuration
Then you'd need to configure the script in the src directory.
Copy `example.config.py` to `config.py`, and put it in the same directory as the script.

Edit the config file to fit your needs.
Edit the config file to fit your needs.

##### api_secret
Start by retrieving your API Key from the "Security" section in new [Gandi Account admin panel](https://account.gandi.net/) to be able to make authenticated requests to the API.
Expand All @@ -50,16 +50,16 @@ api_endpoint = 'https://dns.api.gandi.net/api/v5'
```

##### domain
Your domain for the subdomains to be updated
Your domain for the subdomains to be updated


##### subdomains
All subdomains which should be updated. They get created if they do not yet exist.

```
```
subdomains = ["subdomain1", "subdomain2", "subdomain3"]
```
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.

#### Run the script
And run the script:
Expand All @@ -68,17 +68,26 @@ And run the script:
root@dyndns:~/gandi-live-dns-master/src# ./gandi-live-dns.py
Checking dynamic IP: 127.0.0.1
Checking IP from DNS Record subdomain1: 127.0.0.1
IP Address Match - no further action
IPv4 Address Match - no further action
Checking dynamic IP: fe80::1
Checking IP from DNS Record subdomain1: fe80::1
IPv6 Address Match - no further action
```

If your IP has changed, it will be detected and the update will be triggered.
If your IP has changed, it will be detected and the update will be triggered.


```
root@dyndns:~/gandi-live-dns-master/src# ./gandi-live-dns.py
Checking dynamic IP: 127.0.0.2
Checking IP from DNS Record subdomain1: 127.0.0.1
IP Address Mismatch - going to update the DNS Records for the subdomains with new IP 127.0.0.2
IPv4 Address Mismatch - going to update the DNS Records for the subdomains with new IP 127.0.0.2
Status Code: 201 , DNS Record Created , IP updated for subdomain1
Status Code: 201 , DNS Record Created , IP updated for subdomain2
Status Code: 201 , DNS Record Created , IP updated for subdomain3
Checking dynamic IP: fe80::2
Checking IP from DNS Record subdomain1: fe80::1
IPv6 Address Mismatch - going to update the DNS Records for the subdomains with new IP fe80::2
Status Code: 201 , DNS Record Created , IP updated for subdomain1
Status Code: 201 , DNS Record Created , IP updated for subdomain2
Status Code: 201 , DNS Record Created , IP updated for subdomain3
Expand All @@ -96,44 +105,46 @@ optional arguments:

```

The force option runs the script, even when no IP change has been detected.
It will update all subdomains and even create them if they are missing in the
Zone File/Zone UUID. This can be used if additional/new subdomains get appended to the conig file.
The force option runs the script, even when no IP change has been detected.
It will update all subdomains and even create them if they are missing in the
Zone File/Zone UUID. This can be used if additional/new subdomains get appended to the config file.

### IP address lookup service
There exist several providers for this case, but better is to run your own somewhere.
### IP address lookup service
There exist several providers for this case, but better is to run your own somewhere.

#### Poor Mans PHP Solution
On a LAMP Stack, place the file [index.php](https://github.com/cavebeat/gandi-live-dns/blob/master/src/example-index.php) in a directory /ip in your webroot.
On a LAMP Stack, place the file [index.php](https://github.com/cavebeat/gandi-live-dns/blob/master/src/example-index.php) in a directory /ip in your webroot.

```
root@laptop:~# curl https://blog.cavebeat.org/ip/
127.0.0.1
```
This should fit your personal needs and you still selfhost the whole thing.
This should fit your personal needs and you still selfhost the whole thing.

#### IP address lookup service https://ifconfig.co
https://github.com/mpolden/ipd A simple service for looking up your IP address. This is the code that powers [https://ifconfig.co](https://ifconfig.co)

#### use external services
choose one as described in the config file.
#### Use external services
Choose one as described in the config file.
If you are updating AAAA Records with your IPv6 address, make sure your IP address lookup service supports returning your IPv6 address.
See `example.config.py` for how https://ifconfig.co can be used in this way.

### Cron the script

Run the script every five minutes.
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
```
### 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
* command line Argument for verbose mode

### Inspiration

This DynDNS updater is inspired by https://github.com/jasontbradshaw/gandi-dyndns which worked very well
This DynDNS updater is inspired by https://github.com/jasontbradshaw/gandi-dyndns which worked very well
with the classic DNS from Gandiv4 Website and their XML-RPC API.

Gandi has created a new API, i accidently switched to the new DNS Record System, so someone had to start a new updater.
Gandi has created a new API, I accidentally switched to the new DNS Record System, so someone had to start a new updater.
14 changes: 8 additions & 6 deletions src/example.config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
'''
api_endpoint = 'https://dns.api.gandi.net/api/v5'

#your domain with the subdomains in the zone file/UUID
#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
Expand All @@ -29,17 +29,19 @@
#300 seconds = 5 minutes
ttl = '300'

'''
IP address lookup service
'''
IP address lookup service
run your own external IP provider:
+ https://github.com/mpolden/ipd
+ <?php $ip = $_SERVER['REMOTE_ADDR']; ?>
<?php print $ip; ?>
e.g.
+ https://ifconfig.co/ip
e.g.
+ https://[v4|v6].ifconfig.co/ip
+ http://ifconfig.me/ip
+ http://whatismyip.akamai.com/
+ http://ipinfo.io/ip
+ many more ...
'''
ifconfig = 'choose_from_above_or_run_your_own'
#Leave blank to not update that protocol (v4 or v6)
ifconfigv4 = 'choose_from_above_or_run_your_own'
ifconfigv6 = 'choose_from_above_or_run_your_own'
86 changes: 49 additions & 37 deletions src/gandi-live-dns.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
https://www.gnu.org/licenses/gpl-3.0.html

Created on 13 Aug 2017
http://doc.livedns.gandi.net/
http://doc.livedns.gandi.net/
http://doc.livedns.gandi.net/#api-endpoint -> https://dns.gandi.net/api/v5/
'''

Expand All @@ -18,19 +18,19 @@


def get_dynip(ifconfig_provider):
''' 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
'''
''' find out own IP 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
'''
r = requests.get(ifconfig_provider)
print 'Checking dynamic IP: ' , r._content.strip('\n')
return r.content.strip('\n')

def get_uuid():
'''
'''
find out ZONE UUID from domain
Info on domain "DOMAIN"
GET /domains/<DOMAIN>:

'''
url = config.api_endpoint + '/domains/' + config.domain
u = requests.get(url, headers={"X-Api-Key":config.api_secret})
Expand All @@ -42,29 +42,29 @@ def get_uuid():
print json_object['message']
exit()

def get_dnsip(uuid):
def get_dnsip(uuid, rrtype):
''' find out IP from first Subdomain DNS-Record
List all records with name "NAME" and type "TYPE" in the zone UUID
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
'''

url = config.api_endpoint+ '/zones/' + uuid + '/records/' + config.subdomains[0] + '/A'
url = config.api_endpoint+ '/zones/' + uuid + '/records/' + config.subdomains[0] + '/' + rrtype
headers = {"X-Api-Key":config.api_secret}
u = requests.get(url, headers=headers)
if u.status_code == 200:
json_object = json.loads(u._content)
print 'Checking IP from DNS Record' , config.subdomains[0], ':', json_object['rrset_values'][0].encode('ascii','ignore').strip('\n')
return json_object['rrset_values'][0].encode('ascii','ignore').strip('\n')
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', config.subdomains[0]
print json_object['message']
exit()

def update_records(uuid, dynIP, subdomain):
''' update DNS Records for Subdomains
def update_records(uuid, dynIP, subdomain, rrtype):
''' update DNS Records for Subdomains
Change the "NAME"/"TYPE" record from the zone UUID
PUT /zones/<UUID>/records/<NAME>/<TYPE>:
curl -X PUT -H "Content-Type: application/json" \
Expand All @@ -73,7 +73,7 @@ def update_records(uuid, dynIP, subdomain):
"rrset_values": ["<VALUE>"]}' \
https://dns.gandi.net/api/v5/zones/<UUID>/records/<NAME>/<TYPE>
'''
url = config.api_endpoint+ '/zones/' + uuid + '/records/' + subdomain + '/A'
url = config.api_endpoint+ '/zones/' + uuid + '/records/' + subdomain + '/' + rrtype
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)
Expand All @@ -83,7 +83,7 @@ def update_records(uuid, dynIP, subdomain):
print 'Status Code:', u.status_code, ',', json_object['message'], ', IP updated for', subdomain
return True
else:
print 'Error: HTTP Status Code ', u.status_code, 'when trying to update IP from subdomain', subdomain
print 'Error: HTTP Status Code ', u.status_code, 'when trying to update IP from subdomain', subdomain
print json_object['message']
exit()

Expand All @@ -94,37 +94,49 @@ def main(force_update, verbosity):
if verbosity:
print "verbosity turned on - not implemented by now"


#get zone ID from Account
uuid = get_uuid()

#compare dynIP and DNS IP
dynIP = get_dynip(config.ifconfig)
dnsIP = get_dnsip(uuid)

if force_update:
print "Going to update/create the DNS Records for the subdomains"
for sub in config.subdomains:
update_records(uuid, dynIP, sub)
else:
if dynIP == dnsIP:
print "IP Address Match - no further action"

#compare dynIP and DNS IP for IPv4
if config.ifconfigv4 != "":
dynIPv4 = get_dynip(config.ifconfigv4)
dnsIPv4 = get_dnsip(uuid, "A")

if force_update:
print "Going to update/create the DNS Records for the subdomains"
for sub in config.subdomains:
update_records(uuid, dynIPv4, sub, "A")
else:
print "IP Address Mismatch - going to update the DNS Records for the subdomains with new IP", dynIP
if dynIPv4 == dnsIPv4:
print "IPv4 Address Match - no further action"
else:
print "IPv4 Address Mismatch - going to update the DNS Records for the subdomains with new IP", dynIPv4
for sub in config.subdomains:
update_records(uuid, dynIPv4, sub, "A")

#compare dynIP and DNS IP for IPv6
if config.ifconfigv6 != "":
dynIPv6 = get_dynip(config.ifconfigv6)
dnsIPv6 = get_dnsip(uuid, "AAAA")

if force_update:
print "Going to update/create the DNS Records for the subdomains"
for sub in config.subdomains:
update_records(uuid, dynIP, sub)
update_records(uuid, dynIPv6, sub, "AAAA")
else:
if dynIPv6 == dnsIPv6:
print "IPv6 Address Match - no further action"
else:
print "IPv6 Address Mismatch - going to update the DNS Records for the subdomains with new IP", dynIPv6
for sub in config.subdomains:
update_records(uuid, dynIPv6, sub, "AAAA")

if __name__ == "__main__":
parser = argparse.ArgumentParser()
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")
args = parser.parse_args()


main(args.force, args.verbose)






main(args.force, args.verbose)