├── .gitignore ├── examples └── docker-compose-doh-server │ ├── data │ └── .gitkeep │ ├── .gitignore │ ├── env.sample.conf │ ├── remove.sh │ ├── unbound.sample.conf │ ├── launch.sh │ └── docker-compose.yml ├── helm ├── doh-server-2.2.2.tgz ├── doh-server │ ├── Chart.yaml │ ├── .helmignore │ ├── templates │ │ ├── tests │ │ │ └── test-connection.yaml │ │ ├── service.yaml │ │ ├── _helpers.tpl │ │ ├── NOTES.txt │ │ ├── ingress.yaml │ │ └── deployment.yaml │ └── values.yaml ├── CONTRIBUTING.md └── README.md ├── tests ├── Pipfile ├── test-doh-server.py └── Pipfile.lock ├── app-config-example ├── .github └── workflows │ └── on-push.yml ├── Dockerfile.alpine ├── docker-entrypoint ├── Makefile ├── Dockerfile.ubuntu ├── doh-server.sample.conf ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | data/ 2 | -------------------------------------------------------------------------------- /examples/docker-compose-doh-server/data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/docker-compose-doh-server/.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | .local* 3 | env.conf 4 | -------------------------------------------------------------------------------- /helm/doh-server-2.2.2.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satishweb/docker-doh/HEAD/helm/doh-server-2.2.2.tgz -------------------------------------------------------------------------------- /helm/doh-server/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for Kubernetes 4 | name: doh-server 5 | version: 0.1.0 6 | -------------------------------------------------------------------------------- /helm/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Helm Chart for Harbor 2 | 3 | ## Contributers 4 | 5 | Thanks very much to all contributers who submitted pull requests to Helm Chart for docker-doh. 6 | 7 | -------------------------------------------------------------------------------- /tests/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | docker = "*" 8 | requests = "*" 9 | 10 | [dev-packages] 11 | 12 | [requires] 13 | python_version = "3.12" 14 | -------------------------------------------------------------------------------- /app-config-example: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Author: Satish Gaikwad 3 | # FILE NAME: app-config 4 | 5 | set -e 6 | 7 | ## Customizations 8 | # Here you can manipulate your configuration files before doh service is started 9 | # You can pretty much do anything here 10 | # Please read /docker-entrypoint script before writing any code here 11 | 12 | # Launch app 13 | echo "| ENTRYPOINT: Lauching app..." 14 | exec $@ 15 | -------------------------------------------------------------------------------- /helm/doh-server/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /helm/doh-server/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "doh-server.fullname" . }}-test-connection" 5 | labels: 6 | app.kubernetes.io/name: {{ include "doh-server.name" . }} 7 | helm.sh/chart: {{ include "doh-server.chart" . }} 8 | app.kubernetes.io/instance: {{ .Release.Name }} 9 | app.kubernetes.io/managed-by: {{ .Release.Service }} 10 | annotations: 11 | "helm.sh/hook": test-success 12 | spec: 13 | containers: 14 | - name: wget 15 | image: busybox 16 | command: ['wget'] 17 | args: ['{{ include "doh-server.fullname" . }}:{{ .Values.service.port }}'] 18 | restartPolicy: Never 19 | -------------------------------------------------------------------------------- /helm/doh-server/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "doh-server.fullname" . }} 5 | labels: 6 | app.kubernetes.io/name: {{ include "doh-server.name" . }} 7 | helm.sh/chart: {{ include "doh-server.chart" . }} 8 | app.kubernetes.io/instance: {{ .Release.Name }} 9 | app.kubernetes.io/managed-by: {{ .Release.Service }} 10 | {{- if .Values.service.annotations }} 11 | annotations: 12 | {{ toYaml .Values.service.annotations | indent 4 }} 13 | {{- end }} 14 | spec: 15 | type: {{ .Values.service.type }} 16 | ports: 17 | - port: {{ .Values.service.port }} 18 | targetPort: http 19 | protocol: TCP 20 | name: http 21 | selector: 22 | app.kubernetes.io/name: {{ include "doh-server.name" . }} 23 | app.kubernetes.io/instance: {{ .Release.Name }} 24 | -------------------------------------------------------------------------------- /examples/docker-compose-doh-server/env.sample.conf: -------------------------------------------------------------------------------- 1 | # Docker Compose Settings 2 | # Stack name is used as prefix for docker resource names. 3 | STACK=dns 4 | 5 | # DOH Server settings 6 | DOH_HTTP_PREFIX=/getnsrecord 7 | DOH_SERVER_LISTEN=8053 8 | 9 | # Lets Encrypt Settings 10 | EMAIL=user@example.com 11 | # SSL certificate is generated for base domain and subdomain.domain 12 | DOMAIN=example.com 13 | SUBDOMAIN=dns 14 | 15 | # DNS Challenge Route53 Credentials for Proxy + Letsencrypt 16 | # You may use various DNS providers from the list. 17 | # Please check proxy configuration in docker-compose.yml 18 | AWS_ACCESS_KEY_ID=AKIKJGKKH0JHFMKHFKGAFVA 19 | AWS_SECRET_ACCESS_KEY=Nx3yKjujG8kjjgJhfhjUFBS1k2RZ/FnMjhfJHFvEMRY3 20 | AWS_REGION=us-east-1 21 | AWS_HOSTED_ZONE_ID=Z268AIJGIQT2CE6 22 | 23 | # DNS server whitelisted domains 24 | # Unbound service will remove any blocked domain entry that matches below list 25 | DOMAIN_WHITELIST="domain1.com domain2.com subdomain.domain3.com" 26 | -------------------------------------------------------------------------------- /.github/workflows/on-push.yml: -------------------------------------------------------------------------------- 1 | # This is for github actions testing, to be removed later 2 | name: CI Test 3 | on: 4 | push: 5 | branches: [ ci-integration ] 6 | pull_request: 7 | # allow test from actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | Build: 12 | runs-on: self-hosted 13 | strategy: 14 | fail-fast: false 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v2 18 | with: 19 | # We must fetch at least the immediate parents so that if this is 20 | # a pull request then we can checkout the head. 21 | fetch-depth: 2 22 | - name: Install Dependencies 23 | shell: bash 24 | run: | 25 | export DEBIAN_FRONTEND=noninteractive 26 | sudo apt-get -y update && sudo apt-get -y install libtool cmake automake autoconf make curl unzip jq 27 | mkdir -p test/bin 28 | echo -e '#!/bin/bash\necho "Command was executed successfully"\n' > test/bin/testcmd 29 | chmod +x test/bin/testcmd 30 | export PATH=test/bin:$PATH 31 | echo "Step - Deps: jq is installed at $(which jq)" 32 | 33 | 34 | - name: Build 35 | run: | 36 | echo "Step - Build: jq is installed at $(which jq)" 37 | echo "Build is successful" 38 | 39 | - name: Clean Artifacts 40 | run: | 41 | git clean -xdf 42 | -------------------------------------------------------------------------------- /examples/docker-compose-doh-server/remove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | __findWorkDir() { 5 | ## Get the real script folder path 6 | cd `dirname $0` 7 | script=`basename $0` 8 | # Iterate down a (possible) chain of symlinks 9 | while [ -L "$script" ] 10 | do 11 | script=`readlink $script` 12 | cd `dirname $script` 13 | script=`basename $script` 14 | done 15 | workDir=`pwd -P` 16 | cd $workDir 17 | } 18 | 19 | __loadConfig() { 20 | # env.conf is needed 21 | if [[ ! -f env.conf ]]; then 22 | echo "ERR: env.conf is missing" 23 | echo " Please copy env.sample.conf to env.conf" 24 | echo " and update variable values before running launch script" 25 | exit 1 26 | fi 27 | 28 | while read -r line 29 | do 30 | if [[ "$line" != "" && "$line" =~ = && ! "$line" =~ ^#.* ]]; then 31 | varName=$(echo $line|awk -F '[=]' '{print $1}') 32 | varValue=$(echo $line|sed "s/^${varName}=//") 33 | export ${varName}="${varValue}" 34 | fi 35 | done <<< "$(cat env.conf)" 36 | } 37 | 38 | __validations() { 39 | if [[ ! -f .local-docker-compose.yml ]]; then 40 | echo "ERR: .local-docker-compose.yml is missing, possible that services were never launched" 41 | fi 42 | } 43 | __removeDockerContainers() { 44 | docker-compose -f .local-docker-compose.yml -p ${STACK} down 45 | } 46 | 47 | __findWorkDir 48 | __loadConfig 49 | __validations 50 | __removeDockerContainers -------------------------------------------------------------------------------- /helm/doh-server/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for doh-server. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: satishweb/doh-server 9 | tag: latest 10 | pullPolicy: IfNotPresent 11 | 12 | nameOverride: "" 13 | fullnameOverride: "" 14 | 15 | env: 16 | DEBUG: "0" 17 | UPSTREAM_DNS_SERVER: "udp:8.8.8.8:53" 18 | DOH_HTTP_PREFIX: "/dns-query" 19 | DOH_SERVER_LISTEN: ":8053" 20 | DOH_SERVER_TIMEOUT: "10" 21 | DOH_SERVER_TRIES: "3" 22 | DOH_SERVER_VERBOSE: "false" 23 | 24 | containerPort: 8053 25 | 26 | service: 27 | type: ClusterIP 28 | port: 80 29 | annotations: 30 | 31 | ingress: 32 | enabled: false 33 | className: nginx 34 | domainName: "doh.your-domain.com" 35 | annotations: 36 | # kubernetes.io/ingress.class: nginx 37 | # kubernetes.io/tls-acme: "true" 38 | pathType: Prefix 39 | 40 | resources: 41 | # We usually recommend not to specify default resources and to leave this as a conscious 42 | # choice for the user. This also increases chances charts run on environments with little 43 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 44 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 45 | limits: 46 | cpu: 100m 47 | memory: 128Mi 48 | requests: 49 | cpu: 100m 50 | memory: 128Mi 51 | 52 | probesQuery: name=google.com 53 | 54 | nodeSelector: {} 55 | 56 | tolerations: [] 57 | 58 | affinity: {} 59 | -------------------------------------------------------------------------------- /helm/doh-server/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "doh-server.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "doh-server.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "doh-server.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{- define "doh-server.ingress.apiVersion" -}} 35 | {{- if semverCompare "<1.14-0" .Capabilities.KubeVersion.GitVersion -}} 36 | {{- print "extensions/v1beta1" -}} 37 | {{- else if semverCompare "<1.19-0" .Capabilities.KubeVersion.GitVersion -}} 38 | {{- print "networking.k8s.io/v1beta1" -}} 39 | {{- else -}} 40 | {{- print "networking.k8s.io/v1" -}} 41 | {{- end }} 42 | {{- end -}} 43 | -------------------------------------------------------------------------------- /helm/doh-server/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "doh-server.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "doh-server.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "doh-server.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "doh-server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | echo "Visit http://127.0.0.1:8080 to use your application" 20 | kubectl port-forward $POD_NAME 8080:80 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /helm/doh-server/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "doh-server.fullname" . -}} 3 | apiVersion: {{ include "doh-server.ingress.apiVersion" . }} 4 | kind: Ingress 5 | metadata: 6 | name: {{ $fullName }} 7 | labels: 8 | app.kubernetes.io/name: {{ include "doh-server.name" . }} 9 | helm.sh/chart: {{ include "doh-server.chart" . }} 10 | app.kubernetes.io/instance: {{ .Release.Name }} 11 | app.kubernetes.io/managed-by: {{ .Release.Service }} 12 | {{- with .Values.ingress.annotations }} 13 | annotations: 14 | nginx.ingress.kubernetes.io/force-ssl-redirect: "true" 15 | nginx.ingress.kubernetes.io/proxy-http-version: "1.1" 16 | nginx.ingress.kubernetes.io/proxy-connect-timeout: "86400" 17 | nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST" 18 | nginx.ingress.kubernetes.io/auth-cache-key: "$scheme$proxy_host$uri$is_args$args$request_body" 19 | nginx.ingress.kubernetes.io/auth-cache-duration: "any 10m" 20 | {{- toYaml . | nindent 4 }} 21 | {{- end }} 22 | spec: 23 | ingressClassName: {{ .Values.ingress.className }} 24 | tls: 25 | - hosts: 26 | - "{{ .Values.ingress.domainName }}" 27 | secretName: "{{ .Values.ingress.domainName }}-tls" 28 | rules: 29 | - host: "{{ .Values.ingress.domainName }}" 30 | http: 31 | paths: 32 | - path: /dns-query 33 | {{- if eq (include "doh-server.ingress.apiVersion" $) "networking.k8s.io/v1" }} 34 | pathType: {{ .Values.ingress.pathType }} 35 | {{- end }} 36 | backend: 37 | {{- if eq (include "doh-server.ingress.apiVersion" $) "networking.k8s.io/v1" }} 38 | service: 39 | name: {{ $fullName }} 40 | port: 41 | name: http 42 | {{- else }} 43 | serviceName: {{ $fullName }} 44 | servicePort: http 45 | {{- end }} 46 | {{- end -}} 47 | -------------------------------------------------------------------------------- /Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | # Author: Satish Gaikwad 2 | FROM golang:1.23-alpine AS doh-build 3 | LABEL MAINTAINER=satish@satishweb.com 4 | 5 | RUN apk add --no-cache git make jq curl 6 | 7 | WORKDIR /src 8 | 9 | # Lets download latest version of DOH 10 | ENV GOPROXY=https://proxy.golang.org,direct 11 | RUN set -x ;\ 12 | DOH_VERSION_LATEST="$(curl -s https://api.github.com/repos/m13253/dns-over-https/tags|jq -r '.[0].name')" \ 13 | && curl -L "https://github.com/m13253/dns-over-https/archive/${DOH_VERSION_LATEST}.zip" -o doh.zip \ 14 | && unzip doh.zip \ 15 | && rm doh.zip \ 16 | && cd dns-over-https* \ 17 | && make doh-server/doh-server \ 18 | && mkdir /dist \ 19 | && cp doh-server/doh-server /dist/doh-server \ 20 | && echo ${DOH_VERSION_LATEST} > /dist/doh-server.version 21 | 22 | FROM alpine:3.19 23 | LABEL MAINTAINER=satish@satishweb.com 24 | 25 | COPY --from=doh-build /dist /server 26 | COPY doh-server.sample.conf /server/doh-server.sample.conf 27 | 28 | # Install required packages by docker-entrypoint 29 | RUN apk add --no-cache bash gettext 30 | 31 | # Add docker entrypoint and make it executable 32 | ADD docker-entrypoint /docker-entrypoint 33 | RUN chmod u+x /docker-entrypoint 34 | 35 | # Change owner of the server folder 36 | RUN chown -R nobody:nogroup /server 37 | 38 | # Tell docker that all future commands should run as nobody 39 | USER nobody 40 | 41 | # Set environment defaults 42 | # We are using OpenDNS DNS server address as default 43 | # Here is the list of addresses: https://use.opendns.com/ 44 | ENV UPSTREAM_DNS_SERVER="udp:208.67.222.222:53,udp:208.67.220.220:53" 45 | ENV DOH_HTTP_PREFIX="/getnsrecord" 46 | ENV DOH_SERVER_LISTEN=":8053" 47 | ENV DOH_SERVER_TIMEOUT="10" 48 | ENV DOH_SERVER_TRIES="3" 49 | ENV DOH_SERVER_VERBOSE="false" 50 | 51 | EXPOSE 8053 52 | 53 | ENTRYPOINT ["/docker-entrypoint"] 54 | CMD [ "/server/doh-server", "-conf", "/server/doh-server.conf" ] 55 | 56 | # Healthcheck 57 | HEALTHCHECK --interval=1m --timeout=30s --start-period=1m CMD wget "localhost:$(echo ${DOH_SERVER_LISTEN}|awk -F '[:]' '{print $2}')$(echo ${DOH_HTTP_PREFIX})?name=google.com&type=A" -O /dev/null || exit 1 58 | -------------------------------------------------------------------------------- /docker-entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Author: Satish Gaikwad 3 | set -e 4 | 5 | if [ -f /run/secrets/DEBUG ]; then 6 | export DEBUG=$(cat $i) 7 | fi 8 | 9 | if [ "$DEBUG" = "1" ]; then 10 | set -x 11 | BASH_CMD_FLAGS='-x' 12 | fi 13 | 14 | DOH_CONFIG_SAMPLE_FILE=/server/doh-server.sample.conf 15 | DOH_CONFIG_FILE=/server/doh-server.conf 16 | 17 | printf "|---------------------------------------------------------------------------------------------\n"; 18 | printf "| Starting DNS Over HTTP Service \n" 19 | 20 | # Load env vars 21 | printf "| ENTRYPOINT: \033[0;31mLoading docker secrets if found...\033[0m\n" 22 | for i in $(env|grep '/run/secrets') 23 | do 24 | varName=$(echo $i|awk -F '[=]' '{print $1}'|sed 's/_FILE//') 25 | varFile=$(echo $i|awk -F '[=]' '{print $2}') 26 | exportCmd="export $varName=$(cat $varFile)" 27 | echo "${exportCmd}" >> /etc/profile 28 | eval "${exportCmd}" 29 | printf "| ENTRYPOINT: Exporting var: $varName\n" 30 | done 31 | 32 | # If UPSTREAM_DNS_SERVER contains a comma (i.e., multiple servers are provided) 33 | if [[ "$UPSTREAM_DNS_SERVER" == *","* ]]; then 34 | # Convert the comma-separated list into a valid array for the config file 35 | upstream_servers=$(echo "$UPSTREAM_DNS_SERVER" | sed 's/,/","/g') 36 | upstream_servers="[\"$upstream_servers\"]" 37 | else 38 | # If only a single DNS server is provided, use it as before 39 | upstream_servers="[\"$UPSTREAM_DNS_SERVER\"]" 40 | fi 41 | 42 | export upstream_servers 43 | 44 | # lets generate config file by replacing all variables inside of it. 45 | TMP_FILE=/tmp/doh-server.conf 46 | cp ${DOH_CONFIG_SAMPLE_FILE} ${TMP_FILE} 47 | DOLLAR='$' envsubst < /tmp/doh-server.conf > ${DOH_CONFIG_FILE} 48 | rm ${TMP_FILE} 49 | 50 | # Check if app-config is present 51 | if [ -f /app-config ]; then 52 | # We expect that app-config handles the launch of app command 53 | echo "| ENTRYPOINT: Executing app-config..." 54 | . /app-config "$@" 55 | else 56 | # Let default CMD run if app-config is missing 57 | echo "| ENTRYPOINT: app-config was not available, running given parameters or default CMD..." 58 | exec $@ 59 | fi 60 | -------------------------------------------------------------------------------- /helm/doh-server/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | {{- $fullName := include "doh-server.fullname" . -}} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ include "doh-server.fullname" . }} 6 | labels: 7 | app.kubernetes.io/name: {{ include "doh-server.name" . }} 8 | helm.sh/chart: {{ include "doh-server.chart" . }} 9 | app.kubernetes.io/instance: {{ .Release.Name }} 10 | app.kubernetes.io/managed-by: {{ .Release.Service }} 11 | {{- if .Values.deploymentAnnotations }} 12 | annotations: 13 | {{ toYaml .Values.deploymentAnnotations | indent 4 }} 14 | {{- end }} 15 | spec: 16 | replicas: {{ .Values.replicaCount }} 17 | selector: 18 | matchLabels: 19 | app.kubernetes.io/name: {{ include "doh-server.name" . }} 20 | app.kubernetes.io/instance: {{ .Release.Name }} 21 | template: 22 | metadata: 23 | labels: 24 | app.kubernetes.io/name: {{ include "doh-server.name" . }} 25 | app.kubernetes.io/instance: {{ .Release.Name }} 26 | spec: 27 | containers: 28 | - name: {{ .Chart.Name }} 29 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 30 | imagePullPolicy: {{ .Values.image.pullPolicy }} 31 | env: 32 | {{- range $key, $value := .Values.env }} 33 | - name: {{ $key }} 34 | value: "{{ $value }}" 35 | {{- end }} 36 | ports: 37 | - name: http 38 | containerPort: {{ .Values.containerPort }} 39 | protocol: TCP 40 | livenessProbe: 41 | httpGet: 42 | path: /dns-query?{{ .Values.probesQuery }} 43 | port: http 44 | readinessProbe: 45 | httpGet: 46 | path: /dns-query?{{ .Values.probesQuery }} 47 | port: http 48 | resources: 49 | {{- toYaml .Values.resources | nindent 12 }} 50 | {{- with .Values.nodeSelector }} 51 | nodeSelector: 52 | {{- toYaml . | nindent 8 }} 53 | {{- end }} 54 | {{- with .Values.affinity }} 55 | affinity: 56 | {{- toYaml . | nindent 8 }} 57 | {{- end }} 58 | {{- with .Values.tolerations }} 59 | tolerations: 60 | {{- toYaml . | nindent 8 }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IMAGE=satishweb/doh-server 2 | IMAGE_TEST=satishweb/doh-server-test 3 | ALPINE_PLATFORMS=linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6,linux/ppc64le,linux/s390x,linux/386 4 | UBUNTU_PLATFORMS=linux/amd64,linux/arm/v7 5 | DOCKER_BUILDX_CMD?=docker buildx 6 | 7 | WORKDIR=$(shell pwd) 8 | TAGNAME?=$(shell curl -s https://api.github.com/repos/m13253/dns-over-https/tags|jq -r '.[0].name') 9 | OSF?=alpine 10 | 11 | # Set L to + for debug 12 | L=@ 13 | 14 | ifdef PUSH_IMAGES 15 | EXTRA_BUILD_PARAMS = --push-images 16 | endif 17 | 18 | ifdef PUSH_GIT_TAGS 19 | EXTRA_BUILD_PARAMS += --push-git-tags 20 | endif 21 | 22 | 23 | ifdef LATEST 24 | EXTRA_BUILD_PARAMS += --mark-latest 25 | endif 26 | 27 | ifdef NO-CACHE 28 | EXTRA_BUILD_PARAMS += --no-cache 29 | endif 30 | 31 | all: build-alpine build-ubuntu 32 | 33 | build-alpine: 34 | $(L)./build.sh \ 35 | --image-name "${IMAGE}" \ 36 | --platforms "${ALPINE_PLATFORMS}" \ 37 | --work-dir "${WORKDIR}" \ 38 | --git-tag "${TAGNAME}-alpine" \ 39 | --docker-file "Dockerfile.alpine" \ 40 | --docker-buildx-cmd "${DOCKER_BUILDX_CMD}" \ 41 | ${EXTRA_BUILD_PARAMS} 42 | 43 | build-ubuntu: 44 | $(L)./build.sh \ 45 | --image-name "${IMAGE}" \ 46 | --platforms "${UBUNTU_PLATFORMS}" \ 47 | --work-dir "${WORKDIR}" \ 48 | --git-tag "${TAGNAME}-ubuntu" \ 49 | --docker-file "Dockerfile.ubuntu" \ 50 | --docker-buildx-cmd "${DOCKER_BUILDX_CMD}" \ 51 | $$(echo ${EXTRA_BUILD_PARAMS}|sed 's/--mark-latest//') 52 | 53 | test: 54 | $(L)docker build -t ${IMAGE_TEST}:${TAGNAME} -f ./Dockerfile.${OSF} . 55 | $(L)${MAKE} run-tests 56 | 57 | test-all: 58 | $(L)${MAKE} all PUSH_IMAGES=true 59 | $(L)${MAKE} run-tests IMAGE=${IMAGE_TEST} TAGNAME=${TAGNAME} 60 | 61 | run-tests: 62 | $(L)cd tests; pipenv install --python 3.12 63 | $(L)cd tests; pipenv run python ./test-doh-server.py --image ${IMAGE}:${TAGNAME}-alpine 64 | 65 | # Commands: 66 | # make test OSF=alpine # Build local platform image and then run tests for alpine dockerfile 67 | # make test OSF=ubuntu # Build local platform image and then run tests for ubuntu dockerfile 68 | # make all # Test all platforms docker container image build for alpine and ubuntu (no tests) 69 | # make test-all # Build, push images and then run tests 70 | # make all LATEST=true PUSH_IMAGES=true PUSH_GIT_TAGS=true # Build and push images with latest tag and push git tags 71 | -------------------------------------------------------------------------------- /Dockerfile.ubuntu: -------------------------------------------------------------------------------- 1 | # Author: Satish Gaikwad 2 | FROM golang:1.23-bullseye AS doh-build 3 | LABEL MAINTAINER=satish@satishweb.com 4 | 5 | ENV DEBIAN_FRONTEND=noninteractive 6 | 7 | RUN apt-get update && apt-get -y install \ 8 | build-essential \ 9 | git \ 10 | make \ 11 | jq \ 12 | curl \ 13 | unzip 14 | 15 | WORKDIR /src 16 | 17 | # Lets download latest version of DOH 18 | ENV GOPROXY=https://proxy.golang.org,direct 19 | RUN set -x ;\ 20 | DOH_VERSION_LATEST="$(curl -s https://api.github.com/repos/m13253/dns-over-https/tags|jq -r '.[0].name')" \ 21 | && curl -L "https://github.com/m13253/dns-over-https/archive/${DOH_VERSION_LATEST}.zip" -o doh.zip \ 22 | && unzip doh.zip \ 23 | && rm doh.zip \ 24 | && cd dns-over-https* \ 25 | && make doh-server/doh-server \ 26 | && mkdir /dist \ 27 | && cp doh-server/doh-server /dist/doh-server \ 28 | && echo ${DOH_VERSION_LATEST} > /dist/doh-server.version 29 | 30 | FROM ubuntu:22.04 31 | LABEL MAINTAINER=satish@satishweb.com 32 | 33 | ENV DEBIAN_FRONTEND=noninteractive 34 | 35 | COPY --from=doh-build /dist /server 36 | COPY doh-server.sample.conf /server/doh-server.sample.conf 37 | 38 | # Install required packages by docker-entrypoint 39 | RUN apt-get update && apt-get -y install \ 40 | bash \ 41 | gettext \ 42 | && apt-get clean \ 43 | && rm -rf /var/lib/apt/lists/* \ 44 | /var/cache/apt/archives/*deb 45 | 46 | # Add docker entrypoint and make it executable 47 | ADD docker-entrypoint /docker-entrypoint 48 | RUN chmod u+x /docker-entrypoint 49 | 50 | # Change owner of the server folder 51 | RUN chown -R nobody:nogroup /server 52 | 53 | # Tell docker that all future commands should run as nobody 54 | USER nobody 55 | 56 | # Set environment defaults 57 | # We are using OpenDNS DNS server address as default 58 | # Here is the list of addresses: https://use.opendns.com/ 59 | ENV UPSTREAM_DNS_SERVER="udp:208.67.222.222:53,udp:208.67.220.220:53" 60 | ENV DOH_HTTP_PREFIX="/getnsrecord" 61 | ENV DOH_SERVER_LISTEN=":8053" 62 | ENV DOH_SERVER_TIMEOUT="10" 63 | ENV DOH_SERVER_TRIES="3" 64 | ENV DOH_SERVER_VERBOSE="false" 65 | 66 | EXPOSE 8053 67 | 68 | ENTRYPOINT ["/docker-entrypoint"] 69 | CMD [ "/server/doh-server", "-conf", "/server/doh-server.conf" ] 70 | 71 | # Healthcheck 72 | HEALTHCHECK --interval=1m --timeout=30s --start-period=1m CMD wget "localhost:$(echo ${DOH_SERVER_LISTEN}|awk -F '[:]' '{print $2}')$(echo ${DOH_HTTP_PREFIX})?name=google.com&type=A" -O /dev/null || exit 1 73 | -------------------------------------------------------------------------------- /examples/docker-compose-doh-server/unbound.sample.conf: -------------------------------------------------------------------------------- 1 | # Author: Satish Gaikwad 2 | server: 3 | verbosity: 1 4 | username: unbound 5 | num-threads: 1 6 | interface: 0.0.0.0 7 | port: 53 8 | outgoing-range: 512 9 | num-queries-per-thread: 1024 10 | msg-cache-size: 16m 11 | rrset-cache-size: 32m 12 | msg-cache-slabs: 8 13 | rrset-cache-slabs: 8 14 | edns-buffer-size: 1280 15 | #so-rcvbuf: 1m 16 | val-clean-additional: yes 17 | hide-identity: yes 18 | hide-version: yes 19 | do-ip4: yes 20 | do-ip6: no 21 | do-udp: yes 22 | do-tcp: yes 23 | do-daemonize: no 24 | access-control: 0.0.0.0/0 refuse 25 | access-control: 192.168.0.0/24 allow 26 | access-control: 127.0.0.0/8 allow 27 | access-control: 172.0.0.0/8 allow 28 | access-control: ::1 allow 29 | access-control: ::ffff:127.0.0.1 allow 30 | access-control: 10.0.0.0/8 allow 31 | use-syslog: no 32 | log-queries: no 33 | log-time-ascii: yes 34 | #private-domain: "my.lan" 35 | #domain-insecure: "my.lan" 36 | qname-minimisation: yes 37 | auto-trust-anchor-file: /etc/unbound/keys/root.key 38 | root-hints: "/etc/unbound/root.hints" 39 | include: /etc/unbound/unbound.blocked.hosts 40 | include: /etc/unbound/custom/custom.hosts 41 | 42 | remote-control: 43 | control-enable: yes 44 | control-interface: 127.0.0.1 45 | server-key-file: "/etc/unbound/unbound_server.key" 46 | server-cert-file: "/etc/unbound/unbound_server.pem" 47 | control-key-file: "/etc/unbound/unbound_control.key" 48 | control-cert-file: "/etc/unbound/unbound_control.pem" 49 | 50 | 51 | forward-zone: 52 | name: "." 53 | forward-addr: 8.8.4.4 # Google 54 | forward-addr: 8.8.8.8 # Google 55 | forward-addr: 37.235.1.174 # FreeDNS 56 | forward-addr: 37.235.1.177 # FreeDNS 57 | forward-addr: 50.116.23.211 # OpenNIC 58 | forward-addr: 64.6.64.6 # Verisign 59 | forward-addr: 64.6.65.6 # Verisign 60 | forward-addr: 74.82.42.42 # Hurricane Electric 61 | forward-addr: 84.200.69.80 # DNS Watch 62 | forward-addr: 84.200.70.40 # DNS Watch 63 | forward-addr: 91.239.100.100 # censurfridns.dk 64 | forward-addr: 109.69.8.51 # puntCAT 65 | forward-addr: 216.146.35.35 # Dyn Public 66 | forward-addr: 216.146.36.36 # Dyn Public 67 | -------------------------------------------------------------------------------- /doh-server.sample.conf: -------------------------------------------------------------------------------- 1 | # HTTP listen port 2 | #listen = [ 3 | # 4 | # "127.0.0.1:8053", 5 | # "[::1]:8053", 6 | # 7 | # ## To listen on both 0.0.0.0:8053 and [::]:8053, use the following line 8 | # # ":8053", 9 | #] 10 | listen = [ "${DOH_SERVER_LISTEN}" ] 11 | 12 | # Local address and port for upstream DNS 13 | # If left empty, a local address is automatically chosen. 14 | local_addr = "" 15 | 16 | # TLS certification file 17 | # If left empty, plain-text HTTP will be used. 18 | # You are recommended to leave empty and to use a server load balancer (e.g. 19 | # Caddy, Nginx) and set up TLS there, because this program does not do OCSP 20 | # Stapling, which is necessary for client bootstrapping in a network 21 | # environment with completely no traditional DNS service. 22 | cert = "" 23 | 24 | # TLS private key file 25 | key = "" 26 | 27 | # HTTP path for resolve application 28 | path = "${DOH_HTTP_PREFIX}" 29 | 30 | # Upstream DNS resolver 31 | # If multiple servers are specified, a random one will be chosen each time. 32 | # You can use "udp", "tcp" or "tcp-tls" for the type prefix. 33 | # For "udp", UDP will first be used, and switch to TCP when the server asks to 34 | # or the response is too large. 35 | # For "tcp", only TCP will be used. 36 | # For "tcp-tls", DNS-over-TLS (RFC 7858) will be used to secure the upstream connection. 37 | # upstream = [ 38 | # "udp:1.1.1.1:53", 39 | # "udp:1.0.0.1:53", 40 | # "udp:8.8.8.8:53", 41 | # "udp:8.8.4.4:53", 42 | # ] 43 | upstream = ${upstream_servers} 44 | 45 | # Upstream timeout 46 | # timeout = 10 47 | timeout = ${DOH_SERVER_TIMEOUT} 48 | 49 | # Number of tries if upstream DNS fails 50 | # tries = 3 51 | tries = ${DOH_SERVER_TRIES} 52 | 53 | # Enable logging 54 | # verbose = false 55 | verbose = ${DOH_SERVER_VERBOSE} 56 | 57 | # Enable log IP from HTTPS-reverse proxy header: X-Forwarded-For or X-Real-IP 58 | # Note: http uri/useragent log cannot be controlled by this config 59 | log_guessed_client_ip = false 60 | 61 | # By default, non global IP addresses are never forwarded to upstream servers. 62 | # This is to prevent two things from happening: 63 | # 1. the upstream server knowing your private LAN addresses; 64 | # 2. the upstream server unable to provide geographically near results, 65 | # or even fail to provide any result. 66 | # However, if you are deploying a split tunnel corporation network environment, 67 | # or for any other reason you want to inhibit this behavior, change the following 68 | # option to "true". 69 | ecs_allow_non_global_ip = false 70 | 71 | # If ECS is added to the request, let the full IP address or 72 | # cap it to 24 or 128 mask. This option is to be used only on private 73 | # networks where knwoledge of the terminal endpoint may be required for 74 | # security purposes (eg. DNS Firewalling). Not a good option on the 75 | # internet where IP address may be used to identify the user and 76 | # not only the approximate location. 77 | ecs_use_precise_ip = false 78 | -------------------------------------------------------------------------------- /helm/README.md: -------------------------------------------------------------------------------- 1 | # Helm Chart for docker-doh 2 | 3 | ## Introduction 4 | 5 | This [Helm](https://github.com/kubernetes/helm) chart installs [docker-doh](https://github.com/satishweb/docker-doh) in a Kubernetes cluster. Welcome to [contribute](CONTRIBUTING.md) to Helm Chart for docker-doh. 6 | 7 | ## Prerequisites 8 | 9 | - Kubernetes cluster 1.10+ 10 | - Helm 2.8.0+ 11 | 12 | ## Installation 13 | 14 | ### Package the chart 15 | 16 | ```bash 17 | helm package --version 2.2.2 --app-version 2.2.2 --destination . doh-server 18 | ``` 19 | 20 | ### Configure the chart 21 | 22 | The following items can be set via `--set` flag during installation or configured by editing the `values.yaml` directly(need to download the chart first). 23 | 24 | #### Configure doh-server 25 | 26 | For enable ingress, the ingress controller must be installed in the Kubernetes cluster and a tls secret should be create. 27 | The following table lists the configurable parameters of the Harbor chart and the default values. 28 | 29 | | Parameter | Description | Default | 30 | | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | 31 | | **env** | 32 | | `env.DEBUG` | Disblable (0) or Enable (1) DEBUG | 0 | 33 | | `env.UPSTREAM_DNS_SERVER` | The upstream DNS server | udp:8.8.8.8:53 | 34 | | `env.DOH_SERVER_TIMEOUT` | Timeout to upstream DNS server | 10 | 35 | | `env.DOH_SERVER_TRIES` | DNS Request retry | 3 | 36 | | `env.DOH_SERVER_VERBOSE` | Disblable (false) or Enable (true) verbose server | false | 37 | | **ingress** | 38 | | `ingress.enabled` | Disblable (false) or Enable (true) the ingress | false | 39 | | `ingress.domainName` | The domain name that should be use to expose ingress | doh.your-domain.com | 40 | | `ingress.annotations` | Nginx Ingress annotations | | 41 | 42 | ### Install the chart 43 | 44 | Exemple install the doh-server helm chart with a release name `my-release`: 45 | 46 | helm 2: 47 | ```bash 48 | helm install --name my-release \ 49 | --set env.UPSTREAM_DNS_SERVER="udp:8.8.8.4:53" \ 50 | --set ingress.enabled=true \ 51 | --set ingress.domainName=doh.my-domain.com \ 52 | ./doh-server-2.2.2.tgz 53 | 54 | ``` 55 | 56 | ## Uninstallation 57 | 58 | To uninstall/delete the `my-release` deployment: 59 | 60 | helm 2: 61 | ```bash 62 | helm delete --purge my-release 63 | ``` 64 | -------------------------------------------------------------------------------- /examples/docker-compose-doh-server/launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | __findWorkDir() { 4 | ## Get the real script folder path 5 | cd `dirname $0` 6 | script=`basename $0` 7 | # Iterate down a (possible) chain of symlinks 8 | while [ -L "$script" ] 9 | do 10 | script=`readlink $script` 11 | cd `dirname $script` 12 | script=`basename $script` 13 | done 14 | workDir=`pwd -P` 15 | cd $workDir 16 | } 17 | 18 | envsubst() { 19 | # $1 = Source file path 20 | # $2 = Destination file path 21 | if [ "$2" = "" ]; then 22 | echo "ERR: envsubst function called without parameters" 23 | exit 1 24 | fi 25 | DOLLAR='$' 26 | cp $1 $2 27 | sed ${sedFlags} 's/\\/\\\\/g;s/"/\\"/g;s/`/BACKQUOTE/g' $2 28 | sed ${sedFlags} '' $2 29 | eval "echo \"$(cat $2)\"" > $2 30 | sed ${sedFlags} 's/BACKQUOTE/`/g' $2 31 | } 32 | 33 | __loadConfig() { 34 | # env.conf is needed 35 | if [[ ! -f env.conf ]]; then 36 | echo "ERR: env.conf is missing" 37 | echo " Please copy env.sample.conf to env.conf" 38 | echo " and update variable values before running launch script" 39 | exit 1 40 | fi 41 | 42 | while read -r line 43 | do 44 | if [[ "$line" != "" && "$line" =~ = && ! "$line" =~ ^#.* ]]; then 45 | varName=$(echo $line|awk -F '[=]' '{print $1}') 46 | varValue=$(echo $line|sed "s/^${varName}=//") 47 | export ${varName}="${varValue}" 48 | fi 49 | done <<< "$(cat env.conf)" 50 | } 51 | 52 | __validations() { 53 | 54 | export osv=$(uname|awk '{ print $1 }') 55 | if [[ "$osv" == "Darwin" ]]; then 56 | sedFlags="-i '' " 57 | else 58 | sedFlags="-i " 59 | fi 60 | 61 | # If stack is not up, check for ports before starting 62 | if [[ "$(docker ps --format '{{.Names}}'|grep -e "${STACK}_*"|wc -l)" -lt "1" ]]; then 63 | # check for systemd-resolved package. We can not run unbound with systemd-resolved 64 | if [[ "$(dpkg -l 2>&1|grep systemd-resolved|wc -l)" -gt "0" ]]; then 65 | echo "systemd-resolved package is installed on this system." 66 | echo "We can not have unbound installed here as systemd-resolved use 53 DNS port" 67 | echo "You can safely uninstall the package using command apt-get -y purge systemd-resolved" 68 | exit 1 69 | fi 70 | 71 | if [[ "$(netstat -nl|grep ".*:53.*LISTEN"|wc -l)" -gt "0" ]]; then 72 | echo "Port 53 is in use." 73 | echo "Please identify and stop the service using that port." 74 | echo "Command: netstat -nl" 75 | exit 1 76 | fi 77 | fi 78 | 79 | # Create data directories 80 | mkdir -p data/unbound/custom/ 81 | sudo touch data/unbound/custom/custom.hosts 82 | 83 | # Cleanup exited containers 84 | # Warn: This will clean non related but exited containers as well. 85 | docker rm $(docker ps -q -f "status=exited") >/dev/null 2>&1 86 | } 87 | 88 | __launchDockerContainers() { 89 | set -e 90 | 91 | envsubst "docker-compose.yml" ".local-docker-compose.yml" 92 | docker-compose -f .local-docker-compose.yml -p ${STACK} up -d 93 | if [[ "$?" != "0" ]]; then 94 | echo "Docker compose command failed" 95 | exit 1 96 | else 97 | echo "Docker compose has launched services. Please check container logs" 98 | fi 99 | } 100 | 101 | # Main 102 | 103 | __findWorkDir 104 | __loadConfig 105 | __validations 106 | __launchDockerContainers 107 | -------------------------------------------------------------------------------- /tests/test-doh-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import time 4 | import unittest 5 | import requests 6 | import argparse 7 | import os 8 | 9 | def install_dependencies(): 10 | # Check if Pipfile.lock exists 11 | if not os.path.exists("Pipfile.lock"): 12 | print("Installing dependencies...") 13 | os.system("pipenv install") 14 | print("Dependencies installed.") 15 | 16 | def run_tests(image_name, port, upstream_dns, prefix, timeout, tries, verbose): 17 | install_dependencies() # Install dependencies if not already installed 18 | 19 | import docker 20 | 21 | class TestDockerHealthCheck(unittest.TestCase): 22 | def test_healthcheck(self): 23 | print("Running health check test...") 24 | # Health check URL constructed based on Dockerfile's health check command 25 | healthcheck_url = f"http://localhost:{port}{prefix}?name=google.com&type=A" 26 | 27 | # Send a GET request to the health check URL 28 | response = requests.get(healthcheck_url) 29 | 30 | # Assert that the response status code is 200 (OK) 31 | self.assertEqual(response.status_code, 200) 32 | 33 | # Assert that the response body contains the IP address of google.com 34 | self.assertIn("google.com", response.text) 35 | self.assertIn("A", response.text) # Asserting the record type is A 36 | print("Health check test passed.") 37 | 38 | def wait_for_container_health(container): 39 | print("Waiting for container to be healthy...") 40 | while True: 41 | container.reload() 42 | health = container.attrs.get("State", {}).get("Health", {}) 43 | status = health.get("Status") 44 | if status == "healthy": 45 | break 46 | time.sleep(1) 47 | print("Container is healthy.") 48 | 49 | print(f"Starting Docker container using image '{image_name}' and port '{port}'...") 50 | client = docker.from_env() 51 | container = client.containers.run(image_name, detach=True, ports={f"{port}": f"{port}"}, environment={ 52 | "UPSTREAM_DNS_SERVER": upstream_dns, 53 | "DOH_HTTP_PREFIX": prefix, 54 | "DOH_SERVER_LISTEN": f":{port}", 55 | "DOH_SERVER_TIMEOUT": timeout, 56 | "DOH_SERVER_TRIES": tries, 57 | "DOH_SERVER_VERBOSE": verbose 58 | }) 59 | 60 | wait_for_container_health(container) 61 | 62 | # Load test case into test suite 63 | suite = unittest.TestLoader().loadTestsFromTestCase(TestDockerHealthCheck) 64 | 65 | # Run the tests 66 | print("Running tests...") 67 | unittest.TextTestRunner(verbosity=2).run(suite) 68 | 69 | # Stop and remove the container after tests 70 | print("Stopping Docker container...") 71 | container.stop() 72 | container.remove() 73 | print("Docker container stopped and removed.") 74 | 75 | if __name__ == "__main__": 76 | # Argument parser for overriding default values 77 | parser = argparse.ArgumentParser(description="Run tests for Docker container with health check.") 78 | parser.add_argument("--image", type=str, default="satishweb/doh-server-test", help="Docker image name (default: satishweb/doh-server)") 79 | parser.add_argument("--port", type=str, default="8053", help="Port number for the Docker container (default: 8053)") 80 | parser.add_argument("--upstream_dns", type=str, default="udp:208.67.222.222:53", help="Upstream DNS server (default: udp:208.67.222.222:53)") 81 | parser.add_argument("--prefix", type=str, default="/getnsrecord", help="HTTP prefix (default: /getnsrecord)") 82 | parser.add_argument("--timeout", type=str, default="10", help="Timeout for DNS queries (default: 10)") 83 | parser.add_argument("--tries", type=str, default="3", help="Number of retries for failed DNS queries (default: 3)") 84 | parser.add_argument("--verbose", type=str, default="false", help="Verbose mode for the DoH server (default: false)") 85 | args = parser.parse_args() 86 | 87 | run_tests(args.image, args.port, args.upstream_dns, args.prefix, args.timeout, args.tries, args.verbose) 88 | -------------------------------------------------------------------------------- /examples/docker-compose-doh-server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Author: satish@satishweb.com 2 | # Note: Please use docker-compose-ipv6.yml for ipv6 support 3 | # Please declare DOLLAR variable to dollar sign before running envsubst command on this file 4 | 5 | version: '2.2' 6 | networks: 7 | default: 8 | 9 | services: 10 | proxy: 11 | # The official v2 Traefik docker image 12 | image: traefik:v2.3 13 | hostname: proxy 14 | networks: 15 | - default 16 | environment: 17 | TRAEFIK_ACCESSLOG: "true" 18 | TRAEFIK_API: "true" 19 | TRAEFIK_PROVIDERS_DOCKER: "true" 20 | TRAEFIK_API_INSECURE: "true" 21 | TRAEFIK_PROVIDERS_DOCKER_NETWORK: "${STACK}_default" 22 | # DNS provider specific environment variables for DNS Challenge using route53 (AWS) 23 | AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} 24 | AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} 25 | AWS_REGION: ${AWS_REGION} 26 | AWS_HOSTED_ZONE_ID: ${AWS_HOSTED_ZONE_ID} 27 | ports: 28 | # The HTTP port 29 | - "80:80" 30 | # The HTTPS port 31 | - "443:443" 32 | # The Web UI (enabled by --api.insecure=true) 33 | - "8080:8080" 34 | command: 35 | #- "--log.level=DEBUG" 36 | - "--providers.docker.exposedbydefault=false" 37 | - "--entrypoints.web.address=:80" 38 | - "--entrypoints.websecure.address=:443" 39 | - "--certificatesresolvers.letsencrypt.acme.dnschallenge=true" 40 | # Providers list: 41 | # https://docs.traefik.io/https/acme/#providers 42 | # https://go-acme.github.io/lego/dns/ 43 | - "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=route53" 44 | # Enable below line to use staging letsencrypt server. 45 | #- "--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory" 46 | - "--certificatesresolvers.letsencrypt.acme.email=${EMAIL}" 47 | - "--certificatesresolvers.letsencrypt.acme.storage=/certs/acme.json" 48 | volumes: 49 | # So that Traefik can listen to the Docker events 50 | - /var/run/docker.sock:/var/run/docker.sock 51 | - ./data/proxy/certs:/certs 52 | 53 | doh-server: 54 | image: satishweb/doh-server:latest 55 | hostname: doh-server 56 | networks: 57 | - default 58 | environment: 59 | # Enable below line to see more logs 60 | # DEBUG: "1" 61 | UPSTREAM_DNS_SERVER: "udp:unbound:53" 62 | DOH_HTTP_PREFIX: "${DOH_HTTP_PREFIX}" 63 | DOH_SERVER_LISTEN: ":${DOH_SERVER_LISTEN}" 64 | DOH_SERVER_TIMEOUT: "10" 65 | DOH_SERVER_TRIES: "3" 66 | DOH_SERVER_VERBOSE: "false" 67 | #volumes: 68 | # - ./doh-server.conf:/server/doh-server.conf 69 | # - ./app-config:/app-config 70 | depends_on: 71 | - unbound 72 | labels: 73 | - "traefik.enable=true" 74 | - "traefik.http.routers.doh-server.rule=Host(`${SUBDOMAIN}.${DOMAIN}`) && Path(`${DOH_HTTP_PREFIX}`)" 75 | - "traefik.http.services.doh-server.loadbalancer.server.port=${DOH_SERVER_LISTEN}" 76 | - "traefik.http.middlewares.mw-doh-compression.compress=true" 77 | - "traefik.http.routers.doh-server.tls=true" 78 | - "traefik.http.middlewares.mw-doh-tls.headers.sslredirect=true" 79 | - "traefik.http.middlewares.mw-doh-tls.headers.sslforcehost=true" 80 | - "traefik.http.routers.doh-server.tls.certresolver=letsencrypt" 81 | - "traefik.http.routers.doh-server.tls.domains[0].main=${DOMAIN}" 82 | - "traefik.http.routers.doh-server.tls.domains[0].sans=${SUBDOMAIN}.${DOMAIN}" 83 | # Protection from requests flood 84 | - "traefik.http.middlewares.mw-doh-ratelimit.ratelimit.average=100" 85 | - "traefik.http.middlewares.mw-doh-ratelimit.ratelimit.burst=50" 86 | - "traefik.http.middlewares.mw-doh-ratelimit.ratelimit.period=10s" 87 | unbound: 88 | image: satishweb/unbound:latest 89 | hostname: unbound 90 | networks: 91 | - default 92 | ports: 93 | # Disable these ports if DOH server is the only client 94 | - 53:53/tcp 95 | - 53:53/udp 96 | volumes: 97 | - ./unbound.sample.conf:/templates/unbound.sample.conf 98 | - ./data/unbound/custom:/etc/unbound/custom 99 | # Keep your custom.hosts file inside custom folder 100 | environment: 101 | DOMAIN_WHITELIST: ${DOMAIN_WHITELIST} 102 | # DEBUG: "1" 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DNS Over HTTP Service Docker Image (Compatible with Raspberry Pi) 2 | 3 | ## Overview 4 | 5 | This Docker image provides a DNS Over HTTP (DOH) service, designed to enhance privacy and security by encrypting DNS queries. It supports custom upstream DNS servers and execution of custom scripts. The image is compatible with various architectures including linux/amd64, linux/arm64, and linux/arm/v7. It offers both Alpine and Ubuntu based images for flexibility. 6 | 7 | ## Upcoming Features 8 | 9 | - Helm chart for Kubernetes deployments (current chart is usable but not tied to the latest version of DOH) 10 | - Automated CI/CD using Github Actions 11 | - Kubernetes deployment examples 12 | 13 | ## Features 14 | 15 | - DNS Over HTTP support 16 | - Custom upstream DNS server option 17 | - Support for custom script execution (/app-config) 18 | - Compatible with below architectures: 19 | - Alpine: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6,linux/ppc64le,linux/s390x,linux/386 20 | - Ubuntu: linux/amd64,linux/arm/v7,linux/ppc64le,linux/s390x 21 | - Alpine based tiny images; Ubuntu based image also available 22 | - Comprehensive DOH Server setup example using Docker Compose 23 | 24 | ## Why Use DNS Over HTTP? 25 | 26 | Using DNS Over HTTP offers several advantages: 27 | 28 | - Protects against ISP monitoring 29 | - Avoids reliance on DOH providers, preserving privacy 30 | - Learn more: [Pros & Cons of DNS Over HTTPS](https://www.netsparker.com/blog/web-security/pros-cons-dns-over-https/) 31 | - Additional resource: [DNS over HTTPS - Wikipedia](https://en.wikipedia.org/wiki/DNS_over_HTTPS) 32 | 33 | ## How to Use 34 | 35 | ```bash 36 | docker run -itd --name doh-server \ 37 | -p 8053:8053 \ 38 | -e UPSTREAM_DNS_SERVER=udp:208.67.222.222:53 \ 39 | satishweb/doh-server 40 | ``` 41 | 42 | ## Docker Configuration 43 | 44 | ```yaml 45 | version: '2.2' 46 | networks: 47 | default: 48 | 49 | services: 50 | doh-server: 51 | image: satishweb/doh-server 52 | hostname: doh-server 53 | networks: 54 | - default 55 | environment: 56 | DEBUG: "0" 57 | # Upstream DNS server: proto:host:port 58 | # We are using OpenDNS DNS servers as default, 59 | # Here is the list of addresses: https://use.opendns.com/ 60 | UPSTREAM_DNS_SERVER: "udp:208.67.222.222:53" 61 | DOH_HTTP_PREFIX: "/getnsrecord" 62 | DOH_SERVER_LISTEN: ":8053" 63 | DOH_SERVER_TIMEOUT: "10" 64 | DOH_SERVER_TRIES: "3" 65 | DOH_SERVER_VERBOSE: "true" 66 | # You can add more variables here or as docker secret and entrypoint 67 | # script will replace them inside doh-server.conf file 68 | volumes: 69 | # - ./doh-server.conf:/server/doh-server.conf 70 | # Mount app-config script with your customizations 71 | # - ./app-config:/app-config 72 | deploy: 73 | replicas: 1 74 | # placement: 75 | # constraints: 76 | # - node.labels.type == worker 77 | ``` 78 | 79 | ## Docker Buildx Setup 80 | 81 | ### Setup: Mac M1/x86 82 | 83 | ```bash 84 | brew install colima 85 | colima start --cpu 8 --memory 16 --disk 150 86 | docker context use colima 87 | ``` 88 | 89 | ### Setup: Mac M1 (buildx) 90 | 91 | ```bash 92 | brew install colima 93 | colima start --arch x86_64 --cpu 8 --memory 16 --disk 150 -p buildx 94 | docker context use colima-buildx 95 | ``` 96 | 97 | ### Setup: Linux 98 | 99 | - Install Docker CLI + Containerd 100 | - Install docker-compose 101 | 102 | ### Start Buildx instance 103 | 104 | ```bash 105 | docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 106 | docker buildx create --use 107 | docker buildx inspect --bootstrap 108 | ``` 109 | 110 | ## Build Docker Image 111 | 112 | ```bash 113 | docker build . --no-cache -t satishweb/doh-server -f Dockerfile.alpine 114 | ``` 115 | 116 | ## Pull Docker Hub Image 117 | 118 | ```bash 119 | docker pull satishweb/doh-server 120 | ``` 121 | 122 | # Quick Setup Guide 123 | 124 | Follow these steps to set up DOH Server on Linux, Mac, or Raspberry Pi in minutes using Docker Compose. 125 | 126 | ## Requirements 127 | 128 | - Raspberry Pi/Linux/Mac with Docker preinstalled (Required) 129 | - DNS Server Setup on AWS R53 (Other providers supported) 130 | - AWS Access Key, Secret key, and R53 DNS Hosted Zone ID (for LetsEncrypt based auto installation of SSL Certs) (Optional) 131 | 132 | ## Steps 133 | 134 | 1. Download the latest release from [GitHub](https://github.com/satishweb/docker-doh/releases) to your server: 135 | 136 | ```bash 137 | wget https://github.com/satishweb/docker-doh/archive/v2.3.10.zip 138 | unzip v2.3.10.zip 139 | cp -rf docker-doh-2.3.10/examples/docker-compose-doh-server doh-server 140 | rm -rf v2.3.10.zip docker-doh-2.3.10 141 | cd doh-server 142 | ``` 143 | 144 | 2. Copy `env.sample.conf` to `env.conf` and update environment variables: 145 | 146 | ```bash 147 | EMAIL=user@example.com 148 | DOMAIN=example.com 149 | SUBDOMAIN=dns 150 | AWS_ACCESS_KEY_ID=AKIKJ_CHANGE_ME_FKGAFVA 151 | AWS_SECRET_ACCESS_KEY=Nx3yKjujG8kjj_CHANGE_ME_Z/FnMjhfJHFvEMRY3 152 | AWS_REGION=us-east-1 153 | AWS_HOSTED_ZONE_ID=Z268_CHANGE_ME_IQT2CE6 154 | ``` 155 | 156 | 3. Launch services: 157 | 158 | ```bash 159 | ./launch.sh 160 | ``` 161 | 162 | 4. Add your custom hosts to override DNS records if needed: 163 | 164 | ```bash 165 | mkdir -p data/unbound/custom 166 | vi data/unbound/custom/custom.hosts 167 | ``` 168 | 169 | 5. Determine your DOH address: 170 | 171 | ```bash 172 | https://dns.example.com/getnsrecord 173 | ``` 174 | 175 | 6. Test the DOH Server: 176 | 177 | ```bash 178 | curl -w '\n' 'https://dns.example.com/getnsrecord?name=google.com&type=A' 179 | ``` 180 | 181 | ## Common Issues and Debugging 182 | 183 | - If a proxy is still running with a self-signed certificate: 184 | - Check `data/proxy/certs/acme.json` contents. 185 | - Enable debug mode for the proxy by editing the proxy service in `docker-compose.yml`. 186 | - Check proxy container logs for errors. 187 | 188 | - If unable to bind port 53 for unbound service: 189 | - Stop `systemd-resolved` service: `sudo service systemd-resolved stop; sudo apt-get -y purge systemd-resolved` 190 | - Retry. 191 | 192 | - If unable to bind ports 80 and 443 for proxy service: 193 | - Another program on the Docker host or one of the Docker containers may be using the same ports. 194 | - Stop those programs or change the proxy service ports to unused ports. 195 | 196 | ## IPV6 Support 197 | 198 | Docker-compose configuration with IPV6 support will be added in the future. 199 | 200 | # How to Use DOH Server? 201 | 202 | ## Setup Your Router (Recommended) 203 | 204 | Configure your router's DHCP settings to point to your DOH server's IP address. 205 | 206 | ## Linux, Mac, Windows Clients 207 | 208 | Install Cloudflared for Linux, Mac, or Windows. Set your DOH server as upstream for Cloudflared as follows: 209 | 210 | - Linux: `/usr/local/etc/cloudflared/config.yml` 211 | - Mac: `/usr/local/etc/cloudflared/config.yaml` 212 | - Windows: Location varies 213 | 214 | ```yaml 215 | proxy-dns: true 216 | ``` 217 | -------------------------------------------------------------------------------- /tests/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "d7291f8f448e4607d47af6800c7dc1f19820dfccf5770b80fe18ee411192063a" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.12" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", 22 | "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3" 23 | ], 24 | "markers": "python_version >= '3.6'", 25 | "version": "==2025.4.26" 26 | }, 27 | "charset-normalizer": { 28 | "hashes": [ 29 | "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", 30 | "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", 31 | "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", 32 | "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", 33 | "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", 34 | "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", 35 | "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d", 36 | "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", 37 | "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184", 38 | "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", 39 | "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", 40 | "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64", 41 | "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", 42 | "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", 43 | "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", 44 | "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344", 45 | "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", 46 | "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", 47 | "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", 48 | "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", 49 | "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", 50 | "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", 51 | "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", 52 | "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", 53 | "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", 54 | "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", 55 | "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", 56 | "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", 57 | "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58", 58 | "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", 59 | "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", 60 | "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2", 61 | "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", 62 | "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", 63 | "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", 64 | "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", 65 | "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", 66 | "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f", 67 | "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", 68 | "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", 69 | "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", 70 | "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", 71 | "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", 72 | "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", 73 | "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", 74 | "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", 75 | "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4", 76 | "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", 77 | "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", 78 | "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", 79 | "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", 80 | "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", 81 | "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", 82 | "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", 83 | "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", 84 | "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", 85 | "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", 86 | "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa", 87 | "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", 88 | "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", 89 | "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", 90 | "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", 91 | "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", 92 | "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", 93 | "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02", 94 | "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", 95 | "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", 96 | "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", 97 | "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", 98 | "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", 99 | "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", 100 | "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", 101 | "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", 102 | "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", 103 | "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", 104 | "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", 105 | "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", 106 | "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", 107 | "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", 108 | "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", 109 | "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", 110 | "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", 111 | "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", 112 | "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", 113 | "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", 114 | "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", 115 | "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", 116 | "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da", 117 | "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", 118 | "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f", 119 | "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", 120 | "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f" 121 | ], 122 | "markers": "python_version >= '3.7'", 123 | "version": "==3.4.2" 124 | }, 125 | "docker": { 126 | "hashes": [ 127 | "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", 128 | "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0" 129 | ], 130 | "index": "pypi", 131 | "markers": "python_version >= '3.8'", 132 | "version": "==7.1.0" 133 | }, 134 | "idna": { 135 | "hashes": [ 136 | "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", 137 | "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" 138 | ], 139 | "markers": "python_version >= '3.6'", 140 | "version": "==3.10" 141 | }, 142 | "requests": { 143 | "hashes": [ 144 | "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", 145 | "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422" 146 | ], 147 | "index": "pypi", 148 | "markers": "python_version >= '3.8'", 149 | "version": "==2.32.4" 150 | }, 151 | "urllib3": { 152 | "hashes": [ 153 | "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", 154 | "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813" 155 | ], 156 | "markers": "python_version >= '3.9'", 157 | "version": "==2.4.0" 158 | } 159 | }, 160 | "develop": {} 161 | } 162 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------