Skip to content

Instantly share code, notes, and snippets.

@mschmitt
Last active November 16, 2022 20:02
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mschmitt/6c8e4b9978a166a06a54b413f194cd68 to your computer and use it in GitHub Desktop.
Save mschmitt/6c8e4b9978a166a06a54b413f194cd68 to your computer and use it in GitHub Desktop.
doh.sh - A highly non-scalable CGI DNS-over-HTTPS proxy in Bash. Have fun.
#!/bin/bash
# A highly non-scalable CGI DNS-over-HTTPS proxy in Bash. Have fun.
# Here's an endpoint running this script: https://doh.team-frickel.de
# Relevant Firefox settings:
# network.trr.mode = 2 -> DoH and fall back to DNS (default)
# network.trr.mode = 3 -> DoH only -> MUST use bootstrapAddress
# network.trr.uri = https://doh.team-frickel.de
# network.trr.useGET = true -> Semi-useful for debugging and testing (default false)
# network.trr.excluded-domains = localnet.name -> Your intranet domain
# network.trr.max-fails -> Raise to 11 when talking to this wonky script
# network.trr.bootstrapAddress = 217.182.197.102 -> No longer required since Firefox 73 or so.
# External requirements: base64, socat, wc
# All data received and sent needs to be stored in tmpfiles
# because Bash does not support raw binary data in variables.
function cleanup(){
rm -f "$tmp_query"
rm -f "$tmp_response"
}
trap cleanup INT QUIT TERM EXIT
tmp_query=$(mktemp)
tmp_response=$(mktemp)
if [[ "${REQUEST_METHOD}" == 'GET' ]]
then
# GET request: base64url-encoded raw DNS query data
# Remove leading dns=
query_string1="${QUERY_STRING//dns=/}"
# URL-decode: substitute [-+] with [_/]
query_string2="${query_string1//-/+}"
query_string3="${query_string2//_/\/}"
# URL-decode: substitute %3d with =
query_string4="${query_string3//%3d/=}"
cooked_query_string=${query_string4}
# base64url may omit the %3d or = padding so retry decoding twice with added =
for attempt in 1 2 3
do
if echo "${cooked_query_string}" | base64 -d > "$tmp_query" 2>/dev/null
then
break
else
if [[ ${attempt} -eq 3 ]]
then
printf "Failed to decode after padding twice: %s\n" \
"${cooked_query_string}" >&2
exit 1 # 500 Internal Server Error
fi
cooked_query_string+='='
fi
done
# echo "Serving GET request" >&2
elif [[ "${REQUEST_METHOD}" == 'POST' ]]
then
# POST request: raw DNS query data in body
cat > "$tmp_query"
# echo "Serving POST request" >&2
else
echo "Skipping invalid request method" >&2
exit 1 # 500 Internal Server Error
fi
# No query received.
if [[ ! -s "$tmp_query" ]]
then
printf "Content-Type: text/plain\n\n"
printf "This is a DNS-over-HTTPS resolver.\n"
echo "Responding to missing DNS query." >&2
exit 0 # 200 OK
fi
# Pass unmodified query to upstream server and return unmodified response.
# (Tmpfile required to determine content-length.)
# Time out the UDP connection early to speed up timely responses.
# Repeat for a while and raise timeout until we do have a response.
for timeout in 0.05 0.1 0.2 0.4 0.8 1.5 3.0 5.0 10.0 15.0
do
socat -t "${timeout}" - UDP-SENDTO:[::1]:53 < "$tmp_query" > "$tmp_response"
if [[ -s "$tmp_response" ]]
then
break
fi
done
# Exit with error if no response so far
if [[ ! -s "$tmp_response" ]]
then
echo "No response from name server received." >&2
exit 1 # 500 Internal Server Error
fi
printf "Content-Type: application/dns-message\n"
printf "X-Upstream-Timing: Max timeout %s s\n" "${timeout}"
printf "Content-Length: %s\n\n" "$(wc -c < "$tmp_response")"
cat "$tmp_response"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment