| #!/bin/bash | |
| # Copyright 2010 The ChromiumOS Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| echo "$0: This command is deprecated." >&2 | |
| echo "$0: Please see https://issuetracker.google.com/267537630" >&2 | |
| exit 1 | |
| # Use $PATH lookup for tools. Add sbin in case script is run by non-root. | |
| PATH+=:/sbin:/usr/sbin | |
| IFCONFIG=ifconfig | |
| LSUSB=lsusb | |
| LSPCI=lspci | |
| IW=iw | |
| ARP=arp | |
| ARPING=arping | |
| TLSDATE=tlsdate | |
| ETHTOOL=ethtool | |
| anonymize_output="${ANONYMIZE_OUTPUT-no}" | |
| fail_count=0 | |
| CROS_CERTS=/usr/share/chromeos-ca-certificates/ | |
| declare -A fn_entry | |
| # Annotate function entry. Function name is provided by caller. | |
| _fn_enter() { | |
| local fn="$1" | |
| fn_entry["${fn}"]=1 | |
| echo "### Entering $*" | |
| } | |
| # Annotate function entry. Function name is automatically the parent. | |
| fn_enter() { | |
| _fn_enter "${FUNCNAME[1]}" "$@" | |
| } | |
| # Annotate function entry. Function name is provided by caller. | |
| # 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 "$@" | |
| } | |
| # Annotate function entry. Function name is automatically the parent. | |
| # Also, return non-zero if function has been called more than once. | |
| fn_enter_once() { | |
| _fn_enter_once "${FUNCNAME[1]}" "$@" | |
| } | |
| # Output pass/fail reports | |
| pass() { | |
| echo "PASS: $*" | |
| } | |
| fail() { | |
| echo "FAIL: $*" | |
| fail_count="$((fail_count+1))" | |
| } | |
| # 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 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() { | |
| local pid | |
| 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 PII (except MAC addresses) so they are not transmitted. | |
| output_anonymize() { | |
| if [ "${anonymize_output}" != "yes" ]; then | |
| cat | |
| return | |
| fi | |
| sed -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} | output_anonymize | |
| else | |
| cat ${files} | output_anonymize | |
| fi | |
| } | |
| # List all ethernet devices and their unique manufacturer strings | |
| get_device_list() { | |
| fn_enter_once || 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 || 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 "$@" | |
| 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 "$@" | |
| 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 "$@" | |
| local ip="$1" | |
| local ifc="$2" | |
| local failures=0 | |
| if [ -n "${ip}" ] ; then | |
| # Example arp output: | |
| # 10.0.2.2 ether 52:55:0a:00:02:02 C eth0 | |
| arp="$(${ARP} -en -i "${ifc}" | \ | |
| awk -v ip="${ip}" '$1 == ip { print $3 }')" | |
| 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}" | output_anonymize | |
| fi | |
| fi | |
| _fn_enter_once "${FUNCNAME[0]}_table" && "${ARP}" -an | output_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 "$@" | |
| 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[0]}_table" && show_routes | |
| return "${failures}" | |
| } | |
| # Figure whether we have IP connectivity to each host | |
| diag_ping() { | |
| fn_enter "$@" | |
| 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 || 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 "$@" | |
| 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 || 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). | |
| local log_name | |
| if [ "${anonymize_output}" = "yes" ]; then | |
| log_name="wifi_status" | |
| else | |
| log_name="wifi_status_no_anonymize" | |
| fi | |
| dbus-send --system --print-reply --fixed --dest=org.chromium.debugd \ | |
| /org/chromium/debugd org.chromium.debugd.GetLog "string:${log_name}" | |
| } | |
| diag_wifi_internal() { | |
| local ifc | |
| for ifc in $(get_iflist); do | |
| if is_wifi "${ifc}" ; then | |
| echo "iw dev ${ifc} survey dump:" | |
| "${IW}" dev "${ifc}" survey dump | output_anonymize | |
| echo "iw dev ${ifc} station dump -v:" | |
| "${IW}" dev "${ifc}" station dump -v | output_anonymize | |
| echo "iw dev ${ifc} scan dump:" | |
| "${IW}" dev "${ifc}" scan dump | output_anonymize | grep -v SSID | |
| echo "iw dev ${ifc} link:" | |
| "${IW}" dev "${ifc}" link | output_anonymize | grep -v SSID | |
| fi | |
| done | |
| echo "iw reg get:" | |
| "${IW}" reg get | |
| } | |
| # Perform diagnostics on a WAN interface | |
| diag_link_cellular() { | |
| fn_enter "$@" | |
| 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 "$@" | |
| 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")")" | |
| # Allow 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 "$@" | |
| 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 || return | |
| for ifc in $(get_iflist); do | |
| diag_link "${ifc}" | |
| done | |
| } | |
| # Perform generic diagnostics on a network interface | |
| diag_if() { | |
| fn_enter "$@" | |
| ifc="$1" | |
| config="$("${IFCONFIG}" "${ifc}")" | |
| ret=0 | |
| ${IFCONFIG} "${ifc}" | output_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 || 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 | |
| return 1 | |
| fi | |
| diag_flimflam | |
| return 0 | |
| } | |
| # Diagnose nameserver connectivity | |
| diag_nameservers() { | |
| fn_enter_once || 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 "$@" || 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". | |
| # 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 -v re="^/[0-9]+/${child}[/ ]" 'match($0, re) { print $2 }' | |
| } | |
| diag_flimflam_dbus() { | |
| fn_enter_once "$@" | |
| local ff="org.chromium.flimflam" | |
| if [ -z "$1" ] ; then | |
| echo "Flimflam Manager:" | |
| dbus_get_object_properties "${ff}" Manager / | output_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}" | output_anonymize | |
| dbus_get_object_properties "${ff}" "${child}" "${path}" | output_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 "$@" | |
| 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 "$@" | |
| local lease_dir=/var/lib/dhcpcd | |
| for file in "${lease_dir}/"*.lease; do | |
| echo "DHCP Lease file ${file}" | |
| read_dhcp_lease "${file}" | output_anonymize | |
| done | |
| } | |
| # Probe process status of flimflam and its process descendants | |
| diag_flimflam() { | |
| fn_enter_once || 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" | |
| "chromeos-ca.gstatic.com" | |
| "clients1.google.com" | |
| "clients2.google.com" | |
| "clients3.google.com" | |
| "clients4.google.com" | |
| "commondatastorage.googleapis.com" | |
| "cros-omahaproxy.appspot.com" | |
| "dl.google.com" | |
| "dl-ssl.google.com" | |
| "gweb-gettingstartedguide.appspot.com" | |
| "m.google.com" | |
| "omahaproxy.appspot.com" | |
| "pack.google.com" | |
| "policies.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" | |
| "connectivitycheck.android.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}" | |
| } | |
| 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. | |
| [ "${host}" = "$(printf '%s' "${host}" | tr -cd 'A-Za-z0-9.:-')" ] | |
| } | |
| usage() { | |
| echo "Usage: $0 [--date|--flimflam|--link|--wifi|--help] [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 " --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 " --wifi: Display driver-specific debugging information" | |
| 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 | |
| ;; | |
| --anonymize) | |
| anonymize_output=yes | |
| ;; | |
| --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 | |
| ;; | |
| --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 | |
| ;; | |
| --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 | |
| } | |
| main "$@" | |
| exit "${fail_count}" |