Skip to content

Instantly share code, notes, and snippets.

@jas-
Last active November 19, 2022 17:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jas-/4a777da24e7d0dec9db44c73f3353a4f to your computer and use it in GitHub Desktop.
Save jas-/4a777da24e7d0dec9db44c73f3353a4f to your computer and use it in GitHub Desktop.
A semi self aware packet trace tool; supports multiple packet capture tools such as tcpdump, tshark & snoop, monitors log disk for space constraints & can be configured to run for only a specified amount of time.
#!/bin/bash
#############################################################
# Functional description:
# Automates packet captures while safely monitoring disk
# space constraints for a specified amount of time.
#
# Supported OS:
# Solaris 10/11
# RHEL 5/6/7/8
# Ubuntu
# OEL
#
# Supported packet capture tools (in order or preference):
# tcpdump
# tshark (least tested option)
# snoop
#
# Configurable items:
# - Trace network traffic for how long?
# - What interfaces are we expected to capture on?
# - What systems should we capture traffic on?
# - PCAP save location; i.e. /var/tmp/pcap/<date>/
# - Safe disk capacity threshold? i.e. 80% capacity
#############################################################
# How long should we capture?
how_long="30m"
# We could use some interface names
declare -a ifaces
ifaces+=("enps0")
ifaces+=("enps2")
ifaces+=("enps5")
# What systems should we target?
# If you want the global include it here
# This list limits captures per system/zone and
# filters captures by host(s) on ${ifaces[@]}
declare -a systems
systems+=("host1")
systems+=("host2")
systems+=("host3")
systems+=("host4")
# Where do we save to?
save_path=/var/tmp/pcap
# At what percentage do we stop a capture? This is a percentage value
# Helps eliminate long running captures from exceeding disk capacity
capacity_threshold=80
# Ensure path is robust, but isn't silly
declare -a proposed_paths
proposed_paths="/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin"
all_paths=( $(echo $PATH:${proposed_paths} |
cut -d: -f1 | tr ' ' '\n' | sort -u) )
PATH="$(echo "${all_paths[@]}" | tr ' ' ':')"
#############################################################
# Make sure we should be operating on this system
#############################################################
# Create a filter for zoneadm list to limit scope of capture to
efilter="$(echo "${systems[@]}" | tr ' ' '|')"
# Acquire an array of zones to capture for
zones=( $(zoneadm list 2>/dev/null | egrep -i ${efilter}) )
# Handle non Solaris as well
[ ${#zones[@]} -eq 0 ] &&
zones+=( $(uname -n | cut -d"." -f1 | egrep -i ${efilter}) )
# If nothing configured matches our environment we should exit early
if [ ${#zones[@]} -eq 0 ]; then
echo "Error: Configured list of systems doesn't match our running env., did you configure the systems names for packet captures?"
exit 1
fi
#############################################################
# Functions to assist with repetitive tasks
#############################################################
# @description Kilobytes to bytes
#
# @arg ${1} Integer
#
# @stdout Integer
function aa_kb2b()
{
echo "${1} * 1024" | bc 2>/dev/null
}
# @description Megabytes to bytes
#
# @arg ${1} Integer
#
# @stdout Integer
function aa_mb2b()
{
echo "${1} * 1024 * 1024" | bc 2>/dev/null
}
# @description Gigabytes to bytes
#
# @arg ${1} Integer
#
# @stdout Integer
function aa_gb2b()
{
echo "${1} * 1024 * 1024 * 1024" | bc 2>/dev/null
}
# @description Terabytes to bytes
#
# @arg ${1} Integer
#
# @stdout Integer
function aa_tb2b()
{
echo "${1} * 1024 * 1024 * 1024 * 1024" | bc 2>/dev/null
}
# @description Petabytes to bytes
#
# @arg ${1} Integer
#
# @stdout Integer
function aa_pb2b()
{
echo "${1} * 1024 * 1024 * 1024 * 1024 * 1024" | bc 2>/dev/null
}
# @description Perform conversion from requested size to bytes
#
# @arg ${1} String - Size (should end in K/M/G/T/P)
#
# @stdout Integer
function ab_convert()
{
local size="${1}"
local bytes=
case "${size}" in
*K) bytes=$(aa_kb2b ${size/K}) ;;
*M) bytes=$(aa_mb2b ${size/M}) ;;
*G) bytes=$(aa_gb2b ${size/G}) ;;
*T) bytes=$(aa_gb2b ${size/T}) ;;
*P) bytes=$(aa_pb2b ${size/P}) ;;
esac
echo ${bytes} | cut -d"." -f1
}
# @description Percentage
#
# @arg ${1} Integer
# @arg ${2} Integer
# @arg ${3} Integer Rounding
#
# @stdout Integer
function ab_percent()
{
local total=${1}
local value=${2}
local scale=${3}
# Rounds decimals
echo "scale=${scale:=2}; 100 * ${value} / ${total}" |
bc 2>/dev/null | grep -v "^divide" | awk '{printf "%3.0f\n", $1}'
}
# @descripion Calculate disk capacity percentage
#
# @arg ${1} Integer - Disk total in bytes
# @arg ${2} Integer - Used space in bytes
#
# @stdout Integer
function disk_space_threshold_reached()
{
# Locally scope our variables
local blob
local total
local available
# What is the space available for ${save_path}?
if [ $(uname -a | grep -c SunOS) -gt 0 ]; then
blob="$(zfs list -Ho space ${save_path} | awk '{printf("%s:%s\n", $2, $3)}')"
else
blob="$(df -h ${save_path} | awk 'NR>1{printf("%s:%s\n", $2, $4)}')"
fi
# Convert values to bytes for easy math
total=$(ab_convert $(echo "${blob}" | cut -d: -f1))
available=$(ab_convert $(echo "${blob}" | cut -d: -f2))
# We should bail if ${save_path} exceeds ${capacity_threshold} percentage
if [ $(ab_percent ${total} ${available}) -gt ${capacity_threshold} ]; then
echo 1 && return
fi
echo 0
}
#############################################################
# Make sure we have enough disk space or build the save location
#############################################################
# Does the current save locations disk exceed space constraints?
if [ $(disk_space_threshold_reached) -eq 1 ]; then
echo "Error: Current save location disk space exceeds safe threshold"
echo " Save Path: ${save_path} >${capacity_threshold}% capacity"
exit 1
fi
# Setup some environment for captures and meta data for files
ts=$(date +%Y%m%d-%H%M%S)
d=${save_path}/$(date +%Y%m%d)
[ ! -d ${d} ] && mkdir -p ${d}
#############################################################
# Make sure we have a tool to capture with
#############################################################
# What tool is available? Prefer tcpdump
sniffer="$(which tcpdump 2>/dev/null)"
# If ${sniffer} is null look for tshark
[ "${sniffer}" = "" ] &&
sniffer="$(which tshark 2>/dev/null)"
# If ${sniffer is still null look for snoop
[ "${sniffer}" = "" ] &&
sniffer="$(which snoop 2>/dev/null)"
# Do we even have a sniffer to use? Bail if not
if [ "${sniffer}" = "" ]; then
echo "Error: Unable to find tcpdump/tshark/snoop tool(s)"
exit 1
fi
#############################################################
# Begin the main iterator; in most cases this will be one system
#############################################################
# Iterate ${zones[@]} and do work
for zone in ${zones[@]}; do
#############################################################
# Now begin iterating all defined interfaces for the capture
#############################################################
# Iterate ${ifaces[@]}
for iface in ${ifaces[@]}; do
#############################################################
# Seek the interface; Order of preference: dladm, ipadm, ip addr & ifconfig
#############################################################
# Prefer matching with dladm
[[ $(echo "${zone}" | grep -c "global") -eq 0 ]] && [[ $(zonename | grep -c "global") -eq 0 ]] &&
interface="$(dladm 2>/dev/null | grep "^${iface}" | awk '$1 !~ /\//{print $1}')" ||
interface="$(dladm 2>/dev/null | grep "^${zone}" | grep "${iface}" | awk '{print $1}')"
# Secondary use ipadm
[ "${interface}" = "" ] &&
interface="$(ipadm 2>/dev/null | grep "^${iface}" | awk '{print $1}' |
cut -d"/" -f1 | sort -u | head -1)"
# Tertiary use ip
[ "${interface}" = "" ] &&
interface="$(ip addr 2>/dev/null | grep "^${iface}" | awk '{print $1}' |
cut -d"/" -f1 | sort -u | head -1)"
# As a last resort check ifconfig
[ "${interface}" = "" ] &&
interface="$(ifconfig -a 2>/dev/null | grep "^${iface}" | awk '{print $1}' |
cut -d"/" -f1 | sort -u | head -1 | sed 's/:$//g')"
# Skip on 0 interfaces
[ "${interface}" = "" ] &&
continue
# Create a valid filename from ${interface}
fname_interface="$(echo "${interface}" | tr '/' '_')"
#############################################################
# Build the command string based on whatever tool exists; we are only capturing the packet header btw
#############################################################
# Since tools use different args build them here
case "${sniffer}" in
*tcpdump)
args="-i ${interface} -Z root -C 100 -s 128 -w ${d}/${zone}-${fname_interface}-${ts}.pcap" ;;
*tshark)
args="-i ${interface} -s 128 -w ${d}/${zone}-${fname_interface}-${ts}.pcap -f " ;;
*snoop)
args="-d ${interface} -s 128 -o ${d}/${zone}-${fname_interface}-${ts}.pcap" ;;
esac
#############################################################
# Robot does some work where applicable
#############################################################
# Since tshark is weird, pull the -f from the ${args}
args="$(echo "${args}" | sed 's/\.pcap \-f /\.pcap/g')"
# Start trace
${sniffer} ${args} &
done
done
#############################################################
# Monitor the disk space and bail if necessary
#############################################################
# Temporarily enable the extended debug option
shopt -s extdebug
# Generate a monitoring script to poll disk space constraints
cat <<_eof_ > ${save_path}/poll.sh
#!/bin/bash
# Ensure path is robust
PATH=$PATH:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin
# Variables
save_path=${save_path}
capacity_threshold=${capacity_threshold}
# Functions
$(declare -f)
# Poll the disk to ensure capacity is never reached and kill the pcaps if it does
while [ true ]; do
echo "Polling disk capacity: $(date +%Y%m%d-%H%M%s)"
if [ \$(disk_space_threshold_reached) -eq 1 ]; then
echo "Disk capacity threshold reached! Bye Felicia"
ps -ef | grep -v grep | egrep 'poll\.sh|${sniffer}' | awk '{print $2}' | sort -nr | xargs -iP kill -9 P
exit
fi
sleep 5
done
_eof_
# Disable the extended debug option
shopt -u extdebug
# Make sure it is executable
chmod +x ${save_path}/poll.sh
# Kick the poller into the background and log
${save_path}/poll.sh 2>&1 >${save_path}/${ts}.log &
#############################################################
# Sleep until our timer runs out and clean ourselves up
#############################################################
# Go to sleep robot
sleep ${how_long}
# Wake up and do some damage
ps -ef | grep -v grep | egrep 'poll\.sh|${sniffer}' | awk '{print $2}' | sort -nr | xargs -iP kill -9 P
# Clean up after yourself
[ -f ${save_path}/poll.sh ] &&
rm -f ${save_path}/poll.sh
# bye robot
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment