commit bbc403c3bf9620db6ce25fafe69dd6bbbba56a05 Author: Fabio Date: Tue Mar 11 19:26:36 2025 +0800 first commit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a79687a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# Base image +FROM alpine:latest + + +# Install tools required +RUN apk --no-cache upgrade +RUN apk add --no-cache --virtual=run-deps certbot bash py3-pip python3 python3-dev wget nano py3-virtualenv +RUN virtualenv /venv +ENV PATH="/venv/bin:$PATH" +RUN pip install certbot-dns-ionos +RUN pip install public-ip +RUN mkdir -p /etc/letsencrypt/.secrets +RUN chown root:root /etc/letsencrypt/.secrets +RUN chmod 700 /etc/letsencrypt/.secrets + +# Copy scripts +WORKDIR /scripts +COPY ./scripts /scripts +RUN chmod -R +x /scripts + +# copy IONOS login +COPY ./patachina.it.ini /etc/letsencrypt/.secrets + +# Image starting command +CMD ["/bin/bash", "/scripts/start.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3a73e5 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# Aggiornamento IP e creazione certificato SSL su IONOS + +## Installazione + +Creare file .ini con le API key di IONOS in questo patachina.it.ini +copiare in + + sudo mkdir -p /etc/letsencrypt/.secrets + sudo cp patachina.it.ini /etc/letsencrypt/.secrets + +creare l'immagine con ./built.sh o con il comando + + sudo docker build -t ionos_ddns_ssl . + +far partitire il container con ./run.sh o con il comando + + sudo docker run -d --name ionos_ddns_ssl \ + -e PYTHONUNBUFFERED=1 \ # per visualizzare il log dei file python + -e EMAIL="fabio.micheluz@gmail.com" \ # email per letsencrypt + -e DOMAIN="patachina.it" \ + -v /etc/letsencrypt/.secrets:/secret \ # dove trova il file con le API key di IONOS + -v /etc/letsencrypt:/etc/letsencrypt \ # dove installa i certificati SSL + ionos_ddns_ssl + +in docker-compose.yml o portainer usare + + services: + ionos_ddns_ssl: + container_name: ionos_ddns_ssl + environment: + - PYTHONUNBUFFERED=1 + - EMAIL=fabio.micheluz@gmail.com + - DOMAIN=patachina.it + volumes: + - /etc/letsencrypt/.secrets:/secret + - /etc/letsencrypt:/etc/letsencrypt + image: ionos_ddns_ssl + +## Per fare delle prove + +modificare start.sh in scripts + + #!/bin/sh + bash + +usare folder differente "/etc/letsencrypt1" dove copiare patachina.it.ini + + sudo mkdir -p /etc/letsencrypt1/.secrets + sudo cp patachina.it.ini /etc/letsencrypt1/.secrets + +compilare immagine con un nome diverso esempio "prova" ./built1.sh + + sudo docker build -t prova . + +far partire il container in modalità iterattiva con folder differenti es /etc/letsencrypt1 ./run1.sh + + sudo docker run -it --name prova \ + -e EMAIL="fabio.micheluz@gmail.com" \ + -e DOMAIN="patachina.it" \ + -v /etc/letsencrypt1/.secrets:/secret \ + -v /etc/letsencrypt1:/etc/letsencrypt \ + prova + +ora si è nel docker e si possono provare i comandi \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..1cddfd0 --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +sudo docker build -t ionos_ddns_ssl . diff --git a/build1.sh b/build1.sh new file mode 100755 index 0000000..08dc43a --- /dev/null +++ b/build1.sh @@ -0,0 +1,2 @@ +#!/bin/sh +sudo docker build -t prova . \ No newline at end of file diff --git a/patachina.it.ini b/patachina.it.ini new file mode 100644 index 0000000..2913080 --- /dev/null +++ b/patachina.it.ini @@ -0,0 +1,3 @@ +dns_ionos_prefix = 855b5080c2434ffc99f23fa20f09f0aa +dns_ionos_secret = bcD1lRr5af4UuXUGRSVTj-9uQxrxcj9GKcHo8D3xtaSducnWNxGx35XwqjXOwOSvTO7apFUjDWzbApUShMKPzA +dns_ionos_endpoint = https://api.hosting.ionos.com diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..05f160b --- /dev/null +++ b/run.sh @@ -0,0 +1,2 @@ +#!/bin/sh +sudo docker run -d --name ionos_ddns_ssl -e PYTHONUNBUFFERED=1 -e EMAIL="fabio.micheluz@gmail.com" -e DOMAIN="patachina.it" -v /etc/letsencrypt/.secrets:/secret -v /etc/letsencrypt:/etc/letsencrypt ionos_ddns_ssl diff --git a/run1.sh b/run1.sh new file mode 100755 index 0000000..7befe71 --- /dev/null +++ b/run1.sh @@ -0,0 +1,2 @@ +#!/bin/sh +sudo docker run -it --name prova -e EMAIL="fabio.micheluz@gmail.com" -e DOMAIN="patachina.it" -v /etc/letsencrypt1/.secrets:/secret -v /etc/letsencrypt1:/etc/letsencrypt prova \ No newline at end of file diff --git a/scripts/ionos_dyndns.py b/scripts/ionos_dyndns.py new file mode 100755 index 0000000..1db893c --- /dev/null +++ b/scripts/ionos_dyndns.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 + +# Author: Lázaro Blanc +# GitHub: https://github.com/lazaroblanc +# Usage example: ./ionos_dyndns.py --A --AAAA --api-prefix --api-secret + +import subprocess # Getting IPv6 address from systools instead of via web API +import re # Just regex stuff +import requests # Talk to the API +import json # Work with the API responses +from argparse import ArgumentParser, RawDescriptionHelpFormatter # Commandline argument parsing + +import sys +import logging +logging.basicConfig(stream=sys.stdout, format="%(asctime)s %(message)s", datefmt="%B %d %H:%M:%S", level=logging.INFO) + +def parse_cmdline_args(): + argparser = ArgumentParser( + description="Create and update DNS records for this host using IONOS' API to use as a sort of DynDNS (for example via a cronjob).", + epilog="Author: Lázaro Blanc\nGitHub: https://github.com/lazaroblanc", + formatter_class=RawDescriptionHelpFormatter + ) + argparser.add_argument( + "-4", "--A", + default=False, + action="store_const", + const=True, + help="Create/Update A record" + ) + argparser.add_argument( + "-6", "--AAAA", + default=False, + action="store_const", + const=True, + help="Create/Update AAAA record" + ) + argparser.add_argument( + "-i", "--interface", + default="eth0", + action="store", + metavar="", + help="Interface name for determining the public IPv6 address (Default: eth0)" + ) + argparser.add_argument( + "-H", "--fqdn", + default=subprocess.getoutput(f"hostname -f"), + action="store", + metavar="", + help="Host's FQDN (Default: hostname -f)" + ) + argparser.add_argument( + "--api-prefix", + required=True, + action="store", + metavar="", + help="API key publicprefix" + ) + argparser.add_argument( + "--api-secret", + required=True, + action="store", + metavar="", + help="API key secret" + ) + + args = argparser.parse_args() + + if not args.A and not args.AAAA: + argparser.print_help() + exit() + + return args + + +# Map args to global variables +args = parse_cmdline_args() +fqdn = args.fqdn.lower() +api_url = "https://api.hosting.ionos.com/dns/v1/zones" +api_key_publicprefix = args.api_prefix +api_key_secret = args.api_secret +api_headers = { + "accept": "application/json", + "X-API-Key": f"{api_key_publicprefix}.{api_key_secret}" +} +interface = args.interface + + +def main(): + + domain = get_domain_from_fqdn(fqdn) + zone = get_zone(domain) + all_records = get_all_records_for_fqdn(zone["id"], fqdn) + records_to_create = [] + records_to_update = [] + + # Code duplication. Could use some refactoring but I don't have time for this atm + if args.A: + ipv4_address = get_ipv4_address() + logging.info("Public IPv4: " + ipv4_address) + if filter_records_by_type(all_records, "A"): + if filter_records_by_type(all_records, "A")[0]["content"] == ipv4_address: + logging.info("A record is up-to-date") + else: + logging.info("A record is outdated") + records_to_update.append( + new_record(fqdn, "A", ipv4_address, 60)) + else: + logging.info("No A record found") + records_to_create.append(new_record(fqdn, "A", ipv4_address, 60)) + + # Good god this is ugly. I hate myself for writing this. This really needs refactoring... + if args.AAAA: + ipv6_address = get_ipv6_address(interface) + if ipv6_address != "": + logging.info("Public IPv6: " + ipv6_address) + if filter_records_by_type(all_records, "AAAA"): + if filter_records_by_type(all_records, "AAAA")[0]["content"] == ipv6_address: + logging.info("AAAA record is up-to-date") + else: + logging.info("AAAA record is outdated") + records_to_update.append(new_record( + fqdn, "AAAA", ipv6_address, 60)) + else: + logging.info("No AAAA record found") + records_to_create.append(new_record( + fqdn, "AAAA", ipv6_address, 60)) + else: + logging.info("Could not find a public IPv6 address on this system") + + if (len(records_to_create) > 0): + post_new_records(zone["id"], records_to_create) + + if (len(records_to_update) > 0): + patch_records(zone["id"], records_to_update) + + +def get_domain_from_fqdn(fqdn): + # Place the hyphen at the start of the character class to avoid misinterpretation + regex = r"(?:(?:[\w-]+)\.)?([\w-]+\.\w+)$" + match = re.search(regex, fqdn, re.IGNORECASE) + return match.group(1) if match else None + + +def get_ipv4_address(): + return requests.request("GET", "https://api4.ipify.org").text + + +def get_ipv6_address(interface_name): + ip_output = subprocess.getoutput(f"ip -6 -o address show dev {interface_name} scope global | grep --invert temporary | grep --invert mngtmpaddr") + if ip_output != "": + ip_output_regex = r"(?:inet6)(?:\s+)(.+)(?:\/\d{1,3})" + return re.search(ip_output_regex, ip_output, re.IGNORECASE).group(1) + else: + return "" + + +def get_zone(domain): + response = json.loads(requests.request( + "GET", api_url, headers=api_headers).text) + return list(filter(lambda zone: zone['name'] == domain, response))[0] + + +def get_all_records_for_fqdn(zone_id, host): + url = f"{api_url}/{zone_id}" + records = json.loads(requests.request("GET", url, headers=api_headers).text)['records'] + return list(filter(lambda record: record['name'] == host, records)) + + +def filter_records_by_type(records, type): + return list(filter(lambda record: record['type'] == type, records)) + + +def new_record(name, record_type, ip_address, ttl): + return { + "name": name, + "type": record_type, + "content": ip_address, + "ttl": ttl + } + + +def post_new_records(zone_id, records): + url = f"{api_url}/{zone_id}/records" + return requests.request("POST", url, headers=api_headers, json=records) + + +def patch_records(zone_id, records): + url = f"{api_url}/{zone_id}" + return requests.request("PATCH", url, headers=api_headers, json=records) + + +main() diff --git a/scripts/ip_update.py b/scripts/ip_update.py new file mode 100755 index 0000000..e0e6457 --- /dev/null +++ b/scripts/ip_update.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +import socket +import public_ip as ip +import time +import subprocess +import sys +from configparser import ConfigParser +parser = ConfigParser() +with open('/secret/' + sys.argv[1] + '.ini') as stream: + parser.read_string("[top]\n" + stream.read()) +dns_ionos_prefix=parser.get('top','dns_ionos_prefix') +dns_ionos_secret=parser.get('top','dns_ionos_secret') +wildcard='*.'+ sys.argv[1] +#print(dns_ionos_prefix ,dns_ionos_secret, wildcard) +#print("eseguo ./ionos_dyndns.py -4 -H "+wildcard+" --api-prefix "+dns_ionos_prefix+" --api-secret "+dns_ionos_secret) +#subprocess.call(["./ionos_dyndns.py", "-4","-H",wildcard,"--api-prefix",dns_ionos_prefix,"--api-secret",dns_ionos_secret]) +while True: + a=ip.get() + b=socket.gethostbyname('cicco.' + sys.argv[1]) + #print(a, b) + if a != b: + print() + print("IP cambio indirizzo in", a ) + subprocess.call(["./ionos_dyndns.py", "-4","-H",wildcard,"--api-prefix",dns_ionos_prefix,"--api-secret",dns_ionos_secret]) + else: + print("X",end="",flush=True) + time.sleep(5*60) + diff --git a/scripts/s1.sh b/scripts/s1.sh new file mode 100755 index 0000000..beacc6f --- /dev/null +++ b/scripts/s1.sh @@ -0,0 +1,9 @@ +#!/bin/sh +#echo "certbot certonly --email $EMAIL --authenticator dns-ionos --dns-ionos-credentials /secret/$DOMAIN.ini --dns-ionos-propagation-seconds 900 --server https://acme-v02.api.letsencrypt.org/directory --agree-tos --rsa-key-size 4096 -d $DOMAIN -d *.$DOMAIN" +certbot certonly --non-interactive --email $EMAIL --authenticator dns-ionos --dns-ionos-credentials /secret/$DOMAIN.ini --dns-ionos-propagation-seconds 60 --server https://acme-v02.api.letsencrypt.org/directory --agree-tos --rsa-key-size 4096 -d $DOMAIN -d *.$DOMAIN +while :; do + sleep $((23*60*60)) # Convert to seconds + echo " " + echo "INFO: Attempting SSL certificate renewal" + certbot renew +done diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100755 index 0000000..3727060 --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,5 @@ +#!/bin/bash +./s1.sh & +./ip_update.py $DOMAIN & +wait -n +exit $?