From 6854b239ed5937b29e3fbc865f2deacd570e7b47 Mon Sep 17 00:00:00 2001 From: Adrian Edwards Date: Sun, 9 Jun 2024 21:27:02 -0400 Subject: [PATCH 1/9] ipv6 all the things --- nfsn-ddns.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/nfsn-ddns.py b/nfsn-ddns.py index f4059a9..4505c1d 100644 --- a/nfsn-ddns.py +++ b/nfsn-ddns.py @@ -69,13 +69,13 @@ def makeNFSNHTTPRequest(path, body, nfsn_username, nfsn_apikey): return data -def fetchCurrentIP(): - response = requests.get(IPV4_PROVIDER_URL) +def fetchCurrentIP(v6=False): + response = requests.get(IPV4_PROVIDER_URL if not v6 else IPV6_PROVIDER_URL) response.raise_for_status() return response.text.strip() -def fetchDomainIP(domain, subdomain, nfsn_username, nfsn_apikey): +def fetchDomainIP(domain, subdomain, nfsn_username, nfsn_apikey, v6=False): subdomain = subdomain or "" path = f"/dns/{domain}/listRRs" body = { @@ -95,24 +95,25 @@ def fetchDomainIP(domain, subdomain, nfsn_username, nfsn_apikey): return data[0].get("data") -def replaceDomain(domain, subdomain, current_ip, nfsn_username, nfsn_apikey, create=False, ttl=3600): +def replaceDomain(domain, subdomain, current_ip, nfsn_username, nfsn_apikey, create=False, ttl=3600, v6=False): action = "replaceRR" if not create else "addRR" path = f"/dns/{domain}/{action}" subdomain = subdomain or "" + record_type = "A" if not v6 else "AAAA" body = { "name": subdomain, - "type": "A", + "type": record_type, "data": current_ip, "ttl": ttl } body = urlencode(body) if subdomain == "": - output(f"Setting {domain} to {current_ip}...") + output(f"Setting {record_type} record on {domain} to {current_ip}...") else: - output(f"Setting {subdomain}.{domain} to {current_ip}...") + output(f"Setting {record_type} record on {subdomain}.{domain} to {current_ip}...") makeNFSNHTTPRequest(path, body, nfsn_username, nfsn_apikey) @@ -136,7 +137,7 @@ def createNFSNAuthHeader(nfsn_username, nfsn_apikey, url_path, body) -> Dict[str -def updateIPs(domain, subdomain, domain_ip, current_ip, nfsn_username, nfsn_apikey): +def updateIPs(domain, subdomain, domain_ip, current_ip, nfsn_username, nfsn_apikey, v6=False): # When there's no existing record for a domain name, the # listRRs API query returns the domain name of the name server. if domain_ip is not None and domain_ip.startswith("nearlyfreespeech.net"): @@ -144,10 +145,10 @@ def updateIPs(domain, subdomain, domain_ip, current_ip, nfsn_username, nfsn_apik else: output(f"Current IP: {current_ip} doesn't match Domain IP: {domain_ip or 'UNSET'}") - replaceDomain(domain, subdomain, current_ip, nfsn_username, nfsn_apikey, create=domain_ip is None) + replaceDomain(domain, subdomain, current_ip, nfsn_username, nfsn_apikey, create=domain_ip is None, v6=v6) # Check to see if the update was successful - new_domain_ip = fetchDomainIP(domain, subdomain, nfsn_username, nfsn_apikey) + new_domain_ip = fetchDomainIP(domain, subdomain, nfsn_username, nfsn_apikey, v6=v6) if new_domain_ip is not None and doIPsMatch(ip_address(new_domain_ip), ip_address(current_ip)): output(f"IPs match now! Current IP: {current_ip} Domain IP: {domain_ip}") From 0902bae6194865a2ceb2edde72a76fddce62f3ef Mon Sep 17 00:00:00 2001 From: Adrian Edwards Date: Sun, 9 Jun 2024 21:27:18 -0400 Subject: [PATCH 2/9] factor the update process into a function so it can be rerun for ipv6 --- nfsn-ddns.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/nfsn-ddns.py b/nfsn-ddns.py index 4505c1d..4005130 100644 --- a/nfsn-ddns.py +++ b/nfsn-ddns.py @@ -161,6 +161,17 @@ def ensure_present(value, name): raise ValueError(f"Please ensure {name} is set to a value before running this script") +def check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=False): + + domain_ip = fetchDomainIP(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=v6) + current_ip = fetchCurrentIP(v6=v6) + + if domain_ip is not None and doIPsMatch(ip_address(domain_ip), ip_address(current_ip)): + output(f"IPs still match! Current IP: {current_ip} Domain IP: {domain_ip}") + return + + updateIPs(nfsn_domain, nfsn_subdomain, domain_ip, current_ip, nfsn_username, nfsn_apikey, v6=v6) + if __name__ == "__main__": nfsn_username = os.getenv('USERNAME') @@ -173,10 +184,6 @@ def ensure_present(value, name): ensure_present(nfsn_domain, "DOMAIN") - domain_ip = fetchDomainIP(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey) - current_ip = fetchCurrentIP() - - if domain_ip is not None and doIPsMatch(ip_address(domain_ip), ip_address(current_ip)): - output(f"IPs still match! Current IP: {current_ip} Domain IP: {domain_ip}") - else: - updateIPs(nfsn_domain, nfsn_subdomain, domain_ip, current_ip, nfsn_username, nfsn_apikey) + check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=False) + check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=True) + From 45cb8ffee00cda82b8ec65127bf453d00af3b12d Mon Sep 17 00:00:00 2001 From: Adrian Edwards Date: Sun, 9 Jun 2024 23:22:38 -0400 Subject: [PATCH 3/9] trickle "create if not exists" options all the way up to the main level of the program so it can be turned into a user facing option --- nfsn-ddns.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nfsn-ddns.py b/nfsn-ddns.py index 4005130..068e2c7 100644 --- a/nfsn-ddns.py +++ b/nfsn-ddns.py @@ -137,7 +137,7 @@ def createNFSNAuthHeader(nfsn_username, nfsn_apikey, url_path, body) -> Dict[str -def updateIPs(domain, subdomain, domain_ip, current_ip, nfsn_username, nfsn_apikey, v6=False): +def updateIPs(domain, subdomain, domain_ip, current_ip, nfsn_username, nfsn_apikey, v6=False, create_if_not_exists=False): # When there's no existing record for a domain name, the # listRRs API query returns the domain name of the name server. if domain_ip is not None and domain_ip.startswith("nearlyfreespeech.net"): @@ -145,7 +145,7 @@ def updateIPs(domain, subdomain, domain_ip, current_ip, nfsn_username, nfsn_apik else: output(f"Current IP: {current_ip} doesn't match Domain IP: {domain_ip or 'UNSET'}") - replaceDomain(domain, subdomain, current_ip, nfsn_username, nfsn_apikey, create=domain_ip is None, v6=v6) + replaceDomain(domain, subdomain, current_ip, nfsn_username, nfsn_apikey, create=domain_ip is None and create_if_not_exists, v6=v6) # Check to see if the update was successful new_domain_ip = fetchDomainIP(domain, subdomain, nfsn_username, nfsn_apikey, v6=v6) @@ -161,7 +161,7 @@ def ensure_present(value, name): raise ValueError(f"Please ensure {name} is set to a value before running this script") -def check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=False): +def check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=False, create_if_not_exists=False): domain_ip = fetchDomainIP(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=v6) current_ip = fetchCurrentIP(v6=v6) @@ -184,6 +184,6 @@ def check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=False) ensure_present(nfsn_domain, "DOMAIN") - check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=False) - check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=True) + check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=False, create_if_not_exists=False) + check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=True, create_if_not_exists=False) From 109f1e7207676ff8778132cc878b4a449151f343 Mon Sep 17 00:00:00 2001 From: Adrian Edwards Date: Sun, 9 Jun 2024 23:45:18 -0400 Subject: [PATCH 4/9] add a config option to also run the process for IPv6 --- nfsn-ddns.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nfsn-ddns.py b/nfsn-ddns.py index 068e2c7..e35c98a 100644 --- a/nfsn-ddns.py +++ b/nfsn-ddns.py @@ -183,7 +183,8 @@ def check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=False, ensure_present(nfsn_apikey, "API_KEY") ensure_present(nfsn_domain, "DOMAIN") + v6_enabled=True check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=False, create_if_not_exists=False) - check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=True, create_if_not_exists=False) - + if v6_enabled: + check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=True, create_if_not_exists=False) From 7f1a9a484f592ab755cf56b16db55e60b99b03e0 Mon Sep 17 00:00:00 2001 From: Adrian Edwards Date: Sun, 9 Jun 2024 23:52:19 -0400 Subject: [PATCH 5/9] add CLI and environment variables to enable IPv6 mode --- nfsn-ddns.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/nfsn-ddns.py b/nfsn-ddns.py index e35c98a..d6b3a01 100644 --- a/nfsn-ddns.py +++ b/nfsn-ddns.py @@ -7,6 +7,7 @@ import string from datetime import datetime, timezone import hashlib +import argparse IPAddress = NewType("IPAddress", Union[IPv4Address, IPv6Address]) @@ -174,6 +175,12 @@ def check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=False, if __name__ == "__main__": + parser = argparse.ArgumentParser(description='automate the updating of domain records to create Dynamic DNS for domains registered with NearlyFreeSpeech.net') + # parser.add_argument('integers', metavar='N', type=int, nargs='+', + # help='an integer for the accumulator') + parser.add_argument('--ipv6', '-6', action='store_true', help='also check and update the AAAA (IPv6) records') + + args = parser.parse_args() nfsn_username = os.getenv('USERNAME') nfsn_apikey = os.getenv('API_KEY') nfsn_domain = os.getenv('DOMAIN') @@ -183,7 +190,7 @@ def check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=False, ensure_present(nfsn_apikey, "API_KEY") ensure_present(nfsn_domain, "DOMAIN") - v6_enabled=True + v6_enabled=args.ipv6 or os.getenv('ENABLE_IPV6') is not None check_ips(nfsn_domain, nfsn_subdomain, nfsn_username, nfsn_apikey, v6=False, create_if_not_exists=False) if v6_enabled: From ab78cff8778468972165b6196504f0f1cad8e7d3 Mon Sep 17 00:00:00 2001 From: Adrian Edwards Date: Sun, 9 Jun 2024 23:53:00 -0400 Subject: [PATCH 6/9] document the new IPv6 environment variables --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f99003a..2bf6847 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ Configurations are set by providing the script with environment variables | DOMAIN | Y | Domain that the subdomain belongs to | | SUBDOMAIN | N | Subdomain to update with the script. Leave blank for the bare domain name | | IP_PROVIDER | N | Use a different IP providing service than the default: [http://ipinfo.io/ip](http://ipinfo.io/ip) This might be useful if the default provider is unavailable or is blocked. The alternate provider MUST be served over `http` (please open an issue if this is ever a problem) and MUST return ONLY the IP in the response body | +| IPV6_PROVIDER | N | Use a different IP providing service than the default: [http://v6.ipinfo.io/ip](http://v6.ipinfo.io/ip) This might be useful if the default provider is unavailable or is blocked. The alternate provider MUST be served over `http` (please open an issue if this is ever a problem) and MUST return ONLY the IP in the response body | +| ENABLE_IPV6 | N | Set this to any value to also cause the script to check for and update AAAA records on the specified domain. | ## Running ### Manually From 190a8ae5e5ab1ec15a996b49344947173fb8da51 Mon Sep 17 00:00:00 2001 From: Adrian Edwards Date: Sun, 9 Jun 2024 23:55:36 -0400 Subject: [PATCH 7/9] fix one more hardcoded "A" record type --- nfsn-ddns.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nfsn-ddns.py b/nfsn-ddns.py index d6b3a01..f45f0c6 100644 --- a/nfsn-ddns.py +++ b/nfsn-ddns.py @@ -79,9 +79,10 @@ def fetchCurrentIP(v6=False): def fetchDomainIP(domain, subdomain, nfsn_username, nfsn_apikey, v6=False): subdomain = subdomain or "" path = f"/dns/{domain}/listRRs" + record_type = "A" if not v6 else "AAAA" body = { "name": subdomain, - "type": "A" + "type": record_type } body = urlencode(body) From 9ccaf2a52bfca23cdf75d7051171e1fc97c8b5a9 Mon Sep 17 00:00:00 2001 From: Adrian Edwards Date: Sun, 9 Jun 2024 23:58:27 -0400 Subject: [PATCH 8/9] remove some probably unnecessary extra filtering --- nfsn-ddns.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nfsn-ddns.py b/nfsn-ddns.py index f45f0c6..b080960 100644 --- a/nfsn-ddns.py +++ b/nfsn-ddns.py @@ -88,13 +88,11 @@ def fetchDomainIP(domain, subdomain, nfsn_username, nfsn_apikey, v6=False): response_data = makeNFSNHTTPRequest(path, body, nfsn_username, nfsn_apikey) - data = list(filter(lambda r: r['name'] == subdomain, response_data)) - - if len(data) == 0: + if len(response_data) == 0: output("No IP address is currently set.") return - return data[0].get("data") + return response_data[0].get("data") def replaceDomain(domain, subdomain, current_ip, nfsn_username, nfsn_apikey, create=False, ttl=3600, v6=False): From 8dbebe8af1e1422713bb5c911a1e14d0e86dc8c0 Mon Sep 17 00:00:00 2001 From: Adrian Edwards Date: Mon, 10 Jun 2024 00:35:38 -0400 Subject: [PATCH 9/9] mention AAAA records in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2bf6847..64d4ac4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # NearlyFreeSpeech.NET Dynamic DNS -This script will update the `A` DNS record for a domain/subdomain at [NearlyFreeSpeech.NET](https://www.nearlyfreespeech.net) +This script will update the `A` DNS record (and optionally, the `AAAA` records) for a domain/subdomain at [NearlyFreeSpeech.NET](https://www.nearlyfreespeech.net) with the public IP address for the machine the script runs on. Run this script on a server in which the public IP address is dynamic and changes so your domain is always up to date. ## How It Works There are two steps to this script. First, it retrieves the configured IP address for the domain/subdomain, the current public -IP address of the server, and then compares the two. If the public IP address is different, it updates the `A` record of +IP address of the server, and then compares the two. If the public IP address is different, it updates the `A` (and, if configured, the `AAAA`) record(s) of the domain/subdomain with the new IP address. ## Requirements