| #!/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_output="${ANONYMIZE_OUTPUT-no}" |
| 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))" |
| } |
| |
| # 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 () { |
| 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 "${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 |
| # 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}_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 "${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). |
| 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:" |
| "${IW}" dev "${ifc}" station dump | 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 "${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")")" |
| # 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 "${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}" | 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 "${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 |
| 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 -v re="^/[0-9]+/${child}[/ ]" 'match($0, re) { 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 / | 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 "${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}" | output_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" |
| "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}" |
| diag_gateway_latency "${host}" |
| } |
| |
| 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. |
| [ "${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 " --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 " --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 |
| ;; |
| --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 |
| ;; |
| --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}" |