diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d2cce87 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,153 @@ +FROM caleblloyd/phusion-baseimage-docker-15.04 +MAINTAINER Frank Denis +ENV SERIAL 1 + +ENV BUILD_DEPS autoconf gcc libc-dev make pkg-config git + +RUN set -x && \ + apt-get update && apt-get install -y \ + $BUILD_DEPS \ + bsdmainutils \ + ldnsutils \ + --no-install-recommends + +ENV LIBRESSL_VERSION 2.2.0 +ENV LIBRESSL_SHA256 9690d8f38a5d48425395452eeb305b05bb0f560cd96e0ee30f370d4f16563040 +ENV LIBRESSL_DOWNLOAD_URL http://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-${LIBRESSL_VERSION}.tar.gz + +RUN set -x && \ + mkdir -p /tmp/src && \ + cd /tmp/src && \ + curl -sSL $LIBRESSL_DOWNLOAD_URL -o libressl.tar.gz && \ + echo "${LIBRESSL_SHA256} *libressl.tar.gz" | sha256sum -c - && \ + tar xzf libressl.tar.gz && \ + rm -f libressl.tar.gz && \ + cd libressl-${LIBRESSL_VERSION} && \ + ./configure --disable-dependency-tracking --prefix=/opt/libressl && \ + make check && make install && \ + rm -fr /opt/libressl/share/man && \ + echo /opt/libressl/lib > /etc/ld.so.conf.d/libressl.conf && ldconfig && \ + rm -fr /tmp/* + +ENV UNBOUND_VERSION 1.5.3 +ENV UNBOUND_SHA256 76bdc875ed4d1d3f8e4cfe960e6df78ee5c6c7c18abac11331cf93a7ae129eca +ENV UNBOUND_DOWNLOAD_URL http://www.unbound.net/downloads/unbound-${UNBOUND_VERSION}.tar.gz + +RUN set -x && \ + apt-get update && \ + apt-get install -y \ + libevent-2.0 \ + libevent-dev \ + libexpat1 \ + libexpat1-dev \ + --no-install-recommends && \ + mkdir -p /tmp/src && \ + cd /tmp/src && \ + curl -sSL $UNBOUND_DOWNLOAD_URL -o unbound.tar.gz && \ + echo "${UNBOUND_SHA256} *unbound.tar.gz" | sha256sum -c - && \ + tar xzf unbound.tar.gz && \ + rm -f unbound.tar.gz && \ + cd unbound-${UNBOUND_VERSION} && \ + groupadd _unbound && \ + useradd -g _unbound -s /etc -d /dev/null _unbound && \ + ./configure --disable-dependency-tracking --prefix=/opt/unbound --with-pthreads \ + --with-username=_unbound --with-ssl=/opt/libressl --with-libevent \ + --enable-event-api && \ + make install && \ + mv /opt/unbound/etc/unbound/unbound.conf /opt/unbound/etc/unbound/unbound.conf.example && \ + rm -fr /opt/unbound/share/man && \ + apt-get purge -y --auto-remove \ + libexpat-dev \ + libevent-dev && \ + apt-get autoremove -y && apt-get clean && \ + rm -fr /tmp/* /var/tmp/* + +ENV LIBSODIUM_VERSION 1.0.3 +ENV LIBSODIUM_SHA256 cbcfc63cc90c05d18a20f229a62c7e7054a73731d0aa858c0517152c549b1288 +ENV LIBSODIUM_DOWNLOAD_URL https://download.libsodium.org/libsodium/releases/libsodium-${LIBSODIUM_VERSION}.tar.gz + +RUN set -x && \ + mkdir -p /tmp/src && \ + cd /tmp/src && \ + curl -sSL $LIBSODIUM_DOWNLOAD_URL -o libsodium.tar.gz && \ + echo "${LIBSODIUM_SHA256} *libsodium.tar.gz" | sha256sum -c - && \ + tar xzf libsodium.tar.gz && \ + rm -f libsodium.tar.gz && \ + cd libsodium-${LIBSODIUM_VERSION} && \ + ./configure --disable-dependency-tracking --enable-minimal --prefix=/opt/libsodium && \ + make check && make install && \ + echo /opt/libsodium/lib > /etc/ld.so.conf.d/libsodium.conf && ldconfig && \ + rm -fr /tmp/* + +ENV DNSCRYPT_PROXY_VERSION 1.5.0 +ENV DNSCRYPT_PROXY_SHA256 b78c52efca2e6b26c68638493c1aa7733b41106363a8f2a3bec3b8da44aafcbb +ENV DNSCRYPT_PROXY_DOWNLOAD_URL http://download.dnscrypt.org/dnscrypt-proxy/dnscrypt-proxy-${DNSCRYPT_PROXY_VERSION}.tar.gz + +RUN set -x && \ + mkdir -p /tmp/src && \ + cd /tmp/src && \ + curl -sSL $DNSCRYPT_PROXY_DOWNLOAD_URL -o dnscrypt-proxy.tar.gz && \ + echo "${DNSCRYPT_PROXY_SHA256} *dnscrypt-proxy.tar.gz" | sha256sum -c - && \ + tar xvf dnscrypt-proxy.tar.gz && \ + rm -f dnscrypt-proxy.tar.gz && \ + cd dnscrypt-proxy-${DNSCRYPT_PROXY_VERSION} && \ + mkdir -p /opt/dnscrypt-proxy/empty && \ + groupadd _dnscrypt-proxy && \ + useradd -g _dnscrypt-proxy -s /etc -d /opt/dnscrypt-proxy/empty _dnscrypt-proxy && \ + env CPPFLAGS=-I/opt/libsodium/include LDFLAGS=-L/opt/libsodium/lib \ + ./configure --disable-dependency-tracking --prefix=/opt/dnscrypt-proxy && \ + make install && \ + rm -fr /opt/dnscrypt-proxy/share && \ + rm -fr /tmp/* /var/tmp/* + +ENV DNSCRYPT_WRAPPER_GIT_TAG stable +ENV DNSCRYPT_WRAPPER_GIT_REMOTE_URL https://github.com/jedisct1/dnscrypt-wrapper.git + +RUN set -x && \ + apt-get update && \ + apt-get install -y \ + libevent-2.0 \ + libevent-dev \ + --no-install-recommends && \ + mkdir -p /tmp/src && \ + cd /tmp/src && \ + git clone "$DNSCRYPT_WRAPPER_GIT_REMOTE_URL" dnscrypt-wrapper && \ + cd dnscrypt-wrapper && \ + git checkout "$DNSCRYPT_WRAPPER_GIT_TAG" && \ + mkdir -p /opt/dnscrypt-wrapper/empty && \ + groupadd _dnscrypt-wrapper && \ + useradd -g _dnscrypt-wrapper -s /etc -d /opt/dnscrypt-wrapper/empty _dnscrypt-wrapper && \ + groupadd _dnscrypt-signer && \ + useradd -g _dnscrypt-signer -G _dnscrypt-wrapper -s /etc -d /dev/null _dnscrypt-signer && \ + make configure && \ + ./configure --prefix=/opt/dnscrypt-wrapper --with-sodium=/opt/libsodium && \ + make install && \ + apt-get purge -y --auto-remove libevent-dev && \ + apt-get autoremove -y && apt-get clean && \ + rm -fr /tmp/* /var/tmp/* + +RUN set -x && \ + apt-get purge -y --auto-remove $BUILD_DEPS && \ + apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN mkdir -p \ + /etc/service/unbound \ + /etc/service/watchdog + +ADD entrypoint.sh / + +ADD unbound.sh /etc/service/unbound/run +ADD unbound-check.sh /etc/service/unbound/check + +ADD dnscrypt-wrapper.sh /etc/service/dnscrypt-wrapper/run + +ADD key-rotation.sh /etc/service/key-rotation/run +ADD watchdog.sh /etc/service/watchdog/run + +VOLUME ["/opt/dnscrypt-wrapper/etc/keys"] + +EXPOSE 53/udp 53/tcp 443/udp 443/tcp + +CMD ["start"] + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..471f457 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +DNSCrypt server Docker image +============================ + +Run your own caching, non-censoring, non-logging, DNSSEC-capable, +[DNSCrypt](http://dnscrypt.org)-enabled DNS resolver virtually anywhere! + +If you are already familiar with Docker, it shouldn't take more than 5 minutes +to get your resolver up and running. + +Installation +============ + +Download the Docker image source: + + $ git clone https://github.com/jedisct1/dnscrypt-server-docker.git + +Build the image: + + $ docker build -t dnscrypt-server-image . + +Think about a name. This is going to be part of your DNSCrypt provider name. +If you are planning to make your resolver publicly accessible, this name will +be public. +It has to look like a domain name (`example.com`), but it doesn't have to be +a registered domain either. + +Let's pick `example.com` here. + +Create and initialize the container, once and for all: + + $ docker run --name=dnscrypt-server \ + -p 443:443/udp -p 443:443/tcp \ + dnscrypt-server-image init -N example.com + +This will only accept connections via DNSCrypt. Containers on the same virtual +network can directly access the DNS cache on the standard DNS port (53), but +to create a non-authenticated public DNS resolver, this extra port has to be +explicitly exposed (`-p 53:53/udp -p 53:53/tcp`). + +Now, to start the whole stack: + + $ docker start dnscrypt-server + +Done. + +To check that your DNSCrypt-enabled DNS resolver is accessible, run the +DNSCrypt client proxy on another host: + + # dnscrypt-proxy \ + --provider-key= \ + --resolver-address= \ + --provider-name=2.dnscrypt-cert.example.com + +And try using `127.0.0.1` as a DNS resolver. + +Note that the actual provider name for DNSCrypt is `2.dnscrypt-cert.example.com`, +not just `example.com` as initially entered. The full name has to start with +`2.dnscrypt-cert.` for the client and the server to use the same version of the +protocol. + +Let the world know about your server +==================================== + +Is your brand new DNS resolver publicly accessible? + +Fork the [dnscrypt-proxy repository](https://github.com/jedisct1/dnscrypt-proxy), +edit the [dnscrypt.csv](https://github.com/jedisct1/dnscrypt-proxy/blob/master/dnscrypt-resolvers.csv) +file to add your resolver's informations, and submit a pull request to have it +included in the list of public DNSCrypt resolvers! + +Details +======= + +- Caching resolver: [Unbound](https://www.unbound.net/), with DNSSEC, prefetching, +and no logs. The number of threads and memory usage are automatically adjusted. +Latest stable version, compiled from source. +- [LibreSSL](http://www.libressl.org/) - Latest stable version, compiled from source. +- [libsodium](https://download.libsodium.org/doc/) - Latest stable version, +minimal build compiled from source. +- [dnscrypt-wrapper](https://github.com/Cofyc/dnscrypt-wrapper) - Latest stable version, +compiled from source. +- [dnscrpt-proxy](https://github.com/jedisct1/dnscrypt-proxy) - Latest stable version, +compiled from source. diff --git a/dnscrypt-wrapper.sh b/dnscrypt-wrapper.sh new file mode 100755 index 0000000..ddc03e5 --- /dev/null +++ b/dnscrypt-wrapper.sh @@ -0,0 +1,53 @@ +#! /bin/sh + +KEYS_DIR="/opt/dnscrypt-wrapper/etc/keys" +STKEYS_DIR="${KEYS_DIR}/short-term" + +prune() { + find "$STKEYS_DIR" -type f -cmin +86400 -exec rm -f {} \; +} + +rotation_needed() { + if [ $(find "$STKEYS_DIR" -type f -cmin -43200 -print -quit | wc -l | sed 's/[^0-9]//g') -le 0 ]; then + echo true + else + echo false + fi +} + +new_key() { + ts=$(date '+%s') + /opt/dnscrypt-wrapper/sbin/dnscrypt-wrapper --gen-crypt-keypair \ + --crypt-secretkey-file="${STKEYS_DIR}/${ts}.key" && + /opt/dnscrypt-wrapper/sbin/dnscrypt-wrapper --gen-cert-file \ + --provider-publickey-file="${KEYS_DIR}/public.key" \ + --provider-secretkey-file="${KEYS_DIR}/secret.key" \ + --crypt-secretkey-file="${STKEYS_DIR}/${ts}.key" \ + --provider-cert-file="${STKEYS_DIR}/${ts}.cert" && \ + mv -f "${STKEYS_DIR}/${ts}.cert" "${STKEYS_DIR}/dnscrypt.cert" +} + +stkeys_files() { + res="" + for file in $(ls "$STKEYS_DIR"/[0-9]*.key); do + res="${res}${file}," + done + echo "$res" +} + +if [ ! -f "$KEYS_DIR/provider_name" ]; then + exit 1 +fi +provider_name=$(cat "$KEYS_DIR/provider_name") + +mkdir -p "$STKEYS_DIR" +prune +[ $(rotation_needed) = true ] && new_key + +exec /opt/dnscrypt-wrapper/sbin/dnscrypt-wrapper \ + --user=_dnscrypt-wrapper \ + --listen-address=0.0.0.0:443 \ + --resolver-address=127.0.0.1 \ + --provider-name="$provider_name" \ + --provider-cert-file="${STKEYS_DIR}/dnscrypt.cert" \ + --crypt-secretkey-file=$(stkeys_files) diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..dd45a8b --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,105 @@ +#! /bin/sh + +set -e + +action="$1" + +KEYS_DIR="/opt/dnscrypt-wrapper/etc/keys" + +# -N provider-name + +init() { + if [ $(is_initialized) = yes ]; then + start + exit $? + fi + while getopts "h?N:" opt; do + case "$opt" in + h|\?) usage ;; + N) provider_name=$(echo "$OPTARG" | sed -e 's/^[ \t]*//' | tr A-Z a-z) ;; + esac + done + [ -z "$provider_name" ] && usage + case "$provider_name" in + .*) usage ;; + 2.dnscrypt-cert.*) ;; + *) provider_name="2.dnscrypt-cert.${provider_name}" + esac + echo "Provider name: [$provider_name]" + cd "$KEYS_DIR" + /opt/dnscrypt-wrapper/sbin/dnscrypt-wrapper --gen-provider-keypair | \ + tee "${KEYS_DIR}/provider-info.txt" + chmod 640 "${KEYS_DIR}/secret.key" + chmod 644 "${KEYS_DIR}/public.key" + chown root:_dnscrypt-signer "${KEYS_DIR}/public.key" "${KEYS_DIR}/secret.key" + echo "$provider_name" > "${KEYS_DIR}/provider_name" + chmod 644 "${KEYS_DIR}/provider_name" + hexdump -ve '1/1 "%.2x"' < "${KEYS_DIR}/public.key" > "${KEYS_DIR}/public.key.txt" + chmod 644 "${KEYS_DIR}/public.key.txt" + echo + echo ----------------------------------------------------------------------- + echo + echo "Congratulations! The container has been properly initialized." + echo "Take a look up above at the way dnscrypt-proxy has to be configured in order" + echo "to connect to your resolver. Then, start the container with the default command." +} + +provider_info() { + ensure_initialized + echo "Provider name:" + cat "${KEYS_DIR}/provider_name" + echo + echo "Provider public key:" + cat "${KEYS_DIR}/public.key.txt" + echo +} + +is_initialized() { + if [ ! -f "${KEYS_DIR}/public.key" -a ! -f "${KEYS_DIR}/secret.key" -a ! -f "${KEYS_DIR}/provider_name" ]; then + echo no + else + echo yes + fi +} + +ensure_initialized() { + if [ $(is_initialized) = no ]; then + echo "Please provide an initial configuration (init -N )" >&2 + exit 1 + fi +} + +start() { + ensure_initialized + echo "Starting DNSCrypt service for provider: " + cat "${KEYS_DIR}/provider_name" + exec /sbin/my_init +} + +usage() { + cat << EOT +Commands +======== + +* init -N : initialize the container for a new provider named +This is supposed to be called only once. + +* start (default command): start the resolver and the dnscrypt server proxy. +Ports 443/udp and 443/tcp have to be publicly exposed. +Containers on the same virtual network can directly use this container's Unbound +instance as a DNS resolver, on the standard DNS port (53). + +* provider-info: prints the provide name and provider public key. + +This container has a single volume that you might want to securely keep a +backup of: /opt/dnscrypt-wrapper/etc/keys +EOT + exit 1 +} + +case "$action" in + start) start ;; + init) shift ; init $* ;; + provider-info) provider_info ;; + *) usage ;; +esac diff --git a/key-rotation.sh b/key-rotation.sh new file mode 100755 index 0000000..8fa7aad --- /dev/null +++ b/key-rotation.sh @@ -0,0 +1,18 @@ +#! /bin/sh + +sleep 1800 + +KEYS_DIR="/opt/dnscrypt-wrapper/etc/keys" +STKEYS_DIR="${KEYS_DIR}/short-term" + +rotation_needed() { + if [ $(find "$STKEYS_DIR" -type f -cmin -43200 -print -quit | wc -l | sed 's/[^0-9]//g') -le 0 ]; then + echo true + else + echo false + fi +} + +[ $(rotation_needed) = true ] || exit 0 +sv status dnscrypt-wrapper | egrep -q '^run:' || exit 0 +sv restart dnscrypt-wrapper diff --git a/unbound-check.sh b/unbound-check.sh new file mode 100755 index 0000000..b1571ec --- /dev/null +++ b/unbound-check.sh @@ -0,0 +1,4 @@ +#! /bin/sh + +drill -DQ NS . @127.0.0.1 && +drill -tDQ NS . @127.0.0.1 diff --git a/unbound.sh b/unbound.sh new file mode 100755 index 0000000..b377e72 --- /dev/null +++ b/unbound.sh @@ -0,0 +1,77 @@ +#! /bin/sh + +reserved=8388608 +availableMemory=$((1024 * $(fgrep MemAvailable /proc/meminfo | sed 's/[^0-9]//g') - $reserved)) +if [ $availableMemory -le 0 ]; then + exit 1 +fi +msg_cache_size=$(($availableMemory / 3)) +rr_cache_size=$(($availableMemory / 3)) +nproc=$(nproc) +if [ $nproc -gt 1 ]; then + threads=$(($nproc - 1)) +else + threads=1 +fi + +sed \ + -e "s/@MSG_CACHE_SIZE@/${msg_cache_size}/" \ + -e "s/@RR_CACHE_SIZE@/${rr_cache_size}/" \ + -e "s/@THREADS@/${threads}/" \ + > /opt/unbound/etc/unbound/unbound.conf << EOT +server: + verbosity: 1 + num-threads: @THREADS@ + interface: 0.0.0.0@53 + so-reuseport: yes + edns-buffer-size: 1252 + delay-close: 10000 + cache-min-ttl: 60 + cache-max-ttl: 86400 + do-daemonize: no + username: "_unbound" + log-queries: no + hide-version: yes + identity: "DNSCrypt" + harden-short-bufsize: yes + harden-large-queries: yes + harden-glue: yes + harden-dnssec-stripped: yes + harden-below-nxdomain: yes + harden-referral-path: no + do-not-query-localhost: no + prefetch: yes + prefetch-key: yes + rrset-roundrobin: yes + minimal-responses: yes + chroot: "/opt/unbound/etc/unbound" + directory: "/opt/unbound/etc/unbound" + auto-trust-anchor-file: "var/root.key" + num-queries-per-thread: 4096 + outgoing-range: 8192 + msg-cache-size: @MSG_CACHE_SIZE@ + rrset-cache-size: @RR_CACHE_SIZE@ + access-control: 0.0.0.0/0 allow + access-control: ::0/0 allow + + local-zone: "belkin." static + local-zone: "corp." static + local-zone: "domain." static + local-zone: "example." static + local-zone: "home." static + local-zone: "host." static + local-zone: "invalid." static + local-zone: "lan." static + local-zone: "local." static + local-zone: "localdomain." static + local-zone: "test." static +EOT + +mkdir -p /opt/unbound/etc/unbound/dev && \ +cp -a /dev/random /dev/urandom /opt/unbound/etc/unbound/dev/ + +mkdir -p -m 700 /opt/unbound/etc/unbound/var && \ +chown _unbound:_unbound /opt/unbound/etc/unbound/var && \ +/opt/unbound/sbin/unbound-anchor -a /opt/unbound/etc/unbound/var/root.key + +exec /opt/unbound/sbin/unbound diff --git a/watchdog.sh b/watchdog.sh new file mode 100755 index 0000000..b7d23e5 --- /dev/null +++ b/watchdog.sh @@ -0,0 +1,22 @@ +#! /bin/sh + +sleep 600 + +for service in unbound dnscrypt-wrapper; do + sv check "$service" || sv force-restart "$service" +done + +KEYS_DIR="/opt/dnscrypt-wrapper/etc/keys" +GRACE_PERIOD=6 + +provider_key=$(cat "${KEYS_DIR}/public.key.txt") +provider_name=$(cat "${KEYS_DIR}/provider_name") + +(/opt/dnscrypt-proxy/sbin/dnscrypt-proxy \ + --user=_dnscrypt-proxy \ + --provider-key="$provider_key" \ + --provider-name="$provider_name" \ + --resolver-address=127.0.0.1:443 \ + --test="$GRACE_PERIOD" && \ +drill -p 443 -Q TXT "$provider_name" @127.0.0.1) || \ +sv force-restart dnscrypt-wrapper