From cc7d5ae49b71b1aab708e16d56dce2962bed89f4 Mon Sep 17 00:00:00 2001 From: "Brennan W. Fieck" Date: Wed, 30 May 2018 09:36:18 -0600 Subject: [PATCH] Initial commit for golang version --- .gitignore | 18 +-- MANIFEST.in | 7 - README.md | 122 +++++++-------- README.rst | 277 ---------------------------------- connvitals.go | 248 +++++++++++++++++++++++++++++++ connvitals/__init__.py | 75 ---------- connvitals/collector.py | 158 -------------------- connvitals/config.py | 117 --------------- connvitals/ping.py | 295 ------------------------------------ connvitals/ports.py | 107 ------------- connvitals/traceroute.py | 84 ----------- connvitals/utils.py | 189 ----------------------- ping/ping.go | 243 ++++++++++++++++++++++++++++++ ports/ports.go | 167 +++++++++++++++++++++ setup.py | 208 -------------------------- traceroute/traceroute.go | 314 +++++++++++++++++++++++++++++++++++++++ utils/utils.go | 254 +++++++++++++++++++++++++++++++ 17 files changed, 1283 insertions(+), 1600 deletions(-) delete mode 100644 MANIFEST.in delete mode 100644 README.rst create mode 100644 connvitals.go delete mode 100644 connvitals/__init__.py delete mode 100644 connvitals/collector.py delete mode 100644 connvitals/config.py delete mode 100644 connvitals/ping.py delete mode 100644 connvitals/ports.py delete mode 100644 connvitals/traceroute.py delete mode 100644 connvitals/utils.py create mode 100644 ping/ping.go create mode 100644 ports/ports.go delete mode 100755 setup.py create mode 100644 traceroute/traceroute.go create mode 100644 utils/utils.go diff --git a/.gitignore b/.gitignore index bcf224c..02d026a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,2 @@ -# Sublime project files -*.sublime-* - -# ViM swapfiles -*.swp -*.swp~ - -# Python Byte-Code caches -*.pyc -__pycache__ - -# Setuputils build directories -build -dist -*.egg-info - +# Binary +connvitals diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 7447693..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -# License information -include LICENSE -include NOTICE - -# README in reStructuredText format -README.rst - diff --git a/README.md b/README.md index 5a66058..f52a1d7 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,38 @@ # connvitals + [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) Checks a machines connection to a specific host or list of hosts in terms of packet loss, icmp latency, routing, and anything else that winds up getting added. -*Note: Does not recognize duplicate hosts passed on `argv` and will test each as though unique.* +*Note: Does not recognize duplicate hosts passed via `argv`, and will test all of them as though unique.* -*Note: Under normal execution conditions, requires super-user privileges to run.* +*Note: Under normal execution conditions, requires super-user permissions to run.* ## Dependencies -The utility runs on Python 3 (tested 3.6.3), but requires no non-standard external modules. - -## Installation -### Binary packages -Binary packages are offered in `.rpm` format for Fedora/CentOS/RHEL and `.whl` format for all other operating systems under '[Releases](https://github.com/connvitals/releases)'. - -### From This Repository with `pip` -The easiest way to install is to simply use `pip`. You can install directly from this repository without needing to manually download it by running -```bash -user@hostname ~ $ pip install git+https://github.com/connvitals.git#egg=connvitals -``` -Note that you may need to run this command as root/with `sudo` or with `--user`, depending on your `pip` installation. Also ensure that `pip` is installing packages for Python 3.x. Typically, if both Python2 and Python3 exist on a system with `pip` installed for both, the `pip` to use for Python3 packages is accessible as `pip3`. - -### Manually -To install manually, first download or clone this repository. Then, in the directory you downloaded/cloned it into, run the command +The utility was built using Go, and binaries should work without dependencies. +Building requires the getopt/v2, x/net/icmp, x/net/ipv6 and x/net/ipv4 libraries, which can in most cases be downloaded with: ```bash -user@hostname ~/connvitals $ python setup.py install +go get golang.org/x/net/icmp +go get golang.org/x/net/ipv4 +go get golang.org/x/net/ipv6 +go get github.com/pborman/getopt/v2 ``` -Note that it's highly likely that you will need to run this command as root/with `sudo`. Also ensure that the `python` command points to a valid Python3 interpreter (you can check with `python --version`). On many systems, it is common for `python` to point to a Python2 interpreter. If you have both Python3 and Python2 installed, it's common that they be accessible as `python3` and `python2`, respectively. -Finally, if you are choosing this option because you do not have a Python3 `pip` installation, you may not have `setuptools` installed. On most 'nix distros, this can be installed without installing `pip` by running `sudo apt-get install python3-setuptools` (Debian/Ubuntu), `sudo pacman -S python3-setuptools` (Arch), `sudo yum install python3-setuptools` (RedHat/Fedora/CentOS), or `brew install python3-setuptools` (macOS with `brew` installed). ## Usage ```bash -connvitals [ -h --help ] [ -V --version ] [ -H --hops HOPS ] [ -p --pings PINGS ] [ -P --no-ping ] [ -t --trace ] [ --payload-size PAYLOAD ] [ -s --port-scan ] host [ hosts... ] +connvitals [ -h --help ] [ -V --version ] [ -H --hops HOPS ] [ -p --pings PINGS ] [ -P --no-ping ] [ -t --trace ] [ --payload-size PAYLOAD ] [ --port-scan ] [ -j --json ] host [ hosts... ] ``` -* `hosts` - The host or hosts to check connection to. May be ipv4 addresses, ipv6 addresses, fqdn's, or any combination thereof. +* `hosts` - A list of one or more hosts to check connection to. They can be ipv4 addresses, ipv6 addresses, fqdn's or any combination thereof. * `-h` or `--help` - Prints help text, then exits successfully. * `-V` or `--version` - Prints the program's version information, then exits successfully. * `-H` or `--hops` - Sets max hops for route tracing (default 30). * `-p` or `--pings` - Sets the number of pings to use for aggregate statistics (default 4). * `-P` or `--no-ping` - Don't run ping tests. * `-t` or `--trace` - Run route tracing. -* `-j` or `--json` - Prints output as one line of JSON-formatted text. -* `-s` or `--port-scan` - Perform a limited scan on each hosts' ports. +* `-j` or `--json` - Print output a single line in JSON format. * `--payload-size` - Sets the size (in B) of ping packet payloads (default 41). +* `--port-scan` - Perform a limited scan on each hosts' ports. ### Output Format @@ -56,54 +45,53 @@ Route traces output their results as a list of network hops, separated from each Port scans check for http(s) servers on ports 80 and 443, and MySQL servers running on port 3306. It outputs its results as a tab-separated list containing - in this order - port 80 results, port 443 results, port 3306 results. Results for ports 80 and 443 consist of sending a `HEAD / HTTP/1.1` request and recording "rtt (in milliseconds), response code, server" from the server's response. "server" will be the contents of the "Server" header if found within the first kilobyte of the response, but if it is not found will simply be "Unknown". Port 3306 results report the version of the MySQL server listening on that port if one is found (Note that this version number may be mangled if the server allows unauthenticated connection or supports some other automatic authentication mechanism for the machine running connvitals). If a server is not found on a port, its results are reported as "None", indicating no listening server. If a server on port 80 expects encryption or a server on port 443 does not expect encryption, they will be "erroneously" reported as not existing. -Example Output (with localhost running mysql server): - +example output: ```bash -root@hostname / # connvitals -stp 100 google.com 2607:f8b0:400f:807::200e localhost +root@hostname / # connvitals -t --port-scan google.com 127.0.0.1 2607:f8b0:400f:807::200e google.com (172.217.3.14) -3.543 4.955 11.368 1.442 0.000 -10.169.240.1 3.108 -10.168.253.8 2.373 -10.168.254.252 3.659 -10.168.255.226 2.399 -198.178.8.94 3.059 -69.241.22.33 51.104 -68.86.103.13 16.470 -68.86.92.121 5.488 -68.86.86.77 4.257 -68.86.83.6 3.946 -173.167.58.142 5.290 +5.696 14.684 20.647 3.641 0.000 +10.169.240.1 5.689 +10.168.253.8 10.870 +10.168.254.252 9.621 +10.168.255.226 2.238 +198.178.8.94 3.038 +69.241.22.33 3.790 +68.86.103.13 4.332 +68.86.92.121 6.097 +68.86.86.77 5.397 +68.86.83.6 7.255 +173.167.58.142 10.740 * -216.239.49.247 4.491 -172.217.3.14 3.927 -56.446, 200, gws 75.599, 200, gws None +216.239.49.247 3.886 +172.217.3.14 4.132 +64.778, 200, gws 65.069, 200, gws None +127.0.0.1 +0.847 2.378 3.701 0.654 0.000 +127.0.0.1 0.931 +None None 2.073, 5.7.2 2607:f8b0:400f:807::200e -3.446 4.440 12.422 1.526 0.000 -2001:558:1418:49::1 8.846 -2001:558:3da:74::1 1.453 -2001:558:3da:6f::1 2.955 -2001:558:3da:1::2 2.416 -2001:558:3c2:15::1 2.605 -2001:558:fe1c:6::1 47.516 -2001:558:1c0:65::1 45.442 -2001:558:0:f71e::1 9.165 -* +6.031 12.674 19.786 3.638 0.000 +2001:558:1418:49::1 11.922 +2001:558:3da:74::1 9.625 +2001:558:3da:6f::1 2.740 +2001:558:3da:1::2 2.221 +2001:558:3c2:15::1 3.993 +2001:558:fe1c:6::1 5.599 +2001:558:1c0:65::1 3.877 +2001:558:0:f71e::1 7.185 * -2001:559:0:9::6 3.984 +2001:558:0:f8c1::2 3.977 +2001:559::10c6 4.074 * -2001:4860:0:1::10ad 3.970 -2607:f8b0:400f:807::200e 3.891 -57.706, 200, gws 77.736, 200, gws None -localhost (127.0.0.1) -0.045 0.221 0.665 0.112 1.000 -127.0.0.1 0.351 -None None 0.165, 5.7.2 +2001:4860:0:1::10ad 3.773 +2607:f8b0:400f:807::200e 3.631 +66.074, 200, gws 73.950, 200, gws None ``` #### JSON Output Format The JSON output format option (`-j` or `--json`) will render the output on one line. Each host is represented as an object, indexed by its **address**. This is not necessarily the same as the host as given on the command line, which may be found as an attribute of the host, named `'name'`. Results for ping tests are a dictionary attribute named `'ping'`, with floating point values labeled as `'min'`, `'avg'`, `'max'`, `'std'` and `'loss'`. As with all floating point numbers in json output, these values are **not rounded or truncated** and are printed exactly as calculated, to the greatest degree of precision afforded by the system. -Route traces are output as a list attribute, labeled `'trace'`, where each each step in the route is itself a list. The first element in each list is either the address of the discovered host at that point in the route, or the special string `'*'` which indicates the packet was lost and no host was discovered at this point. The second element, if it exists, is a floating point number giving the round-trip-time of the packet sent at this step, in milliseconds. Once again, unlike normal output format, these floating point numbers **are not rounded or truncated** and are printed exactly as calculated, to the greatest degree of precision afforded by the system. +Route traces are output as a list attribute, labeled `'route'`, where each each step in the route is itself a list. The first element in each list is either the address of the discovered host at that point in the route, or the special string `'*'` which indicates the packet was lost and no host was discovered at this point. The second element, if it exists, is a floating point number giving the round-trip-time of the packet sent at this step, in milliseconds. Once again, unlike normal output format, these floating point numbers **are not rounded or truncated** and are printed exactly as calculated, to the greatest degree of precision afforded by the system. Port scans are represented as a dictionary attribute named `'scan'`. The label of each element of `'scan'` is the name of the server checked for. `'http'` and `'https'` results will report a dictionary of values containing: * `'rtt'` - the time taken for the server to respond * `'response code'` - The decimal representation of the server's response code to a `HEAD / HTML/1.1` request. @@ -116,9 +104,9 @@ Example JSON Output (with localhost running mysql server): root@hostname / # sudo connvitals -j --port-scan -tp 100 google.com 2607:f8b0:400f:807::200e localhost ``` ```json -{"addr":"172.217.3.14","name":"google.com","ping":{"min": 3.525257110595703, "avg": 4.422152042388916, "max": 5.756855010986328, "std": 0.47761748430602524, "loss": 0.0},"trace":[["*"], ["10.168.253.8", 2.187013626098633], ["10.168.254.252", 4.266977310180664], ["10.168.255.226", 3.283977508544922], ["198.178.8.94", 2.7751922607421875], ["69.241.22.33", 3.7970542907714844], ["68.86.103.13", 3.8001537322998047], ["68.86.92.121", 7.291316986083984], ["68.86.86.77", 5.874156951904297], ["68.86.83.6", 4.465818405151367], ["173.167.58.142", 4.443883895874023], ["*"], ["216.239.49.231", 4.090785980224609], ["172.217.3.14", 4.895925521850586]],"scan":{"http": {"rtt": 59.095, "response code": "200", "server": "gws"}, "https": {"rtt": 98.238, "response code": "200", "server": "gws"}, "mysql": "None"}}} -{"addr":"2607:f8b0:400f:807::200e","name":"2607:f8b0:400f:807::200e","ping":{"min": 3.62396240234375, "avg": 6.465864181518555, "max": 24.2769718170166, "std": 5.133322111766303, "loss": 0.0},"trace":[["*"], ["2001:558:3da:74::1", 1.9710063934326172], ["2001:558:3da:6f::1", 2.904176712036133], ["2001:558:3da:1::2", 2.5751590728759766], ["2001:558:3c2:15::1", 2.7141571044921875], ["2001:558:fe1c:6::1", 4.7512054443359375], ["2001:558:1c0:65::1", 3.927946090698242], ["*"], ["*"], ["2001:558:0:f8c1::2", 3.635406494140625], ["2001:559:0:18::2", 3.8270950317382812], ["*"], ["2001:4860:0:1::10ad", 4.517078399658203], ["2607:f8b0:400f:807::200e", 3.91387939453125]],"scan":{"http": {"rtt": 51.335, "response code": "200", "server": "gws"}, "https": {"rtt": 70.521, "response code": "200", "server": "gws"}, "mysql": "None"}}} -{"addr":"127.0.0.1","name":"localhost","ping":{"min": 0.04792213439941406, "avg": 0.29621124267578125, "max": 0.5612373352050781, "std": 0.0995351687014057, "loss": 0.0},"trace":[["127.0.0.1", 1.9199848175048828]],"scan":{"http": "None", "https": "None", "mysql": {"rtt": 0.148, "version": "5.7.2"}}}} +{"addr":"172.217.3.14","name":"google.com","ping":{"min": 3.525257110595703, "avg": 4.422152042388916, "max": 5.756855010986328, "std": 0.47761748430602524, "loss": 0.0},"route":[["*"], ["10.168.253.8", 2.187013626098633], ["10.168.254.252", 4.266977310180664], ["10.168.255.226", 3.283977508544922], ["198.178.8.94", 2.7751922607421875], ["69.241.22.33", 3.7970542907714844], ["68.86.103.13", 3.8001537322998047], ["68.86.92.121", 7.291316986083984], ["68.86.86.77", 5.874156951904297], ["68.86.83.6", 4.465818405151367], ["173.167.58.142", 4.443883895874023], ["*"], ["216.239.49.231", 4.090785980224609], ["172.217.3.14", 4.895925521850586]],"scan":{"http": {"rtt": 59.095, "response code": "200", "server": "gws"}, "https": {"rtt": 98.238, "response code": "200", "server": "gws"}, "mysql": "None"}}} +{"addr":"2607:f8b0:400f:807::200e","name":"2607:f8b0:400f:807::200e","ping":{"min": 3.62396240234375, "avg": 6.465864181518555, "max": 24.2769718170166, "std": 5.133322111766303, "loss": 0.0},"route":[["*"], ["2001:558:3da:74::1", 1.9710063934326172], ["2001:558:3da:6f::1", 2.904176712036133], ["2001:558:3da:1::2", 2.5751590728759766], ["2001:558:3c2:15::1", 2.7141571044921875], ["2001:558:fe1c:6::1", 4.7512054443359375], ["2001:558:1c0:65::1", 3.927946090698242], ["*"], ["*"], ["2001:558:0:f8c1::2", 3.635406494140625], ["2001:559:0:18::2", 3.8270950317382812], ["*"], ["2001:4860:0:1::10ad", 4.517078399658203], ["2607:f8b0:400f:807::200e", 3.91387939453125]],"scan":{"http": {"rtt": 51.335, "response code": "200", "server": "gws"}, "https": {"rtt": 70.521, "response code": "200", "server": "gws"}, "mysql": "None"}}} +"addr":"127.0.0.1","name":"localhost","ping":{"min": 0.04792213439941406, "avg": 0.29621124267578125, "max": 0.5612373352050781, "std": 0.0995351687014057, "loss": 0.0},"route":[["127.0.0.1", 1.9199848175048828]],"scan":{"http": "None", "https": "None", "mysql": {"rtt": 0.148, "version": "5.7.2"}}} ``` @@ -129,10 +117,10 @@ EE: : : - ``` `EE: ` is prepended for ease of readability in the common case that stdout and stderr are being read/parsed from the same place. `` is commonly just `str` or `Exception`, but can in some cases represent more specific error types. `` holds extra information describing why the error occurred. Note that stack traces are not commonly logged, and only occur when the program crashes for unforseen reasons. `` is the time at which the error occurred, given in the system's `ctime` format, which will usually look like `Mon Jan 1 12:59:59 2018`. -Some errors do not affect execution in a large scope, and are printed largely for debugging purposes. These are printed as warnings to `stderr` in the following format: +Some errors do not affect execution in a large scope, and are logged to `stderr` as warnings in the following format: ``` WW: - ``` -Where `WW: ` is prepended both for ease of readability and to differentiate it from an error, `` is the warning message, and `` is the time at which the warning was issued, given in the system's `ctime` format. +where `WW: ` is printed both for ease of readability and to distinguish warnings from errors, `` is the warning message, and `` is the time at which the warning was issued, given in the system's `ctime` format. -In the case that `stderr` is a tty, `connvitals` will attempt to print errors in red and warnings in yellow, using ANSI control sequences (supports all VT/100-compatible terminal emulators). +In the case that `stderr` is a tty, `connvitals` will print errors in red, and warnings in yellow using ANSI control sequences (currently supports most Linux/Unix distributions). diff --git a/README.rst b/README.rst deleted file mode 100644 index 6be76cb..0000000 --- a/README.rst +++ /dev/null @@ -1,277 +0,0 @@ -connvitals -========== - -|License| - -Checks a machines connection to a specific host or list of hosts in -terms of packet loss, icmp latency, routing, and anything else that -winds up getting added. - -*Note: Does not recognize duplicate hosts passed on ``argv`` and will -test each as though unique.* - -*Note: Under normal execution conditions, requires super-user privileges -to run.* - -Dependencies ------------- - -The utility runs on Python 3 (tested 3.6.3), but requires no -non-standard external modules. - -Installation ------------- - -Binary packages -~~~~~~~~~~~~~~~ - -Binary packages are offered in ``.rpm`` format for Fedora/CentOS/RHEL -and ``.whl`` format for all other operating systems under -'`Releases `__'. - -From This Repository with ``pip`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The easiest way to install is to simply use ``pip``. You can install -directly from this repository without needing to manually download it by -running - -.. code:: bash - - user@hostname ~ $ pip install git+https://github.com/connvitals.git#egg=connvitals - -Note that you may need to run this command as root/with ``sudo`` or with -``--user``, depending on your ``pip`` installation. Also ensure that -``pip`` is installing packages for Python 3.x. Typically, if both -Python2 and Python3 exist on a system with ``pip`` installed for both, -the ``pip`` to use for Python3 packages is accessible as ``pip3``. - -Manually -~~~~~~~~ - -To install manually, first download or clone this repository. Then, in -the directory you downloaded/cloned it into, run the command - -.. code:: bash - - user@hostname ~/connvitals $ python setup.py install - -| Note that it's highly likely that you will need to run this command as - root/with ``sudo``. Also ensure that the ``python`` command points to - a valid Python3 interpreter (you can check with ``python --version``). - On many systems, it is common for ``python`` to point to a Python2 - interpreter. If you have both Python3 and Python2 installed, it's - common that they be accessible as ``python3`` and ``python2``, - respectively. -| Finally, if you are choosing this option because you do not have a - Python3 ``pip`` installation, you may not have ``setuptools`` - installed. On most 'nix distros, this can be installed without - installing ``pip`` by running - ``sudo apt-get install python3-setuptools`` (Debian/Ubuntu), - ``sudo pacman -S python3-setuptools`` (Arch), - ``sudo yum install python3-setuptools`` (RedHat/Fedora/CentOS), or - ``brew install python3-setuptools`` (macOS with ``brew`` installed). - -Usage ------ - -.. code:: bash - - connvitals [ -h --help ] [ -V --version ] [ -H --hops HOPS ] [ -p --pings PINGS ] [ -P --no-ping ] [ -t --trace ] [ --payload-size PAYLOAD ] [ -s --port-scan ] host [ hosts... ] - -- ``hosts`` - The host or hosts to check connection to. May be ipv4 - addresses, ipv6 addresses, fqdn's, or any combination thereof. -- ``-h`` or ``--help`` - Prints help text, then exits successfully. -- ``-V`` or ``--version`` - Prints the program's version information, - then exits successfully. -- ``-H`` or ``--hops`` - Sets max hops for route tracing (default 30). -- ``-p`` or ``--pings`` - Sets the number of pings to use for aggregate - statistics (default 4). -- ``-P`` or ``--no-ping`` - Don't run ping tests. -- ``-t`` or ``--trace`` - Run route tracing. -- ``-j`` or ``--json`` - Prints output as one line of JSON-formatted - text. -- ``-s`` or ``--port-scan`` - Perform a limited scan on each hosts' - ports. -- ``--payload-size`` - Sets the size (in B) of ping packet payloads - (default 41). - -Output Format -~~~~~~~~~~~~~ - -Normal Output -^^^^^^^^^^^^^ - -For each host tested, results are printed in the newline-separated order -"host->Ping Results->Route Trace Results->Port Scan Results" where -"host" is the name of the host, as passed on argv. If the name passed -for a host on ``argv`` is not what ends up being used to test connection -vitals (e.g. the program may translate ``google.com`` into -``216.58.218.206``), then the "host" line will contain -``host-as-typed (host IP used)``. - -Ping tests output their results as a tab-separated list containing - in -this order - minimum round-trip time in milliseconds (rtt), mean rtt, -maximum rtt, rtt standard deviation, and packet loss in percent. If all -packets are lost, the min/mean/max/std are all reported as -1. - -Route traces output their results as a list of network hops, separated -from each other by newlines. Each network hop is itself a tab-separated -list of data containing - in this order - a network address for the -machine this hop ended at, and the rtt of a packet traversing this -route. If the packet was lost, a star (``*``) is shown instead of an -address and rtt. - -Port scans check for http(s) servers on ports 80 and 443, and MySQL -servers running on port 3306. It outputs its results as a tab-separated -list containing - in this order - port 80 results, port 443 results, -port 3306 results. Results for ports 80 and 443 consist of sending a -``HEAD / HTTP/1.1`` request and recording "rtt (in milliseconds), -response code, server" from the server's response. "server" will be the -contents of the "Server" header if found within the first kilobyte of -the response, but if it is not found will simply be "Unknown". Port 3306 -results report the version of the MySQL server listening on that port if -one is found (Note that this version number may be mangled if the server -allows unauthenticated connection or supports some other automatic -authentication mechanism for the machine running connvitals). If a -server is not found on a port, its results are reported as "None", -indicating no listening server. If a server on port 80 expects -encryption or a server on port 443 does not expect encryption, they will -be "erroneously" reported as not existing. - -Example Output (with localhost running mysql server): - -.. code:: bash - - root@hostname / # connvitals -stp 100 google.com 2607:f8b0:400f:807::200e localhost - google.com (172.217.3.14) - 3.543 4.955 11.368 1.442 0.000 - 10.169.240.1 3.108 - 10.168.253.8 2.373 - 10.168.254.252 3.659 - 10.168.255.226 2.399 - 198.178.8.94 3.059 - 69.241.22.33 51.104 - 68.86.103.13 16.470 - 68.86.92.121 5.488 - 68.86.86.77 4.257 - 68.86.83.6 3.946 - 173.167.58.142 5.290 - * - 216.239.49.247 4.491 - 172.217.3.14 3.927 - 56.446, 200, gws 75.599, 200, gws None - 2607:f8b0:400f:807::200e - 3.446 4.440 12.422 1.526 0.000 - 2001:558:1418:49::1 8.846 - 2001:558:3da:74::1 1.453 - 2001:558:3da:6f::1 2.955 - 2001:558:3da:1::2 2.416 - 2001:558:3c2:15::1 2.605 - 2001:558:fe1c:6::1 47.516 - 2001:558:1c0:65::1 45.442 - 2001:558:0:f71e::1 9.165 - * - * - 2001:559:0:9::6 3.984 - * - 2001:4860:0:1::10ad 3.970 - 2607:f8b0:400f:807::200e 3.891 - 57.706, 200, gws 77.736, 200, gws None - localhost (127.0.0.1) - 0.045 0.221 0.665 0.112 1.000 - 127.0.0.1 0.351 - None None 0.165, 5.7.2 - -JSON Output Format -^^^^^^^^^^^^^^^^^^ - -| The JSON output format option (``-j`` or ``--json``) will render the - output on one line. Each host is represented as an object, indexed by - its **address**. This is not necessarily the same as the host as given - on the command line, which may be found as an attribute of the host, - named ``'name'``. -| Results for ping tests are a dictionary attribute named ``'ping'``, - with floating point values labeled as ``'min'``, ``'avg'``, ``'max'``, - ``'std'`` and ``'loss'``. As with all floating point numbers in json - output, these values are **not rounded or truncated** and are printed - exactly as calculated, to the greatest degree of precision afforded by - the system. -| Route traces are output as a list attribute, labeled ``'trace'``, - where each each step in the route is itself a list. The first element - in each list is either the address of the discovered host at that - point in the route, or the special string ``'*'`` which indicates the - packet was lost and no host was discovered at this point. The second - element, if it exists, is a floating point number giving the - round-trip-time of the packet sent at this step, in milliseconds. Once - again, unlike normal output format, these floating point numbers **are - not rounded or truncated** and are printed exactly as calculated, to - the greatest degree of precision afforded by the system. -| Port scans are represented as a dictionary attribute named ``'scan'``. - The label of each element of ``'scan'`` is the name of the server - checked for. ``'http'`` and ``'https'`` results will report a - dictionary of values containing: -| \* ``'rtt'`` - the time taken for the server to respond -| \* ``'response code'`` - The decimal representation of the server's - response code to a ``HEAD / HTML/1.1`` request. -| \* ``'server'`` - the name of the server, if found within the first - kilobyte of the server's response, otherwise "Unknown". -| ``'mysql'`` fields will also contain a dictionary of values, and that - dictionary should also contain the ``'rtt'`` field with the same - meaning as for ``'http'`` and ``'https'``, but will replace the other - two fields used by those protocols with ``'version'``, which will give - the version number of the MySQL server. -| If any of these three server types is not detected, the value of its - label will be the string 'None', rather than a dictionary of values. - -Example JSON Output (with localhost running mysql server): - -.. code:: bash - - root@hostname / # sudo connvitals -j --port-scan -tp 100 google.com 2607:f8b0:400f:807::200e localhost - -.. code:: json - - {"addr":"172.217.3.14","name":"google.com","ping":{"min": 3.525257110595703, "avg": 4.422152042388916, "max": 5.756855010986328, "std": 0.47761748430602524, "loss": 0.0},"trace":[["*"], ["10.168.253.8", 2.187013626098633], ["10.168.254.252", 4.266977310180664], ["10.168.255.226", 3.283977508544922], ["198.178.8.94", 2.7751922607421875], ["69.241.22.33", 3.7970542907714844], ["68.86.103.13", 3.8001537322998047], ["68.86.92.121", 7.291316986083984], ["68.86.86.77", 5.874156951904297], ["68.86.83.6", 4.465818405151367], ["173.167.58.142", 4.443883895874023], ["*"], ["216.239.49.231", 4.090785980224609], ["172.217.3.14", 4.895925521850586]],"scan":{"http": {"rtt": 59.095, "response code": "200", "server": "gws"}, "https": {"rtt": 98.238, "response code": "200", "server": "gws"}, "mysql": "None"}}} - {"addr":"2607:f8b0:400f:807::200e","name":"2607:f8b0:400f:807::200e","ping":{"min": 3.62396240234375, "avg": 6.465864181518555, "max": 24.2769718170166, "std": 5.133322111766303, "loss": 0.0},"trace":[["*"], ["2001:558:3da:74::1", 1.9710063934326172], ["2001:558:3da:6f::1", 2.904176712036133], ["2001:558:3da:1::2", 2.5751590728759766], ["2001:558:3c2:15::1", 2.7141571044921875], ["2001:558:fe1c:6::1", 4.7512054443359375], ["2001:558:1c0:65::1", 3.927946090698242], ["*"], ["*"], ["2001:558:0:f8c1::2", 3.635406494140625], ["2001:559:0:18::2", 3.8270950317382812], ["*"], ["2001:4860:0:1::10ad", 4.517078399658203], ["2607:f8b0:400f:807::200e", 3.91387939453125]],"scan":{"http": {"rtt": 51.335, "response code": "200", "server": "gws"}, "https": {"rtt": 70.521, "response code": "200", "server": "gws"}, "mysql": "None"}}} - {"addr":"127.0.0.1","name":"localhost","ping":{"min": 0.04792213439941406, "avg": 0.29621124267578125, "max": 0.5612373352050781, "std": 0.0995351687014057, "loss": 0.0},"trace":[["127.0.0.1", 1.9199848175048828]],"scan":{"http": "None", "https": "None", "mysql": {"rtt": 0.148, "version": "5.7.2"}}}} - -Error Output Format -^^^^^^^^^^^^^^^^^^^ - -When an error occurs, it is printed to ``stderr`` in the following -format: - -:: - - EE: : : - - -``EE:`` is prepended for ease of readability in the common case that -stdout and stderr are being read/parsed from the same place. -```` is commonly just ``str`` or ``Exception``, but can in -some cases represent more specific error types. ```` -holds extra information describing why the error occurred. Note that -stack traces are not commonly logged, and only occur when the program -crashes for unforseen reasons. ```` is the time at which the -error occurred, given in the system's ``ctime`` format, which will -usually look like ``Mon Jan 1 12:59:59 2018``. - -Some errors do not affect execution in a large scope, and are printed -largely for debugging purposes. These are printed as warnings to -``stderr`` in the following format: - -:: - - WW: - - -Where ``WW:`` is prepended both for ease of readability and to -differentiate it from an error, ```` is the warning message, -and ```` is the time at which the warning was issued, given -in the system's ``ctime`` format. - -In the case that ``stderr`` is a tty, ``connvitals`` will attempt to -print errors in red and warnings in yellow, using ANSI control sequences -(supports all VT/100-compatible terminal emulators). - -.. |License| image:: https://img.shields.io/badge/License-Apache%202.0-blue.svg - :target: https://opensource.org/licenses/Apache-2.0 diff --git a/connvitals.go b/connvitals.go new file mode 100644 index 0000000..c87e38b --- /dev/null +++ b/connvitals.go @@ -0,0 +1,248 @@ +package main + +// Copyright 2018 Comcast Cable Communications Management, LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "fmt" +import "connvitals/ping" +import "connvitals/traceroute" +import "connvitals/ports" +import "sync" +import "github.com/pborman/getopt/v2" +import "net" +import "connvitals/utils" +import "bytes" +import "os" + +// Constants +const IP4LEN = 4; +const IP6LEN = 16; +const SOFTWARE_VERSION = "3.0.0"; + +//Holds results until the end of execution +var pingResults map[*net.IPAddr]string = make(map[*net.IPAddr]string); +var traceResults map[*net.IPAddr]string = make(map[*net.IPAddr]string); +var scanResults map[*net.IPAddr]string = make(map[*net.IPAddr]string); + +// Concurrency locks +var pinglock = sync.RWMutex{}; +var tracelock = sync.RWMutex{}; +var scanlock = sync.RWMutex{}; + +/* + Thread-safe function to write a ping result to the results map. +*/ +func writePingResult(host *net.IPAddr, res string) { + pinglock.Lock(); + defer pinglock.Unlock(); + pingResults[host] = res; +} + +/* + Thread-safe function to write a route trace result to the results map. +*/ +func writeRoute(host *net.IPAddr, res string) { + tracelock.Lock(); + defer tracelock.Unlock(); + traceResults[host] = res; +} + +/* + Thread-safe function to write a ping result to the results map. +*/ +func writeScan(host *net.IPAddr, res string) { + scanlock.Lock(); + defer scanlock.Unlock(); + scanResults[host] = res; +} + +func main() { + + MAX_HOPS := getopt.IntLong("hops", 'H', 30, "Sets max hops for route tracing (default 30)."); + HELP := getopt.BoolLong("help", 'h', "Prints help text and exits."); + NUMPINGS := getopt.IntLong("pings", 'p', 10, "Sets the number of pings to use for aggregate statistics (default 10)."); + NOPINGS := getopt.BoolLong("no-ping", 'P', "Don't run ping tests."); + TRACE := getopt.BoolLong("trace", 't', "Run route tracing."); + JSON := getopt.BoolLong("json", 'j', "Print output as one line of JSON formatted information.") + PAYLOAD := getopt.IntLong("payload-size", 0, 41, "Sets the size (in B) of ping packet payloads (default 41)."); + PORTSCAN := getopt.BoolLong("port-scan", 's', "Perform a limited scan on each host's ports.") + VERSION := getopt.BoolLong("version", 'V', "Print the version information, then exit.") + getopt.Parse(); + + if *VERSION { + fmt.Printf("connvitals Version %s\n", SOFTWARE_VERSION); + os.Exit(0); + } else if *HELP { + getopt.Usage(); + os.Exit(0); + } + + args := getopt.Args(); + if len(args) < 1 { + getopt.Usage(); + os.Exit(1); + } + + + //Holds the original names of hosts, for easier identification in output + hostnames := make(map[*net.IPAddr]string); + + // Multiprocessing worker pool + var pool sync.WaitGroup; + + for arg := range args { + + // Parse the host for an IP Address + var host string = args[arg]; + targetIP, err := net.ResolveIPAddr("ip", host); + if err != nil { + utils.Error(utils.GenericError{"Host '"+host+"' could not be resolved"}, 0); + continue; + } + + + // Determine if IP is ipv4 or ipv6 + var IPv6 bool; + if len(targetIP.IP.To4()) == IP4LEN { + IPv6 = false; + } else if len(targetIP.IP) == IP6LEN { + IPv6 = true; + } else { + utils.Error(utils.GenericError{"Host '"+host+"' could not be resolved"}, 0); + continue; + } + + + //Store the user-specified name of this host + hostnames[targetIP] = host; + + // Asynchronously ping this host + if ! *NOPINGS { + pool.Add(1); + go func () { + defer pool.Done(); + min, avg, max, std, loss, err := ping.PingHost(targetIP, IPv6, *NUMPINGS, *PAYLOAD); + if err != nil { + utils.Error(err, 0); + } + + var format string; + + // create json-ified text if necessary... + if *JSON { + format = "{\"min\":%f,\"avg\":%f,\"max\":%f,\"std\":%f,\"loss\":%f}"; + + // ...otherwise just format the results + } else { + format = "%.3f\t%.3f\t%.3f\t%.3f\t%.3f"; + } + + writePingResult(targetIP, fmt.Sprintf(format, min, avg, max, std, loss)); + }(); + } + + + if *TRACE { + pool.Add(1); + go func() { + defer pool.Done(); + tracer, err := traceroute.New(targetIP, *MAX_HOPS, IPv6); + if err != nil { + utils.Error(err, 0); + return; + } + + result, err := tracer.Run(); + if err != nil { + utils.Error(err, 0); + } + + var buffer bytes.Buffer; + + // create json-ified text if necessary... + if *JSON { + buffer.WriteRune('['); + buffer.WriteString(result[0].JSON()); + for step := range result[1:] { + buffer.WriteRune(','); + buffer.WriteString(result[step+1].JSON()); + } + buffer.WriteRune(']'); + + // ...otherwise just format the results + } else { + buffer.WriteString(result[0].String()); + for step := range result[1:] { + buffer.WriteRune('\n'); + buffer.WriteString(result[step+1].String()); + } + } + + writeRoute(targetIP, buffer.String()); + }(); + } + + if *PORTSCAN { + pool.Add(1); + go func () { + defer pool.Done(); + httpScanResult, httpsScanResult, mysqlScanResult := ports.Scan(targetIP.String(), IPv6); + + var buffer bytes.Buffer; + + // create json-ified text if necessary... + if *JSON { + buffer.WriteString("{\"http\":"); + buffer.WriteString(httpScanResult.JSON()); + buffer.WriteString(",\"https\":"); + buffer.WriteString(httpsScanResult.JSON()); + buffer.WriteString(",\"mysql\":"); + buffer.WriteString(mysqlScanResult.JSON()); + buffer.WriteRune('}'); + + // ...otherwise just format the results + } else { + buffer.WriteString(httpScanResult.String()); + buffer.WriteRune('\t'); + buffer.WriteString(httpsScanResult.String()); + buffer.WriteRune('\t'); + buffer.WriteString(mysqlScanResult.String()); + } + writeScan(targetIP, buffer.String()); + }(); + } + } + + pool.Wait(); + + utils.Print(*JSON, hostnames, pingResults, traceResults, scanResults); + + // // Print results + // for key, value := range hostnames { + // if key.String() == value { + // fmt.Println(key.String()); + // } else { + // fmt.Printf("%s (%s)\n", value, key.String()); + // } + // if ! *NOPINGS && len(pingResults[key]) > 0 { + // fmt.Print(pingResults[key]); + // } + // if *TRACE && len(traceResults[key]) > 0 { + // fmt.Print(traceResults[key]); + // } + // if *PORTSCAN && len(scanResults[key]) > 0 { + // fmt.Println(scanResults[key]); + // } + // } +} diff --git a/connvitals/__init__.py b/connvitals/__init__.py deleted file mode 100644 index 7c5a314..0000000 --- a/connvitals/__init__.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright 2018 Comcast Cable Communications Management, LLC - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -A utility to check connection vitals with a remote host. - - Usage: connvitals [ -h --help ] [ -H --hops HOPS ] [ -p --pings PINGS ] [ -P --no-ping ] - [ -t --trace ] [ --payload-size PAYLOAD ] [ --port-scan ] [ -j --json ] - host [hosts... ] - -Each 'host' can be an ipv4 address, ipv6 address, or a fully-qualified domain name. - -Submodules: - utils: Contains utility functionality such as error/warning reporting and host address parsing - ping: Groups functionality related to ICMP/ICMPv6 tests - traceroute: Contains a function for tracing a route to a host - ports: Specifies functions for checking specific host ports for http(s) and MySQL servers - -""" - -__version__ = "4.0.1" -__author__ = "Brennan Fieck" - -def main() -> int: - """ - Runs the utility with the arguments specified on sys.argv. - Returns: Always 0 to indicate "Success", unless the utility terminates - prematurely with a fatal error. - """ - from . import utils - from . import config - from . import collector - - config.init() - - # No hosts could be parsed - if not config.HOSTS: - utils.error("No hosts could be parsed! Exiting...", True) - - collectors = [collector.Collector(host) for host in config.HOSTS] - - # Start all the collectors - for collect in collectors: - collect.start() - - # Wait for every collector to finish - # Print JSON if requested - if config.JSON: - for collect in collectors: - _ = collect.join() - collect.result = collect.recv() - print(repr(collect)) - - # ... else print plaintext - else: - for collect in collectors: - _ = collect.join() - collect.result = collect.recv() - print(collect) - - - # Errors will be indicated on stdout; because we query multiple hosts, as - # long as the main routine doesn't crash, we have exited successfully. - return 0 diff --git a/connvitals/collector.py b/connvitals/collector.py deleted file mode 100644 index 0e42ee7..0000000 --- a/connvitals/collector.py +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright 2018 Comcast Cable Communications Management, LLC - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module defines a single worker to collect stats from a single host""" - -import multiprocessing -import math -from . import utils, config, ping, traceroute, ports - -class Collector(multiprocessing.Process): - """ - A threaded worker that collects stats for a single host. - """ - trace = None - result = [utils.PingResult(-1, -1, -1, -1, 100.), - utils.Trace([utils.TraceStep('*', -1)] * 10), - utils.ScanResult(None, None, None)] - - def __init__(self, host: str): - """ - Initializes the Collector, and its worker pool - """ - super(Collector, self).__init__() - - self.hostname = host - self.host = config.HOSTS[host] - self.name = host - - self.pipe = multiprocessing.Pipe() - - def run(self): - """ - Called when the thread is run - """ - with multiprocessing.pool.ThreadPool() as pool: - pscan_result, trace_result, ping_result = None, None, None - if config.PORTSCAN: - pscan_result = pool.apply_async(ports.portScan, - (self.host, pool), - error_callback=utils.error) - if config.TRACE: - trace_result = pool.apply_async(traceroute.trace, - (self.host,), - error_callback=utils.error) - if not config.NOPING: - try: - self.ping(pool) - except (multiprocessing.TimeoutError, ValueError): - self.result[0] = type(self).result[0] - if config.TRACE: - try: - self.result[1] = trace_result.get(config.HOPS) - except multiprocessing.TimeoutError: - self.result[1] = type(self).result[1] - if config.PORTSCAN: - try: - self.result[2] = pscan_result.get(0.5) - except multiprocessing.TimeoutError: - self.result[2] = type(self).result[2] - - self.pipe[1].send(self.result) - - def ping(self, pool: multiprocessing.pool.ThreadPool): - """ - Pings the host - """ - pinger = ping.Pinger(self.host, bytes(config.PAYLOAD)) - - # Aggregates round-trip time for each packet in the sequence - rtt, lost = [], 0 - - # Sends, receives and parses all icmp packets asynchronously - results = pool.map_async(pinger.ping, - range(config.NUMPINGS), - error_callback=utils.error) - pkts = results.get(8) - pinger.sock.close() - del pinger - - for pkt in pkts: - if pkt != None and pkt > 0: - rtt.append(pkt*1000) - else: - lost += 1 - - try: - avg = sum(rtt) / len(rtt) - std = 0. - for item in rtt: - std += (avg - item)**2 - std /= len(rtt) - 1 - std = math.sqrt(std) - except ZeroDivisionError: - std = 0. - - self.result[0] = utils.PingResult(min(rtt), avg, max(rtt), std, lost/config.NUMPINGS *100.0) - - def __str__(self) -> str: - """ - Implements 'str(self)' - - Returns a plaintext output result - """ - ret = [] - if self.host[0] == self.hostname: - ret.append(self.hostname) - else: - ret.append("%s (%s)" % (self.hostname, self.host[0])) - - pings, trace, scans = self.result - - if pings: - ret.append(str(pings)) - if trace and trace != self.trace: - self.trace = trace - ret.append(str(trace)) - if scans: - ret.append(str(scans)) - - return "\n".join(ret) - - def __repr__(self) -> repr: - """ - Implements `repr(self)` - - Returns a JSON output result - """ - ret = [r'{"addr":"%s"' % self.host[0]] - ret.append(r'"name":"%s"' % self.hostname) - - if not config.NOPING: - ret.append(r'"ping":%s' % repr(self.result[0])) - - if config.TRACE and self.trace != self.result[1]: - self.trace = self.result[1] - ret.append(r'"trace":%s' % repr(self.result[1])) - - if config.PORTSCAN: - ret.append(r'"scan":%s' % repr(self.result[2])) - - return ','.join(ret) + '}' - - def recv(self): - """ - Returns a message from the Collector's Pipe - """ - return self.pipe[0].recv() diff --git a/connvitals/config.py b/connvitals/config.py deleted file mode 100644 index 9f60b9c..0000000 --- a/connvitals/config.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright 2018 Comcast Cable Communications Management, LLC - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This module defines the config options for the 'connvitals' command -""" - - -from . import utils - -# connvitals version -__version__ = "4.0.1" - -# Configuration values -HOPS = 30 -JSON = False -PAYLOAD = b'The very model of a modern Major General.' -TRACE = False -NOPING = False -PORTSCAN = False -NUMPINGS = 10 -HOSTS = {} - -def init(): - """ - Initializes the configuration. - """ - global HOPS, JSON, PAYLOAD, TRACE, NOPING, PORTSCAN, NUMPINGS, HOSTS, __version__ - - from argparse import ArgumentParser as Parser - parser = Parser(description="A utility to check connection vitals with a remote host.", - epilog="'host' can be an ipv4 or ipv6 address, or a fully-qualified domain name.") - - parser.add_argument("hosts", - help="The host or hosts to check connection to. "\ - "These can be ipv4 addresses, ipv6 addresses, fqdn's, "\ - "or any combination thereof.", - nargs="+") - - parser.add_argument("-H", "--hops", - dest="hops", - help="Sets max hops for route tracing (default 30).", - default=30, - type=int) - - parser.add_argument("-p", "--pings", - dest="numpings", - help="Sets the number of pings to use for aggregate statistics (default 10).", - default=10, - type=int) - - parser.add_argument("-P", "--no-ping", - dest="noping", - help="Don't run ping tests.", - action="store_true") - - parser.add_argument("-t", "--trace", - dest="trace", - help="Run route tracing.", - action="store_true") - - parser.add_argument("-s", "--port-scan", - dest="portscan", - help="Scan the host(s)'s ports for commonly-used services", - action="store_true") - - parser.add_argument("--payload-size", - dest="payload", - help="Sets the size (in B) of ping packet payloads (default 41).", - default=b'The very model of a modern Major General.', - type=int) - - parser.add_argument("-j", "--json", - dest="json", - help="Outputs in machine-readable JSON (no newlines)", - action="store_true") - - parser.add_argument("-V", "--version", - dest="version", - help="Print the program's version, then exit.", - action="store_true") - - args = parser.parse_args() - - if args.version: - print("python3-connvitals Version %s" % __version__) - exit(0) - - HOPS = args.hops - JSON = args.json - PAYLOAD = args.payload - TRACE = args.trace - NOPING = args.noping - PORTSCAN = args.portscan - NUMPINGS = args.numpings - hosts = args.hosts - - # Parse the list of hosts and try to find valid addresses for each - HOSTS = {} - - for host in hosts: - info = utils.getaddr(host) - if not info: - utils.error("Unable to resolve host ( %s )" % host) - else: - HOSTS[host] = info diff --git a/connvitals/ping.py b/connvitals/ping.py deleted file mode 100644 index aadb0a9..0000000 --- a/connvitals/ping.py +++ /dev/null @@ -1,295 +0,0 @@ -# Copyright 2018 Comcast Cable Communications Management, LLC - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This module defines a class and utilities to manage icmp/icmpv6 echo requests -and replies to remote hosts. -""" - -import socket -import struct -import time -import sys -from . import utils - - -def calculate_checksum(pkt: bytes) -> bytes: - """ - Implementation of the "Internet Checksum" specified in - RFC 1071 (https://tools.ieft.org/html/rfc1071) - - Ideally this would act on the string as a series of half-words in host byte order, - but this works. - - Network data is big-endian, hosts are typically little-endian, - which makes this much more tedious than it needs to be. - """ - - from sys import byteorder - - countTo = len(pkt) // 2 * 2 - total, count = 0, 0 - - # Handle bytes in pairs (decoding as short ints) - loByte, hiByte = 0, 0 - while count < countTo: - if byteorder == "little": - loByte = pkt[count] - hiByte = pkt[count + 1] - else: - loByte = pkt[count + 1] - hiByte = pkt[count] - total += hiByte * 256 + loByte - count += 2 - - # Handle last byte if applicable (odd-number of bytes) - # Endianness should be irrelevant in this case - if countTo < len(pkt): # Check for odd length - total += pkt[len(pkt) - 1] - - total &= 0xffffffff # Truncate sum to 32 bits (a variance from ping.c, which - # uses signed ints, but overflow is unlikely in ping) - - total = (total >> 16) + (total & 0xffff) # Add high 16 bits to low 16 bits - total += (total >> 16) # Add carry from above (if any) - - return socket.htons((~total) & 0xffff) - -def IPv6_checksum(pkt: bytes, laddr: bytes, raddr: bytes) -> bytes: - """ - Implementation of the ICMPv6 "Internet Checksum" as specified in - RFC 1701 (https://tools.ieft.org/html/rfc1701). - - This takes the Payload Length from the IPv6 layer to be 32 (0x20), since we - don't expect any extension headers and ICMP doesn't carry any length - information. - pkt: A complete ICMP packet, with the checksum field set to 0 - laddr: The (fully-expanded) local address of the socket that will send pkt - raddr: The (fully-expanded) remote address of the host to which the pkt will be sent - returns: A bytes object representing the checksum - """ - - # IPv6 Pseudo-Header used for checksum calculation as specified by - # RFC 2460 (https://tools.ieft.org/html/rfc2460) - psh = laddr + raddr + struct.pack("!I", len(pkt)) + b'\x00\x00\x00:' - # This last bit is the 4-byte-packed icmp6 protocol number (58 or 0xa3) - - - total, packet = 0, psh+pkt - - # Sum all 2-byte words - num_words = len(packet) // 2 - for chunk in struct.unpack("!%sH" % num_words, packet[0:num_words*2]): - total += chunk - - # Add any left-over byte (for odd-length packets) - if len(packet) % 2: - total += ord(packet[-1]) << 8 - - # Fold 32-bits into 16-bits - total = (total >> 16) + (total & 0xffff) - total += total >> 16 - return ~total + 0x10000 & 0xffff - -def icmpParse(pkt: bytes, ipv6: bool) -> int: - """ - Parses an icmp packet, returning its sequence number. - - If the packet is found to be not an echo reply, this will - immediately return -1, indicating that this packet - should be disregarded. - """ - try: - if ipv6: - if pkt[0] == 129: - return struct.unpack("!H", pkt[6:8])[0] - return -1 - if pkt[20] == 0: - return struct.unpack("!H", pkt[26:28])[0] - return -1 - except (IndexError, struct.error): - return -1 - -class Pinger(object): - """ - A data structure that handles icmp pings to a remote machine. - """ - def __init__(self, host: utils.Host, payload: bytes): - """ - Inializes a socket connection to the host on port 22, and returns a Pinger object - referencing it. - """ - - self.sock, self.icmpParse, self.mkPkt = None, None, None - - if host[1] == socket.AF_INET6: - self.sock = socket.socket(host[1], socket.SOCK_RAW, proto=58) - self.icmpParse = self._icmpv6Parse - self.mkPkt = self._mkPkt6 - else: - self.sock = socket.socket(host[1], socket.SOCK_RAW, proto=1) - self.icmpParse = self._icmpv4Parse - self.mkPkt = self._mkPkt4 - - self.sock.settimeout(2) - self.payload = payload - - #Build a socket object - self.host = host - - self.timestamps = {} - - def ping(self, seqno: int) -> float: - """ - Sends a single icmp packet to the remote host. - Returns the round-trip time (in ms) between packet send and receipt - or 0 if packet was not received. - """ - pkt = self.mkPkt(seqno) - - # I set time here so that rtt includes the device latency - self.timestamps[seqno] = time.time() - - try: - # ICMP has no notion of port numbers - self.sock.sendto(pkt, (self.host[0], 1)) - except Exception as e: - #Sometimes, when the network is unreachable this will erroneously report that there's an - #'invalid argument', which is impossible since the hostnames are coming straight from - #`socket` itself - raise Exception("Network is unreachable... (%s)" % e) - return self.recv() - - @staticmethod - def _icmpv4Parse(pkt: bytes) -> int: - """ - Attemtps to parse an icmpv4 packet, returning the sequence number if parsing succeds, - or -1 otherwise. - """ - try: - if pkt[20] == 0: - return struct.unpack("!H", pkt[26:28])[0] - except (IndexError, struct.error): - pass - return -1 - - @staticmethod - def _icmpv6Parse(pkt: bytes) -> int: - """ - Attemtps to parse an icmpv6 packet, returning the sequence number if parsing succeds, - or -1 otherwise. - """ - try: - if pkt[0] == 0x81: - return struct.unpack("!H", pkt[6:8])[0] - except (IndexError, struct.error): - pass - return -1 - - def _mkPkt4(self, seqno: int) -> bytes: - """ - Contsructs and returns an ICMPv4 packet - """ - header = struct.pack("!BBHHH", 8, 0, 0, 2, seqno) - checksum = self._checksum4(header + self.payload) - return struct.pack("!BBHHH", 8, 0, checksum, 2, seqno) + self.payload - - def _mkPkt6(self, seqno: int) -> bytes: - """ - Contsructs and returns an ICMPv6 packet - """ - header = struct.pack("!BBHHH", 0x80, 0, 0, 2, seqno) - checksum = self._checksum6(header) - return struct.pack("!BBHHH", 0x80, 0, checksum, 2, seqno) + self.payload - - @staticmethod - def _checksum4(pkt: bytes) -> int: - """ - calculates and returns the icmpv4 checksum of 'pkt' - """ - - countTo = len(pkt) // 2 * 2 - total, count = 0, 0 - - # Handle bytes in pairs (decoding as short ints) - loByte, hiByte = 0, 0 - while count < countTo: - if sys.byteorder == "little": - loByte = pkt[count] - hiByte = pkt[count + 1] - else: - loByte = pkt[count + 1] - hiByte = pkt[count] - total += hiByte * 256 + loByte - count += 2 - - # Handle last byte if applicable (odd-number of bytes) - # Endianness should be irrelevant in this case - if countTo < len(pkt): # Check for odd length - total += pkt[len(pkt) - 1] - - total &= 0xffffffff # Truncate sum to 32 bits (a variance from ping.c, which - # uses signed ints, but overflow is unlikely in ping) - - total = (total >> 16) + (total & 0xffff) # Add high 16 bits to low 16 bits - total += (total >> 16) # Add carry from above (if any) - - return socket.htons((~total) & 0xffff) - - def _checksum6(self, pkt: bytes) -> int: - """ - calculates and returns the icmpv6 checksum of pkt - """ - laddr = socket.inet_pton(self.host[1], self.sock.getsockname()[0]) - raddr = socket.inet_pton(*reversed(self.host)) - # IPv6 Pseudo-Header used for checksum calculation as specified by - # RFC 2460 (https://tools.ieft.org/html/rfc2460) - psh = laddr + raddr + struct.pack("!I", len(pkt)) + b'\x00\x00\x00:' - # This last bit is the 4-byte-packed icmp6 protocol number (58 or 0xa3) - - - total, packet = 0, psh+pkt - - # Sum all 2-byte words - num_words = len(packet) // 2 - for chunk in struct.unpack("!%sH" % num_words, packet[0:num_words*2]): - total += chunk - - # Add any left-over byte (for odd-length packets) - if len(packet) % 2: - total += ord(packet[-1]) << 8 - - # Fold 32-bits into 16-bits - total = (total >> 16) + (total & 0xffff) - total += total >> 16 - return ~total + 0x10000 & 0xffff - - def recv(self) -> float: - """ - Recieves each ping sent. - """ - # If a packet is not an echo reply, icmpParse will give its seqno as -1 - # This lets us disregard packets from traceroutes immediately - while True: - - try: - pkt, addr = self.sock.recvfrom(100+len(self.payload)) - except socket.timeout: - return -1 - - # The packet must have actually come from the host we pinged - if addr[0] == self.host[0]: - seqno = self.icmpParse(pkt) - if seqno >= 0: - return time.time() - self.timestamps[seqno] diff --git a/connvitals/ports.py b/connvitals/ports.py deleted file mode 100644 index c4b2c1d..0000000 --- a/connvitals/ports.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2018 Comcast Cable Communications Management, LLC - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This module contains functions for scanning some specific hosts for specific -information. Currently has functionality for http(s) servers on ports 80/443 -and MySQL servers on port 3306. -""" - -import socket -import time -import multiprocessing.pool -import typing -import ssl -from . import utils - -def http(url: utils.Host, port: int=80) -> typing.Optional[typing.Tuple[float, str, str]]: - """ - Checks for http content being served by url on a port passed in ssl. - (If ssl is 443, wraps the socket with ssl to communicate HTTPS) - Returns a HEAD request's status code if a server is found, else None - """ - - # Create socket (wrap for ssl as needed) - sock = socket.socket(family=url[1]) - if port == 443: - sock = ssl.wrap_socket(sock, ssl_version=3) - sock.settimeout(0.08) - - # Send request, and return "None" if anything goes wrong - try: - rtt = time.time() - sock.connect((url[0], port)) - sock.send(b"HEAD / HTTP/1.1\r\n\r\n") - ret = sock.recv(1000) - rtt = time.time() - rtt - except (OSError, ConnectionRefusedError, socket.gaierror, socket.timeout) as e: - utils.error(Exception("Could not connect to %s: %s" % (url[0], e))) - return None - except ssl.SSLError as e: - utils.warn("SSL handshake with %s failed: %s" % (url[0], e)) - return None - finally: - sock.close() - - # Servers that enforce ssl encryption when our socket isn't wrapped - or don't - # recognize encrypted requests when it is - will sometimes send empty responses - if not ret: - return None - - # Check for "Server" header if available. - # Note - this assumes that both the contents of the "Server" header and the response code are - # utf8-decodable, which may need to be patched in the future - try: - srv = ret.index(b'Server: ') - except ValueError: - return rtt*1000, ret[9:12].decode(), "Unkown" - return rtt*1000, ret[9:12].decode(), ret[srv+8:ret.index(b'\r', srv)].decode() - - -def mysql(url: utils.Host) -> typing.Optional[typing.Tuple[float, str]]: - """ - Checks for a MySQL server running on the host specified by url. - Returns the server version if one is found, else None. - """ - - sock = socket.socket(family=url[1]) - sock.settimeout(0.08) - try: - rtt = time.time() - sock.connect((url[0], 3306)) - return (time.time() - rtt)* 1000, sock.recv(1000)[5:10].decode() - except (UnicodeError, OSError, ConnectionRefusedError, socket.gaierror, socket.timeout) as e: - utils.error(Exception("Could not connect to %s: %s" % (url[0], e))) - return None - finally: - sock.close() - -def portScan(host:utils.Host, pool:multiprocessing.pool.Pool)-> typing.Tuple[str, utils.ScanResult]: - """ - Scans a host using a multiprocessing worker pool to see if a specific set of ports are open, - possibly returning extra information in the case that they are. - - Returns a tuple of (host, information) where host is the ip of the host scanned and information - is any and all information gathered from each port as a tuple in the order (80, 443). - If the specified port is not open, its spot in the tuple will contain `None`, but will otherwise - contain some information related to the port. - """ - - # Dispatch the workers - hypertext = pool.apply_async(http, (host,)) - https = pool.apply_async(http, (host, 443)) - mysqlserver = pool.apply_async(mysql, (host,)) - - # Collect and return - return utils.ScanResult(hypertext.get(), https.get(), mysqlserver.get()) diff --git a/connvitals/traceroute.py b/connvitals/traceroute.py deleted file mode 100644 index ac99f8e..0000000 --- a/connvitals/traceroute.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright 2018 Comcast Cable Communications Management, LLC - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This module defines a single function which implements route tracing. -""" - -import socket -import time -from . import utils, config - -def trace(host: utils.Host) -> utils.Trace: - """ - Traces a route from the localhost to a given destination. - Returns a tabular list of network hops up to the maximum specfied by 'hops' - """ - ret = [] - - ipv6 = host[1] == socket.AF_INET6 - - receiver = socket.socket(family=host[1], type=socket.SOCK_RAW, proto=58 if ipv6 else 1) - receiver.settimeout(0.05) - sender = socket.socket(family=host[1], type=socket.SOCK_DGRAM, proto=17) - - # Sets up functions used in the main loop, so it can transparently - # handle ipv4 and ipv6 without needing to check which one we're - # using on every iteration. - setTTL, isTraceResponse, getIntendedDestination = None, None, None - if ipv6: - setTTL = lambda x: sender.setsockopt(41, 4, x) - isTraceResponse = lambda x: x[0] in {1, 3} - getIntendedDestination = lambda x: socket.inet_ntop(socket.AF_INET6, x[32:48]) - else: - setTTL = lambda x: sender.setsockopt(socket.SOL_IP, socket.IP_TTL, x) - isTraceResponse = lambda x: x[20] in {11, 3} - getIntendedDestination = lambda x: ".".join(str(byte) for byte in x[44:48]) - - for ttl in range(config.HOPS): - setTTL(ttl+1) - timestamp = time.time() - - try: - sender.sendto(b'', (host[0], 33440)) - except OSError as e: - ret.append(utils.TraceStep("*", -1)) - continue - - try: - #Wait for packets sent by this trace - while True: - pkt, addr = receiver.recvfrom(1024) - rtt = time.time() - timestamp - - # If this is a response from a tracer and the tracer sent - # it to the same place we're sending things, then this - # packet must belong to us. - if isTraceResponse(pkt): - destination = getIntendedDestination(pkt) - if destination == host[0]: - break - - except socket.timeout: - ret.append(utils.TraceStep("*", -1)) - done = False - else: - ret.append(utils.TraceStep(addr[0], rtt*1000)) - done = addr[0] == host[0] - - if done: - break - receiver.close() - sender.close() - return utils.Trace(ret) diff --git a/connvitals/utils.py b/connvitals/utils.py deleted file mode 100644 index 1df54bf..0000000 --- a/connvitals/utils.py +++ /dev/null @@ -1,189 +0,0 @@ -# Copyright 2018 Comcast Cable Communications Management, LLC - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This module contains utility functions used by the main utility to do things -lke printing errors and warnings to stderr, or get a single, valid IP address -for a host. -""" - -import typing -import socket - -# I don't know why, but pylint seems to think that socket.AddressFamily isn't real, but it is. -# Nobody else has this issue as far as I could find. -#pylint: disable=E1101 -Host = typing.NamedTuple("Host", [('addr', str), ('family', socket.AddressFamily)]) -#pylint: enable=E1101 - -PingResult = typing.NamedTuple("PingResult", [ - ('minimum', float), - ('avg', float), - ('maximum', float), - ('std', float), - ('loss', float)]) - -def pingResultToStr(self: PingResult) -> str: - """ - Returns the string representation of a ping result in plaintext - """ - fmt = "%.3f\t%.3f\t%.3f\t%.3f\t%.3f" - return fmt % (self.minimum, self.avg, self.maximum, self.std, self.loss) - -def pingResultRepr(self: PingResult) -> str: - """ - Returns the JSON representation of a ping result - """ - fmt = '{"min":%f,"avg":%f,"max":%f,"std":%f,"loss":%f}' - return fmt % (self.minimum, self.avg, self.maximum, self.std, self.loss) - -PingResult.__str__ = pingResultToStr -PingResult.__repr__ = pingResultRepr - - -TraceStep = typing.NamedTuple("TraceStep", [("host", str), ("rtt", float)]) -Trace = typing.NewType("Trace", typing.List[TraceStep]) - -def traceStepToStr(self: TraceStep) -> str: - """ - Returns the string representation of a step of a route trace in plaintext - """ - if self.rtt < 0 or self.host == "*": - return "*" - return "%s\t%.3f" % (self.host, self.rtt) - -def traceStepRepr(self: TraceStep) -> str: - """ - Returns the JSON representation of a single step in a route trace - """ - if self.rtt < 0 or self.host == "*": - return '["*"]' - return '["%s", %f]' % (self.host, self.rtt) - -def compareTraceSteps(self: TraceStep, other: TraceStep) -> bool: - """ - Implements `self == other` - - Two trace steps are considered equal iff their hosts are the same - rtt is not considered. - """ - return self.host == other.host - -def traceStepIsValid(self: TraceStep): - """ - Implements `bool(self)` - - Returns True if the step reports that the packet reached the host within the timeout, - False otherwise. - """ - return self.rtt >= 0 and self.host != "*" - -TraceStep.__str__ = traceStepToStr -TraceStep.__repr__ = traceStepRepr -TraceStep.__eq__ = compareTraceSteps -TraceStep.__bool__ = traceStepIsValid - -def compareTraces(self: Trace, other: Trace) -> bool: - """ - Implements `self == other` - - Checks that traces are of the same length and contain the same hosts in the same order - i.e. does *not* check the rtts of any or all trace steps. - - Note: ignores steps that are invalid ('*'). - """ - this, that = [step for step in self if step], [step for step in other if step] - return len(this) == len(that) and all(this[i] == that[i] for i in range(len(this))) - -def traceToStr(self: Trace) -> str: - """ - Implements `str(self)` - - Returns the plaintext representation of a route trace. - """ - return '\n'.join(str(step) for step in self) - -def traceRepr(self: Trace) -> str: - """ - Implements `repr(self)` - - Returns the JSON representation of a route trace. - """ - return "[%s]" % ','.join(repr(step) for step in self) - -Trace.__str__ = traceToStr -Trace.__repr__ = traceRepr -Trace.__eq__ = compareTraces - - -ScanResult = typing.NamedTuple("ScanResult", [("httpresult", typing.Tuple[float, str, str]), - ("httpsresult", typing.Tuple[float, str, str]), - ("mysqlresult", typing.Tuple[float, str])]) - -def scanResultToStr(self: ScanResult) -> str: - """ - Returns the string representation of a portscan result in plaintext - """ - return "%s\t%s\t%s" % ("%.3f, %s, %s" % self.httpresult if self.httpresult else 'None', - "%.3f, %s, %s" % self.httpsresult if self.httpsresult else 'None', - "%.3f, %s" % self.mysqlresult if self.mysqlresult else 'None') - -def scanResultRepr(self: ScanResult) -> str: - """ - Returns the JSON representation of a portscan result - """ - httpFmt = '{"rtt":%f,"response code":"%s","server":"%s"}' - http = httpFmt % self.httpresult if self.httpresult else '"None"' - https = httpFmt % self.httpsresult if self.httpsresult else '"None"' - mySQL = '{"rtt":%f,"version":"%s"}' % self.mysqlresult if self.mysqlresult else '"None"' - return '{"http":%s,"https":%s,"mysql":%s}' % (http, https, mySQL) - -ScanResult.__str__ = scanResultToStr -ScanResult.__repr__ = scanResultRepr - - -def error(err: Exception, fatal: int=False): - """ - Logs an error to stderr, then exits if fatal is a non-falsy value, using it as an exit code - """ - from sys import stderr - from time import ctime - if stderr.isatty(): - fmt = "\033[38;2;255;0;0mEE: %s:" - print(fmt % type(err).__name__, "%s" % err, "-\t", ctime(), "\033[m", file=stderr) - else: - print("EE: %s:" % type(err).__name__, "%s" % err, "-\t", ctime(), file=stderr) - if fatal: - exit(int(fatal)) - -def warn(warning: str): - """ - Logs a warning to stderr. - """ - from sys import stderr - from time import ctime - if stderr.isatty(): - print("\033[38;2;238;216;78mWW:", warning, "-\t", ctime(), "\033[m", file=stderr) - else: - print("WW:", warning, '-\t', ctime(), file=stderr) - -def getaddr(host: str) -> typing.Optional[Host]: - """ - Returns a tuple of Address Family, IP Address for the host passed in `host`. - """ - - try: - addrinfo = socket.getaddrinfo(host, 1).pop() - return Host(addrinfo[4][0], addrinfo[0]) - except socket.gaierror: - return None diff --git a/ping/ping.go b/ping/ping.go new file mode 100644 index 0000000..9e1a07e --- /dev/null +++ b/ping/ping.go @@ -0,0 +1,243 @@ +package ping + +// Copyright 2018 Comcast Cable Communications Management, LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "net" +import "golang.org/x/net/ipv4" +import "golang.org/x/net/ipv6" +import "golang.org/x/net/icmp" +import "time" +import "sync" +import "math" +import "connvitals/utils" + +const IPV6_NETWORK_STRING = "ip6:ipv6-icmp"; +const IPV4_NETWORK_STRING = "ip4:icmp" + +/* + A data structure that handles sending/receiving ICMP Echo ("ping") packets +*/ +type Pinger struct { + Host *net.IPAddr; + IPv6 bool; + Payload []byte; + Connection *icmp.PacketConn; + timestamps []time.Time; + RTTS []time.Duration; +}; + +/* + Builds a Pinger object, initializing the connection and setting the rtts to -1 +*/ +func New(host *net.IPAddr, IPv6 bool, payload int, numpings int) (pinger *Pinger, err error) { + initialrtts := make([]time.Duration, numpings); + for i := 0; i < numpings; i++ { + initialrtts[i] = time.Duration(-1); + } + + pinger = &Pinger{ + host, + IPv6, + make([]byte, payload), + nil, + make([]time.Time, numpings), + initialrtts, + }; + + if IPv6 { + pinger.Connection, err = icmp.ListenPacket(IPV6_NETWORK_STRING, "::"); + if err != nil { + return; + } + pinger.Connection.IPv6PacketConn().SetDeadline(time.Now().Add(2 * time.Second)); + } else { + pinger.Connection, err = icmp.ListenPacket(IPV4_NETWORK_STRING, "0.0.0.0"); + if err != nil { + return; + } + pinger.Connection.IPv4PacketConn().SetDeadline(time.Now().Add(2 * time.Second)); + } + + return; +} + +/* + Constructs an icmp packet to send along a connection +*/ +func (pinger *Pinger) MkPacket(seqno int) (msg icmp.Message) { + var typ icmp.Type; + if pinger.IPv6 { + typ = ipv6.ICMPTypeEchoRequest; + } else { + typ = ipv4.ICMPTypeEcho; + } + + msg = icmp.Message{ + Type: typ, + Code: 0, + Body: &icmp.Echo{ + ID: 2, + Seq: seqno, + Data: pinger.Payload, + }, + }; + + return; +} + +/* + Sends a single packet on the Pinger's Connection, identified by the sequence number specified with seqno. + It returns any errors generated from constructing the packet or sending it through the socket. +*/ +func (pinger *Pinger) Send(seqno int) ( err error ) { + pkt := pinger.MkPacket(seqno); + var psh []byte = nil; + if pinger.IPv6 { + psh = icmp.IPv6PseudoHeader(pinger.Connection.LocalAddr().(*net.IPAddr).IP, pinger.Host.IP); + } + + encodedPacket, err := pkt.Marshal(psh); + if err != nil { + return; + } + + pinger.timestamps[seqno] = time.Now(); + if pinger.IPv6 { + _, err = pinger.Connection.WriteTo(encodedPacket, pinger.Host); + } else { + _, err = pinger.Connection.WriteTo(encodedPacket, net.Addr(&net.IPAddr{IP: pinger.Host.IP.To4()})); + } + + return; +} + +/* + Receives a single packet on the Pinger's Connection, and figures out what its sequence number is + to calculate a round-trip time (rtt) for the packet. Returns errors caused by parsing messages. +*/ +func (pinger *Pinger) Recv() (err error) { + buf := make([]byte, 65536); + var size int; + var addr net.Addr; + var msg *icmp.Message; + + // Wait for a ping from the host that we actually pinged + for true { + size, addr, err = pinger.Connection.ReadFrom(buf); + if err != nil { + err = nil; // This is almost certainly a timeout, so just ignore it (for now) + return; + } else if addr.(*net.IPAddr).IP.Equal(pinger.Host.IP) { + var proto int; + if pinger.IPv6 { + proto = ipv6.ICMPTypeEchoRequest.Protocol(); + } else { + proto = ipv4.ICMPTypeEcho.Protocol(); + } + + msg, err = icmp.ParseMessage(proto, buf[:size]); + if err != nil { + return; + } else if (msg.Type == ipv4.ICMPTypeEchoReply || msg.Type == ipv6.ICMPTypeEchoReply ) && msg.Body.(*icmp.Echo).ID == 2 { + break; + } + + } + } + + + + seqno := msg.Body.(*icmp.Echo).Seq; + pinger.RTTS[seqno] = time.Since(pinger.timestamps[seqno]); + return; +} + + + +/* + Pings a single host passed as an argument, and returns a result string that's ready for printing. +*/ +func PingHost(host *net.IPAddr, IPv6 bool, numpings int, payload int) (min, avg, max, std, loss float64, err error) { + pinger, err := New(host, IPv6, payload, numpings); + if err != nil { + return; + } + defer pinger.Connection.Close(); + + var pool sync.WaitGroup; + + for i := 0; i < numpings; i++ { + pool.Add(2); + + go func (seqno int) { + defer pool.Done(); + err = pinger.Send(seqno); + if err != nil { + utils.Warn(err.Error()); + } + }(i); + + go func () { + defer pool.Done(); + err = pinger.Recv(); + if err != nil { + utils.Warn(err.Error()); + } + }(); + } + + // Wait for results + pool.Wait(); + + min = math.Inf(0); + number_of_lost_packets := 0; + for i := 0; i < numpings; i++ { + rtt := float64(pinger.RTTS[i]) / float64(time.Millisecond); + if rtt < 0 { + number_of_lost_packets++; + continue; + } + + if min > rtt { + min = rtt; + } + if max < rtt { + max = rtt; + } + + avg += rtt + } + + avg /= float64(numpings - number_of_lost_packets); + + //Need to loop again once the average is found to get std + for i := 0; i < numpings; i++ { + rtt := float64(pinger.RTTS[i]) / float64(time.Millisecond); + if rtt > 0 { + std += math.Pow(rtt - avg, 2); + } + } + std /= float64(numpings - 1 - number_of_lost_packets); + std = math.Sqrt(std); + + // if all packets are lost, the values of min/avg/max/std are meaningless, so print this instead to avoid confusion + if number_of_lost_packets >= numpings { + return -1, -1, -1, -1, 100, nil; + } else { + loss = float64(number_of_lost_packets)/ float64(numpings) * 100.0; + } + + return; +} diff --git a/ports/ports.go b/ports/ports.go new file mode 100644 index 0000000..e7381ad --- /dev/null +++ b/ports/ports.go @@ -0,0 +1,167 @@ +package ports + +// Copyright 2018 Comcast Cable Communications Management, LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "net" +import "connvitals/utils" +import "time" +import "sync" +import "crypto/tls" +import "bytes" + + + +var request []byte = []byte("HEAD / HTTP/1.1\r\n\r\n"); +const ms = float64(time.Millisecond); +/* + Attempts to connect to a host specified by "host" and return a result of the form: + Response Code, Server Info + where Response Code is the code of a response to a "HEAD / HTTP/1.1" request and + Server Info is the contents of the "Server: " header if present, or "Unkown" otherwise. + If anything goes wrong, it will instead return "None". +*/ +func http(host string) utils.HttpScanResult { + // Create socket + conn, err := net.DialTimeout("tcp", host+":http", 25 * time.Millisecond); + if err != nil { + return utils.HttpScanResult{-1, "", ""}; + } + defer conn.Close(); + + // Set timestamp + ts := time.Now(); + + // Set socket "timeout" + err = conn.SetDeadline(ts.Add(100 * time.Millisecond)); + if err != nil { + return utils.HttpScanResult{-1, "", ""}; // I have no idea why this would happen, unless the fd gets closed for some reason + } + + // Immediately send request + _, err = conn.Write(request); + if err != nil { + return utils.HttpScanResult{-1, "", ""}; + } + + buff := make([]byte, 1000); + _, err = conn.Read(buff); + if err != nil { + return utils.HttpScanResult{-1, "", ""}; + } + + var srv string = "Unkown"; + if srvHeader := bytes.Index(buff, []byte("Server: ")); srvHeader > 0 { + srvEnd := bytes.Index(buff[srvHeader+8:], []byte("\r\n")) + srv = string(buff[srvHeader+8:srvHeader+8+srvEnd]); + } + + return utils.HttpScanResult{float64(time.Since(ts))/ms, string(buff[9:12]), srv}; + +} + +/* + Attempts to connect via TLS to a host specified by "host" and return a result of the form: + Response Code, Server Info + where Response Code is the code of a response to a "HEAD / HTTP/1.1" request and + Server Info is the contents of the "Server: " header if present or "Unkown" otherwise. + If anything goes wrong, it will instead return "None". +*/ +func https(host string) utils.HttpScanResult { + conn, err := tls.Dial("tcp", host+":https", &tls.Config{InsecureSkipVerify: true}); + if err != nil { + return utils.HttpScanResult{-1, "", ""}; + } + + ts := time.Now(); + err = conn.SetDeadline(ts.Add(100 * time.Millisecond)); + if err != nil { + return utils.HttpScanResult{-1, "", ""}; + } + + _, err = conn.Write(request); + if err != nil { + return utils.HttpScanResult{-1, "", ""}; + } + + buff := make([]byte, 1000); + _, err = conn.Read(buff); + if err != nil { + return utils.HttpScanResult{-1, "", ""}; + } + + var srv string = "Unkown"; + if srvHeader := bytes.Index(buff, []byte("Server: ")); srvHeader > 0 { + srvEnd := bytes.Index(buff[srvHeader+8:], []byte("\r\n")) + srv = string(buff[srvHeader+8:srvHeader+8+srvEnd]); + } + + return utils.HttpScanResult{float64(time.Since(ts))/ms, string(buff[9:12]), srv}; +} + +/* + Attempts to connect to a host specified by "host" and return the version of a + MySQL server listening on port 3306 if one can be found. Will otherwise return + "None". +*/ +func mysql(host string) utils.MysqlScanResult { + conn, err := net.DialTimeout("tcp", host+":3306", 25 * time.Millisecond); + if err != nil { + return utils.MysqlScanResult{-1, ""}; + } + + ts := time.Now(); + err = conn.SetDeadline(ts.Add(10 * time.Millisecond)); + if err != nil { + return utils.MysqlScanResult{-1, ""}; + } + + buff := make([]byte, 1000); + _, err = conn.Read(buff); + if err != nil { + return utils.MysqlScanResult{-1, ""}; + } + + return utils.MysqlScanResult{float64(time.Since(ts))/ms, string(buff[5:10])}; +} + +/* + Scans the ports of the host specified by "host" for http(s) and MySQL servers + returns a result that is the concatenation of the results of tests for each server type. +*/ +func Scan(host string, IPv6 bool) (utils.HttpScanResult, utils.HttpScanResult, utils.MysqlScanResult) { + if IPv6 { + host = "["+host+"]"; + } + var httpresult, httpsresult utils.HttpScanResult; + var mysqlresult utils.MysqlScanResult; + var pool sync.WaitGroup; + pool.Add(3); + go func () { + defer pool.Done(); + httpresult = http(host); + }(); + go func () { + defer pool.Done(); + httpsresult = https(host); + }(); + go func () { + defer pool.Done(); + mysqlresult = mysql(host); + }(); + + pool.Wait(); + + return httpresult, httpsresult, mysqlresult; +} diff --git a/setup.py b/setup.py deleted file mode 100755 index e5efe50..0000000 --- a/setup.py +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2018 Comcast Cable Communications Management, LLC - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""A setuptools based setup module. -See: -https://packaging.python.org/en/latest/distributing.html -https://github.com/pypa/sampleproject -""" - -# To use a consistent encoding -import codecs -import os - -# RPMs generated for fedora/rhel/centos need to have a different name -# (debian/ubuntu automatically prepends python3-, but those do not) -import platform - -# Always prefer setuptools over distutils -from setuptools import setup - -pkgname = "connvitals" -if platform.linux_distribution(full_distribution_name=False)[0] in {'centos', 'fedora', 'redhat'}: - pkgname = "python3-"+pkgname - -here = os.path.abspath(os.path.dirname(__file__)) - -# Get the long description from the README file -with codecs.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: - long_description = f.read() - -# Arguments marked as "Required" below must be included for upload to PyPI. -# Fields marked as "Optional" may be commented out. - -setup( - # This is the name of your project. The first time you publish this - # package, this name will be registered for you. It will determine how - # users can install this project, e.g.: - # - # $ pip install sampleproject - # - # And where it will live on PyPI: https://pypi.org/project/sampleproject/ - # - # There are some restrictions on what makes a valid project name - # specification here: - # https://packaging.python.org/specifications/core-metadata/#name - name=pkgname, # Required - - # Versions should comply with PEP 440: - # https://www.python.org/dev/peps/pep-0440/ - # - # For a discussion on single-sourcing the version across setup.py and the - # project code, see - # https://packaging.python.org/en/latest/single_source_version.html - version='4.0.1', # Required - - # This is a one-line description or tagline of what your project does. This - # corresponds to the "Summary" metadata field: - # https://packaging.python.org/specifications/core-metadata/#summary - description='Checks a machines connection to a specific host or list of hosts', # Required - - # This is an optional longer description of your project that represents - # the body of text which users will see when they visit PyPI. - # - # Often, this is the same as your README, so you can just read it in from - # that file directly (as we have already done above) - # - # This field corresponds to the "Description" metadata field: - # https://packaging.python.org/specifications/core-metadata/#description-optional - long_description=long_description, # Optional - - # This should be a valid link to your project's main homepage. - # - # This field corresponds to the "Home-Page" metadata field: - # https://packaging.python.org/specifications/core-metadata/#home-page-optional - url='https://github.com/connvitals', # Optional - - # This should be your name or the name of the organization which owns the - # project. - author='Brennan Fieck', # Optional - - # This should be a valid email address corresponding to the author listed - # above. - author_email='Brennan_WilliamFieck@comcast.com', # Optional - - # Classifiers help users find your project by categorizing it. - # - # For a list of valid classifiers, see - # https://pypi.python.org/pypi?%3Aaction=list_classifiers - classifiers=[ # Optional - # How mature is this project? Common values are - # 3 - Alpha - # 4 - Beta - # 5 - Production/Stable - 'Development Status :: 5 - Production/Stable', - - # Indicate who your project is intended for - 'Intended Audience :: Telecommunications Industry', - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - - # Topic of the project - 'Topic :: Internet', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Scientific/Engineering :: Information Analysis', - 'Topic :: Utilities', - - # Pick your license as you wish - 'License :: Other/Proprietary License', - - # Environment in which this program is designed to run - 'Environment :: Console', - - # Supported Operating Systems - 'Operating Systems :: OS Independent', - - # Specify the Python versions you support here. In particular, ensure - # that you indicate whether you support Python 2, Python 3 or both. - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy' - 'Programming Language :: Python :: 3 :: Only' - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7' - ], - - # This field adds keywords for your project which will appear on the - # project page. What does your project relate to? - # - # Note that this is a string of words separated by whitespace, not a list. - keywords='network statistics connection ping traceroute port ip', # Optional - - # You can just specify package directories manually here if your project is - # simple. Or you can use find_packages(). - # - # Alternatively, if you just want to distribute a single Python file, use - # the `py_modules` argument instead as follows, which will expect a file - # called `my_module.py` to exist: - # - # py_modules=["connvitals", "ping", "traceroute", "ports"], - # - packages=['connvitals'], # Required - - # This field lists other packages that your project depends on to run. - # Any package you put here will be installed by pip when your project is - # installed, so they must be valid existing projects. - # - # For an analysis of "install_requires" vs pip's requirements files see: - # https://packaging.python.org/en/latest/requirements.html - install_requires=['setuptools', 'typing'], # Optional - - # List additional groups of dependencies here (e.g. development - # dependencies). Users will be able to install these using the "extras" - # syntax, for example: - # - # $ pip install sampleproject[dev] - # - # Similar to `install_requires` above, these must be valid existing - # projects. - # extras_require={ # Optional - # 'dev': ['check-manifest'], - # 'test': ['coverage'], - # }, - - # If there are data files included in your packages that need to be - # installed, specify them here. - # - # If using Python 2.6 or earlier, then these have to be included in - # MANIFEST.in as well. - # package_data={ # Optional - # 'sample': ['package_data.dat'], - # }, - - # Although 'package_data' is the preferred approach, in some case you may - # need to place data files outside of your packages. See: - # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files - # - # In this case, 'data_file' will be installed into '/my_data' - # data_files=[('my_data', ['data/data_file'])], # Optional - - # To provide executable scripts, use entry points in preference to the - # "scripts" keyword. Entry points provide cross-platform support and allow - # `pip` to create the appropriate form of executable for the target - # platform. - # - # For example, the following would provide a command called `sample` which - # executes the function `main` from this package when invoked: - entry_points={ # Optional - 'console_scripts': [ - 'connvitals=connvitals.__init__:main', - ], - }, - - # Requires python version >= 3.4, but doesn't support python 4 - python_requires='~=3.4' -) diff --git a/traceroute/traceroute.go b/traceroute/traceroute.go new file mode 100644 index 0000000..3a8969e --- /dev/null +++ b/traceroute/traceroute.go @@ -0,0 +1,314 @@ +package traceroute + +// Copyright 2018 Comcast Cable Communications Management, LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "golang.org/x/net/ipv4" +import "golang.org/x/net/icmp" +import "time" +import "net" +import "golang.org/x/net/ipv6" +import "connvitals/utils" + +const ICMP4 = 1 +const ICMP6 = 58 + + +/* + Contains a transparent interface for ipv4 and ipv6 communication + IPv6: tells whether this structure should serve ipv4 or ipv6 + ipv6connection: either a reference to an ipv6 packet connection, or nil if !IPv6 + ipv4connection: either a reference to an ipv4 packet connection, or nil if IPv6 +*/ +type Conn struct { + IPv6 bool; + ipv6connection *ipv6.PacketConn; + ipv4connection *ipv4.PacketConn; +}; + +/* + Constructs and returns a new Conn object. Will listen on "0.0.0.0" for ipv4, or let the machine decide + what address to bind for ipv6 +*/ +func NewConn(IPv6 bool) (conn Conn, err error) { + // Empty connection + conn = Conn{ + IPv6, + nil, + nil, + }; + + // Set network and listening address according to whether or not we're using ipv6 + var network, laddr string; + if IPv6 { + network = "ip6:ipv6-icmp"; + laddr = ""; + } else { + network = "ip4:icmp"; + laddr = "0.0.0.0"; + } + + // Construct basic packet connection + c, err := net.ListenPacket(network, laddr); + if err != nil { + return; + } + + // Wrap the connection appropriately + if IPv6 { + conn.ipv6connection = ipv6.NewPacketConn(c); + } else { + conn.ipv4connection = ipv4.NewPacketConn(c); + } + + return; +} + + +/* + Sends a sequential ping to the host. + Returns an error if packet construction or writing returns an error +*/ +func (conn *Conn) SendTo(seqno int, host net.IPAddr) (err error) { + var pkt []byte; + var psh []byte = nil; + + if conn.IPv6 { + // Oh, you were kidding me + psh = icmp.IPv6PseudoHeader(conn.ipv6connection.LocalAddr().(*net.IPAddr).IP, host.IP); + } + + msg := conn.MkPacket(seqno); + pkt, err = msg.Marshal(psh); + if err != nil { + return; + } + + if conn.IPv6 { + _, err = conn.ipv6connection.WriteTo(pkt, nil, net.Addr(&host)); + } else { + _, err = conn.ipv4connection.WriteTo(pkt, nil, net.Addr(&host)); + } + return; +} + + +/* + Transparently sets a deadline, returning any error thereby incurred +*/ +func (conn *Conn) SetDeadline(t time.Time) (error) { + if conn.IPv6 { + return conn.ipv6connection.SetDeadline(t); + } else { + return conn.ipv4connection.SetDeadline(t); + } +} + +/* + Sets hop limit on IPv6, and ttl on ipv4, without requiring the caller to know which +*/ +func (conn *Conn) SetMaxHops(n int) error { + if conn.IPv6 { + return conn.ipv6connection.SetHopLimit(n); + } + return conn.ipv4connection.SetTTL(n); +} + +/* + Reads data from an ipv4 or ipv6 connection into the buffer provided by buff. + Returns the amount of data read, the address that sent the data, and any errors raised by the read. + (control flags are dropped on receipt) +*/ +func (conn *Conn) RecvFrom(buff []byte) (amt int, addr net.Addr, err error) { + if conn.IPv6 { + amt, _, addr, err = conn.ipv6connection.ReadFrom(buff); + } else { + amt, _, addr, err = conn.ipv4connection.ReadFrom(buff); + } + return; +} + + +/* + Parses ICMP and ICMPv6 messages, returning the message if successful, else an error +*/ +func (conn *Conn) ICMPParse(pkt []byte) (msg *icmp.Message, err error) { + if conn.IPv6 { + msg, err = icmp.ParseMessage(ICMP6, pkt); + } else { + msg, err = icmp.ParseMessage(ICMP4, pkt); + } + if err != nil { + return nil, err; + } + + return; +} + +/* + Closes a Conn object's underlying ipv4 or ipv6 connection +*/ +func (conn *Conn) Close() { + if conn.IPv6 { + conn.ipv6connection.Close(); + } else { + conn.ipv4connection.Close(); + } +} + +/* + Contains the data necessary to run a route trace. + Host: the ip address of the host to which the trace runs + Max: Maximum number of network hops to go through before giving up + Connection: A persistent network connection to the Host. +*/ +type Tracer struct { + Host *net.IPAddr; + Max int; + Connection Conn; + IPv6 bool; +}; + +/* + Constructs a new Tracer object, initializing its Connection. +*/ +func New(host *net.IPAddr, max int, IPv6 bool) (tracer *Tracer, err error) { + conn, err := NewConn(IPv6); + if err != nil { + return; + } + + tracer = &Tracer{ + host, + max, + conn, + IPv6, + }; + return; +} + +/* + Runs route tracing by sequentially sending packets with a TTL that increments from 1 to the Tracer's Max value. + Returns a string of results, and prints warnings to stderr if a non-timeout error occurs. + Returns an error if the maximum number of hops was reached without finding a route to the Host. +*/ +func (tracer *Tracer) Run() ([]utils.Step, error) { + defer tracer.Connection.Close(); + + // pre-allocated memory for message contents + buff := make([]byte, 1500); + + // allocate enough memory to hold all of our results + results := make([]utils.Step, tracer.Max); + + // increments ttl each iteration + for i := 0; i < tracer.Max; i++ { + tracer.Connection.SetMaxHops(i+1); + + // Re-set deadline for this hop + ts := time.Now(); + tracer.Connection.SetDeadline(ts.Add(100*time.Millisecond)); + + // Send a packet + err := tracer.Connection.SendTo(i, *tracer.Host); + if err != nil { + results[i] = utils.Step{"*", -1}; + utils.Warn(err.Error()); + continue; + } + + var rtt float64; // stores round-trip-time in milliseconds + var size int; // the amount of data received/size of the packet + var addr net.Addr; // address that sent the data + var dest net.IP; // original destination (used when received data implements an ICMP Time Exceeded response packet) + var msg *icmp.Message; // holds the received data in the form of an ICMP packet + + // Keep receiving packets until we get a response to the packet we sent + for true { + + // Receive a packet + size, addr, err = tracer.Connection.RecvFrom(buff); + if err != nil { + //Likely a timeout + results[i] = utils.Step{"*", -1}; + break; + } + + // Record the round-trip-time immediately + rtt = float64(time.Since(ts)) / float64(time.Millisecond); + + msg, err = tracer.Connection.ICMPParse(buff[:size]); + if err != nil { + results[i] = utils.Step{"*", -1}; + utils.Warn(err.Error()); + break; + } + + //Handle the different message types. Will set the 'dest' var if the type is TimeExceeded + switch msg.Type { + + // TTL/Hop_Limit Exceeded - figure out how far it got + case ipv6.ICMPTypeTimeExceeded: + dest = net.IP((*msg).Body.(*icmp.TimeExceeded).Data[24:40]); + case ipv4.ICMPTypeTimeExceeded: + var parts []byte = (*msg).Body.(*icmp.TimeExceeded).Data[16:20]; + dest = net.IPv4(parts[0], parts[1], parts[2], parts[3]); + + // Reply from target, figure out if it's our target and the packet was sent by a tracer + case ipv4.ICMPTypeEchoReply: + fallthrough; + case ipv6.ICMPTypeEchoReply: + if addr.(*net.IPAddr).IP.Equal(tracer.Host.IP) && msg.Body.(*icmp.Echo).ID == 1 { + results[i] = utils.Step{addr.String(), rtt}; + return results[:i+1], nil; + } + } + + // If the packet was a Time Exceeded message, check if it was sent by our tracer. If yes, record result and move on. + if dest.Equal(tracer.Host.IP) { + results[i] = utils.Step{addr.String(), rtt}; + break; + } + + } + } + + // This statement should only be reached if the maximum number of hops was used to try (and fail) to reach the target. + err := utils.GenericError{"Route was longer than the maximum-allowed TTL, or host '"+tracer.Host.String()+"' could not be reached"}; + return results, err; +} + + +/* + Constructs an icmp packet to send along a connection +*/ +func (conn *Conn) MkPacket(seqno int) (msg icmp.Message) { + var typ icmp.Type; + if conn.IPv6 { + typ = ipv6.ICMPTypeEchoRequest; + } else { + typ = ipv4.ICMPTypeEcho; + } + + msg = icmp.Message{ + Type: typ, + Code: 0, + Body: &icmp.Echo{ + ID: 1, + Seq: seqno, + Data: make([]byte, 1), + }, + }; + return; +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..7ce1b8a --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,254 @@ +package utils + +// Copyright 2018 Comcast Cable Communications Management, LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "fmt" +import "os" +import "time" +import "bytes" +import "net" +import "syscall" +import "unsafe" +import "runtime" + +//////////////////////////////////////////////////////// +// Route Trace Objects // +//////////////////////////////////////////////////////// + +/* + A type that holds information about a specific step in a route trace +*/ +type Step struct { + Host string; + RTT float64; +}; + +/* + Returns a Step object's representation in JSON format +*/ +func (step Step) JSON() string { + if step.RTT < 0 || step.Host == "*" { + return "[\"*\"]"; + } + return fmt.Sprintf("[\"%s\", %f]", step.Host, step.RTT); +} + +/* + Returns a Step objects representation as a tab-separated list +*/ +func (step Step) String() string { + if step.RTT < 0 || step.Host == "*" { + return "*"; + } + return fmt.Sprintf("%s\t%.3f", step.Host, step.RTT); +} + + +//////////////////////////////////////////////////////// +// Port Scan Objects // +//////////////////////////////////////////////////////// + +/* + A type that represents the information gathered during an http(s) port scan +*/ +type HttpScanResult struct { + RTT float64; + Response string; + Server string; +}; + +/* + Returns an HttpScanResult object's representation in JSON format +*/ +func (res HttpScanResult) JSON() string { + if res.RTT < 0 || (res.Response == "" && res.Server == "") { + return "\"None\""; + } + return fmt.Sprintf("{\"rtt\":%f,\"response code\":\"%s\",\"server\":\"%s\"}", res.RTT, res.Response, res.Server); +} + +/* + Returns an HttpScanResult object's representation as a delimited list +*/ +func (res HttpScanResult) String() string { + if res.RTT < 0 || (res.Response == "" && res.Server == "") { + return "None"; + } + return fmt.Sprintf("%.3f, %s, %s", res.RTT, res.Response, res.Server); +} + +/* + A type that represents the information gathered during a mysql port scan +*/ +type MysqlScanResult struct { + RTT float64; + Version string; +}; + +/* + Returns the JSON representation of a MysqlScanResult object +*/ +func (res MysqlScanResult) JSON() string { + if res.RTT < 0 || res.Version == "" { + return "\"None\""; + } + return fmt.Sprintf("{\"rtt\":%f,\"version\":\"%s\"}", res.RTT, res.Version); +} + +/* + Returns the delimited, string representation of a MysqlScanResult object +*/ +func (res MysqlScanResult) String() string { + if res.RTT < 0 || res.Version == "" { + return "None"; + } + return fmt.Sprintf("%.3f, %s", res.RTT, res.Version); +} + + +//////////////////////////////////////////////////////// +// Printing/Logging // +//////////////////////////////////////////////////////// + +/* + Prints results, in either JSON or plaintext format, as specified by `json` +*/ +func Print(json bool, hostnames, pingResults, traceResults, scanResults map[*net.IPAddr]string) { + var output_buffer bytes.Buffer; + + if json { + for addr, name := range hostnames { + output_buffer.WriteString("{\"addr\":\""); + var wrotePings, wroteRoutes bool; + output_buffer.WriteString(addr.String()); + output_buffer.WriteString("\",\"name\":\""); + output_buffer.WriteString(name); + output_buffer.WriteString("\","); + if pingResult, resultsRecorded := pingResults[addr]; resultsRecorded { + output_buffer.WriteString("\"ping\":"); + output_buffer.WriteString(pingResult); + wrotePings = true; + } + + if traceResult, resultsRecorded := traceResults[addr]; resultsRecorded { + if wrotePings { + output_buffer.WriteRune(','); + } + output_buffer.WriteString("\"trace\":"); + output_buffer.WriteString(traceResult); + wroteRoutes = true; + } + + if scanResult, resultsRecorded := scanResults[addr]; resultsRecorded { + if wrotePings || wroteRoutes { + output_buffer.WriteRune(','); + } + output_buffer.WriteString("\"scan\":"); + output_buffer.WriteString(scanResult); + } + output_buffer.WriteRune('}'); + fmt.Println(output_buffer.String()); + output_buffer.Reset(); + } + + + } else { + for addr, name := range hostnames { + if addr.String() == name { + fmt.Println(addr.String()); + } else { + fmt.Printf("%s (%s)\n", name, addr.String()); + } + if pingResult, resultsRecorded := pingResults[addr]; resultsRecorded { + fmt.Println(pingResult); + } + if traceResult, resultsRecorded := traceResults[addr]; resultsRecorded { + fmt.Println(traceResult); + } + if scanResult, resultsRecorded := scanResults[addr]; resultsRecorded { + fmt.Println(scanResult); + } + } + } +} + + +func isatty(fd uintptr) bool { + var termios syscall.Termios; + var call uintptr; + + switch runtime.GOOS { + case "linux": + call = 0x5401; + case "freebsd": + fallthrough; + case "openbsd": + fallthrough; + case "netbsd": + fallthrough; + case "dragonfly": + fallthrough; + case "darwin": + call = 0x40487413; + default: + return false; + } + + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, call, uintptr(unsafe.Pointer(&termios)), 0, 0, 0); + return err == 0; +} + +/* + Prints a warning and timestamp to stderr +*/ +func Warn(warning string) { + printstr := "WW: %s -\t%s\n"; + + if isatty(os.Stderr.Fd()) { + printstr = "\033[38;2;238;216;78m" + printstr + "\033[m"; + } + fmt.Fprintf(os.Stderr, "WW: %s -\t%s\n", warning, time.Now().Format(time.UnixDate)); +} + +/* + Generic Error type to aid error construction. +*/ +type GenericError struct { + Msg string; +}; + +/* + Returns the error message as a string + (Necessary to implement the `error` interface) +*/ +func (err GenericError) Error() string { + return err.Msg; +} + +/* + Prints an error and associated information to stderr, and exits with the exit code indicated by fatal if fatal is non-zero +*/ +func Error(err interface{ Error() string}, fatal int) { + printstr := "EE: %T: %s -\t%s\n"; + + if isatty(os.Stderr.Fd()) { + printstr = "\033[38;2;255;0;0m" + printstr + "\033[m"; + } + + fmt.Fprintf(os.Stderr, printstr, err, err.Error(), time.Now().Format(time.UnixDate)); + if fatal != 0 { + os.Exit(fatal); + } +}