#!/bin/sh
#
# SOURCE: https://github.com/ocochard/graphpath
# 
# Graphpath generates an ASCII network diagram from the route table of a Unix/Linux
# https://bsdrp.net
#
# Copyright (c) 2018, Olivier Cochard-Labbé (olivier@cochard.me)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.


########################################################
#           ## Concept documentation ##
#
# From this host ('me'), there are mainly 2 main families diagrams:
#
# - The first model family, when source and destination are towards differents
#   interfaces:
#
# +-----+   +-----+    +-----+    +-----+
# | src |   | src |    | src |    | src |
# +-----+   +-----+    +-----+    +-----+
#    |         |          |          |
# +----+  +--------+  +--------+   +----+
# | me |  | router |  | router |   | me |
# +----+  +--------+  +--------+   +----+
#    |        |           |          |
# +-----+   +----+      +----+   +--------+
# | dst |   | me |      | me |   | router |
# +-----+   +----+      +----+   +--------+
#             |           |          |
#          +-----+    +--------+  +-----+
#          | dst |    | router |  | dst |
#          +-----+    +--------+  +-----+
#                         |
#                      +-----+
#                      | dst |
#                      +-----+
#
# - The second model family, when source and destination are towards the same
#    interface:
#
# +-----+ +-----+ +-----+ +-----+ +-----+ +-----+ +-----+ +-----+ +-----+ +-----+
# | src | | dst | | src | | dst | | src | | dst | | src | | dst | | src | | dst |
# +-----+ +-----+ +-----+ +-----+ +-----+ +-----+ +-----+ +-----+ +-----+ +-----+
#    |       |       |       |       |        |      |        |     |        |
#    |       |       |       |       |        |      |        |    -+----+---+-
#    |       |       |       |       |        |      |        |          |
#   -+--+----+-      |       |       |        |   +--------+ +--------+  |
#       |            |       |       |        |   | router | | router |  |
# +--------+     +--------+  |       | +--------+ +--------+ +--------+  |
# | router |     | router |  |       | | router |     |          |       |
# +--------+     +--------+  |       | +--------+    -+-----+----+-    +----+
#      |            |        |       |      |               |          | me |
#   +----+         -+--+-----+-     -+--+---+-            +----+       +----+
#   | me |             |                |                 | me |
#   +----+          +----+           +----+               +----+
#                   | me |           | me |
#                   +----+           +----+
#
# Looking at these diagrams, we can extract 3 type of boxes:
# - standard and simple unique box: draw_block()
#
#          |
# +----------------+
# | src/dst/router |
# | IP: address    |
# | ARP: MAC       |
# +----------------+
#         |
#
# - A dual box on the same level: draw_2blocks()
#
# +---------------+ +---------------+
# | src or router | | dst or router |
# +---------------+ +---------------+
#

# - A 'me' block: draw_me()
#
#     |
# +---------+
# | if_name |
# | if_ip   |
# | route   |
# (next only if src_int != dst_int)
# | route   |
# | if_ip   |
# | if_name |
# +---------+
#     |
########################################################

# Forcing clean script
set -eu

# Functions definitions

# A usefull function (from: http://code.google.com/p/sh-die/)
die() { echo -n "EXIT: " >&2; echo "$@" >&2; exit 1; }

usage() {
	echo "Usage:"
	echo "$0 [-v] source-IP destination-IP"
	echo "  -v: display version number"
	echo "Example:"
	echo "  $0 198.18.0.10 198.19.0.10"
	exit 0
}

