#!/bin/sh

[ -r /mod/etc/conf/mod.cfg ] && . /mod/etc/conf/mod.cfg

# Global result
IP=""

helpmsg() {
cat << EOF
get_ip - determine external IP address

Usage: $0 [option]
    -h, --help          - print this help message

  IPv4 options:
    -a, --all           - use all methods (order: stun, route, dsld, webcm) [recommended]
    -c, --ctlm          - use ctlmgr_ctl
    -d, --dsld          - use showdsldstat (since firmware 04.86)
    -e, --env           - display environment variable IPADDR, used by multid
    -r, --route         - use routing table
    -s, --stun          - use STUN/VoIP (server: $MOD_GET_IP_STUN)
    -w, --webcm         - use webcm CGI handler / ctlmgr_ctl

  IPv6 options:
    -c6, --ctlm6        - use ctlmgr_ctl (IPv6 address)
    -d6, --dsld6        - use showdsldstat (IPv6 address)
    -cp6, --ctlmpre6    - use ctlmgr_ctl (IPv6 prefix)
    -dp6, --dlsdpre6    - use showdsldstat (IPv6 prefix)

Current default IPv4 method: $MOD_GET_IP_METHOD

EOF
}

# Detect private (RFC 1918) or link-local (RFC 5735) IPs
# Returns 0 for public IP, 1 for private IP and 2 if IP
# is empty or has invalid format
ip_public() {
	# format check (not checking >255)
	local ip="$(echo "$1" | sed -rn '/^([0-9]{1,3}\.){3}[0-9]{1,3}$/p')"
	[ -z "$ip" ] && return 2
	# 10.0.0.0/8 (private), 192.168.0.0/16 (private), 169.254.0.0/16 (link-local)
	(
		[ "$ip" != "${ip#10.}" ] ||
		[ "$ip" != "${ip#192.168.}" ] ||
		[ "$ip" != "${ip#169.254.}" ]
	) && return 1
	# 172.16.0.0/12 (private)
	[ "$ip" == "${ip#172.}" ] && return 0
	ip=$(echo $ip | cut -d '.' -f 2)
	[ $ip -ge 16  ] && [ $ip -le 31 ] && return 1 || return 0
}

via_ctlm() {
	# Determine ip address only from ctlmgr_ctl
	IP=$(ctlmgr_ctl r dslstatistic status/ifacestat0/ipaddr)
	ip_public "$IP" || return 1
}

via_dsld() {
	# Firmware ca. 04.68 and newer should be safe, but some boxes had it since 04.31
	IP="$(/sbin/showdsldstat 2>/dev/null | sed -nr 's/0:.*ip ([0-9.]+).*/\1/p')"
	ip_public "$IP" || return 1
}

via_env() {
	# If multid (or whoever) has already determined the external IP, use it
	IP="$IPADDR"
	ip_public "$IP" || return 1
}

# AVM's home-brew NAT does not expose the external IP on network interface
# "dsl", but sets a route to it. This method should work on most DSL boxes,
# with two known exceptions:
#   a) DNS servers are configured manually to hosts with public IP address.
#      In this case multiple candidate IPs would be found. Thus we return an
#      error rather than trying to guess which IP might be correct.
#   b) If the box does not connect to DSL via PPPoE but uses the DSL modem as a
#      bridge to e.g. a public /21 network, the external IP is not listed in
#      the routing table at all, but the /21 network instead.
# There might be more exceptions in case of routing table manipulation (maybe
# for VPNs or if for any reason a single external host IP is explicitly added
# to the routing table).
via_route() {
	local candidate_count=0
	for ip in $(route -n | sed -nr 's/^([1-9][0-9]*(\.[0-9]+){3}) +0(\.0){3} +255(\.255){3} +.* dsl$/\1/p'); do
		ip_public "$ip" && IP="$ip" && candidate_count=$((candidate_count + 1))
	done
	[ $candidate_count -eq 1 ] && return 0
	unset IP
	return 1
}

