first commit
This commit is contained in:
commit
bbc403c3bf
11 changed files with 334 additions and 0 deletions
25
Dockerfile
Normal file
25
Dockerfile
Normal file
|
@ -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"]
|
64
README.md
Normal file
64
README.md
Normal file
|
@ -0,0 +1,64 @@
|
|||
# Aggiornamento IP e creazione certificato SSL su IONOS
|
||||
|
||||
## Installazione
|
||||
|
||||
Creare file <dominio>.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
|
2
build.sh
Executable file
2
build.sh
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
sudo docker build -t ionos_ddns_ssl .
|
2
build1.sh
Executable file
2
build1.sh
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
sudo docker build -t prova .
|
3
patachina.it.ini
Normal file
3
patachina.it.ini
Normal file
|
@ -0,0 +1,3 @@
|
|||
dns_ionos_prefix = 855b5080c2434ffc99f23fa20f09f0aa
|
||||
dns_ionos_secret = bcD1lRr5af4UuXUGRSVTj-9uQxrxcj9GKcHo8D3xtaSducnWNxGx35XwqjXOwOSvTO7apFUjDWzbApUShMKPzA
|
||||
dns_ionos_endpoint = https://api.hosting.ionos.com
|
2
run.sh
Executable file
2
run.sh
Executable file
|
@ -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
|
2
run1.sh
Executable file
2
run1.sh
Executable file
|
@ -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
|
192
scripts/ionos_dyndns.py
Executable file
192
scripts/ionos_dyndns.py
Executable 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()
|
28
scripts/ip_update.py
Executable file
28
scripts/ip_update.py
Executable file
|
@ -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)
|
||||
|
9
scripts/s1.sh
Executable file
9
scripts/s1.sh
Executable file
|
@ -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
|
5
scripts/start.sh
Executable file
5
scripts/start.sh
Executable file
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
./s1.sh &
|
||||
./ip_update.py $DOMAIN &
|
||||
wait -n
|
||||
exit $?
|
Loading…
Reference in a new issue