mirror of
https://codeberg.org/privacy1st/netcup-dns
synced 2024-12-22 23:36:04 +01:00
feat: AAAA records and json configuration
This commit is contained in:
parent
8e76f6de1e
commit
27492ba1ef
@ -1,12 +1,12 @@
|
|||||||
# netcup DNS
|
# netcup DNS
|
||||||
|
|
||||||
Update DNS records with your current external IP address using the netcup DNS API.
|
Update DNS A/AAAA records with your current external IP address using the netcup DNS API.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
For each netcup customer, create a `.ini` configuration file.
|
For each netcup customer, create a `.json` configuration file inside `/etc/netcup-dpns`.
|
||||||
|
|
||||||
There is an [example configuration](cfg/example.ini).
|
There is an [example configuration](cfg/example.json).
|
||||||
|
|
||||||
## TODOs
|
## TODOs
|
||||||
|
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
;[credentials]
|
|
||||||
;customer = 123456
|
|
||||||
;api_key = abcdefghijklmnopqrstuvwxyz
|
|
||||||
;api_password = abcdefghijklmnopqrstuvwxyz
|
|
||||||
;
|
|
||||||
;[example.com]
|
|
||||||
;hostname = @
|
|
||||||
;type = A
|
|
||||||
;
|
|
||||||
;[foo.bar]
|
|
||||||
;hostname = @
|
|
||||||
;type = A
|
|
25
cfg/example.json
Normal file
25
cfg/example.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"disabled": true,
|
||||||
|
|
||||||
|
"customer": 123456,
|
||||||
|
"api_key": "abcdefghijklmnopqrstuvwxyz",
|
||||||
|
"api_password": "abcdefghijklmnopqrstuvwxyz",
|
||||||
|
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"domain": "example.com",
|
||||||
|
"hostname": "@",
|
||||||
|
"type": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"domain": "example.com",
|
||||||
|
"hostname": "@",
|
||||||
|
"type": "AAAA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"domain": "foo.bar",
|
||||||
|
"hostname": "@",
|
||||||
|
"type": "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
name = netcup-dns
|
name = netcup-dns
|
||||||
version = 0.1.0
|
version = 0.2.0
|
||||||
author = Daniel Langbein
|
author = Daniel Langbein
|
||||||
author_email = daniel@systemli.org
|
author_email = daniel@systemli.org
|
||||||
description = Update DNS records with your current external IP address using the netcup DNS API.
|
description = Update DNS records with your current external IP address using the netcup DNS API.
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import configparser
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from nc_dnsapi import Client, DNSRecord
|
from nc_dnsapi import Client, DNSRecord
|
||||||
@ -22,33 +23,39 @@ def main():
|
|||||||
if not cfg_dir.exists():
|
if not cfg_dir.exists():
|
||||||
raise Exception(f'The config directory is missing: {cfg_dir}')
|
raise Exception(f'The config directory is missing: {cfg_dir}')
|
||||||
|
|
||||||
cfg_files = [file for file in cfg_dir.iterdir() if file.name.endswith('.ini') and file.is_file()]
|
cfg_files = [file for file in cfg_dir.iterdir() if file.name.endswith('.json') and file.is_file()]
|
||||||
for cfg_file in cfg_files:
|
for cfg_file in cfg_files:
|
||||||
cfg = configparser.ConfigParser()
|
cfg = json.loads(cfg_file.read_text())
|
||||||
cfg.read(cfg_file)
|
|
||||||
if len(cfg.sections()) == 0:
|
if len(cfg) == 0 or cfg['disabled'] is True:
|
||||||
# Skip completely empty configuration file.
|
# Skip empty or disabled configuration file.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
customer = cfg['credentials']['customer']
|
customer = cfg['customer']
|
||||||
api_key = cfg['credentials']['api_key']
|
api_key = cfg['api_key']
|
||||||
api_password = cfg['credentials']['api_password']
|
api_password = cfg['api_password']
|
||||||
|
|
||||||
domains = [section for section in cfg.sections() if section != 'credentials']
|
|
||||||
|
|
||||||
|
entries = cfg['records']
|
||||||
with Client(customer, api_key, api_password) as api:
|
with Client(customer, api_key, api_password) as api:
|
||||||
for domain in domains:
|
for entry in entries:
|
||||||
hostname = cfg[domain]['hostname']
|
domain = entry['domain']
|
||||||
type_ = cfg[domain]['type'].upper()
|
hostname = entry['hostname']
|
||||||
|
type_ = entry['type'].upper()
|
||||||
|
|
||||||
if type_ == 'A':
|
if type_ == 'A':
|
||||||
|
# Lazy: Only determine external IPv4 if an A record shall be updated.
|
||||||
destination = external_ipv4()
|
destination = external_ipv4()
|
||||||
if update_record_destination(api, domain, hostname, type_, destination=destination):
|
elif type_ == 'AAAA':
|
||||||
print(f'Set {hostname}.{domain} {type_} record to {destination}')
|
# Lazy: Only determine external IPv6 if an AAAA record shall be updated.
|
||||||
else:
|
destination = external_ipv6()
|
||||||
print(f'The {hostname}.{domain} {type_} record points already to {destination}')
|
|
||||||
else:
|
else:
|
||||||
raise Exception(f'DNS record type {type_} is not supported.')
|
raise Exception(f'DNS record type {type_} is not supported.')
|
||||||
|
|
||||||
|
if update_record_destination(api, domain, hostname, type_, destination=destination):
|
||||||
|
print(f'Set {hostname}.{domain} {type_} record to {destination}')
|
||||||
|
else:
|
||||||
|
print(f'The {hostname}.{domain} {type_} record points already to {destination}')
|
||||||
|
|
||||||
|
|
||||||
def update_record_destination(api: Client, domain: str, hostname: str, type_: str, destination: str) -> bool:
|
def update_record_destination(api: Client, domain: str, hostname: str, type_: str, destination: str) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -89,51 +96,90 @@ def get_record(api: Client, domain: str, hostname: str, type_: str) -> DNSRecord
|
|||||||
return matches[0]
|
return matches[0]
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=None)
|
def ipv4_endpoints() -> list[str]:
|
||||||
def external_ipv4(timeout=5) -> str:
|
|
||||||
"""
|
"""
|
||||||
:argument timeout: Timeout for each IP detection webservice in seconds.
|
:return: List of services that return your external IPv4 address.
|
||||||
:return: Public IPv4 address
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# IPv4 only.
|
# IPv4 only.
|
||||||
endpoints = ['https://checkipv4.dedyn.io/', 'https://api.ipify.org', 'https://v4.ident.me/']
|
endpoints = ['https://checkipv4.dedyn.io', 'https://api.ipify.org', 'https://v4.ident.me/']
|
||||||
# Not sure if they return IPv4 addresses only,
|
# Not sure if they return IPv4 addresses only.
|
||||||
# so we try these endpoints last.
|
endpoints += ['https://ipinfo.io/ip', 'https://ifconfig.me/ip']
|
||||||
|
return endpoints
|
||||||
|
|
||||||
|
|
||||||
|
def ipv6_endpoints() -> list[str]:
|
||||||
|
"""
|
||||||
|
:return: List of services that return your external IPv6 address.
|
||||||
|
"""
|
||||||
|
# IPv6 only.
|
||||||
|
endpoints = ['https://checkipv6.dedyn.io', 'https://api6.ipify.org', 'https://v6.ident.me/']
|
||||||
|
# Returns either IPv4 or IPv6.
|
||||||
|
endpoints += []
|
||||||
|
# Not sure if they return IPv6.
|
||||||
endpoints += ['https://ipinfo.io/ip']
|
endpoints += ['https://ipinfo.io/ip']
|
||||||
|
return 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
|
|
||||||
|
|
||||||
for endpoint in endpoints:
|
def external_ip(version: Type[ipaddress.IPv4Address | ipaddress.IPv6Address], timeout: float = 5,
|
||||||
|
endpoints: list[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
:param version: Weather the public IPv4 or IPv6 address shall be determined.
|
||||||
|
:param endpoints: List of webservices that return ones public IP IPv4/IPv6 address.
|
||||||
|
:argument timeout: Timeout for each webservice in seconds.
|
||||||
|
:return: Public IPv4/IPv6 address.
|
||||||
|
"""
|
||||||
|
if endpoints is None:
|
||||||
|
if version == ipaddress.IPv4Address:
|
||||||
|
endpoints = ipv4_endpoints()
|
||||||
|
elif version == ipaddress.IPv6Address:
|
||||||
|
endpoints = ipv6_endpoints()
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Invalid argument: {version}')
|
||||||
|
|
||||||
|
if len(endpoints) == 0:
|
||||||
|
raise ValueError(f'Invalid argument: {endpoints}')
|
||||||
|
|
||||||
|
for endpoint in endpoints:
|
||||||
|
try:
|
||||||
# Timeout after 5 seconds.
|
# Timeout after 5 seconds.
|
||||||
try:
|
ip = requests.get(endpoint, timeout=timeout).text.strip()
|
||||||
ip = requests.get(endpoint, timeout=timeout).text.strip()
|
except requests.exceptions.RequestException:
|
||||||
except requests.exceptions.RequestException:
|
continue
|
||||||
continue
|
|
||||||
|
|
||||||
# Check if it is actually an IPv4 address.
|
try:
|
||||||
# Some services, such as e.g. v4.ident.me, sometimes return IPv6.
|
# Try to parse the IP address.
|
||||||
try:
|
parsed_ip = ipaddress.ip_address(ip)
|
||||||
ipv4 = ipaddress.ip_address(ip)
|
except ValueError:
|
||||||
except ValueError:
|
continue
|
||||||
continue
|
|
||||||
if not isinstance(ipv4, ipaddress.IPv4Address):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Return ip address as string.
|
# Check if it is actually an IPv4/IPv6 address.
|
||||||
return ipv4.exploded
|
if not isinstance(parsed_ip, version):
|
||||||
finally:
|
continue
|
||||||
if backup is not None:
|
|
||||||
requests.packages.urllib3.util.connection.HAS_IPV6 = backup
|
|
||||||
|
|
||||||
raise Exception('Could not determine public IPv4 address.')
|
# Return IP address as string.
|
||||||
|
return parsed_ip.exploded
|
||||||
|
|
||||||
|
raise Exception('Could not determine public IP address.')
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def external_ipv4(timeout: float = 5, endpoints: list[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
:param endpoints: List of webservices that return ones public IPv4 address.
|
||||||
|
:argument timeout: Timeout for each webservice in seconds.
|
||||||
|
:return: Public IPv4 address.
|
||||||
|
"""
|
||||||
|
return external_ip(ipaddress.IPv4Address, timeout, endpoints)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def external_ipv6(timeout: float = 5, endpoints: list[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
:param endpoints: List of webservices that return ones public IPv6 address.
|
||||||
|
:argument timeout: Timeout for each webservice in seconds.
|
||||||
|
:return: Public IPv6 address.
|
||||||
|
"""
|
||||||
|
return external_ip(ipaddress.IPv6Address, timeout, endpoints)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
Loading…
Reference in New Issue
Block a user