From bbc403c3bf9620db6ce25fafe69dd6bbbba56a05 Mon Sep 17 00:00:00 2001 From: Fabio Date: Tue, 11 Mar 2025 19:26:36 +0800 Subject: [PATCH] first commit --- Dockerfile | 25 ++++++ README.md | 64 ++++++++++++++ build.sh | 2 + build1.sh | 2 + patachina.it.ini | 3 + run.sh | 2 + run1.sh | 2 + scripts/ionos_dyndns.py | 192 ++++++++++++++++++++++++++++++++++++++++ scripts/ip_update.py | 28 ++++++ scripts/s1.sh | 9 ++ scripts/start.sh | 5 ++ 11 files changed, 334 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100755 build.sh create mode 100755 build1.sh create mode 100644 patachina.it.ini create mode 100755 run.sh create mode 100755 run1.sh create mode 100755 scripts/ionos_dyndns.py create mode 100755 scripts/ip_update.py create mode 100755 scripts/s1.sh create mode 100755 scripts/start.sh 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 $?