diff --git a/README.md b/README.md index f99003a..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 @@ -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 diff --git a/nfsn-ddns.py b/nfsn-ddns.py index f4059a9..b080960 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]) @@ -69,50 +70,50 @@ 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" + record_type = "A" if not v6 else "AAAA" body = { "name": subdomain, - "type": "A" + "type": record_type } body = urlencode(body) 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): +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, 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"): @@ -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 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) + 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}") @@ -160,8 +161,25 @@ 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, create_if_not_exists=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__": + 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') @@ -171,11 +189,8 @@ def ensure_present(value, name): ensure_present(nfsn_apikey, "API_KEY") ensure_present(nfsn_domain, "DOMAIN") + v6_enabled=args.ipv6 or os.getenv('ENABLE_IPV6') is not None - 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, 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)