mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
dyndns: replace dnssec-keygen and nsupdate with python code, drop legacy md5 stuff, drop unecessary dyndns 'private' key
This commit is contained in:
parent
ddb1f86f23
commit
63a84f5398
3 changed files with 51 additions and 54 deletions
2
debian/control
vendored
2
debian/control
vendored
|
@ -18,7 +18,7 @@ Depends: ${python3:Depends}, ${misc:Depends}
|
||||||
, python-is-python3
|
, python-is-python3
|
||||||
, nginx, nginx-extras (>=1.18)
|
, nginx, nginx-extras (>=1.18)
|
||||||
, apt, apt-transport-https, apt-utils, dirmngr
|
, apt, apt-transport-https, apt-utils, dirmngr
|
||||||
, openssh-server, iptables, fail2ban, dnsutils, bind9utils
|
, openssh-server, iptables, fail2ban, bind9-dnsutils
|
||||||
, openssl, ca-certificates, netcat-openbsd, iproute2
|
, openssl, ca-certificates, netcat-openbsd, iproute2
|
||||||
, slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd, libpam-ldapd
|
, slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd, libpam-ldapd
|
||||||
, dnsmasq, resolvconf, libnss-myhostname
|
, dnsmasq, resolvconf, libnss-myhostname
|
||||||
|
|
|
@ -109,7 +109,7 @@ EOF
|
||||||
# If we subscribed to a dyndns domain, add the corresponding cron
|
# If we subscribed to a dyndns domain, add the corresponding cron
|
||||||
# - delay between 0 and 60 secs to spread the check over a 1 min window
|
# - delay between 0 and 60 secs to spread the check over a 1 min window
|
||||||
# - do not run the command if some process already has the lock, to avoid queuing hundreds of commands...
|
# - do not run the command if some process already has the lock, to avoid queuing hundreds of commands...
|
||||||
if ls -l /etc/yunohost/dyndns/K*.private 2>/dev/null; then
|
if ls -l /etc/yunohost/dyndns/K*.key 2>/dev/null; then
|
||||||
cat >$pending_dir/etc/cron.d/yunohost-dyndns <<EOF
|
cat >$pending_dir/etc/cron.d/yunohost-dyndns <<EOF
|
||||||
SHELL=/bin/bash
|
SHELL=/bin/bash
|
||||||
# Every 10 minutes,
|
# Every 10 minutes,
|
||||||
|
|
|
@ -45,14 +45,6 @@ from yunohost.regenconf import regen_conf
|
||||||
|
|
||||||
logger = getActionLogger("yunohost.dyndns")
|
logger = getActionLogger("yunohost.dyndns")
|
||||||
|
|
||||||
DYNDNS_ZONE = "/etc/yunohost/dyndns/zone"
|
|
||||||
|
|
||||||
RE_DYNDNS_PRIVATE_KEY_MD5 = re.compile(r".*/K(?P<domain>[^\s\+]+)\.\+157.+\.private$")
|
|
||||||
|
|
||||||
RE_DYNDNS_PRIVATE_KEY_SHA512 = re.compile(
|
|
||||||
r".*/K(?P<domain>[^\s\+]+)\.\+165.+\.private$"
|
|
||||||
)
|
|
||||||
|
|
||||||
DYNDNS_PROVIDER = "dyndns.yunohost.org"
|
DYNDNS_PROVIDER = "dyndns.yunohost.org"
|
||||||
DYNDNS_DNS_AUTH = ["ns0.yunohost.org", "ns1.yunohost.org"]
|
DYNDNS_DNS_AUTH = ["ns0.yunohost.org", "ns1.yunohost.org"]
|
||||||
|
|
||||||
|
@ -111,6 +103,10 @@ def dyndns_subscribe(operation_logger, domain=None, key=None):
|
||||||
|
|
||||||
operation_logger.start()
|
operation_logger.start()
|
||||||
|
|
||||||
|
# '165' is the convention identifier for hmac-sha512 algorithm
|
||||||
|
# '1234' is idk? doesnt matter, but the old format contained a number here...
|
||||||
|
key_file = f"/etc/yunohost/dyndns/K{domain}.+165+1234.key"
|
||||||
|
|
||||||
if key is None:
|
if key is None:
|
||||||
if len(glob.glob("/etc/yunohost/dyndns/*.key")) == 0:
|
if len(glob.glob("/etc/yunohost/dyndns/*.key")) == 0:
|
||||||
if not os.path.exists("/etc/yunohost/dyndns"):
|
if not os.path.exists("/etc/yunohost/dyndns"):
|
||||||
|
@ -118,35 +114,39 @@ def dyndns_subscribe(operation_logger, domain=None, key=None):
|
||||||
|
|
||||||
logger.debug(m18n.n("dyndns_key_generating"))
|
logger.debug(m18n.n("dyndns_key_generating"))
|
||||||
|
|
||||||
os.system(
|
# Here, we emulate the behavior of the old 'dnssec-keygen' utility
|
||||||
"cd /etc/yunohost/dyndns && "
|
# which since bullseye was replaced by ddns-keygen which is now
|
||||||
f"dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER {domain}"
|
# in the bind9 package ... but installing bind9 will conflict with dnsmasq
|
||||||
)
|
# and is just madness just to have access to a tsig keygen utility -.-
|
||||||
|
|
||||||
|
# Use 512 // 8 = 64 bytes for hmac-sha512 (c.f. https://git.hactrn.net/sra/tsig-keygen/src/master/tsig-keygen.py)
|
||||||
|
secret = base64.b64encode(os.urandom(512 // 8)).decode("ascii")
|
||||||
|
|
||||||
|
# Idk why but the secret is split in two parts, with the first one
|
||||||
|
# being 57-long char ... probably some DNS format
|
||||||
|
secret = f"{secret[:56]} {secret[56:]}"
|
||||||
|
|
||||||
|
key_content = f"{domain}. IN KEY 0 3 165 {secret}"
|
||||||
|
write_to_file(key_file, key_content)
|
||||||
|
|
||||||
chmod("/etc/yunohost/dyndns", 0o600, recursive=True)
|
chmod("/etc/yunohost/dyndns", 0o600, recursive=True)
|
||||||
chown("/etc/yunohost/dyndns", "root", recursive=True)
|
chown("/etc/yunohost/dyndns", "root", recursive=True)
|
||||||
|
|
||||||
private_file = glob.glob("/etc/yunohost/dyndns/*%s*.private" % domain)[0]
|
|
||||||
key_file = glob.glob("/etc/yunohost/dyndns/*%s*.key" % domain)[0]
|
|
||||||
with open(key_file) as f:
|
|
||||||
key = f.readline().strip().split(" ", 6)[-1]
|
|
||||||
|
|
||||||
import requests # lazy loading this module for performance reasons
|
import requests # lazy loading this module for performance reasons
|
||||||
|
|
||||||
# Send subscription
|
# Send subscription
|
||||||
try:
|
try:
|
||||||
b64encoded_key = base64.b64encode(key.encode()).decode()
|
# Yeah the secret is already a base64-encoded but we double-bas64-encode it, whatever...
|
||||||
|
b64encoded_key = base64.b64encode(secret.encode()).decode()
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512",
|
f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512",
|
||||||
data={"subdomain": domain},
|
data={"subdomain": domain},
|
||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
rm(private_file, force=True)
|
|
||||||
rm(key_file, force=True)
|
rm(key_file, force=True)
|
||||||
raise YunohostError("dyndns_registration_failed", error=str(e))
|
raise YunohostError("dyndns_registration_failed", error=str(e))
|
||||||
if r.status_code != 201:
|
if r.status_code != 201:
|
||||||
rm(private_file, force=True)
|
|
||||||
rm(key_file, force=True)
|
rm(key_file, force=True)
|
||||||
try:
|
try:
|
||||||
error = json.loads(r.text)["error"]
|
error = json.loads(r.text)["error"]
|
||||||
|
@ -154,7 +154,7 @@ def dyndns_subscribe(operation_logger, domain=None, key=None):
|
||||||
error = f'Server error, code: {r.status_code}. (Message: "{r.text}")'
|
error = f'Server error, code: {r.status_code}. (Message: "{r.text}")'
|
||||||
raise YunohostError("dyndns_registration_failed", error=error)
|
raise YunohostError("dyndns_registration_failed", error=error)
|
||||||
|
|
||||||
# Yunohost regen conf will add the dyndns cron job if a private key exists
|
# Yunohost regen conf will add the dyndns cron job if a key exists
|
||||||
# in /etc/yunohost/dyndns
|
# in /etc/yunohost/dyndns
|
||||||
regen_conf(["yunohost"])
|
regen_conf(["yunohost"])
|
||||||
|
|
||||||
|
@ -185,6 +185,11 @@ def dyndns_update(
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from yunohost.dns import _build_dns_conf
|
from yunohost.dns import _build_dns_conf
|
||||||
|
import dns.query
|
||||||
|
import dns.tsig
|
||||||
|
import dns.tsigkeyring
|
||||||
|
import dns.update
|
||||||
|
|
||||||
|
|
||||||
# If domain is not given, try to guess it from keys available...
|
# If domain is not given, try to guess it from keys available...
|
||||||
key = None
|
key = None
|
||||||
|
@ -196,7 +201,7 @@ def dyndns_update(
|
||||||
|
|
||||||
# If key is not given, pick the first file we find with the domain given
|
# If key is not given, pick the first file we find with the domain given
|
||||||
elif key is None:
|
elif key is None:
|
||||||
keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.private")
|
keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key")
|
||||||
|
|
||||||
if not keys:
|
if not keys:
|
||||||
raise YunohostValidationError("dyndns_key_not_found")
|
raise YunohostValidationError("dyndns_key_not_found")
|
||||||
|
@ -217,12 +222,14 @@ def dyndns_update(
|
||||||
host = domain.split(".")[1:]
|
host = domain.split(".")[1:]
|
||||||
host = ".".join(host)
|
host = ".".join(host)
|
||||||
|
|
||||||
logger.debug("Building zone update file ...")
|
logger.debug("Building zone update ...")
|
||||||
|
|
||||||
lines = [
|
with open(key) as f:
|
||||||
f"server {DYNDNS_PROVIDER}",
|
key = f.readline().strip().split(" ", 6)[-1]
|
||||||
f"zone {host}",
|
|
||||||
]
|
keyring = dns.tsigkeyring.from_text({f'{domain}.': key})
|
||||||
|
# Python's dns.update is similar to the old nsupdate cli tool
|
||||||
|
update = dns.update.Update(domain, keyring=keyring, keyalgorithm=dns.tsig.HMAC_SHA512)
|
||||||
|
|
||||||
auth_resolvers = []
|
auth_resolvers = []
|
||||||
|
|
||||||
|
@ -293,9 +300,8 @@ def dyndns_update(
|
||||||
# [{"name": "...", "ttl": "...", "type": "...", "value": "..."}]
|
# [{"name": "...", "ttl": "...", "type": "...", "value": "..."}]
|
||||||
for records in dns_conf.values():
|
for records in dns_conf.values():
|
||||||
for record in records:
|
for record in records:
|
||||||
action = "update delete {name}.{domain}.".format(domain=domain, **record)
|
name = f"{record['name']}.{domain}." if record['name'] != "@" else f"{domain}."
|
||||||
action = action.replace(" @.", " ")
|
update.delete(name)
|
||||||
lines.append(action)
|
|
||||||
|
|
||||||
# Add the new records for all domain/subdomains
|
# Add the new records for all domain/subdomains
|
||||||
|
|
||||||
|
@ -307,32 +313,22 @@ def dyndns_update(
|
||||||
if record["value"] == "@":
|
if record["value"] == "@":
|
||||||
record["value"] = domain
|
record["value"] = domain
|
||||||
record["value"] = record["value"].replace(";", r"\;")
|
record["value"] = record["value"].replace(";", r"\;")
|
||||||
|
name = f"{record['name']}.{domain}." if record['name'] != "@" else f"{domain}."
|
||||||
|
|
||||||
action = "update add {name}.{domain}. {ttl} {type} {value}".format(
|
update.add(name, record['ttl'], record['type'], record['value'])
|
||||||
domain=domain, **record
|
|
||||||
)
|
|
||||||
action = action.replace(" @.", " ")
|
|
||||||
lines.append(action)
|
|
||||||
|
|
||||||
lines += ["show", "send"]
|
|
||||||
|
|
||||||
# Write the actions to do to update to a file, to be able to pass it
|
|
||||||
# to nsupdate as argument
|
|
||||||
write_to_file(DYNDNS_ZONE, "\n".join(lines))
|
|
||||||
|
|
||||||
logger.debug("Now pushing new conf to DynDNS host...")
|
logger.debug("Now pushing new conf to DynDNS host...")
|
||||||
|
logger.debug(update)
|
||||||
|
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
try:
|
try:
|
||||||
command = ["/usr/bin/nsupdate", "-k", key, DYNDNS_ZONE]
|
dns.query.tcp(update, auth_resolvers[0])
|
||||||
subprocess.check_call(command)
|
except Exception as e:
|
||||||
except subprocess.CalledProcessError:
|
logger.error(e)
|
||||||
raise YunohostError("dyndns_ip_update_failed")
|
raise YunohostError("dyndns_ip_update_failed")
|
||||||
|
|
||||||
logger.success(m18n.n("dyndns_ip_updated"))
|
logger.success(m18n.n("dyndns_ip_updated"))
|
||||||
else:
|
else:
|
||||||
print(read_file(DYNDNS_ZONE))
|
|
||||||
print("")
|
|
||||||
print(
|
print(
|
||||||
"Warning: dry run, this is only the generated config, it won't be applied"
|
"Warning: dry run, this is only the generated config, it won't be applied"
|
||||||
)
|
)
|
||||||
|
@ -347,13 +343,14 @@ def _guess_current_dyndns_domain():
|
||||||
dynette...)
|
dynette...)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
DYNDNS_KEY_REGEX = re.compile(
|
||||||
|
r".*/K(?P<domain>[^\s\+]+)\.\+165.+\.key$"
|
||||||
|
)
|
||||||
|
|
||||||
# Retrieve the first registered domain
|
# Retrieve the first registered domain
|
||||||
paths = list(glob.iglob("/etc/yunohost/dyndns/K*.private"))
|
paths = list(glob.iglob("/etc/yunohost/dyndns/K*.key"))
|
||||||
for path in paths:
|
for path in paths:
|
||||||
# MD5 is legacy ugh
|
match = DYNDNS_KEY_REGEX.match(path)
|
||||||
match = RE_DYNDNS_PRIVATE_KEY_MD5.match(path)
|
|
||||||
if not match:
|
|
||||||
match = RE_DYNDNS_PRIVATE_KEY_SHA512.match(path)
|
|
||||||
if not match:
|
if not match:
|
||||||
continue
|
continue
|
||||||
_domain = match.group("domain")
|
_domain = match.group("domain")
|
||||||
|
|
Loading…
Add table
Reference in a new issue