commit bc17a5d4581cbaff541e6a0af47853b0e8ead3a9 Author: Daniel Langbein Date: Mon Mar 27 21:29:52 2023 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1e11b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea/ +/cfg/ +/venv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..f2a1890 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# netcup DNS + +Update DNS records with your current external IP address using the netcup DNS API. + +## TODOs + +Alternative external IP detection: + +```python +def external_ip_upnp(): + """ + https://stackoverflow.com/a/41385033 + + Didn't work for me. Even after double checking fritz.box settings: + + fritz.box > Heimnetz > Netzwerk > Statusinformationen über UPnP übertragen + """ + import miniupnpc + u = miniupnpc.UPnP() + u.discoverdelay = 1000 + u.discover() + u.selectigd() + print('external ip address: {}'.format(u.externalipaddress())) +``` diff --git a/main.py b/main.py new file mode 100644 index 0000000..98e1a62 --- /dev/null +++ b/main.py @@ -0,0 +1,88 @@ +import configparser +import ipaddress +from pathlib import Path + +import requests +from nc_dnsapi import Client, DNSRecord + + +def main(): + """ + https://github.com/nbuchwitz/nc_dnsapi + """ + destination = external_ipv4() + + files = [file for file in Path('cfg').iterdir() if file.name.endswith('.cfg') and file.is_file()] + for file in files: + cfg = configparser.ConfigParser() + cfg.read(file) + customer = cfg['credentials']['customer'] + api_key = cfg['credentials']['api_key'] + api_password = cfg['credentials']['api_password'] + + domains = [section for section in cfg.sections() if section != 'credentials'] + + with Client(customer, api_key, api_password) as api: + for domain in domains: + hostname = cfg[domain]['hostname'] + type_ = cfg[domain]['type'] + update_record_destination(api, domain, hostname, type_, destination) + + +def update_record_destination(api: Client, domain: str, hostname: str, type_: str, destination: str) -> None: + record = get_record(api, domain, hostname, type_) + record.destination = destination + api.update_dns_record(domain, record) + + +def get_record(api: Client, domain: str, hostname: str, type_: str) -> DNSRecord: + records: list[DNSRecord] = api.dns_records(domain) + record: DNSRecord + + matches = [record for record in records if record.hostname == hostname and record.type == type_] + if len(matches) != 1: + raise Exception(f'Expected one DNSRecord for {hostname}.{domain}, but got {len(matches)}') + return matches[0] + + +def external_ipv4() -> str: + """ + :return: Public IPv4 address + """ + + # IPv4 only. + endpoints = ['https://checkipv4.dedyn.io/', 'https://api.ipify.org', 'https://v4.ident.me/'] + # Not sure if they return IPv4 addresses only, + # so we try these endpoints last. + endpoints += ['https://ipinfo.io/ip'] + + for endpoint in endpoints: + backup = None + try: + # Force the usage of IPv4 + # https://stackoverflow.com/a/72440253/6334421 + # + # Alternatively, use urllib3: https://stackoverflow.com/a/46972341/6334421 + backup = requests.packages.urllib3.util.connection.HAS_IPV6 + requests.packages.urllib3.util.connection.HAS_IPV6 = False + + # Timeout after 5 seconds. + ip = requests.get(endpoint, timeout=5).text.strip() + # Check if it is actually an IPv4 address. + # Some services, such as e.g. v4.ident.me, sometimes return IPv6. + ipv4 = ipaddress.ip_address(ip) + if not isinstance(ipv4, ipaddress.IPv4Address): + continue + # Return ip address as string. + return ipv4.exploded + except Exception as _e: + continue + finally: + # Allow usage of IPv6 again. + if backup is not None: + requests.packages.urllib3.util.connection.HAS_IPV6 = backup + raise Exception('Could not determine public IPv4 address.') + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c4bd0b6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +nc-dnsapi