pax_global_header00006660000000000000000000000064151557604040014521gustar00rootroot0000000000000052 comment=fa477fbd70d6c8515471d847f8018edb0135e996 kyan001-ping3-fa477fb/000077500000000000000000000000001515576040400144245ustar00rootroot00000000000000kyan001-ping3-fa477fb/.github/000077500000000000000000000000001515576040400157645ustar00rootroot00000000000000kyan001-ping3-fa477fb/.github/workflows/000077500000000000000000000000001515576040400200215ustar00rootroot00000000000000kyan001-ping3-fa477fb/.github/workflows/unittests.yml000066400000000000000000000031631515576040400226110ustar00rootroot00000000000000name: Unit Tests on: [push, workflow_dispatch] jobs: ping3-test-ping: runs-on: macos-latest # ubuntu-latest and windows-latest are blocking inbound ICMP packets. More: https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#cloud-hosts-for-github-hosted-runners name: Ping3 Ping Test steps: - name: Checkout uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: '3.x' - name: Echo Python Version run: python --version - run: sudo python tests/test_ping3.py # privileged - run: python tests/test_ping3.py # unprivileged ping3-test-command-line: runs-on: macos-latest name: Ping3 Command Line Test steps: - name: Checkout uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: '3.x' - name: Echo Python Version run: python --version - run: sudo python tests/test_command_line.py # privileged - run: python tests/test_command_line.py # unprivileged ping3-test-benchmark: runs-on: macos-latest name: Ping3 Benchmark Test needs: [ping3-test-ping, ping3-test-command-line] steps: - name: Checkout uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: '3.x' - name: Echo Python Version run: python --version - run: sudo python tests/test_benchmark.py # privileged - run: python tests/test_benchmark.py # unprivileged kyan001-ping3-fa477fb/.gitignore000066400000000000000000000022301515576040400164110ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # macOS .DS_Store kyan001-ping3-fa477fb/CHANGELOG.md000066400000000000000000000070001515576040400162320ustar00rootroot00000000000000# Change Log * 5.1.0: * Feature: Support TTL (Hop Limit) for IPv6 on Linux. * 5.0.0: * Feature: Support IPv6 ping. ( #85 ) * 4.0.8: * Bug Fix: Command does not respect options with `-v/--version` and `-h/--help`. ( #80 ) * 4.0.7: * Bug Fix: Remove unsupported type hints in lower version of Python. ( #79 ) * 4.0.6: * Bug Fix: Type hint and comments refines. ( #78 ) * 4.0.5: * Bug Fix: Type hint refines. ( #78 ) * 4.0.4: * Improvement: Replace setup.py by pyproject.toml * 4.0.2: * Bug Fix: Arg `src` in `verbose_ping` should be `src_addr`. ( #57 ) * 4.0.1: * Bug Fix: `message` should be the first argument in ping3.errors. ( #55 ) * 4.0.0: * Feature: Now errors `TimeToLiveExpired`, `DestinationUnreachable` and `DestinationHostUnreachable` have `ip_header` and `icmp_header` attached. ( #48 ) * 3.0.1: * Bug Fix: `verbose_ping` prints proper message on error. * 3.0.0: * Backward Compatibility: Only Command-line options changed, now the options is more like `ping` on macOS and Linux. * `-w`/`--wait` -> `-t`/`--timeout`. * `-t`/`--ttl` -> `-T`/`--ttl`. * `-l`/`--load` -> `-s`/`--size`. * Improvement: 2 command-line options now have short forms. * `-D` is added as the short form of `--debug`. * `-E` is added as the short form of `--exceptions`. * Feature: Use new command-line option `-S`/`--src` to set source address `src_addr`. * 2.9.3: * Bug Fix: Set packet receive buffer size to 1500. ( #40 ) * 2.9.2: * Improvement: Converted to a proper package. ( #38 #39 ) * 2.9.1: * Bug Fix: macOS is not treated as Linux now. * 2.9.0: * Feature: Support root-less ICMP pings on Linux. ( #10 ) * 2.8.1: * Improvement: Checksum calculation is faster. * 2.8.0: * Feature: Now support endless ping, using `ping3 -c 0 example.com` with a count of 0 or `ping3.verbose_ping('example.com', count=0)` to start, using `ctrl + c` to stop. * 2.7.0: * Feature: Using `SOCK_DGRAM` instead of `SOCK_RAW` on macOS. According to [this](https://apple.stackexchange.com/questions/312857/how-does-macos-allow-standard-users-to-ping), `SOCK_DGRAM` can be sent by standard user on macOS. * 2.6.6: * Bug Fix: `setsockopt` error for `SOL_IP.IP_TTL` on windows. ( #28 ) * 2.6.5: * Bug Fix: When multi-processing or multi-threading, icmp_id will no longer collision. ( #23 ) * 2.6.1: * Feature: Add network interface binding support for Linux. ( #22 ) * 2.5.1: * Features: * Add interval support to `ping3.verbose_ping()`. ( #17 ) * Add `-i/--interval` argument for interval support in command-line. * 2.4.7: * Bug Fix: Input parameter `size` in `ping()` should not include ICMP_Header size. ( #21 ) * 2.4.4: * Bug Fix: When there are a lot of incoming packets, and the destination address has no response, `ping()` never return. ( #14 ) * 2.4.0: * Feature: Return False if HostUnknown error raised (instead of print info to screen). * Improvement: Increase usability of errors and provide a more precisely text. * 2.3.1: * Features: * Add `--debug` argument for DEBUG mode in command-line. * Add `--exceptions` argument for EXCEPTIONS mode in command-line. * 2.2.3: * Feature: Add command-line mode. * 2.0.1: * Features: * Add support of ICMP Payload size. * Add support of EXCEPTIONS mode. * Add support of DEBUG mode. * 1.2.1: * Feature: Add support for multiple interfaces. Use `ping(..., src_addr="INTERFACE IP")` * 1.1.0: * Improvement: Update tests and PyPI info. kyan001-ping3-fa477fb/LICENSE000066400000000000000000000020571515576040400154350ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 Kyan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. kyan001-ping3-fa477fb/README.md000066400000000000000000000264041515576040400157110ustar00rootroot00000000000000# Ping3 [![Build Status](https://travis-ci.org/kyan001/ping3.svg?branch=master)](https://travis-ci.org/kyan001/ping3) ![GitHub release](https://img.shields.io/github/release/kyan001/ping3.svg) [![GitHub license](https://img.shields.io/github/license/kyan001/ping3.svg)](https://github.com/kyan001/ping3/blob/master/LICENSE) ![PyPI - Downloads](https://img.shields.io/pypi/dm/ping3.svg) Ping3 is a pure python3 version of ICMP ping implementation using raw socket.\ (Note that on some platforms, ICMP messages can only be sent from processes running as root.) > The Python2 version originally from [here](http://github.com/samuel/python-ping).\ > This version maintained at [this github repo](https://github.com/kyan001/ping3). 🌐 English | [Русский](README.ru.md) ✨ [CHANGELOG](CHANGELOG.md) ## Get Started * If you met "permission denied", you may need to run this as root. Alternatively see [this](./TROUBLESHOOTING.md#permission-denied-on-linux) for troubleshooting on linux. ```sh pip install ping3 # install ping ``` ```python >>> from ping3 import ping, verbose_ping >>> ping('example.com') # Returns delay in seconds. 0.215697261510079666 >>> verbose_ping('example.com') # Ping 4 times in a row. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms ``` ```sh $ ping3 example.com # Verbose ping. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms ``` ## Installation ```sh pip install ping3 # install ping3 pip install --upgrade ping3 # upgrade ping3 pip uninstall ping3 # uninstall ping3 ``` ## Functions ```python >>> from ping3 import ping, verbose_ping >>> ping('example.com') # Returns delay in seconds. 0.215697261510079666 # `0.0` returned means the delay is lower than the precision of `time.time()`. >>> ping('not.exist.com') # If host unknown (cannot resolve), returns False. False >>> ping("224.0.0.0") # If timed out (no reply), returns None. None >>> ping("2600:1406:bc00:53::b81e:94ce") # Ping an IPv6 address. 0.215697261510079666 # Returns delay in seconds. >>> ping('example.com', version=6) # Force ping an IPv6 address. 4 for IPv4, 6 for IPv6. Default is None for auto-detect. 0.215697261510079666 >>> ping('example.com', timeout=10) # Set timeout to 10 seconds. Default timeout is 4 for 4 seconds. 0.215697261510079666 >>> ping('example.com', unit='ms') # Returns delay in milliseconds. Default unit is 's' for seconds. 215.9627876281738 >>> ping('example.com', src_addr='192.168.1.15') # Set source ip address for multiple interfaces. Default src_addr is None for no binding. 0.215697261510079666 >>> ping('example.com', interface='eth0') # LINUX ONLY. Set source interface for multiple network interfaces. Default interface is None for no binding. 0.215697261510079666 >>> ping('example.com', ttl=5) # Set packet Time-To-Live to 5. The packet is discarded if it does not reach the target host after 5 jumps. Default ttl is 64. None >>> ping('example.com', size=56) # Set ICMP packet payload to 56 bytes. The total ICMP packet size is 8 (header) + 56 (payload) = 64 bytes. Default size is 56. 0.215697261510079666 >>> verbose_ping('example.com') # Ping 4 times in a row. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms >>> verbose_ping('example.com', timeout=10) # Set timeout to 10 seconds. Default timeout is 4 for 4 seconds. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms >>> verbose_ping('example.com', count=6) # Ping 6 times. Default count is 4. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms ping 'example.com' ... 215ms ping 'example.com' ... 216ms >>> verbose_ping('example.com', count=0) # Ping endlessly (0 means infinite loops). Using `ctrl + c` to stop manully. ping 'example.com' ... 215ms ... >>> verbose_ping('example.com', src_addr='192.168.1.15') # Ping from source IP address for multiple interfaces. Default src_addr is None. ping 'example.com' from '192.168.1.15' ... 215ms ping 'example.com' from '192.168.1.15' ... 216ms ping 'example.com' from '192.168.1.15' ... 219ms ping 'example.com' from '192.168.1.15' ... 217ms >>> verbose_ping('example.com', interface='wifi0') # LINUX ONLY. Ping from network interface 'wifi0'. Default interface is None. ping 'example.com' from '192.168.1.15' ... 215ms ping 'example.com' from '192.168.1.15' ... 216ms ping 'example.com' from '192.168.1.15' ... 219ms ping 'example.com' from '192.168.1.15' ... 217ms >>> verbose_ping('example.com', unit='s') # Displays delay in seconds. Default unit is "ms" for milliseconds. ping 'example.com' ... 1s ping 'example.com' ... 2s ping 'example.com' ... 1s ping 'example.com' ... 1s >>> verbose_ping('example.com', ttl=5) # Set TTL to 5. Default is 64. ping 'example.com' ... Timeout ping 'example.com' ... Timeout ping 'example.com' ... Timeout ping 'example.com' ... Timeout >>> verbose_ping('example.com', interval=5) # Wait 5 seconds between each packet. Default is 0. ping 'example.com' ... 215ms # wait 5 secs ping 'example.com' ... 216ms # wait 5 secs ping 'example.com' ... 219ms # wait 5 secs ping 'example.com' ... 217ms >>> verbose_ping('example.com', size=56) # Set ICMP payload to 56 bytes. Default size is 56. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms >>> verbose_ping('example.com', version=6) # Force ping an IPv6 address. 4 for IPv4, 6 for IPv6. Default is None for auto-detect. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms ``` ### DEBUG mode Show more info for developers. ```python >>> import ping3 >>> ping3.DEBUG = True # Default is False. >>> ping3.ping("example.com") # "ping()" prints received IP header and ICMP header. [DEBUG] IP HEADER: {'version': 69, 'tos': 0, 'len': 14336, 'id': 8620, 'flags': 0, 'ttl': 51, 'protocol': 1, 'checksum': *, 'src_addr': *, 'dest_addr': *} [DEBUG] ICMP HEADER: {'type': 0, 'code': 0, 'checksum': 8890, 'id': 21952, 'seq': 0} 0.215697261510079666 >>> ping3.ping("example.com", timeout=0.0001) [DEBUG] Request timeout for ICMP packet. (Timeout = 0.0001s) None >>> ping3.ping("not.exist.com") [DEBUG] Cannot resolve: Unknown host. (Host = not.exist.com) False >>> ping3.ping("example.com", ttl=1) [DEBUG] Time exceeded: Time To Live expired. None ``` ### EXCEPTIONS mode Raise exceptions when there are errors instead of return None ```python >>> import ping3 >>> ping3.EXCEPTIONS = True # Default is False. >>> ping3.ping("example.com", timeout=0.0001) [... Traceback ...] ping3.errors.Timeout: Request timeout for ICMP packet. (Timeout = 0.0001s) >>> ping3.ping("not.exist.com") [... Traceback ...] ping3.errors.HostUnknown: Cannot resolve: Unknown host. (Host = not.exist.com) >>> ping3.ping("example.com", ttl=1) # Linux need root privilege to receive TTL expired. Windows cannot get TTL expired. [... Traceback ...] ping3.errors.TimeToLiveExpired: Time exceeded: Time To Live expired. >>> try: >>> ping3.ping("example.com", ttl=1) >>> except ping3.errors.TimeToLiveExpired as err: >>> print(err.ip_header["src_addr"]) # TimeToLiveExpired, DestinationUnreachable and DestinationHostUnreachable have ip_header and icmp_header attached. 1.2.3.4 # IP address where the TTL happened. >>> help(ping3.errors) # More info about exceptions. ``` ```python import ping3 ping3.EXCEPTIONS = True try: ping3.ping("not.exist.com") except ping3.errors.HostUnknown: # Specific error is catched. print("Host unknown error raised.") except ping3.errors.PingError: # All ping3 errors are subclasses of `PingError`. print("A ping error raised.") ``` ## Command Line Execution Execute ping3 from command-line. Note: On some platforms, `ping3` needs root privilege to send/receive packets. You may want to use `sudo ping3`. ```sh $ ping3 --help # -h/--help. Command-line help message. $ python -m ping3 --help # Same as `ping3`. `ping3` is an alias for `python -m ping3`. $ ping3 --version # -v/--version. Show ping3 version number. 3.0.0 $ ping3 example.com # Verbose ping. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms $ ping3 example.com 8.8.8.8 # Verbose ping all the addresses. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms ping '8.8.8.8' ... 5ms ping '8.8.8.8' ... 2ms ping '8.8.8.8' ... 6ms ping '8.8.8.8' ... 5ms $ ping3 --count 1 example.com # -c/--count. How many pings should be sent. Default is 4. ping 'example.com' ... 215ms $ ping3 --count 0 example.com # Ping endlessly (0 means infinite loops). Using `ctrl + c` to stop manully. ping 'example.com' ... 215ms (*repeat*) $ ping3 --timeout 10 example.com # -t/--timeout. Set timeout to 10 seconds. Default is 4. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms $ ping3 --ttl 5 example.com # -T/--ttl. # Set TTL to 5. Default is 64. ping 'example.com' ... Timeout ping 'example.com' ... Timeout ping 'example.com' ... Timeout ping 'example.com' ... Timeout $ ping3 --size 56 example.com # -s/--size. Set ICMP packet payload to 56 bytes. Default is 56. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms $ ping3 --interval 5 example.com # -i/--interval. Wait 5 seconds between each packet. Default is 0. ping 'example.com' ... 215ms # wait 5 secs ping 'example.com' ... 216ms # wait 5 secs ping 'example.com' ... 219ms # wait 5 secs ping 'example.com' ... 217ms $ ping3 --interface eth0 example.com # -I/--interface. LINUX ONLY. The gateway network interface to ping from. Default is None. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms $ ping3 --src 192.168.1.15 example.com # -S/--src. Ping from source IP address for multiple network interfaces. Default is None. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms $ ping3 -4 example.com # -4/--ipv4. Force ping an IPv4 address. Default is None for auto-detect. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms $ ping3 -6 example.com # -6/--ipv6. Force ping an IPv6 address. Default is None for auto-detect. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms $ ping3 --exceptions --timeout 0.001 example.com # -E/--exceptions. EXCPETIONS mode is on when this shows up. [... Traceback ...] ping3.errors.Timeout: Request timeout for ICMP packet. (Timeout = 0.0001s) $ ping3 --debug --timeout 0.001 example.com # -D/--debug. DEBUG mode is on when this shows up. [DEBUG] Request timeout for ICMP packet. (Timeout = 0.001s) ping 'example.com' ... Timeout > 0.001s [DEBUG] Request timeout for ICMP packet. (Timeout = 0.001s) ping 'example.com' ... Timeout > 0.001s [DEBUG] Request timeout for ICMP packet. (Timeout = 0.001s) ping 'example.com' ... Timeout > 0.001s [DEBUG] Request timeout for ICMP packet. (Timeout = 0.001s) ping 'example.com' ... Timeout > 0.001s ``` kyan001-ping3-fa477fb/README.ru.md000066400000000000000000000342351515576040400163370ustar00rootroot00000000000000# Ping3 [![Build Status](https://travis-ci.org/kyan001/ping3.svg?branch=master)](https://travis-ci.org/kyan001/ping3) ![GitHub release](https://img.shields.io/github/release/kyan001/ping3.svg) [![GitHub license](https://img.shields.io/github/license/kyan001/ping3.svg)](https://github.com/kyan001/ping3/blob/master/LICENSE) ![PyPI - Downloads](https://img.shields.io/pypi/dm/ping3.svg) Ping3 — это реализация ICMP ping на чистом Python 3 с использованием сырых сокетов.\ (Обратите внимание: на некоторых платформах ICMP-сообщения могут отправляться только процессами с правами суперпользователя.) > Версия для Python 2 изначально взята [отсюда](http://github.com/samuel/python-ping).\ > Данная версия поддерживается в [этом репозитории на GitHub](https://github.com/kyan001/ping3). 🌐 [English](README.md) | Русский ✨ [CHANGELOG](CHANGELOG.md) ## Начало работы * Если вы получили ошибку «permission denied», возможно, потребуется запустить программу с правами суперпользователя. Также см. [это руководство](./TROUBLESHOOTING.md#permission-denied-on-linux) по устранению неполадок на Linux. ```sh pip install ping3 # установить ping3 ``` ```python >>> from ping3 import ping, verbose_ping >>> ping('example.com') # Возвращает задержку в секундах. 0.215697261510079666 >>> verbose_ping('example.com') # Пинг 4 раза подряд. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms ``` ```sh $ ping3 example.com # Подробный пинг. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms ``` ## Установка ```sh pip install ping3 # установить ping3 pip install --upgrade ping3 # обновить ping3 pip uninstall ping3 # удалить ping3 ``` ## Функции ```python >>> from ping3 import ping, verbose_ping >>> ping('example.com') # Возвращает задержку в секундах. 0.215697261510079666 # `0.0` означает, что задержка ниже точности `time.time()`. >>> ping('not.exist.com') # Если хост неизвестен (не удалось разрешить), возвращает False. False >>> ping("224.0.0.0") # Если истекло время ожидания (нет ответа), возвращает None. None >>> ping("2600:1406:bc00:53::b81e:94ce") # Пинг IPv6-адреса. 0.215697261510079666 # Возвращает задержку в секундах. >>> ping('example.com', version=6) # Принудительный пинг по IPv6. 4 для IPv4, 6 для IPv6. По умолчанию None (автоопределение). 0.215697261510079666 >>> ping('example.com', timeout=10) # Установить таймаут 10 секунд. По умолчанию 4 секунды. 0.215697261510079666 >>> ping('example.com', unit='ms') # Возвращает задержку в миллисекундах. По умолчанию 's' (секунды). 215.9627876281738 >>> ping('example.com', src_addr='192.168.1.15') # Задать исходный IP-адрес для множественных интерфейсов. По умолчанию None (без привязки). 0.215697261510079666 >>> ping('example.com', interface='eth0') # ТОЛЬКО ДЛЯ LINUX. Задать исходный сетевой интерфейс. По умолчанию None (без привязки). 0.215697261510079666 >>> ping('example.com', ttl=5) # Установить TTL пакета равным 5. Пакет отбрасывается, если не достигает цели за 5 прыжков. По умолчанию 64. None >>> ping('example.com', size=56) # Установить полезную нагрузку ICMP-пакета 56 байт. Общий размер пакета: 8 (заголовок) + 56 (данные) = 64 байта. По умолчанию 56. 0.215697261510079666 >>> verbose_ping('example.com') # Пинг 4 раза подряд. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms >>> verbose_ping('example.com', timeout=10) # Установить таймаут 10 секунд. По умолчанию 4 секунды. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms >>> verbose_ping('example.com', count=6) # Пинг 6 раз. По умолчанию 4. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms ping 'example.com' ... 215ms ping 'example.com' ... 216ms >>> verbose_ping('example.com', count=0) # Бесконечный пинг (0 — бесконечные повторения). Остановить вручную: `ctrl + c`. ping 'example.com' ... 215ms ... >>> verbose_ping('example.com', src_addr='192.168.1.15') # Пинг с указанного исходного IP для множественных интерфейсов. По умолчанию None. ping 'example.com' from '192.168.1.15' ... 215ms ping 'example.com' from '192.168.1.15' ... 216ms ping 'example.com' from '192.168.1.15' ... 219ms ping 'example.com' from '192.168.1.15' ... 217ms >>> verbose_ping('example.com', interface='wifi0') # ТОЛЬКО LINUX. Пинг через сетевой интерфейс 'wifi0'. По умолчанию None. ping 'example.com' from '192.168.1.15' ... 215ms ping 'example.com' from '192.168.1.15' ... 216ms ping 'example.com' from '192.168.1.15' ... 219ms ping 'example.com' from '192.168.1.15' ... 217ms >>> verbose_ping('example.com', unit='s') # Отображать задержку в секундах. По умолчанию "ms" (миллисекунды). ping 'example.com' ... 1s ping 'example.com' ... 2s ping 'example.com' ... 1s ping 'example.com' ... 1s >>> verbose_ping('example.com', ttl=5) # Установить TTL равным 5. По умолчанию 64. ping 'example.com' ... Timeout ping 'example.com' ... Timeout ping 'example.com' ... Timeout ping 'example.com' ... Timeout >>> verbose_ping('example.com', interval=5) # Ждать 5 секунд между пакетами. По умолчанию 0. ping 'example.com' ... 215ms # ждать 5 сек ping 'example.com' ... 216ms # ждать 5 сек ping 'example.com' ... 219ms # ждать 5 сек ping 'example.com' ... 217ms >>> verbose_ping('example.com', size=56) # Установить полезную нагрузку ICMP 56 байт. По умолчанию 56. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms >>> verbose_ping('example.com', version=6) # Принудительный пинг по IPv6. 4 для IPv4, 6 для IPv6. По умолчанию None (автоопределение). ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms ``` ### Режим DEBUG Отображает дополнительную информацию для разработчиков. ```python >>> import ping3 >>> ping3.DEBUG = True # По умолчанию False. >>> ping3.ping("example.com") # "ping()" выводит полученный IP-заголовок и ICMP-заголовок. [DEBUG] IP HEADER: {'version': 69, 'tos': 0, 'len': 14336, 'id': 8620, 'flags': 0, 'ttl': 51, 'protocol': 1, 'checksum': *, 'src_addr': *, 'dest_addr': *} [DEBUG] ICMP HEADER: {'type': 0, 'code': 0, 'checksum': 8890, 'id': 21952, 'seq': 0} 0.215697261510079666 >>> ping3.ping("example.com", timeout=0.0001) [DEBUG] Request timeout for ICMP packet. (Timeout = 0.0001s) None >>> ping3.ping("not.exist.com") [DEBUG] Cannot resolve: Unknown host. (Host = not.exist.com) False >>> ping3.ping("example.com", ttl=1) [DEBUG] Time exceeded: Time To Live expired. None ``` ### Режим EXCEPTIONS Вызывает исключения при ошибках вместо возврата None. ```python >>> import ping3 >>> ping3.EXCEPTIONS = True # По умолчанию False. >>> ping3.ping("example.com", timeout=0.0001) [... Traceback ...] ping3.errors.Timeout: Request timeout for ICMP packet. (Timeout = 0.0001s) >>> ping3.ping("not.exist.com") [... Traceback ...] ping3.errors.HostUnknown: Cannot resolve: Unknown host. (Host = not.exist.com) >>> ping3.ping("example.com", ttl=1) # На Linux нужны права суперпользователя для получения TTL expired. На Windows TTL expired недоступен. [... Traceback ...] ping3.errors.TimeToLiveExpired: Time exceeded: Time To Live expired. >>> try: >>> ping3.ping("example.com", ttl=1) >>> except ping3.errors.TimeToLiveExpired as err: >>> print(err.ip_header["src_addr"]) # TimeToLiveExpired, DestinationUnreachable и DestinationHostUnreachable содержат ip_header и icmp_header. 1.2.3.4 # IP-адрес, на котором произошёл TTL expired. >>> help(ping3.errors) # Подробнее об исключениях. ``` ```python import ping3 ping3.EXCEPTIONS = True try: ping3.ping("not.exist.com") except ping3.errors.HostUnknown: # Перехватывается конкретная ошибка. print("Host unknown error raised.") except ping3.errors.PingError: # Все ошибки ping3 являются подклассами `PingError`. print("A ping error raised.") ``` ## Запуск из командной строки (terminal) Запуск ping3 из командной строки. Примечание: на некоторых платформах `ping3` требует прав суперпользователя для отправки/приёма пакетов. Используйте `sudo ping3`. ```sh $ ping3 --help # -h/--help. Справочное сообщение командной строки. $ python -m ping3 --help # То же, что `ping3`. `ping3` — псевдоним для `python -m ping3`. $ ping3 --version # -v/--version. Показать номер версии ping3. 3.0.0 $ ping3 example.com # Подробный пинг. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms $ ping3 example.com 8.8.8.8 # Подробный пинг нескольких адресов. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms ping '8.8.8.8' ... 5ms ping '8.8.8.8' ... 2ms ping '8.8.8.8' ... 6ms ping '8.8.8.8' ... 5ms $ ping3 --count 1 example.com # -c/--count. Сколько пингов отправить. По умолчанию 4. ping 'example.com' ... 215ms $ ping3 --count 0 example.com # Бесконечный пинг (0 — бесконечные повторения). Остановить вручную: `ctrl + c`. ping 'example.com' ... 215ms (*repeat*) $ ping3 --timeout 10 example.com # -t/--timeout. Установить таймаут 10 секунд. По умолчанию 4. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms $ ping3 --ttl 5 example.com # -T/--ttl. Установить TTL равным 5. По умолчанию 64. ping 'example.com' ... Timeout ping 'example.com' ... Timeout ping 'example.com' ... Timeout ping 'example.com' ... Timeout $ ping3 --size 56 example.com # -s/--size. Установить полезную нагрузку ICMP-пакета 56 байт. По умолчанию 56. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms $ ping3 --interval 5 example.com # -i/--interval. Ждать 5 секунд между пакетами. По умолчанию 0. ping 'example.com' ... 215ms # ждать 5 сек ping 'example.com' ... 216ms # ждать 5 сек ping 'example.com' ... 219ms # ждать 5 сек ping 'example.com' ... 217ms $ ping3 --interface eth0 example.com # -I/--interface. ТОЛЬКО ДЛЯ LINUX. Сетевой интерфейс шлюза для пинга. По умолчанию None. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms $ ping3 --src 192.168.1.15 example.com # -S/--src. Пинг с указанного исходного IP для множественных сетевых интерфейсов. По умолчанию None. ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms $ ping3 -4 example.com # -4/--ipv4. Принудительный пинг по IPv4. По умолчанию None (автоопределение). ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms $ ping3 -6 example.com # -6/--ipv6. Принудительный пинг по IPv6. По умолчанию None (автоопределение). ping 'example.com' ... 215ms ping 'example.com' ... 216ms ping 'example.com' ... 219ms ping 'example.com' ... 217ms $ ping3 --exceptions --timeout 0.001 example.com # -E/--exceptions. Режим EXCEPTIONS включается при указании этого флага. [... Traceback ...] ping3.errors.Timeout: Request timeout for ICMP packet. (Timeout = 0.0001s) $ ping3 --debug --timeout 0.001 example.com # -D/--debug. Режим DEBUG включается при указании этого флага. [DEBUG] Request timeout for ICMP packet. (Timeout = 0.001s) ping 'example.com' ... Timeout > 0.001s [DEBUG] Request timeout for ICMP packet. (Timeout = 0.001s) ping 'example.com' ... Timeout > 0.001s [DEBUG] Request timeout for ICMP packet. (Timeout = 0.001s) ping 'example.com' ... Timeout > 0.001s [DEBUG] Request timeout for ICMP packet. (Timeout = 0.001s) ping 'example.com' ... Timeout > 0.001s ``` kyan001-ping3-fa477fb/TROUBLESHOOTING.md000066400000000000000000000022371515576040400172410ustar00rootroot00000000000000# TROUBLESHOOTING ## "Permission Denied" on Linux Linux uses a kernel parameter (`net.ipv4.ping_group_range`) to restrict who can create ICMP (`ping`) sockets. From [kernel.org](https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt): > ping_group_range - 2 INTEGERS > > Restrict ICMP_PROTO datagram sockets to users in the group range. > The default is "1 0", meaning, that nobody (not even root) may > create ping sockets. Setting it to "100 100" would grant permissions > to the single group. "0 4294967295" would enable it for the world, "100 > 4294967295" would enable it for the users, but not daemons. To *temporarily* set this parameter, allowing all users to create ICMP sockets (until next boot): ```sh sudo sysctl net.ipv4.ping_group_range='0 4294967295' ``` Some systems (e.g. Debian) may have issues with this, returning: ```sh sysctl: setting key "net.ipv4.ping_group_range": Invalid argument ``` In this case, try: ```sh sudo sysctl net.ipv4.ping_group_range='0 2147483647' ``` To *permanently* set this parameter: ```sh echo "# allow all users to create icmp sockets\n net.ipv4.ping_group_range=0 2147483647" | sudo tee -a /etc/sysctl.conf ``` kyan001-ping3-fa477fb/ping3/000077500000000000000000000000001515576040400154445ustar00rootroot00000000000000kyan001-ping3-fa477fb/ping3/__init__.py000066400000000000000000000666611515576040400175740ustar00rootroot00000000000000#!/usr/bin/env python import os import socket import struct import select import time import platform import zlib import threading import logging import functools import errno import ipaddress from . import errors from .enums import ICMP_DEFAULT_CODE, IcmpV4Type, IcmpV4DestinationUnreachableCode, IcmpTimeExceededCode, IcmpV6Type, IcmpV6DestinationUnreachableCode __version__ = "5.1.6" DEBUG = False # DEBUG: Show debug info for developers. (default False) EXCEPTIONS = False # EXCEPTIONS: Raise exception when delay is not available. LOGGER = None # LOGGER: Record logs into console or file. Logger object should have .debug() method. # !=Network Byte Order(Big-Endian), B=Bytes (8), I=Integer (32), H=Unsigned short (16), B=Unsigned char (8) IPV4_HEADER_FORMAT = "!BBHHHBBHII" # B: Version (4) + IHL (4). B: TOS (8). H: Total Length (16). H: ID (16). B: Flags (3) + Fragment Offset (13). B: TTL (8). B: Protocol (8). H: Header Checksum (16). I: Source Address (32). I: Destination Address (32) IPV6_HEADER_FORMAT = "!IHBB16s16s" # I: Version (4) + Traffic Class (8) + Flow Label (20). H: Payload Length (16). B: Next Header (8). B: Hop Limit (8). 16s: Source Address (128). 16s: Destination Address (128). ICMP_HEADER_FORMAT = "!BBHHH" # According to netinet/ip_icmp.h. B: Type (8). B: Code (8). H: Checksum (16). H: ID (16). H: Sequence Number (16). ICMP_TIME_FORMAT = "!d" # d=double ICMPV6_PSEUDO_HEADER_FORMAT = "!16s16sIBBBB" # 16s: Source Address (128), 16s: Destination Address (128), I: ICMPv6 Length (32), B: Zeros (24), B: Next Header (8) SOCKET_SO_BINDTODEVICE = 25 # socket.SO_BINDTODEVICE def _debug(*args) -> None: """Print debug info to stdout if `ping3.DEBUG` is True. Args: *args (any): Usually are strings or objects that can be converted to str. """ def get_logger(): logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) formatter = logging.Formatter("[%(levelname)s] %(message)s") cout_handler = logging.StreamHandler() cout_handler.setLevel(logging.DEBUG) cout_handler.setFormatter(formatter) logger.addHandler(cout_handler) logger.debug("Ping3 Version: {}".format(__version__)) logger.debug("LOGGER: {}".format(logger)) return logger if not DEBUG: return None global LOGGER LOGGER = LOGGER or get_logger() message = " ".join(str(item) for item in args) LOGGER.debug(message) def _raise(err: Exception) -> None: """Raise exception if `ping3.EXCEPTIONS` is True. Args: err (Exception): Exception to be raised. Raise: Exception: Exception passed in args will be raised if `ping3.EXCEPTIONS` is True. """ if EXCEPTIONS: raise err def _func_logger(func): """Decorator that log function calls for debug Args: func (callable): Function to be decorated. Returns: callable: Decorated function. """ @functools.wraps(func) def wrapper(*args, **kwargs): pargs = ", ".join(str(arg) for arg in args) kargs = str(kwargs) if kwargs else "" all_args = ", ".join((pargs, kargs)) if (pargs and kargs) else (pargs or kargs) _debug("Function called:", "{func.__name__}({})".format(all_args, func=func)) func_return = func(*args, **kwargs) _debug("Function returned:", "{func.__name__} -> {rtrn}".format(func=func, rtrn=func_return)) return func_return return wrapper def is_ipv4(sock: socket.socket) -> bool: """Check if the socket is IPv4. Args: sock (socket.socket): The socket to be checked. Returns: bool: True if the socket is IPv4, False otherwise. """ return sock.family == socket.AF_INET def checksum(source: bytes) -> int: """Calculates the checksum of the input bytes. RFC1071: https://tools.ietf.org/html/rfc1071 RFC792: https://tools.ietf.org/html/rfc792 Args: source (Bytes): The input to be calculated. Returns: int: Calculated checksum. """ BITS = 16 # 16-bit long carry = 1 << BITS # 0x10000 result = sum(source[::2]) + (sum(source[1::2]) << (BITS // 2)) # Even bytes (odd indexes) shift 1 byte to the left. while result >= carry: # Ones' complement sum. result = sum(divmod(result, carry)) # Each carry add to right most bit. return ~result & ((1 << BITS) - 1) # Ensure 16-bit def read_icmp_header(raw: bytes) -> dict: """Get information from raw ICMP header data. Args: raw (Bytes): Raw data of ICMP header. Returns: dict: A map contains the infos from the raw header. """ icmp_header_keys = ("type", "code", "checksum", "id", "seq") return dict(zip(icmp_header_keys, struct.unpack(ICMP_HEADER_FORMAT, raw))) def read_ipv4_header(raw: bytes) -> dict: """Get information from raw IPv4 header data. Args: raw (Bytes): Raw data of IPv4 header. Returns: dict: A map contains the infos from the raw header. """ def stringify_ip(ip: int) -> str: return ".".join(str(ip >> offset & 0xFF) for offset in (24, 16, 8, 0)) # str(ipaddress.ip_address(ip)) ipv4_header_keys = ("version", "tos", "len", "id", "flags", "ttl", "protocol", "checksum", "src_addr", "dest_addr") ipv4_header = dict(zip(ipv4_header_keys, struct.unpack(IPV4_HEADER_FORMAT, raw))) ipv4_header["src_addr"] = stringify_ip(ipv4_header["src_addr"]) ipv4_header["dest_addr"] = stringify_ip(ipv4_header["dest_addr"]) return ipv4_header def read_ipv6_header(raw: bytes) -> dict: """Get information from raw IPv6 header data. Args: raw (Bytes): Raw data of IPv6 header. Returns: dict: A map contains the infos from the raw header. """ ipv6_header_keys = ("initial", "len", "next_header", "hop_limit", "src_addr", "dest_addr") ipv6_header_unpacked = dict(zip(ipv6_header_keys, struct.unpack(IPV6_HEADER_FORMAT, raw[:40]))) ipv6_header = { "len": ipv6_header_unpacked["len"], "next_header": ipv6_header_unpacked["next_header"], "hop_limit": ipv6_header_unpacked["hop_limit"], } ipv6_header["version"] = (ipv6_header_unpacked["initial"] >> 28) & 0x0F # First 32 bits is combined version (4), traffic class (8) and flow label (20). ipv6_header["traffic_class"] = (ipv6_header_unpacked["initial"] >> 20) & 0xFF ipv6_header["flow_label"] = ipv6_header_unpacked["initial"] & 0xFFFFF ipv6_header["src_addr"] = socket.inet_ntop(socket.AF_INET6, ipv6_header_unpacked["src_addr"]) # Convert source address to readable format. ipv6_header["dest_addr"] = socket.inet_ntop(socket.AF_INET6, ipv6_header_unpacked["dest_addr"]) # Convert destination address to readable format. return ipv6_header @_func_logger def send_one_ping(sock: socket.socket, dest_addr: str, icmp_id: int, seq: int, size: int) -> None: """Sends one ping to the given destination. ICMP Header (bits): type (8), code (8), checksum (16), id (16), sequence (16) ICMP Payload: time (double), data ICMP Wikipedia: https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol ICMPv6 Wikipedia: https://en.wikipedia.org/wiki/ICMPv6 IPv4 Wikipedia: https://en.wikipedia.org/wiki/IPv4 IPv6 Wikipedia: https://en.wikipedia.org/wiki/IPv6_packet Args: sock (socket.socket): Socket. dest_addr: The destination address, can be an IPv4 address or an IPv6 address or a domain name. Ex. "192.168.1.1"/"example.com"/"2001:db8::1" Socket address is (dest_addr, port) with IPv4 and (dest_addr, port, flowinfo, scopeid) with IPv6. Port is 0 respectively the OS default behavior will be used. With IPv6, Flowinfo is 0, Scope ID is the interface index if dest_addr is a link-local address. icmp_id (int): ICMP packet id. Calculated from Process ID and Thread ID. seq (int): ICMP packet sequence, usually increases from 0 in the same process. size (int): The ICMP packet payload size in bytes. Note this is only for the payload part. Raises: HostUnkown: If destination address is a domain name and cannot resolved. """ _debug("Destination address:", dest_addr) try: # Resolve domain name to IP address if needed. if is_ipv4(sock): dest_addr = socket.gethostbyname(dest_addr) # Domain name will translated into IP address, and IP address leaves unchanged. sock_addr = (dest_addr, 0) # For IPv4, sock_addr is (dest_addr, port). Port is 0 respectively the OS default behavior will be used. else: sock_addr = socket.getaddrinfo(dest_addr, None, socket.AF_INET6, socket.SOCK_RAW, socket.IPPROTO_ICMPV6)[0][4] # For IPv6, sock_addr is (dest_addr, port, flowinfo, scopeid) except socket.gaierror as err: raise errors.HostUnknown(dest_addr=dest_addr) from err _debug("Resolved destination address:", sock_addr[0]) pseudo_checksum = 0 # Pseudo checksum is used to calculate the real checksum. icmp_header = struct.pack( ICMP_HEADER_FORMAT, IcmpV4Type.ECHO_REQUEST if is_ipv4(sock) else IcmpV6Type.ECHO_REQUEST, ICMP_DEFAULT_CODE, pseudo_checksum, icmp_id, seq, ) padding = (size - struct.calcsize(ICMP_TIME_FORMAT)) * "Q" # Using double to store current time. icmp_payload = struct.pack(ICMP_TIME_FORMAT, time.time()) + padding.encode() if is_ipv4(sock): real_checksum = checksum(icmp_header + icmp_payload) # Calculates the checksum on the dummy header and the icmp_payload. else: # ICMPv6 requires a pseudo header for checksum calculation. with socket.socket(socket.AF_INET6, sock.type, sock.proto) as dummy_sock: # Create a dummy socket to get the source address. dummy_sock.connect(sock_addr) # Set default dest_addr so the OS can select the correct source address. No real data is sent. src_addr = dummy_sock.getsockname()[0] # Get the source address. _debug("Source Address: {}".format(src_addr)) pseudo_header = struct.pack( # https://en.wikipedia.org/wiki/ICMPv6#Checksum ICMPV6_PSEUDO_HEADER_FORMAT, # 16s: Source Address (128), 16s: Destination Address (128), B: Next Header (8), B: Payload Length (16) socket.inet_pton(socket.AF_INET6, src_addr), # Source Address socket.inet_pton(socket.AF_INET6, sock_addr[0]), # Destination Address len(icmp_header) + len(icmp_payload), # ICMPv6 Length 0, 0, 0, # Zeros IcmpV6Type.ECHO_REQUEST, # Next Header (ICMPv6 Type) ) real_checksum = checksum(pseudo_header + icmp_header + icmp_payload) # Calculates the checksum on the pseudo header + icmp_header + icmp_payload. icmp_header = struct.pack( ICMP_HEADER_FORMAT, IcmpV4Type.ECHO_REQUEST if is_ipv4(sock) else IcmpV6Type.ECHO_REQUEST, ICMP_DEFAULT_CODE, socket.htons(real_checksum), # Don't know why I need socket.htons() on real_checksum since ICMP_HEADER_FORMAT already in Network Bytes Order (big-endian) icmp_id, seq, ) # Put real checksum into ICMP header. _debug("Sent ICMP header:", read_icmp_header(icmp_header)) _debug("Sent ICMP payload:", icmp_payload) packet = icmp_header + icmp_payload sock.sendto(packet, sock_addr) # sock_addr = (ip, port) or (ip, port, flowinfo, scopeid). @_func_logger def receive_one_ping(sock: socket.socket, icmp_id: int, seq: int, timeout: int): """Receives the ping from the socket. IP Header (bits): version (8), type of service (8), length (16), id (16), flags (16), time to live (8), protocol (8), checksum (16), source ip (32), destination ip (32). ICMP Packet (bytes): IP Header (20), ICMP Header (8), ICMP Payload (*). Ping Wikipedia: https://en.wikipedia.org/wiki/Ping_(networking_utility) ToS (Type of Service) in IP header for ICMP is 0. Protocol in IP header for ICMP is 1. Args: sock (socket.socket): The same socket used for send the ping. icmp_id (int): ICMP packet id. Sent packet id should be identical with received packet id. seq (int): ICMP packet sequence. Sent packet sequence should be identical with received packet sequence. timeout (int): Timeout in seconds. Returns: float | None: The delay in seconds or None on timeout. Raises: TimeToLiveExpired: If the Time-To-Live in IP Header is not large enough for destination. TimeExceeded: If time exceeded but Time-To-Live does not expired. DestinationHostUnreachable: If the destination host is unreachable. DestinationUnreachable: If the destination is unreachable. """ def detect_ip_header(sock, recv_data): """Detect if the received data has an IP header. IPv4 header first 4 bits is 4 (0b0100). ICMPv4 Type starts with 4 (64~79) is unassigned. See https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol#Control_messages IPv6 header first 4 bits is 6 (0b0110). ICMPv6 Type starts with 6 (96~111) is unassigned. See https://en.wikipedia.org/wiki/ICMPv6#Types Args: sock (socket.socket): The socket used to receive the data. recv_data (bytes): The received data. Returns: bool: True if the received data has an IP header, False otherwise. """ first_field = recv_data[0] >> 4 # The first 4 bits of the first byte is the version field of IP Header. _debug("Detecting if received data has IP header. First 4 bits: {}".format(first_field)) return first_field == (4 if is_ipv4(sock) else 6) icmp_type = IcmpV4Type if is_ipv4(sock) else IcmpV6Type timeout_time = time.time() + timeout # Exactly time when timeout. _debug("Timeout time: {} ({})".format(time.ctime(timeout_time), timeout_time)) while True: timeout_left = timeout_time - time.time() # How many seconds left until timeout. timeout_left = timeout_left if timeout_left > 0 else 0 # Timeout must be non-negative _debug("Timeout left: {:.2f}s".format(timeout_left)) selected = select.select([sock], [], [], timeout_left) # Wait until sock is ready to read or time is out. if selected[0] == []: # Timeout raise errors.Timeout(timeout=timeout) time_recv = time.time() _debug("Received time: {} ({}))".format(time.ctime(time_recv), time_recv)) recv_data, addr = sock.recvfrom(1500) # Single packet size limit is 65535 bytes, but usually the network packet limit is 1500 bytes. if is_ipv4(sock): has_ip_header = (os.name != "posix") or (platform.system() == "Darwin") or (sock.type == socket.SOCK_RAW) # No IP Header when unprivileged on Linux. else: has_ip_header = detect_ip_header(sock, recv_data) if has_ip_header: _debug("Has IP header: True") ip_header_slice = slice(0, struct.calcsize(IPV4_HEADER_FORMAT if is_ipv4(sock) else IPV6_HEADER_FORMAT)) # [0:20] icmp_header_slice = slice(ip_header_slice.stop, ip_header_slice.stop + struct.calcsize(ICMP_HEADER_FORMAT)) # [20:28] ip_header_raw = recv_data[ip_header_slice] ip_header = read_ipv4_header(ip_header_raw) if is_ipv4(sock) else read_ipv6_header(ip_header_raw) _debug("Received IP header:", ip_header) else: _debug("Has IP header: False") ip_header = None icmp_header_slice = slice(0, struct.calcsize(ICMP_HEADER_FORMAT)) # [0:8] icmp_header_raw, icmp_payload_raw = recv_data[icmp_header_slice], recv_data[icmp_header_slice.stop:] icmp_header = read_icmp_header(icmp_header_raw) _debug("Received ICMP header:", icmp_header) _debug("Received ICMP payload:", icmp_payload_raw) if icmp_header["type"] in (icmp_type.TIME_EXCEEDED, icmp_type.DESTINATION_UNREACHABLE): # TIME_EXCEEDED and DESTINATION_UNREACHABLE have no icmp_id and icmp_seq. Usually they are 0. # According to RFC 792, both Time Exceeded and Destination Unreachable # messages include the IP Header and the first 64 bits of the Datagram # which is the original ICMP Header. Thus we can extract the icmp_id # and seq from the returned Datagram ICMP Header to match the packet. original_icmp_header_offset = struct.calcsize(IPV4_HEADER_FORMAT if is_ipv4(sock) else IPV6_HEADER_FORMAT) original_icmp_header_slice = slice(original_icmp_header_offset, original_icmp_header_offset + struct.calcsize(ICMP_HEADER_FORMAT)) original_icmp_header = read_icmp_header(icmp_payload_raw[original_icmp_header_slice]) is_icmp_id_matched = original_icmp_header["id"] == icmp_id # ECHO_REPLY should match the ICMP ID if not is_icmp_id_matched and not has_ip_header: # When unprivileged on Linux, ICMP ID is rewrited by kernel.field. icmp_id = sock.getsockname()[1] # According to https://stackoverflow.com/a/14023878/4528364, icmp_id is the port number of the socket. is_icmp_id_matched = original_icmp_header["id"] == icmp_id if is_icmp_id_matched: _debug("ICMP ID rewrited by kernel: {}".format(icmp_id)) if not is_icmp_id_matched: _debug("ICMP ID dismatch. Packet filtered out.") continue if original_icmp_header["seq"] != seq: # ECHO_REPLY should match the ICMP SEQ field. _debug("IMCP SEQ dismatch. Packet filtered out.") continue if icmp_header["type"] == icmp_type.TIME_EXCEEDED: if icmp_header["code"] == IcmpTimeExceededCode.TTL_EXPIRED: # Windows raw socket cannot get TTL_EXPIRED. See https://stackoverflow.com/questions/43239862/socket-sock-raw-ipproto-icmp-cant-read-ttl-response. raise errors.TimeToLiveExpired(ip_header=ip_header, icmp_header=icmp_header) # Some router does not report TTL expired and then timeout shows. raise errors.TimeExceeded() if icmp_header["type"] == icmp_type.DESTINATION_UNREACHABLE: if is_ipv4(sock): if icmp_header["code"] == IcmpV4DestinationUnreachableCode.DESTINATION_HOST_UNREACHABLE: raise errors.DestinationHostUnreachable(ip_header=ip_header, icmp_header=icmp_header) else: if icmp_header["code"] == IcmpV6DestinationUnreachableCode.ADDRESS_UNREACHABLE: raise errors.AddressUnreachable(ip_header=ip_header, icmp_header=icmp_header) elif icmp_header["code"] == IcmpV6DestinationUnreachableCode.PORT_UNREACHABLE: raise errors.PortUnreachable(ip_header=ip_header, icmp_header=icmp_header) raise errors.DestinationUnreachable( ip_header=ip_header, icmp_header=icmp_header ) if icmp_header["id"]: if icmp_header["type"] == icmp_type.ECHO_REQUEST: # filters out the ECHO_REQUEST itself. _debug("ECHO_REQUEST received. Packet filtered out.") continue _debug("ICMP ID:", icmp_header["id"], ",", "Expected:", icmp_id) is_icmp_id_matched = icmp_header["id"] == icmp_id # ECHO_REPLY should match the ICMP ID if not is_icmp_id_matched and not has_ip_header: # When unprivileged on Linux, ICMP ID is rewrited by kernel.field. icmp_id = sock.getsockname()[1] # According to https://stackoverflow.com/a/14023878/4528364, icmp_id is the port number of the socket. is_icmp_id_matched = icmp_header["id"] == icmp_id if is_icmp_id_matched: _debug("ICMP ID rewrited by kernel: {}".format(icmp_id)) if not is_icmp_id_matched: _debug("ICMP ID dismatch. Packet filtered out.") continue if icmp_header["seq"] != seq: # ECHO_REPLY should match the ICMP SEQ field. _debug("IMCP SEQ dismatch. Packet filtered out.") continue if icmp_header["type"] == icmp_type.ECHO_REPLY: time_sent = struct.unpack(ICMP_TIME_FORMAT, icmp_payload_raw[0 : struct.calcsize(ICMP_TIME_FORMAT)])[0] _debug("Received sent time: {} ({})".format(time.ctime(time_sent), time_sent)) return time_recv - time_sent _debug("Uncatched ICMP packet:", icmp_header) @_func_logger def ping(dest_addr: str, timeout: int = 4, unit: str = "s", src_addr: str = "", ttl=None, seq: int = 0, size: int = 56, interface: str = "", version=None): """ Send one ping to destination address with the given timeout. Args: dest_addr (str): The destination address, can be an IP address or a domain name. Ex. "192.168.1.1"/"example.com"/“fd00::1“ timeout (int): Time to wait for a response, in seconds. Default is 4s, same as Windows CMD. (default 4) unit (str): The unit of returned value. "s" for seconds, "ms" for milliseconds. (default "s") src_addr (str): The IP address to ping from. This is for multiple network interfaces. Ex. "192.168.1.20". (default "") ttl (int | None): The Time-To-Live of the outgoing packet. Default is None, which means using OS default ttl -- 64 onLinux and macOS, and 128 on Windows. (default None) seq (int): ICMP packet sequence, usually increases from 0 in the same process. (default 0) size (int): The ICMP packet payload size in bytes. If the input of this is less than the bytes of a double format (usually 8), the size of ICMP packet payload is 8 bytes to hold a time. The max should be the router_MTU(Usually 1480) - IP_Header(20) - ICMP_Header(8). Default is 56, same as in macOS. (default 56) interface (str): LINUX ONLY. The gateway network interface to ping from. Ex. "wlan0". (default "") ip_v (int | None): The IP version to use. 4 for IPv4, 6 for IPv6. If None, the function will try to determine the IP version from `dest_addr`. (default None) Returns: float | None | False: The delay in seconds/milliseconds, False on error and None on timeout. Raises: PingError: Any PingError will raise again if `ping3.EXCEPTIONS` is True. """ if version is None: # Auto detect IP version if not specified. try: ip = ipaddress.ip_address(dest_addr) version = ip.version except ValueError: version = 4 # Default to IPv4 if the address is not a valid IP address. _debug("Ping IPv{}:".format(version), dest_addr) if version == 4: socket_family = socket.AF_INET socket_protocol = socket.IPPROTO_ICMP elif version == 6: socket_family = socket.AF_INET6 socket_protocol = socket.IPPROTO_ICMPV6 else: raise ValueError("Unsupported IP version: {}".format(version)) try: sock = socket.socket(socket_family, socket.SOCK_RAW, socket_protocol) except PermissionError as err: if err.errno == errno.EPERM: # [Errno 1] Operation not permitted _debug("`{}` when create socket.SOCK_RAW, using socket.SOCK_DGRAM instead.".format(err)) sock = socket.socket(socket_family, socket.SOCK_DGRAM, socket_protocol) # TBC: On Linux, using SOCK_DGRAM with IPPROTO_ICMPV6 will not work as expected. It will not send ICMP packets, but will send UDP packets instead. else: raise err with sock: if ttl: if is_ipv4(sock): # socket.IP_TTL and socket.SOL_IP are for IPv4. try: # IPPROTO_IP is for Windows and BSD Linux. if sock.getsockopt(socket.IPPROTO_IP, socket.IP_TTL): # TTL is a IPPROTO_IP option, not IPPROTO_ICMP. See: https://datatracker.ietf.org/doc/html/rfc1122#page-34 sock.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, ttl) except OSError as err: _debug("Set Socket Option `IP_TTL` in `IPPROTO_IP` Failed: {}".format(err)) try: if sock.getsockopt(socket.SOL_IP, socket.IP_TTL): sock.setsockopt(socket.SOL_IP, socket.IP_TTL, ttl) except OSError as err: _debug("Set Socket Option `IP_TTL` in `SOL_IP` Failed: {}".format(err)) else: # IPv6 try: # socket.IPV6_UNICAST_HOPS is for IPv6. if sock.getsockopt(socket.IPPROTO_IPV6, socket.IPV6_UNICAST_HOPS): # Unicast Hop Limit should be used at the IPPROTO_IPV6 Layer, not the IPPROTO_ICMPV6 Layer. See: https://datatracker.ietf.org/doc/html/rfc3493#section-5.1 sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_UNICAST_HOPS, ttl) except OSError as err: _debug("Set Socket Option `IPV6_UNICAST_HOPS` in `IPPROTO_IPV6` Failed: {}".format(err)) if interface: # Packets will be sent from specified interface. sock.setsockopt(socket.SOL_SOCKET, SOCKET_SO_BINDTODEVICE, interface.encode()) # Linux only. Requires root. _debug("Socket Interface Binded:", interface) if src_addr: if is_ipv4(sock): sock.bind((src_addr, 0)) # only packets send to src_addr are received. _debug("Socket Source Address Binded:", src_addr) # TODO: Support src_addr for IPv6. Currently, the source address is determined by the OS when sending packets. thread_id = (threading.get_native_id() if hasattr(threading, "get_native_id") else threading.current_thread().ident) # threading.get_native_id() is supported >= python3.8. process_id = os.getpid() # If ping() run under different process, thread_id may be identical. icmp_id = zlib.crc32("{}{}".format(process_id, thread_id).encode()) & 0xffff # to avoid icmp_id collision. try: send_one_ping(sock=sock, dest_addr=dest_addr, icmp_id=icmp_id, seq=seq, size=size) delay = receive_one_ping(sock=sock, icmp_id=icmp_id, seq=seq, timeout=timeout) # in seconds except errors.Timeout as err: _debug(err) _raise(err) return None except errors.PingError as err: _debug(err) _raise(err) return False if delay is None: return None if unit == "ms": delay *= 1000 # in milliseconds return delay @_func_logger def verbose_ping(dest_addr: str, count: int = 4, interval: float = 0, *args, **kwargs): """ Send pings to destination address with the given timeout and display the result. Args: dest_addr (str): The destination address. Ex. "192.168.1.1"/"example.com" count (int): How many pings should be sent. 0 means infinite loops until manually stopped. Default is 4, same as Windows CMD. (default 4) interval (float): How many seconds between two packets. Default is 0, which means send the next packet as soon as the previous one responsed. (default 0) *args and **kwargs (any): And all the other arguments available in ping() except `seq`. Output: Formatted ping results printed. """ timeout = kwargs.get("timeout") src = kwargs.get("src_addr") unit = kwargs.setdefault("unit", "ms") i = 0 while i < count or count == 0: if interval > 0 and i > 0: time.sleep(interval) output_text = "ping '{}'".format(dest_addr) output_text += " from '{}'".format(src) if src else "" output_text += " ... " delay = ping(dest_addr, seq=i, *args, **kwargs) print(output_text, end="") if delay is None: print("Timeout > {}s".format(timeout) if timeout else "Timeout") elif delay is False: print("Error") else: print("{value}{unit}".format(value=int(delay), unit=unit)) i += 1 kyan001-ping3-fa477fb/ping3/__main__.py000066400000000000000000000001171515576040400175350ustar00rootroot00000000000000from . import command_line if __name__ == "__main__": command_line.main() kyan001-ping3-fa477fb/ping3/command_line.py000066400000000000000000000056531515576040400204540ustar00rootroot00000000000000import argparse import ping3 def main(assigned_args = None) -> None: """ Parse and execute the call from command-line. Args: assigned_args (list[str] | None): List of strings to parse. The default is taken from sys.argv. Returns: Formatted ping results printed. """ parser = argparse.ArgumentParser(prog="ping3", description="A pure python3 version of ICMP ping implementation using raw socket.", epilog="!!Note: ICMP messages can only be sent from processes running as root.") parser.add_argument("-v", "--version", action="version", version=ping3.__version__) parser.add_argument(dest="dest_addr", metavar="DEST_ADDR", nargs="*", default=("example.com", "8.8.8.8"), help="The destination address, can be an IP address or a domain name. Ex. 192.168.1.1/example.com.") parser.add_argument("-c", "--count", dest="count", metavar="COUNT", type=int, default=4, help="How many pings should be sent. Default is 4.") parser.add_argument("-t", "--timeout", dest="timeout", metavar="TIMEOUT", type=float, default=4, help="Time to wait for a response, in seconds. Default is 4.") parser.add_argument("-i", "--interval", dest="interval", metavar="INTERVAL", type=float, default=0, help="Time to wait between each packet, in seconds. Default is 0.") parser.add_argument("-I", "--interface", dest="interface", metavar="INTERFACE", default="", help="LINUX ONLY. The gateway network interface to ping from. Default is None.") parser.add_argument("-S", "--src", dest="src_addr", metavar="SRC_ADDR", default="", help="The IP address to ping from. This is for multiple network interfaces. Default is None") parser.add_argument("-T", "--ttl", dest="ttl", metavar="TTL", type=int, default=64, help="The Time-To-Live of the outgoing packet. Default is 64.") parser.add_argument("-s", "--size", dest="size", metavar="SIZE", type=int, default=56, help="The ICMP packet payload size in bytes. Default is 56.") parser.add_argument("-D", "--debug", action="store_true", dest="debug", help="Turn on DEBUG mode.") parser.add_argument("-E", "--exceptions", action="store_true", dest="exceptions", help="Turn on EXCEPTIONS mode.") parser.add_argument("-4", "--ipv4", action="store_true", dest="ipv4", help="Force ping an IPv4 address. Default is None for auto-detect.") parser.add_argument("-6", "--ipv6", action="store_true", dest="ipv6", help="Force ping an IPv6 address. Default is None for auto-detect.") args = parser.parse_args(assigned_args) ping3.DEBUG = args.debug ping3.EXCEPTIONS = args.exceptions if args.ipv4: args.version = 4 elif args.ipv6: args.version = 6 else: args.version = None for addr in args.dest_addr: ping3.verbose_ping(addr, count=args.count, ttl=args.ttl, timeout=args.timeout, size=args.size, interval=args.interval, interface=args.interface, src_addr=args.src_addr, version=args.version) if __name__ == "__main__": main() kyan001-ping3-fa477fb/ping3/enums.py000066400000000000000000000036541515576040400171550ustar00rootroot00000000000000import enum ICMP_DEFAULT_CODE = 0 # the code for ECHO_REPLY and ECHO_REQUEST class IcmpV4Type(enum.IntEnum): """Enum for Type in ICMPv4 Header.""" ECHO_REPLY = 0 DESTINATION_UNREACHABLE = 3 REDIRECT_MESSAGE = 5 ECHO_REQUEST = 8 ROUTER_ADVERTISEMENT = 9 ROUTER_SOLICITATION = 10 TIME_EXCEEDED = 11 BAD_IP_HEADER = 12 TIMESTAMP = 13 TIMESTAMP_REPLY = 14 class IcmpV6Type(enum.IntEnum): """Enum for Type in ICMPv6 Header.""" DESTINATION_UNREACHABLE = 1 TIME_EXCEEDED = 3 ECHO_REQUEST = 128 ECHO_REPLY = 129 ROUTER_SOLICITATION = 133 ROUTER_ADVERTISEMENT = 134 REDIRECT_MESSAGE = 137 class IcmpV4DestinationUnreachableCode(enum.IntEnum): """Enum for Code in ICMPv4 Header when type is DESTINATION_UNREACHABLE (3)""" DESTINATION_NETWORK_UNREACHABLE = 0 DESTINATION_HOST_UNREACHABLE = 1 DESTINATION_PROTOCOL_UNREACHABLE = 2 DESTINATION_PORT_UNREACHABLE = 3 FRAGMENTATION_REQUIRED = 4 SOURCE_ROUTE_FAILED = 5 DESTINATION_NETWORK_UNKNOWN = 6 DESTINATION_HOST_UNKNOWN = 7 SOURCE_HOST_ISOLATED = 8 NETWORK_ADMINISTRATIVELY_PROHIBITED = 9 HOST_ADMINISTRATIVELY_PROHIBITED = 10 NETWORK_UNREACHABLE_FOR_TOS = 11 HOST_UNREACHABLE_FOR_TOS = 12 COMMUNICATION_ADMINISTRATIVELY_PROHIBITED = 13 HOST_PRECEDENCE_VIOLATION = 14 PRECEDENCE_CUTOFF_IN_EFFECT = 15 class IcmpV6DestinationUnreachableCode(enum.IntEnum): """Enum for Code in ICMPv6 Header when type is DESTINATION_UNREACHABLE (1)""" NO_ROUTE_TO_DESTINATION = 0 COMMUNICATION_PROHIBITED = 1 BEYOND_SCOPE = 2 ADDRESS_UNREACHABLE = 3 PORT_UNREACHABLE = 4 SOURCE_ADDRESS_FAILED = 5 REJECT_ROUTE_TO_DESTINATION = 6 ERROR_IN_SOURCE_ROUTING_HEADER = 7 class IcmpTimeExceededCode(enum.IntEnum): """Enum for Code in ICMPv4 and ICMPv6 Header when type is TIME_EXCEEDED (11)""" TTL_EXPIRED = 0 FRAGMENT_REASSEMBLY_TIME_EXCEEDED = 1 kyan001-ping3-fa477fb/ping3/errors.py000066400000000000000000000047641515576040400173450ustar00rootroot00000000000000class PingError(Exception): pass class TimeExceeded(PingError): pass class TimeToLiveExpired(TimeExceeded): def __init__(self, message="Time exceeded: Time To Live expired.", ip_header=None, icmp_header=None): self.ip_header = ip_header self.icmp_header = icmp_header self.message = message super().__init__(self.message) class DestinationUnreachable(PingError): def __init__(self, message="Destination unreachable.", ip_header=None, icmp_header=None): self.ip_header = ip_header self.icmp_header = icmp_header self.message = message if self.ip_header is None else message + " (Host='{}')".format(self.ip_header.get("src_addr")) super().__init__(self.message) class DestinationHostUnreachable(DestinationUnreachable): def __init__(self, message="Destination unreachable: Host unreachable.", ip_header=None, icmp_header=None): self.ip_header = ip_header self.icmp_header = icmp_header self.message = message if self.ip_header is None else message + " (Host='{}')".format(self.ip_header.get("src_addr")) super().__init__(self.message) class AddressUnreachable(DestinationUnreachable): def __init__(self, message="Destination unreachable: Address unreachable.", ip_header=None, icmp_header=None): self.ip_header = ip_header self.icmp_header = icmp_header self.message = message if self.ip_header is None else message + " (Host='{}')".format(self.ip_header.get("src_addr")) super().__init__(self.message) class PortUnreachable(DestinationUnreachable): def __init__(self, message="Destination unreachable: Port unreachable.", ip_header=None, icmp_header=None): self.ip_header = ip_header self.icmp_header = icmp_header self.message = message if self.ip_header is None else message + " (Host='{}')".format(self.ip_header.get("src_addr")) super().__init__(self.message) class HostUnknown(PingError): def __init__(self, message="Cannot resolve: Unknown host.", dest_addr=None): self.dest_addr = dest_addr self.message = message if self.dest_addr is None else message + " (Host='{}')".format(self.dest_addr) super().__init__(self.message) class Timeout(PingError): def __init__(self, message="Request timeout for ICMP packet.", timeout=None): self.timeout = timeout self.message = message if self.timeout is None else message + " (Timeout={}s)".format(self.timeout) super().__init__(self.message) kyan001-ping3-fa477fb/pyproject.toml000066400000000000000000000023431515576040400173420ustar00rootroot00000000000000[build-system] requires = ["setuptools>=61.0.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "ping3" description = "A pure python3 version of ICMP ping implementation using raw socket." requires-python = ">=3.5" readme = "README.md" keywords = ["python3", "ping", "icmp", "socket", "tool"] license = "MIT" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Topic :: System :: Networking", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", ] dependencies = [] dynamic = ["version"] [[project.authors]] name = "Kyan" email = "kai@kyan001.com" [project.optional-dependencies] dev = ["build", "twine"] [project.urls] Homepage = "https://github.com/kyan001/ping3" Changelog = "https://github.com/kyan001/ping3/blob/master/CHANGELOG.md" "Issue Tracker" = "https://github.com/kyan001/ping3/issues" "Source Code" = "https://github.com/kyan001/ping3" [project.scripts] ping3 = "ping3.command_line:main" [tool.setuptools] py-modules = ["ping3"] [tool.setuptools.dynamic] version = {attr = "ping3.__version__"} [tool.setuptools.packages.find] exclude = ["contrib", "docs", "tests"] kyan001-ping3-fa477fb/requirements-dev.txt000066400000000000000000000000141515576040400204570ustar00rootroot00000000000000build twine kyan001-ping3-fa477fb/tests/000077500000000000000000000000001515576040400155665ustar00rootroot00000000000000kyan001-ping3-fa477fb/tests/test_benchmark.py000066400000000000000000000013021515576040400211250ustar00rootroot00000000000000import os import sys import timeit sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import ping3 # noqa: linter (pycodestyle) should not lint this line. dev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) stmt = "ping3.ping('127.0.0.1')" setup = "import sys; sys.path.insert(0, '{}'); import ping3; print('ping3 version:', ping3.__version__)".format(dev_dir) for count in (1, 10, 100, 1000, 5000): print("Testing `{stmt}` {num} times...".format(stmt=stmt, num=count)) duration = timeit.timeit(stmt, setup=setup, number=count) print("Duration: {drtn:.3f} seconds. {d:.1f} ms/ping".format(drtn=duration, d=duration * 1000 / count)) print() kyan001-ping3-fa477fb/tests/test_command_line.py000066400000000000000000000113111515576040400216210ustar00rootroot00000000000000import sys import os.path import io import time import unittest import socket from unittest.mock import patch sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from ping3 import command_line # noqa: linter (pycodestyle) should not lint this line. from ping3 import errors # noqa: linter (pycodestyle) should not lint this line. DEST_DOMAIN = "captive.apple.com" UNREACHABLE_IP = "10.255.255.1" class test_ping3(unittest.TestCase): """command-line ping3 unittest""" def setUp(self): pass def tearDown(self): pass def test_dest_addr_0(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: command_line.main() self.assertRegex(fake_out.getvalue(), r".*[0-9]+ms.*") def test_dest_addr_1(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: command_line.main(["127.0.0.1"]) self.assertIn("127.0.0.1", fake_out.getvalue()) def test_dest_addr_2(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: command_line.main(["127.0.0.1", "8.8.8.8"]) self.assertIn("127.0.0.1", fake_out.getvalue()) self.assertIn("8.8.8.8", fake_out.getvalue()) def test_count(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: command_line.main(["-c", "1", DEST_DOMAIN]) self.assertEqual(fake_out.getvalue().count("\n"), 1) def test_timeout(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: command_line.main(["-t", "1", UNREACHABLE_IP]) self.assertRegex(fake_out.getvalue(), r".*Timeout \> [1.0]+s.*") def test_ttl(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: command_line.main(["-T", "1", DEST_DOMAIN]) self.assertNotRegex(fake_out.getvalue(), r".*[0-9]+ms.*") def test_size(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: command_line.main(["-s", "100", DEST_DOMAIN]) self.assertRegex(fake_out.getvalue(), r".*[0-9]+ms.*") with self.assertRaises(OSError): command_line.main(["-s", "99999", DEST_DOMAIN]) def test_interval(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: start_time = time.time() command_line.main(["-i", "1", DEST_DOMAIN]) end_time = time.time() self.assertTrue((end_time - start_time) >= 3) # time_expect = (count - 1) * interval self.assertNotIn("Timeout", fake_out.getvalue()) @unittest.skipUnless(sys.platform == "linux", "Linux only") def test_interface(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: try: route_cmd = os.popen("ip -o -4 route show to default") default_route = route_cmd.read() finally: route_cmd.close() my_interface = default_route.split()[4] try: socket.if_nametoindex(my_interface) # test if the interface exists. except OSError: self.fail("Interface Name Error: {}".format(my_interface)) command_line.main(["-I", my_interface, DEST_DOMAIN]) self.assertRegex(fake_out.getvalue(), r".*[0-9]+ms.*") def test_src_addr(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: my_ip = socket.gethostbyname(socket.gethostname()) if my_ip in ("127.0.0.1", "127.0.1.1"): # This may caused by /etc/hosts settings. dest_addr = my_ip # only localhost can send and receive from 127.0.0.1 (or 127.0.1.1 on Ubuntu). else: dest_addr = DEST_DOMAIN command_line.main(["-S", my_ip, dest_addr]) self.assertRegex(fake_out.getvalue(), r".*[0-9]+ms.*") def test_ipv4(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: command_line.main(["-4", DEST_DOMAIN]) self.assertRegex(fake_out.getvalue(), r".*[0-9]+ms.*") def test_ipv6(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: command_line.main(["-6", DEST_DOMAIN]) self.assertRegex(fake_out.getvalue(), r".*[0-9]+ms.*") def test_debug(self): with patch("sys.stdout", new=io.StringIO()), patch("sys.stderr", new=io.StringIO()) as fake_err: command_line.main(["--debug", "-c", "1", DEST_DOMAIN]) self.assertIn("[DEBUG]", fake_err.getvalue()) def test_exceptions(self): with self.assertRaises(errors.Timeout): command_line.main(["--exceptions", "-t", "0.0001", DEST_DOMAIN]) if __name__ == "__main__": unittest.main(verbosity=2, exit=False) kyan001-ping3-fa477fb/tests/test_multi.py000066400000000000000000000014201515576040400203260ustar00rootroot00000000000000import time import multiprocessing import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import ping3 # noqa: linter (pycodestyle) should not lint this line. print("ping3=", ping3.__version__) # ping3.DEBUG = True HOSTS = ['baidu.com', 'example.com'] def ping_in_thread_or_process(host): while True: delay = ping3.ping(host, unit='ms') time.sleep(1) print(host, delay) def standard_delay(): for h in HOSTS: print('Standard Delay:', h, ping3.ping(h, unit='ms')) def multi_processing_ping(): for h in HOSTS: p = multiprocessing.Process(target=ping_in_thread_or_process, args=(h,)) p.start() if __name__ == '__main__': standard_delay() multi_processing_ping() kyan001-ping3-fa477fb/tests/test_ping3.py000066400000000000000000000233011515576040400202160ustar00rootroot00000000000000import sys import os.path import io import unittest import time from unittest.mock import patch import socket sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import ping3 # noqa: linter (pycodestyle) should not lint this line. DEST_DOMAIN = "captive.apple.com" NOT_EXIST_DOMAIN = "not.exist.com" UNREACHABLE_IP = "10.255.255.1" class test_ping3(unittest.TestCase): """ping3 unittest""" def setUp(self): pass def tearDown(self): pass def test_version(self): self.assertIsInstance(ping3.__version__, str) def test_ping_normal(self): delay = ping3.ping(DEST_DOMAIN) self.assertIsInstance(delay, float) def test_verbose_ping_normal(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: ping3.verbose_ping(DEST_DOMAIN) self.assertRegex(fake_out.getvalue(), r".*[0-9]+ms.*") def test_ping_timeout(self): start_time = time.time() ping3.ping(UNREACHABLE_IP, timeout=1) end_time = time.time() self.assertLess(end_time - start_time, 1.1) def test_ping_timeout_exception(self): with patch("ping3.EXCEPTIONS", True): with self.assertRaises(ping3.errors.Timeout): ping3.ping(UNREACHABLE_IP, timeout=1) def test_verbose_ping_timeout(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: ping3.verbose_ping(UNREACHABLE_IP, timeout=1) self.assertRegex(fake_out.getvalue(), r".*Timeout \> [1.0]+s.*") def test_verbose_ping_timeout_exception(self): with patch("ping3.EXCEPTIONS", True): with self.assertRaises(ping3.errors.Timeout): ping3.verbose_ping(UNREACHABLE_IP, timeout=1) def test_ping_error(self): delay = ping3.ping(NOT_EXIST_DOMAIN) self.assertFalse(delay) def test_ping_error_exception(self): with patch("ping3.EXCEPTIONS", True): try: ping3.ping(NOT_EXIST_DOMAIN) except ping3.errors.HostUnknown as e: self.assertEqual(e.dest_addr, NOT_EXIST_DOMAIN) def test_ping_seq(self): delay = ping3.ping(DEST_DOMAIN, seq=199) self.assertIsInstance(delay, float) def test_ping_size(self): delay = ping3.ping(DEST_DOMAIN, size=100) self.assertIsInstance(delay, float) def test_ping_size_exception(self): with self.assertRaises(OSError): ping3.ping(DEST_DOMAIN, size=99999) # most router has 1480 MTU, which is IP_Header(20) + ICMP_Header(8) + ICMP_Payload(1452) def test_ping_ipv4_domain(self): delay = ping3.ping(DEST_DOMAIN, version=4) self.assertIsInstance(delay, float) def test_ping_ipv6_domain(self): delay = ping3.ping(DEST_DOMAIN, version=6) self.assertIsInstance(delay, float) def test_ping_ipv4_ip(self): delay = ping3.ping("127.0.0.1", version=4) self.assertIsInstance(delay, float) def test_ping_ipv6_ip(self): delay = ping3.ping("::1", version=6) self.assertIsInstance(delay, float) def test_ping_ipv4_autodetect(self): delay = ping3.ping("127.0.0.1") self.assertIsInstance(delay, float) def test_ping_ipv6_autodetect(self): delay = ping3.ping("::1") self.assertIsInstance(delay, float) def test_verbose_ping_size(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: ping3.verbose_ping(DEST_DOMAIN, size=100) self.assertRegex(fake_out.getvalue(), r".*[0-9]+ms.*") def test_verbose_ping_size_exception(self): with self.assertRaises(OSError): ping3.verbose_ping(DEST_DOMAIN, size=99999) def test_ping_unit(self): delay = ping3.ping(DEST_DOMAIN, unit="ms") self.assertIsInstance(delay, float) self.assertTrue(delay > 1) def test_verbose_ping_unit(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: ping3.verbose_ping(DEST_DOMAIN, unit="ms") self.assertRegex(fake_out.getvalue(), r".*[0-9]+ms.*") @unittest.skipUnless(sys.platform == "linux", "Linux only") def test_ping_interface(self): try: route_cmd = os.popen("ip -o -4 route show to default") default_route = route_cmd.read() finally: route_cmd.close() my_interface = default_route.split()[4] try: socket.if_nametoindex(my_interface) # test if the interface exists. except OSError: self.fail("Interface Name Error: {}".format(my_interface)) delay = ping3.ping(DEST_DOMAIN, interface=my_interface) self.assertIsInstance(delay, float) @unittest.skipUnless(sys.platform == "linux", "Linux only") def test_verbose_ping_interface(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: try: route_cmd = os.popen("ip -o -4 route show to default") default_route = route_cmd.read() finally: route_cmd.close() my_interface = default_route.split()[4] try: socket.if_nametoindex(my_interface) # test if the interface exists. except OSError: self.fail("Interface Name Error: {}".format(my_interface)) ping3.verbose_ping(DEST_DOMAIN, interface=my_interface) self.assertRegex(fake_out.getvalue(), r".*[0-9]+ms.*") def test_ping_src_addr(self): my_ip = socket.gethostbyname(socket.gethostname()) if my_ip in ("127.0.0.1", "127.0.1.1"): # This may caused by /etc/hosts settings. dest_addr = my_ip # only localhost can send and receive from 127.0.0.1 (or 127.0.1.1 on Ubuntu). else: dest_addr = DEST_DOMAIN delay = ping3.ping(dest_addr, src_addr=my_ip) self.assertIsInstance(delay, float) def test_verbose_ping_src_addr(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: my_ip = socket.gethostbyname(socket.gethostname()) if my_ip in ("127.0.0.1", "127.0.1.1"): # This may caused by /etc/hosts settings. dest_addr = my_ip # only localhost can send and receive from 127.0.0.1 (or 127.0.1.1 on Ubuntu). else: dest_addr = DEST_DOMAIN ping3.verbose_ping(dest_addr, src_addr=my_ip) self.assertRegex(fake_out.getvalue(), r".*[0-9]+ms.*") # @unittest.skipIf(sys.platform.startswith("win"), "Linux and macOS Only") def test_ping_ttl(self): delay = ping3.ping(DEST_DOMAIN, ttl=1) self.assertIn(delay, (None, False)) # When TTL expired, some routers report nothing. # @unittest.skipIf(sys.platform.startswith("win"), "Linux and macOS Only") def test_ping_ttl_exception(self): with patch("ping3.EXCEPTIONS", True): with self.assertRaises((ping3.errors.TimeToLiveExpired, ping3.errors.Timeout)): # When TTL expired, some routers report nothing. ping3.ping(DEST_DOMAIN, ttl=1) def test_ping_ipv6_ttl(self): delay = ping3.ping(DEST_DOMAIN, ttl=1, version=6) self.assertIn(delay, (None, False)) # When TTL expired, some routers report nothing. def test_ping_ipv6_ttl_exception(self): with patch("ping3.EXCEPTIONS", True): with self.assertRaises((ping3.errors.TimeToLiveExpired, ping3.errors.Timeout)): # When TTL expired, some routers report nothing. ping3.ping(DEST_DOMAIN, ttl=1, version=6) def test_verbose_ping_ttl(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: ping3.verbose_ping(DEST_DOMAIN, ttl=1) self.assertNotRegex(fake_out.getvalue(), r".*[0-9]+ms.*") def test_verbose_ping_ipv6_ttl(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: ping3.verbose_ping(DEST_DOMAIN, ttl=1, version=6) self.assertNotRegex(fake_out.getvalue(), r".*[0-9]+ms.*") def test_verbose_ping_ttl_exception(self): with patch("sys.stdout", new=io.StringIO()), patch("ping3.EXCEPTIONS", True): with self.assertRaises((ping3.errors.TimeToLiveExpired, ping3.errors.Timeout)): # When TTL expired, some routers report nothing. ping3.verbose_ping(DEST_DOMAIN, ttl=1) def test_verbose_ping_ipv6_ttl_exception(self): with patch("sys.stdout", new=io.StringIO()), patch("ping3.EXCEPTIONS", True): with self.assertRaises((ping3.errors.TimeToLiveExpired, ping3.errors.Timeout)): # When TTL expired, some routers report nothing. ping3.verbose_ping(DEST_DOMAIN, ttl=1, version=6) def test_verbose_ping_count(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: ping3.verbose_ping(DEST_DOMAIN, count=1) self.assertEqual(fake_out.getvalue().count("\n"), 1) def test_verbose_ping_interval(self): with patch("sys.stdout", new=io.StringIO()) as fake_out: delay = ping3.ping(DEST_DOMAIN) self.assertIsInstance(delay, float) self.assertTrue(0 < delay < 0.75) # If interval does not work, the total delay should be < 3s (4 * 0.75s) start_time = time.time() ping3.verbose_ping(DEST_DOMAIN, interval=1) # If interval does work, the total delay should be > 3s (3 * 1s) end_time = time.time() self.assertTrue((end_time - start_time) >= 3) # time_expect = (count - 1) * interval self.assertNotIn("Timeout", fake_out.getvalue()) # Ensure no timeout def test_DEBUG(self): with patch("ping3.DEBUG", True), patch("sys.stderr", new=io.StringIO()): ping3.ping(DEST_DOMAIN) self.assertIsNotNone(ping3.LOGGER) if __name__ == "__main__": unittest.main(verbosity=2, exit=False)