# On IP clients or UMTS, we are mostly behind a NAT and the only way to
# determine the external IP is to get the information from an external
# server. The most efficient method is STUN (stun-ip applet will try 3x).
# ip is cached for max 1 minute
via_stun() {
	IP=$(find /tmp/.get_ip -mmin 0 -exec cat {} ';' 2>/dev/null)
	[ -n "$IP" ] && return 0
	IP=$(stun-ip $MOD_GET_IP_STUN)
	[ $? -eq 0 -a -n "$IP" ] && echo "$IP" > /tmp/.get_ip && return 0
	return 1
}

via_webcm() {
	local queryfile="/usr/www/all/html/query.txt"
	local querystring=""
	if which ctlmgr_ctl >/dev/null; then
		# Firmware ca. 04.76 and newer
		IP=$(ctlmgr_ctl r connection0 pppoe:status/ip)
	else
		if [ "$(sed -n '/var:n\[/p' $queryfile)" ]; then
			# Firmware ca. 04.84 and newer (should never be used, see above)
			querystring="var:n[0]=connection0:pppoe:status/ip"
		else
			# Older firmware
			querystring="var:cnt=1&var:n0=connection0:pppoe:status/ip"
		fi
		IP="$(/usr/www/html/cgi-bin/webcm "getpage=${queryfile}&${querystring}")"
	fi
	# ctlmgr_ctl return values [box=val]: 7170=176, 7270=177, 7141=172
	# Caveat: must use "-o" instead of "||", otherwise "$?" would be reset
	[ $? -eq 0 -o $? -ge 170 ] && ip_public "$IP" && return 0 || return 1
}

via_ctlm6() {
	#use ctlmgr_ctl for determining the IPv6 address
	IP=$(ctlmgr_ctl r ipv6 status/ip)
}

via_ctlmpre6() {
	# use cltmgr_ctl for determing the IPv6 prefix
	IP=$(ctlmgr_ctl r ipv6 status/prefix | sed 's/\/.*//g')
}

via_dsld6() {
	# use showdsldstat for determining the IPv6 address
	IP=$(showdsldstat | sed -rn 's/.* IPv6: address ([^/]*).*/\1/p')
}

via_dsldpre6() {
	# use showdsldstat for determining the IPv6 prefix
	IP=$(showdsldstat | sed -rn 's/.* IPv6: prefix ([^ ]*).*/\1/p')
}

# Set user-defined method (e.g. via web UI) if no argument is given
[ $# -eq 0 ] && method="$MOD_GET_IP_METHOD" || method="$1"

case $method in
	-h|--help)
		helpmsg
		exit 0
		;;
	-a|--all|""|--extquery|-o|--ostat)
		# for compatibility reason only, may be removed later
		[ "$method" ] && [ "$method" != "-a" ] && [ "$method" != "--all" ] &&
			echo "warning: method $method is obsolete, using --all instead" >&2
		# Why this order?
		#   1.) STUN should always work and is fast
		#   2.) route is fast, works in all firmwares, problematic routing configs are rare
		#   3.) dsld is faster than ctlmgr/webcm, but not always available
		#   4.) ctlmgr/webcm is slow, but should work in all firmwares on DSL boxes
		for mode in stun route dsld webcm; do
			via_$mode
			[ $? -eq 0 ] && break
		done
		;;
	-c|--ctlm)
		via_ctlm
		;;
	-d|--dsld)
		via_dsld
		;;
	-e|--env)
		via_env
		;;
	-r|--route)
		via_route
		;;
	-s|--stun)
		via_stun
		;;
	-w|--webcm)
		via_webcm
		;;
	-c6|--ctlm6)
		via_ctlm6
		;;
	-d6|--dsld6)
		via_dsld6
		;;
	-cp6|--ctlmpre6)
		via_ctlmpre6
		;;
	-dp6|--dsldpre6)
		via_dsldpre6
		;;
	*)
		helpmsg >&2
		exit 1
		;;
esac

[ $? -ne 0 ] && echo "get_ip error" >&2 && exit 1
[ -z $IP ] && echo "get_ip error: no ip address found" >&2 && exit 1
echo "$IP"
