blob: def02a8039b55497d4d69f6f3bcb2234f8216e12 [file] [log] [blame] [edit]
#! /bin/bash
#
# Copyright (c) 2017-2023 Apple Inc. All rights reserved.
#
# This script is currently for Apple Internal use only.
#
declare -r version=1.10
declare -r script=${BASH_SOURCE[0]}
declare -r dnssdutil=${dnssdutil:-dnssdutil}
# The serviceTypesOfInterest array is initialized with commonly-debugged service types or service types whose records can
# provide useful debugging information, e.g., _airport._tcp in case an AirPort base station is a WiFi network's access
# point. Note: Additional service types can be added with the '-s' option.
serviceTypesOfInterest=(
_airplay._tcp # AirPlay
_airport._tcp # AirPort Base Station
_companion-link._tcp # Companion Link
_hap._tcp # HomeKit Accessory Protocol (TCP)
_hap._udp # HomeKit Accessory Protocol (UDP)
_homekit._tcp # HomeKit
_raop._tcp # Remote Audio Output Protocol
)
#============================================================================================================================
# PrintUsage
#============================================================================================================================
PrintUsage()
{
echo ""
echo "Usage: $( basename "${script}" ) [options]"
echo ""
echo "Options:"
echo " -s Specifies a service type of interest, e.g., _airplay._tcp, _raop._tcp, etc. Can be used more than once."
echo " -V Display version of this script and exit."
echo ""
}
#============================================================================================================================
# LogOut
#============================================================================================================================
LogOut()
{
echo "$( date '+%Y-%m-%d %H:%M:%S%z' ): $*"
}
#============================================================================================================================
# LogMsg
#============================================================================================================================
LogMsg()
{
echo "$*"
if [ -d "${workPath}" ]; then
LogOut "$*" >> "${workPath}/log.txt"
fi
}
#============================================================================================================================
# ErrQuit
#============================================================================================================================
ErrQuit()
{
echo "error: $*"
exit 1
}
#============================================================================================================================
# SignalHandler
#============================================================================================================================
SignalHandler()
{
LogMsg "Exiting due to signal."
trap '' SIGINT SIGTERM
pkill -TERM -P $$
wait
exit 2
}
#============================================================================================================================
# ExitHandler
#============================================================================================================================
ExitHandler()
{
if [ -d "${tempPath}" ]; then
rm -fr "${tempPath}"
fi
}
#============================================================================================================================
# GetStateDump
#============================================================================================================================
GetStateDump()
{
local suffix=''
if [ -n "${1}" ]; then
suffix="-${1//[^A-Za-z0-9._-]/_}"
fi
LogMsg "Getting mDNSResponder state dump."
dns-sd -O -stdout &> "${workPath}/state-dump${suffix}.txt"
}
#============================================================================================================================
# RunNetStat
#============================================================================================================================
RunNetStat()
{
LogMsg "Running netstat -g -n -s"
netstat -g -n -s &> "${workPath}/netstat-g-n-s.txt"
}
#============================================================================================================================
# StartPacketCapture
#============================================================================================================================
StartPacketCapture()
{
LogMsg "Starting tcpdump."
# The first tcpdump doesn't filter for any particular protocols because we want to see what else might be going on
# on the network. To avoid packet captures that are larger than necessary, a snapshot length of 9256 B is used,
# which is 9156 B plus 100 B as a fudge factor. This should be large enough to accommodate the largest mDNS message
# sent by mDNSResponder, since mDNS traffic is what we primarily care about.
#
# The 9156 B value is the sum of the following:
#
# 8940 B is the maximum mDNS message size used by mDNSResponder not including the mDNS header.
# 12 B is the size of an mDNS header.
# 8 B is the size of a UDP header.
# 40 B is the size of an IPv6 header (IPv6 headers are larger than IPv4 headers).
# 156 B is the amount of additional per-frame overhead used by tcpdump on macOS 12.0.
tcpdump -n -s 9256 -w "${workPath}/tcpdump.pcapng" &> "${workPath}/tcpdump.txt" &
tcpdumpPID=$!
tcpdump -i lo0 -n -w "${workPath}/tcpdump-loopback.pcapng" &> "${workPath}/tcpdump-loopback.txt" 'udp port 5353' &
tcpdumpLoopbackPID=$!
}
#============================================================================================================================
# SaveExistingPacketCaptures
#============================================================================================================================
SaveExistingPacketCaptures()
{
LogMsg "Saving existing mDNS packet captures."
mkdir "${workPath}/pcaps"
for file in /tmp/mdns-tcpdump.pcapng*; do
[ -e "${file}" ] || continue
baseName=$( basename "${file}" | sed -E 's/^mdns-tcpdump.pcapng([0-9]+)$/mdns-tcpdump-\1.pcapng/' )
gzip < "${file}" > "${workPath}/pcaps/${baseName}.gz"
done
}
#============================================================================================================================
# StopPacketCapture
#============================================================================================================================
StopPacketCapture()
{
LogMsg "Stopping tcpdump."
kill -TERM "${tcpdumpPID}"
kill -TERM "${tcpdumpLoopbackPID}"
}
#============================================================================================================================
# RunInterfaceMulticastTests
#============================================================================================================================
RunInterfaceMulticastTests()
{
local -r ifname=${1}
local -r allHostsV4=224.0.0.1
local -r allHostsV6=ff02::1
local -r mDNSV4=224.0.0.251
local -r mDNSV6=ff02::fb
local -r log="${workPath}/mcast-test-log-${ifname}.txt"
local serviceList=( $( "${dnssdutil}" queryrecord -i "${ifname}" -A -t ptr -n _services._dns-sd._udp.local -l 6 | sed -E -n 's/.*(_.*_(tcp|udp)\.local\.)$/\1/p' ) )
serviceList+=( "${serviceTypesOfInterest[@]/%/.local.}" )
serviceList=( $( IFS=$'\n' sort -f -u <<< "${serviceList[*]}" ) )
LogOut "List of services: ${serviceList[*]}" >> "${log}"
# Ping IPv4 broadcast address.
local broadcastAddr=$( ifconfig "${ifname}" inet | awk '$5 == "broadcast" {print $6}' )
if [ -n "${broadcastAddr}" ]; then
LogOut "Pinging ${broadcastAddr} on interface ${ifname}." >> "${log}"
ping -t 5 -b "${ifname}" "${broadcastAddr}" &> "${workPath}/ping-broadcast-${ifname}.txt"
else
LogOut "No IPv4 broadcast address for ${ifname}." >> "${log}"
fi
# Ping All Hosts IPv4 multicast address.
local routeOutput=$( route -n get -ifscope "${ifname}" "${allHostsV4}" 2> /dev/null )
if [ -n "${routeOutput}" ]; then
LogOut "Pinging ${allHostsV4} on interface ${ifname}." >> "${log}"
ping -t 5 -b "${ifname}" "${allHostsV4}" &> "${workPath}/ping-all-hosts-${ifname}.txt"
else
LogOut "No route to ${allHostsV4} on interface ${ifname}." >> "${log}"
fi
# Ping mDNS IPv4 multicast address.
routeOutput=$( route -n get -ifscope "${ifname}" "${mDNSV4}" 2> /dev/null )
if [ -n "${routeOutput}" ]; then
LogOut "Pinging ${mDNSV4} on interface ${ifname}." >> "${log}"
ping -t 5 -b "${ifname}" "${mDNSV4}" &> "${workPath}/ping-mDNS-${ifname}.txt"
else
LogOut "No route to ${mDNSV4} on interface ${ifname}." >> "${log}"
fi
# Ping All Hosts IPv6 multicast address.
routeOutput=$( route -n get -ifscope "${ifname}" -inet6 "${allHostsV6}" 2> /dev/null )
if [ -n "${routeOutput}" ]; then
LogOut "Pinging ${allHostsV6} on interface ${ifname}." >> "${log}"
ping6 -c 6 -I "${ifname}" "${allHostsV6}" &> "${workPath}/ping6-all-hosts-${ifname}.txt"
else
LogOut "No route to ${allHostsV6} on interface ${ifname}." >> "${log}"
fi
# Ping mDNS IPv6 multicast address.
routeOutput=$( route -n get -ifscope "${ifname}" -inet6 "${mDNSV6}" 2> /dev/null )
if [ -n "${routeOutput}" ]; then
LogOut "Pinging ${mDNSV6} on interface ${ifname}." >> "${log}"
ping6 -c 6 -I "${ifname}" "${mDNSV6}" &> "${workPath}/ping6-mDNS-${ifname}.txt"
else
LogOut "No route to ${mDNSV6} on interface ${ifname}." >> "${log}"
fi
# Send mDNS queries for services.
for service in "${serviceList[@]}"; do
LogOut "Sending mDNS queries for ${service} on interface ${ifname}." >> "${log}"
for(( i = 1; i <= 3; ++i )); do
printf "\n"
"${dnssdutil}" mdnsquery -i "${ifname}" -n "${service}" -t ptr -r 2
printf "\n"
"${dnssdutil}" mdnsquery -i "${ifname}" -n "${service}" -t ptr -r 1 --QU -p 5353
printf "\n"
done >> "${workPath}/mdnsquery-${ifname}.txt" 2>&1
done
}
#============================================================================================================================
# RunMulticastTests
#============================================================================================================================
RunMulticastTests()
{
local -r interfaces=( $( ifconfig -l -u ) )
local -r skipPrefixes=( awdl bridge ipsec llw nan p2p pdp_ip pktap UDC utun )
local -a pids
local ifname
local skip
local pid
LogMsg "List of interfaces: ${interfaces[*]}"
for ifname in "${interfaces[@]}"; do
skip=false
for prefix in "${skipPrefixes[@]}"; do
if [[ ${ifname} =~ ^${prefix}[0-9]*$ ]]; then
skip=true
break
fi
done
if ! "${skip}"; then
ifconfig ${ifname} | egrep -q '\binet6?\b'
if [ $? -ne 0 ]; then
skip=true
fi
fi
if "${skip}"; then
continue
fi
LogMsg "Starting interface multicast tests for ${ifname}."
RunInterfaceMulticastTests "${ifname}" & pids+=( $! )
done
LogMsg "Waiting for interface multicast tests to complete..."
for pid in "${pids[@]}"; do
wait "${pid}"
done
LogMsg "All interface multicast tests completed."
}
#============================================================================================================================
# RunBrowseTest
#============================================================================================================================
RunBrowseTest()
{
local -a typeArgs
if [ "${#serviceTypesOfInterest[@]}" -gt 0 ]; then
for serviceType in "${serviceTypesOfInterest[@]}"; do
typeArgs+=( "-t" "${serviceType}" )
done
LogMsg "Running dnssdutil browseAll command for service types of interest."
"${dnssdutil}" browseAll -A -d local -b 10 -c 10 "${typeArgs[@]}" &> "${workPath}/browseAll-STOI.txt"
fi
LogMsg "Running general dnssdutil browseAll command."
"${dnssdutil}" browseAll -A -d local -b 10 -c 10 &> "${workPath}/browseAll.txt"
}
#============================================================================================================================
# ArchiveLogs
#============================================================================================================================
ArchiveLogs()
{
local parentDir=''
# First, check for the non-macOS sysdiagnose archive path, then check for the macOS sysdiagnose archive path.
for dir in '/var/mobile/Library/Logs/CrashReporter' '/var/tmp'; do
if [ -w "${dir}" ]; then
parentDir="${dir}"
break
fi
done
# If a writable path wasn't available, just use /tmp.
[ -n "${parentDir}" ] || parentDir='/tmp'
local -r workdir=$( basename "${workPath}" )
local -r archivePath="${parentDir}/${workdir}.tar.gz"
LogMsg "Archiving logs."
echo "---"
tar -C "${tempPath}" -czf "${archivePath}" "${workdir}"
if [ -e "${archivePath}" ]; then
echo "Created log archive at ${archivePath}"
echo "*** Please run sysdiagnose NOW. ***"
echo "Attach both the log archive and the sysdiagnose archive to the radar."
if command -v open 2>&1 > /dev/null; then
open "${parentDir}"
fi
else
echo "Failed to create archive at ${archivePath}."
fi
echo "---"
}
#============================================================================================================================
# CreateWorkDirName
#============================================================================================================================
CreateWorkDirName()
{
local suffix=''
local -r productName=$( sw_vers -productName )
if [ -n "${productName}" ]; then
suffix+="_${productName}"
fi
local model=''
if command -v gestalt_query 2>&1 > /dev/null; then
model=$( gestalt_query -undecorated ProductType )
else
model=$( sysctl -n hw.model )
fi
model=${model//,/-}
if [ -n "${model}" ]; then
suffix+="_${model}"
fi
local -r buildVersion=$( sw_vers -buildVersion )
if [ -n "${buildVersion}" ]; then
suffix+="_${buildVersion}"
fi
suffix=${suffix//[^A-Za-z0-9._-]/_}
printf "bonjour-mcast-diags_$( date '+%Y.%m.%d_%H-%M-%S%z' )${suffix}"
}
#============================================================================================================================
# main
#============================================================================================================================
main()
{
while getopts ":s:hV" option; do
case "${option}" in
h)
PrintUsage
exit 0
;;
s)
serviceType=$( awk '{print tolower($0)}' <<< "${OPTARG}" )
if [[ ${serviceType} =~ ^_[-a-z0-9]*\._(tcp|udp)$ ]]; then
serviceTypesOfInterest+=( "${serviceType}" )
else
ErrQuit "Service type '${OPTARG}' is malformed."
fi
;;
V)
echo "$( basename "${script}" ) version ${version}"
exit 0
;;
:)
ErrQuit "option '${OPTARG}' requires an argument."
;;
*)
ErrQuit "unknown option '${OPTARG}'."
;;
esac
done
[ "${OPTIND}" -gt "$#" ] || ErrQuit "unexpected argument \"${!OPTIND}\"."
if [ "${EUID}" -ne 0 ]; then
if command -v sudo 2>&1 > /dev/null; then
echo "Re-launching with sudo"
exec sudo "${script}" "$@"
else
ErrQuit "$( basename "${script}" ) needs to be run as root."
fi
fi
tempPath=$( mktemp -d -q ) || ErrQuit "Failed to make temp directory."
workPath="${tempPath}/$( CreateWorkDirName )"
mkdir "${workPath}" || ErrQuit "Failed to make work directory."
trap SignalHandler SIGINT SIGTERM
trap ExitHandler EXIT
LogMsg "About: $( basename "${script}" ) version ${version} ($( md5 -q "${script}" ))."
if [ "${dnssdutil}" != "dnssdutil" ]; then
if [ -x "$( which "${dnssdutil}" )" ]; then
LogMsg "Using $( "${dnssdutil}" -V ) at $( which "${dnssdutil}" )."
else
LogMsg "WARNING: dnssdutil (${dnssdutil}) isn't an executable."
fi
fi
serviceTypesOfInterest=( $( IFS=$'\n' sort -u <<< "${serviceTypesOfInterest[*]}" ) )
GetStateDump 'before'
RunNetStat
StartPacketCapture
SaveExistingPacketCaptures
RunBrowseTest
RunMulticastTests
GetStateDump 'after'
StopPacketCapture
ArchiveLogs
}
main "$@"