draw_block () {
	# Draw one block for source&destination host and router
	local model=$1       # src or dst, used for drawing link on the upper or lower side
	local label=$2       # text to display into the block
	local ip=$3          # IP address for src & dst host (=direct for a router)
	local gateway=$4     #  IP address for a router
	local gateway_arp=$5 # ARP cache if directly connected to "me" block
	local position=$6
	left=""
	right=""
	# There is a special case: If ip=direct, this mean this block is useless
	[ "$ip" = direct ] && return 0
	# If in second family mode, need to move or add vertical line
	[ "${position}" = "left" ] && left="                  |               "
	[ "${position}" = "right" ] && right="                  |"
	# If label is "ROUTER TOWARDS DESTINATION", need to move this block to the left
	#echo ${label} | grep -q "ROUTER TOWARDS DESTINATION" && left="                  |               "
	# If label is "ROUTER TOWARDS SOURCE", need to add an vertical line to the left"
	#echo ${label} | grep -q "ROUTER TOWARDS SOURCE" && right="                  |"
	[ "$model" = "dst" -a -z "${left}" ] && echo "${left}              |${right}"
	echo "${left}+----------------------------+${right}"
	printf '%s| %-26s |%s\n' "${left}" "${label}" "${right}"
	printf '%s| IP:   %-21s|%s\n' "${left}" ${ip} "${right}"
	[ "$gateway" = "direct" ] && printf '%s| ARP:  %-21s|%s\n' "${left}" "${gateway_arp}" "${right}"
	echo "${left}+----------------------------+${right}"
	[ "$model" = "src" -o -n "${left}" ] && echo "${left}              |               ${right}" || true
}

draw_2blocks () {
	# Draw two blocks on the same level
	# Second model family
	local label1=$1
	local label2=$2
	echo "+----------------------------+    +----------------------------+"
	printf '| %-26s |    | %-26s |\n' "${label1}" "${label2}"
	if echo "${label1}" | grep -q 'ROUTER'; then
		printf '| IP:   %-18s   |    | IP:  %-18s    |\n' ${src_gateway} ${dst_gateway}
		printf '| ARP:  %-18s   |    | ARP:  %-18s   |\n' ${src_gateway_arp} ${dst_gateway_arp}
	else
    	printf '| IP:   %-18s   |    | IP:  %-18s    |\n' ${src_ip} ${dst_ip}
	fi
	if [ "${src_gateway}" = "direct" ]; then
		printf '| ARP:  %-18s   |' ${src_gateway_arp}
	elif [ "${dst_gateway}" = "direct" ]; then
		printf '|                            |'
	fi
    if [ "${dst_gateway}" = "direct" ]; then
        printf '    | ARP: %-18s    |\n' ${dst_gateway_arp}
	elif [ "${src_gateway}" = "direct" ]; then
		printf '    |                            |\n'
    fi
	echo "+----------------------------+    +----------------------------+"
	echo "                  |                             |"
}

draw_me () {
	# Draw this device block
	echo "+----------------------------+"
	printf '| IF:   %-18s   |\n' ${src_interface}
	printf '| MAC:  %-18s   |\n' ${src_interface_mac}
	printf '| IP:   %-18s   |\n' ${src_interface_ip}
	printf '| net:  %-18s   |\n' ${src_destination}
	[ -n "${src_mask}" ] && printf '| mask: %-18s   |\n' ${src_mask}
	echo "|                            |"
	echo "|         THIS ${device}        |"
	if [ "${src_interface}" != "${dst_interface}" ]; then
		# First model type, need to draw lower part
		echo "|                            |"
    	printf '| net:  %-18s   |\n' ${dst_destination}
    	[ -n "${dst_mask}" ] && printf '| mask: %-18s   |\n' ${dst_mask}
    	printf '| IP:   %-18s   |\n' ${dst_interface_ip}
    	printf '| MAC:  %-18s   |\n' ${dst_interface_mac}
    	printf '| IF:   %-18s   |\n' ${dst_interface}
	fi
	echo "+----------------------------+"
}

