diff -Nru 2ping-4.4.1/2ping 2ping-4.5/2ping --- 2ping-4.4.1/2ping 2020-05-09 22:32:44.000000000 +0000 +++ 2ping-4.5/2ping 2020-06-17 21:22:00.000000000 +0000 @@ -18,7 +18,13 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. -if __name__ == '__main__': - import sys - import twoping.cli - sys.exit(twoping.cli.main()) +import sys +import twoping.cli + + +def module_init(): + if __name__ == "__main__": + sys.exit(twoping.cli.main(sys.argv)) + + +module_init() diff -Nru 2ping-4.4.1/2ping6 2ping-4.5/2ping6 --- 2ping-4.4.1/2ping6 2020-05-09 22:32:44.000000000 +0000 +++ 2ping-4.5/2ping6 2020-06-17 21:22:00.000000000 +0000 @@ -18,7 +18,13 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. -if __name__ == '__main__': - import sys - import twoping.cli - sys.exit(twoping.cli.main()) +import sys +import twoping.cli + + +def module_init(): + if __name__ == "__main__": + sys.exit(twoping.cli.main(sys.argv)) + + +module_init() diff -Nru 2ping-4.4.1/2ping.egg-info/PKG-INFO 2ping-4.5/2ping.egg-info/PKG-INFO --- 2ping-4.4.1/2ping.egg-info/PKG-INFO 2020-06-08 20:47:39.000000000 +0000 +++ 2ping-4.5/2ping.egg-info/PKG-INFO 2020-06-18 03:29:04.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: 2ping -Version: 4.4.1 +Version: 4.5 Summary: 2ping a bi-directional ping utility Home-page: https://www.finnie.org/software/2ping/ Author: Ryan Finnie @@ -9,6 +9,9 @@ Download-URL: https://www.finnie.org/software/2ping/ Description: # 2ping - A bi-directional ping utility + ![ci](https://github.com/rfinnie/2ping/workflows/ci/badge.svg) + ![snapcraft](https://github.com/rfinnie/2ping/workflows/snapcraft/badge.svg) + https://www.finnie.org/software/2ping/ ## About @@ -29,7 +32,8 @@ * [distro](https://pypi.org/project/distro/) for system distribution detection * [dnspython](https://pypi.org/project/dnspython/) for --srv * [netifaces](https://pypi.org/project/netifaces/) for listening on all addresses in --listen mode - * [pycrypto](https://pypi.org/project/pycrypto/) for --encrypt + * [pycryptodomex](https://pypi.org/project/pycryptodomex/) (recommended) or [pycryptodome](https://pypi.org/project/pycryptodome/) or [pycrypto](https://pypi.org/project/pycrypto/) for --encrypt + * [systemd](https://pypi.org/project/systemd/) for using systemd-supplied sockets ## Usage diff -Nru 2ping-4.4.1/2ping.egg-info/SOURCES.txt 2ping-4.5/2ping.egg-info/SOURCES.txt --- 2ping-4.4.1/2ping.egg-info/SOURCES.txt 2020-06-08 20:47:39.000000000 +0000 +++ 2ping-4.5/2ping.egg-info/SOURCES.txt 2020-06-18 03:29:04.000000000 +0000 @@ -8,8 +8,10 @@ MANIFEST.in Makefile README.md +requirements.txt setup.py snapcraft.yaml +tox.ini 2ping.egg-info/PKG-INFO 2ping.egg-info/SOURCES.txt 2ping.egg-info/dependency_links.txt @@ -21,14 +23,13 @@ doc/2ping.md doc/Makefile tests/__init__.py -tests/test_best_poller.py tests/test_cli.py tests/test_crc32.py tests/test_packets.py +tests/test_python.py tests/test_utils.py twoping/__init__.py twoping/args.py -twoping/best_poller.py twoping/cli.py twoping/crc32.py twoping/packets.py diff -Nru 2ping-4.4.1/2ping.spec 2ping-4.5/2ping.spec --- 2ping-4.4.1/2ping.spec 2020-06-08 20:14:04.000000000 +0000 +++ 2ping-4.5/2ping.spec 2020-06-18 03:21:39.000000000 +0000 @@ -1,5 +1,5 @@ Name: 2ping -Version: 4.4.1 +Version: 4.5 Release: 1%{?dist} Summary: Bi-directional ping utility License: GPLv2+ @@ -7,6 +7,7 @@ Source0: https://www.finnie.org/software/%{name}/%{name}-%{version}.tar.gz BuildArch: noarch BuildRequires: python3-devel +BuildRequires: python3-pytest BuildRequires: python3-setuptools %description @@ -27,7 +28,7 @@ install -Dp -m 0644 doc/2ping.1 %{buildroot}/%{_mandir}/man1/2ping6.1 %check -%{__python3} setup.py test +%{__python3} -mpytest %post %systemd_post 2ping.service diff -Nru 2ping-4.4.1/ChangeLog 2ping-4.5/ChangeLog --- 2ping-4.4.1/ChangeLog 2020-06-08 20:14:04.000000000 +0000 +++ 2ping-4.5/ChangeLog 2020-06-18 03:10:28.000000000 +0000 @@ -1,3 +1,23 @@ +2ping 4.5 (2020-06-18) + * Added PyCryptodome support (recommended over PyCrypto, though the latter + is still detected/supported). + * Replaced best_poller module with Python native selectors module. + * Changed --flood output: dots/backspaces are no longer printed, and loss + results / errors display full details. + * --audible tones will only occur if stdout is a TTY. + * Improved hostname/IP display edge cases. + * Added an AF_UNIX --loopback test mode. + * Listener sockets are added and removed as needed, instead of being + re-created on each rescan. + * Listener sockets are automatically rescanned periodically. + * Multiple systemd sockets are now allowed. + * A run can be both a listener and a client at the same time (mainly + useful for smoke testing). + * Other socket handling refactoring. + * Other code refactoring. + * Listener statistics are displayer per-bind. + * Many, many testing/CI improvements. + 2ping 4.4.1 (2020-06-08) * Fixed 2ping.spec referencing old README and making `rpmbuild -ta 2ping.tar.gz` fail. diff -Nru 2ping-4.4.1/debian/changelog 2ping-4.5/debian/changelog --- 2ping-4.4.1/debian/changelog 2020-06-08 21:45:25.000000000 +0000 +++ 2ping-4.5/debian/changelog 2020-06-18 03:51:00.000000000 +0000 @@ -1,3 +1,14 @@ +2ping (4.5-1) unstable; urgency=medium + + * New upstream release + * Add autopkgtest suite + * Recommends: python3-distro + * Suggests: python3-systemd + * Replace Suggests: python3-crypto with python3-pycryptodome + (Either are supported by release, but the former is deprecated) + + -- Ryan Finnie Wed, 17 Jun 2020 20:51:00 -0700 + 2ping (4.4.1-1) unstable; urgency=medium * New upstream release diff -Nru 2ping-4.4.1/debian/control 2ping-4.5/debian/control --- 2ping-4.4.1/debian/control 2020-06-08 21:26:36.000000000 +0000 +++ 2ping-4.5/debian/control 2020-06-18 03:38:01.000000000 +0000 @@ -2,7 +2,7 @@ Section: net Priority: optional Maintainer: Ryan Finnie -Build-Depends: debhelper, debhelper-compat (= 13), dpkg-dev (>= 1.16.1~), python3-setuptools, python3-all, dh-python, bash-completion +Build-Depends: debhelper, debhelper-compat (= 13), dpkg-dev (>= 1.16.1~), python3-pytest, python3-setuptools, python3-all, dh-python, bash-completion Rules-Requires-Root: no Standards-Version: 4.5.0 Homepage: https://www.finnie.org/software/2ping/ @@ -13,8 +13,8 @@ Architecture: all Pre-Depends: ${misc:Pre-Depends} Depends: ${shlibs:Depends}, ${misc:Depends}, ${python3:Depends}, python3-pkg-resources, lsb-base (>= 3.0-6) -Recommends: python3-dnspython, python3-netifaces -Suggests: python3-crypto +Recommends: python3-distro, python3-dnspython, python3-netifaces +Suggests: python3-pycryptodome, python3-systemd Description: Ping utility to determine directional packet loss 2ping is a bi-directional ping utility. It uses 3-way pings (akin to TCP SYN, SYN/ACK, ACK) and after-the-fact state comparison between a diff -Nru 2ping-4.4.1/debian/rules 2ping-4.5/debian/rules --- 2ping-4.4.1/debian/rules 2020-06-08 21:34:53.000000000 +0000 +++ 2ping-4.5/debian/rules 2020-06-18 03:51:00.000000000 +0000 @@ -5,6 +5,7 @@ #export DH_VERBOSE=1 export PYBUILD_NAME=2ping +export PYBUILD_TEST_PYTEST=1 %: dh $@ --with=python3,bash_completion --buildsystem=pybuild @@ -19,7 +20,7 @@ override_dh_auto_clean: dh_auto_clean - $(RM) -r twoping/*.pyc 2ping.egg-info + $(RM) -r .pytest_cache override_dh_installsystemd: dh_installsystemd --no-enable --no-start diff -Nru 2ping-4.4.1/debian/source/options 2ping-4.5/debian/source/options --- 2ping-4.4.1/debian/source/options 2020-06-08 21:26:36.000000000 +0000 +++ 2ping-4.5/debian/source/options 2020-06-18 03:51:00.000000000 +0000 @@ -2,3 +2,5 @@ diff-ignore # Ignore VCS files (native packages) #tar-ignore +# Ignore SOURCES.txt modification +extend-diff-ignore = "\.egg-info" diff -Nru 2ping-4.4.1/debian/tests/control 2ping-4.5/debian/tests/control --- 2ping-4.4.1/debian/tests/control 1970-01-01 00:00:00.000000000 +0000 +++ 2ping-4.5/debian/tests/control 2020-06-18 03:44:33.000000000 +0000 @@ -0,0 +1,3 @@ +Tests: help, smoke +Depends: 2ping, python3-distro, python3-dnspython, python3-netifaces, python3-pycryptodome, python3-systemd +Restrictions: allow-stderr diff -Nru 2ping-4.4.1/debian/tests/help 2ping-4.5/debian/tests/help --- 2ping-4.4.1/debian/tests/help 1970-01-01 00:00:00.000000000 +0000 +++ 2ping-4.5/debian/tests/help 2020-06-18 03:34:05.000000000 +0000 @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e + +2ping --help diff -Nru 2ping-4.4.1/debian/tests/smoke 2ping-4.5/debian/tests/smoke --- 2ping-4.4.1/debian/tests/smoke 1970-01-01 00:00:00.000000000 +0000 +++ 2ping-4.5/debian/tests/smoke 2020-06-18 03:45:36.000000000 +0000 @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e + +2ping --count=10 --interval=0.2 --port=-1 --interface-address=127.0.0.1 --listen --nagios=1000,5%,1000,5% 127.0.0.1 diff -Nru 2ping-4.4.1/doc/2ping.1 2ping-4.5/doc/2ping.1 --- 2ping-4.4.1/doc/2ping.1 2020-06-07 06:43:16.000000000 +0000 +++ 2ping-4.5/doc/2ping.1 2020-06-17 21:22:00.000000000 +0000 @@ -185,7 +185,7 @@ .B \-\-encrypt=\f[I]key\f[R] Set a shared key, encrypt 2ping packets, and require encrypted packets from peers encrypted with the same shared key. -Requires the PyCrypto module. +Requires the PyCryptodome or PyCrypto module. .TP .B \-\-encrypt\-method=\f[I]method\f[R] When \f[I]\-\-encrypt\f[R] is used, specify the method used to encrypt @@ -213,6 +213,13 @@ When run as a listener, a SIGHUP will reload the configuration on all interfaces. .TP +.B \-\-loopback +Use one or more client/listener pairs of UNIX datagram sockets. +Mainly for testing purposes. +.TP +.B \-\-loopback\-pairs=\f[I]pairs\f[R] +Number of pairs to generate when using \f[I]\-\-loopback\f[R]. +.TP .B \-\-min\-packet\-size=\f[I]min\f[R] Set the minimum total payload size to \f[I]min\f[R] bytes, default 128. If the payload is smaller than \f[I]min\f[R] bytes, padding will be @@ -275,6 +282,13 @@ With \f[I]\-\-listen\f[R], this is the port to bind as, otherwise this is the port to send to. Default is UDP port 15998. +.RS +.PP +When port \f[I]\[lq]\-1\[rq]\f[R] is specified, a random unused high +port is picked. +This is useful for automated unit and functional testing, but not for +normal use. +.RE .TP .B \-\-send\-monotonic\-clock Send a monotonic clock value with each packet. diff -Nru 2ping-4.4.1/doc/2ping.md 2ping-4.5/doc/2ping.md --- 2ping-4.4.1/doc/2ping.md 2020-06-07 06:43:16.000000000 +0000 +++ 2ping-4.5/doc/2ping.md 2020-06-17 21:22:00.000000000 +0000 @@ -123,7 +123,7 @@ \-\-encrypt=*key* : Set a shared key, encrypt 2ping packets, and require encrypted packets from peers encrypted with the same shared key. - Requires the PyCrypto module. + Requires the PyCryptodome or PyCrypto module. \-\-encrypt-method=*method* : When *\-\-encrypt* is used, specify the method used to encrypt packets. @@ -144,6 +144,13 @@ A listener is required as the remote end for a client. When run as a listener, a SIGHUP will reload the configuration on all interfaces. +\-\-loopback +: Use one or more client/listener pairs of UNIX datagram sockets. + Mainly for testing purposes. + +\-\-loopback-pairs=*pairs* +: Number of pairs to generate when using *\-\-loopback*. + \-\-min-packet-size=*min* : Set the minimum total payload size to *min* bytes, default 128. If the payload is smaller than *min* bytes, padding will be added to the end of the packet. @@ -187,6 +194,9 @@ With *\-\-listen*, this is the port to bind as, otherwise this is the port to send to. Default is UDP port 15998. + When port *"-1"* is specified, a random unused high port is picked. + This is useful for automated unit and functional testing, but not for normal use. + \-\-send-monotonic-clock : Send a monotonic clock value with each packet. Peer time (if sent by the peer) can be viewed with *\-\-verbose*. diff -Nru 2ping-4.4.1/Makefile 2ping-4.5/Makefile --- 2ping-4.4.1/Makefile 2020-05-13 23:30:08.000000000 +0000 +++ 2ping-4.5/Makefile 2020-06-17 21:22:00.000000000 +0000 @@ -1,5 +1,3 @@ -FIND := find -PANDOC := pandoc PYTHON := python3 all: build @@ -8,14 +6,16 @@ $(PYTHON) setup.py build lint: - # TODO: remove C901 once complexity is reduced - $(FIND) setup.py tests twoping -name '*.py' -print0 | xargs \ - -0 $(PYTHON) -mflake8 --config=/dev/null \ - --ignore=C901,E203,E231,W503 --max-line-length=120 \ - --max-complexity=10 + $(PYTHON) -mtox -e flake8 -test: black lint build - $(PYTHON) setup.py test +test: + $(PYTHON) -mtox + +test-quick: + $(PYTHON) -mtox -e black,flake8,pytest-quick + +black-check: + $(PYTHON) -mtox -e black black: $(PYTHON) -mblack $(CURDIR) diff -Nru 2ping-4.4.1/MANIFEST.in 2ping-4.5/MANIFEST.in --- 2ping-4.4.1/MANIFEST.in 2020-06-08 20:07:30.000000000 +0000 +++ 2ping-4.5/MANIFEST.in 2020-06-18 02:51:37.000000000 +0000 @@ -2,12 +2,18 @@ include ChangeLog include Makefile include *.md -include doc/* +include doc/Makefile +include doc/*.1 +include doc/*.md +include doc/*.py include 2ping include 2ping.service include 2ping.spec include 2ping6 include 2ping.bash_completion include tests/*.py -include wireshark/* +include wireshark/*.pcap +include wireshark/*.lua include snapcraft.yaml +include requirements.txt +include tox.ini diff -Nru 2ping-4.4.1/PKG-INFO 2ping-4.5/PKG-INFO --- 2ping-4.4.1/PKG-INFO 2020-06-08 20:47:39.685067700 +0000 +++ 2ping-4.5/PKG-INFO 2020-06-18 03:29:04.769281900 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: 2ping -Version: 4.4.1 +Version: 4.5 Summary: 2ping a bi-directional ping utility Home-page: https://www.finnie.org/software/2ping/ Author: Ryan Finnie @@ -9,6 +9,9 @@ Download-URL: https://www.finnie.org/software/2ping/ Description: # 2ping - A bi-directional ping utility + ![ci](https://github.com/rfinnie/2ping/workflows/ci/badge.svg) + ![snapcraft](https://github.com/rfinnie/2ping/workflows/snapcraft/badge.svg) + https://www.finnie.org/software/2ping/ ## About @@ -29,7 +32,8 @@ * [distro](https://pypi.org/project/distro/) for system distribution detection * [dnspython](https://pypi.org/project/dnspython/) for --srv * [netifaces](https://pypi.org/project/netifaces/) for listening on all addresses in --listen mode - * [pycrypto](https://pypi.org/project/pycrypto/) for --encrypt + * [pycryptodomex](https://pypi.org/project/pycryptodomex/) (recommended) or [pycryptodome](https://pypi.org/project/pycryptodome/) or [pycrypto](https://pypi.org/project/pycrypto/) for --encrypt + * [systemd](https://pypi.org/project/systemd/) for using systemd-supplied sockets ## Usage diff -Nru 2ping-4.4.1/README.md 2ping-4.5/README.md --- 2ping-4.4.1/README.md 2020-06-07 04:57:00.000000000 +0000 +++ 2ping-4.5/README.md 2020-06-17 21:22:00.000000000 +0000 @@ -1,5 +1,8 @@ # 2ping - A bi-directional ping utility +![ci](https://github.com/rfinnie/2ping/workflows/ci/badge.svg) +![snapcraft](https://github.com/rfinnie/2ping/workflows/snapcraft/badge.svg) + https://www.finnie.org/software/2ping/ ## About @@ -20,7 +23,8 @@ * [distro](https://pypi.org/project/distro/) for system distribution detection * [dnspython](https://pypi.org/project/dnspython/) for --srv * [netifaces](https://pypi.org/project/netifaces/) for listening on all addresses in --listen mode -* [pycrypto](https://pypi.org/project/pycrypto/) for --encrypt +* [pycryptodomex](https://pypi.org/project/pycryptodomex/) (recommended) or [pycryptodome](https://pypi.org/project/pycryptodome/) or [pycrypto](https://pypi.org/project/pycrypto/) for --encrypt +* [systemd](https://pypi.org/project/systemd/) for using systemd-supplied sockets ## Usage diff -Nru 2ping-4.4.1/requirements.txt 2ping-4.5/requirements.txt --- 2ping-4.4.1/requirements.txt 1970-01-01 00:00:00.000000000 +0000 +++ 2ping-4.5/requirements.txt 2020-06-17 21:22:00.000000000 +0000 @@ -0,0 +1,4 @@ +distro +dnspython +netifaces +pycryptodomex diff -Nru 2ping-4.4.1/setup.py 2ping-4.5/setup.py --- 2ping-4.4.1/setup.py 2020-06-08 20:14:04.000000000 +0000 +++ 2ping-4.5/setup.py 2020-06-18 03:14:32.000000000 +0000 @@ -6,7 +6,7 @@ from setuptools import setup -__version__ = "4.4.1" +__version__ = "4.5" assert sys.version_info > (3, 5) @@ -46,5 +46,4 @@ entry_points={ "console_scripts": ["2ping = twoping.cli:main", "2ping6 = twoping.cli:main"] }, - test_suite="tests", ) diff -Nru 2ping-4.4.1/snapcraft.yaml 2ping-4.5/snapcraft.yaml --- 2ping-4.4.1/snapcraft.yaml 2020-06-08 20:14:04.000000000 +0000 +++ 2ping-4.5/snapcraft.yaml 2020-06-17 21:22:00.000000000 +0000 @@ -1,5 +1,5 @@ name: 2ping -version: '4.4.1' +version: git summary: 2ping bi-directional ping utility description: | 2ping is a bi-directional ping utility. It uses 3-way pings (akin to @@ -12,6 +12,7 @@ architectures: - build-on: amd64 - build-on: arm64 + - build-on: armhf - build-on: ppc64el - build-on: s390x @@ -23,7 +24,7 @@ - distro - dnspython - netifaces - - pycrypto + - pycryptodomex apps: 2ping: diff -Nru 2ping-4.4.1/tests/__init__.py 2ping-4.5/tests/__init__.py --- 2ping-4.4.1/tests/__init__.py 2020-06-07 04:57:00.000000000 +0000 +++ 2ping-4.5/tests/__init__.py 2020-06-17 21:22:00.000000000 +0000 @@ -1,9 +1,13 @@ -import time -import unittest +import unittest.mock -class Test2Ping(unittest.TestCase): - def test_monotonic(self): - val1 = time.monotonic() - val2 = time.monotonic() - self.assertGreater(val2, val1) +def _test_module_init(module, main_name="main"): + with unittest.mock.patch.object( + module, main_name, return_value=0 + ), unittest.mock.patch.object( + module, "__name__", "__main__" + ), unittest.mock.patch.object( + module.sys, "exit" + ) as exit: + module.module_init() + return exit.call_args[0][0] == 0 diff -Nru 2ping-4.4.1/tests/test_best_poller.py 2ping-4.5/tests/test_best_poller.py --- 2ping-4.4.1/tests/test_best_poller.py 2020-06-07 05:22:29.000000000 +0000 +++ 2ping-4.5/tests/test_best_poller.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,53 +0,0 @@ -import socket -import unittest - -from twoping import best_poller - - -class TestBestPoller(unittest.TestCase): - def test_register(self): - poller = best_poller.best_poller() - s = socket.socket() - poller.register(s) - self.assertEqual(poller.f_dict[s.fileno()], s) - s.close() - - def test_register_multiple(self): - poller = best_poller.best_poller() - s1 = socket.socket() - s2 = socket.socket() - poller.register(s1) - poller.register(s2) - self.assertEqual( - (poller.f_dict[s1.fileno()], poller.f_dict[s2.fileno()]), (s1, s2) - ) - s1.close() - s2.close() - - def test_register_idempotent(self): - poller = best_poller.best_poller() - s = socket.socket() - poller.register(s) - poller.register(s) - self.assertEqual(len(poller.f_dict), 1) - s.close() - - def test_available_pollers(self): - self.assertTrue(len(best_poller.available_pollers()) > 0) - - def test_known_poller(self): - self.assertTrue( - isinstance( - best_poller.best_poller(), - ( - best_poller.EpollPoller, - best_poller.KqueuePoller, - best_poller.PollPoller, - best_poller.SelectPoller, - ), - ) - ) - - -if __name__ == "__main__": - unittest.main() diff -Nru 2ping-4.4.1/tests/test_cli.py 2ping-4.5/tests/test_cli.py --- 2ping-4.4.1/tests/test_cli.py 2020-06-07 19:39:38.000000000 +0000 +++ 2ping-4.5/tests/test_cli.py 2020-06-17 21:22:00.000000000 +0000 @@ -1,171 +1,177 @@ import locale -import os -import signal -import sys -import time +import logging +import pytest import unittest -import uuid +import unittest.mock -from twoping import args, cli, packets, utils +from . import _test_module_init +from twoping import args, cli, utils -@unittest.skipUnless(hasattr(os, "fork"), "CLI tests require os.fork()") -class BaseTestCLI(unittest.TestCase): - bind_address = "127.0.0.1" +class TestCLI(unittest.TestCase): + bind_addresses = ["127.0.0.1"] port = None - child_pid = 0 - listener_opts = [] - canary_filename = None - canary_timeout = 10.0 - - @classmethod - def setUpClass(self): - (self.child_pid, self.port) = self.fork_listener(self, self.listener_opts) - - @classmethod - def tearDownClass(self): - if self.child_pid: - os.kill(self.child_pid, signal.SIGINT) - - @classmethod - def listener_print(self, *args, **kwargs): - pass - - @classmethod - def listener_client_print(self, *args, **kwargs): - pass - - @classmethod - def listener_ready(self): - with open(self.canary_filename, "w"): - pass - - def fork_listener(self, extra_opts=None): - self.canary_filename = os.path.join("/tmp", str(uuid.uuid4())) - if self.port: - port = self.port + logger = None + class_args = None + + def setUp(self): + self.logger = logging.getLogger() + self.logger.level = logging.DEBUG + + def _client(self, test_flags=None, test_positionals=None, test_stats=True, pairs=1): + if self.port is None: + port = -1 else: - port = utils.random.randint(49152, 65535) - opts = [ - "2ping", - "--listen", - "--quiet", - "--no-3way", - "--interface-address={}".format(self.bind_address), - "--port={}".format(port), - ] - if extra_opts: - opts += extra_opts + port = self.port - child_pid = os.fork() - if child_pid == 0: - cli_args = args.parse_args(opts) - p = cli.TwoPing(cli_args) - p.print_out = self.listener_print - p.ready = self.listener_ready - sys.exit(int(p.run())) - - begin_wait = time.monotonic() - found_canary = False - while time.monotonic() < begin_wait + self.canary_timeout: - if os.path.exists(self.canary_filename): - os.remove(self.canary_filename) - found_canary = True - break - time.sleep(0.25) - if not found_canary: - raise RuntimeError("Did not find listener start canary") - - return (child_pid, port) - - def run_listener_client(self, client_opts, listener_opts=None): - if listener_opts is None: - listener_opts = [] - client_base_opts = [ - "2ping", - self.bind_address, - "--port={}".format(self.port), - "--debug", - "--nagios=1000,5%,1000,5%", + flag_args = ["--debug"] + if self.class_args is not None: + flag_args += self.class_args + if test_flags is not None: + flag_args += test_flags + + positional_args = [] + if test_positionals is not None: + positional_args += test_positionals + + if "--loopback" not in flag_args: + flag_args += ["--listen", "--port={}".format(port)] + for bind_address in self.bind_addresses: + flag_args.append("--interface-address={}".format(bind_address)) + positional_args.append(bind_address) + if ("--adaptive" not in flag_args) and ("--flood" not in flag_args): + flag_args.append("--count=1") + if not ("--count=1" in flag_args): + flag_args.append("--interval=5") + all_args = ["2ping"] + flag_args + positional_args + self.logger.info("Passed arguments: {}".format(all_args)) + + p = cli.TwoPing(args.parse_args(all_args)) + self.logger.info("Parsed arguments: {}".format(p.args)) + self.assertEqual(p.run(), 0) + + for sock_class in p.sock_classes: + self.assertTrue(sock_class.closed) + + self.assertEqual(len(p.sock_classes), (pairs * 2)) + client_sock_classes = [ + sock_class for sock_class in p.sock_classes if sock_class.is_client ] - if not (("--adaptive" in client_opts) or ("--flood" in client_opts)): - client_base_opts.append("--count=1") - if not ("--count=1" in client_opts): - client_base_opts.append("--interval=5") - cli_args = args.parse_args(client_base_opts + client_opts) - p = cli.TwoPing(cli_args) - p.print_out = self.listener_client_print - self.assertEqual(int(p.run()), 0) + self.assertEqual(len(client_sock_classes), pairs) + sock_class = client_sock_classes[0] + if not test_stats: + return sock_class -class TestCLIStandard(BaseTestCLI): - def test_notice(self): - self.run_listener_client(["--notice=Notice text"]) - - @unittest.skipUnless( - (locale.getlocale()[1] == "UTF-8"), "UTF-8 environment required" - ) - def test_notice_utf8(self): - self.run_listener_client(["--notice=UTF-8 \u2603"]) - - def test_random(self): - self.run_listener_client(["--send-random=32"]) - - def test_time(self): - self.run_listener_client(["--send-time"]) - - def test_monotonic_clock(self): - self.run_listener_client(["--send-monotonic-clock"]) + self.assertEqual(sock_class.errors_received, 0) + self.assertEqual(sock_class.lost_inbound, 0) + self.assertEqual(sock_class.lost_outbound, 0) + if p.args.count: + self.assertEqual(sock_class.pings_transmitted, p.args.count) + self.assertEqual(sock_class.pings_received, p.args.count) + else: + self.assertGreaterEqual( + sock_class.pings_received / sock_class.pings_transmitted, 0.99 + ) + + return sock_class + + def test_3way(self): + sock_class = self._client([]) + self.assertEqual(sock_class.packets_transmitted, 2) + self.assertEqual(sock_class.packets_received, 1) + + def test_3way_no(self): + sock_class = self._client(["--no-3way"]) + self.assertEqual(sock_class.packets_transmitted, 1) + self.assertEqual(sock_class.packets_received, 1) + @pytest.mark.slow def test_adaptive(self): - self.run_listener_client(["--adaptive", "--deadline=3"]) - - def test_flood(self): - self.run_listener_client(["--flood", "--deadline=3"]) + sock_class = self._client(["--adaptive", "--deadline=3"]) + self.assertGreaterEqual(sock_class.pings_transmitted, 100) + @unittest.skipIf(isinstance(utils.AES, ImportError), "Crypto module required") + def test_encrypt(self): + self._client( + ["--encrypt-method=hkdf-aes256-cbc", "--encrypt=S49HVbnJd3fBdDzdMVVw"] + ) -class TestCLIHMACMD5(BaseTestCLI): - listener_opts = ["--auth-digest=hmac-md5", "--auth=rBgRpBfRbF4DkwFQXncz"] + @pytest.mark.slow + def test_flood(self): + sock_class = self._client(["--flood", "--deadline=3"]) + self.assertGreaterEqual(sock_class.pings_transmitted, 100) - def test_hmac(self): - self.run_listener_client(self.listener_opts) + def test_hmac_crc32(self): + self._client(["--auth-digest=hmac-crc32", "--auth=mc82kJwtXFlhqQSCKptQ"]) + def test_hmac_md5(self): + self._client(["--auth-digest=hmac-md5", "--auth=rBgRpBfRbF4DkwFQXncz"]) -class TestCLIHMACSHA1(BaseTestCLI): - listener_opts = ["--auth-digest=hmac-sha1", "--auth=qnzTCJHnZXdrxRZ8JjQw"] + def test_hmac_sha1(self): + self._client(["--auth-digest=hmac-sha1", "--auth=qnzTCJHnZXdrxRZ8JjQw"]) - def test_hmac(self): - self.run_listener_client(self.listener_opts) + def test_hmac_sha256(self): + self._client(["--auth-digest=hmac-sha256", "--auth=cc8G2Ssbq4WZRq7H7d5L"]) + def test_hmac_sha512(self): + self._client(["--auth-digest=hmac-sha512", "--auth=sjk3kqzcSV3XfHJWNstn"]) -class TestCLIHMACSHA256(BaseTestCLI): - listener_opts = ["--auth-digest=hmac-sha256", "--auth=cc8G2Ssbq4WZRq7H7d5L"] + def test_invalid_hostname(self): + with self.assertRaises(OSError): + self._client([], ["xGkKWDDMnZxCD4XchMnK."]) - def test_hmac(self): - self.run_listener_client(self.listener_opts) + def test_monotonic_clock(self): + self._client(["--send-monotonic-clock"]) + def test_notice(self): + self._client(["--notice=Notice text"]) -class TestCLIHMACSHA512(BaseTestCLI): - listener_opts = ["--auth-digest=hmac-sha512", "--auth=sjk3kqzcSV3XfHJWNstn"] + @unittest.skipUnless( + (locale.getlocale()[1] == "UTF-8"), "UTF-8 environment required" + ) + def test_notice_utf8(self): + self._client(["--notice=UTF-8 \u2603"]) - def test_hmac(self): - self.run_listener_client(self.listener_opts) + @pytest.mark.slow + def test_packet_loss(self): + # There is a small but non-zero chance that packet loss will prevent + # any investigation replies from getting back to the client sock + # between the 10 and 15 second mark, causing a test failure. + # There's an even more minisculely small chance no simulated losses + # will occur within the test period. + sock_class = self._client( + ["--flood", "--deadline=15", "--packet-loss=25"], test_stats=False + ) + self.assertGreaterEqual(sock_class.pings_transmitted, 100) + self.assertEqual(sock_class.errors_received, 0) + self.assertGreater(sock_class.lost_inbound, 0) + self.assertGreater(sock_class.lost_outbound, 0) + def test_random(self): + self._client(["--send-random=32"]) -class TestCLIHMACCRC32(BaseTestCLI): - listener_opts = ["--auth-digest=hmac-crc32", "--auth=mc82kJwtXFlhqQSCKptQ"] + def test_time(self): + self._client(["--send-time"]) - def test_hmac(self): - self.run_listener_client(self.listener_opts) + def test_module_init(self): + self.assertTrue(_test_module_init(cli)) -@unittest.skipIf(isinstance(packets.AES, ImportError), "PyCrypto required") -class TestCLIEncryptAES256(BaseTestCLI): - listener_opts = ["--encrypt-method=hkdf-aes256-cbc", "--auth=S49HVbnJd3fBdDzdMVVw"] +class TestCLIInet(TestCLI): + def test_srv_addresses(self): + self.bind_addresses = [] + with unittest.mock.patch( + "twoping.cli.TwoPing.get_srv_hosts", return_value=[("127.0.0.1", -1)] + ): + self._client( + ["--interface-address=127.0.0.1", "--srv"], ["pssPCPkc3XlMTDZhclPV."] + ) - def test_encrypt(self): - self.run_listener_client(self.listener_opts) +@unittest.skipUnless(hasattr(cli.socket, "AF_UNIX"), "UNIX environment required") +class TestCLILoopback(TestCLI): + class_args = ["--loopback"] -if __name__ == "__main__": - unittest.main() + def test_loopback_pairs(self): + self._client(["--loopback-pairs=3"], pairs=3) diff -Nru 2ping-4.4.1/tests/test_crc32.py 2ping-4.5/tests/test_crc32.py --- 2ping-4.4.1/tests/test_crc32.py 2020-06-07 05:22:47.000000000 +0000 +++ 2ping-4.5/tests/test_crc32.py 2020-06-17 21:51:42.000000000 +0000 @@ -1,19 +1,40 @@ import hmac import unittest +from . import _test_module_init from twoping import crc32 class TestCRC32(unittest.TestCase): def test_crc32(self): - c = crc32.new() - c.update(b"Data to hash") + c = crc32.new(b"Data to hash") self.assertEqual(c.digest(), b"\x44\x9e\x0a\x5c") def test_hmac(self): h = hmac.new(b"Secret key", b"Data to hash", crc32) self.assertEqual(h.digest(), b"\x3c\xe1\xb6\xb9") + def test_update(self): + c = crc32.new() + c.update(b"Data to hash") + self.assertEqual(c.digest(), b"\x44\x9e\x0a\x5c") + + def test_hexdigest(self): + c = crc32.new(b"Data to hash") + self.assertEqual(c.hexdigest(), "449e0a5c") + + def test_hexdigest_zero_padding(self): + c = crc32.new(b"jade") + self.assertEqual(c.hexdigest(), "00835218") + + def test_clear(self): + c = crc32.new(b"Data to hash") + c.clear() + self.assertEqual(c.digest(), b"\x00\x00\x00\x00") + + def test_zero_padding(self): + c = crc32.new(b"jade") + self.assertEqual(c.digest(), b"\x00\x83\x52\x18") -if __name__ == "__main__": - unittest.main() + def test_module_init(self): + self.assertTrue(_test_module_init(crc32)) diff -Nru 2ping-4.4.1/tests/test_packets.py 2ping-4.5/tests/test_packets.py --- 2ping-4.4.1/tests/test_packets.py 2020-06-07 19:39:54.000000000 +0000 +++ 2ping-4.5/tests/test_packets.py 2020-06-17 21:22:00.000000000 +0000 @@ -1,6 +1,6 @@ import unittest -from twoping import packets +from twoping import packets, utils class TestPacketsOpcodes(unittest.TestCase): @@ -172,7 +172,7 @@ self.assertEqual(opcode.id, 0x88A1F7C7) self.assertEqual(opcode.dump(), b"\x00\x02\x00\x00\xff\xff\x00\x01\xce\xa3") - @unittest.skipIf(isinstance(packets.AES, ImportError), "PyCrypto required") + @unittest.skipIf(isinstance(utils.AES, ImportError), "Crypto module required") def test_encrypted_encrypt_decrypt(self): key = b"Secret key" iv = b"\x32\xf0\x4a\x2f\xb3\x78\xe3\xf3\x73\x2b\x4a\x8c\x02\x74\xca\x0e" @@ -471,7 +471,3 @@ b"\x00\x00\x33\x38\x00\x08\x00\x01\x00\x00\x00\x00\xb0\x02" ) self.assertEqual(packet.dump(), expected) - - -if __name__ == "__main__": - unittest.main() diff -Nru 2ping-4.4.1/tests/test_python.py 2ping-4.5/tests/test_python.py --- 2ping-4.4.1/tests/test_python.py 1970-01-01 00:00:00.000000000 +0000 +++ 2ping-4.5/tests/test_python.py 2020-06-17 21:22:00.000000000 +0000 @@ -0,0 +1,9 @@ +import time +import unittest + + +class TestPython(unittest.TestCase): + def test_monotonic(self): + val1 = time.monotonic() + val2 = time.monotonic() + self.assertGreaterEqual(val2, val1) diff -Nru 2ping-4.4.1/tests/test_utils.py 2ping-4.5/tests/test_utils.py --- 2ping-4.4.1/tests/test_utils.py 2020-06-07 20:43:01.000000000 +0000 +++ 2ping-4.5/tests/test_utils.py 2020-06-17 21:22:00.000000000 +0000 @@ -63,10 +63,10 @@ data_fuzzed = utils.fuzz_packet(data, 0) self.assertEqual(data, data_fuzzed) - def test_lazy_div(self): - self.assertEqual(utils.lazy_div(10, 5), 10 / 5) - self.assertEqual(utils.lazy_div(5, 0), 0) - self.assertEqual(utils.lazy_div(0, 0), 0) + def test_div0(self): + self.assertEqual(utils.div0(10, 5), 10 / 5) + self.assertEqual(utils.div0(5, 0), 0) + self.assertEqual(utils.div0(0, 0), 0) def test_npack(self): self.assertEqual(utils.npack(1), b"\x01") @@ -92,7 +92,3 @@ self.assertEqual(utils.stats_time(123.45), "2m 3s 450ms") self.assertEqual(utils.stats_time(0.45678), "456ms") self.assertEqual(utils.stats_time(123456789000), "3914y 288d 30m") - - -if __name__ == "__main__": - unittest.main() diff -Nru 2ping-4.4.1/tox.ini 2ping-4.5/tox.ini --- 2ping-4.4.1/tox.ini 1970-01-01 00:00:00.000000000 +0000 +++ 2ping-4.5/tox.ini 2020-06-17 21:22:00.000000000 +0000 @@ -0,0 +1,38 @@ +[tox] +envlist = black, flake8, pytest + +[testenv] +basepython = python + +[testenv:black] +commands = python -mblack --check . +deps = black + +[testenv:flake8] +commands = python -mflake8 +deps = flake8 + +[testenv:pytest] +commands = python -mpytest --cov=twoping --cov-report=term-missing +deps = pytest + pytest-cov + -r{toxinidir}/requirements.txt + +[testenv:pytest-quick] +commands = python -mpytest -m "not slow" +deps = pytest + -r{toxinidir}/requirements.txt + +[flake8] +exclude = + .git, + __pycache__, + .tox, +# TODO: remove C901 once complexity is reduced +ignore = C901,E203,E231,W503 +max-line-length = 120 +max-complexity = 10 + +[pytest] +markers = + slow diff -Nru 2ping-4.4.1/twoping/args.py 2ping-4.5/twoping/args.py --- 2ping-4.4.1/twoping/args.py 2020-06-07 06:43:16.000000000 +0000 +++ 2ping-4.5/twoping/args.py 2020-06-17 21:22:00.000000000 +0000 @@ -18,10 +18,11 @@ import argparse import os +import socket import sys from . import __version__ -from .utils import _ +from .utils import _, AES def parse_args(argv=None): @@ -83,6 +84,7 @@ dest="interface_address", type=str, action="append", + default=[], help=_("interface bind address"), metavar="ADDRESS", ) @@ -165,6 +167,16 @@ ) parser.add_argument("--listen", action="store_true", help=_("listen mode")) parser.add_argument( + "--loopback", action="store_true", help=_("UNIX loopback test mode") + ) + parser.add_argument( + "--loopback-pairs", + type=int, + default=1, + help=_("number of loopback pairs to create"), + metavar="PAIRS", + ) + parser.add_argument( "--max-packet-size", type=int, default=512, @@ -257,9 +269,11 @@ args = parser.parse_args(args=argv[1:]) - if (not args.listen) and (not args.host): + if (not args.listen) and (not args.host) and (not args.loopback): parser.print_help() parser.exit() + if args.loopback and not hasattr(socket, "AF_UNIX"): + parser.error("--loopback not supported on non-UNIX platforms") if args.nagios: args.quiet = True if (not args.count) and (not args.deadline): @@ -330,11 +344,8 @@ } args.auth_digest_index = hmac_id_map[args.auth_digest] - if args.encrypt: - try: - from Crypto.Cipher import AES # noqa: F401 - except ImportError: - parser.error(_("Python crypto module required for encryption")) + if args.encrypt and isinstance(AES, ImportError): + parser.error(_("Crypto module required for encryption")) encrypt_id_map = {"hkdf-aes256-cbc": 1} args.encrypt_method_index = encrypt_id_map[args.encrypt_method] diff -Nru 2ping-4.4.1/twoping/best_poller.py 2ping-4.5/twoping/best_poller.py --- 2ping-4.4.1/twoping/best_poller.py 2020-06-07 05:21:45.000000000 +0000 +++ 2ping-4.5/twoping/best_poller.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,196 +0,0 @@ -# 2ping - A bi-directional ping utility -# Copyright (C) 2010-2020 Ryan Finnie -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301, USA. - -import errno -import select - - -class EpollPoller: - poller_type = "epoll" - - def __init__(self): - self.poller = select.epoll() - self.f_dict = {} - - def register(self, f): - fileno = f.fileno() - if fileno not in self.f_dict: - self.poller.register(fileno, select.EPOLLIN) - self.f_dict[fileno] = f - - def unregister(self, f): - fileno = f.fileno() - if fileno not in self.f_dict: - return - self.poller.unregister(fileno) - del self.f_dict[fileno] - - def close(self): - return self.poller.close() - - def poll(self, timeout): - try: - poll_res = self.poller.poll(timeout) - except (select.error, IOError, OSError) as e: - if e.args[0] not in (errno.EINTR,): - raise - return [] - res = [] - for i in poll_res: - if i[0] in self.f_dict: - res.append(self.f_dict[i[0]]) - return res - - -class KqueuePoller: - poller_type = "kqueue" - - def __init__(self): - self.poller = select.kqueue() - self.kevents = {} - self.f_dict = {} - - def register(self, f): - fileno = f.fileno() - if fileno not in self.f_dict: - self.kevents[fileno] = select.kevent( - fileno, - filter=select.KQ_FILTER_READ, - flags=select.KQ_EV_ADD | select.KQ_EV_ENABLE, - ) - self.f_dict[fileno] = f - - def unregister(self, f): - fileno = f.fileno() - if fileno not in self.f_dict: - return - del self.kevents[fileno] - del self.f_dict[fileno] - - def close(self): - return self.poller.close() - - def poll(self, timeout): - try: - poll_res = self.poller.control(self.kevents.values(), 10, timeout) - except (select.error, IOError, OSError) as e: - if e.args[0] not in (errno.EINTR,): - raise - return [] - res = [] - for i in poll_res: - if i.ident in self.f_dict: - res.append(self.f_dict[i.ident]) - return res - - -class PollPoller: - poller_type = "poll" - - def __init__(self): - self.poller = select.poll() - self.f_dict = {} - - def register(self, f): - fileno = f.fileno() - if fileno not in self.f_dict: - self.poller.register(fileno, select.POLLIN) - self.f_dict[fileno] = f - - def unregister(self, f): - fileno = f.fileno() - if fileno not in self.f_dict: - return - self.poller.unregister(fileno) - del self.f_dict[fileno] - - def close(self): - return self.poller.close() - - def poll(self, timeout): - try: - poll_res = self.poller.poll(timeout * 1000.0) - except (select.error, IOError, OSError) as e: - if e.args[0] not in (errno.EINTR,): - raise - return [] - res = [] - for i in poll_res: - if i[0] in self.f_dict: - res.append(self.f_dict[i[0]]) - return res - - -class SelectPoller: - poller_type = "select" - - def __init__(self): - self.f_dict = {} - - def register(self, f): - self.f_dict[f.fileno()] = f - - def unregister(self, f): - fileno = f.fileno() - if fileno not in self.f_dict: - return - del self.f_dict[fileno] - - def close(self): - pass - - def poll(self, timeout): - try: - return select.select(self.f_dict.values(), [], [], timeout)[0] - except (select.error, IOError, OSError) as e: - if e.args[0] not in (errno.EINTR,): - raise - return [] - - -def best_poller(): - try: - return EpollPoller() - except AttributeError: - pass - try: - return KqueuePoller() - except AttributeError: - pass - try: - return PollPoller() - except AttributeError: - pass - return SelectPoller() - - -def available_pollers(): - available = [] - for poller in [EpollPoller, KqueuePoller, PollPoller, SelectPoller]: - try: - available.append(poller()) - except AttributeError: - continue - return available - - -if __name__ == "__main__": - available = available_pollers() - print("Available pollers: {}".format(" ".join([p.poller_type for p in available]))) - poller = best_poller() - print("Best poller: {}".format(poller.poller_type)) diff -Nru 2ping-4.4.1/twoping/cli.py 2ping-4.5/twoping/cli.py --- 2ping-4.4.1/twoping/cli.py 2020-06-07 20:43:34.000000000 +0000 +++ 2ping-4.5/twoping/cli.py 2020-06-17 21:33:58.000000000 +0000 @@ -17,7 +17,9 @@ # 02110-1301, USA. import errno +import logging import math +import selectors import signal import socket import sys @@ -33,14 +35,18 @@ except ImportError as e: netifaces = e -from . import __version__, best_poller, packets +try: + import systemd.daemon as systemd_daemon +except ImportError as e: + systemd_daemon = e + +from . import __version__, packets from .args import parse_args from .utils import ( _, _pl, fuzz_packet, - lazy_div, - nunpack, + div0, platform_info, random, random_is_systemrandom, @@ -49,15 +55,29 @@ version_string = "2ping {} - {}".format(__version__, platform_info()) -clock = time.monotonic +clock = time.perf_counter class SocketClass: def __init__(self, sock): self.sock = sock + self.address = sock.getsockname() + if not self.address: + self.address = None + + # Functional tags applied to the SocketClass + self.is_client = False + self.is_listener = False + self.is_loopback = False + self.is_inet = False + self.is_systemd = False # Used during client mode for the host tuple to send UDP packets to. self.client_host = None + # (family, type, proto, canonname, sockaddr) of the bound socket + self.bind_addrinfo = None + # Whether the sock has been closed + self.closed = False # Dict of PeerState instances, indexed by peer tuple self.peer_states = {} @@ -80,8 +100,20 @@ self.next_send = 0 self.shutdown_time = 0 - self.nagios_result = 0 - self.session = b"" + self.session = bytes([random.randint(0, 255) for x in range(8)]) + + def __repr__(self): + attrs = ["fd {}".format(self.fileno())] + if self.bind_addrinfo: + attrs.append("bind {}".format(self.bind_addrinfo)) + if self.client_host: + attrs.append("client {}".format(self.client_host)) + for tag in ("client", "listener", "inet", "loopback", "systemd"): + if getattr(self, "is_{}".format(tag)): + attrs.append(tag) + if self.closed: + attrs.append("closed") + return "".format(", ".join(attrs)) def fileno(self): return self.sock.fileno() @@ -132,24 +164,13 @@ self.time_start = now self.fake_time_epoch = random.random() * (2 ** 32) self.fake_time_generation = random.randint(0, 65535) + self.isatty = sys.stdout.isatty() - self.sock_classes = [] - self.systemd_socks = [] - self.poller = best_poller.best_poller() + self.logger = logging.getLogger() + self.logger.setLevel(logging.DEBUG) - self.pings_transmitted = 0 - self.pings_received = 0 - self.packets_transmitted = 0 - self.packets_received = 0 - self.lost_outbound = 0 - self.lost_inbound = 0 - self.errors_received = 0 - self.rtt_total = 0 - self.rtt_total_sq = 0 - self.rtt_count = 0 - self.rtt_min = 0 - self.rtt_max = 0 - self.rtt_ewma = 0 + self.sock_classes = [] + self.poller = selectors.DefaultSelector() # Scheduled events self.next_cleanup = now + 60.0 @@ -184,63 +205,65 @@ def print_out(self, *args, **kwargs): print(*args, **kwargs) - def print_debug(self, *args, **kwargs): - if not self.args.debug: + def tty_out(self, *args, **kwargs): + if not self.isatty: return self.print_out(*args, **kwargs) + @property + def sock_classes_open(self): + return [sock_class for sock_class in self.sock_classes if not sock_class.closed] + + @property + def sock_classes_active(self): + now = clock() + return [ + sock_class + for sock_class in self.sock_classes_open + if not sock_class.shutdown_time + or (sock_class.shutdown_time and sock_class.shutdown_time > now) + ] + def handle_socket_error(self, e, sock_class, peer_address=None): sock = sock_class.sock # Errors from the last send() can be trapped via IP_RECVERR (Linux only). - self.errors_received += 1 sock_class.errors_received += 1 error_string = str(e) try: MSG_ERRQUEUE = 8192 (error_data, error_address) = sock.recvfrom(16384, MSG_ERRQUEUE) - if self.args.quiet: - pass - elif self.args.flood: - self.print_out("E", end="", flush=True) - else: - self.print_out("{}: {}".format(error_address[0], error_string)) + print_address = error_address[0] except socket.error: - if self.args.quiet: - pass - elif self.args.flood: - self.print_out("E", end="", flush=True) + if peer_address: + print_address = peer_address[0] else: - if peer_address: - self.print_out("{}: {}".format(peer_address[0], error_string)) - else: - self.print_out(error_string) + print_address = str(sock_class) + if self.args.quiet: + pass + elif print_address: + self.logger.error("{}: {}".format(print_address, error_string)) + else: + self.logger.error(error_string) def process_incoming_packet(self, sock_class): sock = sock_class.sock - try: - (data, peer_address) = sock.recvfrom(16384) - except socket.error as e: - self.handle_socket_error(e, sock_class) - return - socket_address = sock.getsockname() - self.print_debug("Socket address: {}".format(repr(socket_address))) - self.print_debug("Peer address: {}".format(repr(peer_address))) - - # Simulate random packet loss. - if self.args.packet_loss_in and ( - random.random() < (self.args.packet_loss_in / 100.0) - ): + recvfrom = self.sock_recvfrom(sock_class) + time_begin = clock() + if recvfrom is None: + # Error handled, simulated packet loss, etc return - # Simulate data corruption - if self.args.fuzz: - data = fuzz_packet(data, self.args.fuzz) + data, peer_address = recvfrom + if peer_address: + print_address = peer_address[0] + else: + print_address = str(sock_class) + socket_address = sock_class.address + self.logger.debug("Socket: {}, peer: {}".format(sock_class, peer_address)) # Per-packet options. - self.packets_received += 1 sock_class.packets_received += 1 calculated_rtt = None - time_begin = clock() peer_tuple = (socket_address, peer_address, sock.type) # Preload state tables if the client has not been seen (or has been cleaned). @@ -253,16 +276,15 @@ packet_in = packets.Packet() packet_in.load(data) if self.args.verbose: - self.print_out("RECV: {}".format(repr(packet_in))) + self.logger.info("RECV: {}".format(packet_in)) # Decrypt packet if needed. if self.args.encrypt: if packets.OpcodeEncrypted.id not in packet_in.opcodes: - self.errors_received += 1 sock_class.errors_received += 1 - self.print_out( + self.logger.error( _("Encryption required but not provided by {address}").format( - address=peer_address[0] + address=print_address ) ) return @@ -270,13 +292,12 @@ packet_in.opcodes[packets.OpcodeEncrypted.id].method_index != self.args.encrypt_method_index ): - self.errors_received += 1 sock_class.errors_received += 1 - self.print_out( + self.logger.error( _( "Encryption method mismatch from {address} (expected {expected}, got {got})" ).format( - address=peer_address[0], + address=print_address, expected=self.args.encrypt_method_index, got=packet_in.opcodes[packets.OpcodeEncrypted.id].method_index, ) @@ -296,19 +317,16 @@ encrypted_packet_in.opcodes[packets.OpcodeEncrypted.id].session != peer_state.encrypted_session_id ): - self.errors_received += 1 sock_class.errors_received += 1 - self.print_out( + self.logger.error( _( "Encryption session mismatch from {address} (expected {expected}, got {got})" ).format( - address=peer_address[0], - expected=repr(peer_state.encrypted_session_id), - got=repr( - encrypted_packet_in.opcodes[ - packets.OpcodeEncrypted.id - ].session - ), + address=print_address, + expected=peer_state.encrypted_session_id, + got=encrypted_packet_in.opcodes[ + packets.OpcodeEncrypted.id + ].session, ) ) return @@ -316,14 +334,11 @@ encrypted_packet_in.opcodes[packets.OpcodeEncrypted.id].iv in peer_state.encrypted_session_ivs ): - self.errors_received += 1 sock_class.errors_received += 1 - self.print_out( + self.logger.error( _("Repeated IV {iv} from {address}, discarding").format( - iv=repr( - encrypted_packet_in.opcodes[packets.OpcodeEncrypted.id].iv - ), - address=peer_address[0], + iv=encrypted_packet_in.opcodes[packets.OpcodeEncrypted.id].iv, + address=print_address, ) ) return @@ -331,16 +346,15 @@ encrypted_packet_in.opcodes[packets.OpcodeEncrypted.id].iv ] = (time_begin,) if self.args.verbose: - self.print_out("DECR: {}".format(repr(packet_in))) + self.logger.info("DECR: {}".format(packet_in)) # Verify HMAC if required. if self.args.auth: if packets.OpcodeHMAC.id not in packet_in.opcodes: - self.errors_received += 1 sock_class.errors_received += 1 - self.print_out( + self.logger.error( _("Auth required but not provided by {address}").format( - address=peer_address[0] + address=print_address ) ) return @@ -348,13 +362,12 @@ packet_in.opcodes[packets.OpcodeHMAC.id].digest_index != self.args.auth_digest_index ): - self.errors_received += 1 sock_class.errors_received += 1 - self.print_out( + self.logger.error( _( "Auth digest type mismatch from {address} (expected {expected}, got {got})" ).format( - address=peer_address[0], + address=print_address, expected=self.args.auth_digest_index, got=packet_in.opcodes[packets.OpcodeHMAC.id].digest_index, ) @@ -376,17 +389,14 @@ packet_in.opcodes[packets.OpcodeHMAC.id], test_data ) if test_hash_calculated != test_hash: - self.errors_received += 1 sock_class.errors_received += 1 - self.print_out( + self.logger.error( _( "Auth hash failed from {address} (expected {expected}, got {got})" ).format( - address=peer_address[0], - expected="".join( - "{hex:02x}".format(hex=x) for x in test_hash_calculated - ), - got="".join("{hex:02x}".format(hex=x) for x in test_hash), + address=print_address, + expected=test_hash_calculated.hex(), + got=test_hash.hex(), ) ) return @@ -396,12 +406,11 @@ replied_message_id = packet_in.opcodes[ packets.OpcodeInReplyTo.id ].message_id - replied_message_id_int = nunpack(replied_message_id) - if replied_message_id_int in peer_state.sent_messages: + if replied_message_id in peer_state.sent_messages: (sent_time, _unused, ping_position) = peer_state.sent_messages[ - replied_message_id_int + replied_message_id ] - del peer_state.sent_messages[replied_message_id_int] + del peer_state.sent_messages[replied_message_id] calculated_rtt = (time_begin - sent_time) * 1000 if ( @@ -416,18 +425,15 @@ if calculated_rtt < 0: calculated_rtt = 0 - self.pings_received += 1 sock_class.pings_received += 1 self.update_rtts(sock_class, calculated_rtt) - if self.args.quiet: + if self.args.quiet or self.args.flood: pass - elif self.args.flood: - self.print_out("\x08", end="", flush=True) else: if self.args.audible: - self.print_out("\x07", end="", flush=True) + self.tty_out("\x07", end="", flush=True) if packets.OpcodeRTTEnclosed.id in packet_in.opcodes: - self.print_out( + self.logger.info( _( ( "{bytes} bytes from {address}: ping_seq={seq} time={ms:0.03f} ms " @@ -435,7 +441,7 @@ ) ).format( bytes=len(data), - address=peer_state.peer_tuple[1][0], + address=print_address, seq=ping_position, ms=calculated_rtt, peerms=( @@ -447,12 +453,12 @@ ) ) else: - self.print_out( + self.logger.info( _( "{bytes} bytes from {address}: ping_seq={seq} time={ms:0.03f} ms" ).format( bytes=len(data), - address=peer_state.peer_tuple[1][0], + address=print_address, seq=ping_position, ms=calculated_rtt, ) @@ -466,10 +472,10 @@ .segments[packets.ExtendedNotice.id] .text ) - self.print_out( + self.logger.info( " " + _("Peer notice: {notice}").format(notice=notice) ) - peer_state.courtesy_messages[replied_message_id_int] = ( + peer_state.courtesy_messages[replied_message_id] = ( time_begin, replied_message_id, ) @@ -482,14 +488,13 @@ for message_id in packet_in.opcodes[ packets.OpcodeCourtesyExpiration.id ].message_ids: - message_id_int = nunpack(message_id) - if message_id_int in peer_state.seen_messages: - del peer_state.seen_messages[message_id_int] + if message_id in peer_state.seen_messages: + del peer_state.seen_messages[message_id] # If the peer requested a reply, prepare one. if packets.OpcodeReplyRequested.id in packet_in.opcodes: # Populate seen_messages. - peer_state.seen_messages[nunpack(packet_in.message_id)] = (time_begin,) + peer_state.seen_messages[packet_in.message_id] = (time_begin,) # Basic packet configuration. packet_out = self.base_packet() @@ -521,7 +526,7 @@ for message_id in packet_in.opcodes[ packets.OpcodeInvestigate.id ].message_ids: - if nunpack(message_id) in peer_state.seen_messages: + if message_id in peer_state.seen_messages: if packets.OpcodeInvestigationSeen.id not in packet_out.opcodes: packet_out.opcodes[ packets.OpcodeInvestigationSeen.id @@ -601,15 +606,13 @@ # Send the packet. self.sock_sendto(sock_class, sock_out, peer_address) - self.packets_transmitted += 1 sock_class.packets_transmitted += 1 # If ReplyRequested is set, we care about its arrival. if packets.OpcodeReplyRequested.id in packet_out.opcodes: - self.pings_transmitted += 1 sock_class.pings_transmitted += 1 peer_state.ping_position += 1 - peer_state.sent_messages[nunpack(packet_out.message_id)] = ( + peer_state.sent_messages[packet_out.message_id] = ( time_send, packet_out.message_id, peer_state.ping_position, @@ -624,29 +627,21 @@ for courtesy_message_id in packet_out_examine.opcodes[ packets.OpcodeCourtesyExpiration.id ].message_ids: - courtesy_message_id_int = nunpack(courtesy_message_id) - if courtesy_message_id_int in peer_state.courtesy_messages: - del peer_state.courtesy_messages[courtesy_message_id_int] + if courtesy_message_id in peer_state.courtesy_messages: + del peer_state.courtesy_messages[courtesy_message_id] if self.args.verbose: - if self.args.encrypt: - self.print_out( - "SEND (encrypted): {}".format(repr(packet_out_examine)) + self.logger.info( + "SEND{}: {}".format( + (" (encrypted)" if self.args.encrypt else ""), + packet_out_examine, ) - else: - self.print_out("SEND: {}".format(repr(packet_out_examine))) + ) - if (not self.args.listen) and (packets.OpcodeInReplyTo.id in packet_in.opcodes): - if self.args.flood: - # If we're in flood mode and this is a ping reply, send a new ping ASAP. - sock_class.next_send = time_begin - elif self.args.adaptive and sock_class.rtt_ewma: - # Adaptive gets recalculated immediately after the reply. - next_send = time_begin + (sock_class.rtt_ewma / 8.0 / 1000.0) - if next_send < sock_class.next_send: - sock_class.next_send = next_send + if sock_class.next_send and (packets.OpcodeInReplyTo.id in packet_in.opcodes): + self.schedule_next_send(sock_class, reply_received=True) - def sock_sendto(self, sock_class, data, address): + def sock_sendto(self, sock_class, data, address=None): sock = sock_class.sock # Simulate random packet loss. if self.args.packet_loss_out and ( @@ -655,10 +650,36 @@ return # Send the packet. try: - sock.sendto(data, address) + if address: + sock.sendto(data, address) + else: + sock.send(data) except socket.error as e: self.handle_socket_error(e, sock_class, peer_address=address) + def sock_recvfrom(self, sock_class): + sock = sock_class.sock + try: + (data, peer_address) = sock.recvfrom(16384) + except socket.error as e: + self.handle_socket_error(e, sock_class) + return + + # Simulate random packet loss. + if self.args.packet_loss_in and ( + random.random() < (self.args.packet_loss_in / 100.0) + ): + return + + # Simulate data corruption + if self.args.fuzz: + data = fuzz_packet(data, self.args.fuzz) + + if not peer_address: + peer_address = None + + return data, peer_address + def start_investigations(self, peer_state, packet_check): if len(peer_state.sent_messages) == 0: return @@ -680,33 +701,27 @@ def check_investigations(self, peer_state, packet_check): found = {} - # Inbound - if packets.OpcodeInvestigationSeen.id in packet_check.opcodes: - for message_id in packet_check.opcodes[ - packets.OpcodeInvestigationSeen.id - ].message_ids: - message_id_int = nunpack(message_id) - if message_id_int not in peer_state.sent_messages: - continue - (_unused, _unused, ping_seq) = peer_state.sent_messages[message_id_int] - found[ping_seq] = ("inbound", peer_state.peer_tuple[1][0]) - del peer_state.sent_messages[message_id_int] - self.lost_inbound += 1 - peer_state.sock_class.lost_inbound += 1 - - # Outbound - if packets.OpcodeInvestigationUnseen.id in packet_check.opcodes: - for message_id in packet_check.opcodes[ - packets.OpcodeInvestigationUnseen.id - ].message_ids: - message_id_int = nunpack(message_id) - if message_id_int not in peer_state.sent_messages: - continue - (_unused, _unused, ping_seq) = peer_state.sent_messages[message_id_int] - found[ping_seq] = ("outbound", peer_state.peer_tuple[1][0]) - del peer_state.sent_messages[message_id_int] - self.lost_outbound += 1 - peer_state.sock_class.lost_outbound += 1 + # Inbound/Outbound + for opcode_id, type_str, type_stat in [ + (packets.OpcodeInvestigationSeen.id, "inbound", "lost_inbound"), + (packets.OpcodeInvestigationUnseen.id, "outbound", "lost_outbound"), + ]: + if opcode_id in packet_check.opcodes: + for message_id in packet_check.opcodes[opcode_id].message_ids: + if message_id not in peer_state.sent_messages: + continue + (_unused, _unused, ping_seq) = peer_state.sent_messages[message_id] + if peer_state.peer_tuple[1]: + address = peer_state.peer_tuple[1][0] + else: + address = peer_state.sock_class + found[ping_seq] = (type_str, address) + del peer_state.sent_messages[message_id] + setattr( + peer_state.sock_class, + type_stat, + getattr(peer_state.sock_class, type_stat) + 1, + ) if self.args.quiet: return @@ -714,99 +729,172 @@ for ping_seq in sorted(found): (loss_type, address) = found[ping_seq] if loss_type == "inbound": - if self.args.flood: - self.print_out("<", end="", flush=True) - else: - self.print_out( - _("Lost inbound packet from {address}: ping_seq={seq}").format( - address=address, seq=ping_seq - ) - ) + loss_message = "Lost inbound packet from {address}: ping_seq={seq}" else: - if self.args.flood: - self.print_out(">", end="", flush=True) - else: - self.print_out( - _("Lost outbound packet to {address}: ping_seq={seq}").format( - address=address, seq=ping_seq - ) - ) + loss_message = "Lost outbound packet to {address}: ping_seq={seq}" + self.logger.error( + _(loss_message).format( + loss_type=loss_type, address=address, seq=ping_seq + ) + ) - def close_socks(self, close_systemd=True): - for sock_class in self.sock_classes: + def close_socks(self, sock_classes=None): + if sock_classes is None: + sock_classes = self.sock_classes_open + for sock_class in sock_classes: + self.logger.debug("Closing socket: {}".format(sock_class)) self.poller.unregister(sock_class) - if (not close_systemd) and (sock_class.sock not in self.systemd_socks): - continue sock_class.sock.close() + sock_class.closed = True + sock_class.shutdown_time = clock() + sock_class.next_send = 0 def gather_systemd_socks(self): - # If we've done this before, don't re-attempt - if len(self.systemd_socks) > 0: - return + # Do nothing if systemd.daemon is not available + if isinstance(systemd_daemon, ImportError): + return [] - try: - import systemd.daemon - except ImportError: - return + # If we've done this before, don't re-attempt + systemd_sock_classes = [ + sock_class for sock_class in self.sock_classes if sock_class.is_systemd + ] + if systemd_sock_classes: + return systemd_sock_classes # Note that listen_fds() defaults to unset_environment=True # so we only want to try this once (see above) - for fd in systemd.daemon.listen_fds(): + sock_classes = [] + for fd in systemd_daemon.listen_fds(): for family in (socket.AF_INET6, socket.AF_INET): - if not systemd.daemon.is_socket_inet( + if not systemd_daemon.is_socket_inet( fd, family=family, type=socket.SOCK_DGRAM ): continue sock = socket.fromfd(fd, family, socket.SOCK_DGRAM) - self.systemd_socks.append(sock) - self.print_debug( - "Using systemd-supplied socket on fd {}: {}".format( - fd, (family, socket.SOCK_DGRAM, sock.getsockname()) + sock_class = SocketClass(sock) + sock_class.is_systemd = True + sock_class.is_listener = True + sock_classes.append(sock_class) + self.sock_classes.append(sock_class) + self.poller.register(sock_class, selectors.EVENT_READ) + self.logger.debug("Opened socket: {}".format(sock_class)) + + return sock_classes + + def setup_interface_address( + self, + interface_address, + port=0, + family=socket.AF_UNSPEC, + type=socket.SOCK_DGRAM, + proto=socket.IPPROTO_UDP, + tags=None, + ): + sock_classes = [] + for addrinfo in socket.getaddrinfo( + interface_address, port, family, type, proto + ): + if ( + (addrinfo[0] == socket.AF_INET6) + and (not self.args.ipv4) + and self.has_ipv6 + ): + pass + elif (addrinfo[0] == socket.AF_INET) and (not self.args.ipv6): + pass + else: + continue + + if tags is not None and "listener" in tags: + found_existing = False + for sock_class in self.sock_classes_open: + if addrinfo == sock_class.bind_addrinfo: + sock_classes.append(sock_class) + found_existing = True + if found_existing: + continue + + sock = self.new_socket(addrinfo) + sock_class = SocketClass(sock) + if tags is not None: + for tag in tags: + setattr(sock_class, "is_{}".format(tag), True) + sock_class.bind_addrinfo = addrinfo + sock_classes.append(sock_class) + self.sock_classes.append(sock_class) + self.poller.register(sock_class, selectors.EVENT_READ) + self.logger.debug("Opened socket: {}".format(sock_class)) + + return sock_classes + + def get_system_addresses(self): + if isinstance(netifaces, ImportError): + return [] + + addrs = set() + for iface in netifaces.interfaces(): + iface_addrs = netifaces.ifaddresses(iface) + if (self.args.ipv4 or (not self.args.ipv4 and not self.args.ipv6)) and ( + netifaces.AF_INET in iface_addrs + ): + addrs.update( + [ + (f["addr"], netifaces.AF_INET) + for f in iface_addrs[netifaces.AF_INET] + if "addr" in f + ] + ) + if ( + self.has_ipv6 + and (self.args.ipv6 or (not self.args.ipv4 and not self.args.ipv6)) + and (netifaces.AF_INET6 in iface_addrs) + ): + addrs.update( + [ + (f["addr"], netifaces.AF_INET6) + for f in iface_addrs[netifaces.AF_INET6] + if "addr" in f + ] + ) + + return list(addrs) + + def set_high_port(self): + interface_addresses = self.get_interface_addresses() + interface_address = interface_addresses[0][0] + family = interface_addresses[0][1] + caught_errors = [] + for attempt in range(100): + port = random.randint(49152, 65535) + addrinfo = socket.getaddrinfo( + interface_address, port, family, socket.SOCK_DGRAM, socket.IPPROTO_UDP + )[0] + sock = socket.socket(addrinfo[0], addrinfo[1], addrinfo[2]) + try: + sock.bind(addrinfo[4]) + except socket.error as e: + caught_errors.append((port, e)) + continue + sock.close() + if attempt > 0: + self.logger.warning( + "It took {} attempts to get an unused port: {}".format( + attempt + 1, caught_errors ) ) - break - - def setup_listener(self): - # If called idempotently, destroy all listeners first - # Do not close systemd-supplied sockets, as they cannot be reopened - self.close_socks(close_systemd=False) + self.args.port = str(port) + return + raise caught_errors[-1][1] - self.sock_classes = [] - bound_addresses = [] - self.gather_systemd_socks() - if len(self.systemd_socks) > 0: - for sock in self.systemd_socks: - self.sock_classes.append(SocketClass(sock)) - interface_addresses = [] - elif self.args.interface_address: - interface_addresses = self.args.interface_address + def get_interface_addresses(self): + if self.args.interface_address: + # Addresses supplied by user + interface_addresses = [ + (addr, socket.AF_UNSPEC) for addr in self.args.interface_address + ] elif not isinstance(netifaces, ImportError): - addrs = set() - for iface in netifaces.interfaces(): - iface_addrs = netifaces.ifaddresses(iface) - if (self.args.ipv4 or (not self.args.ipv4 and not self.args.ipv6)) and ( - netifaces.AF_INET in iface_addrs - ): - addrs.update( - [ - f["addr"] - for f in iface_addrs[netifaces.AF_INET] - if "addr" in f - ] - ) - if ( - self.has_ipv6 - and (self.args.ipv6 or (not self.args.ipv4 and not self.args.ipv6)) - and (netifaces.AF_INET6 in iface_addrs) - ): - addrs.update( - [ - f["addr"] - for f in iface_addrs[netifaces.AF_INET6] - if "addr" in f - ] - ) - interface_addresses = list(addrs) + # Addresses provided by netifaces + interface_addresses = self.get_system_addresses() elif self.args.all_interfaces: # --all-interfaces is pretty much deprecated; this section is # only triggered if it's explicitly passed but netifaces is not @@ -815,73 +903,126 @@ "All interface addresses not available; please install netifaces" ) else: - interface_addresses = ["0.0.0.0"] + # Last resort + interface_addresses = [("0.0.0.0", socket.AF_INET)] if self.has_ipv6: - interface_addresses.append("::") - for interface_address in interface_addresses: - for addrinfo in socket.getaddrinfo( + interface_addresses.append(("::", socket.AF_INET6)) + + return interface_addresses + + def setup_sockets(self): + if self.args.port == "-1": + # Special testing mode + self.set_high_port() + + if self.args.loopback: + self.setup_sockets_loopback() + if self.args.host: + self.setup_sockets_client() + if self.args.listen: + self.gather_systemd_socks() + self.setup_sockets_listener() + + def setup_sockets_loopback(self): + # Client socket setup is only done once. + if [ + sock_class + for sock_class in self.sock_classes_open + if sock_class.is_loopback and sock_class.is_client + ]: + return + + for i in range(self.args.loopback_pairs): + sock_client, sock_listener = socket.socketpair( + socket.AF_UNIX, socket.SOCK_DGRAM + ) + sock_class_client = SocketClass(sock_client) + sock_class_client.is_loopback = True + sock_class_client.is_client = True + sock_class_client.next_send = self.time_start + self.poller.register(sock_class_client, selectors.EVENT_READ) + self.sock_classes.append(sock_class_client) + self.logger.debug("Opened socket: {}".format(sock_class_client)) + + sock_class_listener = SocketClass(sock_listener) + sock_class_listener.is_loopback = True + sock_class_listener.is_listener = True + self.poller.register(sock_class_listener, selectors.EVENT_READ) + self.sock_classes.append(sock_class_listener) + self.logger.debug("Opened socket: {}".format(sock_class_listener)) + + def setup_sockets_listener(self): + # Do not set up listener sockets if systemd sockets exist. + if [ + sock_class for sock_class in self.sock_classes_open if sock_class.is_systemd + ]: + return + + interface_addresses = self.get_interface_addresses() + interface_sock_classes = [] + for interface_address, interface_family in interface_addresses: + interface_sock_classes += self.setup_interface_address( interface_address, - self.args.port, - socket.AF_UNSPEC, - socket.SOCK_DGRAM, - socket.IPPROTO_UDP, - ): - if addrinfo in bound_addresses: - continue - if ( - (addrinfo[0] == socket.AF_INET6) - and (not self.args.ipv4) - and self.has_ipv6 - ): - pass - elif (addrinfo[0] == socket.AF_INET) and (not self.args.ipv6): - pass - else: - continue - sock = self.new_socket(addrinfo[0], addrinfo[1], addrinfo[4]) - self.sock_classes.append(SocketClass(sock)) - bound_addresses.append(addrinfo) - for sock_class in self.sock_classes: - self.poller.register(sock_class) - self.print_out( - _("2PING listener ({address}): {min} to {max} bytes of data.").format( - address=sock_class.sock.getsockname()[0], - min=self.args.min_packet_size, - max=self.args.max_packet_size, - ) + port=self.args.port, + family=interface_family, + tags=["inet", "listener"], + ) + + listener_sock_classes = [ + sock_class + for sock_class in self.sock_classes_open + if sock_class.is_inet and sock_class.is_listener + ] + to_close = [] + for sock_class in listener_sock_classes: + if sock_class not in interface_sock_classes: + to_close.append(sock_class) + + self.close_socks(to_close) + + def get_srv_hosts(self): + if isinstance(dns_resolver, ImportError): + raise socket.error( + "DNS SRV lookups not available; please install dnspython" ) + hosts = [] + for lookup in self.args.host: + lookup_hosts_found = 0 + self.logger.debug("SRV lookup: {}".format(lookup)) + try: + res = dns_resolver.query( + "_{}._udp.{}".format(self.args.srv_service, lookup), "srv" + ) + except dns_resolver.dns.exception.DNSException as e: + raise socket.error("{}: {}".format(lookup, e)) + for rdata in res: + self.logger.debug("SRV result for {}: {}".format(lookup, rdata)) + if (str(rdata.target), rdata.port) in hosts: + continue + hosts.append((str(rdata.target), rdata.port)) + lookup_hosts_found += 1 + if lookup_hosts_found == 0: + raise socket.error("{}: No SRV results".format(lookup)) + return hosts + + def setup_sockets_client(self): + # Client socket setup is only done once. + if [ + sock_class + for sock_class in self.sock_classes_open + if sock_class.is_inet and sock_class.is_client + ]: + return - def setup_client(self): if self.args.srv: - if isinstance(dns_resolver, ImportError): - raise socket.error( - "DNS SRV lookups not available; please install dnspython" - ) - hosts = [] - for lookup in self.args.host: - lookup_hosts_found = 0 - self.print_debug("SRV lookup: {}".format(lookup)) - try: - res = dns_resolver.query( - "_{}._udp.{}".format(self.args.srv_service, lookup), "srv" - ) - except dns_resolver.dns.exception.DNSException as e: - raise socket.error("{}: {}".format(lookup, repr(e))) - for rdata in res: - self.print_debug( - "SRV result for {}: {}".format(lookup, repr(rdata)) - ) - if (str(rdata.target), rdata.port) in hosts: - continue - hosts.append((str(rdata.target), rdata.port)) - lookup_hosts_found += 1 - if lookup_hosts_found == 0: - raise socket.error("{}: No SRV results".format(lookup)) + hosts = self.get_srv_hosts() else: hosts = [(x, self.args.port) for x in self.args.host] for (hostname, port) in hosts: + if str(port) in ("None", "-1"): + port = self.args.port try: - self.setup_client_host(hostname, port) + self.setup_sockets_client_host(hostname, port) except socket.error as e: eargs = list(e.args) if len(eargs) == 1: @@ -890,7 +1031,7 @@ eargs[1] = "{}: {}".format(hostname, eargs[1]) raise socket.error(*eargs) - def setup_client_host(self, hostname, port): + def setup_sockets_client_host(self, hostname, port): host_info = None for addrinfo in socket.getaddrinfo( hostname, @@ -915,44 +1056,28 @@ if host_info is None: raise socket.error("Name or service not known") - bind_info = None - if self.args.interface_address: - h = self.args.interface_address[-1] + if len(self.args.interface_address) == 1: + interface_address = self.args.interface_address[0] + elif host_info[0] == socket.AF_INET6: + interface_address = "::" else: - if host_info[0] == socket.AF_INET6: - h = "::" - else: - h = "0.0.0.0" - for addrinfo in socket.getaddrinfo( - h, 0, host_info[0], socket.SOCK_DGRAM, socket.IPPROTO_UDP + interface_address = "0.0.0.0" + + for sock_class in self.setup_interface_address( + interface_address, + family=host_info[0], + type=host_info[1], + proto=host_info[2], + tags=["inet", "client"], ): - bind_info = addrinfo - break - if bind_info is None: - raise socket.error( - _("Cannot find suitable bind for {address}").format( - address=host_info[4] - ) - ) - sock = self.new_socket(bind_info[0], bind_info[1], bind_info[4]) - sock_class = SocketClass(sock) - sock_class.client_host = host_info - sock_class.session = bytes([random.randint(0, 255) for x in range(8)]) - self.sock_classes.append(sock_class) - self.poller.register(sock_class) - if not self.args.nagios: - self.print_out( - _("2PING {hostname} ({address}): {min} to {max} bytes of data.").format( - hostname=host_info[3], - address=host_info[4][0], - min=self.args.min_packet_size, - max=self.args.max_packet_size, - ) - ) + sock_class.client_host = host_info + sock_class.next_send = self.time_start - def send_new_ping(self, sock_class, peer_address): + def send_new_ping(self, sock_class, peer_address=None): sock = sock_class.sock - socket_address = sock.getsockname() + socket_address = sock_class.address + if not peer_address and sock_class.client_host: + peer_address = sock_class.client_host[4] peer_tuple = (socket_address, peer_address, sock.type) if peer_tuple not in sock_class.peer_states: sock_class.peer_states[peer_tuple] = PeerState(peer_tuple, sock_class) @@ -987,30 +1112,25 @@ now = clock() self.sock_sendto(sock_class, sock_out, peer_address) - self.packets_transmitted += 1 sock_class.packets_transmitted += 1 - self.pings_transmitted += 1 sock_class.pings_transmitted += 1 peer_state.ping_position += 1 - peer_state.sent_messages[nunpack(packet_out.message_id)] = ( + peer_state.sent_messages[packet_out.message_id] = ( now, packet_out.message_id, peer_state.ping_position, ) packet_out_examine = packets.Packet() packet_out_examine.load(dump_out) - if self.args.quiet: - pass - elif self.args.flood: - self.print_out(".", end="", flush=True) if self.args.verbose: - if self.args.encrypt: - self.print_out("SEND (encrypted): {}".format(repr(packet_out_examine))) - else: - self.print_out("SEND: {}".format(repr(packet_out_examine))) + self.logger.info( + "SEND{}: {}".format( + (" (encrypted)" if self.args.encrypt else ""), packet_out_examine + ) + ) def update_rtts(self, sock_class, rtt): - for c in (self, sock_class): + for c in (sock_class,): c.rtt_total += rtt c.rtt_total_sq += rtt ** 2 c.rtt_count += 1 @@ -1027,50 +1147,86 @@ self.print_stats(short=True) def sighup_handler(self, signum, frame): - self.print_debug("Received SIGHUP, scheduling reload") + self.logger.debug("Received SIGHUP, scheduling reload") self.is_reload = True + def get_nagios_stats(self): + sock_class = [ + sock_class for sock_class in self.sock_classes if sock_class.is_client + ][0] + + pings_lost = sock_class.pings_transmitted - sock_class.pings_received + lost_pct = div0(pings_lost, sock_class.pings_transmitted) * 100 + rtt_avg = div0(float(sock_class.rtt_total), sock_class.rtt_count) + + if (lost_pct >= self.args.nagios_crit_loss) or ( + rtt_avg >= self.args.nagios_crit_rta + ): + nagios_result = 2 + nagios_result_text = "CRITICAL" + elif (lost_pct >= self.args.nagios_warn_loss) or ( + rtt_avg >= self.args.nagios_warn_rta + ): + nagios_result = 1 + nagios_result_text = "WARNING" + else: + nagios_result = 0 + nagios_result_text = "OK" + + nagios_stats_text = _( + "{result} 2PING - Packet loss = {loss}%, RTA = {avg:0.03f} ms" + ).format(result=nagios_result_text, loss=int(lost_pct), avg=rtt_avg) + ( + "|rta={avg:0.06f}ms;{avgwarn:0.06f};{avgcrit:0.06f};0.000000 pl={loss}%;{losswarn};{losscrit};0" + ).format( + avg=rtt_avg, + loss=int(lost_pct), + avgwarn=self.args.nagios_warn_rta, + avgcrit=self.args.nagios_crit_rta, + losswarn=int(self.args.nagios_warn_loss), + losscrit=int(self.args.nagios_crit_loss), + ) + return nagios_result, nagios_stats_text + def print_stats(self, short=False): time_end = clock() - if self.args.listen: - self.print_stats_sock(time_end, short=short, sock_class=None) - else: - for sock_class in self.sock_classes: - self.print_stats_sock(time_end, short=short, sock_class=sock_class) + for sock_class in self.sock_classes: + self.print_stats_sock(sock_class, time_end, short=short) - def print_stats_sock(self, time_end, short=False, sock_class=None): - if sock_class is not None: - stats_class = sock_class - else: - stats_class = self + def print_stats_sock(self, sock_class, time_end, short=False): + stats_class = sock_class time_start = self.time_start pings_lost = stats_class.pings_transmitted - stats_class.pings_received - lost_pct = lazy_div(pings_lost, stats_class.pings_transmitted) * 100 + lost_pct = div0(pings_lost, stats_class.pings_transmitted) * 100 lost_undetermined = pings_lost - ( stats_class.lost_outbound + stats_class.lost_inbound ) outbound_pct = ( - lazy_div(stats_class.lost_outbound, stats_class.pings_transmitted) * 100 + div0(stats_class.lost_outbound, stats_class.pings_transmitted) * 100 ) inbound_pct = ( - lazy_div(stats_class.lost_inbound, stats_class.pings_transmitted) * 100 + div0(stats_class.lost_inbound, stats_class.pings_transmitted) * 100 ) - undetermined_pct = ( - lazy_div(lost_undetermined, stats_class.pings_transmitted) * 100 - ) - rtt_avg = lazy_div(float(stats_class.rtt_total), stats_class.rtt_count) + undetermined_pct = div0(lost_undetermined, stats_class.pings_transmitted) * 100 + rtt_avg = div0(float(stats_class.rtt_total), stats_class.rtt_count) rtt_ewma = stats_class.rtt_ewma / 8.0 rtt_mdev = math.sqrt( - lazy_div(stats_class.rtt_total_sq, stats_class.rtt_count) - - (lazy_div(stats_class.rtt_total, stats_class.rtt_count) ** 2) + div0(stats_class.rtt_total_sq, stats_class.rtt_count) + - (div0(stats_class.rtt_total, stats_class.rtt_count) ** 2) ) - if self.args.listen: - hostname = _("Listener") + socket_address = sock_class.address + if sock_class.client_host: + if sock_class.client_host[3]: + hostname = "{} ({})".format( + sock_class.client_host[3], sock_class.client_host[4][0] + ) + else: + hostname = sock_class.client_host[4][0] + elif socket_address: + hostname = socket_address[0] else: - hostname = sock_class.client_host[3] + hostname = sock_class if short: - self.print_out("\x0d", end="", flush=True, file=sys.stderr) - self.print_out( + self.logger.info( _pl( ( "{hostname}: {transmitted}/{received} ping, {loss}% loss " @@ -1096,46 +1252,16 @@ ewma=rtt_ewma, max=stats_class.rtt_max, mdev=rtt_mdev, - ), - file=sys.stderr, - ) - elif self.args.nagios: - if (lost_pct >= self.args.nagios_crit_loss) or ( - rtt_avg >= self.args.nagios_crit_rta - ): - self.nagios_result = 2 - nagios_result_text = "CRITICAL" - elif (lost_pct >= self.args.nagios_warn_loss) or ( - rtt_avg >= self.args.nagios_warn_rta - ): - self.nagios_result = 1 - nagios_result_text = "WARNING" - else: - self.nagios_result = 0 - nagios_result_text = "OK" - self.print_out( - _( - "2PING {result} - Packet loss = {loss}%, RTA = {avg:0.03f} ms" - ).format(result=nagios_result_text, loss=int(lost_pct), avg=rtt_avg) - + ( - "|rta={avg:0.06f}ms;{avgwarn:0.06f};{avgcrit:0.06f};0.000000 pl={loss}%;{losswarn};{losscrit};0" - ).format( - avg=rtt_avg, - loss=int(lost_pct), - avgwarn=self.args.nagios_warn_rta, - avgcrit=self.args.nagios_crit_rta, - losswarn=int(self.args.nagios_warn_loss), - losscrit=int(self.args.nagios_crit_loss), ) ) else: - self.print_out("") - self.print_out( + self.logger.info("") + self.logger.info( "--- {} ---".format( _("{hostname} 2ping statistics").format(hostname=hostname) ) ) - self.print_out( + self.logger.info( _pl( "{transmitted} ping transmitted, {received} received, {loss}% ping loss, time {time}", "{transmitted} pings transmitted, {received} received, {loss}% ping loss, time {time}", @@ -1147,7 +1273,7 @@ time=stats_time(time_end - time_start), ) ) - self.print_out( + self.logger.info( _pl( ( "{outbound} outbound ping loss ({outboundpct}%), {inbound} inbound ({inboundpct}%), " @@ -1167,7 +1293,7 @@ undeterminedpct=int(undetermined_pct), ) ) - self.print_out( + self.logger.info( _( "rtt min/avg/ewma/max/mdev = {min:0.03f}/{avg:0.03f}/{ewma:0.03f}/{max:0.03f}/{mdev:0.03f} ms" ).format( @@ -1178,7 +1304,7 @@ mdev=rtt_mdev, ) ) - self.print_out( + self.logger.info( _pl( "{transmitted} raw packet transmitted, {received} received", "{transmitted} raw packets transmitted, {received} received", @@ -1193,33 +1319,71 @@ pass def run(self): - self.print_debug("Clock value: {:f}".format(clock())) - self.print_debug("Poller: {}".format(self.poller.poller_type)) + self.logger.debug("Clock value: {:f}".format(clock())) + self.logger.debug("Poller: {}".format(self.poller)) if hasattr(signal, "SIGQUIT"): signal.signal(signal.SIGQUIT, self.sigquit_handler) if hasattr(signal, "SIGHUP"): signal.signal(signal.SIGHUP, self.sighup_handler) try: - if self.args.listen: - self.setup_listener() - else: - self.setup_client() + self.setup_sockets() except (socket.error, socket.gaierror) as e: - self.print_out(str(e)) - return 1 + if self.args.debug: + raise + else: + self.logger.error(str(e)) + return 1 + + for sock_class in self.sock_classes_active: + if self.args.nagios: + continue + socket_address = sock_class.address + if sock_class.client_host: + if sock_class.client_host[3]: + host_display = "{} ({})".format( + sock_class.client_host[3], sock_class.client_host[4][0] + ) + else: + host_display = "{}".format(sock_class.client_host[4][0]) + elif sock_class.next_send: + host_display = "{}".format(sock_class) + elif socket_address: + host_display = "listener ({})".format(socket_address[0]) + else: + host_display = "listener ({})".format(sock_class) + self.logger.info( + _("2PING {host}: {min} to {max} bytes of data.").format( + host=host_display, + min=self.args.min_packet_size, + max=self.args.max_packet_size, + ) + ) self.ready() + + if self.args.preload > 1: + for sock_class in self.sock_classes_active: + if not sock_class.next_send: + continue + for i in range(self.args.preload - 1): + self.send_new_ping(sock_class) + try: self.loop() except KeyboardInterrupt: pass - self.print_stats() + if self.args.nagios: + nagios_result, nagios_stats_text = self.get_nagios_stats() + self.print_out(nagios_stats_text) + else: + self.print_stats() self.close_socks() if self.args.nagios: - return self.nagios_result - return 0 + return nagios_result + else: + return 0 def base_packet(self): packet_out = packets.Packet() @@ -1290,8 +1454,9 @@ return packet_out def scheduled_cleanup(self): - self.print_debug("Cleanup") - for sock_class in self.sock_classes: + self.logger.debug("Cleanup") + self.setup_sockets() + for sock_class in self.sock_classes_active: self.scheduled_cleanup_sock_class(sock_class) def scheduled_cleanup_sock_class(self, sock_class): @@ -1300,7 +1465,7 @@ peer_state = sock_class.peer_states[peer_tuple] if now > peer_state.last_seen + 600.0: del sock_class.peer_states[peer_tuple] - self.print_debug("Cleanup: Removed {}".format(repr(peer_tuple))) + self.logger.debug("Cleanup: Removed {}".format(peer_tuple)) continue for table_name, max_time in ( ("sent_messages", 600.0), @@ -1312,14 +1477,19 @@ for table_key_name in tuple(table.keys()): if now > (table[table_key_name][0] + max_time): del table[table_key_name] - self.print_debug( + self.logger.debug( "Cleanup: Removed {} {} {}".format( - repr(peer_tuple), table_name, table_key_name + peer_tuple, table_name, table_key_name ) ) - def new_socket(self, family, type, bind): - sock = socket.socket(family, type) + def new_socket(self, addrinfo): + family = addrinfo[0] + type = addrinfo[1] + proto = addrinfo[2] + bind = addrinfo[4] + + sock = socket.socket(family, type, proto) try: import IN @@ -1332,115 +1502,180 @@ except (AttributeError, socket.error): pass sock.bind(bind) - self.print_debug("Bound to: {}".format(repr((family, type, bind)))) return sock + def get_next_wakeup(self): + now = clock() + events = [(self.next_cleanup, "cleanup")] + for sock_class in self.sock_classes_active: + if sock_class.next_send: + events.append((sock_class.next_send, "send ({})".format(sock_class))) + if sock_class.shutdown_time > now: + events.append( + (sock_class.shutdown_time, "shutdown ({})".format(sock_class)) + ) + if self.args.stats: + events.append((self.next_stats, "stats")) + if self.args.deadline: + events.append((self.time_start + self.args.deadline, "deadline")) + + next_wakeup = now + self.old_age_interval + next_wakeup_reason = "old age" + for wakeup, reason in events: + if wakeup < next_wakeup: + next_wakeup = wakeup + next_wakeup_reason = reason + if next_wakeup < now: + next_wakeup = now + next_wakeup_reason = "time travel ({})".format(next_wakeup_reason) + + return next_wakeup, next_wakeup_reason + + def schedule_next_send(self, sock_class, reply_received=False): + now = clock() + if self.args.adaptive and sock_class.rtt_ewma: + # Adaptive gets recalculated immediately. + sock_class.next_send = now + (sock_class.rtt_ewma / 8.0 / 1000.0) + elif self.args.flood: + if reply_received: + # If we're in flood mode and a ping reply was received, send a new ping ASAP. + sock_class.next_send = now + else: + # Send has just happened, give it a short time for reply. + sock_class.next_send = now + 0.01 + elif not reply_received: + sock_class.next_send = now + self.args.interval + + def loop_client_send(self): + now = clock() + for sock_class in self.sock_classes_active: + if not sock_class.next_send: + # Not scheduled to send + continue + if now < sock_class.next_send: + # Not yet time + continue + + if self.args.count and (sock_class.pings_transmitted >= self.args.count): + self.logger.debug( + "loop_client_send: Setting shutdown time to +{} on {}".format( + self.args.interval, sock_class + ) + ) + sock_class.shutdown_time = now + self.args.interval + sock_class.next_send = 0 + continue + self.send_new_ping(sock_class) + self.schedule_next_send(sock_class) + + def loop_poller(self, next_wakeup): + timeout = next_wakeup - clock() + for key, mask in self.poller.select(timeout=(0 if timeout < 0 else timeout)): + sock_class = key.fileobj + try: + self.process_incoming_packet(sock_class) + except Exception: + self.logger.exception( + _("Received unexpected exception (please file a bug report)") + ) + if self.args.debug: + raise + + if ( + self.args.count + and (sock_class.pings_transmitted >= self.args.count) + and (sock_class.pings_transmitted == sock_class.pings_received) + ): + self.logger.debug( + "loop_poller: Setting shutdown time to now on {}".format(sock_class) + ) + sock_class.shutdown_time = clock() + sock_class.next_send = 0 + def loop(self): while True: + self.loop_client_send() + next_wakeup, next_wakeup_reason = self.get_next_wakeup() + self.logger.debug( + "Next wakeup: {} ({})".format( + (next_wakeup - clock()), next_wakeup_reason + ) + ) + self.loop_poller(next_wakeup) + now = clock() + + if self.args.stats and now >= self.next_stats: + self.print_stats(short=True) + self.next_stats = now + self.args.stats + + if self.args.deadline and now >= (self.time_start + self.args.deadline): + for sock_class in self.sock_classes_active: + self.logger.debug( + "loop (deadline): Setting shutdown time to now on {}".format( + sock_class + ) + ) + sock_class.shutdown_time = now + sock_class.next_send = 0 + + if ( + self.args.count + and self.args.no_3way + and len( + [ + sock_class + for sock_class in self.sock_classes_active + if sock_class.is_client + ] + ) + == 0 + ): + # Special case for --count and --no-3way on a listener + for sock_class in [ + sock_class + for sock_class in self.sock_classes_active + if sock_class.is_listener + ]: + self.logger.debug( + "loop (no_3way): Setting shutdown time to now on {}".format( + sock_class + ) + ) + sock_class.shutdown_time = clock() + sock_class.next_send = 0 + if now >= self.next_cleanup: self.scheduled_cleanup() self.next_cleanup = now + 60.0 - if not self.args.listen: - for sock_class in self.sock_classes: - if sock_class.shutdown_time: - # Scheduled to shutdown, do not send any more pings - continue - if now >= sock_class.next_send: - if self.args.count and ( - sock_class.pings_transmitted >= self.args.count - ): - sock_class.shutdown_time = now + self.args.interval - sock_class.next_send = now + self.args.interval - continue - if (sock_class.pings_transmitted == 0) and ( - self.args.preload > 1 - ): - for i in range(self.args.preload): - self.send_new_ping( - sock_class, sock_class.client_host[4] - ) - else: - self.send_new_ping(sock_class, sock_class.client_host[4]) - if self.args.adaptive and sock_class.rtt_ewma: - sock_class.next_send = now + ( - sock_class.rtt_ewma / 8.0 / 1000.0 - ) - elif self.args.flood: - sock_class.next_send = now + 0.01 - else: - sock_class.next_send = now + self.args.interval - - next_wakeup = now + self.old_age_interval - next_wakeup_reason = "old age" - for sock_class in self.sock_classes: - if (not self.args.listen) and (sock_class.next_send < next_wakeup): - next_wakeup = sock_class.next_send - next_wakeup_reason = "send" - if sock_class.shutdown_time and ( - sock_class.shutdown_time < next_wakeup - ): - next_wakeup = sock_class.shutdown_time - next_wakeup_reason = "shutdown" - if self.args.stats: - if now >= self.next_stats: - self.print_stats(short=True) - self.next_stats = now + self.args.stats - if self.next_stats < next_wakeup: - next_wakeup = self.next_stats - next_wakeup_reason = "stats" - if self.args.deadline: - time_deadline = self.time_start + self.args.deadline - if now >= time_deadline: - return - if time_deadline < next_wakeup: - next_wakeup = time_deadline - next_wakeup_reason = "deadline" - if self.next_cleanup < next_wakeup: - next_wakeup = self.next_cleanup - next_wakeup_reason = "cleanup" - - if next_wakeup < now: - next_wakeup = now - next_wakeup_reason = "time travel" - self.print_debug( - "Next wakeup: {} ({})".format((next_wakeup - now), next_wakeup_reason) - ) - - for sock_class in self.poller.poll(next_wakeup - now): - try: - self.process_incoming_packet(sock_class) - except Exception as e: - self.print_out(_("Exception: {error}").format(error=str(e))) - if self.args.debug: - raise - if ( - self.args.count - and (sock_class.pings_transmitted >= self.args.count) - and (sock_class.pings_transmitted == sock_class.pings_received) - ): - sock_class.shutdown_time = now + if self.is_reload: + self.is_reload = False + self.setup_sockets() - all_shutdown = True - for sock_class in self.sock_classes: - if (not sock_class.shutdown_time) or (sock_class.shutdown_time >= now): - all_shutdown = False - break - if all_shutdown: + if len(self.sock_classes_active) == 0: return - if self.is_reload: - self.is_reload = False - if self.args.listen: - self.setup_listener() +def main(argv=None): + if argv is None: + argv = sys.argv -def main(): - args = parse_args() + args = parse_args(argv) t = TwoPing(args) + + if t.args.debug: + log_level = logging.DEBUG + else: + log_level = logging.INFO + logging.basicConfig(format="%(message)s", stream=sys.stdout, level=log_level) + return t.run() -if __name__ == "__main__": - sys.exit(int(main())) +def module_init(): + if __name__ == "__main__": + sys.exit(main(sys.argv)) + + +module_init() diff -Nru 2ping-4.4.1/twoping/crc32.py 2ping-4.5/twoping/crc32.py --- 2ping-4.4.1/twoping/crc32.py 2020-06-07 05:21:54.000000000 +0000 +++ 2ping-4.5/twoping/crc32.py 2020-06-17 21:36:15.000000000 +0000 @@ -18,6 +18,7 @@ import binascii import copy +import sys digest_size = 4 @@ -53,33 +54,36 @@ return out def hexdigest(self): - return "".join("{hex:02x}".format(hex=x) for x in self.digest()) + return self.digest().hex() def new(buf=None): return CRC32(buf) -if __name__ == "__main__": - import sys +def main(argv): + if argv is None: + argv = sys.argv - files = sys.argv[1:] + files = argv[1:] if len(files) == 0: - if hasattr(sys.stdin, "buffer"): - stdin = sys.stdin.buffer - else: - stdin = sys.stdin + files = ["-"] + + for file in files: c = new() - for buf in stdin.readlines(): - c.update(buf) - print(c.hexdigest()) - else: - for file in files: + if file == "-": + for buf in sys.stdin.buffer.readlines(): + c.update(buf) + else: with open(file, "rb") as f: - c = new() for buf in f.readlines(): c.update(buf) - if len(files) > 1: - print("{}\t{}".format(c.hexdigest(), file)) - else: - print(c.hexdigest()) + print("{}\t{}".format(c.hexdigest(), file)) + + +def module_init(): + if __name__ == "__main__": + sys.exit(main(sys.argv)) + + +module_init() diff -Nru 2ping-4.4.1/twoping/__init__.py 2ping-4.5/twoping/__init__.py --- 2ping-4.4.1/twoping/__init__.py 2020-06-08 20:14:04.000000000 +0000 +++ 2ping-4.5/twoping/__init__.py 2020-06-18 03:14:56.000000000 +0000 @@ -19,5 +19,5 @@ import sys -__version__ = "4.4.1" +__version__ = "4.5" assert sys.version_info > (3, 5) diff -Nru 2ping-4.4.1/twoping/packets.py 2ping-4.5/twoping/packets.py --- 2ping-4.4.1/twoping/packets.py 2020-06-07 19:39:00.000000000 +0000 +++ 2ping-4.5/twoping/packets.py 2020-06-17 21:49:27.000000000 +0000 @@ -21,13 +21,8 @@ from math import ceil import time -try: - from Crypto.Cipher import AES -except ImportError as e: - AES = e - from . import crc32 -from .utils import npack, nunpack, random, twoping_checksum +from .utils import AES, npack, nunpack, random, twoping_checksum class Extended: @@ -40,8 +35,7 @@ if self.id is None: return "".format(len(self.data)) else: - id_hex = "".join(["{:02x}".format(x) for x in npack(self.id, 4)]) - return "".format(id_hex, len(self.data)) + return "".format(self.id, len(self.data)) def load(self, data): self.data = data @@ -215,8 +209,7 @@ if self.id is None: return "".format(len(self.data)) else: - id_hex = "".join(["{:02x}".format(x) for x in npack(self.id, 2)]) - return "".format(id_hex, len(self.data)) + return "".format(self.id, len(self.data)) def load(self, data): self.data = data @@ -248,8 +241,7 @@ self.message_id = b"" def __repr__(self): - message_id_hex = "".join(["{:02x}".format(x) for x in self.message_id]) - return "".format(message_id_hex) + return "".format(self.message_id.hex()) def load(self, data): self.message_id = data[0:6] @@ -279,17 +271,16 @@ class OpcodeMessageIDList(Opcode): + _repr_name = "ID List (Generic)" + def __init__(self): self.message_ids = [] def __repr__(self): - ids = [ - "0x{}".format( - "".join(["{:02x}".format(x) for x in y]) for y in self.message_ids - ) - ] - return "".format( - ", ".join(ids), len(self.message_ids) + return "<{}: [{}] ({})>".format( + self._repr_name, + ", ".join(["0x{}".format(x.hex()) for x in self.message_ids]), + len(self.message_ids), ) def load(self, data): @@ -315,52 +306,22 @@ class OpcodeInvestigationSeen(OpcodeMessageIDList): id = 0x0008 - - def __repr__(self): - ids = [ - "0x{}".format("".join(["{:02x}".format(x) for x in y])) - for y in self.message_ids - ] - return "".format( - ", ".join(ids), len(self.message_ids) - ) + _repr_name = "Investigation Seen" class OpcodeInvestigationUnseen(OpcodeMessageIDList): id = 0x0010 - - def __repr__(self): - ids = [ - "0x{}".format("".join(["{:02x}".format(x) for x in y])) - for y in self.message_ids - ] - return "".format( - ", ".join(ids), len(self.message_ids) - ) + _repr_name = "Investigation Unseen" class OpcodeInvestigate(OpcodeMessageIDList): id = 0x0020 - - def __repr__(self): - ids = [ - "0x{}".format("".join(["{:02x}".format(x) for x in y])) - for y in self.message_ids - ] - return "".format(", ".join(ids), len(self.message_ids)) + _repr_name = "Investigate" class OpcodeCourtesyExpiration(OpcodeMessageIDList): id = 0x0040 - - def __repr__(self): - ids = [ - "0x{}".format("".join(["{:02x}".format(x) for x in y])) - for y in self.message_ids - ] - return "".format( - ", ".join(ids), len(self.message_ids) - ) + _repr_name = "Courtesy Expiration" class OpcodeHMAC(Opcode): @@ -382,8 +343,7 @@ def __repr__(self): if self.digest_index is not None: return "<{}: 0x{}>".format( - self.digest_map[self.digest_index][2], - "".join(["{:02x}".format(x) for x in self.hash]), + self.digest_map[self.digest_index][2], self.hash.hex() ) return "" @@ -574,7 +534,7 @@ class Packet: def __repr__(self): return "".format( - "".join(["{:02x}".format(x) for x in self.message_id]), + self.message_id.hex(), repr(sorted(self.opcodes.values(), key=lambda x: x.id)), ) diff -Nru 2ping-4.4.1/twoping/utils.py 2ping-4.5/twoping/utils.py --- 2ping-4.4.1/twoping/utils.py 2020-06-07 20:40:50.000000000 +0000 +++ 2ping-4.5/twoping/utils.py 2020-06-17 21:22:00.000000000 +0000 @@ -25,6 +25,14 @@ except ImportError as e: distro = e +try: + from Cryptodome.Cipher import AES +except ImportError as e: + try: + from Crypto.Cipher import AES + except ImportError: + AES = e + _ = gettext.translation("2ping", fallback=True).gettext _pl = gettext.translation("2ping", fallback=True).ngettext @@ -62,11 +70,9 @@ return checksum -def lazy_div(n, d): +def div0(n, d): """Pretend we live in a world where n / 0 == 0""" - if d == 0: - return 0 - return n / d + return 0 if d == 0 else n / d def npack(i, minimum=1):