Imagine, not entirely hypotethically, that you have a packet capture containing a series of DHCP transactions that end in DHCP DECLINE (ie the DHCP client decides to reject the DHCP IP it was offered). And you want to example the context of each of those DHCP transactions to try to find common factors for why the DHCP offer was rejected.

Wireshark has some fairly good DHCP analysis features built in, but in a large packet capture finding the start/end of a DHCP transaction while also showing the related packets that occured in that time frame, can be tricky. The general process is:

  • identify a DHCP transaction of interest, perhaps by searching for DHCP DECLINEs (eg dhcp.option.dhcp==4, where 4 is the magic value for DECLINE, maybe with and dhcp.option.requested_ip_address=... including the IP address of interest)

  • for a given DHCP transaction, find the start and end frames by searching for that DHCP transaction ID (eg, dhcp.id==0x....), where the transaction ID is one you identified in the previous step, and looking for the first and last frame number displayed in that DHCP transaction ID match

  • display the relevant frames between the first DHCP message in that transaction and th e last DHCP message in that transaction (eg, (frame.number>=START and frame.number<=END))

  • maybe optionally filter out a bunch of the "LAN broadcast" noise protocols that are unlikely to be relevant to the DHCP failure (eg, not nbns and not igmp and not llmnr and not mdns and not dns and not tcp and not ssdp and not cldap and not ntp and not fip and not vrrp and not browser and not stp and not db-lsp-disc and not (udp and not dhcp))

That is possible to do by hand, but painful. Fortunately it is also fairly easy to automate by using tshark -- the command line interface to the Wireshark engine -- to split the packet capture into individual "DHCP transaction related frames" files. Which can then be loaded in turn, with the same display filter on them to hide the LAN broadcast noise.

Using tshark and the shell script below, enables running commands something like:

./extract-dhcp-sessions MACADDRESS IPADDRESS PCAP_FILE

which will look for DHCP DECLINEs for that IP address, find their start and end frames, and then output a series of packet capture files named something like:

PCAP_FILE-MACADDRESS-IPADDRESS-dhcp-DHCP_ID-frame-START-END.pcapng

where the filename includes the relevant information for what to expect to find in that filename. (As written the MACADDRESS is not filtered on at this level, but filtering on the MACADDRESS or the broadcast MAC -- ff:ff:ff:ff:ff:ff -- might also reduce the noise in the files.)

Bourne shell script below.

#! /bin/sh
# Split packet capture up into DHCP DECLINE transaction sections with tshark
#
# Usage: ./extract-dhcp-sessions MACADDRESS IPADDRESS PCAP_FILE
#
# Useful display filter to use with the extracts to eliminate a lot of
# the rest of the noise:
#
# not nbns and not igmp and not llmnr and not mdns and not dns and not tcp and not ssdp and not cldap and not ntp and not fip and not vrrp and not browser and not stp and not db-lsp-disc and not (udp and not dhcp)
#
# Written by Ewen McNeill <ewen@naos.co.nz>, 2023-09-19
#---------------------------------------------------------------------------

MACADDRESS="$1"
IPADDRESS="$2"
INPCAP="$3"

if [ -z "${MACADDRESS}" -o -z "${IPADDRESS}" -o -z "${INPCAP}" ]; then
  echo "Usage: $0 MACADDRESS IPADDRESS PCAP_FILE" >&2
  exit 1
fi

CLIENT=$(basename "${INPCAP}" | cut -f 1 -d "_")
CLEAN_MACADDRESS=$(echo "${MACADDRESS}" | tr -d ':-')
MACADDRESS=$(echo "${CLEAN_MACADDRESS}" | sed 's/../&:/g;' | sed 's/:$//;')

# Find the transaction IDs of the DHCP Declines
DHCP_DECLINES=$(
  tshark -r "${INPCAP}" \
     "dhcp.option.dhcp==4 and dhcp.option.requested_ip_address==${IPADDRESS}" | 
     awk '/Transaction ID/ { print $16;}'
)

for DHCP_ID in ${DHCP_DECLINES}; do
  # Figure out the frame range that we care about for that DHCP session
  DHCP_FRAMES=$(tshark -r "${INPCAP}" "dhcp.id==${DHCP_ID}")
  START_FRAME=$(echo "${DHCP_FRAMES}" | head -1 | awk '{print $1;}')
  END_FRAME=$(  echo "${DHCP_FRAMES}" | tail -1 | awk '{print $1;}')
  PADDED_START_FRAME=$(printf "%05d" "${START_FRAME}")
  PADDED_END_FRAME=$(  printf "%05d" "${END_FRAME}")
  FILENAME="${CLIENT}-${CLEAN_MACADDRESS}-${IPADDRESS}-dhcp-${DHCP_ID}-frames-${PADDED_START_FRAME}-${PADDED_END_FRAME}.pcapng"

  echo "Extracting ${CLIENT} (${MACADDRESS} / ${IPADDRESS}) dhcp.id==${DHCP_ID} (${START_FRAME}-${END_FRAME}) to ${FILENAME}"
  tshark -r "${INPCAP}" -w "${FILENAME}" \
     "not (dhcp.id ne ${DHCP_ID}) and not (arp.dst.proto_ipv4 ne ${IPADDRESS}) and (frame.number>=${START_FRAME} and frame.number<=${END_FRAME})"
done