Use DNS requests to discover external IP address

Using DNS requests is a much more robust and reliable method to discover
a machine's external IP address, instead of the previous method of using
Curl against an HTTP service.

Using Curl is fine, but the kind of services that are typically used
(here it was icanhazip.com, but there are lots more with a similar
behavior) are are not as dependable as the official DNS request methods
supported by some of the biggest service providers.

This uses "dig", which Alpine provides in the package "bind-tools" and
Debian in "dnstools".
This commit is contained in:
Juan Navarro 2021-04-19 12:51:00 +02:00
parent b31b004922
commit 24dbd9459d
4 changed files with 230 additions and 8 deletions

View File

@ -109,6 +109,9 @@ RUN apk update \
libpq mariadb-connector-c sqlite-libs \ libpq mariadb-connector-c sqlite-libs \
hiredis \ hiredis \
mongo-c-driver \ mongo-c-driver \
# Install `dig` tool for `detect-external-ip.sh`.
&& apk add --no-cache \
bind-tools \
# Cleanup unnecessary stuff. # Cleanup unnecessary stuff.
&& rm -rf /var/cache/apk/* && rm -rf /var/cache/apk/*

View File

@ -1,7 +1,115 @@
#!/bin/sh #!/usr/bin/env sh
# shellcheck shell=dash
if [ -z "$REAL_EXTERNAL_IP" ]; then #/ Use DNS to find out about the external IP of the running system.
export REAL_EXTERNAL_IP="$(curl -4 https://icanhazip.com 2>/dev/null)" #/
#/ This script is useful when running from a machine that sits behind a NAT.
#/ Due to how NAT works, machines behind it belong to an internal or private
#/ subnet, with a different address space than the external or public side.
#/
#/ Typically it is possible to make an HTTP request to a number of providers
#/ that offer the external IP in their response body (eg: ifconfig.me). However,
#/ why do a slow and heavy HTTP request, when DNS exists and is much faster?
#/ Well established providers such as OpenDNS or Google offer special hostnames
#/ that, when resolved, will actually return the IP address of the caller.
#/
#/ https://unix.stackexchange.com/questions/22615/how-can-i-get-my-external-ip-address-in-a-shell-script/81699#81699
#/
#/
#/ Arguments
#/ ---------
#/
#/ --ipv4
#/
#/ Find the external IPv4 address.
#/ Optional. Default: Enabled.
#/
#/ --ipv6
#/
#/ Find the external IPv6 address.
#/ Optional. Default: Disabled.
# Shell setup
# ===========
# Shell options for strict error checking.
for OPTION in errexit errtrace pipefail nounset; do
set -o | grep -wq "$OPTION" && set -o "$OPTION"
done
# Trace all commands (to stderr).
#set -o xtrace
# Shortcut: REAL_EXTERNAL_IP
# ==========================
if [ -n "${REAL_EXTERNAL_IP:-}" ]; then
echo "$REAL_EXTERNAL_IP"
exit 0
fi fi
exec echo "$REAL_EXTERNAL_IP"
# Parse call arguments
# ====================
CFG_IPV4="true"
while [ $# -gt 0 ]; do
case "${1-}" in
--ipv4) CFG_IPV4="true" ;;
--ipv6) CFG_IPV4="false" ;;
*)
echo "Invalid argument: '${1-}'" >&2
exit 1
;;
esac
shift
done
# Discover the external IP address
# ================================
if [ "$CFG_IPV4" = "true" ]; then
COMMANDS='dig @resolver1.opendns.com myip.opendns.com A -4 +short
dig @ns1.google.com o-o.myaddr.l.google.com TXT -4 +short | tr -d \"
dig @1.1.1.1 whoami.cloudflare TXT CH -4 +short | tr -d \"
dig @ns1-1.akamaitech.net whoami.akamai.net A -4 +short'
is_valid_ip() {
# Check if the input looks like an IPv4 address.
# Doesn't check if the actual values are valid; assumes they are.
echo "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$'
}
else
COMMANDS='dig @resolver1.opendns.com myip.opendns.com AAAA -6 +short
dig @ns1.google.com o-o.myaddr.l.google.com TXT -6 +short | tr -d \"
dig @2606:4700:4700::1111 whoami.cloudflare TXT CH -6 +short | tr -d \"'
is_valid_ip() {
# Check if the input looks like an IPv6 address.
# It's almost impossible to check the IPv6 representation because it
# varies wildly, so just check that there are at least 2 colons.
[ "$(echo "$1" | awk -F':' '{print NF-1}')" -ge 2 ]
}
fi
echo "$COMMANDS" | while read -r COMMAND; do
if IP="$(eval "$COMMAND")" && is_valid_ip "$IP"; then
echo "$IP"
exit 100 # Exits the pipe subshell.
fi
done
if [ $? -eq 100 ]; then
exit 0
else
echo "[$0] All providers failed" >&2
exit 1
fi

View File

@ -164,6 +164,9 @@ RUN apt-get update \
libssl1.1 \ libssl1.1 \
libpq5 libmariadb3 libsqlite3-0 \ libpq5 libmariadb3 libsqlite3-0 \
libhiredis0.14 \ libhiredis0.14 \
# Install `dig` tool for `detect-external-ip.sh`.
&& apt-get install -y --no-install-recommends --no-install-suggests \
dnsutils \
# Cleanup unnecessary stuff. # Cleanup unnecessary stuff.
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

View File

@ -1,7 +1,115 @@
#!/bin/sh #!/usr/bin/env sh
# shellcheck shell=dash
if [ -z "$REAL_EXTERNAL_IP" ]; then #/ Use DNS to find out about the external IP of the running system.
export REAL_EXTERNAL_IP="$(curl -4 https://icanhazip.com 2>/dev/null)" #/
#/ This script is useful when running from a machine that sits behind a NAT.
#/ Due to how NAT works, machines behind it belong to an internal or private
#/ subnet, with a different address space than the external or public side.
#/
#/ Typically it is possible to make an HTTP request to a number of providers
#/ that offer the external IP in their response body (eg: ifconfig.me). However,
#/ why do a slow and heavy HTTP request, when DNS exists and is much faster?
#/ Well established providers such as OpenDNS or Google offer special hostnames
#/ that, when resolved, will actually return the IP address of the caller.
#/
#/ https://unix.stackexchange.com/questions/22615/how-can-i-get-my-external-ip-address-in-a-shell-script/81699#81699
#/
#/
#/ Arguments
#/ ---------
#/
#/ --ipv4
#/
#/ Find the external IPv4 address.
#/ Optional. Default: Enabled.
#/
#/ --ipv6
#/
#/ Find the external IPv6 address.
#/ Optional. Default: Disabled.
# Shell setup
# ===========
# Shell options for strict error checking.
for OPTION in errexit errtrace pipefail nounset; do
set -o | grep -wq "$OPTION" && set -o "$OPTION"
done
# Trace all commands (to stderr).
#set -o xtrace
# Shortcut: REAL_EXTERNAL_IP
# ==========================
if [ -n "${REAL_EXTERNAL_IP:-}" ]; then
echo "$REAL_EXTERNAL_IP"
exit 0
fi fi
exec echo "$REAL_EXTERNAL_IP"
# Parse call arguments
# ====================
CFG_IPV4="true"
while [ $# -gt 0 ]; do
case "${1-}" in
--ipv4) CFG_IPV4="true" ;;
--ipv6) CFG_IPV4="false" ;;
*)
echo "Invalid argument: '${1-}'" >&2
exit 1
;;
esac
shift
done
# Discover the external IP address
# ================================
if [ "$CFG_IPV4" = "true" ]; then
COMMANDS='dig @resolver1.opendns.com myip.opendns.com A -4 +short
dig @ns1.google.com o-o.myaddr.l.google.com TXT -4 +short | tr -d \"
dig @1.1.1.1 whoami.cloudflare TXT CH -4 +short | tr -d \"
dig @ns1-1.akamaitech.net whoami.akamai.net A -4 +short'
is_valid_ip() {
# Check if the input looks like an IPv4 address.
# Doesn't check if the actual values are valid; assumes they are.
echo "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$'
}
else
COMMANDS='dig @resolver1.opendns.com myip.opendns.com AAAA -6 +short
dig @ns1.google.com o-o.myaddr.l.google.com TXT -6 +short | tr -d \"
dig @2606:4700:4700::1111 whoami.cloudflare TXT CH -6 +short | tr -d \"'
is_valid_ip() {
# Check if the input looks like an IPv6 address.
# It's almost impossible to check the IPv6 representation because it
# varies wildly, so just check that there are at least 2 colons.
[ "$(echo "$1" | awk -F':' '{print NF-1}')" -ge 2 ]
}
fi
echo "$COMMANDS" | while read -r COMMAND; do
if IP="$(eval "$COMMAND")" && is_valid_ip "$IP"; then
echo "$IP"
exit 100 # Exits the pipe subshell.
fi
done
if [ $? -eq 100 ]; then
exit 0
else
echo "[$0] All providers failed" >&2
exit 1
fi