├── .dockerignore ├── .gitignore ├── .gitmodules ├── Dockerfile ├── LICENCE ├── Makefile ├── README.rst ├── bin └── .gitkeep ├── doh-wrapper.sh ├── doh.conf ├── gitsubmodules.sh ├── letsencrypt-wrapper.sh ├── pass_to_doh ├── set_log_format.sh └── supervisord.conf /.dockerignore: -------------------------------------------------------------------------------- 1 | rust-doh/target/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | rust-doh/ 2 | bin/doh-proxy 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "rust-doh"] 2 | path = rust-doh 3 | url = https://github.com/jedisct1/rust-doh 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rustlang/rust:nightly as builder 2 | 3 | COPY rust-doh /src/ 4 | WORKDIR /src/ 5 | RUN cargo update \ 6 | && cargo build --release 7 | 8 | FROM debian:stretch 9 | 10 | LABEL version="1.0.0" \ 11 | maintainer="Leonardo Barcaroli " \ 12 | description="A docker image to host one's DNS-over-HTTPS proxy" 13 | 14 | 15 | ENV DEBIAN_FRONTEND=noninteractive 16 | 17 | RUN mkdir -p /etc/doh/ \ 18 | && mkdir -p /var/www/letsencrypt \ 19 | && chown -R www-data:www-data /var/www/letsencrypt \ 20 | && apt-get update \ 21 | && apt-get install -y --no-install-recommends \ 22 | nginx \ 23 | supervisor \ 24 | certbot \ 25 | python3-pip \ 26 | python3-setuptools \ 27 | python3-pkg-resources \ 28 | && pip3 --no-cache-dir install reload \ 29 | && rm -rf /etc/nginx/sites-*/default \ 30 | && rm -rf /var/lib/apt/lists/* 31 | COPY set_log_format.sh /srv/ 32 | COPY letsencrypt-wrapper.sh /srv/ 33 | COPY doh-wrapper.sh /srv/ 34 | COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf 35 | COPY doh.conf /etc/nginx/conf.d/ 36 | COPY pass_to_doh /etc/nginx/conf.d/ 37 | COPY --from=builder /src/target/release/doh-proxy /srv/ 38 | RUN /srv/set_log_format.sh /etc/nginx/nginx.conf 39 | 40 | VOLUME ["/var/www/letsencrypt", "/var/log", "/etc/letsencrypt"] 41 | 42 | EXPOSE "80" 43 | EXPOSE "443" 44 | 45 | ENTRYPOINT ["/usr/bin/supervisord"] 46 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | GLWTS(Good Luck With That Shit) Public License 2 | Copyright (c) Every-fucking-one, except the Author 3 | 4 | The author has absolutely no fucking clue what the code in this project does. 5 | It might just fucking work or not, there is no third option. 6 | 7 | Everyone is permitted to copy, distribute, modify, merge, sell, publish, 8 | sublicense or whatever fuck they want with this software but at their OWN RISK. 9 | 10 | 11 | GOOD LUCK WITH THAT SHIT PUBLIC LICENSE 12 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION, AND MODIFICATION 13 | 14 | 0. You just DO WHATEVER THE FUCK YOU WANT TO as long as you NEVER LEAVE A 15 | FUCKING TRACE TO TRACK THE AUTHOR of the original product to blame for or held 16 | responsible. 17 | 18 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 19 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | Good luck and Godspeed. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOH-PROXY := rust-doh 2 | IMAGE := doh-docker 3 | TAG := 1.1.0 4 | DOH_CONTAINER ?= doh-docker 5 | DOH_DOMAINS ?= localhost 6 | DOH_EMAIL ?= test@email.me 7 | DOH_UPSTREAM_DNS ?= 8.8.8.8:53 8 | DOH_PATH ?= /doh 9 | DOH_EXT_DOM := "" 10 | DOH_DOCKER_OPTS := "" 11 | 12 | $(DOH-PROXY): 13 | if [ ! -d $(DOH-PROXY) ]; then \ 14 | ./gitsubmodules.sh; \ 15 | else \ 16 | git submodule update --init --recursive; \ 17 | fi 18 | 19 | build: 20 | docker build -t $(IMAGE) . 21 | 22 | build-release: 23 | make build 24 | docker tag $(IMAGE) leophys/$(IMAGE):latest 25 | docker tag $(IMAGE) leophys/$(IMAGE):$(TAG) 26 | 27 | run-detached: build 28 | ifeq ($(DOH_DOMAINS),localhost) 29 | @echo "######################################################" 30 | @echo "" 31 | @echo "WARNING! Default value for DOH_DOMAINS: $(DOH_DOMAINS)" 32 | @echo "" 33 | @echo "######################################################" 34 | endif 35 | ifeq ($(DOH_EMAIL),test@email.me) 36 | @echo "######################################################" 37 | @echo "" 38 | @echo "WARNING! Default value for DOH_EMAIL: $(DOH_EMAIL)" 39 | @echo "" 40 | @echo "######################################################" 41 | endif 42 | ifneq ($(DOH_LE_VOL),"") 43 | @- $(foreach DOM,$(DOH_EXT_DOM), \ 44 | $(eval DOH_DOCKER_OPTS += -v "/etc/letsencrypt/live/$(DOM)") \ 45 | $(eval DOH_DOCKER_OPTS += -v "/etc/letsencrypt/archive/$(DOM)") \ 46 | ) 47 | endif 48 | docker run -p 80:80 -p 443:443 \ 49 | -e DOMAINS=$(DOH_DOMAINS) \ 50 | -e EMAIL=$(DOH_EMAIL) \ 51 | -e UPSTREAM_DNS=$(DOH_UPSTREAM_DNS) \ 52 | -e DOH_PATH=$(DOH_PATH) \ 53 | $(DOH_DOCKER_OPTS) \ 54 | --name $(DOH_CONTAINER) -d $(IMAGE) 55 | 56 | logs: 57 | docker logs -f $(DOH_CONTAINER) 58 | 59 | run: 60 | make run-detached 61 | make logs 62 | 63 | start-detached: 64 | docker container start $(DOH_CONTAINER) 65 | 66 | start: 67 | make start-detached 68 | make logs 69 | 70 | stop: 71 | docker container stop $(DOH_CONTAINER) 72 | 73 | clean: stop 74 | docker container rm $(DOH_CONTAINER) 75 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | DOH-Docker 3 | ========== 4 | 5 | A docker image to host a simple instance of one's own DNS-over-HTTPS proxy. 6 | 7 | Why 8 | === 9 | 10 | Because browsers are beginning to migrate_ to DOH (DNS-over-HTTPS). Motivations 11 | for securing the DNS traffic make a lot of sense. But the risk to run towards 12 | a new monopoly is high. This project should encourage people to deploy one's own 13 | resolver. 14 | 15 | Break down 16 | ========== 17 | 18 | This project is a patchwork of others: 19 | 20 | - doh-proxy_: a HTTP-to-DNS proxy written in Rust. 21 | - letsencrypt_: the Mozilla free certification authority (and the related certbot utility). 22 | - nginx_: a very nice HTTP server and reverse proxy. 23 | 24 | All the above is glued together via docker_ and orchestrated by supervisord_. 25 | 26 | What does what 27 | ============== 28 | 29 | ``doh-proxy`` is responsible to receive the HTTP request on a specific path and issue a 30 | corresponding DNS request (and serve the response as HTTP response). 31 | ``letsencrypt`` provides a widely accepted certificate for TLS secure connection and 32 | ``nginx`` proxies the HTTP connection into a secure HTTP2 connection for the clients 33 | to be used. 34 | 35 | What do you need 36 | ================ 37 | 38 | You need three things: 39 | 40 | - A machine with a public ip address, ideally always up, capable 41 | of running docker and with TCP ports 80 and 443 reachable from 42 | the outside and able to DNS-query towards a chosen server. 43 | - A domain that resolves on the said machine. 44 | - An email to be able to obtain a valid TLS certificate from letsencrypt. 45 | 46 | This translate directly in two environment variables to be passed to 47 | the running container: 48 | 49 | - ``EMAIL`` should be a valid email address under your control. 50 | - ``DOMAINS`` should be a space-separated list of domains to which your 51 | machine is reachable (can also be a single name). 52 | 53 | Optionally you may tweak with the upstream dns resolver (i.e. **where all the queries 54 | are sent, in the end - you may want to really change it from the default google 55 | resolver - 8.8.8.8:53**) and the path to which the HTTP server responds: 56 | 57 | - ``UPSTREAM_DNS`` should be a valid IP address plus the UDP port (defaults to 58 | google's ``8.8.8.8:53``). 59 | - ``DOH_PATH`` should be a path, that begins with a ``/``. 60 | 61 | Do you need it 62 | ============== 63 | 64 | No, you don't. You may yet have a VPS with an HTTP server that you may want to use 65 | for the purpose. In this case, I invite you to take a look at doh-proxy_ and to 66 | use it directly. 67 | 68 | How to use it 69 | ============= 70 | 71 | Shallow 72 | ------- 73 | 74 | .. code:: bash 75 | 76 | $ docker run --restart unless-stopped \ 77 | -e DOMAINS="my.domain.org" \ 78 | -e EMAIL="me@myemail.org" \ 79 | --name="my-doh-resolver" \ 80 | leophys/doh-proxy 81 | 82 | Paranoid (preferred way) 83 | ------------------------ 84 | 85 | On the machine you want to run the resolver you'll need: 86 | 87 | - ``GNU make`` 88 | - ``git`` 89 | - ``docker`` (of course) 90 | 91 | Then run: 92 | 93 | .. code:: bash 94 | 95 | $ git clone https://github.com/leophys/doh-docker 96 | $ cd doh-docker 97 | $ make run -e DOH_DOMAINS="my.domain.com myother.domain.com" \ 98 | DOH_EMAIL="me@myemail.org" 99 | 100 | If you don't trust me (you shouldn't), **read the code**. 101 | 102 | Note on the use of host's letsencrypt certificates 103 | -------------------------------------------------- 104 | 105 | This image supports the use of the host's letsencrypt default path. The use case is 106 | when another service on the host needs the same certificates as those inside the 107 | container. There are two possibilities then: 108 | 109 | 1. Another service refreshes the certificates when they expire or 110 | 2. the service inside the container takes care of renewing them 111 | 112 | Both are supported. In the first case just mount the relevant paths onto the container: 113 | 114 | .. code:: bash 115 | 116 | $ docker run --restart unless-stopped \ 117 | -e DOMAINS="my.domain.org" \ 118 | -e EMAIL="me@myemail.org" \ 119 | -v /etc/letsencrypt/live/my.domain.tld:/etc/letsencrypt/live/my.domain.tld \ 120 | -v /etc/letsencrypt/archive/my.domain.tld:/etc/letsencrypt/archive/my.domain.tld \ 121 | --name="my-doh-resolver" \ 122 | leophys/doh-proxy 123 | 124 | The relevant script with skip that domain (letsencrypt-wrapper.sh_). 125 | 126 | If the certificates has to be renewed by an hosts service, just do the same, but take 127 | care of touching a file named ``.doh-force`` in the ``live`` path: 128 | 129 | .. code:: bash 130 | 131 | $ touch /etc/letsencrypt/live/my.domain.tld/.doh-force 132 | $ docker run --restart unless-stopped \ 133 | -e DOMAINS="my.domain.org" \ 134 | -e EMAIL="me@myemail.org" \ 135 | -v /etc/letsencrypt/live/my.domain.tld:/etc/letsencrypt/live/my.domain.tld \ 136 | -v /etc/letsencrypt/archive/my.domain.tld:/etc/letsencrypt/archive/my.domain.tld \ 137 | --name="my-doh-resolver" \ 138 | leophys/doh-proxy 139 | 140 | Licence 141 | ======= 142 | 143 | See LICENCE_. 144 | 145 | 146 | .. _migrate: https://blog.usejournal.com/getting-started-with-dns-over-https-on-firefox-e9b5fc865a43 147 | .. _doh-proxy: https://github.com/jedisct1/rust-doh 148 | .. _letsencrypt: https://letsencrypt.org/ 149 | .. _nginx: https://www.nginx.com/ 150 | .. _docker: https://www.nginx.com/ 151 | .. _supervisord: http://supervisord.org/ 152 | .. _LICENCE: https://github.com/leophys/doh-docker/blob/master/LICENCE 153 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leophys/doh-docker/4643a4191221887f3761ae5d4328c510fcf58caf/bin/.gitkeep -------------------------------------------------------------------------------- /doh-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ $DEBUG ]]; then 4 | set -x 5 | fi 6 | 7 | logger() { 8 | echo "[doh] :: $(date +%x-%X) :: $@" | tee -a /var/log/doh.log 9 | } 10 | 11 | main() { 12 | U=${UPSTREAM_DNS:-"8.8.8.8:53"} 13 | P=${DOH_PATH:-"/doh"} 14 | logger "Starting doh-proxy with upstream dns $U, dns path $P" 15 | /srv/doh-proxy -l 127.0.0.1:11443 -u $U -p $P 16 | logger "Stopping doh-proxy" 17 | } 18 | 19 | main 20 | -------------------------------------------------------------------------------- /doh.conf: -------------------------------------------------------------------------------- 1 | server{ 2 | listen 80; 3 | server_name _; 4 | 5 | location ^~ /.well-known/acme-challenge/ { 6 | default_type "text/plain"; 7 | root /var/www/letsencrypt; 8 | allow all; 9 | } 10 | 11 | location = /.well-known/acme-challenge/ { 12 | return 404; 13 | 14 | } 15 | 16 | location / { 17 | return 301 https://$host$request_uri; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gitsubmodules.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Taken from: https://gist.github.com/gilbarbara/f644cc76f70127d216b5 4 | 5 | set -e 6 | 7 | git config -f .gitmodules --get-regexp '^submodule\..*\.path$' | 8 | while read path_key path 9 | do 10 | url_key=$(echo $path_key | sed 's/\.path/.url/') 11 | url=$(git config -f .gitmodules --get "$url_key") 12 | git submodule add $url $path 13 | done 14 | -------------------------------------------------------------------------------- /letsencrypt-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ $DEBUG ]]; then 4 | set -x 5 | fi 6 | 7 | logger() { 8 | echo "[letsencrypt] :: $(date +%x-%X) :: $@" | tee -a /var/log/letsencrypt-wrapper.log 9 | } 10 | 11 | edit_nginx_conf() { 12 | logger "Adding configuration to nginx for domain ${1}" 13 | cat > /etc/nginx/conf.d/${1}.conf << EOC 14 | 15 | # ${1} ##### 16 | server{ 17 | listen 443 ssl http2 default_server; 18 | server_name ${1}; 19 | 20 | access_log /dev/stdout; 21 | error_log /dev/stderr; 22 | 23 | ssl_certificate /etc/letsencrypt/live/${1}/fullchain.pem; 24 | ssl_certificate_key /etc/letsencrypt/live/${1}/privkey.pem; 25 | 26 | include conf.d/pass_to_doh; 27 | } 28 | ##################################################### 29 | EOC 30 | 31 | } 32 | 33 | get_first_time() { 34 | local d=$1 35 | if certbot certonly --agree-tos --email $EMAIL \ 36 | -n --webroot -w /var/www/letsencrypt -d $d 37 | then 38 | logger "First certificate got for $d" 39 | edit_nginx_conf $d 40 | else 41 | logger "ERROR on first time certificate for $d" 42 | exit 2 43 | fi 44 | } 45 | 46 | renew_certs() { 47 | certbot renew -n --webroot -w /var/www/letsencrypt -d "${1}" 48 | } 49 | 50 | check_env() { 51 | if ! [[ $DOMAINS ]]; then 52 | logger "ERROR! Missing domains to be used! Set DOMAINS environment variable." 53 | exit 1 54 | fi 55 | if ! [[ $EMAIL ]]; then 56 | logger "ERROR! Missing email address to use to register the domain(s) certificates." 57 | fi 58 | } 59 | 60 | should_force() { 61 | if ! [[ -f "/etc/letsencrypt/live/${1}/.doh-force" ]]; then 62 | return 0 63 | fi 64 | logger "Not skipping - found file /etc/letsencrypt/live/${1}/.doh-force" 65 | return 1 66 | } 67 | 68 | le_vol_mounted() { 69 | if grep "/etc/letsencrypt/live/${1}" <(mount) > /dev/null; then 70 | logger "A letsencrypt volume is mounted for domain ${1}" 71 | should_force $1 && return 0 72 | elif grep "/etc/letsencrypt" <(mount) > /dev/null; then 73 | logger "The whole /etc/letsencrypt is mounted" 74 | if [[ -d "/etc/letsencrypt/live/${1}" ]]; then 75 | should_force $1 && return 0 76 | fi 77 | fi 78 | return 1 79 | } 80 | 81 | main() { 82 | check_env 83 | for d in $DOMAINS; do 84 | if le_vol_mounted ${d}; then 85 | # Assuming that if the volume is mounted from the host 86 | # creation and renewal is in charge of others, 87 | # except if the .doh-force file il present in the 88 | # directory of the certificates. In such case, the container 89 | # will take care of renewing them. 90 | edit_nginx_conf $d 91 | logger "Skipping domain ${1}" 92 | continue 93 | fi 94 | if [[ -f /var/www/letsencrypt/live/$d/privkey.pem ]]; then 95 | logger "Renewing certificate for $d" 96 | renew_certs $d 97 | else 98 | logger "Getting first time certificates for $d" 99 | get_first_time $d 100 | fi 101 | done 102 | logger "All done, sleeping for ${WAIT_TIME:-"1d"}." 103 | sleep ${WAIT_TIME:-"1d"} 104 | } 105 | 106 | while true 107 | do 108 | main 109 | done 110 | -------------------------------------------------------------------------------- /pass_to_doh: -------------------------------------------------------------------------------- 1 | location / { 2 | proxy_pass http://127.0.0.1:11443; 3 | proxy_set_header Host $host; 4 | proxy_set_header X-Real-IP $remote_addr; 5 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 6 | } 7 | -------------------------------------------------------------------------------- /set_log_format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | LOG_FORMAT="\'[NGINX] \$remote_addr - \$remote_user [\$time_local] \ 4 | \"\$request\" \$status \$bytes_sent \"\$http_referer\" \ 5 | \"\$http_user_agent\" \"\$gzip_ratio\";\'" 6 | 7 | 8 | if grep --quiet log_format $@ 9 | then 10 | sed -i "s|\(.*\)log_format.*$|\1log_format $LOG_FORMAT|" $@ 11 | else 12 | sed -i "s|^\(.*\)\(access_log.*$\)|\1\2\n\1log_format $LOG_FORMAT|" $@ 13 | fi 14 | -------------------------------------------------------------------------------- /supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:letsencrypt] 5 | command=/srv/letsencrypt-wrapper.sh 6 | stdout_logfile=/dev/fd/1 7 | stdout_logfile_maxbytes=0 8 | stderr_logfile=/dev/fd/2 9 | stderr_logfile_maxbytes=0 10 | username=www-data 11 | autorestart=true 12 | 13 | [program:doh-proxy] 14 | command=/srv/doh-wrapper.sh 15 | stdout_logfile=/dev/fd/1 16 | stdout_logfile_maxbytes=0 17 | stderr_logfile=/dev/fd/2 18 | stderr_logfile_maxbytes=0 19 | username=www-data 20 | autorestart=true 21 | 22 | [program:nginx] 23 | command=/usr/local/bin/reload /usr/sbin/nginx -g "daemon off;" 24 | directory=/etc/nginx/conf.d 25 | ; priority=900 26 | stdout_logfile=/dev/fd/1 27 | stdout_logfile_maxbytes=0 28 | stderr_logfile=/dev/fd/2 29 | stderr_logfile_maxbytes=0 30 | username=www-data 31 | autorestart=true 32 | --------------------------------------------------------------------------------