blob: 212aa6b35f5b8e94cbc377820332bd5409b48501 [file] [log] [blame]
#!/bin/bash
# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
IFCONFIG=/bin/ifconfig
LSUSB=/usr/sbin/lsusb
LSPCI=/usr/sbin/lspci
IW=/usr/sbin/iw
ARP=/sbin/arp
ARPING=/sbin/arping
TLSDATE=/usr/bin/tlsdate
ETHTOOL=/usr/sbin/ethtool
anonymize_macs=${ANONYMIZE_MACS-yes}
fail_count=0
CROS_CERTS=/usr/share/chromeos-ca-certificates/
declare -A fn_entry
# Annotate function entry.
fn_enter () {
local fn="$1"
fn_entry["${fn}"]=1
echo "Entering $*"
}
# Annotate function entry. Also, return non-zero if function has
# been called more than once
fn_enter_once () {
local fn="$1"
[ -n "${fn_entry["${fn}"]}" ] && return 1
fn_enter "$@"
}
# Output pass/fail reports
pass () {
echo "PASS: $*"
}
fail () {
echo "FAIL: $*"
fail_count="$((fail_count+1))"
}
# Create a unique filename -- this is a "non secure" implementation (we would
# have used mktemp if that was the concern), but instead we want to create an
# easy way to tell which file is more recent. Also, make a symlink to the most
# recent file
unique_file () {
local out_template="$1"
out_file="$(echo "${out_template}" | \
sed -e 's/\*/'$(date +'%Y-%m-%d.%H:%M:%S')'/')"
link_file="$(echo "${out_template}" | sed -e 's/\*/latest/')"
rm -f "${out_file}" "${link_file}"
ln -s "$(basename $out_file)" "${link_file}"
echo "$out_file"
}
# Split a dotted decimal string
do_address_parts () {
(IFS=" ."; echo $1)
}
# Perform a mask (binary "&") between two dotted-decimal strings
do_netmask () {
local -a ip=($(do_address_parts "$1"))
local -a mask=($(do_address_parts "$2"))
local -a ret
for part in ${!ip[@]}; do
ret+=("$((ip[part] & mask[part]))")
done
(IFS=.; echo "${ret[*]}")
}
# Search an entry from the routing table, given the destination IP address
# @ip: Destination IP address
# @route_flags: Search for this value in the "Flags" column
# @part: Column from the netstat output to return
get_route () {
local ip="$1"
local route_flags="$2"
shift ; shift
netstat -nr | while read line; do
local -a netstat=(${line})
if [ "${netstat[3]}" = "${route_flags}" -a \
"$(do_netmask "${ip}" "${netstat[2]}")" = "${netstat[0]}" ] ; then
for part in "$@"; do
echo "${netstat["${part}"]}"
done
return 0
fi
done
return 1
}
# Return the interface through which traffic to given neigbor IP should go
get_if_route () {
get_route "$1" U 7
}
# Return the gateway IP to which traffic to given remote IP should be forwarded
get_gw_route () {
get_route "$1" UG 1 7
}
# Trace down a symbolic link until we reach a dead-end (or a real file)
get_tracelink () {
local file="$1"
local link_count=0
while [ -h "${file}" -o -e "${file}" ] ; do
ls -ld "${file}"
new_file="$(readlink -f "${file}")"
[ "${new_file}" = "${file}" ] && return
if [ "${link_count}" -ge 10 ] ; then
fail "Gave up after ${link_count} steps"
return
fi
link_count="$((link_count + 1))"
file="${new_file}"
done
fail "${file} does not exist"
}
# Return the IP addresses of the currently configured nameservers
get_nameservers () {
awk '/^nameserver/ {print $2}' /etc/resolv.conf
}
# Return list of interface names
get_iflist () {
ls /sys/class/net/ | egrep -v '^(lo|sit)'
}
# Return the interface name that has the default route
get_default_if () {
route -n | awk '/^0.0.0.0/ { print $2; exit 0 }'
}
# Return IP address of a given network interface
get_if_addr () {
ifc="$1"
${IFCONFIG} "${ifc}" | grep "inet addr" | sed -e 's/.*addr://; s/ .*//'
}
# Super-gross method for looking up IP address of a given hostname
get_host_addr () {
# TODO(pstew): super gross -- but there's no host/dig/nslookup/dnsquery
local host="$1"
ping -c1 "${host}" | head -1 | awk -v FS='[()]' '{print $2}'
}
# Find a the device name of a device given the driver's name
get_class_driver () {
local find_pat="$1"
local find_driver="$2"
for device in /sys/class/${find_pat}*; do
local driver="$(basename $(readlink "${device}/device/driver"))"
if [ "${driver}" = "${find_driver}" ] ; then
echo "$(basename "${device}")"
return 0
fi
done
}
# The "status" command can't be run as non-root, so we fake it here
get_status () {
pid="$(pgrep $1)"
if [ -n "${pid}" ] ; then
pass "$1 is running, pid ${pid}"
return 0
else
fail "$1 is stopped"
return 1
fi
}
# Join the rest of the arguments using the first argument as a delimiter.
join () {
local delim="$1"
shift
echo $(IFS="${delim}"; echo "$*")
}
# Compose a string from hex characters.
hex_to_string () {
local hex
for hex in "$@"; do
printf "\x${hex}"
done
printf "\n"
}
seconds_compare () {
local test_time="$1"
local reference_time="$2"
if [ $test_time -gt $reference_time ] ; then
echo "$((test_time - reference_time)) seconds from now"
else
echo "$((reference_time - test_time)) seconds ago"
fi
}
# Read a DHCPCD lease file
read_dhcp_lease () {
local file="$1"
local message_types=(UNKNOWN DISCOVER OFFER REQUEST DECLINE \
ACK NACK RELEASE INFORM)
local now="$(date +%s)"
local lease_start="$(stat -c %Y $file)"
local lease_age="$((now-lease_start))"
echo " Lease file age: $lease_age"
local lease_int=($(od --address-radix=n --format=u1 \
--output-duplicates ${file}))
local lease_hex=($(od --address-radix=n --format=x1 \
--output-duplicates ${file}))
echo " Client MAC address: $(join : ${lease_hex[*]:28:6})"
echo " Leased address: $(join . ${lease_int[*]:16:4})"
local server_address="$(join . ${lease_int[*]:20:4})"
if [ "${server_address}" != "0.0.0.0" ]; then
echo " Server address: ${server_address}"
fi
if [ "${lease_hex[*]:236:4}" != "63 82 53 63" ] ; then
echo " This lease file does not contain the DHCP magic number (RFC 2131)"
return
fi
local idx=240
while [ -n "${lease_int[$idx]}" -a -n "${lease_int[$((idx+1))]}" ] ; do
local opt="${lease_int[$idx]}"
local len="${lease_int[$((idx+1))]}"
local data_int=(${lease_int[*]:$((idx+2)):$len})
local data_hex=(${lease_hex[*]:$((idx+2)):$len})
idx="$((idx + 2 + len))"
case $opt in
0)
break
;;
1)
echo " Netmask: $(join . ${data_int[*]})"
;;
6)
local -a dns_servers
local server_index=0
while [ -n "${data_hex[$server_index]}" ] ; do
dns_server="$(join . "${data_int[@]:$server_index:4}")"
dns_servers=("${dns_servers[@]}" "${dns_server}")
server_index="$((server_index + 4))"
done
echo " Domain name servers: $(join , "${dns_servers[@]}")"
;;
15)
echo " Domain name: $(hex_to_string "${data_hex[@]}")"
;;
51)
local lease_time="$(printf "%d" 0x$(join '' "${data_hex[@]}"))"
local lease_time_comment="$(seconds_compare "$lease_time" "$lease_age")"
echo " Lease Time: ${lease_time} seconds (${lease_time_comment})"
;;
53)
echo " Message type: ${message_types[$data_int]-UNKNOWN}"
;;
58)
local renew_time="$(printf "%d" 0x$(join '' "${data_hex[@]}"))"
local renew_time_comment="$(seconds_compare "$renew_time" "$lease_age")"
echo " Renew Time: $renew_time seconds (${renew_time_comment})"
;;
59)
local rebind_time="$(printf "%d" 0x$(join '' "${data_hex[@]}"))"
local rebind_time_comment="$(seconds_compare \
"$rebind_time" "$lease_age")"
echo " Rebinding Time: $rebind_time seconds (${rebind_time_comment})"
;;
esac
done
}
# Anonymize MAC addresses so they are not transmitted
mac_anonymize () {
if [ "${anonymize_macs}" != "yes" ]; then
cat
return
fi
sed -e \
's/\([0-9A-Fa-f][0-9A-Fa-f]\):\([0-9A-Fa-f][0-9A-Fa-f]\):\([0-9A-Fa-f][0-9A-Fa-f]\):[0-9A-Fa-f][0-9A-Fa-f]:[0-9A-Fa-f][0-9A-Fa-f]:[0-9A-Fa-f][0-9A-Fa-f]/\1:\2:\3:##:##:##/' \
-e 's/\([^0-9a-fA-F][0-9a-fA-F]\{8,11\}\)[0-9a-fA-F]\{4\}$/\1####/' \
-e 's/\([^0-9a-fA-F][0-9a-fA-F]\{8\}\)[0-9a-fA-F]\{4\}\([^0-9a-fA-F]\)/\1####\2/' \
-e 's/\(\/[0-9a-fA-F]\{8\}\)[0-9a-fA-F]\{4\}\([^0-9a-fA-F]\)/\1####\2/' \
-e 's/\(\/.*Passphrase \).*/\1[removed]/' \
-e 's/\(\/.*PSK \).*/\1[removed]/' \
-e 's/\(\/.*Password \).*/\1[removed]/' \
-e 's/\(UUID: \).*/\1[removed]/'
}
# Read the logs
tail_logs () {
files="$(for f in /var/log/messages{.2,.1,} ; do
[ -f "${f}" ] && echo "${f}";
done)"
if [ -n "$*" ] ; then
tail "$@" ${files} | mac_anonymize
else
cat ${files} | mac_anonymize
fi
}
# List all ethernet devices and their unique manufacturer strings
get_device_list () {
fn_enter_once "${FUNCNAME}" || return
echo "Device list:"
for ifc in $(get_iflist); do
dir="$(readlink -f "/sys/class/net/${ifc}/device")"
driver="$(basename "$(readlink "${dir}/driver")")"
if expr "${dir}" : '.*usb' > /dev/null; then
type=usb
vendor="$(cat < "${dir}/../idVendor")"
device="$(cat "${dir}/../idProduct")"
elif expr "${dir}" : '.*pci' > /dev/null; then
type=pci
vendor="$(sed -e 's/0x//' < "${dir}/vendor")"
device="$(sed -e 's/0x//' < "${dir}/device")"
else
type=unknown
fi
echo -e "${ifc}\t${type}:${device}:${vendor}\t${driver}"
done
}
# List all USB and PCI devices
diag_devs () {
fn_enter_once "${FUNCNAME}" || return
${LSUSB}
${LSPCI}
}
# Check whether we are in sync with the time server
diag_date () {
local host="$1"
local proxy="$2"
local cmd=()
fn_enter "${FUNCNAME}" "$@"
echo "Local time of day: $(date)"
cmd=( "${TLSDATE}" -nv -H "${host}" )
if [[ -n "${proxy}" ]]; then
cmd+=( -x "${proxy}" )
fi
date_out="$( "${cmd[@]}" 2>&1 >/dev/null )"
date_exit="$?"
offset="$(echo "$date_out" | grep 'difference is' | cut -f8 -d' ')"
if [ "${date_exit}" != 0 ]; then
fail "Unable to get date via tlsdate from ${host}: ${date_out}"
elif [ "${offset}" = 0 ] ; then
pass "Time appears to be correct"
elif [ "${offset}" -lt -3600 -o "${offset}" -gt 3600 ] ; then
fail "Time offset = ${offset}"
else
pass "Time offset is small (${offset})"
fi
}
# Try to detect IP address collisions
diag_ip_collision () {
fn_enter "${FUNCNAME}" "$@"
local ifc="$1"
ip="$(get_if_addr "${ifc}")"
if [ -z "${ip}" ]; then
fail "${ifc} does not have IP address"
return 1
fi
if ! "${ARPING}" -c 3 -I "${ifc}" -D "${ip}"; then
fail "IP Address Collision Detected!"
return 1
fi
return 0
}
# Make sure we have an ARP table entry for $ip through $ifc
diag_arp () {
fn_enter "${FUNCNAME}" "$@"
local ip="$1"
local ifc="$2"
local failures=0
if [ -n "${ip}" ] ; then
arp="$(${ARP} -an | awk '/('${ip}').*'${ifc}'$/ { print $4 }')"
if [ -z "${arp}" ]; then
fail "Arp table does not contain entry for ${ip}"
failures="$((failures + 1))"
elif [ "${arp}" = "<incomplete>" ] ; then
fail "Can't arp for ${ip}"
diag_ip_collision "${ifc}"
failures="$((failures + 1))"
else
pass "ARP for ${ip} is ${arp}" | mac_anonymize
fi
fi
fn_enter_once "${FUNCNAME}_table" && "${ARP}" -an | mac_anonymize
return "${failures}"
}
show_routes () {
netstat -nr
echo "Raw routing tables:"
cat /proc/net/route
cat /proc/net/ipv6_route
}
# Make sure we have a route to each host
diag_route () {
fn_enter "${FUNCNAME}" "$@"
local failures=0
for ip in "$@"; do
ifinfo="$(get_if_route "${ip}")"
gwinfo="$(get_gw_route "${ip}")"
if [ -n "${ifinfo}" ]; then
diag_arp "${ip}" "${ifinfo}" || failures="$((failures + 1))"
elif [ -n "${gwinfo}" ]; then
diag_arp "${gwinfo}" || failures="$((failures + 1))"
else
fail "No route to host ${ip}"
diag_ifall || failures="$((failures + 1))"
fi
done
fn_enter_once "${FUNCNAME}_table" && show_routes
return "${failures}"
}
# Figure whether we have IP connectivity to each host
diag_ping () {
fn_enter "${FUNCNAME}" "$@"
local failures=0
for ip in "$@"; do
if ping -c 3 "${ip}" | grep -q '0 received'; then
fail "Ping to ${ip} failed"
if diag_route "${ip}"; then
fail "We were able to reach the router but we cannot get packets to"
fail "any machines on the other side. The problem is probably with"
fail "the router configuration or connectivity and not this system."
else
fail "We were successfully able to join the network, but we cannot"
fail "seem to reach the router right now. This is either a link"
fail "level issue, the router is down, or our lease is expired."
diag_linkall
diag_dhcp_lease
fi
failures="$((failures + 1))"
else
pass "address ${ip}: ping OK"
fi
done
return "${failures}"
}
# Report the last DHCP interaction for a given network interface
diag_dhcp () {
fn_enter_once "${FUNCNAME}" || return
local ifc="$1"
shift
local -a dhcp_event=($(tail_logs "$@" | \
grep "dhcpcd.*event.*on interface ${ifc}" | \
sed -e 's/^\([^ ]*\).*event \([A-Z]*\).*/\1 \2/' | tail -1))
last_dhcpcd_event_time="${dhcp_event[0]}"
last_dhcpcd_event_type="${dhcp_event[1]}"
case ${last_dhcpcd_event_type} in
RENEW|BOUND|REBOOT)
pass "${ifc}: last dhcp event was successful:" \
"${last_dhcpcd_event_type} at ${last_dhcpcd_event_time}"
;;
*)
fail "${ifc}: last dhcp event was" \
"${last_dhcpd_event_type} at ${last_dhcpcd_event_time}"
fail "This could be a link-level connectivity problem"
;;
esac
}
# Perform diagnostics on an 802.11 interface
diag_link_wifi () {
fn_enter "${FUNCNAME}" "$@"
ifc="$1"
echo "wifi stats (all interfaces):"
diag_wifi
last_connect="$(grep -n "${ifc}: connect SSID" /var/log/messages{.1,})"
if [ -z "${last_connect}" ] ; then
fail "${ifc}: no recent 802.11 connection attempts"
return 1
fi
local -a lastconn_info=($(IFS=" :"; echo "${last_connect}"))
file="${lastconn_info[0]}"
line="${lastconn_info[1]}"
size="$(wc -l "${file}" | awk '{print $1}')"
tail="$((size - line + 1))"
state_changes="$(tail -"${tail}" "${file}" | grep 'state change')"
last_state_change="$(grep 'state change' "${file}" | tail -1 | \
sed -e 's/.*state change //')"
echo "${ifc}: last flimflam state change: ${last_state_change}"
if echo "${state_changes}" | grep -q ' -> COMPLETED'; then
pass "${ifc}: last connection attempt appears successful"
return 0
elif echo "${state_changes}" | \
grep -q '4WAY_HANDSHAKE -> DISCONNECTED'; then
fail "${ifc}: It appears that the wrong WPA PSK was used"
fi
echo "wpa_supplicant network blocks:"
wpa_cli list_networks
diag_dhcp "${ifc}" "-${tail}" "${file}"
}
# Print out WiFi debugging info
diag_wifi () {
fn_enter_once "${FUNCNAME}" || return
# Call out to debugd (which in turn calls this script's diag_wifi_internal
# function with the debugd privileges needed to read debugfs contents).
dbus-send --system --print-reply --fixed --dest=org.chromium.debugd \
/org/chromium/debugd org.chromium.debugd.GetLog "string:wifi_status"
}
diag_wifi_internal () {
# Only callable by debugd or root, since debugfs is locked down.
for dir in /sys/kernel/debug/ieee80211/phy*/ath9k; do
[ -d "${dir}" ] && head -1000 "${dir}"/{dma,interrupt,recv,xmit,samples} | \
mac_anonymize
done
for ifc in $(get_iflist); do
if is_wifi "${ifc}" ; then
echo "iw dev ${ifc} survey dump:"
"${IW}" dev "${ifc}" survey dump | mac_anonymize
echo "iw dev ${ifc} station dump:"
"${IW}" dev "${ifc}" station dump | mac_anonymize
echo "iw dev ${ifc} scan dump:"
"${IW}" dev "${ifc}" scan dump | mac_anonymize | grep -v SSID
echo "iw dev ${ifc} link:"
"${IW}" dev "${ifc}" link | mac_anonymize | grep -v SSID
fi
done
}
# Perform diagnostics on a WAN interface
diag_link_cellular () {
fn_enter "${FUNCNAME}" "$@"
diag_cellular_dbus
diag_devs
qcdev="$(get_class_driver tty/ttyUSB qcserial)"
if [ -n "${qcdev}" ] ; then
echo "QCSerial device is ${qcdev}"
fi
tail_logs | grep "QDL unable" | tail -3
}
diag_link_wired () {
fn_enter "${FUNCNAME}" "$@"
local ifc="$1"
echo "Output from ethtool ${ifc}:"
"${ETHTOOL}" "${ifc}"
return 0
}
# Tests to check to see if this is a modem. Very abstract adaptation
# from flimflam's tests
is_modem () {
local ifc="$1"
driver="$(basename "$(readlink "/sys/class/net/${ifc}/device/driver")")"
# Whitelist certain device types
[ "${driver}" = "QCUSBNet2k" ] && return 0
# See if there is a TTY device that is associated the same USB device
dev_root="$(readlink -f /sys/class/net/${ifc}/device)"
if expr "${dev_root}" : '.*usb' > /dev/null; then
local -a tty_devs=($(echo $(dirname ${dev_root})/*/*/tty))
[ -e "${tty_devs[0]}" ] && return 0
fi
return 1
}
is_wifi () {
local ifc="$1"
if expr ${ifc} : wlan > /dev/null || \
[ -e "/sys/class/net/${ifc}/phy80211" ] ; then
return 0
fi
return 1
}
# Perform type-specific link diagnostics on a network interface
diag_link () {
fn_enter "${FUNCNAME}" "$@"
ifc="$1"
if [ "$(cat "/sys/class/net/${ifc}/carrier")" -eq 1 ]; then
pass "${ifc}: link detected"
else
pass "${ifc}: link not detected"
fi
if is_wifi "${ifc}" ; then
diag_link_wifi "${ifc}"
elif is_modem "${ifc}"; then
diag_link_cellular "${ifc}"
else
diag_link_wired "${ifc}"
fi
echo "Last 10 kernel messages for ${ifc}:"
tail_logs | grep "kernel:.*${ifc}" | tail -10
}
diag_linkall () {
fn_enter_once "${FUNCNAME}" || return
for ifc in $(get_iflist); do
diag_link "${ifc}"
done
}
# Perform generic diagnostics on a network interface
diag_if () {
fn_enter "${FUNCNAME}" "$@"
ifc="$1"
config="$("${IFCONFIG}" "${ifc}")"
ret=0
${IFCONFIG} "${ifc}" | mac_anonymize
if ! echo "${config}" | grep -q ' UP '; then
fail "${ifc} is not listed as up"
ret=1
elif ! echo "${config}" | grep -q ' RUNNING '; then
fail "${ifc} is not listed as running"
ret=1
fi
addr="$(get_if_addr "${ifc}")"
if [ -n "${addr}" ]; then
pass "${ifc} assigned IP address ${addr}"
diag_dhcp "${ifc}"
else
fail "${ifc} is not assigned an IP address"
ret=1
fi
diag_link "${ifc}"
return "${ret}"
}
# Query interface status on all network interfaces, and return an error if
# none of them appear to be up
diag_ifall () {
fn_enter_once "${FUNCNAME}" || return
local good_ifs=0
for ifc in $(get_iflist); do
diag_if "${ifc}" && good_ifs="$((good_ifs + 1))"
done
if [ "${good_ifs}" -eq 0 ] ; then
fail "No good interfaces found. You are not connected to a network."
get_status shill
get_status wpa_supplicant
get_status cromo
return 1
fi
diag_flimflam
return 0
}
# Diagnose nameserver connectivity
diag_nameservers () {
fn_enter_once "${FUNCNAME}" || return
nameservers="$(get_nameservers)"
if [ -z "${nameservers}" ] ; then
fail "No nameservers -- this is either a network failure or net misconfig"
get_tracelink /etc/resolv.conf
diag_ifall
else
echo "Testing connectivity to nameservers"
if diag_ping "$(get_nameservers)"; then
fail "We can reach the nameservers but were not able to resolve hostnames"
fail "You may be behind a captive portal or there may be a DNS"
fail "configuration problem"
fi
fi
}
# See if we can connect to a given host
diag_connectivity () {
fn_enter_once "${FUNCNAME}" "$@" || exit 0
local host="$1"
local ip="$(get_host_addr "$host")"
if [ -z "${ip}" ] ; then
fail "Could not lookup host ${host}"
diag_route
diag_nameservers
return 1
fi
if diag_ping "${ip}"; then
fail "We were able to ping ${host} but were not able to connect to it."
fail "This probably means that you are behind a portal or (unlikely)"
fail "${host} is encountering technical difficulties"
fi
}
# Get information from flimflam about its state over D-Bus.
dbus_get_object_properties () {
local entity="$1"
local object_type="$2"
local object_path="${3-/}"
dbus-send --fixed --system --dest="${entity}" --print-reply \
"${object_path}" "${entity}.${object_type}.GetProperties"
}
dbus_get_object_list () {
local entity="$1"
local parent="$2"
local child="$3"
local parent_path="${4-/}"
# The child (property) we are looking for is something like "Devices"
# or "LinkMonitorResponseTime". The sed manipulation below will return
# the list of values for each entry. For example, with an output that
# looks like:
# /8/Devices/0 /device/wlan0
# /8/Devices/1 /device/eth1
# /8/Devices/2 /device/eth0
# and a search for "Devices", we'd return a newline separated
# "/device/wlan0 /device/eth1 /device/eth0"
dbus_get_object_properties "${entity}" "${parent}" "${parent_path}" | \
awk '/^\/[0-9]+\/'${child}'[\/ ]/ { print $2 }'
}
diag_flimflam_dbus () {
fn_enter_once "${FUNCNAME}" "$@"
local ff="org.chromium.flimflam"
if [ -z "$1" ] ; then
echo "Flimflam Manager:"
dbus_get_object_properties "${ff}" Manager / | mac_anonymize
# For each Service defined on the Manager, list its properties
diag_flimflam_dbus Manager Service
# For each Device defined in the Manager, list its properties
diag_flimflam_dbus Manager Device
else
local parent="$1"
local child="$2"
local parent_path="${3-/}"
for path in \
$(dbus_get_object_list "${ff}" "${parent}" "${child}s" \
"${parent_path}"); do
echo "${child} ${path}" | mac_anonymize
dbus_get_object_properties "${ff}" "${child}" "${path}" | mac_anonymize
if [ "${child}" = Device ] ; then
# For each Network defined in each Device, list its properties
diag_flimflam_dbus Device Network "${path}"
fi
done
fi
}
# Get information from ModemManager about its state over D-Bus
diag_cellular_dbus () {
fn_enter "${FUNCNAME}" "$@"
dbus-send --fixed --system --print-reply --dest=org.chromium.ModemManager \
/org/chromium/ModemManager \
org.freedesktop.ModemManager.EnumerateDevices || \
fail "Could not contact ModemManager!"
}
# Read lease information
diag_dhcp_lease () {
fn_enter "${FUNCNAME}" "$@"
local lease_dir=/var/lib/dhcpcd
for file in ${lease_dir}/*.lease; do
echo "DHCP Lease file ${file}"
read_dhcp_lease "${file}" | mac_anonymize
done
}
# Probe process status of flimflam and its process descendants
diag_flimflam () {
fn_enter_once "${FUNCNAME}" || return
local status="$(get_status shill)"
echo "${status}"
if ! echo "${status}" | fgrep -q 'running'; then
return
fi
local pid="$(echo ${status} | awk '{print $4}')"
ps jx | awk '/^ *'${pid}'/ {print $10,$11,$12}' | sort | uniq -c
echo "Listing of /run/shill"
ls -al /run/shill
diag_flimflam_dbus
}
run_hosts_webget() {
local host="$1"
local proxy="$2"
local cmd=()
local timeout_secs=10
cmd=( curl -s "https://${host}" --max-time ${timeout_secs} --capath "${CROS_CERTS}" )
if [[ -n "${proxy}" ]]; then
cmd+=( --proxy "${proxy}" )
fi
printf "checking %-40s" "${host}..."
"${cmd[@]}" >/dev/null
}
# Try to connect to a host via SSL, diagnose failures.
diag_hosts () {
local host
local proxy="$1"
# Hosts is pulled from
# https://support.google.com/chrome/a/answer/3504942?hl=en#sslinspection
local hosts=(
"accounts.google.com"
"accounts.gstatic.com"
"accounts.youtube.com"
"clients1.google.com"
"clients2.google.com"
"clients3.google.com"
"clients4.google.com"
"commondatastorage.googleapis.com"
"dl.google.com"
"gweb-gettingstartedguide.appspot.com"
"m.google.com"
"safebrowsing-cache.google.com"
"safebrowsing.google.com"
"ssl.gstatic.com"
"storage.googleapis.com"
"tools.google.com"
"www.googleapis.com"
"www.gstatic.com"
"chrome.google.com"
"clients2.googleusercontent.com"
"lh3.ggpht.com"
"lh4.ggpht.com"
"lh5.ggpht.com"
"lh6.ggpht.com"
)
for host in "${hosts[@]}"; do
run_hosts_webget "${host}" "${proxy}"
local err=$?
case ${err} in
0)
echo "PASS"
;;
5)
echo "FAIL: proxy resolution error"
;;
6)
echo "FAIL: DNS resolution error"
;;
7)
echo "FAIL: connection error"
;;
28)
echo "FAIL: connection timed out"
;;
35)
echo "FAIL: SSL connection error"
;;
60)
echo "FAIL: non-Google SSL/TLS certificate"
;;
*)
echo "FAIL: unknown error ${err}"
;;
esac
done
}
run_webget () {
local url="$1"
local proxy="$2"
local cmd=()
local timeout_secs=10
cmd=( curl -s "${url}" --max-time "${timeout_secs}" )
if [[ -n "${proxy}" ]]; then
cmd+=( --proxy "${proxy}" )
fi
echo "Trying to contact ${url} ... (waiting up to ${timeout_secs} seconds)"
"${cmd[@]}" >/dev/null
}
# Try to connect to a host via SSL, diagnose failures
diag_webget () {
local host="$1"
local url="https://${host}"
local proxy="$2"
run_webget "${url}" "${proxy}"
local err="$?"
case ${err} in
0)
pass "We can get to ${url} just fine"
;;
6)
# DNS resolution error
fail "Got DNS resolution error -- trying to debug nameservers"
diag_nameservers
;;
7)
# Failed to connect to host
fail "Got connection error -- trying to debug connection to host"
diag_connectivity "${host}"
;;
28)
# Operation timed out
fail "Operation timed out during connection to ${host}"
diag_connectivity "${host}"
diag_ifall
;;
35)
# SSL connect error. The SSL handshaking failed
fail "SSL connect error. The SSL handshaking failed"
diag_connectivity "${host}"
diag_ifall
;;
60)
# Peer certificate cannot be authenticated with known CA certificates
fail "Peer cert not authenticated -- probably a captive portal!"
;;
*)
fail "Encountered unhandled curl error ${err}"
;;
esac
if [ "${err}" -ne 0 ] ; then
get_device_list
diag_flimflam
fi
return "$err"
}
# Do standard run of tests
diag_run () {
local host="$1"
local proxy="$2"
if run_webget "https://${host}" "${proxy}"; then
pass "Loaded $host via HTTPS"
elif run_webget "http://${host}" "${proxy}"; then
pass "Loaded $host via HTTP (ignore any tlsdate failure below)"
else
diag_webget "${host}" "${proxy}"
fi
diag_date "${host}" "${proxy}"
diag_gateway_latency "${host}"
}
monitor_get_stats () {
local tmp_file="$(mktemp /tmp/iwlink.XXXXX)"
local err_file="$(mktemp /tmp/iwlink.XXXXX)"
"${IW}" dev "${mon_wlan_device}" link > "${tmp_file}"
if ! grep -q 'Connected to' "${tmp_file}"; then
values_reset=0
unlink "${tmp_file}"
unlink "${err_file}"
return
fi
for param in ${mon_link_params[@]}; do
val="$(grep "${param}" "${tmp_file}" | \
sed -e "s/.*${param}:*[ ]*\([^ ]*\).*/\1/")"
mon_current["${param}"]="${val}"
done
"${IW}" dev "${mon_wlan_device}" station dump 2>"${err_file}" > "${tmp_file}"
if [ "$?" -ne 0 -o -s "${err_file}" ] ; then
values_reset=0
unlink "${tmp_file}"
unlink "${err_file}"
return
fi
for param in ${mon_station_params[@]}; do
val="$(grep "${param}" "${tmp_file}" | \
sed -e "s/.*${param}:*[ ]*\([^ ]*\).*/\1/")"
mon_current["${param}"]="${val}"
done
"${IW}" dev "${mon_wlan_device}" survey dump 2>"${err_file}" | \
grep -A6 "${mon_current[freq]}" > "${tmp_file}"
if [ "$?" -ne 0 -o -s "${err_file}" ] ; then
values_reset=0
unlink "${tmp_file}"
unlink "${err_file}"
return
fi
for param in ${mon_survey_params[@]}; do
val="$(grep "${param}" "${tmp_file}" | \
sed -e "s/.*${param}:*[ ]*\([^ ]*\).*/\1/")"
mon_current["${param}"]="${val}"
done
echo -n "$(date +%s)"
for param in ${mon_delta_params[@]}; do
if [ -n "${values_reset}" ] ; then
prev=0
else
prev="${mon_last["${param}"]-0}"
fi
val="$((mon_current[${param}] - $prev))"
mon_last["${param}"]="${mon_current["${param}"]}"
echo -n " ${param}:${val}"
done
for param in ${mon_static_params[@]}; do
echo -n " ${param}:${mon_current[${param}]}"
done
echo
values_reset=
unlink "${tmp_file}"
unlink "${err_file}"
}
monitor_cleanup () {
kill "${mon_event_proc}"
[ -n "${mon_msgs_proc}" ] && kill "${mon_msgs_proc}"
echo -n TOTALS:
for param in ${mon_delta_params[@]} ${mon_static_params[@]}; do
echo -n " ${param}:${mon_current[${param}]}"
done
exit 0
}
diag_wifi_monitor () {
if ! [ -d /sys/bus/pci/drivers/ath9k ] ; then
echo "Unfortunately this script only spports ath9k at the moment."
return
fi
declare -A mon_last
declare -A mon_current
mon_wlan_device=wlan0
declare -a mon_link_params
mon_link_params=( freq signal 'tx.bitrate' 'dtim.period' 'beacon.int' SSID \
Connected.to )
declare -a mon_delta_station_params
declare -a mon_static_station_params
declare -a mon_station_params
mon_delta_station_params=( 'rx.bytes' 'tx.bytes' 'rx.packets' \
'tx.packets' 'tx.retries' 'tx.failed' )
mon_static_station_params=( 'inactive.time' 'signal.avg' )
mon_station_params=("${mon_static_station_params[@]}" \
"${mon_delta_station_params[@]}")
declare -a mon_delta_survey_params
declare -a mon_static_survey_params
declare -a mon_survey_params
mon_delta_survey_params=( 'channel.active.time' 'channel.busy.time' \
'channel.receive.time' 'channel.transmit.time' )
mon_static_survey_params=( noise )
mon_survey_params=("${mon_static_survey_params[@]}" \
"${mon_delta_survey_params[@]}")
mon_static_params=("${mon_static_survey_params[@]}" "${mon_link_params[@]}" \
"${mon_static_station_params[@]}")
mon_delta_params=("${mon_delta_survey_params[@]}" \
"${mon_delta_station_params[@]}")
values_reset=0
trap monitor_cleanup SIGINT SIGTERM
"${IW}" event -t &
mon_event_proc="$!"
mon_debug_file=/sys/kernel/debug/ieee80211/phy0/ath9k/debug
if [ -f "${mon_debug_file}" -a -r /proc/kmsg ] ; then
[ -w "${mon_debug_file}" ] && echo 0x440 > "${mon_debug_file}"
# NB: Even if we can't write to the flag, debug might already enabled
egrep 'ath:.*(cal|ANI)' /proc/kmsg &
mon_msgs_proc="$!"
fi
while sleep 1; do
monitor_get_stats
done
}
diag_gateway_latency () {
local ff="org.chromium.flimflam"
local services=($(dbus_get_object_list "${ff}" Manager Services /))
local service="${services[0]}"
local latency_threshold=800 # milliseconds
if [ -z "${service}" ]; then
fail "Found no services."
return
fi
local device="$(dbus_get_object_list "${ff}" Service Device "${service}")"
if [ -z "${device}" ]; then
fail "Service ${service} does not have an associated Device."
return
fi
local monitor_val="$(dbus_get_object_list "${ff}" Device \
LinkMonitorResponseTime "${device}")"
if [ -z "${monitor_val}" ]; then
echo "Service ${service} on device ${device} has no current latency value"
return
fi
message="Current LinkMonitor latency for ${device} is ${monitor_val}ms"
if [ "${monitor_val}" -le "${latency_threshold}" ]; then
pass "$message"
else
fail "$message"
fi
}
valid_host () {
local host="$1"
# Allow things that look like valid IPv4/IPv6/DNS. This doesn't deal with
# fully validating the forms, just reject grossly invalid ones. We'll still
# rely on the network to do the final resolution.
# Note: This probably doesn't support IDN, so people will have to handle the
# punycode transformations themselves.
echo "${host}" | grep -q -E '^[-A-Za-z0-9.:]+$'
}
usage () {
echo "Usage: $0 [--date|--flimflam|--link|--show-macs|--wifi|--help|"
echo " --wifi-mon] [host]"
echo " --date: Diagnose time-of-day"
echo " ([host] must support SSL)"
echo " --dhcp: Display DHCP information"
echo " --flimflam: Diagnose flimflam status"
echo " --hosts: Diagnose SSL connection to Google hosts"
echo " --interface: Diagnose interface status"
echo " --latency: Diagnose link-latency to default gateway"
echo " --link: Diagnose all network links"
echo " --no-log: Do not log output"
echo " --proxy: Specify proxy to use with tests"
echo " --route: Diagnose routes to each host"
echo " --show-macs: Display full MAC addresses"
echo " --wifi: Display driver-specific debugging information"
echo " --wifi-mon: Monitor WiFi performance metrics"
echo " --help: Display this message"
echo " [host] Hostname to perform web access test on " \
"(default: clients3.google.com)"
}
check_no_arg () {
if [[ "$2" == "yes" ]]; then
echo "Option $1 does not take any arguments"
usage
exit 1
fi
}
main () {
local test_host=clients3.google.com
local proxy
local option_and_arg_provided=no
local option
local param
local rest=()
while [[ $# -gt 0 ]]; do
param="$1"
shift
# Arguments that modify the behavior of diagnostic modules
# should be placed here. This ensures that they are processed
# before running the diagnostics.
case ${param} in
--proxy)
proxy="$1"
if [[ "${proxy}" != *://* ]]; then
echo "proxy needs a protocol like http://"
exit 1
fi
shift
;;
--show-macs)
anonymize_macs=no
;;
--no-log)
# Handled below before starting main
;;
*)
rest+=( "${param}" )
;;
esac
done
case ${#rest[@]} in
0)
diag_run "${test_host}" "${proxy}"
return
;;
1)
if [[ ${rest[0]} == --* ]]; then
option=${rest[0]}
else
test_host=${rest[0]}
if ! valid_host "${test_host}"; then
echo "Invalid host: ${test_host}"
exit 1
fi
diag_run "${test_host}" "${proxy}"
return
fi
;;
2)
option=${rest[0]}
test_host=${rest[1]}
if ! valid_host "${test_host}"; then
echo "Invalid host: ${test_host}"
exit 1
fi
option_and_arg_provided=yes
;;
*)
echo "Too many arguments"
usage
exit 1
;;
esac
if [[ -n "${option}" ]]; then
case ${option} in
--date)
diag_date "${test_host}" "${proxy}"
;;
--dhcp)
check_no_arg "${option}" "${option_and_arg_provided}"
diag_dhcp_lease
;;
--flimflam)
check_no_arg "${option}" "${option_and_arg_provided}"
diag_flimflam
;;
--hosts)
diag_hosts "${proxy}"
;;
--interface)
check_no_arg "${option}" "${option_and_arg_provided}"
diag_ifall
;;
--latency)
check_no_arg "${option}" "${option_and_arg_provided}"
diag_gateway_latency
;;
--link)
check_no_arg "${option}" "${option_and_arg_provided}"
diag_linkall
;;
--route)
diag_route "${test_host}"
;;
--wifi)
check_no_arg "${option}" "${option_and_arg_provided}"
diag_wifi
;;
--wifi-internal)
# Only called by debugd.
check_no_arg "${option}" "${option_and_arg_provided}"
diag_wifi_internal
exit 0
;;
--wifi-mon)
check_no_arg "${option}" "${option_and_arg_provided}"
diag_wifi_monitor
;;
--help|-help|-h)
check_no_arg "${option}" "${option_and_arg_provided}"
usage
exit 0
;;
*)
echo "Unknown option: $option"
usage
exit 1
;;
esac
exit "${fail_count}"
fi
}
if [ -d "/home/${USER}/user/Downloads" ] && \
! expr "$*" : '.*--no-log' >/dev/null; then
# Log this output to somewhere the user can upload from
main "$@" 2>&1 | \
tee "$(unique_file \
"/home/${USER}/user/Downloads/network_diagnostics_*.txt")"
else
main "$@"
fi
exit "${fail_count}"