From ed2d6949958b2d879f186a0aa3f776f13edf1a8b Mon Sep 17 00:00:00 2001 From: John M <5000729+jmuchovej@users.noreply.github.com> Date: Thu, 25 Feb 2021 03:21:57 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Add=20ZeroTier,=20arm64=20suppor?= =?UTF-8?q?t,=20and=20some=20QOL=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds ZeroTier support within the container (currently not toggleable). - Add arm64 support. - Removes the need for a configuration file. - Migrates Corefile into /app, as it's now more generic (while still supporting same name-resolution features). - Move to port 5053 exposure so that it doesn't conflict with built-in DNS services. - Move from hand-built ZeroTier API to ZTC, a project by one of ZeroTier's founders to access ZeroTier Central's API. - TODO: figure out how to lessen the bloat from node.js app. - Migrate from `invoke` to `click` for CLI interface. --- Dockerfile | 92 ++++++++++++++++---------- requirements.txt | 4 -- root/app/Corefile | 10 +++ root/app/inv | 3 - root/app/requirements.txt | 3 + root/app/tasks/__init__.py | 6 -- root/app/tasks/templates/Corefile.j2 | 9 --- root/app/tasks/ztapi.py | 71 -------------------- root/app/tasks/ztdns.py | 56 ---------------- root/app/update-networks | 5 ++ root/app/utils.py | 60 +++++++++++++++++ root/app/zerodns | 40 +++++++++++ root/etc/cont-init.d/40-cp-networks | 4 ++ root/etc/cont-init.d/50-setup-ztc | 5 ++ root/etc/cont-init.d/60-update-hosts | 4 +- root/etc/cont-init.d/70-update-coredns | 4 -- root/etc/crontabs/root | 4 +- root/etc/services.d/coredns/run | 2 +- root/etc/services.d/zerotier-one/run | 7 ++ 19 files changed, 195 insertions(+), 194 deletions(-) delete mode 100644 requirements.txt create mode 100644 root/app/Corefile delete mode 100755 root/app/inv create mode 100644 root/app/requirements.txt delete mode 100755 root/app/tasks/__init__.py delete mode 100755 root/app/tasks/templates/Corefile.j2 delete mode 100755 root/app/tasks/ztapi.py delete mode 100755 root/app/tasks/ztdns.py create mode 100755 root/app/update-networks create mode 100644 root/app/utils.py create mode 100755 root/app/zerodns create mode 100755 root/etc/cont-init.d/40-cp-networks create mode 100755 root/etc/cont-init.d/50-setup-ztc delete mode 100755 root/etc/cont-init.d/70-update-coredns create mode 100644 root/etc/services.d/zerotier-one/run diff --git a/Dockerfile b/Dockerfile index 3bdb5b6..4644384 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,51 +1,71 @@ -FROM ghcr.io/linuxserver/baseimage-ubuntu:focal +# Build ZeroTier ----------------------------------------------------------------------- +FROM --platform=$TARGETPLATFORM lsiodev/ubuntu:focal as zerotier +ARG ZeroTier + +RUN apt-get update -y && apt-get install -y git make gcc g++ +RUN git clone https://github.com/zerotier/ZeroTierOne /src +WORKDIR /src + +RUN commit="$(git log ${ZeroTier} -n1 | head -1 | cut -d ' ' -f 2)" \ + git reset --quiet --hard ${commit} +RUN make -f make-linux.mk +RUN chmod +x zerotier-one + +# Build CoreDNS ------------------------------------------------------------------------ +FROM --platform=$TARGETPLATFORM lsiodev/ubuntu:focal as coredns +ARG CoreDNS + +RUN apt-get update -y && apt-get install -y git make golang +RUN git clone https://github.com/coredns/coredns /src +WORKDIR /src + +RUN commit="$(git log ${CoreDNS} -n1 | head -1 | cut -d ' ' -f 2)" \ + git reset --quiet --hard ${commit} +RUN make +RUN chmod +x coredns + +# Setup ZeroDNS ------------------------------------------------------------------------ +FROM --platform=$TARGETPLATFORM lsiodev/ubuntu:focal as zerodns # set version label -ARG BUIL_DATE +ARG BUILD_DATE ARG ZeroDNS ARG CoreDNS -ARG CoreDNSpkg -LABEL build_version="DNS.zt version:- ${ZeroDNS} Build-date:- ${BUILD_DATE}" -LABEL maintainer="ionlights" +ARG ZeroTier +LABEL build_version="ZeroDNS version:- ${ZeroDNS} Build-date:- ${BUILD_DATE}" +LABEL maintainer="jmuchovej" LABEL vCoreDNS="${CoreDNS}" LABEL vZeroDNS="${ZeroDNS}" +LABEL vZeroTier="${ZeroTier}" # environment settings ARG DEBIAN_FRONTEND="noninteractive" -ENV HOME="/config" \ -XDG_CONFIG_HOME="/config" \ -XDG_DATA_HOME="/config" - -# add dependencies -RUN echo "*** install packages ***" && \ - apt-get update && \ - apt-get install -y \ - ca-certificates \ - python3 \ - python3-pip - -# install CoreDNS -RUN echo "*** install CoreDNS ***" && \ - curl -fsSL ${CoreDNSpkg} -o /tmp/coredns.tgz && \ - tar -xzf /tmp/coredns.tgz && \ - mv coredns /usr/bin/coredns && \ - chmod +x /usr/bin/coredns - -# install Invoke -COPY requirements.txt /tmp -RUN echo "*** install Invoke ***" && \ - pip3 install -r /tmp/requirements.txt - -RUN echo "*** cleanup ***" && \ - apt-get clean && \ - rm -rf \ - /tmp/* \ - /var/lib/apt/lists/* \ - /var/tmp/* # add local files COPY root/ / +COPY --from=zerotier /src/zerotier-one /usr/sbin/ +COPY --from=coredns /src/coredns /usr/bin/ + +# add dependencies (this bloats the image quite a bit) +RUN echo "*** install ZeroDNS ***" \ + && apt-get update -y \ + && apt-get install -y ca-certificates python3 python3-pip npm nodejs iputils-ping net-tools \ + && pip3 install -r /app/requirements.txt \ + && npm install -g @laduke/zerotier-central-cli + +RUN mkdir -p /var/lib/zerotier-one \ + && ln -s /usr/sbin/zerotier-one /usr/sbin/zerotier-idtool \ + && ln -s /usr/sbin/zerotier-one /usr/sbin/zerotier-cli + +# cleanup installation artifacts +RUN echo "*** cleanup ***" \ + && apt-get clean \ + && rm -rf \ + /tmp/* \ + /var/lib/apt/lists/* \ + /var/tmp/* + # ports and volumes -EXPOSE 53 53/udp +EXPOSE 5053 5053/udp VOLUME /config \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3c6fbba..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -invoke>=1.4 -jinja2>=2.11 -requests>=2.25 -word2number>=1.1 \ No newline at end of file diff --git a/root/app/Corefile b/root/app/Corefile new file mode 100644 index 0000000..5761358 --- /dev/null +++ b/root/app/Corefile @@ -0,0 +1,10 @@ +.:5053 { + errors + log + hosts /config/hosts { + reload 15m + fallthrough + } + forward . /etc/resolv.conf + reload 15m +} \ No newline at end of file diff --git a/root/app/inv b/root/app/inv deleted file mode 100755 index 527eed4..0000000 --- a/root/app/inv +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/with-contenv bash - -inv -r /app -f /config/config.yml $@ >> /config/ztdns.log 2> /config/ztdns.err.log diff --git a/root/app/requirements.txt b/root/app/requirements.txt new file mode 100644 index 0000000..2ce1caa --- /dev/null +++ b/root/app/requirements.txt @@ -0,0 +1,3 @@ +click>=1.4 +pandas>=1.0 +word2number>=1.1 \ No newline at end of file diff --git a/root/app/tasks/__init__.py b/root/app/tasks/__init__.py deleted file mode 100755 index 778221a..0000000 --- a/root/app/tasks/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from invoke import Collection - -from . import ztdns - -ns = Collection("ztdns") -ns.add_collection(ztdns) \ No newline at end of file diff --git a/root/app/tasks/templates/Corefile.j2 b/root/app/tasks/templates/Corefile.j2 deleted file mode 100755 index e11a105..0000000 --- a/root/app/tasks/templates/Corefile.j2 +++ /dev/null @@ -1,9 +0,0 @@ -. { - forward . {{ dnses }} - reload -} -{{ domains }} { - hosts /config/zt.hosts - log - errors -} \ No newline at end of file diff --git a/root/app/tasks/ztapi.py b/root/app/tasks/ztapi.py deleted file mode 100755 index 1bd01c7..0000000 --- a/root/app/tasks/ztapi.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python -"""Query ZeroTier is a script that accesses the ZeroTier API to determine network peers -of a given network, then updates /etc/hosts to allow for human-like name access. - -(c) Copyright 2020 John Muchovej -""" - -import os -import json - -import requests -from jinja2 import Template -from word2number import w2n - - -def get(postfix: str, token: str): - ZT = "https://my.zerotier.com/api" - headers = { - "Authorization": f"bearer {token}", - } - try: - response = requests.get(f"{ZT}/{postfix}", headers=headers) - response.raise_for_status() - except requests.execptions.HTTPError as e: - raise SystemExit(e) - - return json.loads(response.content.decode("utf-8")) - - -def get_network(lookup: str, token: str): - networks = get("network", token) - - by_nm = next(filter(lambda x: x["config"]["name"] == lookup, networks), None) - by_id = next(filter(lambda x: x["config"]["id"] == lookup, networks), None) - try: - assert by_nm != by_id - except AssertionError: - raise ValueError(f"`{lookup}` was either not present in networks OR `{lookup}` is both the network name and network ID.") - return by_nm or by_id - - -def get_clients(network_name: str, token: str): - network = get_network(network_name, token) - zt_id = network["config"]["id"] - - members = get(f"network/{zt_id}/member", token) - - host = Template("{{ ip }} {{ name }}") - hosts = [] - for member in members: - name = member["name"] - for ip in member["config"]["ipAssignments"]: - hosts.append({ - "ip": ip, - "aliases": abbr(name), - }) - - return hosts - - -def abbr(hostname: str): - split = hostname.split("-") - alias = "" - - for s in split: - try: - alias += str(w2n.word_to_num(s)) - except ValueError: - alias += s[0] - - return [hostname, alias] diff --git a/root/app/tasks/ztdns.py b/root/app/tasks/ztdns.py deleted file mode 100755 index be04457..0000000 --- a/root/app/tasks/ztdns.py +++ /dev/null @@ -1,56 +0,0 @@ -import itertools -from typing import Union - -from invoke import task -from jinja2 import Template, Environment -from jinja2.loaders import FileSystemLoader - -from . import ztapi as zt - - -@task -def get_config(ctx): - ctx.j2env = Environment(loader=FileSystemLoader("/app/tasks/templates")) - - if type(ctx.zt.networks) == str: - ctx.zt.networks = [ctx.zt.networks] - - if type(ctx.zt.tlds) == str: - ctx.zt.tlds = [ctx.zt.tlds] * len(ctx.zt.networks) - - ctx.zt.domains = list(map(".".join, zip(ctx.zt.networks, ctx.zt.tlds))) - if "dnses" not in ctx: - ctx.dnses = ["1.1.1.1", "1.0.0.1", ] - - -@task(pre=[get_config, ]) -def update_coredns(ctx): - corefile = ctx.j2env.get_template("Corefile.j2") - with open("/config/Corefile", "w+") as core: - core.write(corefile.render( - domains=" ".join(ctx.zt.domains), - dnses=" ".join(ctx.dnses) - )) - - -@task(pre=[get_config, ]) -def update_hosts(ctx): - zt_clients = { - k: zt.get_clients(netname, ctx.zt.access_token) - for k, netname in zip(ctx.zt.domains, ctx.zt.networks) - } - - def line(ip: str, aliases: Union[str, list]) -> str: - host_line = Template("{{ ip }} {{ aliases }}") - aliases = [aliases] if type(aliases) != list else aliases - ip = f"{ip:15s}" - return host_line.render(ip=ip, aliases=" ".join(aliases)) + "\n" - - with open("/config/zt.hosts", "w+") as hosts: - hosts.write(line(ip="127.0.0.1", aliases="localhost")) - for domain, clients in zt_clients.items(): - for client in clients: - # TODO support non-domain endings - client["aliases"] = [[f"{a}.{domain}"] for a in client["aliases"]] - client["aliases"] = list(itertools.chain(*client["aliases"])) - hosts.write(line(**client)) diff --git a/root/app/update-networks b/root/app/update-networks new file mode 100755 index 0000000..f9b9241 --- /dev/null +++ b/root/app/update-networks @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +mkdir /var/lib/zerotier-one/networks.d > /dev/null 2> /dev/null +cd /config +find *.conf | xargs -I {} ln -sf /config/{} /var/lib/zerotier-one/networks.d/{} \ No newline at end of file diff --git a/root/app/utils.py b/root/app/utils.py new file mode 100644 index 0000000..c4b0a8f --- /dev/null +++ b/root/app/utils.py @@ -0,0 +1,60 @@ +from typing import Union +import io +import re +import subprocess +import shlex +import functools +from pathlib import Path + +import pandas as pd +from word2number import w2n + + +shell = functools.partial(subprocess.Popen, stdout=subprocess.PIPE) + + +def ztc_get_networks(network: str) -> str: + cmd = shlex.split(f"ztc network:get {network} --columns=name --no-header") + with shell(cmd) as proc: + return proc.stdout.read().decode("utf-8").strip() + + +def ztc_to_csv(network: str, tld: str) -> pd.DataFrame: + cmd = shlex.split(f"ztc member:list --csv {network}") + with shell(cmd) as proc: + members = io.StringIO(proc.stdout.read().decode("utf-8")) + + df = pd.read_csv(members) + df = df[df["Authorized"]] + df = df[["IP-Assignments", "Node-ID", "Name"]] + df["Network"] = network + df["TLD"] = tld + + df = df.rename(columns={"IP-Assignments": "IP", "Node-ID": "Node"}) + return df + + +def add_host(ip: str, aliases: list) -> str: + assert type(aliases) == list + return f"{ip.strip():15s} {' '.join(aliases)}\n" + + +def member_to_host(row) -> list: + aliases = host_abbr(f"{row.Name}.{row.TLD}") + aliases += [row.Name, row.Node, f"{row.Node}.{row.Network}"] + return add_host(row.IP, aliases) + + +def host_abbr(hostname: str): + split = re.split(r" |-", hostname.split(".")[0]) + alias = "" + + for s in split: + try: + alias += str(w2n.word_to_num(s)) + except ValueError: + alias += s[0] + + tld = ".".join(hostname.split(".")[1:]) + return [str(hostname), f"{alias}.{tld}"] + diff --git a/root/app/zerodns b/root/app/zerodns new file mode 100755 index 0000000..285b89a --- /dev/null +++ b/root/app/zerodns @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +from pathlib import Path +from collections import namedtuple + +import click +import pandas as pd + +import utils + + +@click.group() +@click.pass_context +def zerodns(ctx: click.Context) -> None: + network_hexids = Path("/config").glob("*.conf") + network_hexids = list(filter(lambda x: "local" not in x.stem, network_hexids)) + network_hexids = list(map(lambda x: x.stem, network_hexids)) + + network_humans = [utils.ztc_get_networks(network) for network in network_hexids] + + tlds = ["com"] * len(network_humans) + domains = [f"{network}.{tld}" for network, tld in zip(network_humans, tlds)] + + ZeroDNS = namedtuple("ZeroDNS", ("domains", "networks", "readable")) + ctx.obj = ZeroDNS(domains, network_hexids, network_humans) + + +@zerodns.command() +@click.pass_context +def update_hosts(ctx: click.Context) -> None: + hosts = [utils.add_host(ip="127.0.0.1", aliases=["localhost"])] + for net, tld in zip(ctx.obj.networks, ctx.obj.domains): + members = utils.ztc_to_csv(net, tld) + hosts += members.apply(utils.member_to_host, axis=1).tolist() + + with open("/config/hosts", "w") as zthosts: + zthosts.writelines(hosts) + + +if __name__ == "__main__": + zerodns() \ No newline at end of file diff --git a/root/etc/cont-init.d/40-cp-networks b/root/etc/cont-init.d/40-cp-networks new file mode 100755 index 0000000..fa0d54d --- /dev/null +++ b/root/etc/cont-init.d/40-cp-networks @@ -0,0 +1,4 @@ +#!/usr/bin/with-contenv bash + +# Link ZeroTier .conf files to the right location +/app/update-networks \ No newline at end of file diff --git a/root/etc/cont-init.d/50-setup-ztc b/root/etc/cont-init.d/50-setup-ztc new file mode 100755 index 0000000..508ea4a --- /dev/null +++ b/root/etc/cont-init.d/50-setup-ztc @@ -0,0 +1,5 @@ +#!/usr/bin/with-contenv bash + +[ -z ${ACCESS_TOKEN} ] && echo "Please provide an ACCESS_TOKEN" && exit 1 + +ztc conf:set --token "${ACCESS_TOKEN}" >/dev/null 2>/dev/null \ No newline at end of file diff --git a/root/etc/cont-init.d/60-update-hosts b/root/etc/cont-init.d/60-update-hosts index bd6c4a1..6aeec87 100755 --- a/root/etc/cont-init.d/60-update-hosts +++ b/root/etc/cont-init.d/60-update-hosts @@ -1,4 +1,4 @@ #!/usr/bin/with-contenv bash -# Re-run PyInvoke: Update Hosts -/app/inv ztdns.update-hosts \ No newline at end of file +# Re-run ZeroDNS: Update Hosts +/app/zerodns update-hosts \ No newline at end of file diff --git a/root/etc/cont-init.d/70-update-coredns b/root/etc/cont-init.d/70-update-coredns deleted file mode 100755 index 4dfe664..0000000 --- a/root/etc/cont-init.d/70-update-coredns +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/with-contenv bash - -# Re-run PyInvoke: Update CoreDNS -/app/inv ztdns.update-coredns \ No newline at end of file diff --git a/root/etc/crontabs/root b/root/etc/crontabs/root index b63f5bb..bd90053 100755 --- a/root/etc/crontabs/root +++ b/root/etc/crontabs/root @@ -6,5 +6,5 @@ 0 3 * * 6 run-parts /etc/periodic/weekly 0 5 1 * * run-parts /etc/periodic/monthly # renew Hosts and CoreDNS configurations -*/15 * * * * /app/inv ztdns.update-hosts -*/15 * * * * /app/inv ztdns.update-coredns +*/15 * * * * /app/zerodns update-hosts +*/15 * * * * /app/update-networks diff --git a/root/etc/services.d/coredns/run b/root/etc/services.d/coredns/run index e36e18e..e39d8db 100755 --- a/root/etc/services.d/coredns/run +++ b/root/etc/services.d/coredns/run @@ -5,4 +5,4 @@ UMASK_SET=${UMASK_SET:-022} umask "$UMASK_SET" exec \ - s6-setuidgid abc /usr/bin/coredns -conf /config/Corefile \ No newline at end of file + s6-setuidgid abc /usr/bin/coredns -conf /app/Corefile \ No newline at end of file diff --git a/root/etc/services.d/zerotier-one/run b/root/etc/services.d/zerotier-one/run new file mode 100644 index 0000000..481c54a --- /dev/null +++ b/root/etc/services.d/zerotier-one/run @@ -0,0 +1,7 @@ +#!/usr/bin/with-contenv bash + +[ -z ${ACCESS_TOKEN} ] && echo "Please provide an ACCESS_TOKEN" && exit 1 + +ztc conf:set --token "${ACCESS_TOKEN}" >/dev/null 2>/dev/null + +exec /usr/sbin/zerotier-one \ No newline at end of file