get_bsd() {
	# Extract routes data
	# Check if router enabled
	[ $(sysctl -n net.inet.ip.forwarding) -eq 0 ] && forwarding=false || forwarding=true
	[ $(sysctl -n net.inet6.ip6.forwarding) -eq  0 ] && forwarding6=false || forwarding6=true

	# Populate src_* and dst_* variables
	for i in src dst; do
		eval "
			# if route didn't fill _gateway, this mean its directly connected
			${i}_gateway='direct'
			# some route crossing PPP tun doesn't have _mask
			${i}_mask='255.255.255.255'
			# call route only once
			tmp=\$(mktemp)
			route -n get \${${i}_ip} > \${tmp} || die \"Route towards \${${i}_ip} not found\"
			# Output are like this one:
			#   route to: 2.2.2.2
			#destination: 0.0.0.0
			#       mask: 0.0.0.0
			#    gateway: 192.168.1.100
			#        fib: 0
			#  interface: bxe3
			#      flags: <UP,GATEWAY,DONE,STATIC>
			# recvpipe  sendpipe  ssthresh  rtt,msec    mtu        weight    expire
			#       0         0         0         0      1500         1         0
			while read line; do
				data=\$(echo \$line | cut -d ':' -f 1)
				case \$data in
				\"route to\")
					${i}_routeto=\$(echo \$line | cut -d ':' -f 2 | tr -d ' ')
					;;
				destination)
					${i}_destination=\$(echo \$line | cut -d ':' -f 2 | tr -d ' ')
					;;
				mask)
					${i}_mask=\$(echo \$line | cut -d ':' -f 2 | tr -d ' ')
					;;
				gateway)
					${i}_gateway=\$(echo \$line | cut -d ':' -f 2 | tr -d ' ')
					;;
				fib)
					${i}_fib=\$(echo \$line | cut -d ':' -f 2)
					;;
				interface)
					${i}_interface=\$(echo \$line | cut -d ':' -f 2 | tr -d ' ')
					${i}_interface_mac=\$(ifconfig \${${i}_interface} | grep -E 'ether|lladdr|address' | cut -d ' ' -f 2)
					${i}_interface_ip=\$(ifconfig \${${i}_interface} | grep -w inet | cut -d ' ' -f 2)
					;;
				flags)
					${i}_flags=\$(echo \$line | cut -d ':' -f 2)
					;;
				esac
			done < \${tmp}
			[ \"\${${i}_gateway}\" = \"direct\" ] && lookup=\${${i}_routeto} || lookup=\${${i}_gateway}
			case \${OS} in
			OpenBSD)
				${i}_gateway_arp=\$(arp -n \${lookup} | grep \${lookup} | tr -s ' ' | cut -d ' ' -f 2)
				;;
			*)
				${i}_gateway_arp=\$(arp -n \${lookup} | cut -d ' ' -f 4)
				;;
			esac
			[ \"\${${i}_gateway_arp}\" = \"no\" ] && ${i}_gateway_arp='empty' || true
		"
		rm ${tmp} || die "can't delete ${tmp}"
	done
}

