#!/bin/sh # # Graphpath generates an ASCII network diagram from the route table of a Unix/Linux # https://bsdrp.net # # Copyright (c) 2018-2021, 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 | # +---------+ # | # # - About the box witdh # # inet4 maximum witdh size: #| ROUTER TOWARDS DESTINATION | #| IP: 192.168.100.100 | #| ARP: 00:0d:b9:3c:a0:cd | #+----------------------------+ #123456789012345678901234567890 # # => 30 (or 28 without line) # # inet6 maximum witdh size: #| IP: 2001:0db8:0000:0000:0000:ff00:0042:8329 | #| NDP: 00:0d:b9:3c:a0:cd | #+-----------------------------------------------+ #1234567890123456789012345679012345679012345679012 # # => 52 (or 50 without line) # # ######################################################## # 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" echo " $0 2001:db8:18::10 2001:db8:19::10" exit 0 } draw_lan_line () { # Draw a LAN line, like this one: # --+---+-----------------------------+--- # This line is only used in top of 'THIS ROUTER' box printf '%*s--+---+' $((box_hw - 1)) printf '%*s' $((box_w - 4)) ' ' | tr ' ' '-' printf '+---\n' } draw_connector () { # Draw one connector, like this one: # | eval "printf '%s%-$((box_hw))s|%s\n' "\${l_pad}\" "\${r_pad}\"" } draw_dual_connectors () { # Draw dual connectors, like this one: # | | eval "printf '%-$((box_hw + 1))s|%-$((box_w))s|\n'" } draw_block_line () { # Draw box's upper or lower line, like this one: # +----------------------------+ # The 'me' box didn't need left & right padding # because it's always put on the left side local ll_pad="$1" local lr_pad="$2" printf "%s+" "${ll_pad}" printf '%*s' $((box_w + 1)) ' ' | tr ' ' '-' printf "+%s\n" "${lr_pad}" } draw_2blocks_line () { # Draw 2 box's upper or lower line, like this one: # +-----------------------------+ +-----------------------------+ printf "+" printf '%*s' $((box_w + 1)) ' ' | tr ' ' '-' printf "+ +" printf '%*s' $((box_w + 1)) ' ' | tr ' ' '-' printf "+\n" } draw_block () { # Draw one block for source&destination host and router # # $l_pad | $r_pad # $l_pad +-------+ $r_pad # $l_pad |$label | $r_pad # $l_pad |$ip | $r_pad # $l_pad +-------+ $r_pad # $l_pad | $r_pad # # l_pad will draw a vertical line on the left side of the box # r_pad will draw a vertical line on the rigth side of the box 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 for box on left size, right other l_pad="" # left padding text r_pad="" # right padding text # 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 if [ "${position}" = "left" ]; then eval "l_pad=\$(printf '%-$((box_hw + 1))s|%-$((box_hw + 5))s')" fi if [ "${position}" = "right" ]; then r_pad=$(printf '%*s|' $((box_hw - 1))) fi if [ "$model" = "dst" -a -z "${l_pad}" ]; then # Draw connector on upper side draw_connector fi # Draw box's upper line draw_block_line "${l_pad}" "${r_pad}" eval " printf '%s| %-$((box_w - 1))s |%s\n' \"\${l_pad}\" \"\${label}\" \"\${r_pad}\" printf '%s| IP: %-$((box_w - 6))s|%s\n' \"\${l_pad}\" \${ip} \"\${r_pad}\" " if [ "$gateway" = "direct" ]; then eval "printf '%s| %s: %-$((box_w - 6))s|%s\n' \"\${l_pad}\" \"\${ADD_RES}\" \"${gateway_arp}\" \"${r_pad}\"" fi # Draw box's lower line draw_block_line "${l_pad}" "${r_pad}" # Draw block lower connectors if [ "$model" = "src" ]; then # needs a simple connectors if l_pad and r_pad are empty # needs dual connectors in other case [ -z "${l_pad}" -a -z "${r_pad}" ] && draw_connector || draw_dual_connectors else # If destination block and l_pad used, need a dual connectors [ -n "${l_pad}" ] && draw_dual_connectors fi } draw_2blocks () { # Draw two blocks on the same level # Second model family local label1="$1" local label2="$2" draw_2blocks_line eval "printf '| %-$((box_w))s| | %-$((box_w))s|\n' \"${label1}\" \"${label2}\"" if echo "${label1}" | grep -q 'ROUTER'; then eval " printf '| IP: %-$((box_w - 6))s| | IP: %-$((box_w - 6))s|\n' \${src_gateway} \${dst_gateway} printf '| %s: %-$((box_w - 6))s| | %s: %-$((box_w - 6))s|\n' \${ADD_RES} \${src_gateway_arp} \${ADD_RES} \${dst_gateway_arp} " else eval "printf '| IP: %-$((box_w - 6))s| | IP: %-$((box_w - 6))s|\n' \${src_ip} \${dst_ip}" fi if [ "${src_gateway}" = "direct" ]; then eval "printf '| %s: %-$((box_w - 6))s|' \${ADD_RES} \${src_gateway_arp}" elif [ "${dst_gateway}" = "direct" ]; then eval "printf '|%-$((box_w + 1))s|'" fi if [ "${dst_gateway}" = "direct" ]; then eval "printf ' | %s: %-$((box_w - 6))s|\n' \${ADD_RES} \${dst_gateway_arp}" elif [ "${src_gateway}" = "direct" ]; then eval "printf ' | %-$((box_w))s|\n'" fi draw_2blocks_line draw_dual_connectors } draw_me () { # Draw this device block # Draw box's lower line draw_block_line "" "" eval " printf '| IF: %-$((box_w - 6))s|\n' \${src_interface} printf '| MAC: %-$((box_w - 6))s|\n' \${src_interface_mac} printf '| IP: %-$((box_w - 6))s|\n' \${src_interface_ip} printf '| net: %-$((box_w - 6))s|\n' \${src_destination} " [ -n "${src_mask}" ] && eval "printf '| mask: %-$((box_w - 6))s|\n' \${src_mask}" eval " printf '|%-$((box_w + 1))s|\n' printf '|%-$((box_hw - 5))s THIS %-$((box_hw + 2))s|\n' \"\" \${device} " if [ "${src_interface}" != "${dst_interface}" ]; then # First model type, need to draw lower part eval " printf '|%-$((box_w +1))s|\n' printf '| net: %-$((box_w - 6))s|\n' \${dst_destination} " [ -n "${dst_mask}" ] && eval "printf '| mask: %-$((box_w - 6))s|\n' \${dst_mask}" eval " printf '| IP: %-$((box_w - 6))s|\n' ${dst_interface_ip} printf '| MAC: %-$((box_w - 6))s|\n' ${dst_interface_mac} printf '| IF: %-$((box_w - 6))s|\n' ${dst_interface} " fi # Draw box's lower line draw_block_line "" "" } 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 (${inet6}) && family="-inet6" || family="-inet" tmp=$(mktemp) # 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 case \${OS} in FreeBSD|Darwin) route -n get \${family} \${${i}_ip} > \${tmp} || die \"Route towards \${${i}_ip} not found\" ;; *) route -n get \${${i}_ip} > \${tmp} || die \"Route towards \${${i}_ip} not found\" ;; esac # Output are like this one: ### inet4 ### # 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: # recvpipe sendpipe ssthresh rtt,msec mtu weight expire # 0 0 0 0 1500 1 0 ### inet6 ### # route to: 2001:db8:11::11 #destination: 2001:db8:11:: # mask: ffff:ffff:ffff:ffff:: # gateway: 2001:db8:1::11 # fib: 0 # interface: bridge1 # flags: # 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 # Theorically we can remove for all lines, the first 13 characters # but the while read didn't take care of the first spaces \"route to\") ${i}_routeto=\${line#??????????} ;; destination) ${i}_destination=\${line#?????????????} ;; mask) ${i}_mask=\${line#??????} ;; gateway) ${i}_gateway=\${line#?????????} ;; fib) ${i}_fib=\${line#?????} ;; interface) ${i}_interface=\${line#???????????} ${i}_interface_mac=\$(ifconfig \${${i}_interface} | grep -E 'ether|lladdr|address' | cut -d ' ' -f 2) if (\${inet6}); then ${i}_interface_ip=\$(ifconfig \${${i}_interface} | grep -w inet6 | head -1 | cut -d ' ' -f 2) else ${i}_interface_ip=\$(ifconfig \${${i}_interface} | grep -w inet | cut -d ' ' -f 2) fi ;; flags) ${i}_flags=\${line#?????????} ;; esac done < \${tmp} [ \"\${${i}_gateway}\" = \"direct\" ] && lookup=\${${i}_routeto} || lookup=\${${i}_gateway} if (\${inet6}); then ${i}_gateway_arp=\$(ndp -n \${lookup} | tail -1 | tr -s ' ' | cut -d ' ' -f 2) # When it's empty, it return the IPv6 address between () echo \${${i}_gateway_arp} | grep -q '(' && ${i}_gateway_arp='empty' else 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 fi [ \"\${${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 (${inet6}) && family="-6" || family="-4" # ip route has a non consistent output: STUPID Linux ! # Check if router enabled if [ -f /proc/sys/net/ipv4/ip_forward ]; then [ $(cat /proc/sys/net/ipv4/ip_forward) -eq 0 ] && forwarding=false || forwarding=true else forwarding=false fi if [ -f /proc/sys/net/ipv6/conf/all/forwarding ]; then [ $(cat /proc/sys/net/ipv6/conf/all/forwarding) -eq 0 ] && forwarding6=false || forwarding6=true else forwarding6=false fi tmp=$(mktemp) # 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 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 # 2607:f8b0:4004:80c::200e from :: via 2001:470:66:324::1 dev he-ipv6 src 2001:470:66:324::2 metric 1024 pref medium 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) elif [ \"\${via}\" = \"from\" ]; then # Inet6 route ${i}_routeto=\${${i}_ip} ${i}_gateway=\${if} # 2002:333:333::1 dev eth1 lladdr 00:12:1e:33:AA:BB router REACHABLE ${i}_gateway_arp=\$(ip -6 neigh show \${${i}_gateway} | cut -d ' ' -f 5) ${i}_interface=\$(echo \${if_ip} | cut -d ' ' -f1) ${i}_interface_ip=\$(echo \${if_ip} | cut -d ' ' -f3) 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 command to retrieve 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 \${family} 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.2" 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" inet6=false # Small checks [ "${src_ip}" = "${dst_ip}" ] && die "Same source and destination IP addresses" $(echo ${src_ip} | grep -q ':') && inet6=true OS=$(uname) case ${OS} in FreeBSD|OpenBSD|NetBSD|Darwin) 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 (${inet6}); then ADD_RES="NDP" # inet6 addresses needs large box able to contains 50 caracters box_w=50 else ADD_RES="ARP" # inet4 addresses only need to contains 28 caracters box_w=28 fi box_hw=$((box_w / 2 - 1)) 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 draw_lan_line eval "printf '%-$((box_hw + 1))s|\n'" 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 draw_lan_line eval "printf '%-$((box_hw + 5))s|\n'" fi draw_me fi