├── .github └── workflows │ └── ci.yml ├── .gitignore ├── 2ping ├── 2ping.bash_completion ├── 2ping.service ├── 2ping.spec ├── 2ping6 ├── COPYING.md ├── ChangeLog.md ├── MANIFEST.in ├── Makefile ├── README.md ├── doc ├── 2ping-protocol-examples.py ├── 2ping-protocol.md ├── 2ping.1 ├── 2ping.md └── Makefile ├── requirements.txt ├── setup.py ├── tests ├── __init__.py ├── test_cli.py ├── test_crc32.py ├── test_packets.py ├── test_python.py └── test_utils.py ├── tox.ini ├── twoping ├── __init__.py ├── args.py ├── cli.py ├── crc32.py ├── packets.py └── utils.py └── wireshark ├── 2ping.lua └── 2ping.pcap /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | os: 9 | - macos-latest 10 | - ubuntu-latest 11 | - windows-latest 12 | python-version: 13 | - "3.10" 14 | - "3.12" 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Python dependencies 22 | run: | 23 | python -mpip install tox 24 | - name: tox 25 | run: | 26 | python -mtox 27 | - name: Upload sdist zip 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: 2ping-sdist-${{ github.job }}-${{ github.run_id }}.${{ github.run_number }}-${{ runner.os }}-py${{ matrix.python-version }} 31 | path: .tox/dist/*.zip 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | .pybuild/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | -------------------------------------------------------------------------------- /2ping: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # 2ping - A bi-directional ping utility 4 | # Copyright (C) 2010-2021 Ryan Finnie 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | import sys 8 | import twoping.cli 9 | 10 | 11 | def module_init(): 12 | if __name__ == "__main__": 13 | sys.exit(twoping.cli.main(sys.argv)) 14 | 15 | 16 | module_init() 17 | -------------------------------------------------------------------------------- /2ping.bash_completion: -------------------------------------------------------------------------------- 1 | # 2ping(1) completion -*- shell-script -*- 2 | 3 | _2ping() 4 | { 5 | local cur prev words cword 6 | if declare -F _init_completion >/dev/null 2>&1; then 7 | _init_completion 8 | elif declare -F _get_comp_words_by_ref >/dev/null 2>&1; then 9 | COMPREPLY=() 10 | _get_comp_words_by_ref cur prev words cword 11 | else 12 | return 13 | fi 14 | 15 | if [[ $cur == -* ]] && declare -F _parse_usage >/dev/null 2>&1; then 16 | COMPREPLY=( $( compgen -W '$( _parse_usage "$1" )' -- "$cur" ) ) 17 | return 18 | fi 19 | 20 | if declare -F _known_hosts_real >/dev/null 2>&1; then 21 | _known_hosts_real -- "$cur" 22 | fi 23 | } 24 | 25 | complete -F _2ping 2ping 2ping6 26 | 27 | # ex: filetype=sh 28 | -------------------------------------------------------------------------------- /2ping.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=2ping listener 3 | Documentation=man:2ping(1) 4 | Documentation=https://www.finnie.org/software/2ping/ 5 | 6 | # Recommended listener method is with python3-netifaces, so we want to 7 | # wait until the network is up before starting, so we can get a full 8 | # list of IPs. 9 | Wants=network-online.target 10 | After=network-online.target 11 | 12 | [Service] 13 | ExecStart=/usr/bin/2ping --listen --quiet 14 | 15 | # 2ping requires very little; lock down the running space as much as 16 | # possible. 17 | DevicePolicy=closed 18 | LockPersonality=yes 19 | NoNewPrivileges=yes 20 | PrivateDevices=yes 21 | PrivateTmp=yes 22 | PrivateUsers=yes 23 | ProtectControlGroups=yes 24 | ProtectHome=yes 25 | ProtectKernelLogs=yes 26 | ProtectKernelModules=yes 27 | ProtectKernelTunables=yes 28 | ProtectSystem=strict 29 | RestrictAddressFamilies=AF_NETLINK AF_INET AF_INET6 AF_UNIX 30 | RestrictNamespaces=yes 31 | RestrictRealtime=yes 32 | RestrictSUIDSGID=yes 33 | DynamicUser=yes 34 | 35 | [Install] 36 | WantedBy=multi-user.target 37 | -------------------------------------------------------------------------------- /2ping.spec: -------------------------------------------------------------------------------- 1 | Name: 2ping 2 | Version: 4.5.1 3 | Release: 1%{?dist} 4 | Summary: Bi-directional ping utility 5 | License: MPLv2.0 6 | URL: https://www.finnie.org/software/2ping 7 | Source0: https://www.finnie.org/software/%{name}/%{name}-%{version}.tar.gz 8 | BuildArch: noarch 9 | BuildRequires: python3-devel 10 | BuildRequires: python3-pytest 11 | BuildRequires: python3-setuptools 12 | BuildRequires: systemd 13 | 14 | %description 15 | 2ping is a bi-directional ping utility. It uses 3-way pings (akin to TCP SYN, 16 | SYN/ACK, ACK) and after-the-fact state comparison between a 2ping listener and 17 | a 2ping client to determine which direction packet loss occurs. 18 | 19 | %prep 20 | %autosetup 21 | 22 | %build 23 | %py3_build 24 | 25 | %install 26 | %py3_install 27 | install -Dp -m 0644 2ping.service %{buildroot}/%{_unitdir}/2ping.service 28 | install -Dp -m 0644 doc/2ping.1 %{buildroot}/%{_mandir}/man1/2ping.1 29 | install -Dp -m 0644 doc/2ping.1 %{buildroot}/%{_mandir}/man1/2ping6.1 30 | 31 | %check 32 | %{__python3} -mpytest 33 | 34 | %post 35 | %systemd_post 2ping.service 36 | 37 | %preun 38 | %systemd_preun 2ping.service 39 | 40 | %postun 41 | %systemd_postun 2ping.service 42 | 43 | %files 44 | %doc ChangeLog.md README.md 45 | %license COPYING.md 46 | %{python3_sitelib}/* 47 | %{_bindir}/%{name} 48 | %{_bindir}/%{name}6 49 | %{_mandir}/man1/%{name}.1* 50 | %{_mandir}/man1/%{name}6.1* 51 | %{_unitdir}/2ping.service 52 | -------------------------------------------------------------------------------- /2ping6: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # 2ping - A bi-directional ping utility 4 | # Copyright (C) 2010-2021 Ryan Finnie 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | import sys 8 | import twoping.cli 9 | 10 | 11 | def module_init(): 12 | if __name__ == "__main__": 13 | sys.exit(twoping.cli.main(sys.argv)) 14 | 15 | 16 | module_init() 17 | -------------------------------------------------------------------------------- /COPYING.md: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | ### 1. Definitions 5 | 6 | **1.1. “Contributor”** 7 | means each individual or legal entity that creates, contributes to 8 | the creation of, or owns Covered Software. 9 | 10 | **1.2. “Contributor Version”** 11 | means the combination of the Contributions of others (if any) used 12 | by a Contributor and that particular Contributor's Contribution. 13 | 14 | **1.3. “Contribution”** 15 | means Covered Software of a particular Contributor. 16 | 17 | **1.4. “Covered Software”** 18 | means Source Code Form to which the initial Contributor has attached 19 | the notice in Exhibit A, the Executable Form of such Source Code 20 | Form, and Modifications of such Source Code Form, in each case 21 | including portions thereof. 22 | 23 | **1.5. “Incompatible With Secondary Licenses”** 24 | means 25 | 26 | * **(a)** that the initial Contributor has attached the notice described 27 | in Exhibit B to the Covered Software; or 28 | * **(b)** that the Covered Software was made available under the terms of 29 | version 1.1 or earlier of the License, but not also under the 30 | terms of a Secondary License. 31 | 32 | **1.6. “Executable Form”** 33 | means any form of the work other than Source Code Form. 34 | 35 | **1.7. “Larger Work”** 36 | means a work that combines Covered Software with other material, in 37 | a separate file or files, that is not Covered Software. 38 | 39 | **1.8. “License”** 40 | means this document. 41 | 42 | **1.9. “Licensable”** 43 | means having the right to grant, to the maximum extent possible, 44 | whether at the time of the initial grant or subsequently, any and 45 | all of the rights conveyed by this License. 46 | 47 | **1.10. “Modifications”** 48 | means any of the following: 49 | 50 | * **(a)** any file in Source Code Form that results from an addition to, 51 | deletion from, or modification of the contents of Covered 52 | Software; or 53 | * **(b)** any new file in Source Code Form that contains any Covered 54 | Software. 55 | 56 | **1.11. “Patent Claims” of a Contributor** 57 | means any patent claim(s), including without limitation, method, 58 | process, and apparatus claims, in any patent Licensable by such 59 | Contributor that would be infringed, but for the grant of the 60 | License, by the making, using, selling, offering for sale, having 61 | made, import, or transfer of either its Contributions or its 62 | Contributor Version. 63 | 64 | **1.12. “Secondary License”** 65 | means either the GNU General Public License, Version 2.0, the GNU 66 | Lesser General Public License, Version 2.1, the GNU Affero General 67 | Public License, Version 3.0, or any later versions of those 68 | licenses. 69 | 70 | **1.13. “Source Code Form”** 71 | means the form of the work preferred for making modifications. 72 | 73 | **1.14. “You” (or “Your”)** 74 | means an individual or a legal entity exercising rights under this 75 | License. For legal entities, “You” includes any entity that 76 | controls, is controlled by, or is under common control with You. For 77 | purposes of this definition, “control” means **(a)** the power, direct 78 | or indirect, to cause the direction or management of such entity, 79 | whether by contract or otherwise, or **(b)** ownership of more than 80 | fifty percent (50%) of the outstanding shares or beneficial 81 | ownership of such entity. 82 | 83 | 84 | ### 2. License Grants and Conditions 85 | 86 | #### 2.1. Grants 87 | 88 | Each Contributor hereby grants You a world-wide, royalty-free, 89 | non-exclusive license: 90 | 91 | * **(a)** under intellectual property rights (other than patent or trademark) 92 | Licensable by such Contributor to use, reproduce, make available, 93 | modify, display, perform, distribute, and otherwise exploit its 94 | Contributions, either on an unmodified basis, with Modifications, or 95 | as part of a Larger Work; and 96 | * **(b)** under Patent Claims of such Contributor to make, use, sell, offer 97 | for sale, have made, import, and otherwise transfer either its 98 | Contributions or its Contributor Version. 99 | 100 | #### 2.2. Effective Date 101 | 102 | The licenses granted in Section 2.1 with respect to any Contribution 103 | become effective for each Contribution on the date the Contributor first 104 | distributes such Contribution. 105 | 106 | #### 2.3. Limitations on Grant Scope 107 | 108 | The licenses granted in this Section 2 are the only rights granted under 109 | this License. No additional rights or licenses will be implied from the 110 | distribution or licensing of Covered Software under this License. 111 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 112 | Contributor: 113 | 114 | * **(a)** for any code that a Contributor has removed from Covered Software; 115 | or 116 | * **(b)** for infringements caused by: **(i)** Your and any other third party's 117 | modifications of Covered Software, or **(ii)** the combination of its 118 | Contributions with other software (except as part of its Contributor 119 | Version); or 120 | * **(c)** under Patent Claims infringed by Covered Software in the absence of 121 | its Contributions. 122 | 123 | This License does not grant any rights in the trademarks, service marks, 124 | or logos of any Contributor (except as may be necessary to comply with 125 | the notice requirements in Section 3.4). 126 | 127 | #### 2.4. Subsequent Licenses 128 | 129 | No Contributor makes additional grants as a result of Your choice to 130 | distribute the Covered Software under a subsequent version of this 131 | License (see Section 10.2) or under the terms of a Secondary License (if 132 | permitted under the terms of Section 3.3). 133 | 134 | #### 2.5. Representation 135 | 136 | Each Contributor represents that the Contributor believes its 137 | Contributions are its original creation(s) or it has sufficient rights 138 | to grant the rights to its Contributions conveyed by this License. 139 | 140 | #### 2.6. Fair Use 141 | 142 | This License is not intended to limit any rights You have under 143 | applicable copyright doctrines of fair use, fair dealing, or other 144 | equivalents. 145 | 146 | #### 2.7. Conditions 147 | 148 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 149 | in Section 2.1. 150 | 151 | 152 | ### 3. Responsibilities 153 | 154 | #### 3.1. Distribution of Source Form 155 | 156 | All distribution of Covered Software in Source Code Form, including any 157 | Modifications that You create or to which You contribute, must be under 158 | the terms of this License. You must inform recipients that the Source 159 | Code Form of the Covered Software is governed by the terms of this 160 | License, and how they can obtain a copy of this License. You may not 161 | attempt to alter or restrict the recipients' rights in the Source Code 162 | Form. 163 | 164 | #### 3.2. Distribution of Executable Form 165 | 166 | If You distribute Covered Software in Executable Form then: 167 | 168 | * **(a)** such Covered Software must also be made available in Source Code 169 | Form, as described in Section 3.1, and You must inform recipients of 170 | the Executable Form how they can obtain a copy of such Source Code 171 | Form by reasonable means in a timely manner, at a charge no more 172 | than the cost of distribution to the recipient; and 173 | 174 | * **(b)** You may distribute such Executable Form under the terms of this 175 | License, or sublicense it under different terms, provided that the 176 | license for the Executable Form does not attempt to limit or alter 177 | the recipients' rights in the Source Code Form under this License. 178 | 179 | #### 3.3. Distribution of a Larger Work 180 | 181 | You may create and distribute a Larger Work under terms of Your choice, 182 | provided that You also comply with the requirements of this License for 183 | the Covered Software. If the Larger Work is a combination of Covered 184 | Software with a work governed by one or more Secondary Licenses, and the 185 | Covered Software is not Incompatible With Secondary Licenses, this 186 | License permits You to additionally distribute such Covered Software 187 | under the terms of such Secondary License(s), so that the recipient of 188 | the Larger Work may, at their option, further distribute the Covered 189 | Software under the terms of either this License or such Secondary 190 | License(s). 191 | 192 | #### 3.4. Notices 193 | 194 | You may not remove or alter the substance of any license notices 195 | (including copyright notices, patent notices, disclaimers of warranty, 196 | or limitations of liability) contained within the Source Code Form of 197 | the Covered Software, except that You may alter any license notices to 198 | the extent required to remedy known factual inaccuracies. 199 | 200 | #### 3.5. Application of Additional Terms 201 | 202 | You may choose to offer, and to charge a fee for, warranty, support, 203 | indemnity or liability obligations to one or more recipients of Covered 204 | Software. However, You may do so only on Your own behalf, and not on 205 | behalf of any Contributor. You must make it absolutely clear that any 206 | such warranty, support, indemnity, or liability obligation is offered by 207 | You alone, and You hereby agree to indemnify every Contributor for any 208 | liability incurred by such Contributor as a result of warranty, support, 209 | indemnity or liability terms You offer. You may include additional 210 | disclaimers of warranty and limitations of liability specific to any 211 | jurisdiction. 212 | 213 | 214 | ### 4. Inability to Comply Due to Statute or Regulation 215 | 216 | If it is impossible for You to comply with any of the terms of this 217 | License with respect to some or all of the Covered Software due to 218 | statute, judicial order, or regulation then You must: **(a)** comply with 219 | the terms of this License to the maximum extent possible; and **(b)** 220 | describe the limitations and the code they affect. Such description must 221 | be placed in a text file included with all distributions of the Covered 222 | Software under this License. Except to the extent prohibited by statute 223 | or regulation, such description must be sufficiently detailed for a 224 | recipient of ordinary skill to be able to understand it. 225 | 226 | 227 | ### 5. Termination 228 | 229 | **5.1.** The rights granted under this License will terminate automatically 230 | if You fail to comply with any of its terms. However, if You become 231 | compliant, then the rights granted under this License from a particular 232 | Contributor are reinstated **(a)** provisionally, unless and until such 233 | Contributor explicitly and finally terminates Your grants, and **(b)** on an 234 | ongoing basis, if such Contributor fails to notify You of the 235 | non-compliance by some reasonable means prior to 60 days after You have 236 | come back into compliance. Moreover, Your grants from a particular 237 | Contributor are reinstated on an ongoing basis if such Contributor 238 | notifies You of the non-compliance by some reasonable means, this is the 239 | first time You have received notice of non-compliance with this License 240 | from such Contributor, and You become compliant prior to 30 days after 241 | Your receipt of the notice. 242 | 243 | **5.2.** If You initiate litigation against any entity by asserting a patent 244 | infringement claim (excluding declaratory judgment actions, 245 | counter-claims, and cross-claims) alleging that a Contributor Version 246 | directly or indirectly infringes any patent, then the rights granted to 247 | You by any and all Contributors for the Covered Software under Section 248 | 2.1 of this License shall terminate. 249 | 250 | **5.3.** In the event of termination under Sections 5.1 or 5.2 above, all 251 | end user license agreements (excluding distributors and resellers) which 252 | have been validly granted by You or Your distributors under this License 253 | prior to termination shall survive termination. 254 | 255 | 256 | ### 6. Disclaimer of Warranty 257 | 258 | > Covered Software is provided under this License on an “as is” 259 | > basis, without warranty of any kind, either expressed, implied, or 260 | > statutory, including, without limitation, warranties that the 261 | > Covered Software is free of defects, merchantable, fit for a 262 | > particular purpose or non-infringing. The entire risk as to the 263 | > quality and performance of the Covered Software is with You. 264 | > Should any Covered Software prove defective in any respect, You 265 | > (not any Contributor) assume the cost of any necessary servicing, 266 | > repair, or correction. This disclaimer of warranty constitutes an 267 | > essential part of this License. No use of any Covered Software is 268 | > authorized under this License except under this disclaimer. 269 | 270 | ### 7. Limitation of Liability 271 | 272 | > Under no circumstances and under no legal theory, whether tort 273 | > (including negligence), contract, or otherwise, shall any 274 | > Contributor, or anyone who distributes Covered Software as 275 | > permitted above, be liable to You for any direct, indirect, 276 | > special, incidental, or consequential damages of any character 277 | > including, without limitation, damages for lost profits, loss of 278 | > goodwill, work stoppage, computer failure or malfunction, or any 279 | > and all other commercial damages or losses, even if such party 280 | > shall have been informed of the possibility of such damages. This 281 | > limitation of liability shall not apply to liability for death or 282 | > personal injury resulting from such party's negligence to the 283 | > extent applicable law prohibits such limitation. Some 284 | > jurisdictions do not allow the exclusion or limitation of 285 | > incidental or consequential damages, so this exclusion and 286 | > limitation may not apply to You. 287 | 288 | 289 | ### 8. Litigation 290 | 291 | Any litigation relating to this License may be brought only in the 292 | courts of a jurisdiction where the defendant maintains its principal 293 | place of business and such litigation shall be governed by laws of that 294 | jurisdiction, without reference to its conflict-of-law provisions. 295 | Nothing in this Section shall prevent a party's ability to bring 296 | cross-claims or counter-claims. 297 | 298 | 299 | ### 9. Miscellaneous 300 | 301 | This License represents the complete agreement concerning the subject 302 | matter hereof. If any provision of this License is held to be 303 | unenforceable, such provision shall be reformed only to the extent 304 | necessary to make it enforceable. Any law or regulation which provides 305 | that the language of a contract shall be construed against the drafter 306 | shall not be used to construe this License against a Contributor. 307 | 308 | 309 | ### 10. Versions of the License 310 | 311 | #### 10.1. New Versions 312 | 313 | Mozilla Foundation is the license steward. Except as provided in Section 314 | 10.3, no one other than the license steward has the right to modify or 315 | publish new versions of this License. Each version will be given a 316 | distinguishing version number. 317 | 318 | #### 10.2. Effect of New Versions 319 | 320 | You may distribute the Covered Software under the terms of the version 321 | of the License under which You originally received the Covered Software, 322 | or under the terms of any subsequent version published by the license 323 | steward. 324 | 325 | #### 10.3. Modified Versions 326 | 327 | If you create software not governed by this License, and you want to 328 | create a new license for such software, you may create and use a 329 | modified version of this License if you rename the license and remove 330 | any references to the name of the license steward (except to note that 331 | such modified license differs from this License). 332 | 333 | #### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 334 | 335 | If You choose to distribute Source Code Form that is Incompatible With 336 | Secondary Licenses under the terms of this version of the License, the 337 | notice described in Exhibit B of this License must be attached. 338 | 339 | ## Exhibit A - Source Code Form License Notice 340 | 341 | This Source Code Form is subject to the terms of the Mozilla Public 342 | License, v. 2.0. If a copy of the MPL was not distributed with this 343 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 344 | 345 | If it is not possible or desirable to put the notice in a particular 346 | file, then You may include the notice in a location (such as a LICENSE 347 | file in a relevant directory) where a recipient would be likely to look 348 | for such a notice. 349 | 350 | You may add additional accurate notices of copyright ownership. 351 | 352 | ## Exhibit B - “Incompatible With Secondary Licenses” Notice 353 | 354 | This Source Code Form is "Incompatible With Secondary Licenses", as 355 | defined by the Mozilla Public License, v. 2.0. 356 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # 2ping 4.5.1 (2021-03-20) 2 | 3 | - 2ping.bash_completion: Make more resilient to failure / missing 4 | features. 5 | - 2ping.spec: Add missing BuildRequires: systemd. 6 | - Minimum Python version changed from 3.5 to 3.6. 7 | - Minor no-op code and distribution updates. 8 | 9 | # 2ping 4.5 (2020-06-18) 10 | 11 | - Added PyCryptodome support (recommended over PyCrypto, though the 12 | latter is still detected/supported). 13 | - Replaced best_poller module with Python native selectors module. 14 | - Changed --flood output: dots/backspaces are no longer printed, and 15 | loss results / errors display full details. 16 | - --audible tones will only occur if stdout is a TTY. 17 | - Improved hostname/IP display edge cases. 18 | - Added an AF_UNIX --loopback test mode. 19 | - Listener sockets are added and removed as needed, instead of being 20 | re-created on each rescan. 21 | - Listener sockets are automatically rescanned periodically. 22 | - Multiple systemd sockets are now allowed. 23 | - A run can be both a listener and a client at the same time (mainly 24 | useful for smoke testing). 25 | - Other socket handling refactoring. 26 | - Other code refactoring. 27 | - Listener statistics are displayer per-bind. 28 | - Many, many testing/CI improvements. 29 | 30 | # 2ping 4.4.1 (2020-06-08) 31 | 32 | - Fixed 2ping.spec referencing old README and making `rpmbuild -ta 33 | 2ping.tar.gz` fail. 34 | - Added systemd 2ping.service. 35 | - Added snapcraft.yaml. 36 | 37 | # 2ping 4.4 (2020-06-07) 38 | 39 | - Minimum Python version changed from 3.4 to 3.5 40 | - Monotonic clock is always used 41 | - If the Python "netifaces" module is installed (preferred), --listen 42 | will now listen on all addresses by default, as opposed to requiring 43 | --all-interfaces previously 44 | - Add --subtract-peer-host-latency 45 | - Add support for systemd-supplied sockets 46 | - Remove deprecated/removed linux_distribution, use distro if available 47 | - Code/documentation cleanups, 2ping protocol 4.1 48 | 49 | # 2ping 4.3 (2018-12-03) 50 | 51 | - Add --srv-service 52 | - Change --adaptive behavior to better match ping -A 53 | - Fix typos in manpage 54 | 55 | # 2ping 4.2 (2018-08-11) 56 | 57 | - Added SIGHUP handling of listener processes 58 | - Added an example bash_completion script 59 | - Better cleanup handling of peer information 60 | 61 | # 2ping 4.1.2 (2018-08-09) 62 | 63 | - Fix UTF-8 tests when run with invalid locale (Debian Bug#897498) 64 | - Fix cleanup on non-encrypted sessions (GitHub rfinnie/2ping#5) 65 | 66 | # 2ping 4.1 (2017-08-06) 67 | 68 | - Fixed --fuzz CRC function. 69 | - Added --encrypt option for shared-secret encrypted packets. 70 | - Added --listen --all-interfaces option for automatically binding to 71 | all interface IPs (requires Python netifaces module). 72 | - Simplified monotonic_clock functionality, relying on Python 3 for most 73 | functionality, reducing the possibility of platform bugs. 74 | - Minor fixes and unit test suite improvements. 75 | 76 | # 2ping 4.0.1 (2017-07-22) 77 | 78 | - Fixed unit tests causing test failure in certain conditions. 79 | 80 | # 2ping 4.0 (2017-07-22) 81 | 82 | - Rewrite from Python 2 to Python 3 (3.4 or higher). 83 | - Fixed hmac-sha256 handling, added hmac-sha512. 84 | - --nagios will now work when combined with --deadline, in addition to 85 | --count. 86 | - Added Wireshark Lua dissector and sample capture. 87 | - Added battery level (ExtID 0x88a1f7c7). Note that while 2ping 88 | recognizes the new option in incoming packets, it currently does not 89 | have the capability to send battery levels. 90 | - Minor fixes. 91 | 92 | # 2ping 3.2.1 (2016-03-26) 93 | 94 | - Do not error out when non-ASCII notice text is received (only causes a 95 | remote denial of service crash when --debug is specified on the remote 96 | peer). 97 | 98 | # 2ping 3.2.0 (2016-02-10) 99 | 100 | - Added --nagios, for Nagios-compatible output and status codes. 101 | - Added unit tests. 102 | - Added --send-time, which sends an extended segment containing the 103 | current wall time. 104 | - Added --send-monotonic-clock, which sends an extended segment 105 | containing a monotonically-incrementing counter, on supported 106 | platforms. 107 | - Added --send-random, which sends an extended segment containing random 108 | bytes. 109 | - Added -fuzz, which randomly fuzzes incoming packets (developer 110 | feature). 111 | - Fixed over-cautious handling of length limits when assembling extended 112 | segments. 113 | 114 | # 2ping 3.1.0 (2015-11-16) 115 | 116 | - Best available poller for each platform (e.g. epoll on Linux, kqueue 117 | on BSD / OS X) is automatically used. 118 | - Old age timeout is set to a lower value on Win32 (1 second instead of 119 | 60), as KeyboardInterrupt does not interrupt select() on Win32. 120 | - Packet loss is now better visible in flood mode. 121 | - Adaptive mode now ramps up to EWMA faster. 122 | - Adaptive mode RTT predictions are now calculated per destination. 123 | - In client mode, statistics are now separated for each destination. 124 | - Added optional DNS SRV client support (requires dnspython). When 125 | given --srv, all SRV records for the 2ping UDP service of a host are 126 | pinged in parallel. 127 | - Investigation results are now sorted by sequence number. 128 | - Hostnames are displayed in statistics, if known. 129 | - 2ping will exit earlier if safe to do so (e.g. "-c 1" will not wait a 130 | full second if the ping is received immediately). 131 | - --port can now be given service names (as determined by the system 132 | resolver) instead of numeric ports. 133 | - System platform (Linux, Mach, etc) is sent in packets along with 2ping 134 | version. 135 | - Statistics use a more human-readable format (m, s, ms, etc). 136 | 137 | # 2ping 3.0.1 (2015-10-29) 138 | 139 | - Fix peer_address on error when MSG_ERRQUEUE is not set 140 | - Documentation update 141 | 142 | # 2ping 3.0.0 (2015-10-25) 143 | 144 | - Total rewrite from Perl to Python. 145 | - Multiple hostnames/addresses may be specified in client mode, and will 146 | be pinged in parallel. 147 | - Improved IPv6 support: 148 | - In most cases, specifying -4 or -6 is unnecessary. You should be 149 | able to specify IPv4 and/or IPv6 addresses and it will "just 150 | work". 151 | - IPv6 addresses may be specified without needing to add -6. 152 | - If a hostname is given in client mode and the hostname provides 153 | both AAAA and A records, the AAAA record will be chosen. This can 154 | be forced to one or another with -4 or -6. 155 | - If a hostname is given in listener mode with -I, it will be 156 | resolved to addresses to bind as. If the hostname provides both 157 | AAAA and A records, they will both be bound. Again, -4 or -6 can 158 | be used to restrict the bind. 159 | - IPv6 scope IDs (e.g. fe80::213:3bff:fe0e:8c08%eth0) may be used as 160 | bind addresses or destinations. 161 | - Better Windows compatibility. 162 | - ping(8)-compatible superuser restrictions (e.g. flood ping) have been 163 | removed, as 2ping is a scripted program using unprivileged sockets, 164 | and restrictions would be trivial to bypass. Also, the concept of a 165 | "superuser" is rather muddied these days. 166 | - Better timing support, preferring high-resolution monotonic clocks 167 | whenever possible instead of gettimeofday(). On Windows and OS X, 168 | monotonic clocks should always be available. On other Unix platforms, 169 | monotonic clocks should be available when using Python 2.7 170 | - Long option names for ping(8)-compatible options (e.g. adaptive mode 171 | can be called as --adaptive in addition to -A). See 2ping --help for a 172 | full option list. 173 | 174 | # 2ping 2.1.1 (2014-04-15) 175 | 176 | - Switch to Switch to ExtUtils::MakeMaker build system 177 | 178 | # 2ping 2.0 (2012-04-22) 179 | 180 | - Updated to support 2ping protocol 2.0 181 | - Protocol 1.0 and 2.0 are backwards and forwards compatible with 182 | each other 183 | - Added support for extended segments 184 | - Added extended segment support for program version and notice text 185 | - Changed default minimum packet size from 64 to 128 bytes 186 | - Added peer reply packet size matching support, turned on by default 187 | - Added extra error output for socket errors (such as hostname not 188 | found) 189 | - Added extra version support for downstream distributions 190 | - Removed generation of 2ping6 symlinks at "make all" time (symlinks are 191 | still generated during "make install" in the destination tree 192 | 193 | # 2ping 1.2.3 (2012-01-01) 194 | 195 | - Fixed ewma report (was always showing the last rtt) 196 | - Fixed the various brown paper bag stuff I did in 1.2.1 and 1.2.2 while 197 | I rediscovered the magical journey that is git 198 | 199 | # 2ping 1.2 (2011-12-24) 200 | 201 | - Added exponentially-weighted moving average (ewma) and moving standard 202 | drviation (mdev) statistics to the summary display 203 | 204 | # 2ping 1.1 (2011-04-05) 205 | 206 | - Host processing delays sent by the peer are no longer considered when 207 | calculating RTT 208 | - Changed ID expiration (for which no courtesty was received) time from 209 | 10 minutes to 2 minutes 210 | - Manpage fix: correct UDP port number listed 211 | - Added an RPM spec file 212 | 213 | # 2ping 1.0 (2010-10-20) 214 | 215 | - Protocol now "finished", 2ping is now "stable"! 216 | - Removed the sample initscript 217 | - Small Makefile and documentation changes 218 | 219 | # 2ping 0.9.1 (2010-10-09) 220 | 221 | - Version bumped to 0.9.1 to signify a stable standardization is close 222 | - Changed the default UDP port from 58277 to 15998 (IANA-registered 223 | port) 224 | - Host processing latency is now subtracted where possible (protocol 225 | extension, backwards compatible) 226 | - Minor code cleanup 227 | - 0.9.0 (unreleased) was a Brown Paper Bag commit; typo in ChangeLog 228 | fixed 229 | 230 | # 2ping 0.0.3 (2010-10-03) 231 | 232 | - Large cleanup and documentation push -- code is now "acceptable" 233 | - Fixed calculation of opcode data area lengths on some opcodes; 234 | implementation now incompatible with 0.0.2 235 | - Added more checks against malformed packets; 2ping no longer produces 236 | produces Perl warnings when fuzzing 237 | - Added a preload (-l) option, mimicking ping's -l functionality 238 | - Added a 2ping6 symlink; 2ping will now assume -6 if called as 2ping6 239 | - Added a message authentication code (MAC) option with a pre-shared key 240 | (--auth=key), allowing for message authentication and verification 241 | while in transit 242 | - Added a timed interval of brief statistics output (--stats=int) 243 | - STDOUT buffering is disabled in all modes now 244 | - Added compatibility down to Perl 5.6.0 245 | - Cleaned up distribution tarball, added a Makefile 246 | - Changed man section from 1 to 8 247 | 248 | # 2ping 0.0.2 (2010-09-07) 249 | 250 | - Fixed potential endianness issues 251 | - Added packet checksum field, in a fixed position near the beginning of 252 | the packet (PROTOCOL NOW INCOMPATIBLE WITH 0.0.1 RELEASE) 253 | - Added state table cleanup notification between peers, which will keep 254 | memory usage down in longer flood ping situations (protocol opcode 255 | added) 256 | - Added support for multiple binds in listen mode (specify -I IP 257 | multiple times) 258 | - Added support for multiple peers in client mode (specify multiple IP 259 | arguments) 260 | - Added additional packet error checks 261 | - Misc code cleanup and documentation (not yet to my satisfaction, but 262 | it's a start) 263 | 264 | # 2ping 0.0.1 (2010-08-29) 265 | 266 | - Initial release 267 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include COPYING.md 2 | include ChangeLog.md 3 | include Makefile 4 | include *.md 5 | include doc/Makefile 6 | include doc/*.1 7 | include doc/*.md 8 | include doc/*.py 9 | include 2ping 10 | include 2ping.service 11 | include 2ping.spec 12 | include 2ping6 13 | include 2ping.bash_completion 14 | include tests/*.py 15 | include wireshark/*.pcap 16 | include wireshark/*.lua 17 | include requirements.txt 18 | include tox.ini 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON := python3 2 | 3 | all: build 4 | 5 | build: 6 | $(PYTHON) setup.py build 7 | 8 | lint: 9 | $(PYTHON) -mtox -e py-flake8 10 | 11 | test: 12 | $(PYTHON) -mtox 13 | 14 | test-quick: 15 | $(PYTHON) -mtox -e py-black,py-flake8,py-pytest-quick 16 | 17 | black-check: 18 | $(PYTHON) -mtox -e py-black 19 | 20 | black: 21 | $(PYTHON) -mtox -e py-black-reformat 22 | 23 | install: build 24 | $(PYTHON) setup.py install 25 | 26 | clean: 27 | $(PYTHON) setup.py clean 28 | $(RM) -r build MANIFEST 29 | 30 | doc: 31 | $(MAKE) -C doc 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2ping - A bi-directional ping utility 2 | 3 | ![ci](https://github.com/rfinnie/2ping/workflows/ci/badge.svg) 4 | 5 | https://www.finnie.org/software/2ping/ 6 | 7 | ## About 8 | 9 | 2ping is a bi-directional ping utility. 10 | It uses 3-way pings (akin to TCP SYN, SYN/ACK, ACK) and after-the-fact state comparison between a 2ping listener and a 2ping client to determine which direction packet loss occurs. 11 | 12 | ## Installation 13 | 14 | 2ping requires Python 3 version 3.6 or higher. 15 | 16 | To install: 17 | 18 | sudo python3 setup.py install 19 | 20 | Python 3 stdlib is the only requirement for base functionality, but 2ping can utilize the following modules if available: 21 | 22 | * [distro](https://pypi.org/project/distro/) for system distribution detection 23 | * [dnspython](https://pypi.org/project/dnspython/) for --srv 24 | * [netifaces](https://pypi.org/project/netifaces/) for listening on all addresses in --listen mode 25 | * [pycryptodomex](https://pypi.org/project/pycryptodomex/) (recommended) or [pycryptodome](https://pypi.org/project/pycryptodome/) or [pycrypto](https://pypi.org/project/pycrypto/) for --encrypt 26 | * [systemd](https://pypi.org/project/systemd/) for using systemd-supplied sockets 27 | 28 | ## Usage 29 | 30 | Please see the 2ping manpage for invocation options, but in short, start a listener on the far end: 31 | 32 | 2ping --listen 33 | 34 | And run 2ping on the near end, connecting to the far end listener: 35 | 36 | 2ping $LISTENER 37 | 38 | Where "$LISTENER" is the name or IP address of the listener. 39 | 40 | ## License 41 | 42 | 2ping - A bi-directional ping utility 43 | 44 | Copyright (C) 2010-2021 [Ryan Finnie](https://www.finnie.org/) 45 | 46 | This Source Code Form is subject to the terms of the Mozilla Public 47 | License, v. 2.0. If a copy of the MPL was not distributed with this 48 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 49 | -------------------------------------------------------------------------------- /doc/2ping-protocol-examples.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # 2ping - A bi-directional ping utility 4 | # Copyright (C) 2010-2021 Ryan Finnie 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | from twoping import packets 8 | 9 | 10 | def h(input): 11 | return " ".join("{:02x}".format(x) for x in input) 12 | 13 | 14 | print("### Example 1") 15 | print() 16 | packet = packets.Packet() 17 | packet.message_id = b"\x00\x00\x00\x00\xa0\x01" 18 | print(" CLIENT: {}".format(h(packet.dump()))) 19 | print() 20 | 21 | print("### Example 2") 22 | print() 23 | packet = packets.Packet() 24 | packet.message_id = b"\x00\x00\x00\x00\xa0\x01" 25 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 26 | print(" CLIENT: {}".format(h(packet.dump()))) 27 | packet = packets.Packet() 28 | packet.message_id = b"\x00\x00\x00\x00\xb0\x01" 29 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 30 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = b"\x00\x00\x00\x00\xa0\x01" 31 | print(" SERVER: {}".format(h(packet.dump()))) 32 | print() 33 | 34 | print("### Example 3") 35 | print() 36 | packet = packets.Packet() 37 | packet.message_id = b"\x00\x00\x00\x00\xa0\x01" 38 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 39 | print(" CLIENT: {}".format(h(packet.dump()))) 40 | packet = packets.Packet() 41 | packet.message_id = b"\x00\x00\x00\x00\xb0\x01" 42 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 43 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 44 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = b"\x00\x00\x00\x00\xa0\x01" 45 | print(" SERVER: {}".format(h(packet.dump()))) 46 | packet = packets.Packet() 47 | packet.message_id = b"\x00\x00\x00\x00\xa0\x02" 48 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 49 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = b"\x00\x00\x00\x00\xb0\x01" 50 | packet.opcodes[packets.OpcodeRTTEnclosed.id] = packets.OpcodeRTTEnclosed() 51 | packet.opcodes[packets.OpcodeRTTEnclosed.id].rtt_us = 12345 52 | print(" CLIENT: {}".format(h(packet.dump()))) 53 | print() 54 | 55 | print("### Example 4") 56 | print() 57 | packet = packets.Packet() 58 | packet.message_id = b"\x00\x00\x00\x00\xa0\x01" 59 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 60 | print(" CLIENT: {}".format(h(packet.dump()))) 61 | packet = packets.Packet() 62 | packet.message_id = b"\x00\x00\x00\x00\xa0\x02" 63 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 64 | packet.opcodes[packets.OpcodeInvestigate.id] = packets.OpcodeInvestigate() 65 | packet.opcodes[packets.OpcodeInvestigate.id].message_ids.append( 66 | b"\x00\x00\x00\x00\xa0\x01" 67 | ) 68 | print(" CLIENT: {}".format(h(packet.dump()))) 69 | packet = packets.Packet() 70 | packet.message_id = b"\x00\x00\x00\x00\xb0\x02" 71 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 72 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 73 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = b"\x00\x00\x00\x00\xa0\x02" 74 | packet.opcodes[packets.OpcodeInvestigationSeen.id] = packets.OpcodeInvestigationSeen() 75 | packet.opcodes[packets.OpcodeInvestigationSeen.id].message_ids.append( 76 | b"\x00\x00\x00\x00\xa0\x01" 77 | ) 78 | print(" SERVER: {}".format(h(packet.dump()))) 79 | packet = packets.Packet() 80 | packet.message_id = b"\x00\x00\x00\x00\xa0\x03" 81 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 82 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = b"\x00\x00\x00\x00\xb0\x02" 83 | packet.opcodes[packets.OpcodeRTTEnclosed.id] = packets.OpcodeRTTEnclosed() 84 | packet.opcodes[packets.OpcodeRTTEnclosed.id].rtt_us = 12345 85 | print(" CLIENT: {}".format(h(packet.dump()))) 86 | print() 87 | 88 | print("### Example 5") 89 | print() 90 | packet = packets.Packet() 91 | packet.message_id = b"\x00\x00\x00\x00\xa0\x01" 92 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 93 | print(" CLIENT: {}".format(h(packet.dump()))) 94 | packet = packets.Packet() 95 | packet.message_id = b"\x00\x00\x00\x00\xa0\x02" 96 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 97 | packet.opcodes[packets.OpcodeInvestigate.id] = packets.OpcodeInvestigate() 98 | packet.opcodes[packets.OpcodeInvestigate.id].message_ids.append( 99 | b"\x00\x00\x00\x00\xa0\x01" 100 | ) 101 | print(" CLIENT: {}".format(h(packet.dump()))) 102 | packet = packets.Packet() 103 | packet.message_id = b"\x00\x00\x00\x00\xb0\x01" 104 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 105 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 106 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = b"\x00\x00\x00\x00\xa0\x02" 107 | packet.opcodes[packets.OpcodeInvestigationUnseen.id] = ( 108 | packets.OpcodeInvestigationUnseen() 109 | ) 110 | packet.opcodes[packets.OpcodeInvestigationUnseen.id].message_ids.append( 111 | b"\x00\x00\x00\x00\xa0\x01" 112 | ) 113 | print(" SERVER: {}".format(h(packet.dump()))) 114 | packet = packets.Packet() 115 | packet.message_id = b"\x00\x00\x00\x00\xa0\x03" 116 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 117 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = b"\x00\x00\x00\x00\xb0\x01" 118 | packet.opcodes[packets.OpcodeRTTEnclosed.id] = packets.OpcodeRTTEnclosed() 119 | packet.opcodes[packets.OpcodeRTTEnclosed.id].rtt_us = 12345 120 | print(" CLIENT: {}".format(h(packet.dump()))) 121 | print() 122 | 123 | print("### Example 6") 124 | print() 125 | packet = packets.Packet() 126 | packet.message_id = b"\x00\x00\x00\x00\xa0\x01" 127 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 128 | print(" CLIENT: {}".format(h(packet.dump()))) 129 | packet = packets.Packet() 130 | packet.message_id = b"\x00\x00\x00\x00\xa0\x02" 131 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 132 | print(" CLIENT: {}".format(h(packet.dump()))) 133 | packet = packets.Packet() 134 | packet.message_id = b"\x00\x00\x00\x00\xa0\x03" 135 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 136 | print(" CLIENT: {}".format(h(packet.dump()))) 137 | packet = packets.Packet() 138 | packet.message_id = b"\x00\x00\x00\x00\xb0\x02" 139 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 140 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 141 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = b"\x00\x00\x00\x00\xa0\x03" 142 | print(" SERVER: {}".format(h(packet.dump()))) 143 | packet = packets.Packet() 144 | packet.message_id = b"\x00\x00\x00\x00\xa0\x04" 145 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 146 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = b"\x00\x00\x00\x00\xb0\x02" 147 | packet.opcodes[packets.OpcodeRTTEnclosed.id] = packets.OpcodeRTTEnclosed() 148 | packet.opcodes[packets.OpcodeRTTEnclosed.id].rtt_us = 12823 149 | print(" CLIENT: {}".format(h(packet.dump()))) 150 | print(" ... etc") 151 | packet = packets.Packet() 152 | packet.message_id = b"\x00\x00\x00\x00\xa0\x0a" 153 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 154 | packet.opcodes[packets.OpcodeInvestigate.id] = packets.OpcodeInvestigate() 155 | packet.opcodes[packets.OpcodeInvestigate.id].message_ids.append( 156 | b"\x00\x00\x00\x00\xa0\x01" 157 | ) 158 | packet.opcodes[packets.OpcodeInvestigate.id].message_ids.append( 159 | b"\x00\x00\x00\x00\xa0\x02" 160 | ) 161 | print(" CLIENT: {}".format(h(packet.dump()))) 162 | packet = packets.Packet() 163 | packet.message_id = b"\x00\x00\x00\x00\xb0\x06" 164 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 165 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 166 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = b"\x00\x00\x00\x00\xa0\x0a" 167 | packet.opcodes[packets.OpcodeInvestigationSeen.id] = packets.OpcodeInvestigationSeen() 168 | packet.opcodes[packets.OpcodeInvestigationSeen.id].message_ids.append( 169 | b"\x00\x00\x00\x00\xa0\x01" 170 | ) 171 | packet.opcodes[packets.OpcodeInvestigationUnseen.id] = ( 172 | packets.OpcodeInvestigationUnseen() 173 | ) 174 | packet.opcodes[packets.OpcodeInvestigationUnseen.id].message_ids.append( 175 | b"\x00\x00\x00\x00\xa0\x02" 176 | ) 177 | packet.opcodes[packets.OpcodeInvestigate.id] = packets.OpcodeInvestigate() 178 | packet.opcodes[packets.OpcodeInvestigate.id].message_ids.append( 179 | b"\x00\x00\x00\x00\xb0\x02" 180 | ) 181 | print(" SERVER: {}".format(h(packet.dump()))) 182 | packet = packets.Packet() 183 | packet.message_id = b"\x00\x00\x00\x00\xa0\x0b" 184 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 185 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = b"\x00\x00\x00\x00\xb0\x06" 186 | packet.opcodes[packets.OpcodeRTTEnclosed.id] = packets.OpcodeRTTEnclosed() 187 | packet.opcodes[packets.OpcodeRTTEnclosed.id].rtt_us = 13112 188 | packet.opcodes[packets.OpcodeInvestigationSeen.id] = packets.OpcodeInvestigationSeen() 189 | packet.opcodes[packets.OpcodeInvestigationSeen.id].message_ids.append( 190 | b"\x00\x00\x00\x00\xb0\x02" 191 | ) 192 | print(" CLIENT: {}".format(h(packet.dump()))) 193 | print() 194 | -------------------------------------------------------------------------------- /doc/2ping-protocol.md: -------------------------------------------------------------------------------- 1 | # 2ping protocol 2 | 3 | * Line protocol version: 4.1 4 | * Document version: 20181225 5 | 6 | ## Introduction 7 | 8 | The 2ping protocol is a bi-directional ping system. 9 | This allows for several features, such as 3-way ping (akin to SYN, SYN/ACK, ACK), and the ability to determine whether packet loss was inbound or outbound. 10 | This is accomplished by the client and server each keeping state tables of needed incoming and outgoing requests. 11 | If packet loss occurs without a complete network failure, the client and server can eventually resume communication and compare notes about which sides received what requests. 12 | 13 | ## Network transit 14 | 15 | 2ping operates over UDP, and is compatible with IPv4, IPv6 and possibly future Layer 3 protocols. 16 | The server/listener listens on port 15998. 17 | The client can use any high source port, and will send packets to the server on destination port 15998. 18 | The server will then respond to the client on the port the client originally used as a source port. 19 | The UDP message payload is currently completely binary data, and is designed with consistent and/or minimum space, consistent parsing, and forwards compatibility in mind. 20 | The protocol payload does not contain any IP-specific information, and is NAT/PAT safe. 21 | 22 | ## Message format 23 | 24 | The 2ping format is a variable length binary format, with the length determined by which opcodes are set. 25 | Opcode data lengths are in a standardized location, which allows for forward compatibility. 26 | A program utilizing the 2ping protocol can still parse a complete payload, even if the program does not understand a certain opcode. 27 | 28 | All data is in network byte order (big endian), with one byte equal to 8 bits (1 octet). 29 | Bytes are most significant bit first, and multiple-byte sequences (flags and integers capable of being larger than 8 bits) are most significant byte first. 30 | 31 | | Field | Length | 32 | | ----- | ------ | 33 | | Magic number 0x3250 | 2 bytes, required | 34 | | Checksum | 2 bytes, field required, checksum optional | 35 | | Message ID | 6 bytes, required | 36 | | Opcode flags | 2 bytes, required | 37 | | Opcode data | Variable, zero or more bytes, length depends on opcode flags | 38 | | Padding | Variable, optional | 39 | 40 | Padding may be added to a packet to bring it up to a desired minimum size. 41 | A 128 byte minimum packet size is recommended. 42 | Padding bytes should be null character bytes by default, but the contents of the padding is not important, as the packet parser should not use the contents of the padding for program operation. 43 | 44 | The minimum valid length of a 2ping payload is 12 bytes (2-byte magic number + 2-byte checksum field + 6-byte message ID + 2-byte opcode flags with no flags enabled + zero padding). 45 | 46 | ## Message IDs 47 | 48 | Message IDs are 48 bits (6 bytes) of data to identify a specific message. 49 | The message ID must be 48 bits of pseudo-random data; do not use incrementing numbers or a discernible pattern. 50 | Likewise, do not attempt to analyze or devise logic from the message IDs that a peer sends. 51 | 52 | Message IDs are unique within a socket's 5-tuple (local address, local port, peer address, peer port, protocol). 53 | For example, when implementing a listener, care should be taken to differentiate between message ID 0xF1E2D3C4B5A6 sent from peer 10.0.0.10, and message ID 0xF1E2D3C4B5A6 sent from peer 172.16.0.10, or even between the same message ID sent from the same host on different source ports. 54 | While the specification's random ID requirement and keyspace size makes the chance of a collision from two different peers unlikely, it is still possible. 55 | 56 | It is understandable that a 48-bit random unique ID is not easy to read from an end user's perspective. 57 | The implementation may therefore wish to keep a local mapping between message IDs and a more human-friendly identifier, such as an incrementing integer. 58 | 59 | ## Checksum 60 | 61 | The checksum allows for verification of packet integrity from unintentional transit errors. 62 | (If cryptographic verification is desired to prevent intentional tampering in transit, see opcode 0x0080 below.) 63 | While UDP includes its own checksum, it is optional in IPv4 and cannot be relied upon. 64 | (IPv6 makes the UDP checksum mandatory.) 65 | 66 | The 2ping protocol uses the following checksum method, as described in pseudocode: 67 | 68 | checksum = 0 69 | 70 | if length(input) is not even: 71 | input.append(0) 72 | 73 | for (a, b) in input as ((int1, int2), (int3, int4), ...): 74 | checksum = checksum + (a << 8) + b 75 | checksum = ((checksum & 0xffff) + (checksum >> 16)) 76 | 77 | checksum = ~checksum & 0xffff 78 | 79 | if checksum == 0: 80 | checksum = 0xffff 81 | 82 | The input data is defined as the entire packet data payload, from the magic number to the end of the padding. 83 | The checksum field itself is zeroed for the purpose of the method input, and is replaced with the result of the method. 84 | An all zero transmitted checksum value means that the transmitter generated no checksum. 85 | 86 | The checksum field is required, though an implementation is not required to perform or verify checksums. 87 | However, if verification of checksums are performed, the verifier must be able to recognize the zero transmitted checksum value from a peer that did not compute a checksum. 88 | 89 | Checksum computation and verification attempts are strongly recommend for IPv4 packets, but should not be necessary for IPv6 packets, as verification is already performed at the UDPv6 level, and malformed packets should be discarded by the time they reach the userland. 90 | 91 | This specification originally specified a different checksum method (RFC 768's UDP checksum method). 92 | However, it was discovered that the only known 2ping implementation which computed checksums at the time was using an incorrect method, so the specification was changed to match the implementation to preserve compatibility. 93 | 94 | ## Opcodes 95 | 96 | Opcodes' data in the opcode data area is stored in sequence according to which flags are set in the opcode flags field, starting with the least significant bit. 97 | Thus if no opcode flags are set, the opcode data area does not exist. 98 | 99 | For example, if 0x0001 and 0x0020 flags are set, the entire opcode data area consists of the opcode header and segment data for 0x0001, followed by the opcode header and segment data for 0x0020. 100 | 101 | 2 bytes of opcode flags allow for up to 16 opcodes. 102 | The final flag (0x8000) is a container for an extended segment format, and allows for an arbitrary number of segments. 103 | 104 | Each opcode data segment is comprised of the following: 105 | 106 | | Field | Length | 107 | | ----- | ------ | 108 | | Segment data length (not including length header itself) | 2 bytes, required | 109 | | Segment data | Variable, zero or more bytes, determined by segment data length header above | 110 | 111 | An opcode data segment may be between zero and 65,535 bytes long, plus 2 bytes for the segment header. 112 | 113 | This layout is designed for forward compatibility with future protocol revisions. 114 | A 2ping implementation can simply stop processing once it reaches the last opcode flag it understands, or it can at least parse the opcodes it does not understand, as the opcode data segment header includes the length of the segment data -- using this information, the implementation can simply skip over the opcodes it does not understand. 115 | 116 | If there is a numeric gap in the understood opcodes of a 2ping implementation, it must at least check for the opcode and parse the opcode data segment in order to skip over it. 117 | For example, a 2ping implementation might understand 0x0001, 0x0002 and 0x0008, but not 0x0004, 0x0010 or 0x0020. 118 | To be able to parse the data segment for 0x0008, it must still check whether 0x0004 is set and seek to its segment data length header, determine the length of the segment data for 0x0004, and skip over it to begin parsing 0x0008. 119 | Due to the standardized length headers, this is possible without knowing what 0x0004 does. 120 | Once it is finished parsing 0x0008, it may stop checking opcode flags altogether. 121 | 122 | The minimum 2ping implementation must be able to understand and follow opcodes 0x0001 (reply requested) and 0x0002 (in reply to). 123 | Implementation of all other opcodes are optional (but highly recommended, as investigation opcodes are the core focus of 2ping). 124 | 125 | ### 0x0001 - Reply requested 126 | 127 | | Field | Length | 128 | | ----- | ------ | 129 | | No segment data | 0 bytes | 130 | 131 | The sending end requests a reply from the receiving end. 132 | If the receiving end does not send a reply back to the sending end, the sending end will consider it a lost packet. 133 | Note: No segment data is part of this opcode, but as all used opcode segments must have a header, there will still be a 2-byte length header, signifying zero bytes of data. 134 | 135 | ### 0x0002 - In reply to 136 | 137 | | Field | Length | 138 | | ----- | ------ | 139 | | Replied message ID | 6 bytes, required | 140 | 141 | This opcode signifies the reply to a packet that requested a reply. 142 | The original sender's message ID is enclosed. 143 | 144 | ### 0x0004 - RTT enclosed 145 | 146 | | Field | Length | 147 | | ----- | ------ | 148 | | RTT in microseconds | 4 bytes, required | 149 | 150 | If this packet is a reply packet and an RTT is enclosed, the packet being replied to was a successful ping, and the RTT is the round trip time of the previous operation, in microseconds. 151 | Up to 2^32-1 microseconds are possible, approximately 71 minutes. 152 | In a typical 3-way ping scenario, this is only sent in the third message in the sequence, from the client to the server. 153 | Thus the server will know both the server->client->server ping RTT, as well as the original client->server->client ping RTT. 154 | 155 | ### 0x0008 - Investigation complete, originally replied to as requested 156 | 157 | | Field | Length | 158 | | ----- | ------ | 159 | | Number of message IDs enclosed | 2 bytes, required | 160 | | Message ID | 6 bytes, optional | 161 | | Message ID... | 6 bytes..., optional | 162 | 163 | This is a response to "0x0020 - Expected reply never received, please investigate", please continue reading below. 164 | The message IDs listed in this opcode are packets that the responder knows about, and had responded to. 165 | Since the requester never received the response, the requester can assume that the packet loss occurred inbound (relative to the requester). 166 | 167 | In theory, the number of message IDs enclosed may be between zero and 65,535, but limits on the total byte length of the opcode data area will make this impossible to achieve. 168 | In addition, UDP, IP and Ethernet length restrictions will further limit the number of message IDs that can be enclosed. 169 | The number of message IDs enclosed may legally be zero, but if that is the case, it's better to just not set the 0x0008 opcode flag. 170 | 171 | Investigation replies must not be unsolicited, and may only be sent in response to messages that requested a reply, and requested an investigation into specific message IDs. 172 | However, investigation responses by the requestee are not required, and can be ignored if packet payload space is limited or other problems arise. 173 | It is the responsibility of the requester to resend investigation requests if the requestee does not respond to them in its reply. 174 | 175 | ### 0x0010 - Investigation complete, request never received 176 | 177 | | Field | Length | 178 | | ----- | ------ | 179 | | Number of message IDs enclosed | 2 bytes, required | 180 | | Message ID | 6 bytes, optional | 181 | | Message ID... | 6 bytes..., optional | 182 | 183 | This is a response to "0x0020 - Expected reply never received, please investigate", please continue reading below. 184 | The message IDs listed in this opcode are packets that the responder does not know about. 185 | Since the responder never received the request, the requester can assume that the packet loss occurred outbound (relative to the requester). 186 | 187 | In theory, the number of message IDs enclosed may be between zero and 65,535, but limits on the total byte length of the opcode data area will make this impossible to achieve. 188 | In addition, UDP, IP and Ethernet length restrictions will further limit the number of message IDs that can be enclosed. 189 | The number of message IDs enclosed may legally be zero, but if that is the case, it's better to just not set the 0x0010 opcode flag. 190 | 191 | Investigation replies must not be unsolicited, and may only be sent in response to messages that requested a reply, and requested an investigation into specific message IDs. 192 | However, investigation responses by the requestee are not required, and can be ignored if packet payload space is limited or other problems arise. 193 | It is the responsibility of the requester to resend investigation requests if the requestee does not respond to them in its reply. 194 | 195 | ### 0x0020 - Expected reply never received, please investigate 196 | 197 | | Field | Length | 198 | | ----- | ------ | 199 | | Number of message IDs enclosed | 2 bytes, required | 200 | | Message ID | 6 bytes, optional | 201 | | Message ID... | 6 bytes..., optional | 202 | 203 | If the requester sends a packet to a remote party and indicates a reply is requested (0x0001), and a reply is never received, the requester can use this opcode to inquire about what happened to it. 204 | If replies to this inquiry come back in either "0x0008 - Investigation complete, originally replied to as requested" or "0x0010 - Investigation complete, request never received" opcodes (as explained above), the requester can use this information to determine whether the packet loss occurred inbound or outbound relative to the requester. 205 | 206 | In theory, the number of message IDs enclosed may be between zero and 65,535, but limits on the total byte length of the opcode data area will make this impossible to achieve. 207 | In addition, UDP, IP and Ethernet length restrictions will further limit the number of message IDs that can be enclosed. 208 | The number of message IDs enclosed may legally be zero, but if that is the case, it's better to just not set the 0x0020 opcode flag. 209 | 210 | Care should be taken not to begin inquiring too quickly. 211 | UDP packets may arrive delayed or out of order, so 10 seconds should be a good amount of time to wait before inquiring. 212 | 213 | The requester may send inquiries multiple times. 214 | The requestee is not required to respond to a specific inquiry immediately, as UDP payload space may be limited. 215 | Thus it is the responsibility of the requester to continue to send inquiries until it receives a response, so the requestee does not need to keep a state table of unreplied inquiries. 216 | 217 | ### 0x0040 - Courtesy message ID expiration 218 | 219 | | Field | Length | 220 | | ----- | ------ | 221 | | Number of message IDs enclosed | 2 bytes, required | 222 | | Message ID | 6 bytes, optional | 223 | | Message ID... | 6 bytes..., optional | 224 | 225 | To facilitate bi-directional packet loss detection, it is necessary for each peer to maintain a set of state tables: messages expecting a reply and not yet received, and remote peer messages expecting a reply that the near end has replied to. 226 | Maintenance of the first cache is easy; simply remove a message ID once a reply or investigation has been received. 227 | However, the second case is trickier. 228 | In some cases, it may be possible to know if the far end has received an expected response and will not inquire about it. 229 | But in others, there may not be enough hints to be able to guarantee a peer will not ask for an investigation later. 230 | 231 | In particular, in a 3-way ping (assuming "Peer 1" initiated the ping), Peer 2 can figure out that the near end is satisfied with the response to the first leg, since Peer 1 sent the third leg to Peer 2, and so Peer 2 can remove the first leg's message ID from its cache. 232 | However, Peer 1 cannot tell if Peer 2 received the third leg (a response to the second leg), since a response to the third leg does not exist. 233 | But Peer 2 could in the future inquire about the response to the second leg sometime in the future. 234 | 235 | In this situation, it would be best to do a periodic cleanup of the state tables, but ideally the age of the message ID should be long enough (recommended default is 10 minutes), since it may take the peer awhile to request an investigation. 236 | In a session with one or two pings per second that's not a lot, but in a flood mode, thousands or millions of message IDs could get stored in the state table during that 10 minute period. 237 | 238 | To combat this, the courtesy opcode contains a list of message IDs that a peer is no longer concerned about, and can be removed from the remote peer's cache. 239 | Courtesy opcodes can be sent any time a packet is being sent from one peer to another (unlike investigation requests, which may only be sent when requesting a reply, or investigation results, which are in the immediate response to the investigation request). 240 | 241 | This opcode is optional (but recommended), and due to its opportunistic nature, it is not guaranteed the message will reach the remote peer. 242 | So it is still up to an implementation to do periodic maintenance on its caches, since it cannot rely on courtesy responses to prune its caches. 243 | 244 | Peers may send unsolicited messages with courtesy opcodes (a message not requesting a reply, not replying to another message, and containing the courtesy opcode), but in the normal course of operation, it should not be necessary. 245 | 246 | ### 0x0080 - Message authentication code (MAC) 247 | 248 | | Field | Length | 249 | | ----- | ------ | 250 | | Digest type index | 2 bytes, required | 251 | | Computed hash value | Length depends on digest type, required | 252 | 253 | The message authentication code field is allowed to provide cryptographic data integrity and authentication of the remote side. 254 | This helps to prevent injection and replay attacks during transit. 255 | 256 | The payload data, combined with a shared secret key, is hashed and is verified by the other peer. 257 | The digest types supported are: 258 | 259 | | Index | Digest Type | Computed hash size | 260 | | ----- | ----------- | ------------------ | 261 | | 0 | Private / locally reserved | Variable | 262 | | 1 | HMAC-MD5 | 16 bytes | 263 | | 2 | HMAC-SHA1 | 20 bytes | 264 | | 3 | HMAC-SHA256 | 32 bytes | 265 | | 4 | HMAC-CRC32 | 4 bytes | 266 | | 5 | HMAC-SHA512 | 64 bytes | 267 | 268 | The two peers must use the same key and digest type. 269 | If a peer is instructed to hash its messages, it must not accept replies that are not hashed with the same digest type and key. 270 | 271 | Index 0 may be used by clients for digest types not defined by this specification, and implementations must ignore index 0 hashes if a non-standard digest has not been defined locally. 272 | Additional digest types may be added to this specification in the future. 273 | 274 | The entire payload is hashed, from the magic number to the padding, inclusive. 275 | When computing the MAC hash, the hash value itself is filled with zeroed bytes, and replaced with the computed hash. 276 | The MAC hash is computed before the payload checksum, therefore the checksum area must also be zeroed before the MAC hash is computed. 277 | 278 | Use of this opcode is optional and its parameters (whether to use hashing, the digest type and the shared secret) must be coordinated between the two sides ahead of time. 279 | An implementation is not required to implement all digest types. 280 | 281 | HMAC-CRC32 is included in this specification mostly as a joke, and a programming exercise for the reader. 282 | Please don't use this (and expect any sort of cryptographic data integrity). 283 | Endianness is not specified by CRC32; use network byte order (big endian). 284 | The HMAC specification requires aligning the key to the digest function's block size; CRC32 does not use a block size, so assume a block size of 64 bytes (same as MD5, SHA1, SHA256 and SHA512). 285 | 286 | ### 0x0100 - Host processing latency 287 | 288 | | Field | Length | 289 | | ----- | ------ | 290 | | Delay in microseconds | 4 bytes, required | 291 | 292 | The time between receiving a message packet and sending a reply packet may be non-trivial, due to host processing. 293 | This opcode can be used by an implementation, when sending a reply (0x0002), to inform a peer how much time progressed between a receive and a send. 294 | The peer may then subtract this latency time to better calculate network transit time. 295 | Up to 2^32-1 microseconds are possible, approximately 71 minutes. 296 | 297 | The delta should be computed from immediately after the original message packet has been received, to as soon as possible before the reply packet has been sent. 298 | Note that due to optional MAC hash and checksum calculation processing, which must be computed after the rest of the opcode data area, there still could be some host processing latency not accounted for in the reported latency time. 299 | 300 | This opcode should only be used when combined with a monotonic clock, as using a wall clock may result in processing being greater than the RTT, less than zero, etc. 301 | 302 | ### 0x0200 - Encrypted packet 303 | 304 | | Field | Length | 305 | | ----- | ------ | 306 | | Method index | 2 bytes, required | 307 | | Encrypted data | Variable, required | 308 | 309 | This opcode acts as a container for a shared-secret encrypted 2ping packet. 310 | A complete valid 2ping packet, from magic number and checksum through optional padding, is encrypted using a known method and contained in this opcode in a stub 2ping packet. 311 | 312 | The stub packet, which is what is sent along the the wire, is also a valid 2ping packet, but contains only a single opcode, 0x0200. 313 | When the encrypted payload in the stub packet is decrypted on the receiving end, the decrypted packet is used in place of the stub packet and is parsed normally. 314 | The stub packet's message ID is not used and should be pseudo-random. 315 | Implementations must not re-use the encrypted packet's message ID for the unencrypted stub's message ID. 316 | 317 | When one end specifies it is using encryption, it must not accept replies from the other end if the message is not encrypted, the method index does not match, or the encrypted payload does not decrypt to a valid 2ping packet. 318 | 319 | As of this protocol revision, a single method is specified, HKDF-AES256-CBC (index 1). 320 | Implementations may use a locally-reserved method by specifying index 0, as long as both ends have implemented the method. 321 | 322 | #### Method index 1 - HKDF-AES256-CBC 323 | 324 | * Shared secret 325 | * HKDF (RFC 5869) key derivation function, extract + expand rounds 326 | * Input key material: shared secret (UTF-8) 327 | * Salt: 16 byte (128 bit) per-message pseudo-random value 328 | * Output key length: 32 bytes (256 bits) 329 | * Expand round info value: 0xd889ac93aceba1f398d0c69bc8c6a7aa + 8 byte (64 bit) per-session pseudo-random value (concatenated) 330 | * AES encryption 331 | * Cipher mode: AES-CBC 332 | * Key: 32 byte (256 bit) output of HKDF extract + expand rounds above 333 | * Initialization vector (IV): Same as HKDF salt above 334 | * Input: Complete unencrypted 2ping packet 335 | 336 | The session ID, IV/salt and the result of the AES encryption are concatenated to form the encrypted data field. 337 | To decrypt, extract the session ID from the first 8 bytes of the encrypted data field, the IV/salt from the following 16 bytes, and use the method to AES decrypt the remaining bytes. 338 | 339 | AES has a 128 bit block size, which data must be padded to when encrypting. 340 | As trailing padding is a core feature of 2ping, no special padding method is required. 341 | The implementation only needs to make sure the unencrypted packet length is a multiple of 16 bytes before encryption. 342 | 343 | ### 0x8000 - Extended segments 344 | 345 | This opcode data segment extends the 2ping format, allowing for a nearly unlimited number of available segments. 346 | Within the opcode segment data area is a series of sub-segments, referred to as extended segments, consisting of a 32-bit extID, a length of the extended segment data, and the extended segment data itself. 347 | Each part is built as so: 348 | 349 | | Field | Length | 350 | | ----- | ------ | 351 | | Extended segment ID | 4 bytes, required | 352 | | Extended segment data length | 2 bytes, required | 353 | | Extended segment data | Variable, zero or more bytes, determined by extended segment data length header above | 354 | 355 | Multiple extended segment parts are chained together to form the data area of a 0x8000 opcode. 356 | The opcode data area must include a maximum of one instance of any individual extended segment. 357 | 358 | Parsing this opcode's data area is similar to parsing the opcodes as a whole: read the extended segment ID and determine if it's a known ID. 359 | If so, parse its contents by reading the extended segment data length and extended segment data. 360 | If not, read the extended segment data length to determine how far to skip over the extended segment data. 361 | If no extended segments are to be included with the packet payload, do not set the opcode. 362 | 363 | ## Defined extended segments 364 | 365 | Extended segment IDs are 32 bits, allowing for approximately 4.2 billion different pieces of functionality. 366 | When defining a local extension, please choose a random ID to avoid the possibility of conflicting extensions. 367 | If the extension is useful to the public as a whole, please consider submitting it for inclusion in this specification. 368 | 369 | The following are registered extended segments. 370 | An implementation is not required to implement any extended segments (or the extended segment opcode itself, for that matter), but you are strongly encouraged to do so. 371 | 372 | ### 0x3250564e - Program version 373 | 374 | | Field | Length | 375 | | ----- | ------ | 376 | | Program version text (UTF-8) | Variable length | 377 | 378 | The human-readable text version of the program or firmware generating the packet, with optional information such as architecture, etc. 379 | An example could be "Network Tools 3.0-1distro2 (x86_64-linux)". 380 | 381 | As the 2ping protocol is designed to be backwards and forwards compatible, this field must not be used by an implementation to determine functionality. 382 | It is recommended that this field be sent with every packet, but received segments should not be displayed to the user unless in a verbose/debug/etc mode. 383 | 384 | This segment's number, 0x3250564e, evaluates to ASCII "2PVN" ("2ping version number"), and was chosen before the decision to recommend segment numbers be randomly assigned to avoid collisions with unregistered extended segments. 385 | All other extended segment numbers in this specification were randomly chosen. 386 | 387 | ### 0x2ff6ad68 - Random data 388 | 389 | | Field | Length | 390 | | ----- | ------ | 391 | | Flags | 2 bytes, required | 392 | | Random data | Variable length | 393 | 394 | Random data as generated by the host's random number generator. 395 | The following flags may be present: 396 | 397 | * 0x0001 - Whether the data was generated by a hardware random number generator. 398 | * 0x0002 - Whether the data was generated by the operating system's random number generator. 399 | 400 | If this segment is to be used for cryptographic or trusted purposes, it should be combined with "0x0080 - Message authentication code (MAC)" or "0x0200 - Encrypted packet" to ensure end-to-end integrity. 401 | 402 | ### 0x64f69319 - Wall clock 403 | 404 | | Field | Length | 405 | | ----- | ------ | 406 | | Time in microseconds | 8 bytes, required | 407 | 408 | Host time (wall clock) of the sender, in microseconds since 1970-01-01 00:00:00 UTC (Unix epoch). 409 | 410 | ### 0x771d8dfb - Monotonic clock 411 | 412 | | Field | Length | 413 | | ----- | ------ | 414 | | Generation ID | 2 bytes, required | 415 | | Time in microseconds | 8 bytes, required | 416 | 417 | Monotonic clock time, in microseconds since an epoch. 418 | The epoch is arbitrary; set the generation ID to a random value and the epoch to a random offset from the host's monotonic clock. 419 | The generation ID and epoch may occasionally be regenerated together (for example, every few hours or days), to avoid leaking information about wall clock or program start time. 420 | 421 | A peer may compare two successive values by making sure the generation IDs match and the later time is greater than the earlier time. 422 | 423 | This segment must only be sent if the host is capable of using a monotonic, high-precision clock. 424 | 425 | ### 0x88a1f7c7 - Battery levels 426 | 427 | | Field | Length | 428 | | ----- | ------ | 429 | | Number of batteries enclosed | 2 bytes, required | 430 | | Battery ID | 2 bytes, optional | 431 | | Battery level | 2 bytes, optional | 432 | | Battery ID... | 2 bytes..., optional | 433 | | Battery level... | 2 bytes..., optional | 434 | 435 | If the host is a device which includes batteries, this may be used to report their levels. 436 | Multiple batteries may be reported using different battery IDs. 437 | The level is indicated as a percentage between 0x0000 (completely empty) and 0xffff (completely full). 438 | 439 | The number of batteries enclosed may legally be zero, but if that is the case, it's better to just not include the 0x88a1f7c7 segment. 440 | 441 | ### 0xa837b44e - Notice text 442 | 443 | | Field | Length | 444 | | ----- | ------ | 445 | | Notice text (UTF-8) | Variable length | 446 | 447 | Arbitrary text to be sent with the packet. 448 | This text should be defined by the user by a program flag, UI option, etc. 449 | It is designed to be human-readable; do not use this segment to pass machine-parsed data between the client and listener. 450 | Instead, if extended data transfer is desired between the client and listener, simply define a new extended segment ID. 451 | 452 | The implementation may display this text to the user, but it is not guaranteed the user will see it. 453 | 454 | ## Procedures 455 | 456 | In the following pseudocode examples, message IDs are represented as zero-padded incrementing numbers. 457 | This is for the purpose of illustrating reply chains in these examples only. 458 | In real life, message IDs MUST be randomly-generated 48-bit (6 byte) identifiers. 459 | The program implementing the 2ping protocol may choose to locally associate a better identifier for the user (such as an incrementing integer), but the protocol message ID must be random and pseudo-unique. 460 | 461 | As mentioned above, the simplest 2ping packet is a 12-byte payload: 2 bytes for the magic number (always 0x3250), 2 bytes for the checksum field, 6 bytes for the message ID, and 2 blank opcode flag bytes: 462 | 463 | ### Example 1 464 | 465 | CLIENT: 00000000a001, no opcodes 466 | 467 | Of course, this isn't very useful, and would be analogous to a NOOP. 468 | The simplest ping would require the "reply requested" opcode: 469 | 470 | ### Example 2 471 | 472 | CLIENT: 00000000a001, reply requested 473 | SERVER: 00000000b001, in reply to 00000000a001 474 | 475 | A 3-way ping further extends this. 476 | 477 | ### Example 3 478 | 479 | CLIENT: 00000000a001, reply requested 480 | SERVER: 00000000b001, reply requested, in reply to 00000000a001 481 | CLIENT: 00000000a002, in reply to 00000000b001, successful ping rtt 12345 µs 482 | 483 | The client successfully received the response from the server and was able to measure an RTT of 12345 µs. 484 | The client then sends a reply back to the server, referencing the original reply, and letting it know the RTT it measured. 485 | The server is then able to determine its RTT between the second and third leg (say, 11804 µs), and also know the RTT between the first and second leg. 486 | 487 | Now let's say the reply to the original client ping never came back. 488 | The client can start inquiring about whether the server saw the original request, and the server can provide info at the same time it is responding to a new ping request: 489 | 490 | ### Example 4 491 | 492 | CLIENT: 00000000a001, reply requested 493 | CLIENT: 00000000a002, reply requested, did not receive reply to 00000000a001 494 | SERVER: 00000000b002, reply requested, in reply to 00000000a002, received and replied to 00000000a001 495 | CLIENT: 00000000a003, in reply to 00000000b002, successful ping rtt 12345 µs 496 | 497 | Now the client can tell that the server received the original request, and replied, which suggests inbound packet loss. 498 | Note from the example message IDs, the server most likely originally replied to 00000000a001 with 00000000b001. 499 | However, the specific message ID the server originally replied with is not important and not tracked, and hence is not indicated in the investigation reply, only that it was originally received and replied to. 500 | 501 | Conversely, for a request for reply that the server never actually received: 502 | 503 | ### Example 5 504 | 505 | CLIENT: 00000000a001, reply requested 506 | CLIENT: 00000000a002, reply requested, did not receive reply to 00000000a001 507 | SERVER: 00000000b001, reply requested, in reply to 00000000a002, never received 00000000a001 508 | CLIENT: 00000000a003, in reply to 00000000b001, successful ping rtt 12345 µs 509 | 510 | This suggests outbound packet loss. 511 | As mentioned before, the client should not immediately start asking about replies never received. 512 | An actual sequence of events may look something like this: 513 | 514 | ### Example 6 515 | 516 | CLIENT: 00000000a001, reply requested 517 | CLIENT: 00000000a002, reply requested 518 | CLIENT: 00000000a003, reply requested 519 | SERVER: 00000000b002, reply requested, in reply to 00000000a003 520 | CLIENT: 00000000a004, in reply to 00000000b002, successful ping rtt 12823 µs 521 | ... etc 522 | CLIENT: 00000000a00a, reply requested, did not receive reply to 00000000a001 or 00000000a002 523 | SERVER: 00000000b006, reply requested, in reply to 00000000a00a, received and replied to 00000000a001, never received 00000000a002, did not receive reply to 00000000b002 524 | CLIENT: 00000000a00b, in reply to 00000000b006, successful ping rtt 13112 µs, received and replied to 00000000b002 525 | 526 | The client waited a few seconds before asking about 00000000a001 and 00000000a002. 527 | The server replied that it knew about 00000000a001, but never received 00000000a002, indicating there is some packet loss both inbound and outbound. 528 | 529 | Also notice that in the same packet where the server replied with info about the client's lost packets inquiry, it also inquired about its own lost packet. 530 | In this case, the server never received the last segment of the 3-way ping, 00000000b002. 531 | The client then tells the server it did originally receive and respond to the packet in question. 532 | 533 | This raises an interesting point, that there is essentially no difference between a server and a client in 2ping. 534 | The "server" is expected to listen for the initial datagrams, and the "client" is expected to initiate ping requests at regular intervals. 535 | But if the "server" decides to randomly initiate a ping request of its own, the "client" is expected to respond appropriately, as a server would do. 536 | 537 | ## Reference packet dumps 538 | 539 | ### Example 1 540 | 541 | CLIENT: 32 50 2d ae 00 00 00 00 a0 01 00 00 542 | 543 | ### Example 2 544 | 545 | CLIENT: 32 50 2d ad 00 00 00 00 a0 01 00 01 00 00 546 | SERVER: 32 50 7d a4 00 00 00 00 b0 01 00 02 00 06 00 00 00 00 a0 01 547 | 548 | ### Example 3 549 | 550 | CLIENT: 32 50 2d ad 00 00 00 00 a0 01 00 01 00 00 551 | SERVER: 32 50 7d a3 00 00 00 00 b0 01 00 03 00 00 00 06 00 00 00 00 a0 01 552 | CLIENT: 32 50 4d 62 00 00 00 00 a0 02 00 06 00 06 00 00 00 00 b0 01 00 04 00 00 30 39 553 | 554 | ### Example 4 555 | 556 | CLIENT: 32 50 2d ad 00 00 00 00 a0 01 00 01 00 00 557 | CLIENT: 32 50 8d 81 00 00 00 00 a0 02 00 21 00 00 00 08 00 01 00 00 00 00 a0 01 558 | SERVER: 32 50 dd 8e 00 00 00 00 b0 02 00 0b 00 00 00 06 00 00 00 00 a0 02 00 08 00 01 00 00 00 00 a0 01 559 | CLIENT: 32 50 4d 60 00 00 00 00 a0 03 00 06 00 06 00 00 00 00 b0 02 00 04 00 00 30 39 560 | 561 | ### Example 5 562 | 563 | CLIENT: 32 50 2d ad 00 00 00 00 a0 01 00 01 00 00 564 | CLIENT: 32 50 8d 81 00 00 00 00 a0 02 00 21 00 00 00 08 00 01 00 00 00 00 a0 01 565 | SERVER: 32 50 dd 87 00 00 00 00 b0 01 00 13 00 00 00 06 00 00 00 00 a0 02 00 08 00 01 00 00 00 00 a0 01 566 | CLIENT: 32 50 4d 61 00 00 00 00 a0 03 00 06 00 06 00 00 00 00 b0 01 00 04 00 00 30 39 567 | 568 | ### Example 6 569 | 570 | CLIENT: 32 50 2d ad 00 00 00 00 a0 01 00 01 00 00 571 | CLIENT: 32 50 2d ac 00 00 00 00 a0 02 00 01 00 00 572 | CLIENT: 32 50 2d ab 00 00 00 00 a0 03 00 01 00 00 573 | SERVER: 32 50 7d a0 00 00 00 00 b0 02 00 03 00 00 00 06 00 00 00 00 a0 03 574 | CLIENT: 32 50 4b 81 00 00 00 00 a0 04 00 06 00 06 00 00 00 00 b0 02 00 04 00 00 32 17 575 | ... etc 576 | CLIENT: 32 50 ed 6f 00 00 00 00 a0 0a 00 21 00 00 00 0e 00 02 00 00 00 00 a0 01 00 00 00 00 a0 02 577 | SERVER: 32 50 8d 3b 00 00 00 00 b0 06 00 3b 00 00 00 06 00 00 00 00 a0 0a 00 08 00 01 00 00 00 00 a0 01 00 08 00 01 00 00 00 00 a0 02 00 08 00 01 00 00 00 00 b0 02 578 | CLIENT: 32 50 9a 41 00 00 00 00 a0 0b 00 0e 00 06 00 00 00 00 b0 06 00 04 00 00 33 38 00 08 00 01 00 00 00 00 b0 02 579 | 580 | ## Changelog 581 | 582 | ### 4.1 (20181225) 583 | * Moved all opcode-related information to the Opcodes section. 584 | * Program version text, notice text and HKDF input key material are explicitly specified as being UTF-8 encoded. 585 | * Added note that host processing latency should only be used with a monotonic clock. 586 | * Clarified monotonic clock best practices. 587 | * Added note about the meaning of extended segment number 0x3250564e, and how it shouldn't be used as a guide. 588 | * Changed all instances of "octet" to "byte", and explicitly defined a byte as 8 bits. 589 | * General cleanup and minor clarifications. 590 | 591 | ### 4.0 (20170806) 592 | * Added opcode 0x0200 - Encrypted packet 593 | 594 | ### 3.2 (20170722) 595 | * Added HMAC-SHA512 (index 5) to 0x0080 MAC digest types. 596 | * Added the following registered extended segments: 597 | * 0x88a1f7c7: Battery levels 598 | * Clarified that only one instance of a specific ExtID may be present in a packet. 599 | 600 | ### 3.1 (20160210) 601 | * Added the following registered extended segments: 602 | * 0x2ff6ad68: Random data 603 | * 0x64f69319: Wall clock 604 | * 0x771d8dfb: Monotonic clock 605 | 606 | ### 3.0 (20151025) 607 | * Changed the checksum method from RFC 768 to a custom method with example pseudocode. 608 | This creates a functional incompatibility with previous versions of the specification. 609 | However, it was discovered that the only known 2ping implementation which computed checksums at the time was using an incorrect method, so the specification was changed to match the implementation to preserve compatibility. 610 | * Fixed a typo in the extended segment table, changing "Extended segment ID" from 8 bytes to 4 bytes. 611 | This clarifies the previous (correct) assertion that the segment ID is 32 bits (4 bytes). 612 | * Populated checksums of example packet dumps. 613 | * Corrected the 5th client line of Example 6's packet dump (pseudocode version was correct, but example dump had 2 incorrect bytes added). 614 | * Adjusted wording of psuedocode examples to clarify "reply requested" opcode comes before "in reply to" opcode. 615 | 616 | ### 2.0 (20120422) 617 | 618 | * Protocol versions 1.0 and 2.0 are backwards and forwards compatible with each other. 619 | * Changed recommended default minimum packet size from 64 bytes to 128 bytes. 620 | * Added an extended segment container at opcode 0x8000. 621 | * Added the following registered extended segments: 622 | * 0x3250564e: Program version 623 | * 0xa837b44e: Notice text 624 | 625 | ### 1.0 (20101020) 626 | 627 | * Finalized initial release. 628 | 629 | ## Copyright 630 | 631 | Copyright (C) 2010-2021 Ryan Finnie 632 | 633 | This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/). 634 | -------------------------------------------------------------------------------- /doc/2ping.1: -------------------------------------------------------------------------------- 1 | .\" Automatically generated by Pandoc 2.5 2 | .\" 3 | .TH "2PING" "1" "" "" "2ping" 4 | .hy 5 | .SH NAME 6 | .PP 7 | 2ping \- A bi\-directional ping utility 8 | .SH SYNOPSIS 9 | .PP 10 | 2ping [\f[I]options\f[R]] \f[I]\-\-listen\f[R] | host/IP [host/IP 11 | [\&...]] 12 | .SH DESCRIPTION 13 | .PP 14 | \f[C]2ping\f[R] is a bi\-directional ping utility. 15 | It uses 3\-way pings (akin to TCP SYN, SYN/ACK, ACK) and 16 | after\-the\-fact state comparison between a 2ping listener and a 2ping 17 | client to determine which direction packet loss occurs. 18 | .PP 19 | To use 2ping, start a listener on a known stable network host. 20 | The relative network stability of the 2ping listener host should not be 21 | in question, because while 2ping can determine whether packet loss is 22 | occurring inbound or outbound relative to an endpoint, that will not 23 | help you determine the cause if both of the endpoints are in question. 24 | .PP 25 | Once the listener is started, start 2ping in client mode and tell it to 26 | connect to the listener. 27 | The ends will begin pinging each other and displaying network 28 | statistics. 29 | If packet loss occurs, 2ping will wait a few seconds (default 10, 30 | configurable with \f[I]\-\-inquire\-wait\f[R]) before comparing notes 31 | between the two endpoints to determine which direction the packet loss 32 | is occurring. 33 | .PP 34 | To quit 2ping on the client or listener ends, enter \[ha]C, and a list 35 | of statistics will be displayed. 36 | To get a short inline display of statistics without quitting, enter 37 | \[ha]\[rs] or send the process a QUIT signal. 38 | .SH OPTIONS 39 | .PP 40 | \f[C]ping\f[R]\-compatible options (long option names are 41 | \f[C]2ping\f[R]\-specific): 42 | .TP 43 | .B \-\-audible, \-a 44 | Audible ping. 45 | .TP 46 | .B \-\-adaptive, \-A 47 | Adaptive ping. 48 | Interpacket interval adapts to round\-trip time, so that effectively not 49 | more than one (or more, if preload is set) unanswered probe is present 50 | in the network. 51 | On networks with low rtt this mode is essentially equivalent to flood 52 | mode. 53 | .TP 54 | .B \-\-count=\f[I]count\f[R], \-c \f[I]count\f[R] 55 | Stop after sending \f[I]count\f[R] ping requests. 56 | .TP 57 | .B \-\-flood, \-f 58 | Flood ping. 59 | For every ping sent a period \[lq].\[rq] is printed, while for ever ping 60 | received a backspace is printed. 61 | This provides a rapid display of how many pings are being dropped. 62 | If interval is not given, it sets interval to zero and outputs pings as 63 | fast as they come back or one hundred times per second, whichever is 64 | more. 65 | .RS 66 | .PP 67 | \f[C]2ping\f[R]\-specific notes: Detected outbound/inbound loss 68 | responses are printed as \[lq]>\[rq] and \[lq]<\[rq], respectively. 69 | Receive errors are printed as \[lq]E\[rq]. 70 | Due to the asynchronous nature of \f[C]2ping\f[R], successful responses 71 | (backspaces) may overwrite these loss and error characters. 72 | .RE 73 | .TP 74 | .B \-\-interval=\f[I]interval\f[R], \-i \f[I]interval\f[R] 75 | Wait \f[I]interval\f[R] seconds between sending each ping. 76 | The default is to wait for one second between each ping normally, or not 77 | to wait in flood mode. 78 | .TP 79 | .B \-\-interface\-address=\f[I]address\f[R], \-I \f[I]address\f[R] 80 | Set source IP address. 81 | When in listener mode, this option may be specified multiple to bind to 82 | multiple IP addresses. 83 | When in client mode, this option may only be specified once, and all 84 | outbound pings will be bound to this source IP. 85 | .RS 86 | .PP 87 | \f[C]2ping\f[R]\-specific notes: This option only takes an IP address, 88 | not a device name. 89 | Note that in listener mode, if the machine has an interface with 90 | multiple IP addresses and an request comes in via a sub IP, the reply 91 | still leaves via the interface\[cq]s main IP. 92 | So either this option \[en] or (preferred) listening on all IPs 93 | individually via the Python \[lq]netifaces\[rq] module \[en] must be 94 | used if you would like to respond via an interface\[cq]s sub\-IP. 95 | .RE 96 | .TP 97 | .B \-\-preload=\f[I]count\f[R], \-l \f[I]count\f[R] 98 | If specified, \f[C]2ping\f[R] sends that many packets not waiting for 99 | reply. 100 | .TP 101 | .B \-\-pattern=\f[I]hex_bytes\f[R], \-p \f[I]hex_bytes\f[R] 102 | You may specify a number of hex bytes to fill out the packets you send. 103 | This is useful for diagnosing data\-dependent problems in a network. 104 | For example, \f[I]\-\-pattern=ff\f[R] will cause the sent packet pad 105 | area to be filled with all ones. 106 | .RS 107 | .PP 108 | \f[C]2ping\f[R]\-specific notes: This pads the portion of the packet 109 | that does not contain the active payload data. 110 | If the active payload data is larger than the minimum packet size 111 | (\f[I]\-\-min\-packet\-size\f[R]), no padding will be sent. 112 | .RE 113 | .TP 114 | .B \-\-quiet, \-q 115 | Quiet output. 116 | Nothing is displayed except the summary lines at startup time and when 117 | finished. 118 | .TP 119 | .B \-\-packetsize\-compat=\f[I]bytes\f[R], \-s \f[I]bytes\f[R] 120 | \f[C]ping\f[R] compatibility; this will set 121 | \f[I]\-\-min\-packet\-size\f[R] to this plus 8 bytes. 122 | .TP 123 | .B \-\-verbose, \-v 124 | Verbose output. 125 | In \f[C]2ping\f[R], this prints decodes of packets that are sent and 126 | received. 127 | .TP 128 | .B \-\-version, \-V 129 | Show version and exit. 130 | .TP 131 | .B \-\-deadline=\f[I]seconds\f[R], \-w \f[I]seconds\f[R] 132 | Specify a timeout, in seconds, before \f[C]2ping\f[R] exits regardless 133 | of how many pings have been sent or received. 134 | Due to blocking, this may occur up to one second after the deadline 135 | specified. 136 | .PP 137 | \f[C]2ping\f[R]\-specific options: 138 | .TP 139 | .B \-\-help, \-h 140 | Print a synposis and exit. 141 | .TP 142 | .B \-\-ipv4, \-4 143 | Limit binds to IPv4. 144 | In client mode, this forces resolution of dual\-homed hostnames to the 145 | IPv4 address. 146 | (Without \f[I]\-\-ipv4\f[R] or \f[I]\-\-ipv6\f[R], the first result will 147 | be used as specified by your operating system, usually the AAAA address 148 | on IPv6\-routable machines, or the A address on IPv4\-only machines.) In 149 | listener mode, this filters out any non\-IPv4 150 | \f[I]\-\-interface\-address\f[R] binds, either through hostname 151 | resolution or explicit passing. 152 | .TP 153 | .B \-\-ipv6, \-6 154 | Limit binds to IPv6. 155 | In client mode, this forces resolution of dual\-homed hostnames to the 156 | IPv6 address. 157 | (Without \f[I]\-4\f[R] or \f[I]\-6\f[R], the first result will be used 158 | as specified by your operating system, usually the AAAA address on 159 | IPv6\-routable machines, or the A address on IPv4\-only machines.) In 160 | listener mode, this filters out any non\-IPv6 161 | \f[I]\-\-interface\-address\f[R] binds, either through hostname 162 | resolution or explicit passing. 163 | .TP 164 | .B \-\-all\-interfaces 165 | Deprecated. 166 | In listener mode, all addresses will be listened to by default if the 167 | Python \[lq]netifaces\[rq] module is installed, unless overridden by one 168 | or more \f[I]\-\-interface\-address\f[R] invocations. 169 | .TP 170 | .B \-\-auth=\f[I]key\f[R] 171 | Set a shared key, send cryptographic hashes with each packet, and 172 | require cryptographic hashes from peer packets signed with the same 173 | shared key. 174 | .TP 175 | .B \-\-auth\-digest=\f[I]digest\f[R] 176 | When \f[I]\-\-auth\f[R] is used, specify the digest type to compute the 177 | cryptographic hash. 178 | Valid options are \f[C]hmac\-md5\f[R] (default), \f[C]hmac\-sha1\f[R], 179 | \f[C]hmac\-sha256\f[R] and \f[C]hmac\-sha512\f[R]. 180 | .TP 181 | .B \-\-debug 182 | Print (lots of) debugging information. 183 | .TP 184 | .B \-\-encrypt=\f[I]key\f[R] 185 | Set a shared key, encrypt 2ping packets, and require encrypted packets 186 | from peers encrypted with the same shared key. 187 | Requires the PyCryptodome or PyCrypto module. 188 | .TP 189 | .B \-\-encrypt\-method=\f[I]method\f[R] 190 | When \f[I]\-\-encrypt\f[R] is used, specify the method used to encrypt 191 | packets. 192 | Valid options are \f[C]hkdf\-aes256\-cbc\f[R] (default). 193 | .TP 194 | .B \-\-fuzz=\f[I]percent\f[R] 195 | Simulate corruption of incoming packets, with a \f[I]percent\f[R] 196 | probability each bit will be flipped. 197 | After fuzzing, the packet checksum will be recalculated, and then the 198 | checksum itself will be fuzzed (but at a lower probability). 199 | .TP 200 | .B \-\-inquire\-wait=\f[I]secs\f[R] 201 | Wait at least \f[I]secs\f[R] seconds before inquiring about a lost 202 | packet. 203 | Default is 10 seconds. 204 | UDP packets can arrive delayed or out of order, so it is best to give it 205 | some time before inquiring about a lost packet. 206 | .TP 207 | .B \-\-listen 208 | Start as a listener. 209 | The listener will not send out ping requests at regular intervals, and 210 | will instead wait for the far end to initiate ping requests. 211 | A listener is required as the remote end for a client. 212 | When run as a listener, a SIGHUP will reload the configuration on all 213 | interfaces. 214 | .TP 215 | .B \-\-loopback 216 | Use one or more client/listener pairs of UNIX datagram sockets. 217 | Mainly for testing purposes. 218 | .TP 219 | .B \-\-loopback\-pairs=\f[I]pairs\f[R] 220 | Number of pairs to generate when using \f[I]\-\-loopback\f[R]. 221 | .TP 222 | .B \-\-min\-packet\-size=\f[I]min\f[R] 223 | Set the minimum total payload size to \f[I]min\f[R] bytes, default 128. 224 | If the payload is smaller than \f[I]min\f[R] bytes, padding will be 225 | added to the end of the packet. 226 | .TP 227 | .B \-\-max\-packet\-size=\f[I]max\f[R] 228 | Set the maximum total payload size to \f[I]max\f[R] bytes, default 512, 229 | absolute minimum 64. 230 | If the payload is larger than \f[I]max\f[R] bytes, information will be 231 | rearranged and sent in future packets when possible. 232 | .TP 233 | .B \-\-nagios=\f[I]wrta\f[R],\f[I]wloss%\f[R],\f[I]crta\f[R],\f[I]closs%\f[R] 234 | Produce output suitable for use in a Nagios check. 235 | If \f[I]\-\-count\f[R] is not specified, defaults to 5 pings. 236 | A warning condition (exit code 1) will be returned if average RTT 237 | exceeds \f[I]wrta\f[R] or ping loss exceeds \f[I]wloss%\f[R]. 238 | A critical condition (exit code 2) will be returned if average RTT 239 | exceeds \f[I]crta\f[R] or ping loss exceeds \f[I]closs%\f[R]. 240 | .TP 241 | .B \-\-no\-3way 242 | Do not perform 3\-way pings. 243 | Used most often when combined with \f[I]\-\-listen\f[R], as the listener 244 | is usually the one to determine whether a ping reply should become a 245 | 3\-way ping. 246 | .RS 247 | .PP 248 | Strictly speaking, a 3\-way ping is not necessary for determining 249 | directional packet loss between the client and the listener. 250 | However, the extra leg of the 3\-way ping allows for extra chances to 251 | determine packet loss more efficiently. 252 | Also, with 3\-way ping disabled, the listener will receive no client 253 | performance indicators, nor will the listener be able to determine 254 | directional packet loss that it detects. 255 | .RE 256 | .TP 257 | .B \-\-no\-match\-packet\-size 258 | When sending replies, 2ping will try to match the packet size of the 259 | received packet by adding padding if necessary, but will not exceed 260 | \f[I]\-\-max\-packet\-size\f[R]. 261 | \f[I]\-\-no\-match\-packet\-size\f[R] disables this behavior, always 262 | setting the minimum to \f[I]\-\-min\-packet\-size\f[R]. 263 | .TP 264 | .B \-\-no\-send\-version 265 | Do not send the current running version of 2ping with each packet. 266 | .TP 267 | .B \-\-notice=\f[I]text\f[R] 268 | Send arbitrary notice \f[I]text\f[R] with each packet. 269 | If the remote peer supports it, this may be displayed to the user. 270 | .TP 271 | .B \-\-packet\-loss=\f[I]out:in\f[R] 272 | Simulate random packet loss outbound and inbound. 273 | For example, \f[I]25:10\f[R] means a 25% chance of not sending a packet, 274 | and a 10% chance of ignoring a received packet. 275 | A single number without colon separation means use the same percentage 276 | for both outbound and inbound. 277 | .TP 278 | .B \-\-port=\f[I]port\f[R] 279 | Use UDP port \f[I]port\f[R], either a numeric port number or a service 280 | name string. 281 | With \f[I]\-\-listen\f[R], this is the port to bind as, otherwise this 282 | is the port to send to. 283 | Default is UDP port 15998. 284 | .RS 285 | .PP 286 | When port \f[I]\[lq]\-1\[rq]\f[R] is specified, a random unused high 287 | port is picked. 288 | This is useful for automated unit and functional testing, but not for 289 | normal use. 290 | .RE 291 | .TP 292 | .B \-\-send\-monotonic\-clock 293 | Send a monotonic clock value with each packet. 294 | Peer time (if sent by the peer) can be viewed with 295 | \f[I]\-\-verbose\f[R]. 296 | .TP 297 | .B \-\-send\-random=\f[I]bytes\f[R] 298 | Send random data to the peer, up to \f[I]bytes\f[R]. 299 | The number of bytes will be limited by other factors, up to 300 | \f[I]\-\-max\-packet\-size\f[R]. 301 | If this data is to be used for trusted purposes, it should be combined 302 | with \f[I]\-\-auth\f[R] for HMAC authentication. 303 | .TP 304 | .B \-\-send\-time 305 | Send the host time (wall clock) with each packet. 306 | Peer time (if sent by the peer) can be viewed with 307 | \f[I]\-\-verbose\f[R]. 308 | .TP 309 | .B \-\-srv 310 | In client mode, causes hostnames to be looked up via DNS SRV records. 311 | If the SRV query returns multiple record targets, they will all be 312 | pinged in parallel; priority and weight are not considered. 313 | The record\[cq]s port will be used instead of \f[I]\-\-port\f[R]. 314 | This functionality requires the dnspython module to be installed. 315 | .TP 316 | .B \-\-srv\-service=\f[I]service\f[R] 317 | When combined with \f[I]\-\-srv\f[R], service name to be used for SRV 318 | lookups. 319 | Default service is \[lq]2ping\[rq]. 320 | .TP 321 | .B \-\-stats=\f[I]interval\f[R] 322 | Print a line of brief current statistics every \f[I]interval\f[R] 323 | seconds. 324 | The same line can be printed on demand by entering \[ha]\[rs] or sending 325 | the QUIT signal to the 2ping process. 326 | .TP 327 | .B \-\-subtract\-peer\-host\-latency 328 | If a peer sends its host latency (the amount of time it spends between 329 | receiving a packet and sending out a reply), subtract it from RTT 330 | calculations. 331 | .SH BUGS 332 | .PP 333 | None known, many assumed. 334 | .SH AUTHORS 335 | Ryan Finnie . 336 | -------------------------------------------------------------------------------- /doc/2ping.md: -------------------------------------------------------------------------------- 1 | % 2PING(1) | 2ping 2 | % Ryan Finnie \ 3 | # NAME 4 | 5 | 2ping - A bi-directional ping utility 6 | 7 | # SYNOPSIS 8 | 9 | 2ping [*options*] *\-\-listen* | host/IP [host/IP [...]] 10 | 11 | # DESCRIPTION 12 | 13 | `2ping` is a bi-directional ping utility. 14 | It uses 3-way pings (akin to TCP SYN, SYN/ACK, ACK) and after-the-fact state comparison between a 2ping listener and a 2ping client to determine which direction packet loss occurs. 15 | 16 | To use 2ping, start a listener on a known stable network host. 17 | The relative network stability of the 2ping listener host should not be in question, because while 2ping can determine whether packet loss is occurring inbound or outbound relative to an endpoint, that will not help you determine the cause if both of the endpoints are in question. 18 | 19 | Once the listener is started, start 2ping in client mode and tell it to connect to the listener. 20 | The ends will begin pinging each other and displaying network statistics. 21 | If packet loss occurs, 2ping will wait a few seconds (default 10, configurable with *\-\-inquire-wait*) before comparing notes between the two endpoints to determine which direction the packet loss is occurring. 22 | 23 | To quit 2ping on the client or listener ends, enter \^C, and a list of statistics will be displayed. 24 | To get a short inline display of statistics without quitting, enter \^\\ or send the process a QUIT signal. 25 | 26 | # OPTIONS 27 | 28 | `ping`-compatible options (long option names are `2ping`-specific): 29 | 30 | \-\-audible, -a 31 | : Audible ping. 32 | 33 | \-\-adaptive, -A 34 | : Adaptive ping. 35 | Interpacket interval adapts to round-trip time, so that effectively not more than one (or more, if preload is set) unanswered probe is present in the network. 36 | On networks with low rtt this mode is essentially equivalent to flood mode. 37 | 38 | \-\-count=*count*, -c *count* 39 | : Stop after sending *count* ping requests. 40 | 41 | \-\-flood, -f 42 | : Flood ping. 43 | For every ping sent a period "." is printed, while for ever ping received a backspace is printed. 44 | This provides a rapid display of how many pings are being dropped. 45 | If interval is not given, it sets interval to zero and outputs pings as fast as they come back or one hundred times per second, whichever is more. 46 | 47 | `2ping`-specific notes: Detected outbound/inbound loss responses are printed as "\>" and "\<", respectively. 48 | Receive errors are printed as "E". 49 | Due to the asynchronous nature of `2ping`, successful responses (backspaces) may overwrite these loss and error characters. 50 | 51 | \-\-interval=*interval*, -i *interval* 52 | : Wait *interval* seconds between sending each ping. 53 | The default is to wait for one second between each ping normally, or not to wait in flood mode. 54 | 55 | \-\-interface-address=*address*, -I *address* 56 | : Set source IP address. 57 | When in listener mode, this option may be specified multiple to bind to multiple IP addresses. 58 | When in client mode, this option may only be specified once, and all outbound pings will be bound to this source IP. 59 | 60 | `2ping`-specific notes: This option only takes an IP address, not a device name. 61 | Note that in listener mode, if the machine has an interface with multiple IP addresses and an request comes in via a sub IP, the reply still leaves via the interface's main IP. 62 | So either this option -- or (preferred) listening on all IPs individually via the Python "netifaces" module -- must be used if you would like to respond via an interface's sub-IP. 63 | 64 | \-\-preload=*count*, -l *count* 65 | : If specified, `2ping` sends that many packets not waiting for reply. 66 | 67 | \-\-pattern=*hex_bytes*, -p *hex_bytes* 68 | : You may specify a number of hex bytes to fill out the packets you send. 69 | This is useful for diagnosing data-dependent problems in a network. 70 | For example, *\-\-pattern=ff* will cause the sent packet pad area to be filled with all ones. 71 | 72 | `2ping`-specific notes: This pads the portion of the packet that does not contain the active payload data. 73 | If the active payload data is larger than the minimum packet size (*\-\-min-packet-size*), no padding will be sent. 74 | 75 | \-\-quiet, -q 76 | : Quiet output. 77 | Nothing is displayed except the summary lines at startup time and when finished. 78 | 79 | \-\-packetsize-compat=*bytes*, -s *bytes* 80 | : `ping` compatibility; this will set *\-\-min-packet-size* to this plus 8 bytes. 81 | 82 | \-\-verbose, -v 83 | : Verbose output. 84 | In `2ping`, this prints decodes of packets that are sent and received. 85 | 86 | \-\-version, -V 87 | : Show version and exit. 88 | 89 | \-\-deadline=*seconds*, -w *seconds* 90 | : Specify a timeout, in seconds, before `2ping` exits regardless of how many pings have been sent or received. 91 | Due to blocking, this may occur up to one second after the deadline specified. 92 | 93 | `2ping`-specific options: 94 | 95 | \-\-help, -h 96 | : Print a synposis and exit. 97 | 98 | \-\-ipv4, -4 99 | : Limit binds to IPv4. 100 | In client mode, this forces resolution of dual-homed hostnames to the IPv4 address. 101 | (Without *\-\-ipv4* or *\-\-ipv6*, the first result will be used as specified by your operating system, usually the AAAA address on IPv6-routable machines, or the A address on IPv4-only machines.) 102 | In listener mode, this filters out any non-IPv4 *\-\-interface-address* binds, either through hostname resolution or explicit passing. 103 | 104 | \-\-ipv6, -6 105 | : Limit binds to IPv6. 106 | In client mode, this forces resolution of dual-homed hostnames to the IPv6 address. 107 | (Without *-4* or *-6*, the first result will be used as specified by your operating system, usually the AAAA address on IPv6-routable machines, or the A address on IPv4-only machines.) 108 | In listener mode, this filters out any non-IPv6 *\-\-interface-address* binds, either through hostname resolution or explicit passing. 109 | 110 | \-\-all-interfaces 111 | : Deprecated. 112 | In listener mode, all addresses will be listened to by default if the Python "netifaces" module is installed, unless overridden by one or more *\-\-interface-address* invocations. 113 | 114 | \-\-auth=*key* 115 | : Set a shared key, send cryptographic hashes with each packet, and require cryptographic hashes from peer packets signed with the same shared key. 116 | 117 | \-\-auth-digest=*digest* 118 | : When *\-\-auth* is used, specify the digest type to compute the cryptographic hash. 119 | Valid options are `hmac-md5` (default), `hmac-sha1`, `hmac-sha256` and `hmac-sha512`. 120 | 121 | \-\-debug 122 | : Print (lots of) debugging information. 123 | 124 | \-\-encrypt=*key* 125 | : Set a shared key, encrypt 2ping packets, and require encrypted packets from peers encrypted with the same shared key. 126 | Requires the PyCryptodome or PyCrypto module. 127 | 128 | \-\-encrypt-method=*method* 129 | : When *\-\-encrypt* is used, specify the method used to encrypt packets. 130 | Valid options are `hkdf-aes256-cbc` (default). 131 | 132 | \-\-fuzz=*percent* 133 | : Simulate corruption of incoming packets, with a *percent* probability each bit will be flipped. 134 | After fuzzing, the packet checksum will be recalculated, and then the checksum itself will be fuzzed (but at a lower probability). 135 | 136 | \-\-inquire-wait=*secs* 137 | : Wait at least *secs* seconds before inquiring about a lost packet. 138 | Default is 10 seconds. 139 | UDP packets can arrive delayed or out of order, so it is best to give it some time before inquiring about a lost packet. 140 | 141 | \-\-listen 142 | : Start as a listener. 143 | The listener will not send out ping requests at regular intervals, and will instead wait for the far end to initiate ping requests. 144 | A listener is required as the remote end for a client. 145 | When run as a listener, a SIGHUP will reload the configuration on all interfaces. 146 | 147 | \-\-loopback 148 | : Use one or more client/listener pairs of UNIX datagram sockets. 149 | Mainly for testing purposes. 150 | 151 | \-\-loopback-pairs=*pairs* 152 | : Number of pairs to generate when using *\-\-loopback*. 153 | 154 | \-\-min-packet-size=*min* 155 | : Set the minimum total payload size to *min* bytes, default 128. 156 | If the payload is smaller than *min* bytes, padding will be added to the end of the packet. 157 | 158 | \-\-max-packet-size=*max* 159 | : Set the maximum total payload size to *max* bytes, default 512, absolute minimum 64. 160 | If the payload is larger than *max* bytes, information will be rearranged and sent in future packets when possible. 161 | 162 | \-\-nagios=*wrta*,*wloss%*,*crta*,*closs%* 163 | : Produce output suitable for use in a Nagios check. 164 | If *\-\-count* is not specified, defaults to 5 pings. 165 | A warning condition (exit code 1) will be returned if average RTT exceeds *wrta* or ping loss exceeds *wloss%*. 166 | A critical condition (exit code 2) will be returned if average RTT exceeds *crta* or ping loss exceeds *closs%*. 167 | 168 | \-\-no-3way 169 | : Do not perform 3-way pings. 170 | Used most often when combined with *\-\-listen*, as the listener is usually the one to determine whether a ping reply should become a 3-way ping. 171 | 172 | Strictly speaking, a 3-way ping is not necessary for determining directional packet loss between the client and the listener. 173 | However, the extra leg of the 3-way ping allows for extra chances to determine packet loss more efficiently. 174 | Also, with 3-way ping disabled, the listener will receive no client performance indicators, nor will the listener be able to determine directional packet loss that it detects. 175 | 176 | \-\-no-match-packet-size 177 | : When sending replies, 2ping will try to match the packet size of the received packet by adding padding if necessary, but will not exceed *\-\-max-packet-size*. 178 | *\-\-no-match-packet-size* disables this behavior, always setting the minimum to *\-\-min-packet-size*. 179 | 180 | \-\-no-send-version 181 | : Do not send the current running version of 2ping with each packet. 182 | 183 | \-\-notice=*text* 184 | : Send arbitrary notice *text* with each packet. 185 | If the remote peer supports it, this may be displayed to the user. 186 | 187 | \-\-packet-loss=*out:in* 188 | : Simulate random packet loss outbound and inbound. 189 | For example, *25:10* means a 25% chance of not sending a packet, and a 10% chance of ignoring a received packet. 190 | A single number without colon separation means use the same percentage for both outbound and inbound. 191 | 192 | \-\-port=*port* 193 | : Use UDP port *port*, either a numeric port number or a service name string. 194 | With *\-\-listen*, this is the port to bind as, otherwise this is the port to send to. 195 | Default is UDP port 15998. 196 | 197 | When port *"-1"* is specified, a random unused high port is picked. 198 | This is useful for automated unit and functional testing, but not for normal use. 199 | 200 | \-\-send-monotonic-clock 201 | : Send a monotonic clock value with each packet. 202 | Peer time (if sent by the peer) can be viewed with *\-\-verbose*. 203 | 204 | \-\-send-random=*bytes* 205 | : Send random data to the peer, up to *bytes*. 206 | The number of bytes will be limited by other factors, up to *\-\-max-packet-size*. 207 | If this data is to be used for trusted purposes, it should be combined with *\-\-auth* for HMAC authentication. 208 | 209 | \-\-send-time 210 | : Send the host time (wall clock) with each packet. 211 | Peer time (if sent by the peer) can be viewed with *\-\-verbose*. 212 | 213 | \-\-srv 214 | : In client mode, causes hostnames to be looked up via DNS SRV records. 215 | If the SRV query returns multiple record targets, they will all be pinged in parallel; priority and weight are not considered. 216 | The record's port will be used instead of *\-\-port*. 217 | This functionality requires the dnspython module to be installed. 218 | 219 | \-\-srv-service=*service* 220 | : When combined with *\-\-srv*, service name to be used for SRV lookups. 221 | Default service is "2ping". 222 | 223 | \-\-stats=*interval* 224 | : Print a line of brief current statistics every *interval* seconds. 225 | The same line can be printed on demand by entering \^\\ or sending the QUIT signal to the 2ping process. 226 | 227 | \-\-subtract-peer-host-latency 228 | : If a peer sends its host latency (the amount of time it spends between receiving a packet and sending out a reply), subtract it from RTT calculations. 229 | 230 | # BUGS 231 | 232 | None known, many assumed. 233 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | MARKDOWN=2ping.md 2 | MAN=$(patsubst %.md,%.1,$(MARKDOWN)) 3 | 4 | all: man 5 | doc: man 6 | man: ${MAN} 7 | 8 | clean: 9 | rm -f ${MAN} 10 | 11 | %.1: %.md 12 | pandoc -s -t man -o $@ $< 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | distro 2 | dnspython 3 | netifaces 4 | pycryptodomex 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | 6 | from setuptools import setup 7 | 8 | 9 | __version__ = "4.5.1" 10 | assert sys.version_info > (3, 6) 11 | 12 | 13 | def read(filename): 14 | with open(os.path.join(os.path.dirname(__file__), filename), encoding="utf-8") as f: 15 | return f.read() 16 | 17 | 18 | setup( 19 | name="2ping", 20 | description="2ping a bi-directional ping utility", 21 | long_description=read("README.md"), 22 | long_description_content_type="text/markdown", 23 | version=__version__, 24 | license="MPL-2.0", 25 | platforms=["Unix"], 26 | author="Ryan Finnie", 27 | author_email="ryan@finnie.org", 28 | url="https://www.finnie.org/software/2ping/", 29 | download_url="https://www.finnie.org/software/2ping/", 30 | packages=["twoping"], 31 | classifiers=[ 32 | "Development Status :: 5 - Production/Stable", 33 | "Environment :: Console", 34 | "Intended Audience :: System Administrators", 35 | "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", 36 | "Natural Language :: English", 37 | "Operating System :: MacOS :: MacOS X", 38 | "Operating System :: Microsoft :: Windows", 39 | "Operating System :: POSIX", 40 | "Operating System :: Unix", 41 | "Programming Language :: Python :: 3 :: Only", 42 | "Topic :: Internet", 43 | "Topic :: System :: Networking", 44 | "Topic :: Utilities", 45 | ], 46 | entry_points={ 47 | "console_scripts": ["2ping = twoping.cli:main", "2ping6 = twoping.cli:main"] 48 | }, 49 | ) 50 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest.mock 2 | 3 | 4 | def _test_module_init(module, main_name="main"): 5 | with unittest.mock.patch.object( 6 | module, main_name, return_value=0 7 | ), unittest.mock.patch.object( 8 | module, "__name__", "__main__" 9 | ), unittest.mock.patch.object( 10 | module.sys, "exit" 11 | ) as exit: 12 | module.module_init() 13 | return exit.call_args[0][0] == 0 14 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import locale 2 | import logging 3 | import pytest 4 | import unittest 5 | import unittest.mock 6 | 7 | from . import _test_module_init 8 | from twoping import args, cli, utils 9 | 10 | 11 | class TestCLI(unittest.TestCase): 12 | bind_addresses = ["127.0.0.1"] 13 | port = None 14 | logger = None 15 | class_args = None 16 | 17 | def setUp(self): 18 | self.logger = logging.getLogger() 19 | self.logger.level = logging.DEBUG 20 | 21 | def _client(self, test_flags=(), test_positionals=(), test_stats=True, pairs=1): 22 | if self.port is None: 23 | port = -1 24 | else: 25 | port = self.port 26 | 27 | flag_args = ["--debug"] 28 | if self.class_args is not None: 29 | flag_args += self.class_args 30 | flag_args += test_flags 31 | 32 | positional_args = list(test_positionals) 33 | 34 | if "--loopback" not in flag_args: 35 | flag_args += ["--listen", "--port={}".format(port)] 36 | for bind_address in self.bind_addresses: 37 | flag_args.append("--interface-address={}".format(bind_address)) 38 | positional_args.append(bind_address) 39 | if ("--adaptive" not in flag_args) and ("--flood" not in flag_args): 40 | flag_args.append("--count=1") 41 | if not ("--count=1" in flag_args): 42 | flag_args.append("--interval=5") 43 | all_args = ["2ping"] + flag_args + positional_args 44 | self.logger.info("Passed arguments: {}".format(all_args)) 45 | 46 | p = cli.TwoPing(args.parse_args(all_args)) 47 | self.logger.info("Parsed arguments: {}".format(p.args)) 48 | self.assertEqual(p.run(), 0) 49 | 50 | for sock_class in p.sock_classes: 51 | self.assertTrue(sock_class.closed) 52 | 53 | self.assertEqual(len(p.sock_classes), (pairs * 2)) 54 | client_sock_classes = [ 55 | sock_class for sock_class in p.sock_classes if sock_class.is_client 56 | ] 57 | self.assertEqual(len(client_sock_classes), pairs) 58 | sock_class = client_sock_classes[0] 59 | 60 | if not test_stats: 61 | return sock_class 62 | 63 | self.assertEqual(sock_class.errors_received, 0) 64 | self.assertEqual(sock_class.lost_inbound, 0) 65 | self.assertEqual(sock_class.lost_outbound, 0) 66 | if p.args.count: 67 | self.assertEqual(sock_class.pings_transmitted, p.args.count) 68 | self.assertEqual(sock_class.pings_received, p.args.count) 69 | 70 | return sock_class 71 | 72 | def test_3way(self): 73 | sock_class = self._client([]) 74 | self.assertEqual(sock_class.packets_transmitted, 2) 75 | self.assertEqual(sock_class.packets_received, 1) 76 | 77 | def test_3way_no(self): 78 | sock_class = self._client(["--no-3way"]) 79 | self.assertEqual(sock_class.packets_transmitted, 1) 80 | self.assertEqual(sock_class.packets_received, 1) 81 | 82 | @pytest.mark.slow 83 | def test_adaptive(self): 84 | sock_class = self._client(["--adaptive", "--deadline=3"]) 85 | self.assertGreaterEqual(sock_class.pings_transmitted, 100) 86 | 87 | @unittest.skipIf(isinstance(utils.AES, ImportError), "Crypto module required") 88 | def test_encrypt(self): 89 | self._client( 90 | ["--encrypt-method=hkdf-aes256-cbc", "--encrypt=S49HVbnJd3fBdDzdMVVw"] 91 | ) 92 | 93 | @pytest.mark.slow 94 | def test_flood(self): 95 | sock_class = self._client(["--flood", "--deadline=3"]) 96 | self.assertGreaterEqual(sock_class.pings_transmitted, 100) 97 | 98 | def test_hmac_crc32(self): 99 | self._client(["--auth-digest=hmac-crc32", "--auth=mc82kJwtXFlhqQSCKptQ"]) 100 | 101 | def test_hmac_md5(self): 102 | self._client(["--auth-digest=hmac-md5", "--auth=rBgRpBfRbF4DkwFQXncz"]) 103 | 104 | def test_hmac_sha1(self): 105 | self._client(["--auth-digest=hmac-sha1", "--auth=qnzTCJHnZXdrxRZ8JjQw"]) 106 | 107 | def test_hmac_sha256(self): 108 | self._client(["--auth-digest=hmac-sha256", "--auth=cc8G2Ssbq4WZRq7H7d5L"]) 109 | 110 | def test_hmac_sha512(self): 111 | self._client(["--auth-digest=hmac-sha512", "--auth=sjk3kqzcSV3XfHJWNstn"]) 112 | 113 | def test_invalid_hostname(self): 114 | with self.assertRaises(OSError): 115 | self._client([], ["xGkKWDDMnZxCD4XchMnK."]) 116 | 117 | def test_monotonic_clock(self): 118 | self._client(["--send-monotonic-clock"]) 119 | 120 | def test_notice(self): 121 | self._client(["--notice=Notice text"]) 122 | 123 | @unittest.skipUnless( 124 | (locale.getlocale()[1] == "UTF-8"), "UTF-8 environment required" 125 | ) 126 | def test_notice_utf8(self): 127 | self._client(["--notice=UTF-8 \u2603"]) 128 | 129 | @pytest.mark.slow 130 | def test_packet_loss(self): 131 | # There is a small but non-zero chance that packet loss will prevent 132 | # any investigation replies from getting back to the client sock 133 | # between the 10 and 15 second mark, causing a test failure. 134 | # There's an even more minisculely small chance no simulated losses 135 | # will occur within the test period. 136 | sock_class = self._client( 137 | ["--flood", "--deadline=15", "--packet-loss=25"], test_stats=False 138 | ) 139 | self.assertGreaterEqual(sock_class.pings_transmitted, 100) 140 | self.assertEqual(sock_class.errors_received, 0) 141 | self.assertGreater(sock_class.lost_inbound, 0) 142 | self.assertGreater(sock_class.lost_outbound, 0) 143 | 144 | def test_random(self): 145 | self._client(["--send-random=32"]) 146 | 147 | def test_time(self): 148 | self._client(["--send-time"]) 149 | 150 | def test_module_init(self): 151 | self.assertTrue(_test_module_init(cli)) 152 | 153 | 154 | class TestCLIInet(TestCLI): 155 | def test_srv_addresses(self): 156 | self.bind_addresses = [] 157 | with unittest.mock.patch( 158 | "twoping.cli.TwoPing.get_srv_hosts", return_value=[("127.0.0.1", -1)] 159 | ): 160 | self._client( 161 | ["--interface-address=127.0.0.1", "--srv"], ["pssPCPkc3XlMTDZhclPV."] 162 | ) 163 | 164 | 165 | @unittest.skipUnless(hasattr(cli.socket, "AF_UNIX"), "UNIX environment required") 166 | class TestCLILoopback(TestCLI): 167 | class_args = ["--loopback"] 168 | 169 | def test_loopback_pairs(self): 170 | self._client(["--loopback-pairs=3"], pairs=3) 171 | -------------------------------------------------------------------------------- /tests/test_crc32.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import unittest 3 | 4 | from . import _test_module_init 5 | from twoping import crc32 6 | 7 | 8 | class TestCRC32(unittest.TestCase): 9 | def test_crc32(self): 10 | c = crc32.new(b"Data to hash") 11 | self.assertEqual(c.digest(), b"\x44\x9e\x0a\x5c") 12 | 13 | def test_hmac(self): 14 | h = hmac.new(b"Secret key", b"Data to hash", crc32) 15 | self.assertEqual(h.digest(), b"\x3c\xe1\xb6\xb9") 16 | 17 | def test_update(self): 18 | c = crc32.new() 19 | c.update(b"Data to hash") 20 | self.assertEqual(c.digest(), b"\x44\x9e\x0a\x5c") 21 | 22 | def test_hexdigest(self): 23 | c = crc32.new(b"Data to hash") 24 | self.assertEqual(c.hexdigest(), "449e0a5c") 25 | 26 | def test_hexdigest_zero_padding(self): 27 | c = crc32.new(b"jade") 28 | self.assertEqual(c.hexdigest(), "00835218") 29 | 30 | def test_clear(self): 31 | c = crc32.new(b"Data to hash") 32 | c.clear() 33 | self.assertEqual(c.digest(), b"\x00\x00\x00\x00") 34 | 35 | def test_zero_padding(self): 36 | c = crc32.new(b"jade") 37 | self.assertEqual(c.digest(), b"\x00\x83\x52\x18") 38 | 39 | def test_module_init(self): 40 | self.assertTrue(_test_module_init(crc32)) 41 | -------------------------------------------------------------------------------- /tests/test_packets.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from twoping import packets, utils 4 | 5 | 6 | class TestPacketsOpcodes(unittest.TestCase): 7 | def test_opcode_unknown(self): 8 | data = b"Unknown data" 9 | opcode = packets.Opcode() 10 | opcode.load(data) 11 | self.assertEqual(opcode.id, None) 12 | self.assertEqual(opcode.dump(), data) 13 | 14 | def test_opcode_reply_requested(self): 15 | data = b"" 16 | opcode = packets.OpcodeReplyRequested() 17 | opcode.load(data) 18 | self.assertEqual(opcode.id, 0x0001) 19 | self.assertEqual(opcode.dump(), data) 20 | 21 | def test_opcode_in_reply_to(self): 22 | data = b"\x01\x02\x03\x04\x05\x06" 23 | opcode = packets.OpcodeInReplyTo() 24 | opcode.load(data) 25 | self.assertEqual(opcode.id, 0x0002) 26 | self.assertEqual(opcode.dump(), data) 27 | 28 | def test_opcode_rtt_enclosed(self): 29 | data = b"\x12\x34\x56\x78" 30 | opcode = packets.OpcodeRTTEnclosed() 31 | opcode.load(data) 32 | self.assertEqual(opcode.id, 0x0004) 33 | self.assertEqual(opcode.dump(), data) 34 | 35 | def test_opcode_investigation_seen(self): 36 | data = b"\x01\x02\x03\x04\x05\x06\x11\x12\x13\x14\x15\x16" 37 | opcode = packets.OpcodeInvestigationSeen() 38 | opcode.load(data) 39 | self.assertEqual(opcode.id, 0x0008) 40 | self.assertEqual(opcode.dump(), data) 41 | 42 | def test_opcode_investigation_unseen(self): 43 | data = b"\x01\x02\x03\x04\x05\x06\x11\x12\x13\x14\x15\x16" 44 | opcode = packets.OpcodeInvestigationUnseen() 45 | opcode.load(data) 46 | self.assertEqual(opcode.id, 0x0010) 47 | self.assertEqual(opcode.dump(), data) 48 | 49 | def test_opcode_investigate(self): 50 | data = b"\x01\x02\x03\x04\x05\x06\x11\x12\x13\x14\x15\x16" 51 | opcode = packets.OpcodeInvestigate() 52 | opcode.load(data) 53 | self.assertEqual(opcode.id, 0x0020) 54 | self.assertEqual(opcode.dump(), data) 55 | 56 | def test_opcode_courtesy_expiration(self): 57 | data = b"\x01\x02\x03\x04\x05\x06\x11\x12\x13\x14\x15\x16" 58 | opcode = packets.OpcodeCourtesyExpiration() 59 | opcode.load(data) 60 | self.assertEqual(opcode.id, 0x0040) 61 | self.assertEqual(opcode.dump(), data) 62 | 63 | def test_opcode_hmac(self): 64 | data = b"\x00\x04\x01\x02\x03\x04" 65 | # The dump always includes a zeroed hash area 66 | data_out = b"\x00\x04\x00\x00\x00\x00" 67 | opcode = packets.OpcodeHMAC() 68 | opcode.load(data) 69 | self.assertEqual(opcode.id, 0x0080) 70 | self.assertEqual(opcode.dump(), data_out) 71 | 72 | def test_opcode_host_latency(self): 73 | data = b"\x12\x34\x56\x78" 74 | opcode = packets.OpcodeHostLatency() 75 | opcode.load(data) 76 | self.assertEqual(opcode.id, 0x0100) 77 | self.assertEqual(opcode.dump(), data) 78 | 79 | def test_opcode_encrypted(self): 80 | data = ( 81 | b"\x00\x01\xaa\xb1\xc0\x0f\x0f\x83\xd2\xc4\x78\xfe\xa1\xe2\x10\x62\x79\xee\x22\x52\x70\xf1\x93" 82 | b"\xee\xe9\x38\x47\xea\x11\xd1\xc9\x80\x3d\xe3" 83 | ) 84 | opcode = packets.OpcodeEncrypted() 85 | opcode.load(data) 86 | self.assertEqual(opcode.id, 0x0200) 87 | self.assertEqual(opcode.dump(), data) 88 | 89 | def test_opcode_extended(self): 90 | data = b"\x32\x50\x56\x4e\x00\x12\x54\x65\x73\x74\x20\x32\x70\x69\x6e\x67\x20\x76\x65\x72\x73\x69\x6f\x6e" 91 | opcode = packets.OpcodeExtended() 92 | opcode.load(data) 93 | self.assertEqual(opcode.id, 0x8000) 94 | self.assertEqual(opcode.dump(), data) 95 | 96 | def test_extended_unknown(self): 97 | data = b"\x55\x6e\x6b\x6e\x6f\x77\x6e\x20\x65\x78\x74\x65\x6e\x64\x65\x64\x20\x64\x61\x74\x61" 98 | opcode = packets.Extended() 99 | opcode.load(data) 100 | self.assertEqual(opcode.id, None) 101 | self.assertEqual(opcode.dump(), data) 102 | 103 | def test_extended_version(self): 104 | data = ( 105 | b"\x54\x65\x73\x74\x20\x32\x70\x69\x6e\x67\x20\x76\x65\x72\x73\x69\x6f\x6e" 106 | ) 107 | opcode = packets.ExtendedVersion() 108 | opcode.load(data) 109 | self.assertEqual(opcode.id, 0x3250564E) 110 | self.assertEqual(opcode.dump(), data) 111 | 112 | def test_extended_notice(self): 113 | data = b"\x4e\x6f\x74\x69\x63\x65\x20\x61\x6e\x6e\x6f\x75\x6e\x63\x65\x6d\x65\x6e\x74" 114 | opcode = packets.ExtendedNotice() 115 | opcode.load(data) 116 | self.assertEqual(opcode.id, 0xA837B44E) 117 | self.assertEqual(opcode.dump(), data) 118 | 119 | def test_extended_wallclock_load(self): 120 | opcode = packets.ExtendedWallClock() 121 | opcode.load(b"\x00\x03\x63\x73\xe7\x7a\xc2\x20") 122 | self.assertEqual(opcode.id, 0x64F69319) 123 | self.assertEqual(opcode.time_us, int(953774386.102816 * 1000000)) 124 | 125 | def test_extended_wallclock_dump(self): 126 | opcode = packets.ExtendedWallClock() 127 | opcode.time_us = int(1454187789.993266 * 1000000) 128 | self.assertEqual(opcode.id, 0x64F69319) 129 | self.assertEqual(opcode.dump(), b"\x00\x05\x2a\x93\x7a\xa8\xc5\x32") 130 | 131 | def test_extended_monotonicclock_load(self): 132 | opcode = packets.ExtendedMonotonicClock() 133 | opcode.load(b"\x7d\x67\x00\x03\x63\x73\xe7\x7a\xc2\x20") 134 | self.assertEqual(opcode.id, 0x771D8DFB) 135 | self.assertEqual(opcode.generation, 32103) 136 | self.assertEqual(opcode.time_us, int(953774386.102816 * 1000000)) 137 | 138 | def test_extended_monotonicclock_dump(self): 139 | opcode = packets.ExtendedMonotonicClock() 140 | opcode.generation = 9311 141 | opcode.time_us = int(1454187789.993266 * 1000000) 142 | self.assertEqual(opcode.id, 0x771D8DFB) 143 | self.assertEqual(opcode.dump(), b"\x24\x5f\x00\x05\x2a\x93\x7a\xa8\xc5\x32") 144 | 145 | def test_extended_random_load(self): 146 | random_data = b"\xf1\xfd\xf8\x9c\xe3\x9a\x87\x14" 147 | opcode = packets.ExtendedRandom() 148 | opcode.load(b"\x00\x03" + random_data) 149 | self.assertEqual(opcode.id, 0x2FF6AD68) 150 | self.assertTrue(opcode.is_hwrng) 151 | self.assertTrue(opcode.is_os) 152 | self.assertEqual(opcode.random_data, random_data) 153 | 154 | def test_extended_random_dump(self): 155 | random_data = b"\xbc\xde\xdc\xe5\xa8\x9a\xbd\x14" 156 | opcode = packets.ExtendedRandom() 157 | opcode.is_hwrng = True 158 | opcode.random_data = random_data 159 | self.assertEqual(opcode.id, 0x2FF6AD68) 160 | self.assertEqual(opcode.dump(), b"\x00\x01" + random_data) 161 | 162 | def test_extended_batteries_load(self): 163 | opcode = packets.ExtendedBatteryLevels() 164 | opcode.load(b"\x00\x02\x00\x00\xff\xff\x00\x01\xce\xa3") 165 | self.assertEqual(opcode.id, 0x88A1F7C7) 166 | self.assertEqual(opcode.batteries[0], 65535) 167 | self.assertEqual(opcode.batteries[1], 52899) 168 | 169 | def test_extended_batteries_dump(self): 170 | opcode = packets.ExtendedBatteryLevels() 171 | opcode.batteries = {0: 65535, 1: 52899} 172 | self.assertEqual(opcode.id, 0x88A1F7C7) 173 | self.assertEqual(opcode.dump(), b"\x00\x02\x00\x00\xff\xff\x00\x01\xce\xa3") 174 | 175 | @unittest.skipIf(isinstance(utils.AES, ImportError), "Crypto module required") 176 | def test_encrypted_encrypt_decrypt(self): 177 | key = b"Secret key" 178 | iv = b"\x32\xf0\x4a\x2f\xb3\x78\xe3\xf3\x73\x2b\x4a\x8c\x02\x74\xca\x0e" 179 | minimal_packet_data = ( 180 | b"\x32\x50\xda\x0a\x0e\xa5\x5b\xe2\x89\x1d\x00\x00\x00\x00\x00\x00" 181 | ) 182 | opcode = packets.OpcodeEncrypted() 183 | opcode.session = b"\x7a\xe3\xcb\xdf\x65\x4b\x86\x96" 184 | opcode.iv = iv 185 | opcode.method_index = 1 186 | opcode.encrypt(minimal_packet_data, key) 187 | self.assertEqual(opcode.decrypt(key), minimal_packet_data) 188 | 189 | 190 | class TestPacketsReference(unittest.TestCase): 191 | """Test protocol reference packets 192 | 193 | These tests replicate the reference packets included as examples in 194 | the 2ping protocol specification. 195 | """ 196 | 197 | def test_reference_1a(self): 198 | packet = packets.Packet() 199 | packet.message_id = b"\x00\x00\x00\x00\xa0\x01" 200 | expected = b"\x32\x50\x2d\xae\x00\x00\x00\x00\xa0\x01\x00\x00" 201 | self.assertEqual(packet.dump(), expected) 202 | 203 | def test_reference_2a(self): 204 | packet = packets.Packet() 205 | packet.message_id = b"\x00\x00\x00\x00\xa0\x01" 206 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 207 | expected = b"\x32\x50\x2d\xad\x00\x00\x00\x00\xa0\x01\x00\x01\x00\x00" 208 | self.assertEqual(packet.dump(), expected) 209 | 210 | def test_reference_2b(self): 211 | packet = packets.Packet() 212 | packet.message_id = b"\x00\x00\x00\x00\xb0\x01" 213 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 214 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = ( 215 | b"\x00\x00\x00\x00\xa0\x01" 216 | ) 217 | expected = b"\x32\x50\x7d\xa4\x00\x00\x00\x00\xb0\x01\x00\x02\x00\x06\x00\x00\x00\x00\xa0\x01" 218 | self.assertEqual(packet.dump(), expected) 219 | 220 | def test_reference_3a(self): 221 | packet = packets.Packet() 222 | packet.message_id = b"\x00\x00\x00\x00\xa0\x01" 223 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 224 | expected = b"\x32\x50\x2d\xad\x00\x00\x00\x00\xa0\x01\x00\x01\x00\x00" 225 | self.assertEqual(packet.dump(), expected) 226 | 227 | def test_reference_3b(self): 228 | packet = packets.Packet() 229 | packet.message_id = b"\x00\x00\x00\x00\xb0\x01" 230 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 231 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 232 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = ( 233 | b"\x00\x00\x00\x00\xa0\x01" 234 | ) 235 | expected = b"\x32\x50\x7d\xa3\x00\x00\x00\x00\xb0\x01\x00\x03\x00\x00\x00\x06\x00\x00\x00\x00\xa0\x01" 236 | self.assertEqual(packet.dump(), expected) 237 | 238 | def test_reference_3c(self): 239 | packet = packets.Packet() 240 | packet.message_id = b"\x00\x00\x00\x00\xa0\x02" 241 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 242 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = ( 243 | b"\x00\x00\x00\x00\xb0\x01" 244 | ) 245 | packet.opcodes[packets.OpcodeRTTEnclosed.id] = packets.OpcodeRTTEnclosed() 246 | packet.opcodes[packets.OpcodeRTTEnclosed.id].rtt_us = 12345 247 | expected = ( 248 | b"\x32\x50\x4d\x62\x00\x00\x00\x00\xa0\x02\x00\x06\x00\x06\x00\x00\x00\x00\xb0\x01\x00\x04" 249 | b"\x00\x00\x30\x39" 250 | ) 251 | self.assertEqual(packet.dump(), expected) 252 | 253 | def test_reference_4a(self): 254 | packet = packets.Packet() 255 | packet.message_id = b"\x00\x00\x00\x00\xa0\x01" 256 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 257 | expected = b"\x32\x50\x2d\xad\x00\x00\x00\x00\xa0\x01\x00\x01\x00\x00" 258 | self.assertEqual(packet.dump(), expected) 259 | 260 | def test_reference_4b(self): 261 | packet = packets.Packet() 262 | packet.message_id = b"\x00\x00\x00\x00\xa0\x02" 263 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 264 | packet.opcodes[packets.OpcodeInvestigate.id] = packets.OpcodeInvestigate() 265 | packet.opcodes[packets.OpcodeInvestigate.id].message_ids.append( 266 | b"\x00\x00\x00\x00\xa0\x01" 267 | ) 268 | expected = b"\x32\x50\x8d\x81\x00\x00\x00\x00\xa0\x02\x00\x21\x00\x00\x00\x08\x00\x01\x00\x00\x00\x00\xa0\x01" 269 | self.assertEqual(packet.dump(), expected) 270 | 271 | def test_reference_4c(self): 272 | packet = packets.Packet() 273 | packet.message_id = b"\x00\x00\x00\x00\xb0\x02" 274 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 275 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 276 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = ( 277 | b"\x00\x00\x00\x00\xa0\x02" 278 | ) 279 | packet.opcodes[packets.OpcodeInvestigationSeen.id] = ( 280 | packets.OpcodeInvestigationSeen() 281 | ) 282 | packet.opcodes[packets.OpcodeInvestigationSeen.id].message_ids.append( 283 | b"\x00\x00\x00\x00\xa0\x01" 284 | ) 285 | expected = ( 286 | b"\x32\x50\xdd\x8e\x00\x00\x00\x00\xb0\x02\x00\x0b\x00\x00\x00\x06\x00\x00\x00\x00\xa0\x02" 287 | b"\x00\x08\x00\x01\x00\x00\x00\x00\xa0\x01" 288 | ) 289 | self.assertEqual(packet.dump(), expected) 290 | 291 | def test_reference_4d(self): 292 | packet = packets.Packet() 293 | packet.message_id = b"\x00\x00\x00\x00\xa0\x03" 294 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 295 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = ( 296 | b"\x00\x00\x00\x00\xb0\x02" 297 | ) 298 | packet.opcodes[packets.OpcodeRTTEnclosed.id] = packets.OpcodeRTTEnclosed() 299 | packet.opcodes[packets.OpcodeRTTEnclosed.id].rtt_us = 12345 300 | expected = ( 301 | b"\x32\x50\x4d\x60\x00\x00\x00\x00\xa0\x03\x00\x06\x00\x06\x00\x00\x00\x00\xb0\x02\x00\x04" 302 | b"\x00\x00\x30\x39" 303 | ) 304 | self.assertEqual(packet.dump(), expected) 305 | 306 | def test_reference_5a(self): 307 | packet = packets.Packet() 308 | packet.message_id = b"\x00\x00\x00\x00\xa0\x01" 309 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 310 | expected = b"\x32\x50\x2d\xad\x00\x00\x00\x00\xa0\x01\x00\x01\x00\x00" 311 | self.assertEqual(packet.dump(), expected) 312 | 313 | def test_reference_5b(self): 314 | packet = packets.Packet() 315 | packet.message_id = b"\x00\x00\x00\x00\xa0\x02" 316 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 317 | packet.opcodes[packets.OpcodeInvestigate.id] = packets.OpcodeInvestigate() 318 | packet.opcodes[packets.OpcodeInvestigate.id].message_ids.append( 319 | b"\x00\x00\x00\x00\xa0\x01" 320 | ) 321 | expected = b"\x32\x50\x8d\x81\x00\x00\x00\x00\xa0\x02\x00\x21\x00\x00\x00\x08\x00\x01\x00\x00\x00\x00\xa0\x01" 322 | self.assertEqual(packet.dump(), expected) 323 | 324 | def test_reference_5c(self): 325 | packet = packets.Packet() 326 | packet.message_id = b"\x00\x00\x00\x00\xb0\x01" 327 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 328 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 329 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = ( 330 | b"\x00\x00\x00\x00\xa0\x02" 331 | ) 332 | packet.opcodes[packets.OpcodeInvestigationUnseen.id] = ( 333 | packets.OpcodeInvestigationUnseen() 334 | ) 335 | packet.opcodes[packets.OpcodeInvestigationUnseen.id].message_ids.append( 336 | b"\x00\x00\x00\x00\xa0\x01" 337 | ) 338 | expected = ( 339 | b"\x32\x50\xdd\x87\x00\x00\x00\x00\xb0\x01\x00\x13\x00\x00\x00\x06\x00\x00\x00\x00\xa0\x02" 340 | b"\x00\x08\x00\x01\x00\x00\x00\x00\xa0\x01" 341 | ) 342 | self.assertEqual(packet.dump(), expected) 343 | 344 | def test_reference_5d(self): 345 | packet = packets.Packet() 346 | packet.message_id = b"\x00\x00\x00\x00\xa0\x03" 347 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 348 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = ( 349 | b"\x00\x00\x00\x00\xb0\x01" 350 | ) 351 | packet.opcodes[packets.OpcodeRTTEnclosed.id] = packets.OpcodeRTTEnclosed() 352 | packet.opcodes[packets.OpcodeRTTEnclosed.id].rtt_us = 12345 353 | expected = ( 354 | b"\x32\x50\x4d\x61\x00\x00\x00\x00\xa0\x03\x00\x06\x00\x06\x00\x00\x00\x00\xb0\x01\x00\x04" 355 | b"\x00\x00\x30\x39" 356 | ) 357 | self.assertEqual(packet.dump(), expected) 358 | 359 | def test_reference_6a(self): 360 | packet = packets.Packet() 361 | packet.message_id = b"\x00\x00\x00\x00\xa0\x01" 362 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 363 | expected = b"\x32\x50\x2d\xad\x00\x00\x00\x00\xa0\x01\x00\x01\x00\x00" 364 | self.assertEqual(packet.dump(), expected) 365 | 366 | def test_reference_6b(self): 367 | packet = packets.Packet() 368 | packet.message_id = b"\x00\x00\x00\x00\xa0\x02" 369 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 370 | expected = b"\x32\x50\x2d\xac\x00\x00\x00\x00\xa0\x02\x00\x01\x00\x00" 371 | self.assertEqual(packet.dump(), expected) 372 | 373 | def test_reference_6c(self): 374 | packet = packets.Packet() 375 | packet.message_id = b"\x00\x00\x00\x00\xa0\x03" 376 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 377 | expected = b"\x32\x50\x2d\xab\x00\x00\x00\x00\xa0\x03\x00\x01\x00\x00" 378 | self.assertEqual(packet.dump(), expected) 379 | 380 | def test_reference_6d(self): 381 | packet = packets.Packet() 382 | packet.message_id = b"\x00\x00\x00\x00\xb0\x02" 383 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 384 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 385 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = ( 386 | b"\x00\x00\x00\x00\xa0\x03" 387 | ) 388 | expected = b"\x32\x50\x7d\xa0\x00\x00\x00\x00\xb0\x02\x00\x03\x00\x00\x00\x06\x00\x00\x00\x00\xa0\x03" 389 | self.assertEqual(packet.dump(), expected) 390 | 391 | def test_reference_6e(self): 392 | packet = packets.Packet() 393 | packet.message_id = b"\x00\x00\x00\x00\xa0\x04" 394 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 395 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = ( 396 | b"\x00\x00\x00\x00\xb0\x02" 397 | ) 398 | packet.opcodes[packets.OpcodeRTTEnclosed.id] = packets.OpcodeRTTEnclosed() 399 | packet.opcodes[packets.OpcodeRTTEnclosed.id].rtt_us = 12823 400 | expected = ( 401 | b"\x32\x50\x4b\x81\x00\x00\x00\x00\xa0\x04\x00\x06\x00\x06\x00\x00\x00\x00\xb0\x02\x00\x04" 402 | b"\x00\x00\x32\x17" 403 | ) 404 | self.assertEqual(packet.dump(), expected) 405 | 406 | def test_reference_6f(self): 407 | packet = packets.Packet() 408 | packet.message_id = b"\x00\x00\x00\x00\xa0\x0a" 409 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 410 | packet.opcodes[packets.OpcodeInvestigate.id] = packets.OpcodeInvestigate() 411 | packet.opcodes[packets.OpcodeInvestigate.id].message_ids.append( 412 | b"\x00\x00\x00\x00\xa0\x01" 413 | ) 414 | packet.opcodes[packets.OpcodeInvestigate.id].message_ids.append( 415 | b"\x00\x00\x00\x00\xa0\x02" 416 | ) 417 | expected = ( 418 | b"\x32\x50\xed\x6f\x00\x00\x00\x00\xa0\x0a\x00\x21\x00\x00\x00\x0e\x00\x02\x00\x00\x00\x00" 419 | b"\xa0\x01\x00\x00\x00\x00\xa0\x02" 420 | ) 421 | self.assertEqual(packet.dump(), expected) 422 | 423 | def test_reference_6g(self): 424 | packet = packets.Packet() 425 | packet.message_id = b"\x00\x00\x00\x00\xb0\x06" 426 | packet.opcodes[packets.OpcodeReplyRequested.id] = packets.OpcodeReplyRequested() 427 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 428 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = ( 429 | b"\x00\x00\x00\x00\xa0\x0a" 430 | ) 431 | packet.opcodes[packets.OpcodeInvestigationSeen.id] = ( 432 | packets.OpcodeInvestigationSeen() 433 | ) 434 | packet.opcodes[packets.OpcodeInvestigationSeen.id].message_ids.append( 435 | b"\x00\x00\x00\x00\xa0\x01" 436 | ) 437 | packet.opcodes[packets.OpcodeInvestigationUnseen.id] = ( 438 | packets.OpcodeInvestigationUnseen() 439 | ) 440 | packet.opcodes[packets.OpcodeInvestigationUnseen.id].message_ids.append( 441 | b"\x00\x00\x00\x00\xa0\x02" 442 | ) 443 | packet.opcodes[packets.OpcodeInvestigate.id] = packets.OpcodeInvestigate() 444 | packet.opcodes[packets.OpcodeInvestigate.id].message_ids.append( 445 | b"\x00\x00\x00\x00\xb0\x02" 446 | ) 447 | expected = ( 448 | b"\x32\x50\x8d\x3b\x00\x00\x00\x00\xb0\x06\x00\x3b\x00\x00\x00\x06\x00\x00\x00\x00\xa0\x0a" 449 | b"\x00\x08\x00\x01\x00\x00\x00\x00\xa0\x01\x00\x08\x00\x01\x00\x00\x00\x00\xa0\x02\x00\x08" 450 | b"\x00\x01\x00\x00\x00\x00\xb0\x02" 451 | ) 452 | self.assertEqual(packet.dump(), expected) 453 | 454 | def test_reference_6h(self): 455 | packet = packets.Packet() 456 | packet.message_id = b"\x00\x00\x00\x00\xa0\x0b" 457 | packet.opcodes[packets.OpcodeInReplyTo.id] = packets.OpcodeInReplyTo() 458 | packet.opcodes[packets.OpcodeInReplyTo.id].message_id = ( 459 | b"\x00\x00\x00\x00\xb0\x06" 460 | ) 461 | packet.opcodes[packets.OpcodeRTTEnclosed.id] = packets.OpcodeRTTEnclosed() 462 | packet.opcodes[packets.OpcodeRTTEnclosed.id].rtt_us = 13112 463 | packet.opcodes[packets.OpcodeInvestigationSeen.id] = ( 464 | packets.OpcodeInvestigationSeen() 465 | ) 466 | packet.opcodes[packets.OpcodeInvestigationSeen.id].message_ids.append( 467 | b"\x00\x00\x00\x00\xb0\x02" 468 | ) 469 | expected = ( 470 | b"\x32\x50\x9a\x41\x00\x00\x00\x00\xa0\x0b\x00\x0e\x00\x06\x00\x00\x00\x00\xb0\x06\x00\x04" 471 | b"\x00\x00\x33\x38\x00\x08\x00\x01\x00\x00\x00\x00\xb0\x02" 472 | ) 473 | self.assertEqual(packet.dump(), expected) 474 | -------------------------------------------------------------------------------- /tests/test_python.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | 4 | 5 | class TestPython(unittest.TestCase): 6 | def test_monotonic(self): 7 | val1 = time.monotonic() 8 | val2 = time.monotonic() 9 | self.assertGreaterEqual(val2, val1) 10 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import unittest 3 | 4 | from twoping import packets, utils 5 | 6 | 7 | class TestUtils(unittest.TestCase): 8 | def test_twoping_checksum_hello(self): 9 | data = b"Hello World" 10 | self.assertEqual(utils.twoping_checksum(data), 0xAE31) 11 | 12 | def test_twoping_checksum_junk(self): 13 | data = ( 14 | b"\x45\xc6\xca\x92\x10\x0e\xb1\xcf\x98\x88\x0b\x42\xc0\xf8\x58\xac\xd9\x81\x76\xc0\x45\x8c\x6f" 15 | b"\x04\x9c\x0e\x93\xb3\x49\x3d\x38\x6d\xb7\xd5\x86\xa7\x66\x2c\x32\x15\xd9\x7f\xad\x3e\x1a\xe0" 16 | b"\x39\x89\x4a\xdd\x0b\x28\xa0\x07\x61\x47\x54\xc6\x50\x04\x2d\x28\x4e\x48\x3c\x40\xc7\xdb\x82" 17 | b"\xfa\x0b\xab\x37\x6e\x23\x5e\x80\xab\xb9\xe5\x05\x14\xb1\xfc\x72\x54\x37\x8a\x66\xf2\x51\xa1" 18 | b"\x77\xda\xd5\x88\xd5\x38\x43\x31\x30\xe7\x1e\x7a\x2b\xc8\x14\x0d\x4b\x7c\x33\x9a\x9b\xc1\xc1" 19 | b"\xc8\xc5\xfe\x5a\x48\x44\x1b\x87\x9d\x24\xd9\x22\x7d" 20 | ) 21 | self.assertEqual(utils.twoping_checksum(data), 0x4A06) 22 | 23 | def test_twoping_checksum_packet(self): 24 | data = bytearray( 25 | b"\x32\x50\x8d\x3b\x00\x00\x00\x00\xb0\x06\x00\x3b\x00\x00\x00\x06\x00\x00\x00\x00\xa0\x0a\x00" 26 | b"\x08\x00\x01\x00\x00\x00\x00\xa0\x01\x00\x08\x00\x01\x00\x00\x00\x00\xa0\x02\x00\x08\x00\x01" 27 | b"\x00\x00\x00\x00\xb0\x02" 28 | ) 29 | data[2] = 0 30 | data[3] = 0 31 | self.assertEqual(utils.twoping_checksum(data), 0x8D3B) 32 | 33 | def test_twoping_checksum_iter(self): 34 | i = 65535 35 | for x in range(256): 36 | for y in range(256): 37 | data = bytes([x, y]) 38 | checksum = utils.twoping_checksum(data) 39 | self.assertEqual(checksum, i) 40 | i = i - 1 41 | if i == 0: 42 | i = 65535 43 | 44 | def test_fuzz_bytearray(self): 45 | data = packets.Packet().dump() 46 | data_fuzzed = bytearray(data) 47 | utils.fuzz_bytearray(data_fuzzed, 100) 48 | self.assertNotEqual(data, bytes(data_fuzzed)) 49 | 50 | def test_fuzz_bytearray_zero(self): 51 | data = packets.Packet().dump() 52 | data_fuzzed = bytearray(data) 53 | utils.fuzz_bytearray(data_fuzzed, 0) 54 | self.assertEqual(data, bytes(data_fuzzed)) 55 | 56 | def test_fuzz_packet(self): 57 | data = packets.Packet().dump() 58 | data_fuzzed = utils.fuzz_packet(data, 100) 59 | self.assertNotEqual(data, data_fuzzed) 60 | 61 | def test_fuzz_packet_zero(self): 62 | data = packets.Packet().dump() 63 | data_fuzzed = utils.fuzz_packet(data, 0) 64 | self.assertEqual(data, data_fuzzed) 65 | 66 | def test_div0(self): 67 | self.assertEqual(utils.div0(10, 5), 10 / 5) 68 | self.assertEqual(utils.div0(5, 0), 0) 69 | self.assertEqual(utils.div0(0, 0), 0) 70 | 71 | def test_npack(self): 72 | self.assertEqual(utils.npack(1), b"\x01") 73 | self.assertEqual(utils.npack(1234), b"\x04\xd2") 74 | self.assertEqual(utils.npack(123456), b"\x01\xe2\x40") 75 | 76 | def test_npack_minimum(self): 77 | self.assertEqual(utils.npack(1, 2), b"\x00\x01") 78 | self.assertEqual(utils.npack(1234, 2), b"\x04\xd2") 79 | self.assertEqual(utils.npack(123456, 4), b"\x00\x01\xe2\x40") 80 | 81 | def test_nunpack(self): 82 | self.assertEqual(utils.nunpack(b"\x01"), 1) 83 | self.assertEqual(utils.nunpack(b"\x04\xd2"), 1234) 84 | self.assertEqual(utils.nunpack(b"\x00\x00\x04\xd2"), 1234) 85 | self.assertEqual(utils.nunpack(b"\x01\xe2\x40"), 123456) 86 | self.assertEqual(utils.nunpack(b"\x00\x01\xe2\x40"), 123456) 87 | 88 | def test_platform_info(self): 89 | self.assertIn(platform.system(), utils.platform_info()) 90 | 91 | def test_stats_time(self): 92 | self.assertEqual(utils.stats_time(123.45), "2m 3s 450ms") 93 | self.assertEqual(utils.stats_time(0.45678), "456ms") 94 | self.assertEqual(utils.stats_time(123456789000), "3914y 288d 30m") 95 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py-black, py-flake8, py-pytest 3 | 4 | [testenv:py-black] 5 | commands = python -mblack --check . 6 | deps = black 7 | 8 | [testenv:py-black-reformat] 9 | commands = python -mblack . 10 | deps = black 11 | 12 | [testenv:py-flake8] 13 | commands = python -mflake8 14 | deps = flake8 15 | 16 | [testenv:py-pytest] 17 | commands = python -mpytest --cov=twoping --cov-report=term-missing 18 | deps = pytest 19 | pytest-cov 20 | -r{toxinidir}/requirements.txt 21 | 22 | [testenv:py-pytest-quick] 23 | commands = python -mpytest -m "not slow" 24 | deps = pytest 25 | -r{toxinidir}/requirements.txt 26 | 27 | [flake8] 28 | exclude = 29 | .git, 30 | __pycache__, 31 | .tox, 32 | # TODO: remove C901 once complexity is reduced 33 | ignore = C901,E203,E231,W503 34 | max-line-length = 120 35 | max-complexity = 10 36 | 37 | [pytest] 38 | markers = 39 | slow 40 | -------------------------------------------------------------------------------- /twoping/__init__.py: -------------------------------------------------------------------------------- 1 | # 2ping - A bi-directional ping utility 2 | # Copyright (C) 2010-2021 Ryan Finnie 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | import sys 6 | 7 | 8 | __version__ = "4.5.1" 9 | assert sys.version_info > (3, 6) 10 | -------------------------------------------------------------------------------- /twoping/args.py: -------------------------------------------------------------------------------- 1 | # 2ping - A bi-directional ping utility 2 | # Copyright (C) 2010-2021 Ryan Finnie 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | import argparse 6 | import os 7 | import socket 8 | import sys 9 | import types 10 | 11 | from . import __version__, packets 12 | from .utils import _, AES 13 | 14 | 15 | def _type_nagios(string): 16 | (warn_rta, warn_loss, crit_rta, crit_loss) = string.split(",", 3) 17 | if (warn_loss[-1:] != "%") or (crit_loss[-1:] != "%"): 18 | raise argparse.ArgumentTypeError(_("Invalid limits")) 19 | return types.SimpleNamespace( 20 | warn_rta=float(warn_rta), 21 | warn_loss=float(warn_loss[:-1]), 22 | crit_rta=float(crit_rta), 23 | crit_loss=float(crit_loss[:-1]), 24 | ) 25 | 26 | 27 | def _type_packet_loss(string): 28 | if ":" in string: 29 | (v_out, v_in) = string.split(":", 1) 30 | else: 31 | v_out = v_in = string 32 | return types.SimpleNamespace(out_pct=float(v_out), in_pct=float(v_in)) 33 | 34 | 35 | def parse_args(argv=None): 36 | if argv is None: 37 | argv = sys.argv 38 | 39 | if argv[0].endswith("2ping6"): 40 | ipv6_default = True 41 | else: 42 | ipv6_default = False 43 | 44 | parser = argparse.ArgumentParser( 45 | description="2ping ({})".format(__version__), 46 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 47 | prog=os.path.basename(argv[0]), 48 | ) 49 | parser.add_argument( 50 | "--version", 51 | "-V", 52 | action="version", 53 | version=__version__, 54 | help=_("report the program version"), 55 | ) 56 | 57 | # Positionals 58 | parser.add_argument( 59 | "host", type=str, default=None, nargs="*", help=_("host to ping") 60 | ) 61 | 62 | # ping-compatible options 63 | ping_group = parser.add_argument_group(title=_("ping-compatible options")) 64 | 65 | ping_group.add_argument( 66 | "--audible", "-a", dest="audible", action="store_true", help=_("audible ping") 67 | ) 68 | ping_group.add_argument( 69 | "--adaptive", 70 | "-A", 71 | dest="adaptive", 72 | action="store_true", 73 | help=_("adaptive RTT ping"), 74 | ) 75 | ping_group.add_argument( 76 | "--count", "-c", dest="count", type=int, help=_("number of pings to send") 77 | ) 78 | ping_group.add_argument( 79 | "--flood", "-f", dest="flood", action="store_true", help=_("flood mode") 80 | ) 81 | ping_group.add_argument( 82 | "--interval", 83 | "-i", 84 | dest="interval", 85 | type=float, 86 | default=1.0, 87 | help=_("seconds between pings"), 88 | metavar="SECONDS", 89 | ) 90 | ping_group.add_argument( 91 | "--interface-address", 92 | "-I", 93 | dest="interface_address", 94 | type=str, 95 | action="append", 96 | default=[], 97 | help=_("interface bind address"), 98 | metavar="ADDRESS", 99 | ) 100 | ping_group.add_argument( 101 | "--preload", 102 | "-l", 103 | dest="preload", 104 | type=int, 105 | default=1, 106 | help=_("number of pings to send at start"), 107 | metavar="COUNT", 108 | ) 109 | ping_group.add_argument( 110 | "--pattern", 111 | "-p", 112 | dest="pattern", 113 | type=lambda string: bytes.fromhex(string), 114 | default="00", 115 | help=_("hex pattern for padding"), 116 | metavar="HEX_BYTES", 117 | ) 118 | ping_group.add_argument( 119 | "--quiet", "-q", dest="quiet", action="store_true", help=_("quiet mode") 120 | ) 121 | ping_group.add_argument( 122 | "--packetsize-compat", 123 | "-s", 124 | dest="packetsize_compat", 125 | type=int, 126 | help=_("packet size (ping compatible)"), 127 | metavar="BYTES", 128 | ) 129 | ping_group.add_argument( 130 | "--verbose", "-v", dest="verbose", action="store_true", help=_("verbose mode") 131 | ) 132 | ping_group.add_argument( 133 | "--deadline", 134 | "-w", 135 | dest="deadline", 136 | type=float, 137 | help=_("maximum run time"), 138 | metavar="SECONDS", 139 | ) 140 | 141 | # 2ping options 142 | twoping_group = parser.add_argument_group(title=_("2ping-specific options")) 143 | 144 | twoping_group.add_argument( 145 | "--auth", type=str, help=_("HMAC authentication key"), metavar="KEY" 146 | ) 147 | twoping_group.add_argument( 148 | "--auth-digest", 149 | type=str, 150 | default="hmac-md5", 151 | choices=[x[2].lower() for x in packets.OpcodeHMAC().digest_map.values()], 152 | help=_("HMAC authentication digest"), 153 | metavar="DIGEST", 154 | ) 155 | twoping_group.add_argument("--debug", action="store_true", help=_("debug mode")) 156 | twoping_group.add_argument( 157 | "--encrypt", type=str, help=_("Encryption key"), metavar="KEY" 158 | ) 159 | twoping_group.add_argument( 160 | "--encrypt-method", 161 | type=str, 162 | default="hkdf-aes256-cbc", 163 | choices=[x[0].lower() for x in packets.OpcodeEncrypted().method_map.values()], 164 | help=_("Encryption method"), 165 | metavar="METHOD", 166 | ) 167 | twoping_group.add_argument( 168 | "--fuzz", type=float, help=_("incoming fuzz percentage"), metavar="PERCENT" 169 | ) 170 | twoping_group.add_argument( 171 | "--inquire-wait", 172 | type=float, 173 | default=10.0, 174 | help=_("maximum time before loss inquiries"), 175 | metavar="SECONDS", 176 | ) 177 | twoping_group.add_argument( 178 | "--ipv4", "-4", action="store_true", help=_("force IPv4") 179 | ) 180 | twoping_group.add_argument( 181 | "--ipv6", "-6", action="store_true", default=ipv6_default, help=_("force IPv6") 182 | ) 183 | twoping_group.add_argument("--listen", action="store_true", help=_("listen mode")) 184 | twoping_group.add_argument( 185 | "--loopback", action="store_true", help=_("UNIX loopback test mode") 186 | ) 187 | twoping_group.add_argument( 188 | "--loopback-pairs", 189 | type=int, 190 | default=1, 191 | help=_("number of loopback pairs to create"), 192 | metavar="PAIRS", 193 | ) 194 | twoping_group.add_argument( 195 | "--max-packet-size", 196 | type=int, 197 | default=512, 198 | help=_("maximum packet size"), 199 | metavar="BYTES", 200 | ) 201 | twoping_group.add_argument( 202 | "--min-packet-size", 203 | type=int, 204 | default=128, 205 | help=_("minimum packet size"), 206 | metavar="BYTES", 207 | ) 208 | twoping_group.add_argument( 209 | "--nagios", 210 | type=_type_nagios, 211 | help=_("nagios-compatible output"), 212 | metavar="WRTA,WLOSS%,CRTA,CLOSS%", 213 | ) 214 | twoping_group.add_argument( 215 | "--no-3way", action="store_true", help=_("do not send 3-way pings") 216 | ) 217 | twoping_group.add_argument( 218 | "--no-match-packet-size", 219 | action="store_true", 220 | help=_("do not match packet size of peer"), 221 | ) 222 | twoping_group.add_argument( 223 | "--no-send-version", 224 | action="store_true", 225 | help=_("do not send program version to peers"), 226 | ) 227 | twoping_group.add_argument( 228 | "--notice", type=str, help=_("arbitrary notice text"), metavar="TEXT" 229 | ) 230 | twoping_group.add_argument( 231 | "--packet-loss", 232 | type=_type_packet_loss, 233 | help=_("percentage simulated packet loss"), 234 | metavar="OUT:IN", 235 | ) 236 | twoping_group.add_argument( 237 | "--port", type=str, default="15998", help=_("port to connect / bind to") 238 | ) 239 | twoping_group.add_argument( 240 | "--send-monotonic-clock", 241 | action="store_true", 242 | help=_("send monotonic clock to peers"), 243 | ) 244 | twoping_group.add_argument( 245 | "--send-random", type=int, help=_("send random data to peers"), metavar="BYTES" 246 | ) 247 | twoping_group.add_argument( 248 | "--send-time", action="store_true", help=_("send wall clock time to peers") 249 | ) 250 | twoping_group.add_argument( 251 | "--stats", type=float, help=_("print recurring statistics"), metavar="SECONDS" 252 | ) 253 | twoping_group.add_argument( 254 | "--srv", action="store_true", help=_("lookup SRV records in client mode") 255 | ) 256 | twoping_group.add_argument( 257 | "--srv-service", 258 | type=str, 259 | default="2ping", 260 | help=_("service name for SRV lookups"), 261 | ) 262 | twoping_group.add_argument( 263 | "--subtract-peer-host-latency", 264 | action="store_true", 265 | help=_("subtract peer host latency from RTT calculations, if sent"), 266 | ) 267 | 268 | # ping-compatible ignored options 269 | for opt in "b|B|d|L|n|R|r|U".split("|"): 270 | parser.add_argument( 271 | "-{}".format(opt), 272 | action="store_true", 273 | dest="ignored_{}".format(opt), 274 | help=argparse.SUPPRESS, 275 | ) 276 | for opt in "F|Q|S|t|T|M|W".split("|"): 277 | parser.add_argument( 278 | "-{}".format(opt), 279 | type=str, 280 | default=None, 281 | dest="ignored_{}".format(opt), 282 | help=argparse.SUPPRESS, 283 | ) 284 | 285 | # Deprecated ignored options 286 | parser.add_argument("--all-interfaces", action="store_true", help=argparse.SUPPRESS) 287 | 288 | args = parser.parse_args(args=argv[1:]) 289 | 290 | if (not args.listen) and (not args.host) and (not args.loopback): 291 | parser.print_help() 292 | parser.exit() 293 | if args.loopback and not hasattr(socket, "AF_UNIX"): 294 | parser.error("--loopback not supported on non-UNIX platforms") 295 | if args.nagios: 296 | args.quiet = True 297 | if (not args.count) and (not args.deadline): 298 | args.count = 5 299 | if args.packetsize_compat: 300 | args.min_packet_size = args.packetsize_compat + 8 301 | if args.max_packet_size < args.min_packet_size: 302 | parser.error(_("Maximum packet size must be at least minimum packet size")) 303 | if args.max_packet_size < 64: 304 | parser.error(_("Maximum packet size must be at least 64")) 305 | 306 | if args.encrypt and isinstance(AES, ImportError): 307 | parser.error(_("Crypto module required for encryption")) 308 | 309 | if args.debug: 310 | args.verbose = True 311 | 312 | return args 313 | -------------------------------------------------------------------------------- /twoping/crc32.py: -------------------------------------------------------------------------------- 1 | # 2ping - A bi-directional ping utility 2 | # Copyright (C) 2010-2021 Ryan Finnie 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | import binascii 6 | import copy 7 | import sys 8 | 9 | digest_size = 4 10 | 11 | 12 | class CRC32: 13 | digest_size = 4 14 | block_size = 64 15 | _crc = 0 16 | 17 | def __init__(self, buf=None): 18 | if buf is not None: 19 | self.update(buf) 20 | 21 | def copy(self): 22 | return copy.copy(self) 23 | 24 | def update(self, buf): 25 | self._crc = binascii.crc32(buf, self._crc) 26 | 27 | def clear(self): 28 | self._crc = 0 29 | 30 | def digest(self): 31 | i = self._crc & 0xFFFFFFFF 32 | out = bytearray() 33 | while i >= 256: 34 | out.insert(0, i & 0xFF) 35 | i = i >> 8 36 | out.insert(0, i) 37 | out_len = len(out) 38 | if out_len < 4: 39 | out = bytearray(4 - out_len) + out 40 | return out 41 | 42 | def hexdigest(self): 43 | return self.digest().hex() 44 | 45 | 46 | def new(buf=None): 47 | return CRC32(buf) 48 | 49 | 50 | def main(argv): 51 | if argv is None: 52 | argv = sys.argv 53 | 54 | files = argv[1:] 55 | if len(files) == 0: 56 | files = ["-"] 57 | 58 | for file in files: 59 | c = new() 60 | if file == "-": 61 | for buf in sys.stdin.buffer.readlines(): 62 | c.update(buf) 63 | else: 64 | with open(file, "rb") as f: 65 | for buf in f.readlines(): 66 | c.update(buf) 67 | print("{}\t{}".format(c.hexdigest(), file)) 68 | 69 | 70 | def module_init(): 71 | if __name__ == "__main__": 72 | sys.exit(main(sys.argv)) 73 | 74 | 75 | module_init() 76 | -------------------------------------------------------------------------------- /twoping/packets.py: -------------------------------------------------------------------------------- 1 | # 2ping - A bi-directional ping utility 2 | # Copyright (C) 2010-2021 Ryan Finnie 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | import hashlib 6 | import hmac 7 | from math import ceil 8 | import time 9 | 10 | from . import crc32 11 | from .utils import AES, npack, nunpack, random, twoping_checksum 12 | 13 | 14 | class Extended: 15 | id = None 16 | 17 | def __init__(self): 18 | self.data = b"" 19 | 20 | def __repr__(self): 21 | if self.id is None: 22 | return "".format(len(self.data)) 23 | else: 24 | return "".format(self.id, len(self.data)) 25 | 26 | def load(self, data): 27 | self.data = data 28 | 29 | def dump(self): 30 | return self.data 31 | 32 | 33 | class ExtendedText(Extended): 34 | def __init__(self): 35 | self.text = str() 36 | 37 | def __repr__(self): 38 | return "".format(self.text) 39 | 40 | def load(self, data): 41 | self.text = data.decode("UTF-8") 42 | 43 | def dump(self, max_length=None): 44 | text_bytes = self.text.encode("UTF-8") 45 | if (max_length is not None) and (max_length < len(text_bytes)): 46 | return None 47 | return text_bytes 48 | 49 | 50 | class ExtendedVersion(ExtendedText): 51 | id = 0x3250564E 52 | 53 | def __repr__(self): 54 | return "".format(self.text) 55 | 56 | 57 | class ExtendedNotice(ExtendedText): 58 | id = 0xA837B44E 59 | 60 | def __repr__(self): 61 | return "".format(self.text) 62 | 63 | 64 | class ExtendedWallClock(Extended): 65 | id = 0x64F69319 66 | 67 | def __init__(self): 68 | self.time_us = 0 69 | 70 | def __repr__(self): 71 | return "".format( 72 | time.strftime("%c", time.gmtime(self.time_us / 1000000.0)) 73 | ) 74 | 75 | def load(self, data): 76 | self.time_us = nunpack(data[0:8]) 77 | 78 | def dump(self, max_length=None): 79 | if (max_length is not None) and (max_length < 8): 80 | return None 81 | return npack(self.time_us, 8) 82 | 83 | 84 | class ExtendedMonotonicClock(Extended): 85 | id = 0x771D8DFB 86 | 87 | def __init__(self): 88 | self.generation = 0 89 | self.time_us = 0 90 | 91 | def __repr__(self): 92 | return "".format( 93 | (self.time_us / 1000000.0), self.generation 94 | ) 95 | 96 | def load(self, data): 97 | self.generation = nunpack(data[0:2]) 98 | self.time_us = nunpack(data[2:10]) 99 | 100 | def dump(self, max_length=None): 101 | if (max_length is not None) and (max_length < 10): 102 | return None 103 | return npack(self.generation, 2) + npack(self.time_us, 8) 104 | 105 | 106 | class ExtendedRandom(Extended): 107 | id = 0x2FF6AD68 108 | 109 | def __init__(self): 110 | self.is_hwrng = False 111 | self.is_os = False 112 | self.random_data = b"" 113 | 114 | def __repr__(self): 115 | return "".format( 116 | repr(self.random_data), 117 | len(self.random_data), 118 | repr(self.is_hwrng), 119 | repr(self.is_os), 120 | ) 121 | 122 | def load(self, data): 123 | flags = nunpack(data[0:2]) 124 | self.is_hwrng = bool(flags & 0x0001) 125 | self.is_os = bool(flags & 0x0002) 126 | self.random_data = data[2:] 127 | 128 | def dump(self, max_length=None): 129 | random_data = self.random_data 130 | if len(random_data) == 0: 131 | return None 132 | if max_length is not None: 133 | if max_length < 3: 134 | return None 135 | if max_length < (len(random_data) - 2): 136 | random_data = random_data[0 : max_length - 2] 137 | flags = 0 138 | if self.is_hwrng: 139 | flags = flags | 0x0001 140 | if self.is_os: 141 | flags = flags | 0x0002 142 | return npack(flags, 2) + random_data 143 | 144 | 145 | class ExtendedBatteryLevels(Extended): 146 | id = 0x88A1F7C7 147 | 148 | def __init__(self): 149 | self.batteries = {} 150 | 151 | def __repr__(self): 152 | return "".format( 153 | len(self.batteries), 154 | ", ".join( 155 | [ 156 | "{}: {:0.03%}".format(x, self.batteries[x] / 65535.0) 157 | for x in sorted(self.batteries) 158 | ] 159 | ), 160 | ) 161 | 162 | def load(self, data): 163 | self.batteries = {} 164 | pos = 2 165 | for i in range(nunpack(data[0:2])): 166 | battery_id = nunpack(data[pos : pos + 2]) 167 | battery_level = nunpack(data[pos + 2 : pos + 4]) 168 | self.batteries[battery_id] = battery_level 169 | pos += 4 170 | 171 | def dump(self, max_length=None): 172 | if max_length is not None: 173 | if max_length < 6: 174 | return None 175 | batteries = {} 176 | for i in sorted(self.batteries.keys())[0 : int((max_length - 2) / 4)]: 177 | batteries[i] = self.batteries[i] 178 | else: 179 | batteries = self.batteries 180 | 181 | out = npack(len(batteries), 2) 182 | for i in batteries: 183 | out += npack(i, 2) 184 | out += npack(batteries[i], 2) 185 | return out 186 | 187 | 188 | class Opcode: 189 | id = None 190 | 191 | def __init__(self): 192 | self.data = b"" 193 | 194 | def __repr__(self): 195 | if self.id is None: 196 | return "".format(len(self.data)) 197 | else: 198 | return "".format(self.id, len(self.data)) 199 | 200 | def load(self, data): 201 | self.data = data 202 | 203 | def dump(self): 204 | return self.data 205 | 206 | 207 | class OpcodeReplyRequested(Opcode): 208 | id = 0x0001 209 | 210 | def __init__(self): 211 | pass 212 | 213 | def __repr__(self): 214 | return "" 215 | 216 | def load(self, data): 217 | pass 218 | 219 | def dump(self, max_length=None): 220 | return b"" 221 | 222 | 223 | class OpcodeInReplyTo(Opcode): 224 | id = 0x0002 225 | 226 | def __init__(self): 227 | self.message_id = b"" 228 | 229 | def __repr__(self): 230 | return "".format(self.message_id.hex()) 231 | 232 | def load(self, data): 233 | self.message_id = data[0:6] 234 | 235 | def dump(self, max_length=None): 236 | if (max_length is not None) and (max_length < 6): 237 | return None 238 | return self.message_id 239 | 240 | 241 | class OpcodeRTTEnclosed(Opcode): 242 | id = 0x0004 243 | 244 | def __init__(self): 245 | self.rtt_us = 0 246 | 247 | def __repr__(self): 248 | return "".format(self.rtt_us) 249 | 250 | def load(self, data): 251 | self.rtt_us = nunpack(data[0:4]) 252 | 253 | def dump(self, max_length=None): 254 | if (max_length is not None) and (max_length < 4): 255 | return None 256 | return npack(self.rtt_us, 4) 257 | 258 | 259 | class OpcodeMessageIDList(Opcode): 260 | _repr_name = "ID List (Generic)" 261 | 262 | def __init__(self): 263 | self.message_ids = [] 264 | 265 | def __repr__(self): 266 | return "<{}: [{}] ({})>".format( 267 | self._repr_name, 268 | ", ".join(["0x{}".format(x.hex()) for x in self.message_ids]), 269 | len(self.message_ids), 270 | ) 271 | 272 | def load(self, data): 273 | self.message_ids = [] 274 | pos = 2 275 | for i in range(nunpack(data[0:2])): 276 | self.message_ids.append(data[pos : pos + 6]) 277 | pos += 6 278 | 279 | def dump(self, max_length=None): 280 | if max_length is not None: 281 | if max_length < 8: 282 | return None 283 | output_ids = self.message_ids[0 : int((max_length - 2) / 6)] 284 | else: 285 | output_ids = self.message_ids 286 | 287 | out = npack(len(output_ids), 2) 288 | for i in output_ids: 289 | out += i 290 | return out 291 | 292 | 293 | class OpcodeInvestigationSeen(OpcodeMessageIDList): 294 | id = 0x0008 295 | _repr_name = "Investigation Seen" 296 | 297 | 298 | class OpcodeInvestigationUnseen(OpcodeMessageIDList): 299 | id = 0x0010 300 | _repr_name = "Investigation Unseen" 301 | 302 | 303 | class OpcodeInvestigate(OpcodeMessageIDList): 304 | id = 0x0020 305 | _repr_name = "Investigate" 306 | 307 | 308 | class OpcodeCourtesyExpiration(OpcodeMessageIDList): 309 | id = 0x0040 310 | _repr_name = "Courtesy Expiration" 311 | 312 | 313 | class OpcodeHMAC(Opcode): 314 | id = 0x0080 315 | 316 | def __init__(self): 317 | self.key = b"" 318 | self.digest_index = None 319 | self.hash = b"" 320 | 321 | self.digest_map = { 322 | 1: (hashlib.md5, 16, "HMAC-MD5"), 323 | 2: (hashlib.sha1, 20, "HMAC-SHA1"), 324 | 3: (hashlib.sha256, 32, "HMAC-SHA256"), 325 | 4: (crc32, 4, "HMAC-CRC32"), 326 | 5: (hashlib.sha512, 64, "HMAC-SHA512"), 327 | } 328 | 329 | def __repr__(self): 330 | if self.digest_index is not None: 331 | return "<{}: 0x{}>".format( 332 | self.digest_map[self.digest_index][2], self.hash.hex() 333 | ) 334 | return "" 335 | 336 | def load(self, data): 337 | self.digest_index = nunpack(data[0:2]) 338 | self.hash = data[2:] 339 | 340 | def dump(self, max_length=None): 341 | if self.digest_index is not None: 342 | (hasher, size, hasher_name) = self.digest_map[self.digest_index] 343 | return npack(self.digest_index, 2) + bytes(size) 344 | return None 345 | 346 | 347 | class OpcodeHostLatency(Opcode): 348 | id = 0x0100 349 | 350 | def __init__(self): 351 | self.delay_us = 0 352 | 353 | def __repr__(self): 354 | return "".format(self.delay_us) 355 | 356 | def load(self, data): 357 | self.delay_us = nunpack(data[0:4]) 358 | 359 | def dump(self, max_length=None): 360 | if (max_length is not None) and (max_length < 4): 361 | return None 362 | return npack(self.delay_us, 4) 363 | 364 | 365 | class OpcodeEncrypted(Opcode): 366 | id = 0x0200 367 | 368 | def __init__(self): 369 | self.hkdf_info = ( 370 | b"\xd8\x89\xac\x93\xac\xeb\xa1\xf3\x98\xd0\xc6\x9b\xc8\xc6\xa7\xaa" 371 | ) 372 | self.method_index = None 373 | self.encrypted = b"" 374 | self.session = b"" 375 | self.iv = None 376 | 377 | self.method_map = {1: ("HKDF-AES256-CBC",)} 378 | 379 | def __repr__(self): 380 | if self.method_index is not None: 381 | return "<{} (Session {}, IV {}, {} bytes)>".format( 382 | self.method_map[self.method_index][0], 383 | repr(self.session), 384 | repr(self.iv), 385 | len(self.encrypted), 386 | ) 387 | return "" 388 | 389 | def load(self, data): 390 | self.method_index = nunpack(data[0:2]) 391 | if self.method_index == 1: 392 | self.session = data[2:10] 393 | self.iv = data[10:26] 394 | self.encrypted = data[26:] 395 | else: 396 | self.encrypted = data[2:] 397 | 398 | def dump(self, max_length=None): 399 | if self.method_index == 1: 400 | return npack(self.method_index, 2) + self.session + self.iv + self.encrypted 401 | return None 402 | 403 | def encrypt(self, unencrypted, key): 404 | if isinstance(AES, ImportError): 405 | return None 406 | if self.method_index is None: 407 | return None 408 | if self.method_index == 1: 409 | if self.iv is None: 410 | self.iv = bytes([random.randint(0, 255) for x in range(16)]) 411 | aeskey = self.hkdf( 412 | 32, 413 | key, 414 | salt=self.iv, 415 | info=(self.hkdf_info + self.session), 416 | digestmod=hashlib.sha256, 417 | ) 418 | aes_e = AES.new(aeskey, AES.MODE_CBC, self.iv) 419 | self.encrypted = aes_e.encrypt(unencrypted) 420 | else: 421 | return None 422 | 423 | def decrypt(self, key): 424 | if isinstance(AES, ImportError): 425 | return None 426 | if self.method_index is None: 427 | return None 428 | if self.method_index == 1: 429 | aeskey = self.hkdf( 430 | 32, 431 | key, 432 | salt=self.iv, 433 | info=(self.hkdf_info + self.session), 434 | digestmod=hashlib.sha256, 435 | ) 436 | aes_d = AES.new(aeskey, AES.MODE_CBC, self.iv) 437 | return aes_d.decrypt(self.encrypted) 438 | 439 | def hkdf(self, length, ikm, salt=b"", info=b"", digestmod=None): 440 | if digestmod is None: 441 | digestmod = hashlib.sha256 442 | prk = hmac.new(salt, ikm, digestmod).digest() 443 | hash_len = len(prk) 444 | t = b"" 445 | okm = b"" 446 | for i in range(ceil(length / hash_len)): 447 | t = hmac.new(prk, t + info + bytes([1 + i]), digestmod).digest() 448 | okm += t 449 | return okm[:length] 450 | 451 | 452 | class OpcodeExtended(Opcode): 453 | id = 0x8000 454 | 455 | def __init__(self): 456 | self.segments = {} 457 | self.segment_data_positions = {} 458 | 459 | def __repr__(self): 460 | return "".format( 461 | repr(sorted(self.segments.values(), key=lambda x: x.id)) 462 | ) 463 | 464 | def load(self, data): 465 | self.segments = {} 466 | self.segment_data_positions = {} 467 | 468 | pos = 0 469 | known_segments = ( 470 | ExtendedVersion, 471 | ExtendedNotice, 472 | ExtendedMonotonicClock, 473 | ExtendedWallClock, 474 | ExtendedRandom, 475 | ExtendedBatteryLevels, 476 | ) 477 | 478 | while pos < len(data): 479 | flag = nunpack(data[pos : pos + 4]) 480 | pos += 4 481 | segment_data_length = nunpack(data[pos : pos + 2]) 482 | pos += 2 483 | self.segment_data_positions[flag] = (pos, segment_data_length) 484 | segment_handler = None 485 | for seg in known_segments: 486 | if flag == seg.id: 487 | segment_handler = seg 488 | break 489 | if segment_handler is None: 490 | segment_handler = Extended 491 | segment_handler.id = flag 492 | self.segments[flag] = segment_handler() 493 | self.segments[flag].load(data[pos : (pos + segment_data_length)]) 494 | pos += segment_data_length 495 | 496 | def dump(self, max_length=None): 497 | if (max_length is not None) and (max_length < 6): 498 | return None 499 | out = b"" 500 | pos = 0 501 | for segment in self.segments.values(): 502 | if max_length is None: 503 | segment_max_length = None 504 | else: 505 | segment_max_length = max_length - pos - 6 506 | segment_data = segment.dump(max_length=segment_max_length) 507 | if segment_data is None: 508 | continue 509 | out += npack(segment.id, 4) 510 | pos += 4 511 | out += npack(len(segment_data), 2) 512 | pos += 2 513 | out += segment_data 514 | pos += len(segment_data) 515 | if len(out) == 0: 516 | return None 517 | return out 518 | 519 | 520 | class Packet: 521 | def __repr__(self): 522 | return "".format( 523 | self.message_id.hex(), 524 | repr(sorted(self.opcodes.values(), key=lambda x: x.id)), 525 | ) 526 | 527 | def __init__(self): 528 | self.message_id = b"" 529 | self.opcodes = {} 530 | self.min_length = 0 531 | self.max_length = 1024 532 | self.align_length = 0 533 | self.padding_pattern = b"\x00" 534 | self.opcode_data_positions = {} 535 | 536 | def load(self, data): 537 | magic_number = data[0:2] 538 | if magic_number != b"\x32\x50": 539 | raise Exception("Invalid magic number") 540 | checksum = nunpack(data[2:4]) 541 | if checksum: 542 | if twoping_checksum(data[0:2] + b"\x00\x00" + data[4:]) != checksum: 543 | raise Exception("Invalid checksum") 544 | self.message_id = data[4:10] 545 | opcode_flags = nunpack(data[10:12]) 546 | self.opcodes = {} 547 | 548 | pos = 12 549 | known_opcodes = ( 550 | OpcodeReplyRequested, 551 | OpcodeInReplyTo, 552 | OpcodeRTTEnclosed, 553 | OpcodeInvestigationSeen, 554 | OpcodeInvestigationUnseen, 555 | OpcodeInvestigate, 556 | OpcodeCourtesyExpiration, 557 | OpcodeHMAC, 558 | OpcodeHostLatency, 559 | OpcodeEncrypted, 560 | OpcodeExtended, 561 | ) 562 | for flag in (2**x for x in range(16)): 563 | if not opcode_flags & flag: 564 | continue 565 | opcode_data_length = nunpack(data[pos : pos + 2]) 566 | pos += 2 567 | self.opcode_data_positions[flag] = (pos, opcode_data_length) 568 | opcode_handler = None 569 | for oc in known_opcodes: 570 | if flag == oc.id: 571 | opcode_handler = oc 572 | break 573 | if opcode_handler is None: 574 | opcode_handler = Opcode 575 | opcode_handler.id = flag 576 | self.opcodes[flag] = opcode_handler() 577 | self.opcodes[flag].load(data[pos : (pos + opcode_data_length)]) 578 | pos += opcode_data_length 579 | 580 | def dump(self): 581 | auth_pos_begin = 0 582 | auth_pos_end = 0 583 | if not self.message_id: 584 | self.message_id = bytes([random.randint(0, 255) for x in range(6)]) 585 | opcode_datas = {} 586 | packet_length = 12 587 | for flag in ( 588 | OpcodeEncrypted.id, 589 | OpcodeHMAC.id, 590 | OpcodeReplyRequested.id, 591 | OpcodeInReplyTo.id, 592 | OpcodeRTTEnclosed.id, 593 | OpcodeInvestigationSeen.id, 594 | OpcodeInvestigationUnseen.id, 595 | OpcodeInvestigate.id, 596 | OpcodeHostLatency.id, 597 | OpcodeCourtesyExpiration.id, 598 | OpcodeExtended.id, 599 | ): 600 | if flag not in self.opcodes: 601 | continue 602 | if (packet_length + 2) > self.max_length: 603 | break 604 | res = self.opcodes[flag].dump( 605 | max_length=(self.max_length - packet_length - 2) 606 | ) 607 | if res is None: 608 | continue 609 | opcode_datas[flag] = res 610 | res_len = len(res) 611 | packet_length += res_len + 2 612 | opcode_flags = 0 613 | opcode_data = b"" 614 | packet_length = 12 615 | for flag in sorted(opcode_datas.keys()): 616 | res = opcode_datas[flag] 617 | res_len = len(res) 618 | if flag == OpcodeHMAC.id: 619 | auth_pos_begin = packet_length + 4 620 | auth_pos_end = auth_pos_begin + (res_len - 2) 621 | opcode_flags = opcode_flags | flag 622 | opcode_data += npack(res_len, 2) 623 | opcode_data += res 624 | packet_length += res_len + 2 625 | out = bytearray( 626 | b"\x32\x50\x00\x00" + self.message_id + npack(opcode_flags, 2) + opcode_data 627 | ) 628 | target_length = len(out) 629 | if len(out) < self.min_length: 630 | target_length = self.min_length 631 | if self.align_length and (target_length % self.align_length): 632 | target_length += self.align_length - (target_length % self.align_length) 633 | if len(out) < target_length: 634 | target_padding = target_length - len(out) 635 | padding = ( 636 | self.padding_pattern 637 | * int(target_padding / len(self.padding_pattern) + 1) 638 | )[0:target_padding] 639 | out += padding 640 | if (OpcodeHMAC.id in self.opcodes) and auth_pos_begin: 641 | out[auth_pos_begin:auth_pos_end] = self.calculate_hash( 642 | self.opcodes[OpcodeHMAC.id], out 643 | ) 644 | out[2:4] = npack(twoping_checksum(out), 2) 645 | return bytes(out) 646 | 647 | def calculate_hash(self, opcode, payload): 648 | (hasher, size, hasher_name) = opcode.digest_map[opcode.digest_index] 649 | return hmac.new(opcode.key, payload, hasher).digest() 650 | -------------------------------------------------------------------------------- /twoping/utils.py: -------------------------------------------------------------------------------- 1 | # 2ping - A bi-directional ping utility 2 | # Copyright (C) 2010-2021 Ryan Finnie 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | import gettext 6 | import platform 7 | import random as _pyrandom 8 | 9 | try: 10 | import distro 11 | except ImportError as e: 12 | distro = e 13 | 14 | try: 15 | from Cryptodome.Cipher import AES 16 | except ImportError as e: 17 | try: 18 | from Crypto.Cipher import AES 19 | except ImportError: 20 | AES = e 21 | 22 | 23 | _ = gettext.translation("2ping", fallback=True).gettext 24 | _pl = gettext.translation("2ping", fallback=True).ngettext 25 | 26 | try: 27 | random = _pyrandom.SystemRandom() 28 | random_is_systemrandom = True 29 | except AttributeError: 30 | random = _pyrandom 31 | random_is_systemrandom = False 32 | 33 | 34 | def twoping_checksum(packet): 35 | """Calculate 2ping checksum on a 2ping packet 36 | 37 | Packet may be bytes or bytearray, return is a 32-bit int. 38 | Checksum is calculated as described by the 2ping protocol 39 | specification. 40 | """ 41 | checksum = 0 42 | 43 | if (len(packet) % 2) == 1: 44 | # Convert from (possible) bytearray to bytes before appending 45 | packet = bytes(packet) + b"\x00" 46 | 47 | for i in range(0, len(packet), 2): 48 | checksum = checksum + (packet[i] << 8) + packet[i + 1] 49 | checksum = (checksum & 0xFFFF) + (checksum >> 16) 50 | 51 | checksum = ~checksum & 0xFFFF 52 | 53 | if checksum == 0: 54 | checksum = 0xFFFF 55 | 56 | return checksum 57 | 58 | 59 | def div0(n, d): 60 | """Pretend we live in a world where n / 0 == 0""" 61 | return 0 if d == 0 else n / d 62 | 63 | 64 | def npack(i, minimum=1): 65 | """Pack int to network bytes 66 | 67 | Takes an int and packs it to a network (big-endian) bytes. 68 | If minimum is specified, bytes output is padded with zero'd bytes. 69 | Minimum does not need to be aligned to 32-bits, 64-bits, etc. 70 | """ 71 | out = bytearray() 72 | while i >= 256: 73 | out.insert(0, i & 0xFF) 74 | i = i >> 8 75 | out.insert(0, i) 76 | out_len = len(out) 77 | if out_len < minimum: 78 | out = bytearray(minimum - out_len) + out 79 | return bytes(out) 80 | 81 | 82 | def nunpack(b): 83 | """Unpack network bytes to int 84 | 85 | Takes arbitrary length network (big-endian) bytes, and returns an 86 | int. 87 | """ 88 | out = 0 89 | for x in b: 90 | out = (out << 8) + x 91 | return out 92 | 93 | 94 | def platform_info(): 95 | """Return a string containing platform/OS information""" 96 | platform_name = platform.system() 97 | platform_machine = platform.machine() 98 | platform_info = "{} {}".format(platform_name, platform_machine) 99 | distro_name = "" 100 | distro_version = "" 101 | distro_info = "" 102 | if not isinstance(distro, ImportError): 103 | if hasattr(distro, "name"): 104 | distro_name = distro.name() 105 | if hasattr(distro, "version"): 106 | distro_version = distro.version() 107 | if distro_name: 108 | distro_info = distro_name 109 | if distro_version: 110 | distro_info += " " + distro_version 111 | return "{} ({})".format(platform_info, distro_info) 112 | else: 113 | return platform_info 114 | 115 | 116 | def fuzz_bytearray(data, percent, start=0, end=None): 117 | """Fuzz a bytearray in-place 118 | 119 | Each bit has a chance of being flipped. 120 | """ 121 | if end is None: 122 | end = len(data) 123 | for p in range(start, end): 124 | xor_byte = 0 125 | for i in range(8): 126 | if random.random() < (percent / 100.0): 127 | xor_byte = xor_byte + (2**i) 128 | data[p] = data[p] ^ xor_byte 129 | 130 | 131 | def fuzz_packet(packet, percent): 132 | """Fuzz a dumped 2ping packet 133 | 134 | Each bit in the opcode areas has a chance of being flipped. 135 | Each bit in the magic number and checksum have a / 10 136 | chance of being flipped. 137 | 138 | Returns the fuzzed packet. 139 | """ 140 | packet = bytearray(packet) 141 | 142 | # Fuzz the entire packet 143 | fuzz_bytearray(packet, percent, 4) 144 | 145 | # Fuzz the magic number, at a lower probability 146 | fuzz_bytearray(packet, percent / 10.0, 0, 2) 147 | 148 | # Recalculate the checksum 149 | packet[2:4] = b"\x00\x00" 150 | packet[2:4] = npack(twoping_checksum(packet), 2) 151 | 152 | # Fuzz the recalculated checksum itself, at a lower probability 153 | fuzz_bytearray(packet, percent / 10.0, 2, 4) 154 | 155 | return bytes(packet) 156 | 157 | 158 | def stats_time(seconds): 159 | """Convert seconds to ms/s/m/h/d/y time string""" 160 | 161 | conversion = ( 162 | (1000, "ms"), 163 | (60, "s"), 164 | (60, "m"), 165 | (24, "h"), 166 | (365, "d"), 167 | (None, "y"), 168 | ) 169 | out = "" 170 | rest = int(seconds * 1000) 171 | for div, suffix in conversion: 172 | if div is None: 173 | if out: 174 | out = " " + out 175 | out = "{}{}{}".format(rest, suffix, out) 176 | break 177 | p = rest % div 178 | rest = int(rest / div) 179 | if p > 0: 180 | if out: 181 | out = " " + out 182 | out = "{}{}{}".format(p, suffix, out) 183 | if rest == 0: 184 | break 185 | return out 186 | -------------------------------------------------------------------------------- /wireshark/2ping.lua: -------------------------------------------------------------------------------- 1 | local twoping = Proto("2ping","2ping Protocol") 2 | 3 | local mac_digests = { 4 | [0] = "Private", 5 | [1] = "HMAC-MD5", 6 | [2] = "HMAC-SHA1", 7 | [3] = "HMAC-SHA256", 8 | [4] = "HMAC-CRC32", 9 | [5] = "HMAC-SHA512" 10 | } 11 | 12 | local encrypted_methods = { 13 | [0] = "Private", 14 | [1] = "HKDF-AES256-CBC" 15 | } 16 | 17 | local extended_ids = { 18 | [0x3250564e] = "Program version", 19 | [0x2ff6ad68] = "Random data", 20 | [0x64f69319] = "Wall clock", 21 | [0x771d8dfb] = "Monotonic clock", 22 | [0x88a1f7c7] = "Battery levels", 23 | [0xa837b44e] = "Notice text" 24 | } 25 | 26 | local pf_magic_number = ProtoField.new ("Magic number", "2ping.magic_number", ftypes.UINT16, nil, base.HEX) 27 | local pf_checksum = ProtoField.new ("Checksum", "2ping.checksum", ftypes.UINT16, nil, base.HEX) 28 | local pf_message_id = ProtoField.new ("Message ID", "2ping.message_id", ftypes.ETHER) 29 | local pf_opcode_flags = ProtoField.new ("Opcode flags", "2ping.opcode_flags", ftypes.UINT16, nil, base.HEX) 30 | 31 | local pf_opcode_flag_reply_requested = ProtoField.bool ("2ping.opcode_flags.reply_requested", "Reply requested", 16, nil, 0x0001) 32 | local pf_opcode_flag_in_reply_to = ProtoField.bool ("2ping.opcode_flags.in_reply_to", "In reply to", 16, nil, 0x0002) 33 | local pf_opcode_flag_rtt_enclosed = ProtoField.bool ("2ping.opcode_flags.rtt_enclosed", "RTT enclosed", 16, nil, 0x0004) 34 | local pf_opcode_flag_investigation_replied = ProtoField.bool ("2ping.opcode_flags.investigation_replied", "Investigation (replied)", 16, nil, 0x0008) 35 | local pf_opcode_flag_investigation_lost = ProtoField.bool ("2ping.opcode_flags.investigation_lost", "Investigation (lost)", 16, nil, 0x0010) 36 | local pf_opcode_flag_investigation_request = ProtoField.bool ("2ping.opcode_flags.investigation_request", "Investigation request", 16, nil, 0x0020) 37 | local pf_opcode_flag_courtesy_expiration = ProtoField.bool ("2ping.opcode_flags.courtesy_expiration", "Courtesy expiration", 16, nil, 0x0040) 38 | local pf_opcode_flag_mac = ProtoField.bool ("2ping.opcode_flags.mac", "Message authentication code", 16, nil, 0x0080) 39 | local pf_opcode_flag_host_latency = ProtoField.bool ("2ping.opcode_flags.host_latency", "Host processing latency", 16, nil, 0x0100) 40 | local pf_opcode_flag_encrypted = ProtoField.bool ("2ping.opcode_flags.encrypted", "Encrypted packet", 16, nil, 0x0200) 41 | local pf_opcode_flag_extended = ProtoField.bool ("2ping.opcode_flags.extended", "Extended segments", 16, nil, 0x8000) 42 | 43 | local pf_segment_length = ProtoField.new ("Length", "2ping.segment.length", ftypes.UINT16) 44 | 45 | local pf_reply_requested = ProtoField.new ("Reply requested", "2ping.reply_requested", ftypes.NONE) 46 | 47 | local pf_in_reply_to = ProtoField.new ("In reply to", "2ping.in_reply_to", ftypes.STRING) 48 | local pf_in_reply_to_message_id = ProtoField.new ("Message ID", "2ping.in_reply_to.message_id", ftypes.ETHER) 49 | 50 | local pf_rtt_enclosed = ProtoField.new ("RTT enclosed", "2ping.rtt_enclosed", ftypes.UINT32) 51 | local pf_rtt_enclosed_rtt = ProtoField.new ("RTT (μs)", "2ping.rtt_enclosed.rtt", ftypes.UINT32) 52 | 53 | local pf_investigation_replied = ProtoField.new ("Investigation (replied)", "2ping.investigation_replied", ftypes.STRING) 54 | local pf_investigation_replied_count = ProtoField.new ("Message ID count", "2ping.investigation_replied.count", ftypes.UINT16) 55 | local pf_investigation_replied_message_id = ProtoField.new ("Message ID", "2ping.investigation_replied.message_id", ftypes.ETHER) 56 | 57 | local pf_investigation_lost = ProtoField.new ("Investigation (lost)", "2ping.investigation_lost", ftypes.STRING) 58 | local pf_investigation_lost_count = ProtoField.new ("Message ID count", "2ping.investigation_lost.count", ftypes.UINT16) 59 | local pf_investigation_lost_message_id = ProtoField.new ("Message ID", "2ping.investigation_lost.message_id", ftypes.ETHER) 60 | 61 | local pf_investigation_request = ProtoField.new ("Investigation request", "2ping.investigation_request", ftypes.STRING) 62 | local pf_investigation_request_count = ProtoField.new ("Message ID count", "2ping.investigation_request.count", ftypes.UINT16) 63 | local pf_investigation_request_message_id = ProtoField.new ("Message ID", "2ping.investigation_request.message_id", ftypes.ETHER) 64 | 65 | local pf_courtesy_expiration = ProtoField.new ("Courtesy expiration", "2ping.courtesy_expiration", ftypes.STRING) 66 | local pf_courtesy_expiration_count = ProtoField.new ("Message ID count", "2ping.courtesy_expiration.count", ftypes.UINT16) 67 | local pf_courtesy_expiration_message_id = ProtoField.new ("Message ID", "2ping.courtesy_expiration.message_id", ftypes.ETHER) 68 | 69 | local pf_mac = ProtoField.new ("Message authentication code", "2ping.mac", ftypes.STRING) 70 | local pf_mac_digest = ProtoField.uint16 ("2ping.mac.digest", "Digest", base.DEC, mac_digests) 71 | local pf_mac_hash = ProtoField.new ("Hash", "2ping.mac.hash", ftypes.BYTES) 72 | 73 | local pf_host_latency = ProtoField.new ("Host processing latency", "2ping.host_latency", ftypes.UINT32) 74 | local pf_host_latency_delay = ProtoField.new ("Delay (μs)", "2ping.host_latency.delay", ftypes.UINT32) 75 | 76 | local pf_encrypted = ProtoField.new ("Encrypted packet", "2ping.encrypted", ftypes.STRING) 77 | local pf_encrypted_method = ProtoField.uint16 ("2ping.encrypted.method", "Method", base.DEC, encrypted_methods) 78 | local pf_encrypted_data = ProtoField.new ("Data", "2ping.encrypted.data", ftypes.BYTES) 79 | 80 | local pf_unknown = ProtoField.new ("Unknown", "2ping.unknown", ftypes.STRING) 81 | local pf_unknown_data = ProtoField.new ("Data", "2ping.unknown.data", ftypes.BYTES) 82 | 83 | local pf_extended = ProtoField.new ("Extended segments", "2ping.extended", ftypes.STRING) 84 | local pf_extended_count = ProtoField.new ("Count", "2ping.extended.count", ftypes.UINT16) 85 | 86 | local pf_extended_id = ProtoField.uint32 ("2ping.extended.id", "ID", base.HEX, extended_ids) 87 | 88 | local pf_version = ProtoField.new ("Program version", "2ping.version", ftypes.STRING) 89 | local pf_version_text = ProtoField.new ("Text", "2ping.version.text", ftypes.STRING) 90 | 91 | local pf_notice = ProtoField.new ("Notice text", "2ping.notice", ftypes.STRING) 92 | local pf_notice_text = ProtoField.new ("Text", "2ping.notice.text", ftypes.STRING) 93 | 94 | local pf_random = ProtoField.new ("Random data", "2ping.random", ftypes.STRING) 95 | local pf_random_data = ProtoField.new ("Data", "2ping.random.data", ftypes.BYTES) 96 | local pf_random_flag_hardware = ProtoField.bool ("2ping.random.hardware", "Hardware RNG", 16, nil, 0x0001) 97 | local pf_random_flag_os = ProtoField.bool ("2ping.random.os", "Operating system RNG", 16, nil, 0x0002) 98 | 99 | local pf_wallclock = ProtoField.new ("Wall clock", "2ping.wallclock", ftypes.ABSOLUTE_TIME) 100 | local pf_wallclock_time = ProtoField.new ("Time", "2ping.wallclock.time", ftypes.ABSOLUTE_TIME) 101 | 102 | local pf_monotonic = ProtoField.new ("Monotonic clock", "2ping.monotonic", ftypes.ABSOLUTE_TIME) 103 | local pf_monotonic_generation = ProtoField.new ("Generation", "2ping.monotonic.generation", ftypes.UINT16) 104 | local pf_monotonic_time = ProtoField.new ("Time", "2ping.monotonic.time", ftypes.ABSOLUTE_TIME) 105 | 106 | local pf_battery_levels = ProtoField.new ("Battery levels", "2ping.battery_levels", ftypes.STRING) 107 | local pf_battery_levels_count = ProtoField.new ("Battery count", "2ping.battery_levels.count", ftypes.UINT16) 108 | local pf_battery_levels_id = ProtoField.new ("Battery ID", "2ping.battery_levels.id", ftypes.UINT16) 109 | local pf_battery_levels_level = ProtoField.new ("Battery level", "2ping.battery_levels.level", ftypes.UINT16) 110 | 111 | local pf_padding = ProtoField.new ("Padding", "2ping.padding", ftypes.BYTES) 112 | 113 | twoping.fields = { 114 | pf_magic_number, 115 | pf_checksum, 116 | pf_message_id, 117 | pf_opcode_flags, 118 | pf_opcode_flag_reply_requested, 119 | pf_opcode_flag_in_reply_to, 120 | pf_opcode_flag_rtt_enclosed, 121 | pf_opcode_flag_investigation_replied, 122 | pf_opcode_flag_investigation_lost, 123 | pf_opcode_flag_investigation_request, 124 | pf_opcode_flag_courtesy_expiration, 125 | pf_opcode_flag_mac, 126 | pf_opcode_flag_host_latency, 127 | pf_opcode_flag_encrypted, 128 | pf_opcode_flag_extended, 129 | pf_segment_length, 130 | pf_reply_requested, 131 | pf_in_reply_to, 132 | pf_in_reply_to_message_id, 133 | pf_rtt_enclosed, 134 | pf_rtt_enclosed_rtt, 135 | pf_investigation_replied, 136 | pf_investigation_replied_count, 137 | pf_investigation_replied_message_id, 138 | pf_investigation_lost, 139 | pf_investigation_lost_count, 140 | pf_investigation_lost_message_id, 141 | pf_investigation_request, 142 | pf_investigation_request_count, 143 | pf_investigation_request_message_id, 144 | pf_courtesy_expiration, 145 | pf_courtesy_expiration_count, 146 | pf_courtesy_expiration_message_id, 147 | pf_mac, 148 | pf_mac_digest, 149 | pf_mac_hash, 150 | pf_host_latency, 151 | pf_host_latency_delay, 152 | pf_encrypted, 153 | pf_encrypted_method, 154 | pf_encrypted_data, 155 | pf_unknown, 156 | pf_unknown_data, 157 | pf_extended, 158 | pf_extended_count, 159 | pf_extended_segment, 160 | pf_extended_id, 161 | pf_version, 162 | pf_version_text, 163 | pf_notice, 164 | pf_notice_text, 165 | pf_random, 166 | pf_random_data, 167 | pf_random_flag_hardware, 168 | pf_random_flag_os, 169 | pf_wallclock, 170 | pf_wallclock_time, 171 | pf_monotonic, 172 | pf_monotonic_generation, 173 | pf_monotonic_time, 174 | pf_battery_levels, 175 | pf_battery_levels_count, 176 | pf_battery_levels_id, 177 | pf_battery_levels_level, 178 | pf_padding, 179 | } 180 | 181 | local reply_requested_field = Field.new("2ping.opcode_flags.reply_requested") 182 | local in_reply_to_field = Field.new("2ping.opcode_flags.in_reply_to") 183 | local rtt_enclosed_field = Field.new("2ping.opcode_flags.rtt_enclosed") 184 | local investigation_replied_field = Field.new("2ping.opcode_flags.investigation_replied") 185 | local investigation_lost_field = Field.new("2ping.opcode_flags.investigation_lost") 186 | local investigation_request_field = Field.new("2ping.opcode_flags.investigation_request") 187 | local courtesy_expiration_field = Field.new("2ping.opcode_flags.courtesy_expiration") 188 | local mac_field = Field.new("2ping.opcode_flags.mac") 189 | local host_latency_field = Field.new("2ping.opcode_flags.host_latency") 190 | local encrypted_field = Field.new("2ping.opcode_flags.encrypted") 191 | local extended_field = Field.new("2ping.opcode_flags.extended") 192 | 193 | local function shift_opcode_data(buf) 194 | local data_length = buf:range(0,2):uint() 195 | local opcode_range = buf:range(0,data_length+2) 196 | local data_range = opcode_range:range(0,0) 197 | if data_length > 0 then 198 | data_range = opcode_range:range(2) 199 | end 200 | local remaining_buf = buf:range(0,0) 201 | if data_length+2 < buf:len() then 202 | remaining_buf = buf:range(data_length+2) 203 | end 204 | return opcode_range, data_range, remaining_buf 205 | end 206 | 207 | local function process_common_message_id_list(tree, opcode_range, data_range, pf, pf_count, pf_message_id) 208 | local num_message_ids = data_range:range(0,2):uint() 209 | local local_tree = tree:add(pf, opcode_range) 210 | local_tree:add(pf_segment_length, opcode_range:range(0,2)) 211 | local_tree:add(pf_count, data_range:range(0,2)) 212 | local local_pos = 2 213 | for i=1,num_message_ids,1 do 214 | local_tree:add(pf_message_id, data_range:range(local_pos,6)) 215 | local_pos = local_pos + 6 216 | end 217 | if num_message_ids == 1 then 218 | local_tree:append_text(tostring(data_range:range(2):ether())) 219 | else 220 | local_tree:append_text(num_message_ids) 221 | local_tree:append_text(" IDs") 222 | end 223 | end 224 | 225 | function twoping.dissector(tvbuf,pktinfo,root) 226 | pktinfo.cols.protocol:set("2PING") 227 | local pktlen = tvbuf:reported_length_remaining() 228 | local tree = root:add(twoping, tvbuf:range(0,pktlen)) 229 | tree:add(pf_magic_number, tvbuf:range(0,2)) 230 | tree:add(pf_checksum, tvbuf:range(2,2)) 231 | tree:add(pf_message_id, tvbuf:range(4,6)) 232 | 233 | local flagrange = tvbuf:range(10,2) 234 | local flag_tree = tree:add(pf_opcode_flags, flagrange) 235 | 236 | local opcode_remaining = tvbuf:range(12) 237 | 238 | flag_tree:add(pf_opcode_flag_reply_requested, flagrange) 239 | if reply_requested_field()() then 240 | local opcode_range, data_range, remaining_buf = shift_opcode_data(opcode_remaining) 241 | opcode_remaining = remaining_buf 242 | local reply_requested_tree = tree:add(pf_reply_requested, opcode_range) 243 | reply_requested_tree:add(pf_segment_length, opcode_range:range(0,2)) 244 | end 245 | 246 | flag_tree:add(pf_opcode_flag_in_reply_to, flagrange) 247 | if in_reply_to_field()() then 248 | local opcode_range, data_range, remaining_buf = shift_opcode_data(opcode_remaining) 249 | opcode_remaining = remaining_buf 250 | local in_reply_to_tree = tree:add(pf_in_reply_to, opcode_range, tostring(data_range:ether())) 251 | in_reply_to_tree:add(pf_segment_length, opcode_range:range(0,2)) 252 | in_reply_to_tree:add(pf_in_reply_to_message_id, data_range) 253 | end 254 | 255 | flag_tree:add(pf_opcode_flag_rtt_enclosed, flagrange) 256 | if rtt_enclosed_field()() then 257 | local opcode_range, data_range, remaining_buf = shift_opcode_data(opcode_remaining) 258 | opcode_remaining = remaining_buf 259 | local rtt_enclosed_tree = tree:add(pf_rtt_enclosed, opcode_range, data_range:uint(), nil, "μs") 260 | rtt_enclosed_tree:add(pf_segment_length, opcode_range:range(0,2)) 261 | rtt_enclosed_tree:add(pf_rtt_enclosed_rtt, data_range) 262 | end 263 | 264 | flag_tree:add(pf_opcode_flag_investigation_replied, flagrange) 265 | if investigation_replied_field()() then 266 | local opcode_range, data_range, remaining_buf = shift_opcode_data(opcode_remaining) 267 | opcode_remaining = remaining_buf 268 | process_common_message_id_list(tree, opcode_range, data_range, pf_investigation_replied, pf_investigation_replied_count, pf_investigation_replied_message_id) 269 | end 270 | 271 | flag_tree:add(pf_opcode_flag_investigation_lost, flagrange) 272 | if investigation_lost_field()() then 273 | local opcode_range, data_range, remaining_buf = shift_opcode_data(opcode_remaining) 274 | opcode_remaining = remaining_buf 275 | process_common_message_id_list(tree, opcode_range, data_range, pf_investigation_lost, pf_investigation_lost_count, pf_investigation_lost_message_id) 276 | end 277 | 278 | flag_tree:add(pf_opcode_flag_investigation_request, flagrange) 279 | if investigation_request_field()() then 280 | local opcode_range, data_range, remaining_buf = shift_opcode_data(opcode_remaining) 281 | opcode_remaining = remaining_buf 282 | process_common_message_id_list(tree, opcode_range, data_range, pf_investigation_request, pf_investigation_request_count, pf_investigation_request_message_id) 283 | end 284 | 285 | flag_tree:add(pf_opcode_flag_courtesy_expiration, flagrange) 286 | if courtesy_expiration_field()() then 287 | local opcode_range, data_range, remaining_buf = shift_opcode_data(opcode_remaining) 288 | opcode_remaining = remaining_buf 289 | process_common_message_id_list(tree, opcode_range, data_range, pf_courtesy_expiration, pf_courtesy_expiration_count, pf_courtesy_expiration_message_id) 290 | end 291 | 292 | flag_tree:add(pf_opcode_flag_mac, flagrange) 293 | if mac_field()() then 294 | local opcode_range, data_range, remaining_buf = shift_opcode_data(opcode_remaining) 295 | opcode_remaining = remaining_buf 296 | local mac_tree = tree:add(pf_mac, opcode_range, tostring(data_range:range(2):bytes()):lower()) 297 | mac_tree:add(pf_segment_length, opcode_range:range(0,2)) 298 | mac_tree:add(pf_mac_digest, data_range:range(0,2)) 299 | mac_tree:add(pf_mac_hash, data_range:range(2)) 300 | end 301 | 302 | flag_tree:add(pf_opcode_flag_host_latency, flagrange) 303 | if host_latency_field()() then 304 | local opcode_range, data_range, remaining_buf = shift_opcode_data(opcode_remaining) 305 | opcode_remaining = remaining_buf 306 | local host_latency_tree = tree:add(pf_host_latency, opcode_range, data_range:uint(), nil, "μs") 307 | host_latency_tree:add(pf_segment_length, opcode_range:range(0,2)) 308 | host_latency_tree:add(pf_host_latency_delay, data_range) 309 | end 310 | 311 | flag_tree:add(pf_opcode_flag_encrypted, flagrange) 312 | if encrypted_field()() then 313 | local opcode_range, data_range, remaining_buf = shift_opcode_data(opcode_remaining) 314 | opcode_remaining = remaining_buf 315 | local encrypted_tree = tree:add(pf_encrypted, opcode_range, tostring(data_range:range(2):bytes()):lower()) 316 | encrypted_tree:add(pf_segment_length, opcode_range:range(0,2)) 317 | encrypted_tree:add(pf_encrypted_method, data_range:range(0,2)) 318 | encrypted_tree:add(pf_encrypted_data, data_range:range(2)) 319 | end 320 | 321 | for bitpos=5,1,-1 do 322 | if flagrange:bitfield(bitpos,1) == 1 then 323 | local opcode_range, data_range, remaining_buf = shift_opcode_data(opcode_remaining) 324 | opcode_remaining = remaining_buf 325 | local unknown_tree = tree:add(pf_unknown, opcode_range, tostring(data_range:bytes()):lower(), nil, data_range:len(), "bytes") 326 | unknown_tree:add(pf_segment_length, opcode_range:range(0,2)) 327 | unknown_tree:add(pf_unknown_data, data_range) 328 | end 329 | end 330 | 331 | flag_tree:add(pf_opcode_flag_extended, flagrange) 332 | if extended_field()() then 333 | local opcode_range, data_range, remaining_buf = shift_opcode_data(opcode_remaining) 334 | opcode_remaining = remaining_buf 335 | local extended_tree = tree:add(pf_extended, opcode_range) 336 | extended_tree:add(pf_segment_length, opcode_range:range(0,2)) 337 | 338 | local extended_range = data_range 339 | local num_extended_segments = 0 340 | while( extended_range:len() > 0 ) do 341 | num_extended_segments = num_extended_segments + 1 342 | local id_range = extended_range:range(0,4) 343 | local length_range = extended_range:range(4,2) 344 | local extended_id = id_range:uint() 345 | local extended_length = length_range:uint() 346 | local segment_range = extended_range:range(0,extended_length+6) 347 | local segment_data_range = segment_range:range(6) 348 | if extended_range:len() <= extended_length+6 then 349 | extended_range = extended_range:range(0,0) 350 | else 351 | extended_range = extended_range:range(extended_length+6) 352 | end 353 | 354 | if extended_id == 0x3250564e then 355 | local version_text = segment_data_range:string() 356 | local extended_segment_tree = tree:add(pf_version, segment_range, version_text) 357 | extended_segment_tree:add(pf_extended_id, id_range) 358 | extended_segment_tree:add(pf_segment_length, length_range) 359 | extended_segment_tree:add(pf_version_text, segment_data_range) 360 | elseif extended_id == 0xa837b44e then 361 | local notice_text = segment_data_range:string() 362 | local extended_segment_tree = tree:add(pf_notice, segment_range, notice_text) 363 | extended_segment_tree:add(pf_extended_id, id_range) 364 | extended_segment_tree:add(pf_segment_length, length_range) 365 | extended_segment_tree:add(pf_notice_text, segment_data_range) 366 | elseif extended_id == 0x2ff6ad68 then 367 | local extended_segment_tree = tree:add(pf_random, segment_range, tostring(segment_data_range:range(2):bytes()):lower(), nil, segment_data_range:len()-2, "bytes") 368 | extended_segment_tree:add(pf_extended_id, id_range) 369 | extended_segment_tree:add(pf_segment_length, length_range) 370 | extended_segment_tree:add(pf_random_flag_hardware, segment_data_range:range(0,2)) 371 | extended_segment_tree:add(pf_random_flag_os, segment_data_range:range(0,2)) 372 | extended_segment_tree:add(pf_random_data, segment_data_range:range(2)) 373 | elseif extended_id == 0x64f69319 then 374 | local usecs = segment_data_range:range(0,8):uint64() 375 | local secs = (usecs / 1000000):tonumber() 376 | local nsecs = (usecs % 1000000):tonumber() * 1000 377 | local nstime = NSTime.new(secs, nsecs) 378 | local extended_segment_tree = tree:add(pf_wallclock, segment_range, nstime) 379 | extended_segment_tree:add(pf_extended_id, id_range) 380 | extended_segment_tree:add(pf_segment_length, length_range) 381 | extended_segment_tree:add(pf_wallclock_time, segment_data_range:range(0,8), nstime) 382 | elseif extended_id == 0x771d8dfb then 383 | local usecs = segment_data_range:range(2,8):uint64() 384 | local secs = (usecs / 1000000):tonumber() 385 | local nsecs = (usecs % 1000000):tonumber() * 1000 386 | local nstime = NSTime.new(secs, nsecs) 387 | local extended_segment_tree = tree:add(pf_monotonic, segment_range, nstime) 388 | extended_segment_tree:add(pf_extended_id, id_range) 389 | extended_segment_tree:add(pf_segment_length, length_range) 390 | extended_segment_tree:add(pf_monotonic_generation, segment_data_range:range(0,2)) 391 | extended_segment_tree:add(pf_monotonic_time, segment_data_range:range(2,8), nstime) 392 | elseif extended_id == 0x88a1f7c7 then 393 | local num_batteries = segment_data_range:range(0,2):uint() 394 | local extended_segment_tree = tree:add(pf_battery_levels, segment_range, "") 395 | extended_segment_tree:add(pf_extended_id, id_range) 396 | extended_segment_tree:add(pf_segment_length, length_range) 397 | extended_segment_tree:add(pf_battery_levels_count, segment_data_range:range(0,2)) 398 | local local_pos = 2 399 | for i=1,num_batteries,1 do 400 | extended_segment_tree:add(pf_battery_levels_id, segment_data_range:range(local_pos,2)) 401 | extended_segment_tree:add(pf_battery_levels_level, segment_data_range:range(local_pos+2,2)) 402 | local_pos = local_pos + 4 403 | end 404 | if num_batteries == 1 then 405 | extended_segment_tree:append_text(tostring(segment_data_range:range(4,2):uint())) 406 | else 407 | extended_segment_tree:append_text(num_batteries) 408 | extended_segment_tree:append_text(" batteries") 409 | end 410 | else 411 | local extended_segment_tree = tree:add(pf_unknown, segment_range, tostring(segment_data_range:bytes()):lower(), nil, segment_data_range:len(), "bytes") 412 | extended_segment_tree:add(pf_extended_id, id_range) 413 | extended_segment_tree:add(pf_segment_length, length_range) 414 | extended_segment_tree:add(pf_unknown_data, segment_data_range) 415 | end 416 | end 417 | 418 | local segments_count_tree = extended_tree:add(pf_extended_count, data_range, num_extended_segments) 419 | segments_count_tree:set_generated() 420 | 421 | extended_tree:append_text(num_extended_segments) 422 | if num_extended_segments == 1 then 423 | extended_tree:append_text(" segment") 424 | else 425 | extended_tree:append_text(" segments") 426 | end 427 | end 428 | 429 | if opcode_remaining:len() > 0 then 430 | tree:add(pf_padding, opcode_remaining) 431 | end 432 | 433 | local info_append = "" 434 | if reply_requested_field()() or in_reply_to_field()() then 435 | if reply_requested_field()() and in_reply_to_field()() then 436 | info_append = info_append .. " [RR,IRT]" 437 | elseif reply_requested_field()() then 438 | info_append = info_append .. " [RR]" 439 | elseif in_reply_to_field()() then 440 | info_append = info_append .. " [IRT]" 441 | end 442 | end 443 | if investigation_request_field()() or investigation_replied_field()() or investigation_lost_field()() then 444 | local inv_append = " [" 445 | local inv_printed = false 446 | if investigation_request_field()() then 447 | if inv_printed then 448 | inv_append = inv_append .. "," 449 | end 450 | inv_append = inv_append .. "?" 451 | inv_printed = true 452 | end 453 | if investigation_replied_field()() then 454 | if inv_printed then 455 | inv_append = inv_append .. "," 456 | end 457 | inv_append = inv_append .. ">" 458 | inv_printed = true 459 | end 460 | if investigation_lost_field()() then 461 | if inv_printed then 462 | inv_append = inv_append .. "," 463 | end 464 | inv_append = inv_append .. "<" 465 | inv_printed = true 466 | end 467 | inv_append = inv_append .. "]" 468 | info_append = info_append .. inv_append 469 | end 470 | info_append = info_append .. " ID=" .. tostring(tvbuf:range(4,6):ether()) 471 | pktinfo.cols.info:append(info_append) 472 | end 473 | 474 | local function heur_dissect_twoping(tvbuf,pktinfo,root) 475 | if tvbuf:len() < 12 then 476 | return false 477 | end 478 | 479 | if tvbuf:range(0,2):uint() ~= 0x3250 then 480 | return false 481 | end 482 | 483 | twoping.dissector(tvbuf,pktinfo,root) 484 | pktinfo.conversation = twoping 485 | 486 | return true 487 | end 488 | 489 | udp_table = DissectorTable.get("udp.port") 490 | udp_table:add(15998,twoping) 491 | twoping:register_heuristic("udp",heur_dissect_twoping) 492 | -------------------------------------------------------------------------------- /wireshark/2ping.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfinnie/2ping/d5b12f80188092b4b77d6c6b210ecf568859ee90/wireshark/2ping.pcap --------------------------------------------------------------------------------