1#! /bin/bash 2# 3# Copyright (c) 2017-2020 Apple Inc. All rights reserved. 4# 5# This script is currently for Apple Internal use only. 6# 7 8declare -r version=1.8 9declare -r script=${BASH_SOURCE[0]} 10declare -r dnssdutil=${dnssdutil:-dnssdutil} 11 12# The serviceTypesOfInterest array is initialized with commonly-debugged service types or service types whose records can 13# provide useful debugging information, e.g., _airport._tcp in case an AirPort base station is a WiFi network's access 14# point. Note: Additional service types can be added with the '-s' option. 15 16serviceTypesOfInterest=( 17 _airplay._tcp # AirPlay 18 _airport._tcp # AirPort Base Station 19 _companion-link._tcp # Companion Link 20 _hap._tcp # HomeKit Accessory Protocol 21 _homekit._tcp # HomeKit 22 _raop._tcp # Remote Audio Output Protocol 23) 24 25#============================================================================================================================ 26# PrintUsage 27#============================================================================================================================ 28 29PrintUsage() 30{ 31 echo "" 32 echo "Usage: $( basename "${script}" ) [options]" 33 echo "" 34 echo "Options:" 35 echo " -s Specifies a service type of interest, e.g., _airplay._tcp, _raop._tcp, etc. Can be used more than once." 36 echo " -V Display version of this script and exit." 37 echo "" 38} 39 40#============================================================================================================================ 41# LogOut 42#============================================================================================================================ 43 44LogOut() 45{ 46 echo "$( date '+%Y-%m-%d %H:%M:%S%z' ): $*" 47} 48 49#============================================================================================================================ 50# LogMsg 51#============================================================================================================================ 52 53LogMsg() 54{ 55 echo "$*" 56 if [ -d "${workPath}" ]; then 57 LogOut "$*" >> "${workPath}/log.txt" 58 fi 59} 60 61#============================================================================================================================ 62# ErrQuit 63#============================================================================================================================ 64 65ErrQuit() 66{ 67 echo "error: $*" 68 exit 1 69} 70 71#============================================================================================================================ 72# SignalHandler 73#============================================================================================================================ 74 75SignalHandler() 76{ 77 LogMsg "Exiting due to signal." 78 trap '' SIGINT SIGTERM 79 pkill -TERM -P $$ 80 wait 81 exit 2 82} 83 84#============================================================================================================================ 85# ExitHandler 86#============================================================================================================================ 87 88ExitHandler() 89{ 90 if [ -d "${tempPath}" ]; then 91 rm -fr "${tempPath}" 92 fi 93} 94 95#============================================================================================================================ 96# GetStateDump 97#============================================================================================================================ 98 99GetStateDump() 100{ 101 local suffix='' 102 if [ -n "${1}" ]; then 103 suffix="-${1//[^A-Za-z0-9._-]/_}" 104 fi 105 LogMsg "Getting mDNSResponder state dump." 106 dns-sd -O -stdout &> "${workPath}/state-dump${suffix}.txt" 107} 108 109#============================================================================================================================ 110# RunNetStat 111#============================================================================================================================ 112 113RunNetStat() 114{ 115 LogMsg "Running netstat -g -n -s" 116 netstat -g -n -s &> "${workPath}/netstat-g-n-s.txt" 117} 118 119#============================================================================================================================ 120# StartPacketCapture 121#============================================================================================================================ 122 123StartPacketCapture() 124{ 125 LogMsg "Starting tcpdump." 126 tcpdump -n -w "${workPath}/tcpdump.pcapng" &> "${workPath}/tcpdump.txt" & 127 tcpdumpPID=$! 128 tcpdump -i lo0 -n -w "${workPath}/tcpdump-loopback.pcapng" &> "${workPath}/tcpdump-loopback.txt" 'udp port 5353' & 129 tcpdumpLoopbackPID=$! 130} 131 132#============================================================================================================================ 133# SaveExistingPacketCaptures 134#============================================================================================================================ 135 136SaveExistingPacketCaptures() 137{ 138 LogMsg "Saving existing mDNS packet captures." 139 mkdir "${workPath}/pcaps" 140 for file in /tmp/mdns-tcpdump.pcapng*; do 141 [ -e "${file}" ] || continue 142 baseName=$( basename "${file}" | sed -E 's/^mdns-tcpdump.pcapng([0-9]+)$/mdns-tcpdump-\1.pcapng/' ) 143 gzip < "${file}" > "${workPath}/pcaps/${baseName}.gz" 144 done 145} 146 147#============================================================================================================================ 148# StopPacketCapture 149#============================================================================================================================ 150 151StopPacketCapture() 152{ 153 LogMsg "Stopping tcpdump." 154 kill -TERM "${tcpdumpPID}" 155 kill -TERM "${tcpdumpLoopbackPID}" 156} 157 158#============================================================================================================================ 159# RunInterfaceMulticastTests 160#============================================================================================================================ 161 162RunInterfaceMulticastTests() 163{ 164 local -r ifname=${1} 165 local -r allHostsV4=224.0.0.1 166 local -r allHostsV6=ff02::1 167 local -r mDNSV4=224.0.0.251 168 local -r mDNSV6=ff02::fb 169 local -r log="${workPath}/mcast-test-log-${ifname}.txt" 170 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' ) ) 171 serviceList+=( "${serviceTypesOfInterest[@]/%/.local.}" ) 172 serviceList=( $( IFS=$'\n' sort -f -u <<< "${serviceList[*]}" ) ) 173 174 LogOut "List of services: ${serviceList[*]}" >> "${log}" 175 176 # Ping IPv4 broadcast address. 177 178 local broadcastAddr=$( ifconfig "${ifname}" inet | awk '$5 == "broadcast" {print $6}' ) 179 if [ -n "${broadcastAddr}" ]; then 180 LogOut "Pinging ${broadcastAddr} on interface ${ifname}." >> "${log}" 181 ping -t 5 -b "${ifname}" "${broadcastAddr}" &> "${workPath}/ping-broadcast-${ifname}.txt" 182 else 183 LogOut "No IPv4 broadcast address for ${ifname}." >> "${log}" 184 fi 185 186 # Ping All Hosts IPv4 multicast address. 187 188 local routeOutput=$( route -n get -ifscope "${ifname}" "${allHostsV4}" 2> /dev/null ) 189 if [ -n "${routeOutput}" ]; then 190 LogOut "Pinging ${allHostsV4} on interface ${ifname}." >> "${log}" 191 ping -t 5 -b "${ifname}" "${allHostsV4}" &> "${workPath}/ping-all-hosts-${ifname}.txt" 192 else 193 LogOut "No route to ${allHostsV4} on interface ${ifname}." >> "${log}" 194 fi 195 196 # Ping mDNS IPv4 multicast address. 197 198 routeOutput=$( route -n get -ifscope "${ifname}" "${mDNSV4}" 2> /dev/null ) 199 if [ -n "${routeOutput}" ]; then 200 LogOut "Pinging ${mDNSV4} on interface ${ifname}." >> "${log}" 201 ping -t 5 -b "${ifname}" "${mDNSV4}" &> "${workPath}/ping-mDNS-${ifname}.txt" 202 else 203 LogOut "No route to ${mDNSV4} on interface ${ifname}." >> "${log}" 204 fi 205 206 # Ping All Hosts IPv6 multicast address. 207 208 routeOutput=$( route -n get -ifscope "${ifname}" -inet6 "${allHostsV6}" 2> /dev/null ) 209 if [ -n "${routeOutput}" ]; then 210 LogOut "Pinging ${allHostsV6} on interface ${ifname}." >> "${log}" 211 ping6 -c 6 -I "${ifname}" "${allHostsV6}" &> "${workPath}/ping6-all-hosts-${ifname}.txt" 212 else 213 LogOut "No route to ${allHostsV6} on interface ${ifname}." >> "${log}" 214 fi 215 216 # Ping mDNS IPv6 multicast address. 217 218 routeOutput=$( route -n get -ifscope "${ifname}" -inet6 "${mDNSV6}" 2> /dev/null ) 219 if [ -n "${routeOutput}" ]; then 220 LogOut "Pinging ${mDNSV6} on interface ${ifname}." >> "${log}" 221 ping6 -c 6 -I "${ifname}" "${mDNSV6}" &> "${workPath}/ping6-mDNS-${ifname}.txt" 222 else 223 LogOut "No route to ${mDNSV6} on interface ${ifname}." >> "${log}" 224 fi 225 226 # Send mDNS queries for services. 227 228 for service in "${serviceList[@]}"; do 229 LogOut "Sending mDNS queries for ${service} on interface ${ifname}." >> "${log}" 230 for(( i = 1; i <= 3; ++i )); do 231 printf "\n" 232 "${dnssdutil}" mdnsquery -i "${ifname}" -n "${service}" -t ptr -r 2 233 printf "\n" 234 "${dnssdutil}" mdnsquery -i "${ifname}" -n "${service}" -t ptr -r 1 --QU -p 5353 235 printf "\n" 236 done >> "${workPath}/mdnsquery-${ifname}.txt" 2>&1 237 done 238} 239 240#============================================================================================================================ 241# RunMulticastTests 242#============================================================================================================================ 243 244RunMulticastTests() 245{ 246 local -r interfaces=( $( ifconfig -l -u ) ) 247 local -r skipPrefixes=( ap awdl bridge ipsec llw nan p2p pdp_ip pktap UDC utun ) 248 local -a pids 249 local ifname 250 local skip 251 local pid 252 253 LogMsg "List of interfaces: ${interfaces[*]}" 254 for ifname in "${interfaces[@]}"; do 255 skip=false 256 for prefix in "${skipPrefixes[@]}"; do 257 if [[ ${ifname} =~ ^${prefix}[0-9]*$ ]]; then 258 skip=true 259 break 260 fi 261 done 262 263 if ! "${skip}"; then 264 ifconfig ${ifname} | egrep -q '\binet6?\b' 265 if [ $? -ne 0 ]; then 266 skip=true 267 fi 268 fi 269 270 if "${skip}"; then 271 continue 272 fi 273 274 LogMsg "Starting interface multicast tests for ${ifname}." 275 RunInterfaceMulticastTests "${ifname}" & pids+=( $! ) 276 done 277 278 LogMsg "Waiting for interface multicast tests to complete..." 279 for pid in "${pids[@]}"; do 280 wait "${pid}" 281 done 282 LogMsg "All interface multicast tests completed." 283} 284 285#============================================================================================================================ 286# RunBrowseTest 287#============================================================================================================================ 288 289RunBrowseTest() 290{ 291 local -a typeArgs 292 293 if [ "${#serviceTypesOfInterest[@]}" -gt 0 ]; then 294 for serviceType in "${serviceTypesOfInterest[@]}"; do 295 typeArgs+=( "-t" "${serviceType}" ) 296 done 297 298 LogMsg "Running dnssdutil browseAll command for service types of interest." 299 "${dnssdutil}" browseAll -A -d local -b 10 -c 10 "${typeArgs[@]}" &> "${workPath}/browseAll-STOI.txt" 300 fi 301 302 LogMsg "Running general dnssdutil browseAll command." 303 "${dnssdutil}" browseAll -A -d local -b 10 -c 10 &> "${workPath}/browseAll.txt" 304} 305 306#============================================================================================================================ 307# ArchiveLogs 308#============================================================================================================================ 309 310ArchiveLogs() 311{ 312 local parentDir='' 313 # First, check for the non-macOS sysdiagnose archive path, then check for the macOS sysdiagnose archive path. 314 for dir in '/var/mobile/Library/Logs/CrashReporter' '/var/tmp'; do 315 if [ -w "${dir}" ]; then 316 parentDir="${dir}" 317 break 318 fi 319 done 320 # If a writable path wasn't available, just use /tmp. 321 [ -n "${parentDir}" ] || parentDir='/tmp' 322 local -r workdir=$( basename "${workPath}" ) 323 local -r archivePath="${parentDir}/${workdir}.tar.gz" 324 LogMsg "Archiving logs." 325 echo "---" 326 tar -C "${tempPath}" -czf "${archivePath}" "${workdir}" 327 if [ -e "${archivePath}" ]; then 328 echo "Created log archive at ${archivePath}" 329 echo "*** Please run sysdiagnose NOW. ***" 330 echo "Attach both the log archive and the sysdiagnose archive to the radar." 331 if command -v open 2>&1 > /dev/null; then 332 open "${parentDir}" 333 fi 334 else 335 echo "Failed to create archive at ${archivePath}." 336 fi 337 echo "---" 338} 339 340#============================================================================================================================ 341# CreateWorkDirName 342#============================================================================================================================ 343 344CreateWorkDirName() 345{ 346 local suffix='' 347 local -r productName=$( sw_vers -productName ) 348 if [ -n "${productName}" ]; then 349 suffix+="_${productName}" 350 fi 351 352 local model='' 353 if command -v gestalt_query 2>&1 > /dev/null; then 354 model=$( gestalt_query -undecorated ProductType ) 355 else 356 model=$( sysctl -n hw.model ) 357 fi 358 model=${model//,/-} 359 if [ -n "${model}" ]; then 360 suffix+="_${model}" 361 fi 362 363 local -r buildVersion=$( sw_vers -buildVersion ) 364 if [ -n "${buildVersion}" ]; then 365 suffix+="_${buildVersion}" 366 fi 367 368 suffix=${suffix//[^A-Za-z0-9._-]/_} 369 370 printf "bonjour-mcast-diags_$( date '+%Y.%m.%d_%H-%M-%S%z' )${suffix}" 371} 372 373#============================================================================================================================ 374# main 375#============================================================================================================================ 376 377main() 378{ 379 while getopts ":s:hV" option; do 380 case "${option}" in 381 h) 382 PrintUsage 383 exit 0 384 ;; 385 s) 386 serviceType=$( awk '{print tolower($0)}' <<< "${OPTARG}" ) 387 if [[ ${serviceType} =~ ^_[-a-z0-9]*\._(tcp|udp)$ ]]; then 388 serviceTypesOfInterest+=( "${serviceType}" ) 389 else 390 ErrQuit "Service type '${OPTARG}' is malformed." 391 fi 392 ;; 393 V) 394 echo "$( basename "${script}" ) version ${version}" 395 exit 0 396 ;; 397 :) 398 ErrQuit "option '${OPTARG}' requires an argument." 399 ;; 400 *) 401 ErrQuit "unknown option '${OPTARG}'." 402 ;; 403 esac 404 done 405 406 [ "${OPTIND}" -gt "$#" ] || ErrQuit "unexpected argument \"${!OPTIND}\"." 407 408 if [ "${EUID}" -ne 0 ]; then 409 if command -v sudo 2>&1 > /dev/null; then 410 echo "Re-launching with sudo" 411 exec sudo "${script}" "$@" 412 else 413 ErrQuit "$( basename "${script}" ) needs to be run as root." 414 fi 415 fi 416 417 tempPath=$( mktemp -d -q ) || ErrQuit "Failed to make temp directory." 418 workPath="${tempPath}/$( CreateWorkDirName )" 419 mkdir "${workPath}" || ErrQuit "Failed to make work directory." 420 421 trap SignalHandler SIGINT SIGTERM 422 trap ExitHandler EXIT 423 424 LogMsg "About: $( basename "${script}" ) version ${version} ($( md5 -q "${script}" ))." 425 if [ "${dnssdutil}" != "dnssdutil" ]; then 426 if [ -x "$( which "${dnssdutil}" )" ]; then 427 LogMsg "Using $( "${dnssdutil}" -V ) at $( which "${dnssdutil}" )." 428 else 429 LogMsg "WARNING: dnssdutil (${dnssdutil}) isn't an executable." 430 fi 431 fi 432 433 serviceTypesOfInterest=( $( IFS=$'\n' sort -u <<< "${serviceTypesOfInterest[*]}" ) ) 434 435 GetStateDump 'before' 436 RunNetStat 437 StartPacketCapture 438 SaveExistingPacketCaptures 439 RunBrowseTest 440 RunMulticastTests 441 GetStateDump 'after' 442 StopPacketCapture 443 ArchiveLogs 444} 445 446main "$@" 447