| #!/usr/bin/env python3 | |
| # Copyright 2019 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| #### CATEGORY=Run, inspect and debug | |
| ### packet capture and display tool | |
| ## | |
| ## usage: | |
| ## $ fx sniff wlan capture packets over WLAN interface, | |
| ## and show their summaries | |
| ## $ fx sniff --view hex eth capture packets over Ethernet interface, | |
| ## and show their hexadecimal dump | |
| ## $ fx sniff --view wireshark wlan capture packets over WLAN interface, | |
| ## and start wireshark GUI for display | |
| ## $ fx sniff --file myfile eth capture packets and store | |
| ## at //out/myfile.pcapng | |
| ## $ fx sniff -t 10 wlan capture for 10 sec | |
| ## $ fx sniff --help show all command line options | |
| """Fuchsia packet capture and display tool. | |
| fx sniff captures the packets flowing in-and-out the Fuchsia target device | |
| and displays the packets in a useful view. This is to run on the development | |
| host. | |
| fx sniff will automatically filter out fx-workflow related packets such as ssh, | |
| package server, logs, zxdb, etc., so that the user can focus on the application | |
| of interest. For those who need to debug the fx workflow itself, | |
| the full command under use is also printed; one can easily modify to meet | |
| their own needs. | |
| [Typical usages] | |
| $ fx sniff wlan # capture packets over WLAN interface, | |
| # and show their summaries | |
| $ fx sniff --view hex eth # capture packets over Ethernet interface, | |
| # and show their hexadecimal dump | |
| $ fx sniff --view wireshark wlan # capture packets over WLAN interface, | |
| # and start wireshark GUI for display | |
| $ fx sniff --file myfile eth # capture packets and store | |
| # at //out/myfile.pcapng | |
| $ fx sniff -t 10 wlan # capture for 10 sec | |
| $ fx sniff --help # show all command line options | |
| """ | |
| import argparse | |
| import fcntl | |
| import os | |
| import subprocess | |
| import sys | |
| import termios | |
| import time | |
| LINUX_BIN_WIRESHARK = u"wireshark" | |
| LINUX_BIN_TSHARK = u"tshark" | |
| TARGET_TMP_DIR = u"/tmp/" | |
| def has_cmd(binary_name): | |
| return any( | |
| os.access(os.path.join(path, binary_name), os.X_OK) | |
| for path in os.environ["PATH"].split(os.pathsep)) | |
| def has_wireshark_env(): | |
| """Test if wireshark GUI can run. | |
| Returns: | |
| (True, "") if wireshark can run in the environment. | |
| (False, Error_String) otherwise. | |
| """ | |
| platform = os.uname() | |
| print(platform) | |
| if len(platform) is 0: | |
| return False, u"Failed to get uname" | |
| if platform[0].lower() != u"linux": | |
| return False, u"Supported only on Linux" | |
| if not has_cmd(LINUX_BIN_WIRESHARK): | |
| return False, u"can\'t find %s" % (LINUX_BIN_WIRESHARK) | |
| # All look good. | |
| return True, u"" | |
| def run_cmd(cmd): | |
| """Run subprocess.run() safely and returns the stdout. | |
| Args: | |
| cmd: a command line string. | |
| Returns: | |
| The stdout outcome of the shell command. | |
| """ | |
| result = subprocess.run( | |
| cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| return result.stdout.decode() | |
| def can_run_cmd(cmd): | |
| """Test if the environment can run cmd. | |
| Args: | |
| cmd: a command line string. | |
| Returns: | |
| True if the command can run without error catched as an Exception. | |
| False otherwise. | |
| """ | |
| try: | |
| child = subprocess.check_call(cmd.split(), stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT) | |
| except Exception: | |
| return False | |
| return True | |
| def invoke_shell(cmd): | |
| """invoke_shell() uses shell=True to support the command | |
| that includes shell escape sequences. | |
| Args: | |
| cmd: command string, which may include escape sequence. | |
| """ | |
| print(u"Invoke shell:" + cmd) | |
| p = subprocess.Popen(cmd, shell=True) | |
| p.wait() | |
| def get_interface_names(): | |
| """Get all network interface names of the target except for the loopback. | |
| Returns: | |
| A list of interface names. | |
| """ | |
| result = run_cmd(u"fx shell net if list") | |
| names = [] | |
| for l in result.split(u"\n"): | |
| if u"name" not in l: | |
| # Look for name field only. | |
| continue | |
| parsed = l.split() | |
| if len(parsed) != 2: | |
| continue | |
| name = parsed[1].strip() | |
| if name == u"lo": | |
| # sniffing on loopback interface is not supported | |
| continue | |
| names.append(name) | |
| return names | |
| def get_interface_filepath(interface_name): | |
| """Get the filepaths for all the interfaces on the target. | |
| Args: | |
| interface_name: interface name. | |
| Returns: | |
| A list of the interface file paths. | |
| """ | |
| result = run_cmd(u"fx shell net if list %s" % interface_name) | |
| filepaths = [] | |
| for l in result.split(u"\n"): | |
| if u"filepath" not in l: | |
| continue | |
| parsed = l.split() | |
| if len(parsed) != 2: | |
| continue | |
| filepath = parsed[1].strip() | |
| if filepath.lower() == u"[none]": | |
| # sniffing on loopback interface is not supported | |
| continue | |
| filepaths.append(filepath) | |
| return filepaths | |
| def has_fuzzy_name(name_under_test, names): | |
| if not name_under_test: | |
| return False | |
| for n in names: | |
| if name_under_test in n: | |
| return True | |
| return False | |
| def build_cmd(args): | |
| """Build cmd line for sniffing and displaying. | |
| Args: | |
| args: command line arguments. | |
| Returns: | |
| cmd string. | |
| """ | |
| fx_workflow_filter = ( | |
| u"not ( " | |
| u"port ssh or dst port 8083 or dst port 2345 or port 1900 " | |
| u"or ip6 dst port 33330-33341 or ip6 dst port 33337-33338" | |
| u" )") | |
| # Pay special attention to the escape sequence | |
| # This command goes through the host shell, and ssh shell. | |
| cmd_prefix = u"fx shell sh -c '\"netdump -t %s -f \\\"%s\\\" " % ( | |
| args.timeout, fx_workflow_filter) | |
| cmd_suffix = u" %s\"'" % (args.interface_filepath) | |
| cmd_options = u"" | |
| # Build more options | |
| if args.file: | |
| full_path = u"%s%s" % (TARGET_TMP_DIR, args.file) | |
| cmd_options += u"-w %s " % full_path | |
| if args.view == u"summary": | |
| cmd_options += u"" # Default behavior | |
| elif args.view == u"hex": | |
| cmd_options += u"--hexdump" | |
| elif args.view == u"wireshark": | |
| (result, err_str) = has_wireshark_env() | |
| if not result: | |
| msg = ( | |
| u"Does not have a working wireshark envirionment. " | |
| u"Note it requires graphical environment " | |
| u"such as X Display: %s" % err_str) | |
| print(msg) | |
| return | |
| cmd_options += u"--pcapdump" | |
| cmd_suffix += u" | wireshark -k -i -" | |
| cmd = cmd_prefix + cmd_options + cmd_suffix | |
| return cmd | |
| def get_keystroke_unblocking(): | |
| """Returns a keystroke in a non-blocking way. | |
| """ | |
| fd = sys.stdin.fileno() | |
| attr_org = termios.tcgetattr(fd) | |
| flags_org = fcntl.fcntl(fd, fcntl.F_GETFL) | |
| attr_new = attr_org[::] | |
| attr_new[3] = attr_new[3] & ~termios.ICANON & ~termios.ECHO | |
| flags_new = flags_org | os.O_NONBLOCK | |
| try: | |
| termios.tcsetattr(fd, termios.TCSANOW, attr_new) | |
| fcntl.fcntl(fd, fcntl.F_SETFL, flags_new) | |
| key = sys.stdin.read(1) | |
| except: | |
| pass | |
| finally: | |
| termios.tcsetattr(fd, termios.TCSAFLUSH, attr_org) | |
| fcntl.fcntl(fd, fcntl.F_SETFL, flags_org) | |
| return key | |
| def do_sniff(cmd): | |
| """Run user-interruptible sniffer. | |
| Args: | |
| cmd: command string, which may include escape sequence. | |
| """ | |
| print("Run: {}".format(cmd)) | |
| p = subprocess.Popen(cmd, shell=True) | |
| while p.poll() is None: | |
| time.sleep(0.07) # To tame the CPU cycle consumption | |
| user_key = get_keystroke_unblocking() | |
| if user_key in ["q", "c", "Q", "C"]: | |
| print(" ... forced stop by user ({})".format(user_key)) | |
| run_cmd("fx shell killall netdump") | |
| p.terminate() | |
| def move_out_dumpfile(filename): | |
| """Move the PCAPNG dump file from the target device to the host device. | |
| Args: | |
| filename: filename stored in the target. Empty string if no file was stored. | |
| """ | |
| if not filename: | |
| return | |
| full_path = u"%s%s" % (TARGET_TMP_DIR, filename) | |
| cmd = u"cd %s" % os.environ[u"FUCHSIA_OUT_DIR"] | |
| cmd += u"; fx scp \"[$(fx get-device-addr)]:%s\" ." % full_path | |
| cmd += u"; fx shell rm -rf %s" % full_path | |
| invoke_shell(cmd) | |
| def is_target_ready(): | |
| """Tests if the target Fuchsia device is ready to capture packets. | |
| Returns: | |
| True if the target is ready. False otherwise. | |
| """ | |
| if not can_run_cmd("fx shell exit"): | |
| print("failed to run: the target device unreachable by 'fx shell'") | |
| return False | |
| if not can_run_cmd("fx shell which netdump"): | |
| msg = ("failed to run: the target does not have 'netdump'. " | |
| "Build with '--with-base //src/connectivity/network/netdump' " | |
| "and reload the tarnet") | |
| print(msg) | |
| return False | |
| return True | |
| def main(): | |
| if not is_target_ready(): | |
| sys.exit(1) | |
| iface_names = get_interface_names() | |
| parser = argparse.ArgumentParser( | |
| description=u"Capture packets on the target, Display on the host") | |
| parser.add_argument( | |
| u"interface_name", | |
| nargs=u"?", | |
| default=u"", | |
| help=u"Choose one interface name from: %s" % | |
| (" ".join(i for i in iface_names))) | |
| parser.add_argument( | |
| u"-t", u"--timeout", default=30, help=u"Time duration to sniff") | |
| parser.add_argument( | |
| u"--view", | |
| nargs=u"?", | |
| choices=[u"wireshark", u"hex", u"summary"], | |
| default=u"summary", | |
| const=u"summary", | |
| help=u"Wireshark requires X Display GUI environment.") | |
| parser.add_argument( | |
| u"--file", | |
| type=str, | |
| default=u"", | |
| help= | |
| u"Store PCAPNG file in //out directory. May use with --view option") | |
| parser.add_argument( | |
| u"--iface_filepath", | |
| dest=u"iface_filepath", | |
| default=u"", | |
| help=u"Specify the interface filepath directly. For advanced users.") | |
| args = parser.parse_args() | |
| # Sanitize the file name | |
| if args.file: | |
| if not args.file.endswith(u".pcapng"): | |
| args.file = args.file + ".pcapng" | |
| if not has_fuzzy_name(args.interface_name, iface_names): | |
| print( | |
| u"Choose one interface name from: %s" % | |
| " ".join(i for i in iface_names)) | |
| sys.exit(1) | |
| iface_file_paths = get_interface_filepath(args.interface_name) | |
| if len(iface_file_paths) != 1: | |
| msg = u"Querying interface name |%s| yielded non-unique result: %s" % ( | |
| args.interface_name, u" ".join(f for f in iface_file_paths)) | |
| print(msg) | |
| sys.exit(1) | |
| args.interface_filepath = iface_file_paths[0] | |
| do_sniff(build_cmd(args)) | |
| print(u"\nEnd of fx sniff") | |
| move_out_dumpfile(args.file) | |
| sys.exit(0) | |
| if __name__ == u"__main__": | |
| sys.exit(main()) |