first commit

This commit is contained in:
Fabio 2025-06-25 15:40:38 +08:00
commit 4add234ba4
9 changed files with 321 additions and 0 deletions

26
Dockerfile Normal file
View file

@ -0,0 +1,26 @@
# Base image
FROM ubuntu
# Install tools required
RUN apt update --fix-missing
RUN apt-get install certbot python3-certbot-nginx -y
RUN apt install nano -y
RUN apt install bash wget -y
RUN apt install supervisor -y
RUN apt install python3-pip python3 -y
RUN apt install python3-virtualenv -y
RUN mkdir -p /var/log/supervisor
RUN virtualenv /venv
ENV PATH="/venv/bin:$PATH"
RUN pip install certbot-dns-ionos
RUN pip install public-ip
RUN pip install tqdm
RUN pip install DateTime
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY scripts/ip_update.py /venv/bin
COPY scripts/ionos_dyndns.py /venv/bin
COPY scripts/ssl_update.py /venv/bin
CMD ["/usr/bin/supervisord"]

20
README.md Normal file
View file

@ -0,0 +1,20 @@
# Installazione
copiare i file patachina.it.ini
per creare l'immagine
./build.sh
per lanciare il docker
./start.sh
i log si trovano all'interno del docker sia per ssl che per ddns come da file supervisord.conf
stderr_logfile=/var/log/ssl.err.log
stdout_logfile=/var/log/ssl.out.log
stderr_logfile=/var/log/ddns.err.log
stdout_logfile=/var/log/ddns.out.log
nel file supervisord.conf vengono anche lanciati i 2 processi

2
build.sh Executable file
View file

@ -0,0 +1,2 @@
#!/bin/sh
sudo docker build -t ionos_ddns_ssl_supervisor2 .

3
patachina.it.ini Executable file
View file

@ -0,0 +1,3 @@
dns_ionos_prefix = 855b5080c2434ffc99f23fa20f09f0aa
dns_ionos_secret = bcD1lRr5af4UuXUGRSVTj-9uQxrxcj9GKcHo8D3xtaSducnWNxGx35XwqjXOwOSvTO7apFUjDWzbApUShMKPzA
dns_ionos_endpoint = https://api.hosting.ionos.com

192
scripts/ionos_dyndns.py Executable file
View file

@ -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_prefix> --api-secret <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()

45
scripts/ip_update.py Executable file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env python3
import socket
import public_ip as ip
import time
import subprocess
import sys
import os
from datetime import datetime
from configparser import ConfigParser
parser = ConfigParser()
domain = os.environ["DOMAIN"]
site = os.environ["SITE"]
sites = site.split(",")
with open('/secret/' + domain + '.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')
#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])
k = 0
while True:
a=ip.get()
b=socket.gethostbyname('cicco.' + domain)
#print(a, b)
if a != b:
for y in sites:
if (y==""):
d=domain
else:
d=y+'.'+domain
print()
print("IP cambio indirizzo di ",d," in ", a )
subprocess.call(["ionos_dyndns.py", "-4","-H",d,"--api-prefix",dns_ionos_prefix,"--api-secret",dns_ionos_secret])
else:
if k == 0:
now = datetime.now()
print(now.strftime("%Y-%m-%d %H:%M"))
if k == 10:
k = 0
else:
k=k+1
print("X", end="", flush=True)
time.sleep(300)

13
scripts/ssl_update.py Executable file
View file

@ -0,0 +1,13 @@
#!/usr/bin/env python3
#1 email
#2 domain
import subprocess
import sys
import time
import os
email = os.environ["EMAIL"]
domain = os.environ["DOMAIN"]
subprocess.call(["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 True:
time.sleep(23*60*60)
subprocess.call(["certbot","renew"])

2
start.sh Executable file
View file

@ -0,0 +1,2 @@
#!/bin/sh
sudo docker run -d --name ionos_ddns_ssl_supervisor -e EMAIL="fabio.micheluz@gmail.com" -e DOMAIN="patachina.it" -v /etc/letsencrypt1/.secrets:/secret -v /etc/letsencrypt1:/etc/letsencrypt ionos_ddns_ssl_supervisor

18
supervisord.conf Normal file
View file

@ -0,0 +1,18 @@
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
[program:ssl]
command=ssl_update.py
#environment = PYTHONUNBUFFERED=1
stderr_logfile=/var/log/ssl.err.log
stdout_logfile=/var/log/ssl.out.log
stdout_logfile_maxbytes=0
[program:ddns]
#environment = PYTHONUNBUFFERED=1
command=ip_update.py
stderr_logfile=/var/log/ddns.err.log
stdout_logfile=/var/log/ddns.out.log
stdout_logfile_maxbytes=0