get_linux () {
	# Extract routes data from a Linux
	# ip route has a non consistent output: STUPID Linux !
	# Check if router enabled
	[ $(cat /proc/sys/net/ipv4/ip_forward) -eq 0 ] && forwarding=false || forwarding=true
	[ $(cat /proc/sys/net/ipv6/conf/all/forwarding) -eq  0 ] && forwarding6=false || forwarding6=true

	# Populate src_* and dst_* variables
	for i in src dst; do
		eval "
			# if route didn't fill _gateway, this mean its directly connected
			${i}_gateway='direct'
			# No mask on linux (using a / into the destination variable)
			${i}_mask=''
			# call route only once
			tmp=\$(mktemp)
			ip -o route get \${${i}_ip} > \${tmp}
			# Output are like these:
			# 2.2.2.3 via 10.253.52.126 dev eth2 src 10.253.52.115 \ cache
			# 192.168.237.116 dev eth1.337 src 192.168.237.115 \    cache
			# 10.253.52.124 dev eth2 src 10.253.52.115 \    cache
			# local 127.0.0.1 dev lo src 127.0.0.1
			read ip via gateway dev if src if_ip < \${tmp}
			if [ \"\${via}\" = \"via\" ]; then
				${i}_routeto=\${${i}_ip}
				${i}_gateway=\${gateway}
				${i}_gateway_arp=\$(ip neigh show \${${i}_gateway} | cut -d ' ' -f 5)
				${i}_interface=\${if}
				${i}_interface_ip=\$(echo \${if_ip} | cut -d ' ' -f1)
			elif [ \"\${via}\" = \"dev\" ]; then
				${i}_routeto=\${${i}_ip}
				${i}_interface=\${gateway}
				${i}_interface_ip=\$(echo \${if} | cut -d ' ' -f1)
			else
				echo "WARNING: Not supported condition!"
				${i}_routeto=\${via}
				${i}_interface=\${dev}
				${i}_interface_ip=\${via}
			fi
			${i}_interface_mac=\$(ip link show \${${i}_interface}| grep ether | tr -s ' ' | cut -d ' ' -f 3)
			# We still need other data (destination subnet and mask)
			# need to use another ip commande for getting the subnet matching
			# ip route show to match 192.168.229.58
			# default via 10.253.52.126 dev eth2 onlink
			# 192.168.229.56/29 via 192.168.237.100 dev eth1.337
			${i}_destination=\$(ip -o route show to match \${${i}_ip} | grep \${if} | cut -d ' ' -f1)
			if [ \"\${${i}_gateway}\" = \"direct\" ]; then
				${i}_gateway_arp=\$(ip neigh show \${${i}_routeto} | cut -d ' ' -f 5)
				[ \"\${${i}_gateway_arp}\" = \"no\" ] && ${i}_gateway_arp='empty' || true
			fi
		"
		rm ${tmp} || die "can't delete ${tmp}"
	done
}

### Main function ###

version="1.0"

if [ "$#" -eq 1 ]; then
	if [ "$1" = "-v" ]; then
		echo "Version ${version}"
		exit 0
	else
		usage
	fi
fi
[ "$#" -ne 2 ] && usage

src_ip=$1
dst_ip=$2
device="ROUTER"

# Small checks
[ "${src_ip}" = "${dst_ip}" ] && die "Same source and destination IP addresses"

OS=$(uname)
case ${OS} in
	FreeBSD|OpenBSD|NetBSD)
		get_bsd
		;;
	Linux)
		get_linux
		;;
	*)
		die "This script was not tested on ${OS}"
		;;
esac

if [ $forwarding = false -o $forwarding6 = false ]; then
	device="HOST  "
fi


[ "${device}" = "HOST" ] && echo "This tool is mainly designed for drawing router or firewall routing view"

### Start ASCII drawing

if [ "${src_interface}" != "${dst_interface}" ]; then
	# First model family
	if [ "${src_ip}" != "${src_interface_ip}" ]; then
		draw_block src 'SOURCE HOST' "${src_ip}" "${src_gateway}" "${src_gateway_arp}" ""
		[ "${src_gateway}" != "direct" ] && draw_block src 'ROUTER TOWARDS SOURCE' "${src_gateway}" direct "${src_gateway_arp}" ""
	fi
	draw_me
	if [ "${dst_ip}" != "${dst_interface_ip}" ]; then
		[ "${dst_gateway}" != "direct" ] && draw_block dst 'ROUTER TOWARDS DESTINATION' "${dst_gateway}" direct "${dst_gateway_arp}" ""
		draw_block dst 'DESTINATION HOST' "${dst_ip}" "${dst_gateway}" "${dst_gateway_arp}" ""
	fi
else
	# Second model family
	draw_2blocks 'SOURCE HOST' 'DESTINATION HOST'
	if [ "${src_gateway}" = "${dst_gateway}" ]; then
		echo "            --+---+-----------------------------+---"
		echo "              | "
		draw_block src 'ROUTER' "${src_gateway}" direct "${src_gateway_arp}" ""
	else
		if [ "${src_gateway}" != "direct" -a "${dst_gateway}" = "direct" ]; then
			draw_block src 'ROUTER TOWARDS SOURCE' "${src_gateway}" direct "${src_gateway_arp}" "right"
		elif [ "${dst_gateway}" != "direct" -a "${src_gateway}" = "direct" ]; then
			draw_block dst 'ROUTER TOWARDS DESTINATION' "${dst_gateway}" direct "${dst_gateway_arp}" "left"
		else
			draw_2blocks 'ROUTER TOWARDS SOURCE' 'ROUTER TOWARDS DESTINATION'
		fi
		echo "            --+---+-----------------------------+---"
		echo "                  |"
	fi
	draw_me
fi