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