diff --git a/CHANGELOG.md b/CHANGELOG.md index 302979e..9378ca1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,45 @@ # Changelog All notable changes to this project will be documented in this file. + +## [v2.1.1](https://github.com/ValentinBELYN/icmplib/releases/tag/v2.1.1) - 2021-03-21 +- Rollback changes made to the `traceroute` function due to a bug. + +## [v2.1.0](https://github.com/ValentinBELYN/icmplib/releases/tag/v2.1.0) - 2021-03-20 +- Add a `family` parameter to the `resolve` function to define the address family. +- Improve the reliability of the results of the `traceroute` function. + +> This version is the last of the 2.x branch. See you soon for the release of icmplib 3.0! + +## [v2.0.2](https://github.com/ValentinBELYN/icmplib/releases/tag/v2.0.2) - 2021-02-07 +- Rename the default branch from `master` to `main`. +- Fix a bug preventing the `traceroute` function to work with IPv6 addresses (part 2). +- Add more details about the `privileged` parameter in the `README` file (part 2). + +## [v2.0.1](https://github.com/ValentinBELYN/icmplib/releases/tag/v2.0.1) - 2020-12-12 +- Handle `EACCES` errors at sockets level. +- Fix a bug preventing the `traceroute` function to work with IPv6 addresses. +- Add some details about the `privileged` parameter in the `README` file. + +## [v2.0.0](https://github.com/ValentinBELYN/icmplib/releases/tag/v2.0.0) - 2020-11-15 +icmplib 2.0 is here! :tada: + +Here is an overview of the improvements: +- All the foundations of the library have been completely reworked to make it even faster and simplify future developments. +- You can now use the library without root privileges. Remember to disable the `privileged` parameter on functions and sockets. +- The `multiping` function has been rewritten to use only one thread instead of as many threads as hosts to reach. This function will be up to 10 times faster and up to 2 times more memory efficient. +- You can set a source IP address for sending your ICMP packets. +- The `traceroute` function now has a `first_hop` parameter to specify the initial time to live value. +- Two new exceptions have been added: `NameLookupError` and `SocketAddressError` +- Compatibility with Linux, macOS and Windows has been improved. +- Docstrings, examples and documentation have been updated. + +And more! +- The `receive` method of sockets can receive all incoming packets. +- The new `BufferedSocket` class (experimental) can read and classify incoming ICMP packets into a buffer, in real time. Useful if you want to send several ICMP packets consecutively without waiting for a response between each sending. +- Sockets throw new exceptions during instantiation, sending and receiving. +- The `resolve` function now raises a `NameLookupError` if the requested name does not exist or cannot be resolved. +- Compatibility with existing programs is maintained. ## [v1.2.2](https://github.com/ValentinBELYN/icmplib/releases/tag/v1.2.2) - 2020-10-10 - Add support for hostnames and FQDN resolution to IPv6 addresses. diff --git a/README.md b/README.md index 2bde128..0d6d72a 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,10 @@

- icmplib + icmplib

-

Easily forge ICMP packets and make your own ping and traceroute.

- - statistics -
-
-
Features    Installation    @@ -22,12 +16,14 @@ License

-
icmplib is a brand new and modern implementation of the ICMP protocol in Python.
 Use the built-in functions or build your own, you have the choice!
 
-Root privileges are required to use this library (raw sockets).
+- You can now use this library without root privileges - + + statistics +

@@ -36,8 +32,9 @@ - :deciduous_tree: **Ready-to-use:** icmplib offers ready-to-use functions such as the most popular ones: `ping`, `multiping` and `traceroute`. - :gem: **Modern:** This library uses the latest technologies offered by Python 3.6+ and is fully object-oriented. -- :rocket: **Fast:** Each class and function has been designed and optimized to deliver the best performance. Some functions are also multithreaded (like the `multiping` function). You can ping the world in seconds! -- :nut_and_bolt: **Powerful and evolutive:** Easily build your own classes and functions with `ICMPv4` and `ICMPv6` sockets. +- :rocket: **Fast:** Each class and function has been designed and optimized to deliver the best performance. Some functions are also multithreaded like the `multiping` function. You can ping the world in seconds! +- :zap: **Powerful:** Use the library without root privileges, set the traffic class of ICMP packets, customize their payload and more! +- :nut_and_bolt: **Evolutive:** Easily build your own classes and functions with `ICMPv4` and `ICMPv6` sockets. - :fire: **Seamless integration of IPv6:** Use IPv6 the same way you use IPv4. Automatic detection is done without impacting performance. - :rainbow: **Broadcast support** (you must use the `ICMPv4Socket` class to enable it). - :beer: **Support of all operating systems.** Tested on Linux, macOS and Windows. @@ -47,27 +44,27 @@ ## Installation -Install, upgrade and uninstall icmplib with these commands: +The recommended way to install or upgrade icmplib is to use `pip3`: ```shell $ pip3 install icmplib $ pip3 install --upgrade icmplib -$ pip3 uninstall icmplib -``` - -icmplib requires Python 3.6 or later. - -Import icmplib into your project (only import what you need): +``` + +*icmplib requires Python 3.6 or later.* + +To import icmplib into your project (only import what you need): ```python # For simple use -from icmplib import ping, multiping, traceroute, Host, Hop +from icmplib import ping, multiping, traceroute, resolve, Host, Hop # For advanced use (sockets) from icmplib import ICMPv4Socket, ICMPv6Socket, ICMPRequest, ICMPReply # Exceptions -from icmplib import ICMPLibError, ICMPSocketError, SocketPermissionError +from icmplib import ICMPLibError, NameLookupError, ICMPSocketError +from icmplib import SocketAddressError, SocketPermissionError from icmplib import SocketUnavailableError, SocketBroadcastError, TimeoutExceeded from icmplib import ICMPError, DestinationUnreachable, TimeExceeded ``` @@ -77,17 +74,16 @@ ## Built-in functions ### Ping -Send *ICMP ECHO_REQUEST* packets to a network host. - -#### Definition -```python -ping(address, count=4, interval=1, timeout=2, id=PID, **kwargs) +Send ICMP Echo Request packets to a network host. + +```python +ping(address, count=4, interval=1, timeout=2, id=PID, source=None, privileged=True, **kwargs) ``` #### Parameters - `address` - The IP address of the gateway or host to which the message should be sent. + The IP address, hostname or FQDN of the host to which messages should be sent. For deterministic behavior, prefer to use an IP address. - Type: `str` @@ -114,34 +110,77 @@ - `id` - The identifier of the request. Used to match the reply with the request.
- In practice, a unique identifier is used for every ping process. + The identifier of ICMP requests. Used to match the responses with requests. In practice, a unique identifier should be used for every ping process. On Linux, this identifier is ignored when the `privileged` parameter is disabled. - Type: `int` - Default: `PID` -- `**kwargs` - - `Optional` Advanced use: arguments passed to `ICMPRequest` objects. +- `source` + + The IP address from which you want to send packets. By default, the interface is automatically chosen according to the specified destination. + + - Type: `str` + - Default: `None` + +- `privileged` + + When this option is enabled, this library fully manages the exchanges and the structure of ICMP packets. Disable this option if you want to use this function without root privileges and let the kernel handle ICMP headers. + + *Only available on Unix systems. Ignored on Windows.* + + - Type: `bool` + - Default: `True` + +- `payload` + + The payload content in bytes. A random payload is used by default. + + - Type: `bytes` + - Default: `None` + +- `payload_size` + + The payload size. Ignored when the `payload` parameter is set. + + - Type: `int` + - Default: `56` + +- `traffic_class` + + The traffic class of ICMP packets. Provides a defined level of service to packets by setting the DS Field (formerly TOS) or the Traffic Class field of IP headers. Packets are delivered with the minimum priority by default (Best-effort delivery). Intermediate routers must be able to support this feature. + + *Only available on Unix systems. Ignored on Windows.* + + - Type: `int` + - Default: `0` #### Return value -- `Host` object - - A `Host` object containing statistics about the desired destination:
- `address`, `min_rtt`, `avg_rtt`, `max_rtt`, `packets_sent`,
- `packets_received`, `packet_loss`, `is_alive`. +- A `Host` object containing statistics about the desired destination:
+ `address`, `min_rtt`, `avg_rtt`, `max_rtt`, `packets_sent`, `packets_received`, `packet_loss`, `is_alive` #### Exceptions +- `NameLookupError` + + If you pass a hostname or FQDN in parameters and it does not exist or cannot be resolved. + - `SocketPermissionError` - If the permissions are insufficient to create a socket. + If the privileges are insufficient to create the socket. + +- `SocketAddressError` + + If the source address cannot be assigned to the socket. + +- `ICMPSocketError` + + If another error occurs. See the `ICMPv4Socket` or `ICMPv6Socket` class for details. #### Example ```python >>> host = ping('1.1.1.1', count=10, interval=0.2) ->>> host.address # The IP address of the gateway or host -'1.1.1.1' # that responded to the request +>>> host.address # The IP address of the host that responded +'1.1.1.1' # to the request >>> host.min_rtt # The minimum round-trip time 12.2 @@ -168,17 +207,18 @@
### Multiping -Send *ICMP ECHO_REQUEST* packets to multiple network hosts. - -#### Definition -```python -multiping(addresses, count=2, interval=1, timeout=2, id=PID, max_threads=10, **kwargs) +Send ICMP Echo Request packets to several network hosts. + +This function relies on a single thread to send multiple packets simultaneously. If you mix IPv4 and IPv6 addresses, up to two threads are used. + +```python +multiping(addresses, count=2, interval=0.01, timeout=2, id=PID, source=None, privileged=True, **kwargs) ``` #### Parameters - `addresses` - The IP addresses of the gateways or hosts to which messages should be sent. + The IP addresses of the hosts to which messages should be sent. Hostnames and FQDNs are not allowed. You can easily retrieve their IP address by calling the built-in `resolve` function. - Type: `list of str` @@ -194,45 +234,79 @@ The interval in seconds between sending each packet. - Type: `int` or `float` - - Default: `1` + - Default: `0.01` - `timeout` - The maximum waiting time for receiving a reply in seconds. + The maximum waiting time for receiving all responses in seconds. - Type: `int` or `float` - Default: `2` - `id` - The identifier of the requests. This identifier will be incremented by one for each destination. + The identifier of ICMP requests. Used to match the responses with requests. This identifier will be incremented by one for each destination. On Linux, this identifier is ignored when the `privileged` parameter is disabled. - Type: `int` - Default: `PID` -- `max_threads` - - The number of threads allowed to speed up processing. - - - Type: `int` - - Default: `10` - -- `**kwargs` - - `Optional` Advanced use: arguments passed to `ICMPRequest` objects. +- `source` + + The IP address from which you want to send packets. By default, the interface is automatically chosen according to the specified destination. This parameter should not be used if you are passing both IPv4 and IPv6 addresses to this function. + + - Type: `str` + - Default: `None` + +- `privileged` + + When this option is enabled, this library fully manages the exchanges and the structure of ICMP packets. Disable this option if you want to use this function without root privileges and let the kernel handle ICMP headers. + + *Only available on Unix systems. Ignored on Windows.* + + - Type: `bool` + - Default: `True` + +- `payload` + + The payload content in bytes. A random payload is used by default. + + - Type: `bytes` + - Default: `None` + +- `payload_size` + + The payload size. Ignored when the `payload` parameter is set. + + - Type: `int` + - Default: `56` + +- `traffic_class` + + The traffic class of ICMP packets. Provides a defined level of service to packets by setting the DS Field (formerly TOS) or the Traffic Class field of IP headers. Packets are delivered with the minimum priority by default (Best-effort delivery). Intermediate routers must be able to support this feature. + + *Only available on Unix systems. Ignored on Windows.* + + - Type: `int` + - Default: `0` #### Return value -- `List of Host` - - A `list of Host` objects containing statistics about the desired destinations:
- `address`, `min_rtt`, `avg_rtt`, `max_rtt`, `packets_sent`,
- `packets_received`, `packet_loss`, `is_alive`.
+- A `list of Host` objects containing statistics about the desired destinations:
+ `address`, `min_rtt`, `avg_rtt`, `max_rtt`, `packets_sent`, `packets_received`, `packet_loss`, `is_alive` + The list is sorted in the same order as the addresses passed in parameters. #### Exceptions - `SocketPermissionError` - If the permissions are insufficient to create a socket. + If the privileges are insufficient to create the socket. + +- `SocketAddressError` + + If the source address cannot be assigned to the socket. + +- `ICMPSocketError` + + If another error occurs. See the `ICMPv4Socket` or `ICMPv6Socket` class for details. #### Example ```python @@ -257,17 +331,18 @@ ### Traceroute Determine the route to a destination host. -The Internet is a large and complex aggregation of network hardware, connected together by gateways. Tracking the route one's packets follow can be difficult. This function utilizes the IP protocol time to live field and attempts to elicit an *ICMP TIME_EXCEEDED* response from each gateway along the path to some host. - -#### Definition -```python -traceroute(address, count=3, interval=0.05, timeout=2, id=PID, traffic_class=0, max_hops=30, fast_mode=False, **kwargs) +The Internet is a large and complex aggregation of network hardware, connected together by gateways. Tracking the route one's packets follow can be difficult. This function uses the IP protocol time to live field and attempts to elicit an ICMP Time Exceeded response from each gateway along the path to some host. + +*This function requires root privileges to run.* + +```python +traceroute(address, count=2, interval=0.05, timeout=2, id=PID, first_hop=1, max_hops=30, source=None, fast=False, **kwargs) ``` #### Parameters - `address` - The destination IP address. + The IP address, hostname or FQDN of the host to reach. For deterministic behavior, prefer to use an IP address. - Type: `str` @@ -276,7 +351,7 @@ The number of ping to perform per hop. - Type: `int` - - Default: `3` + - Default: `2` - `interval` @@ -294,21 +369,17 @@ - `id` - The identifier of the request. Used to match the reply with the request.
- In practice, a unique identifier is used for every ping process. + The identifier of ICMP requests. Used to match the responses with requests. In practice, a unique identifier should be used for every traceroute process. - Type: `int` - Default: `PID` -- `traffic_class` - - The traffic class of packets. Provides a defined level of service to packets by setting the DS Field (formerly TOS) or the Traffic Class field of IP headers. Packets are delivered with the minimum priority by default (Best-effort delivery). - - Intermediate routers must be able to support this feature.
- *Only available on Unix systems. Ignored on Windows.* - - - Type: `int` - - Default: `0` +- `first_hop` + + The initial time to live value used in outgoing probe packets. + + - Type: `int` + - Default: `1` - `max_hops` @@ -317,37 +388,75 @@ - Type: `int` - Default: `30` -- `fast_mode` - - When this option is enabled and an intermediate router has been reached, skip to the next hop rather than perform additional requests. The `count` parameter then becomes the maximum number of requests in case of no responses. +- `source` + + The IP address from which you want to send packets. By default, the interface is automatically chosen according to the specified destination. + + - Type: `str` + - Default: `None` + +- `fast` + + When this option is enabled and an intermediate router has been reached, skip to the next hop rather than perform additional requests. The `count` parameter then becomes the maximum number of requests in the event of no response. - Type: `bool` - Default: `False` -- `**kwargs` - - `Optional` Advanced use: arguments passed to `ICMPRequest` objects. +- `payload` + + The payload content in bytes. A random payload is used by default. + + - Type: `bytes` + - Default: `None` + +- `payload_size` + + The payload size. Ignored when the `payload` parameter is set. + + - Type: `int` + - Default: `56` + +- `traffic_class` + + The traffic class of ICMP packets. Provides a defined level of service to packets by setting the DS Field (formerly TOS) or the Traffic Class field of IP headers. Packets are delivered with the minimum priority by default (Best-effort delivery). Intermediate routers must be able to support this feature. + + *Only available on Unix systems. Ignored on Windows.* + + - Type: `int` + - Default: `0` #### Return value -- `List of Hop` - - A `list of Hop` objects representing the route to the desired host. A `Hop` is a `Host` object with an additional attribute: a `distance`. The list is sorted in ascending order according to the distance (in terms of hops) that separates the remote host from the current machine. +- A `list of Hop` objects representing the route to the desired destination. A `Hop` has the same properties as a `Host` object but it also has a `distance`. + + The list is sorted in ascending order according to the distance, in terms of hops, that separates the remote host from the current machine. Gateways that do not respond to requests are not added to this list. #### Exceptions +- `NameLookupError` + + If you pass a hostname or FQDN in parameters and it does not exist or cannot be resolved. + - `SocketPermissionError` - If the permissions are insufficient to create a socket. + If the privileges are insufficient to create the socket. + +- `SocketAddressError` + + If the source address cannot be assigned to the socket. + +- `ICMPSocketError` + + If another error occurs. See the `ICMPv4Socket` or `ICMPv6Socket` class for details. #### Example ```python >>> hops = traceroute('1.1.1.1') ->>> print('Distance (ttl) Address Average round-trip time') +>>> print('Distance/TTL Address Average round-trip time') >>> last_distance = 0 >>> for hop in hops: ... if last_distance + 1 != hop.distance: -... print('Some routers are not responding') +... print('Some gateways are not responding') ... ... # See the Hop class for details ... print(f'{hop.distance} {hop.address} {hop.avg_rtt} ms') @@ -355,55 +464,52 @@ ... last_distance = hop.distance ... -# Distance (ttl) Address Average round-trip time -# 1 10.0.0.1 5.196 ms -# 2 194.149.169.49 7.552 ms -# 3 194.149.166.54 12.21 ms -# * Some routers are not responding -# 5 212.73.205.22 22.15 ms -# 6 1.1.1.1 13.59 ms +# Distance/TTL Address Average round-trip time +# 1 10.0.0.1 5.196 ms +# 2 194.149.169.49 7.552 ms +# 3 194.149.166.54 12.21 ms +# * Some gateways are not responding +# 5 212.73.205.22 22.15 ms +# 6 1.1.1.1 13.59 ms ```
## ICMP sockets -If you want to create your own functions and classes using the ICMP protocol, you can use the `ICMPv4Socket` (for IPv4) and the `ICMPv6Socket` (for IPv6 only). These classes have many methods and attributes in common. They manipulate `ICMPRequest` and `ICMPReply` objects. - -``` - ┌─────────────────┐ - ┌─────────────────┐ send(...) │ ICMPSocket: │ receive() ┌─────────────────┐ - │ ICMPRequest │ ────────────> │ ICMPv4Socket or │ ────────────> │ ICMPReply │ - └─────────────────┘ │ ICMPv6Socket │ └─────────────────┘ - └─────────────────┘ +If you want to create your own functions and classes using the ICMP protocol, you can use the `ICMPv4Socket` (for IPv4 only) and the `ICMPv6Socket` (for IPv6 only). These classes have many methods and properties in common. They manipulate `ICMPRequest` and `ICMPReply` objects. + +``` + ┌──────────────────┐ + ┌─────────────────┐ send(...) │ ICMPv4Socket │ receive() ┌─────────────────┐ + │ ICMPRequest │ ────────────> │ or │ ────────────> │ ICMPReply │ + └─────────────────┘ │ ICMPv6Socket │ └─────────────────┘ + └──────────────────┘ ``` ### ICMPRequest -A user-created object that represents an *ICMP ECHO_REQUEST*. - -#### Definition -```python -ICMPRequest(destination, id, sequence, payload=None, payload_size=56, timeout=2, ttl=64, traffic_class=0) -``` - -#### Parameters / Getters +A user-created object that represents an ICMP Echo Request. + +```python +ICMPRequest(destination, id, sequence, payload=None, payload_size=56, ttl=64, traffic_class=0) +``` + +#### Parameters and properties - `destination` - The IP address of the gateway or host to which the message should be sent. + The IP address of the host to which the message should be sent. - Type: `str` - `id` - The identifier of the request. Used to match the reply with the request.
- In practice, a unique identifier is used for every ping process. + The identifier of the request. Used to match the reply with the request. In practice, a unique identifier is used for every ping process. On Linux, this identifier is automatically replaced if the request is sent from an unprivileged socket. - Type: `int` - `sequence` - The sequence number. Used to match the reply with the request.
- Typically, the sequence number is incremented for each packet sent during the process. + The sequence number. Used to match the reply with the request. Typically, the sequence number is incremented for each packet sent during the process. - Type: `int` @@ -421,48 +527,39 @@ - Type: `int` - Default: `56` -- `timeout` - - The maximum waiting time for receiving a reply in seconds. - - - Type: `int` or `float` - - Default: `2` - - `ttl` - The time to live of the packet in seconds. + The time to live of the packet in terms of hops. - Type: `int` - Default: `64` - `traffic_class` - The traffic class of the packet. Provides a defined level of service to the packet by setting the DS Field (formerly TOS) or the Traffic Class field of the IP header. Packets are delivered with the minimum priority by default (Best-effort delivery). - - Intermediate routers must be able to support this feature.
+ The traffic class of the packet. Provides a defined level of service to the packet by setting the DS Field (formerly TOS) or the Traffic Class field of the IP header. Packets are delivered with the minimum priority by default (Best-effort delivery). Intermediate routers must be able to support this feature. + *Only available on Unix systems. Ignored on Windows.* - Type: `int` - Default: `0` -#### Getters only +#### Properties only - `time` - The timestamp of the ICMP request. Initialized to zero when creating the request and replaced by `ICMPv4Socket` or `ICMPv6Socket` with the time of sending. + The timestamp of the ICMP request. Initialized to zero when creating the request and replaced by the `send` method of `ICMPv4Socket` or `ICMPv6Socket` with the time of sending. - Type: `float`
### ICMPReply -A class that represents an ICMP reply. Generated from an `ICMPSocket` object (`ICMPv4Socket` or `ICMPv6Socket`). - -#### Definition +A class that represents an ICMP reply. Generated from an ICMP socket (`ICMPv4Socket` or `ICMPv6Socket`). + ```python ICMPReply(source, id, sequence, type, code, bytes_received, time) ``` -#### Parameters / Getters +#### Parameters and properties - `source` The IP address of the gateway or host that composes the ICMP message. @@ -471,7 +568,7 @@ - `id` - The identifier of the request. Used to match the reply with the request. + The identifier of the reply. Used to match the reply with the request. - Type: `int` @@ -483,13 +580,13 @@ - `type` - The type of message. + The type of ICMP message. - Type: `int` - `code` - The error code. + The ICMP error code. - Type: `int` @@ -508,72 +605,100 @@ #### Methods - `raise_for_status()` - Throw an exception if the reply is not an *ICMP ECHO_REPLY*.
- Otherwise, do nothing. - - - Raises `ICMPv4DestinationUnreachable`: If the ICMPv4 reply is type 3. - - Raises `ICMPv4TimeExceeded`: If the ICMPv4 reply is type 11. - - Raises `ICMPv6DestinationUnreachable`: If the ICMPv6 reply is type 1. - - Raises `ICMPv6TimeExceeded`: If the ICMPv6 reply is type 3. - - Raises `ICMPError`: If the reply is of another type and is not an *ICMP ECHO_REPLY*. + Throw an exception if the reply is not an ICMP Echo Reply. Otherwise, do nothing. + + - Raises `DestinationUnreachable`: If the destination is unreachable for some reason. + - Raises `TimeExceeded`: If the time to live field of the ICMP request has reached zero. + - Raises `ICMPError`: Raised for any other type and ICMP error code, except ICMP Echo Reply messages.
### ICMPv4Socket -Socket for sending and receiving ICMPv4 packets. - -#### Definition -```python -ICMPv4Socket() -``` +Class for sending and receiving ICMPv4 packets. + +```python +ICMPv4Socket(address=None, privileged=True) +``` + +#### Parameters +- `source` + + The IP address from which you want to listen and send packets. By default, the socket listens on all interfaces. + + - Type: `str` + - Default: `None` + +- `privileged` + + When this option is enabled, the socket fully manages the exchanges and the structure of the ICMP packets. Disable this option if you want to instantiate and use the socket without root privileges and let the kernel handle ICMP headers. + + *Only available on Unix systems. Ignored on Windows.* + + - Type: `bool` + - Default: `True` #### Methods -- `__init__()` +- `__init__(address=None, privileged=True)` *Constructor. Automatically called: do not call it directly.* - - Raises `SocketPermissionError`: If the permissions are insufficient to create the socket. + - Raises `SocketPermissionError`: If the privileges are insufficient to create the socket. + - Raises `SocketAddressError`: If the requested address cannot be assigned to the socket. + - Raises `ICMPSocketError`: If another error occurs while creating the socket. - `__del__()` *Destructor. Automatically called: do not call it directly.* - - Call the `close` method. + Call the `close` method. - `send(request)` - Send a request to a host. - + Send an ICMP request message over the network to a remote host.
This operation is non-blocking. Use the `receive` method to get the reply. - - Parameter `ICMPRequest`: The ICMP request you have created. + - Parameter `request` *(ICMPRequest)*: The ICMP request you have created. If the socket is used in non-privileged mode on a Linux system, the identifier defined in the request will be replaced by the kernel. - Raises `SocketBroadcastError`: If a broadcast address is used and the corresponding option is not enabled on the socket (ICMPv4 only). - Raises `SocketUnavailableError`: If the socket is closed. - Raises `ICMPSocketError`: If another error occurs while sending. -- `receive()` - - Receive a reply from a host. - - This method can be called multiple times if you expect several responses (as with a broadcast address). - - - Raises `TimeoutExceeded`: If no response is received before the timeout defined in the request. This exception is also useful for stopping a possible loop in case of multiple responses. +- `receive(request=None, timeout=2)` + + Receive an ICMP reply message from the socket.
+ This method can be called multiple times if you expect several responses as with a broadcast address. + + - Parameter `request` *(ICMPRequest)*: The ICMP request to use to match the response. By default, all ICMP packets arriving on the socket are returned. + - Parameter `timeout` *(int or float)*: The maximum waiting time for receiving the response in seconds. Default to `2`. + - Raises `TimeoutExceeded`: If no response is received before the timeout specified in parameters. - Raises `SocketUnavailableError`: If the socket is closed. - Raises `ICMPSocketError`: If another error occurs while receiving. - - Returns `ICMPReply`: An `ICMPReply` object containing the reply of the desired destination. See the `ICMPReply` class for details. + + Returns an `ICMPReply` object representing the response of the desired destination or an upstream gateway. - `close()` Close the socket. It cannot be used after this call. -#### Getters only +#### Properties +- `address` + + The IP address from which the socket listens and sends packets. Return `None` if the socket listens on all interfaces. + + - Type: `str` + +- `is_privileged` + + Indicate whether the socket is running in privileged mode. + + - Type: `bool` + - `is_closed` Indicate whether the socket is closed. - Type: `bool` -#### Getters / Setters +#### Properties and setters - `broadcast` Enable or disable the broadcast support on the socket. @@ -584,15 +709,14 @@
### ICMPv6Socket -Socket for sending and receiving ICMPv6 packets. - -#### Definition -```python -ICMPv6Socket() -``` - -#### Methods -The same methods as for the `ICMPv4Socket` class. +Class for sending and receiving ICMPv6 packets. + +```python +ICMPv6Socket(address=None, privileged=True) +``` + +#### Methods and properties +The same methods and properties as for the `ICMPv4Socket` class, except the `broadcast` property.
@@ -601,25 +725,29 @@ ``` ICMPLibError + ├─ NameLookupError ├─ ICMPSocketError + │ ├─ SocketAddressError │ ├─ SocketPermissionError │ ├─ SocketUnavailableError │ ├─ SocketBroadcastError │ └─ TimeoutExceeded - │ + │ └─ ICMPError ├─ DestinationUnreachable │ ├─ ICMPv4DestinationUnreachable │ └─ ICMPv6DestinationUnreachable - │ + │ └─ TimeExceeded ├─ ICMPv4TimeExceeded └─ ICMPv6TimeExceeded ``` - `ICMPLibError`: Exception class for the icmplib package. +- `NameLookupError`: Raised when the requested name does not exist or cannot be resolved. This concerns both Fully Qualified Domain Names and hostnames. - `ICMPSocketError`: Base class for ICMP sockets exceptions. -- `SocketPermissionError`: Raised when the permissions are insufficient to create a socket. +- `SocketAddressError`: Raised when the requested address cannot be assigned to the socket. +- `SocketPermissionError`: Raised when the privileges are insufficient to create the socket. - `SocketUnavailableError`: Raised when an action is performed while the socket is closed. - `SocketBroadcastError`: Raised when a broadcast address is used and the corresponding option is not enabled on the socket. - `TimeoutExceeded`: Raised when a timeout occurs on a socket. @@ -627,9 +755,7 @@ - `DestinationUnreachable`: Destination Unreachable message is generated by the host or its inbound gateway to inform the client that the destination is unreachable for some reason. - `TimeExceeded`: Time Exceeded message is generated by a gateway to inform the source of a discarded datagram due to the time to live field reaching zero. A Time Exceeded message may also be sent by a host if it fails to reassemble a fragmented datagram within its time limit. -Use the `message` property to retrieve the error message. - -`ICMPError` subclasses have properties to retrieve the response (`reply` property) and the specific message of the error (`message` property). +Use the `message` property to get the error message. `ICMPError` subclasses have a `reply` property to retrieve the response.
@@ -638,51 +764,46 @@ ```python def single_ping(address, timeout=2, id=PID): # Create an ICMP socket - socket = ICMPv4Socket() - - # Create a request - # See the ICMPRequest class for details + sock = ICMPv4Socket() + + # Create an ICMP request + # See the 'ICMPRequest' class for details request = ICMPRequest( destination=address, id=id, - sequence=1, - timeout=timeout) + sequence=1) try: - socket.send(request) - - # If the program arrives in this section, - # it means that the packet has been transmitted - - reply = socket.receive() - - # If the program arrives in this section, - # it means that a packet has been received - # The reply has the same identifier and sequence number that - # the request but it can come from an intermediate gateway + sock.send(request) + + # If the program arrives in this section, it means that the + # packet has been transmitted. + + reply = sock.receive(request, timeout) + + # If the program arrives in this section, it means that a + # packet has been received. The reply has the same identifier + # and sequence number that the request but it can come from + # an intermediate gateway. reply.raise_for_status() - # If the program arrives in this section, - # it means that the destination host has responded to - # the request + # If the program arrives in this section, it means that the + # destination host has responded to the request. except TimeoutExceeded as err: # The timeout has been reached - # Equivalent to print(err.message) print(err) except DestinationUnreachable as err: - # The reply indicates that the destination host is - # unreachable + # The reply indicates that the destination host is unreachable print(err) # Retrieve the response reply = err.reply except TimeExceeded as err: - # The reply indicates that the time to live exceeded - # in transit + # The reply indicates that the time to live exceeded in transit print(err) # Retrieve the response @@ -698,31 +819,30 @@ #### Verbose ping ```python def verbose_ping(address, count=4, interval=1, timeout=2, id=PID): - # ICMPRequest uses a payload of 56 bytes by default - # You can modify it using the payload_size parameter - print(f'PING {address}: 56 data bytes') - - # Detection of the socket to use + # A payload of 56 bytes is used by default. You can modify it using + # the 'payload_size' parameter of your ICMP request. + print(f'PING {address}: 56 data bytes\n') + + # We detect the socket to use from the specified IP address if is_ipv6_address(address): - socket = ICMPv6Socket() + sock = ICMPv6Socket() else: - socket = ICMPv4Socket() + sock = ICMPv4Socket() for sequence in range(count): # We create an ICMP request request = ICMPRequest( destination=address, id=id, - sequence=sequence, - timeout=timeout) + sequence=sequence) try: # We send the request - socket.send(request) + sock.send(request) # We are awaiting receipt of an ICMP reply - reply = socket.receive() + reply = sock.receive(request, timeout) # We received a reply # We display some information @@ -768,14 +888,50 @@ ## FAQ -### How to resolve a FQDN / domain name? -Python has a method to do this in its libraries: -```python ->>> import socket ->>> socket.gethostbyname('github.com') +### How to resolve a FQDN/domain name or a hostname? +The use of the built-in `resolve` function is recommended: + +```python +>>> resolve('github.com') '140.82.118.4' ``` +- If several IP addresses are available, only the first one is returned. This function searches for IPv4 addresses first before searching for IPv6 addresses. +- If you pass an IP address, no lookup is done. The same address is returned. +- Raises a `NameLookupError` exception if the requested name does not exist or cannot be resolved. + +### How to use the library without root privileges? +Since its version 2.0, icmplib can be used without root privileges. + +For this, you can set the `privileged` parameter to `False` on the `ping` and `multiping` functions, as well as the low level classes. By disabling this parameter, the kernel handles some parts of the ICMP headers. + +On some Linux systems, you must allow this feature: + +```shell +$ echo 'net.ipv4.ping_group_range = 0 2147483647' | sudo tee -a /etc/sysctl.conf +$ sudo sysctl -p +``` + +You can check the current value with the following command: + +```shell +$ sysctl net.ipv4.ping_group_range +net.ipv4.ping_group_range = 0 2147483647 +``` + +*Since Ubuntu 20.04 LTS, this manipulation is no longer necessary.* + +[Read more on www.kernel.org](https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt) + +### Why I have no response from a remote host? +In the event of no response from a remote host, several causes are possible: +- Your computer's firewall may not be properly configured. This impacts in particular the `traceroute` function which can no longer receive ICMP Time Exceeded messages. +- The remote host or an upstream gateway is down. +- The remote host or an upstream gateway drops ICMP messages for security reasons. +- In the case of the `traceroute` function, if the last host in the list is not the one expected, more than 30 hops (default) may be needed to reach it. You can try increasing the value of the `max_hops` parameter. + +
+ ## Contributing Comments and enhancements are welcome. @@ -790,6 +946,6 @@ ## License -Copyright 2017-2020 Valentin BELYN. +Copyright 2017-2021 Valentin BELYN. Code released under the GNU LGPLv3 license. See the [LICENSE](LICENSE) for details. diff --git a/debian/changelog b/debian/changelog index ce952ce..fe6cbcf 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,9 +1,10 @@ -python-icmplib (1.2.2-0kali2) UNRELEASED; urgency=medium +python-icmplib (2.1.1-0kali1) UNRELEASED; urgency=medium * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, Repository-Browse. + * New upstream release. - -- Kali Janitor Thu, 19 Nov 2020 14:29:21 -0000 + -- Kali Janitor Fri, 09 Apr 2021 12:40:15 -0000 python-icmplib (1.2.2-0kali1) kali-dev; urgency=medium diff --git a/examples/README.md b/examples/README.md index 44c0645..76cdfcb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -21,6 +21,8 @@ 64 bytes from 1.1.1.1: icmp_seq=1 time=12.597 ms 64 bytes from 1.1.1.1: icmp_seq=2 time=12.475 ms 64 bytes from 1.1.1.1: icmp_seq=3 time=10.822 ms + + Completed. ``` - [verbose-traceroute](verbose_traceroute.py) @@ -33,12 +35,14 @@ Traceroute to ovh.com (198.27.92.1): 56 data bytes, 30 hops max 1 192.168.0.254 192.168.0.254 9.86 ms - 2 194.149.164.56 194.149.164.56 4.6 ms - 3 213.186.32.181 be100-159.th2-1-a9.fr.eu 11.99 ms - 4 94.23.122.146 be102.rbx-g1-nc5.fr.eu 7.81 ms + 2 194.149.164.56 194.149.164.56 4.61 ms + 3 213.186.32.181 be100-159.th2-1-a9.fr.eu 11.97 ms + 4 94.23.122.146 be102.rbx-g1-nc5.fr.eu 15.81 ms 5 * * * - 6 37.187.231.75 be5.rbx-iplb1-a70.fr.eu 17.1 ms + 6 37.187.231.75 be5.rbx-iplb1-a70.fr.eu 17.12 ms 7 198.27.92.1 www.ovh.com 10.87 ms + + Completed. ``` - [broadcast-ping](broadcast_ping.py) @@ -65,4 +69,6 @@ 64 bytes from 10.0.0.17: icmp_seq=3 time=1.112 ms 64 bytes from 10.0.0.40: icmp_seq=3 time=1.384 ms 64 bytes from 10.0.0.41: icmp_seq=3 time=9.565 ms + + Completed. ``` diff --git a/examples/broadcast_ping.py b/examples/broadcast_ping.py index 66829b3..c63c521 100644 --- a/examples/broadcast_ping.py +++ b/examples/broadcast_ping.py @@ -2,9 +2,12 @@ icmplib ~~~~~~~ + A powerful library for forging ICMP packets and performing ping + and traceroute. + https://github.com/ValentinBELYN/icmplib - :copyright: Copyright 2017-2020 Valentin BELYN. + :copyright: Copyright 2017-2021 Valentin BELYN. :license: GNU LGPLv3, see the LICENSE for details. ~~~~~~~ @@ -16,60 +19,52 @@ 64 bytes from 10.0.0.17: icmp_seq=0 time=1.065 ms 64 bytes from 10.0.0.40: icmp_seq=0 time=1.595 ms 64 bytes from 10.0.0.41: icmp_seq=0 time=9.471 ms - 64 bytes from 10.0.0.17: icmp_seq=1 time=0.983 ms 64 bytes from 10.0.0.40: icmp_seq=1 time=1.579 ms 64 bytes from 10.0.0.41: icmp_seq=1 time=9.345 ms - 64 bytes from 10.0.0.17: icmp_seq=2 time=0.916 ms 64 bytes from 10.0.0.40: icmp_seq=2 time=2.031 ms 64 bytes from 10.0.0.41: icmp_seq=2 time=9.554 ms - 64 bytes from 10.0.0.17: icmp_seq=3 time=1.112 ms 64 bytes from 10.0.0.40: icmp_seq=3 time=1.384 ms 64 bytes from 10.0.0.41: icmp_seq=3 time=9.565 ms + + Completed. ''' -from icmplib import ( - ICMPv4Socket, - ICMPRequest, - TimeoutExceeded, - ICMPLibError, - PID) +from icmplib import ICMPv4Socket, ICMPRequest +from icmplib import ICMPLibError, TimeoutExceeded, PID def broadcast_ping(address, count=4, timeout=1, id=PID): - # ICMPRequest uses a payload of 56 bytes by default - # You can modify it using the payload_size parameter - print(f'PING {address}: 56 data bytes') + # A payload of 56 bytes is used by default. You can modify it using + # the 'payload_size' parameter of your ICMP request. + print(f'PING {address}: 56 data bytes\n') # Broadcast is only possible in IPv4 - socket = ICMPv4Socket() + sock = ICMPv4Socket() # We allow the socket to send broadcast packets - socket.broadcast = True + sock.broadcast = True for sequence in range(count): # We create an ICMP request request = ICMPRequest( destination=address, id=id, - sequence=sequence, - timeout=timeout) - - print() + sequence=sequence) try: # We send the request - socket.send(request) + sock.send(request) while 'we receive replies': - # We are awaiting receipt of an ICMP reply - # If there is no more responses, the TimeoutExceeded - # exception is thrown and the loop is stopped - reply = socket.receive() + # We are awaiting receipt of an ICMP reply. If there is + # no more responses, the 'TimeoutExceeded' exception is + # thrown and the loop is stopped. + reply = sock.receive(request, timeout) - # We calculate the round-trip time of the reply + # We calculate the round-trip time round_trip_time = (reply.time - request.time) * 1000 # We display some information @@ -84,7 +79,9 @@ except ICMPLibError: # All other errors - print('An error has occurred.') + print(' An error has occurred.') + + print('\nCompleted.') # Limited broadcast diff --git a/examples/multiping.py b/examples/multiping.py index 0083b38..043bbc3 100644 --- a/examples/multiping.py +++ b/examples/multiping.py @@ -2,9 +2,12 @@ icmplib ~~~~~~~ + A powerful library for forging ICMP packets and performing ping + and traceroute. + https://github.com/ValentinBELYN/icmplib - :copyright: Copyright 2017-2020 Valentin BELYN. + :copyright: Copyright 2017-2021 Valentin BELYN. :license: GNU LGPLv3, see the LICENSE for details. ~~~~~~~ @@ -12,15 +15,10 @@ Example: multiping ''' -from icmplib import multiping +from icmplib import resolve, multiping addresses = [ - # A fully qualified domain name (FQDN) is allowed. The first - # address returned from the DNS resolution will be used. - # For deterministic behavior, prefer to use an IP address. - 'github.com', - # IPv4 addresses '1.1.1.1', '8.8.8.8', @@ -29,9 +27,16 @@ # IPv6 addresses '::1', + + # Hostnames and Fully Qualified Domain Names (FQDNs) are not + # allowed. You can easily retrieve their IP address by calling the + # built-in 'resolve' function. The first address returned from the + # DNS resolution will be used. For deterministic behavior, prefer + # to use an IP address. + resolve('github.com') ] -hosts = multiping(addresses, count=2, interval=0.5, timeout=1) +hosts = multiping(addresses, count=2, timeout=1) hosts_alive = [] hosts_dead = [] @@ -44,7 +49,7 @@ hosts_dead.append(host.address) print(hosts_alive) -# ['github.com', '1.1.1.1', '8.8.8.8', '::1'] +# ['1.1.1.1', '8.8.8.8', '::1', '140.82.121.4'] print(hosts_dead) # ['10.0.0.100', '10.0.0.200'] diff --git a/examples/ping.py b/examples/ping.py index cdb8853..6ab16bb 100644 --- a/examples/ping.py +++ b/examples/ping.py @@ -2,9 +2,12 @@ icmplib ~~~~~~~ + A powerful library for forging ICMP packets and performing ping + and traceroute. + https://github.com/ValentinBELYN/icmplib - :copyright: Copyright 2017-2020 Valentin BELYN. + :copyright: Copyright 2017-2021 Valentin BELYN. :license: GNU LGPLv3, see the LICENSE for details. ~~~~~~~ @@ -21,15 +24,15 @@ print(host.address) # '1.1.1.1' -# The minimum round-trip time +# The minimum round-trip time in milliseconds print(host.min_rtt) # 12.2 -# The average round-trip time +# The average round-trip time in milliseconds print(host.avg_rtt) # 13.2 -# The maximum round-trip time +# The maximum round-trip time in milliseconds print(host.max_rtt) # 17.6 diff --git a/examples/traceroute.py b/examples/traceroute.py index ad758bd..c8ca5ce 100644 --- a/examples/traceroute.py +++ b/examples/traceroute.py @@ -2,9 +2,12 @@ icmplib ~~~~~~~ + A powerful library for forging ICMP packets and performing ping + and traceroute. + https://github.com/ValentinBELYN/icmplib - :copyright: Copyright 2017-2020 Valentin BELYN. + :copyright: Copyright 2017-2021 Valentin BELYN. :license: GNU LGPLv3, see the LICENSE for details. ~~~~~~~ @@ -15,31 +18,29 @@ from icmplib import traceroute -hops = traceroute('1.1.1.1', timeout=1, fast_mode=True) +hops = traceroute('1.1.1.1', timeout=1, fast=True) print(hops) -# [, , -# , , -# , , -# ] - +# [ , +# , +# , +# , +# ] last_distance = 0 for hop in hops: if last_distance + 1 != hop.distance: - print(' * Some routers are not responding') + print(' * Some gateways are not responding') - print(f'{hop.distance:4} {hop.address:15} ' + print(f' {hop.distance:<2} {hop.address:15} ' f'{hop.avg_rtt} ms') last_distance = hop.distance -# 1 192.168.0.254 11.327 ms -# 2 194.149.169.162 16.354 ms -# * Some routers are not responding -# 4 149.11.115.13 11.498 ms -# 5 154.54.61.21 4.335 ms -# 6 154.54.60.126 5.645 ms -# 7 149.11.0.126 5.873 ms -# 8 1.1.1.1 4.561 ms +# 1 10.0.0.1 5.196 ms +# 2 194.149.169.49 7.552 ms +# 3 194.149.166.54 12.21 ms +# * Some gateways are not responding +# 5 212.73.205.22 22.15 ms +# 6 1.1.1.1 13.59 ms diff --git a/examples/verbose_ping.py b/examples/verbose_ping.py index 7535d1d..7a0fe70 100644 --- a/examples/verbose_ping.py +++ b/examples/verbose_ping.py @@ -2,9 +2,12 @@ icmplib ~~~~~~~ + A powerful library for forging ICMP packets and performing ping + and traceroute. + https://github.com/ValentinBELYN/icmplib - :copyright: Copyright 2017-2020 Valentin BELYN. + :copyright: Copyright 2017-2021 Valentin BELYN. :license: GNU LGPLv3, see the LICENSE for details. ~~~~~~~ @@ -17,47 +20,42 @@ 64 bytes from 1.1.1.1: icmp_seq=1 time=12.597 ms 64 bytes from 1.1.1.1: icmp_seq=2 time=12.475 ms 64 bytes from 1.1.1.1: icmp_seq=3 time=10.822 ms + + Completed. ''' - -from icmplib import ( - ICMPv4Socket, - ICMPv6Socket, - ICMPRequest, - TimeoutExceeded, - ICMPError, - ICMPLibError, - is_ipv6_address, - PID) from time import sleep +from icmplib import ICMPv4Socket, ICMPv6Socket, ICMPRequest +from icmplib import ICMPLibError, ICMPError, TimeoutExceeded +from icmplib import PID, is_ipv6_address + def verbose_ping(address, count=4, interval=1, timeout=2, id=PID): - # ICMPRequest uses a payload of 56 bytes by default - # You can modify it using the payload_size parameter + # A payload of 56 bytes is used by default. You can modify it using + # the 'payload_size' parameter of your ICMP request. print(f'PING {address}: 56 data bytes\n') - # Detection of the socket to use + # We detect the socket to use from the specified IP address if is_ipv6_address(address): - socket = ICMPv6Socket() + sock = ICMPv6Socket() else: - socket = ICMPv4Socket() + sock = ICMPv4Socket() for sequence in range(count): # We create an ICMP request request = ICMPRequest( destination=address, id=id, - sequence=sequence, - timeout=timeout) + sequence=sequence) try: # We send the request - socket.send(request) + sock.send(request) # We are awaiting receipt of an ICMP reply - reply = socket.receive() + reply = sock.receive(request, timeout) # We received a reply # We display some information @@ -79,7 +77,7 @@ except TimeoutExceeded: # The timeout has been reached - print(f'Request timeout for icmp_seq {sequence}') + print(f' Request timeout for icmp_seq {sequence}') except ICMPError as err: # An ICMP error message has been received @@ -87,7 +85,9 @@ except ICMPLibError: # All other errors - print('An error has occurred.') + print(' An error has occurred.') + + print('\nCompleted.') verbose_ping('1.1.1.1') diff --git a/examples/verbose_traceroute.py b/examples/verbose_traceroute.py index 3ae1a39..5407460 100644 --- a/examples/verbose_traceroute.py +++ b/examples/verbose_traceroute.py @@ -2,9 +2,12 @@ icmplib ~~~~~~~ + A powerful library for forging ICMP packets and performing ping + and traceroute. + https://github.com/ValentinBELYN/icmplib - :copyright: Copyright 2017-2020 Valentin BELYN. + :copyright: Copyright 2017-2021 Valentin BELYN. :license: GNU LGPLv3, see the LICENSE for details. ~~~~~~~ @@ -14,41 +17,42 @@ Traceroute to ovh.com (198.27.92.1): 56 data bytes, 30 hops max 1 192.168.0.254 192.168.0.254 9.86 ms - 2 194.149.164.56 194.149.164.56 4.6 ms - 3 213.186.32.181 be100-159.th2-1-a9.fr.eu 11.99 ms - 4 94.23.122.146 be102.rbx-g1-nc5.fr.eu 7.81 ms + 2 194.149.164.56 194.149.164.56 4.61 ms + 3 213.186.32.181 be100-159.th2-1-a9.fr.eu 11.97 ms + 4 94.23.122.146 be102.rbx-g1-nc5.fr.eu 15.81 ms 5 * * * - 6 37.187.231.75 be5.rbx-iplb1-a70.fr.eu 17.1 ms + 6 37.187.231.75 be5.rbx-iplb1-a70.fr.eu 17.12 ms 7 198.27.92.1 www.ovh.com 10.87 ms + + Completed. ''' -from icmplib import ( - ICMPv4Socket, - ICMPv6Socket, - ICMPRequest, - TimeoutExceeded, - TimeExceeded, - ICMPLibError, - is_ipv6_address, - PID) - -from socket import getfqdn, gethostbyname +from socket import getfqdn from time import sleep +from icmplib import ICMPv4Socket, ICMPv6Socket, ICMPRequest +from icmplib import ICMPLibError, TimeoutExceeded, TimeExceeded +from icmplib import PID, resolve, is_ipv6_address -def verbose_traceroute(address, count=3, interval=0.05, timeout=2, + +def verbose_traceroute(address, count=2, interval=0.05, timeout=2, id=PID, max_hops=30): - # ICMPRequest uses a payload of 56 bytes by default - # You can modify it using the payload_size parameter - print(f'Traceroute to {address} ({gethostbyname(address)}): ' + # We perform a DNS resolution of the address passed in parameters. + # If the address is already an IP address, no lookup is done. The + # same address is returned. + ip_address = resolve(address) + + # A payload of 56 bytes is used by default. You can modify it using + # the 'payload_size' parameter of your ICMP request. + print(f'Traceroute to {address} ({ip_address}): ' f'56 data bytes, {max_hops} hops max\n') - # Detection of the socket to use - if is_ipv6_address(address): - socket = ICMPv6Socket() + # We detect the socket to use from the specified IP address + if is_ipv6_address(ip_address): + sock = ICMPv6Socket() else: - socket = ICMPv4Socket() + sock = ICMPv4Socket() ttl = 1 host_reached = False @@ -57,24 +61,23 @@ for sequence in range(count): # We create an ICMP request request = ICMPRequest( - destination=address, + destination=ip_address, id=id, sequence=sequence, - timeout=timeout, ttl=ttl) try: # We send the request - socket.send(request) + sock.send(request) # We are awaiting receipt of an ICMP reply - reply = socket.receive() + reply = sock.receive(request, timeout) # We received a reply # We display some information source_name = getfqdn(reply.source) - print(f'{ttl:3} {reply.source:15} ' + print(f' {ttl:<2} {reply.source:15} ' f'{source_name:40} ', end='') # We throw an exception if it is an ICMP error message @@ -104,9 +107,9 @@ except TimeoutExceeded: # The timeout has been reached and no host or gateway - # has responded after multiple attemps + # has responded after multiple attempts if sequence >= count - 1: - print(f'{ttl:3} * * *') + print(f' {ttl:<2} * * *') except ICMPLibError: # Other errors are ignored @@ -114,7 +117,9 @@ ttl += 1 - print() + print('\nCompleted.') +# This function supports both FQDNs and IP addresses. See the 'resolve' +# function for details. verbose_traceroute('ovh.com') diff --git a/icmplib/__init__.py b/icmplib/__init__.py index 813c956..a5a5fed 100644 --- a/icmplib/__init__.py +++ b/icmplib/__init__.py @@ -2,9 +2,12 @@ icmplib ~~~~~~~ + A powerful library for forging ICMP packets and performing ping + and traceroute. + https://github.com/ValentinBELYN/icmplib - :copyright: Copyright 2017-2020 Valentin BELYN. + :copyright: Copyright 2017-2021 Valentin BELYN. :license: GNU LGPLv3, see the LICENSE for details. ~~~~~~~ @@ -29,12 +32,12 @@ from .ping import ping, multiping from .traceroute import traceroute from .exceptions import * -from .utils import PID, is_ipv4_address, is_ipv6_address +from .utils import PID, resolve, is_ipv4_address, is_ipv6_address __author__ = 'Valentin BELYN' -__copyright__ = 'Copyright 2017-2020 Valentin BELYN' +__copyright__ = 'Copyright 2017-2021 Valentin BELYN' __license__ = 'GNU Lesser General Public License v3.0' -__version__ = '1.2.2' -__build__ = '201010' +__version__ = '2.1.1' +__build__ = '210321' diff --git a/icmplib/exceptions.py b/icmplib/exceptions.py index e461d6c..142142d 100644 --- a/icmplib/exceptions.py +++ b/icmplib/exceptions.py @@ -2,9 +2,12 @@ icmplib ~~~~~~~ + A powerful library for forging ICMP packets and performing ping + and traceroute. + https://github.com/ValentinBELYN/icmplib - :copyright: Copyright 2017-2020 Valentin BELYN. + :copyright: Copyright 2017-2021 Valentin BELYN. :license: GNU LGPLv3, see the LICENSE for details. ~~~~~~~ @@ -41,6 +44,18 @@ return self._message +class NameLookupError(ICMPLibError): + ''' + Raised when the requested name does not exist or cannot be + resolved. This concerns both Fully Qualified Domain Names and + hostnames. + + ''' + def __init__(self, name): + message = f'The name \'{name}\' cannot be resolved' + super().__init__(message) + + class ICMPSocketError(ICMPLibError): ''' Base class for ICMP sockets exceptions. @@ -48,9 +63,20 @@ ''' +class SocketAddressError(ICMPSocketError): + ''' + Raised when the requested address cannot be assigned to the socket. + + ''' + def __init__(self, address): + message = f'The requested address ({address}) cannot be ' \ + 'assigned to the socket' + super().__init__(message) + + class SocketPermissionError(ICMPSocketError): ''' - Raised when the permissions are insufficient to create a socket. + Raised when the privileges are insufficient to create the socket. ''' def __init__(self): @@ -76,7 +102,7 @@ ''' def __init__(self): message = 'Broadcast is not allowed: ' \ - 'please use broadcast method (setter) to allow it' + 'please use the \'broadcast\' property to allow it' super().__init__(message) diff --git a/icmplib/ip.py b/icmplib/ip.py deleted file mode 100644 index fe1110f..0000000 --- a/icmplib/ip.py +++ /dev/null @@ -1,152 +0,0 @@ -''' - icmplib - ~~~~~~~ - - https://github.com/ValentinBELYN/icmplib - - :copyright: Copyright 2017-2020 Valentin BELYN. - :license: GNU LGPLv3, see the LICENSE for details. - - ~~~~~~~ - - This program is free software: you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public License - as published by the Free Software Foundation, either version 3 of - the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this program. If not, see - . -''' - -import socket -from .utils import PLATFORM_WINDOWS - - -# Fix for Windows -if PLATFORM_WINDOWS: - socket.IPPROTO_IPV6 = 41 - - -class IPSocket: - def __init__(self, family, protocol): - self._socket = socket.socket( - family=family, - type=socket.SOCK_RAW, - proto=protocol) - - self.timeout = 5 - self.ttl = 64 - self.traffic_class = 0 - - def send(self, payload, address, port): - return self._socket.sendto(payload, (address, port)) - - def receive(self, buffer_size=1024): - packet = self._socket.recvfrom(buffer_size) - - payload = packet[0] - address = packet[1][0] - port = packet[1][1] - - return payload, address, port - - def close(self): - self._socket.close() - - @property - def timeout(self): - return self._timeout - - @timeout.setter - def timeout(self, timeout): - self._socket.settimeout(timeout) - self._timeout = timeout - - @property - def ttl(self): - return self._ttl - - @ttl.setter - def ttl(self, ttl): - self._ttl = ttl - - @property - def traffic_class(self): - return self._traffic_class - - @traffic_class.setter - def traffic_class(self, traffic_class): - self._traffic_class = traffic_class - - -class IPv4Socket(IPSocket): - def __init__(self, protocol): - super().__init__( - family=socket.AF_INET, - protocol=protocol) - - self._broadcast = False - - @IPSocket.ttl.setter - def ttl(self, ttl): - self._ttl = ttl - - self._socket.setsockopt( - socket.IPPROTO_IP, - socket.IP_TTL, - ttl) - - @IPSocket.traffic_class.setter - def traffic_class(self, traffic_class): - self._traffic_class = traffic_class - - if not PLATFORM_WINDOWS: - self._socket.setsockopt( - socket.IPPROTO_IP, - socket.IP_TOS, - traffic_class) - - @property - def broadcast(self): - return self._broadcast - - @broadcast.setter - def broadcast(self, allow): - self._broadcast = allow - - self._socket.setsockopt( - socket.SOL_SOCKET, - socket.SO_BROADCAST, - allow) - - -class IPv6Socket(IPSocket): - def __init__(self, protocol): - super().__init__( - family=socket.AF_INET6, - protocol=protocol) - - @IPSocket.ttl.setter - def ttl(self, ttl): - self._ttl = ttl - - self._socket.setsockopt( - socket.IPPROTO_IPV6, - socket.IPV6_MULTICAST_HOPS, - ttl) - - @IPSocket.traffic_class.setter - def traffic_class(self, traffic_class): - self._traffic_class = traffic_class - - if not PLATFORM_WINDOWS: - self._socket.setsockopt( - socket.IPPROTO_IPV6, - socket.IPV6_TCLASS, - traffic_class) diff --git a/icmplib/models.py b/icmplib/models.py index 4d455e4..1389b26 100644 --- a/icmplib/models.py +++ b/icmplib/models.py @@ -2,9 +2,12 @@ icmplib ~~~~~~~ + A powerful library for forging ICMP packets and performing ping + and traceroute. + https://github.com/ValentinBELYN/icmplib - :copyright: Copyright 2017-2020 Valentin BELYN. + :copyright: Copyright 2017-2021 Valentin BELYN. :license: GNU LGPLv3, see the LICENSE for details. ~~~~~~~ @@ -25,44 +28,41 @@ ''' from .exceptions import * -from .utils import is_ipv6_address class ICMPRequest: ''' - A user-created object that represents an ICMP ECHO_REQUEST. + A user-created object that represents an ICMP Echo Request. :type destination: str - :param destination: The IP address of the gateway or host to which - the message should be sent. + :param destination: The IP address of the host to which the message + should be sent. :type id: int :param id: The identifier of the request. Used to match the reply with the request. In practice, a unique identifier is used for - every ping process. + every ping process. On Linux, this identifier is automatically + replaced if the request is sent from an unprivileged socket. :type sequence: int :param sequence: The sequence number. Used to match the reply with the request. Typically, the sequence number is incremented for each packet sent during the process. - :type payload: bytes - :param payload: (Optional) The payload content in bytes. A random - payload is used by default. - - :type payload_size: int - :param payload_size: (Optional) The payload size. Ignored when the - `payload` parameter is set. - - :type timeout: int or float - :param timeout: (Optional) The maximum waiting time for receiving - the reply in seconds. - - :type ttl: int - :param ttl: (Optional) The time to live of the packet in seconds. - - :type traffic_class: int - :param traffic_class: (Optional) The traffic class of the packet. + :type payload: bytes, optional + :param payload: The payload content in bytes. A random payload is + used by default. + + :type payload_size: int, optional + :param payload_size: The payload size. Ignored when the `payload` + parameter is set. Default to 56. + + :type ttl: int, optional + :param ttl: The time to live of the packet in terms of hops. + Default to 64. + + :type traffic_class: int, optional + :param traffic_class: The traffic class of the packet. Provides a defined level of service to the packet by setting the DS Field (formerly TOS) or the Traffic Class field of the IP header. Packets are delivered with the minimum priority by @@ -72,20 +72,16 @@ ''' def __init__(self, destination, id, sequence, payload=None, - payload_size=56, timeout=2, ttl=64, traffic_class=0): - - id &= 0xffff - sequence &= 0xffff + payload_size=56, ttl=64, traffic_class=0): if payload: payload_size = len(payload) self._destination = destination - self._id = id - self._sequence = sequence + self._id = id & 0xffff + self._sequence = sequence & 0xffff self._payload = payload self._payload_size = payload_size - self._timeout = timeout self._ttl = ttl self._traffic_class = traffic_class self._time = 0 @@ -96,8 +92,7 @@ @property def destination(self): ''' - The IP address of the gateway or host to which the message - should be sent. + The IP address of the host to which the message should be sent. ''' return self._destination @@ -138,17 +133,9 @@ return self._payload_size @property - def timeout(self): - ''' - The maximum waiting time for receiving the reply in seconds. - - ''' - return self._timeout - - @property def ttl(self): ''' - The time to live of the packet in seconds. + The time to live of the packet in terms of hops. ''' return self._ttl @@ -165,8 +152,10 @@ def time(self): ''' The timestamp of the ICMP request. + Initialized to zero when creating the request and replaced by - `ICMPv4Socket` or `ICMPv6Socket` with the time of sending. + the `send` method of `ICMPv4Socket` or `ICMPv6Socket` with the + time of sending. ''' return self._time @@ -174,8 +163,8 @@ class ICMPReply: ''' - A class that represents an ICMP reply. Generated from an - `ICMPSocket` object (`ICMPv4Socket` or `ICMPv6Socket`). + A class that represents an ICMP reply. Generated from an ICMP + socket (`ICMPv4Socket` or `ICMPv6Socket`). :type source: str :param source: The IP address of the gateway or host that composes @@ -190,10 +179,10 @@ the request. :type type: int - :param type: The type of message. + :param type: The type of ICMP message. :type code: int - :param code: The error code. + :param code: The ICMP error code. :type bytes_received: int :param bytes_received: The number of bytes received. @@ -218,21 +207,20 @@ def raise_for_status(self): ''' - Throw an exception if the reply is not an ICMP ECHO_REPLY. + Throw an exception if the reply is not an ICMP Echo Reply. Otherwise, do nothing. - :raises ICMPv4DestinationUnreachable: If the ICMPv4 reply is - type 3. - :raises ICMPv4TimeExceeded: If the ICMPv4 reply is type 11. - :raises ICMPv6DestinationUnreachable: If the ICMPv6 reply is - type 1. - :raises ICMPv6TimeExceeded: If the ICMPv6 reply is type 3. - :raises ICMPError: If the reply is of another type and is not - an ICMP ECHO_REPLY. - - ''' - if is_ipv6_address(self._source): + :raises DestinationUnreachable: If the destination is + unreachable for some reason. + :raises TimeExceeded: If the time to live field of the ICMP + request has reached zero. + :raises ICMPError: Raised for any other type and ICMP error + code, except ICMP Echo Reply messages. + + ''' + if ':' in self._source: echo_reply_type = 129 + errors = { 1: ICMPv6DestinationUnreachable, 3: ICMPv6TimeExceeded @@ -240,6 +228,7 @@ else: echo_reply_type = 0 + errors = { 3: ICMPv4DestinationUnreachable, 11: ICMPv4TimeExceeded @@ -266,7 +255,7 @@ @property def id(self): ''' - The identifier of the request. + The identifier of the reply. Used to match the reply with the request. ''' @@ -284,7 +273,7 @@ @property def type(self): ''' - The type of message. + The type of ICMP message. ''' return self._type @@ -292,7 +281,7 @@ @property def code(self): ''' - The error code. + The ICMP error code. ''' return self._code @@ -306,18 +295,6 @@ return self._bytes_received @property - def received_bytes(self): - ''' - Deprecated: use the `bytes_received` property instead. - - ''' - print('[icmplib] Deprecation Warning: The `received_bytes` ' - 'property will be removed from icmplib 2.0. Use the ' - '`bytes_received` property instead.') - - return self._bytes_received - - @property def time(self): ''' The timestamp of the ICMP reply. @@ -328,21 +305,21 @@ class Host: ''' - A class that represents a host. Simplifies the exploitation of - results from `ping` and `traceroute` functions. + A class that represents a host. It simplifies the use of the + results from the `ping`, `multiping` and `traceroute` functions. :type address: str :param address: The IP address of the gateway or host that responded to the request. :type min_rtt: float - :param min_rtt: The minimum round-trip time. + :param min_rtt: The minimum round-trip time in milliseconds. :type avg_rtt: float - :param avg_rtt: The average round-trip time. + :param avg_rtt: The average round-trip time in milliseconds. :type max_rtt: float - :param max_rtt: The maximum round-trip time. + :param max_rtt: The maximum round-trip time in milliseconds. :type packets_sent: int :param packets_sent: The number of packets transmitted to the @@ -378,7 +355,7 @@ @property def min_rtt(self): ''' - The minimum round-trip time. + The minimum round-trip time in milliseconds. ''' return self._min_rtt @@ -386,7 +363,7 @@ @property def avg_rtt(self): ''' - The average round-trip time. + The average round-trip time in milliseconds. ''' return self._avg_rtt @@ -394,7 +371,7 @@ @property def max_rtt(self): ''' - The maximum round-trip time. + The maximum round-trip time in milliseconds. ''' return self._max_rtt @@ -405,18 +382,6 @@ The number of packets transmitted to the destination host. ''' - return self._packets_sent - - @property - def transmitted_packets(self): - ''' - Deprecated: use the `packets_sent` property instead. - - ''' - print('[icmplib] Deprecation Warning: The ' - '`transmitted_packets` property will be removed from ' - 'icmplib 2.0. Use the `packets_sent` property instead.') - return self._packets_sent @property @@ -429,18 +394,6 @@ return self._packets_received @property - def received_packets(self): - ''' - Deprecated: use the `packets_received` property instead. - - ''' - print('[icmplib] Deprecation Warning: The `received_packets` ' - 'property will be removed from icmplib 2.0. Use the ' - '`packets_received` property instead.') - - return self._packets_received - - @property def packet_loss(self): ''' Packet loss occurs when packets fail to reach their destination. @@ -450,12 +403,15 @@ if not self._packets_sent: return 0.0 - return 1 - self._packets_received / self._packets_sent + packet_loss = 1 - self._packets_received / self._packets_sent + + return round(packet_loss, 3) @property def is_alive(self): ''' - Indicate whether the host is reachable. Return a `boolean`. + Indicate whether the host is reachable. + Return a `boolean`. ''' return self._packets_received > 0 @@ -471,13 +427,13 @@ responded to the request. :type min_rtt: float - :param min_rtt: The minimum round-trip time. + :param min_rtt: The minimum round-trip time in milliseconds. :type avg_rtt: float - :param avg_rtt: The average round-trip time. + :param avg_rtt: The average round-trip time in milliseconds. :type max_rtt: float - :param max_rtt: The maximum round-trip time. + :param max_rtt: The maximum round-trip time in milliseconds. :type packets_sent: int :param packets_sent: The number of packets transmitted to the @@ -488,7 +444,7 @@ host and received by the current host. :type distance: int - :param distance: The distance (in terms of hops) that separates the + :param distance: The distance, in terms of hops, that separates the remote host from the current machine. ''' @@ -506,7 +462,7 @@ @property def distance(self): ''' - The distance (in terms of hops) that separates the remote host + The distance, in terms of hops, that separates the remote host from the current machine. ''' diff --git a/icmplib/ping.py b/icmplib/ping.py index ebe254b..d1dfaf3 100644 --- a/icmplib/ping.py +++ b/icmplib/ping.py @@ -2,9 +2,12 @@ icmplib ~~~~~~~ + A powerful library for forging ICMP packets and performing ping + and traceroute. + https://github.com/ValentinBELYN/icmplib - :copyright: Copyright 2017-2020 Valentin BELYN. + :copyright: Copyright 2017-2021 Valentin BELYN. :license: GNU LGPLv3, see the LICENSE for details. ~~~~~~~ @@ -24,71 +27,93 @@ . ''' -from threading import Thread from time import sleep -from .sockets import ICMPv4Socket, ICMPv6Socket +from .sockets import ICMPv4Socket, ICMPv6Socket, BufferedSocket from .models import ICMPRequest, Host from .exceptions import * -from .utils import PID, resolve, is_ipv6_address - - -class PingThread(Thread): - def __init__(self, **kwargs): - super().__init__() - self._kwargs = kwargs - self._host = None - - def run(self): - self._host = ping(**self._kwargs) - - @property - def host(self): - return self._host - - -def ping(address, count=4, interval=1, timeout=2, id=PID, **kwargs): +from .utils import PID, PLATFORM_LINUX, resolve, is_ipv6_address + + +def ping(address, count=4, interval=1, timeout=2, id=PID, source=None, + privileged=True, **kwargs): ''' - Send ICMP ECHO_REQUEST packets to a network host. + Send ICMP Echo Request packets to a network host. :type address: str - :param address: The IP address of the gateway or host to which - the message should be sent. - - :type count: int - :param count: (Optional) The number of ping to perform. - - :type interval: int or float - :param interval: (Optional) The interval in seconds between sending - each packet. - - :type timeout: int or float - :param timeout: (Optional) The maximum waiting time for receiving - a reply in seconds. - - :type id: int - :param id: (Optional) The identifier of the request. Used to match - the reply with the request. In practice, a unique identifier is - used for every ping process. - - :param **kwargs: (Optional) Advanced use: arguments passed to - `ICMPRequest` objects. + :param address: The IP address, hostname or FQDN of the host to + which messages should be sent. For deterministic behavior, + prefer to use an IP address. + + :type count: int, optional + :param count: The number of ping to perform. Default to 4. + + :type interval: int or float, optional + :param interval: The interval in seconds between sending each + packet. Default to 1. + + :type timeout: int or float, optional + :param timeout: The maximum waiting time for receiving a reply in + seconds. Default to 2. + + :type id: int, optional + :param id: The identifier of ICMP requests. Used to match the + responses with requests. In practice, a unique identifier + should be used for every ping process. On Linux, this + identifier is ignored when the `privileged` parameter is + disabled. + + :type source: str, optional + :param source: The IP address from which you want to send packets. + By default, the interface is automatically chosen according to + the specified destination. + + :type privileged: bool, optional + :param privileged: When this option is enabled, this library fully + manages the exchanges and the structure of ICMP packets. + Disable this option if you want to use this function without + root privileges and let the kernel handle ICMP headers. + Default to True. + Only available on Unix systems. Ignored on Windows. + + Advanced (**kwags): + + :type payload: bytes, optional + :param payload: The payload content in bytes. A random payload is + used by default. + + :type payload_size: int, optional + :param payload_size: The payload size. Ignored when the `payload` + parameter is set. Default to 56. + + :type traffic_class: int, optional + :param traffic_class: The traffic class of ICMP packets. + Provides a defined level of service to packets by setting the + DS Field (formerly TOS) or the Traffic Class field of IP + headers. Packets are delivered with the minimum priority by + default (Best-effort delivery). + Intermediate routers must be able to support this feature. + Only available on Unix systems. Ignored on Windows. :rtype: Host :returns: A `Host` object containing statistics about the desired destination. - :raises SocketPermissionError: If the permissions are insufficient - to create a socket. + :raises NameLookupError: If you pass a hostname or FQDN in + parameters and it does not exist or cannot be resolved. + :raises SocketPermissionError: If the privileges are insufficient + to create the socket. + :raises SocketAddressError: If the source address cannot be + assigned to the socket. + :raises ICMPSocketError: If another error occurs. See the + `ICMPv4Socket` or `ICMPv6Socket` class for details. Usage:: >>> from icmplib import ping >>> host = ping('1.1.1.1') - >>> host.avg_rtt 13.2 - >>> host.is_alive True @@ -98,10 +123,14 @@ address = resolve(address) if is_ipv6_address(address): - socket = ICMPv6Socket() + sock = ICMPv6Socket( + address=source, + privileged=privileged) else: - socket = ICMPv4Socket() + sock = ICMPv4Socket( + address=source, + privileged=privileged) packets_sent = 0 packets_received = 0 @@ -115,14 +144,13 @@ destination=address, id=id, sequence=sequence, - timeout=timeout, **kwargs) try: - socket.send(request) + sock.send(request) packets_sent += 1 - reply = socket.receive() + reply = sock.receive(request, timeout) reply.raise_for_status() packets_received += 1 @@ -151,49 +179,89 @@ packets_sent=packets_sent, packets_received=packets_received) - socket.close() + sock.close() return host -def multiping(addresses, count=2, interval=1, timeout=2, id=PID, - max_threads=10, **kwargs): +def multiping(addresses, count=2, interval=0.01, timeout=2, id=PID, + source=None, privileged=True, **kwargs): ''' - Send ICMP ECHO_REQUEST packets to multiple network hosts. + Send ICMP Echo Request packets to several network hosts. + + This function relies on a single thread to send multiple packets + simultaneously. If you mix IPv4 and IPv6 addresses, up to two + threads are used. :type addresses: list of str - :param addresses: The IP addresses of the gateways or hosts to - which messages should be sent. - - :type count: int - :param count: (Optional) The number of ping to perform per address. - - :type interval: int or float - :param interval: (Optional) The interval in seconds between sending - each packet. - - :type timeout: int or float - :param timeout: (Optional) The maximum waiting time for receiving - a reply in seconds. - - :type id: int - :param id: (Optional) The identifier of the requests. This - identifier will be incremented by one for each destination. - - :type max_threads: int - :param max_threads: (Optional) The number of threads allowed to - speed up processing. - - :param **kwargs: (Optional) Advanced use: arguments passed to - `ICMPRequest` objects. + :param addresses: The IP addresses of the hosts to which messages + should be sent. Hostnames and FQDNs are not allowed. You can + easily retrieve their IP address by calling the built-in + `resolve` function. + + :type count: int, optional + :param count: The number of ping to perform per address. + Default to 2. + + :type interval: int or float, optional + :param interval: The interval in seconds between sending each + packet. Default to 0.01. + + :type timeout: int or float, optional + :param timeout: The maximum waiting time for receiving all + responses in seconds. Default to 2. + + :type id: int, optional + :param id: The identifier of ICMP requests. Used to match the + responses with requests. This identifier will be incremented by + one for each destination. On Linux, this identifier is ignored + when the `privileged` parameter is disabled. + + :type source: str, optional + :param source: The IP address from which you want to send packets. + By default, the interface is automatically chosen according to + the specified destinations. This parameter should not be used + if you are passing both IPv4 and IPv6 addresses to this + function. + + :type privileged: bool, optional + :param privileged: When this option is enabled, this library fully + manages the exchanges and the structure of ICMP packets. + Disable this option if you want to use this function without + root privileges and let the kernel handle ICMP headers. + Default to True. + Only available on Unix systems. Ignored on Windows. + + Advanced (**kwags): + + :type payload: bytes, optional + :param payload: The payload content in bytes. A random payload is + used by default. + + :type payload_size: int, optional + :param payload_size: The payload size. Ignored when the `payload` + parameter is set. Default to 56. + + :type traffic_class: int, optional + :param traffic_class: The traffic class of ICMP packets. + Provides a defined level of service to packets by setting the + DS Field (formerly TOS) or the Traffic Class field of IP + headers. Packets are delivered with the minimum priority by + default (Best-effort delivery). + Intermediate routers must be able to support this feature. + Only available on Unix systems. Ignored on Windows. :rtype: list of Host :returns: A list of `Host` objects containing statistics about the desired destinations. The list is sorted in the same order as the addresses passed in parameters. - :raises SocketPermissionError: If the permissions are insufficient - to create a socket. + :raises SocketPermissionError: If the privileges are insufficient + to create the socket. + :raises SocketAddressError: If the source address cannot be + assigned to the socket. + :raises ICMPSocketError: If another error occurs. See the + `ICMPv4Socket` or `ICMPv6Socket` class for details. Usage:: @@ -214,34 +282,105 @@ See the `Host` class for details. ''' + index = {} + sock_ipv4 = None + sock_ipv6 = None + sequence_offset = 0 + + # We create the ICMP requests and instantiate the sockets + for i, address in enumerate(addresses): + if not privileged and PLATFORM_LINUX: + sequence_offset = i * count + + requests = [ + ICMPRequest( + destination=address, + id=id + i, + sequence=sequence + sequence_offset, + **kwargs) + + for sequence in range(count) + ] + + if is_ipv6_address(address): + if not sock_ipv6: + sock_ipv6 = BufferedSocket( + ICMPv6Socket( + address=source, + privileged=privileged)) + + sock = sock_ipv6 + + else: + if not sock_ipv4: + sock_ipv4 = BufferedSocket( + ICMPv4Socket( + address=source, + privileged=privileged)) + + sock = sock_ipv4 + + index[address] = requests, sock + + # We send the ICMP requests + for sequence in range(count): + for address in addresses: + request = index[address][0][sequence] + sock = index[address][1] + + try: + sock.send(request) + sleep(interval) + + except ICMPSocketError: + pass + hosts = [] - inactive_threads = [] - active_threads = [] - - for i, address in enumerate(addresses): - thread = PingThread( + + # We retrieve the responses and relate them to the ICMP requests + for address in addresses: + requests = index[address][0] + sock = index[address][1] + + packets_received = 0 + min_rtt = float('inf') + avg_rtt = 0.0 + max_rtt = 0.0 + + for request in requests: + try: + reply = sock.receive(request, timeout) + reply.raise_for_status() + packets_received += 1 + + round_trip_time = (reply.time - request.time) * 1000 + avg_rtt += round_trip_time + min_rtt = min(round_trip_time, min_rtt) + max_rtt = max(round_trip_time, max_rtt) + + except ICMPLibError: + pass + + if packets_received: + avg_rtt /= packets_received + + else: + min_rtt = 0.0 + + host = Host( address=address, - count=count, - interval=interval, - timeout=timeout, - id=id + i, - **kwargs) - - inactive_threads.append(thread) - - while inactive_threads: - thread = inactive_threads.pop(0) - thread.start() - active_threads.append(thread) - - if (inactive_threads and - len(active_threads) < max_threads): - sleep(0.05) - continue - - while active_threads: - thread = active_threads.pop(0) - thread.join() - hosts.append(thread.host) + min_rtt=min_rtt, + avg_rtt=avg_rtt, + max_rtt=max_rtt, + packets_sent=len(requests), + packets_received=packets_received) + + hosts.append(host) + + if sock_ipv4: + sock_ipv4.close() + + if sock_ipv6: + sock_ipv6.close() return hosts diff --git a/icmplib/sockets.py b/icmplib/sockets.py index 9267a1a..efdff73 100644 --- a/icmplib/sockets.py +++ b/icmplib/sockets.py @@ -2,9 +2,12 @@ icmplib ~~~~~~~ + A powerful library for forging ICMP packets and performing ping + and traceroute. + https://github.com/ValentinBELYN/icmplib - :copyright: Copyright 2017-2020 Valentin BELYN. + :copyright: Copyright 2017-2021 Valentin BELYN. :license: GNU LGPLv3, see the LICENSE for details. ~~~~~~~ @@ -25,120 +28,92 @@ ''' import socket + +from threading import Thread, Lock, Event from struct import pack, unpack from time import time -from .ip import IPv4Socket, IPv6Socket from .models import ICMPRequest, ICMPReply from .exceptions import * -from .utils import random_byte_message - - -# Echo Request and Echo Reply messages -- RFC 792 / 4443 -# -# 0 1 2 3 -# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Identifier | Sequence Number | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Data ... -# +-+-+-+-+- -# -# ICMPv4 Error message -- RFC 792 -# -# 0 1 2 3 -# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Unused / Depends on the error | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Internet Header + 64 bits of Original Data Datagram | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# -# ICMPv6 Error message -- RFC 4443 -# -# 0 1 2 3 -# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Unused / Depends on the error | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Original packet without exceed the minimum IPv6 MTU | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -class ICMPConfig: - IP_PROTOCOL = -1 - IP_SOCKET = None - - ICMP_HEADER_OFFSET = -1 - ICMP_CODE_OFFSET = ICMP_HEADER_OFFSET + 1 - ICMP_CHECKSUM_OFFSET = ICMP_HEADER_OFFSET + 2 - ICMP_ID_OFFSET = ICMP_HEADER_OFFSET + 4 - ICMP_SEQUENCE_OFFSET = ICMP_HEADER_OFFSET + 6 - ICMP_PAYLOAD_OFFSET = ICMP_HEADER_OFFSET + 8 - - ICMP_ECHO_REQUEST = -1 - ICMP_ECHO_REPLY = -1 - - -class ICMPv4Config(ICMPConfig): - IP_PROTOCOL = 1 - IP_SOCKET = IPv4Socket - - ICMP_HEADER_OFFSET = 20 - ICMP_CODE_OFFSET = ICMP_HEADER_OFFSET + 1 - ICMP_CHECKSUM_OFFSET = ICMP_HEADER_OFFSET + 2 - ICMP_ID_OFFSET = ICMP_HEADER_OFFSET + 4 - ICMP_SEQUENCE_OFFSET = ICMP_HEADER_OFFSET + 6 - ICMP_PAYLOAD_OFFSET = ICMP_HEADER_OFFSET + 8 - - ICMP_ECHO_REQUEST = 8 - ICMP_ECHO_REPLY = 0 - - -class ICMPv6Config(ICMPConfig): - IP_PROTOCOL = 58 - IP_SOCKET = IPv6Socket - - ICMP_HEADER_OFFSET = 0 - ICMP_CODE_OFFSET = ICMP_HEADER_OFFSET + 1 - ICMP_CHECKSUM_OFFSET = ICMP_HEADER_OFFSET + 2 - ICMP_ID_OFFSET = ICMP_HEADER_OFFSET + 4 - ICMP_SEQUENCE_OFFSET = ICMP_HEADER_OFFSET + 6 - ICMP_PAYLOAD_OFFSET = ICMP_HEADER_OFFSET + 8 - - ICMP_ECHO_REQUEST = 128 - ICMP_ECHO_REPLY = 129 +from .utils import * class ICMPSocket: ''' Base class for ICMP sockets. - :type config: ICMPConfig - :param config: The ICMP socket configuration used to create and - read ICMP packets. - - :raises SocketPermissionError: If the permissions are insufficient + :type address: str, optional + :param address: The IP address from which you want to listen and + send packets. By default, the socket listens on all interfaces. + + :type privileged: bool, optional + :param privileged: When this option is enabled, the socket fully + manages the exchanges and the structure of the ICMP packets. + Disable this option if you want to instantiate and use the + socket without root privileges and let the kernel handle ICMP + headers. Default to True. + Only available on Unix systems. Ignored on Windows. + + :raises SocketPermissionError: If the privileges are insufficient to create the socket. + :raises SocketAddressError: If the requested address cannot be + assigned to the socket. + :raises ICMPSocketError: If another error occurs while creating the + socket. ''' - def __init__(self, config): - self._socket = None - self._config = config - self._last_request = None + _ICMP_HEADER_OFFSET = -1 + _ICMP_HEADER_REAL_OFFSET = -1 + + _ICMP_CODE_OFFSET = _ICMP_HEADER_OFFSET + 1 + _ICMP_CHECKSUM_OFFSET = _ICMP_HEADER_OFFSET + 2 + _ICMP_ID_OFFSET = _ICMP_HEADER_OFFSET + 4 + _ICMP_SEQUENCE_OFFSET = _ICMP_HEADER_OFFSET + 6 + _ICMP_PAYLOAD_OFFSET = _ICMP_HEADER_OFFSET + 8 + + _ICMP_ECHO_REQUEST = -1 + _ICMP_ECHO_REPLY = -1 + + def __init__(self, address=None, privileged=True): + self._sock = None + self._address = address + + # The Linux kernel allows unprivileged users to use datagram + # sockets (SOCK_DGRAM) to send ICMP requests. This feature is + # now supported by the majority of Unix systems. + # Windows is not compatible. + self._privileged = privileged or PLATFORM_WINDOWS try: - self._socket = config.IP_SOCKET( - config.IP_PROTOCOL) - - except OSError: - raise SocketPermissionError + self._sock = self._create_socket( + socket.SOCK_RAW if self._privileged else + socket.SOCK_DGRAM) + + if address: + self._sock.bind((address, 0)) + + except OSError as err: + if err.errno in (1, 13, 10013): + raise SocketPermissionError + + if err.errno in (-9, 49, 99, 10049, 11001): + raise SocketAddressError(address) + + raise ICMPSocketError(str(err)) + + def __enter__(self): + ''' + Return this object. + + ''' + return self + + def __exit__(self, type, value, traceback): + ''' + Call the `close` method. + + ''' + self.close() def __del__(self): ''' @@ -147,27 +122,42 @@ ''' self.close() - def _create_header(self, type, code, checksum, id, sequence): - ''' - Create the ICMP header of a packet. - - ''' - # B: 8 bits - # H: 16 bits - return pack('!2B3H', type, code, checksum, id, sequence) + def _create_socket(self, type): + ''' + Create and return a new socket. Must be overridden. + + ''' + raise NotImplementedError + + def _set_ttl(self, ttl): + ''' + Set the time to live of every IP packet originating from this + socket. Must be overridden. + + ''' + raise NotImplementedError + + def _set_traffic_class(self, traffic_class): + ''' + Set the DS Field (formerly TOS) or the Traffic Class field of + every IP packet originating from this socket. Must be + overridden. + + ''' + raise NotImplementedError def _checksum(self, data): ''' - Calculate the checksum of a packet. - - ''' + Compute the checksum of an ICMP packet. Checksums are used to + verify the integrity of packets. + + ''' + sum = 0 data += b'\x00' - end = len(data) - 1 - sum = 0 - - for i in range(0, end, 2): + + for i in range(0, len(data) - 1, 2): sum += (data[i] << 8) + data[i + 1] - sum = (sum >> 16) + (sum & 0xffff) + sum = (sum & 0xffff) + (sum >> 16) sum = ~sum & 0xffff @@ -175,68 +165,78 @@ def _create_packet(self, id, sequence, payload): ''' - Create a packet. - - ''' - type = self._config.ICMP_ECHO_REQUEST - - # Temporary header to calculate the checksum - header = self._create_header(type, 0, 0, id, sequence) + Build an ICMP packet from an identifier, a sequence number and + a payload. + + This method returns the newly created ICMP header concatenated + to the payload passed in parameters. + + ''' + checksum = 0 + + # Temporary ICMP header to compute the checksum + header = pack('!2B3H', self._ICMP_ECHO_REQUEST, 0, checksum, + id, sequence) + checksum = self._checksum(header + payload) - # Definitive header - header = self._create_header(type, 0, checksum, id, sequence) + # Definitive ICMP header + header = pack('!2B3H', self._ICMP_ECHO_REQUEST, 0, checksum, + id, sequence) return header + payload - def _read_reply(self, packet, source, reply_time): - ''' - Read a reply from bytes. Return an `ICMPReply` object or `None` - if the reply cannot be parsed. - - ''' - bytes_received = ( - len(packet) - - self._config.ICMP_HEADER_OFFSET) - - if len(packet) < self._config.ICMP_CHECKSUM_OFFSET: + def _parse_reply(self, packet, source, current_time): + ''' + Parse an ICMP reply from bytes. + + This method returns an `ICMPReply` object or `None` if the + reply cannot be parsed. + + ''' + bytes_received = len(packet) - self._ICMP_HEADER_OFFSET + + if len(packet) < self._ICMP_CHECKSUM_OFFSET: return None type, code = unpack('!2B', packet[ - self._config.ICMP_HEADER_OFFSET: - self._config.ICMP_CHECKSUM_OFFSET]) - - if type != self._config.ICMP_ECHO_REPLY: - packet = packet[self._config.ICMP_PAYLOAD_OFFSET:] - - if len(packet) < self._config.ICMP_PAYLOAD_OFFSET: + self._ICMP_HEADER_OFFSET: + self._ICMP_CHECKSUM_OFFSET]) + + if type != self._ICMP_ECHO_REPLY: + packet = packet[ + self._ICMP_PAYLOAD_OFFSET + - self._ICMP_HEADER_OFFSET + + self._ICMP_HEADER_REAL_OFFSET:] + + if len(packet) < self._ICMP_PAYLOAD_OFFSET: return None id, sequence = unpack('!2H', packet[ - self._config.ICMP_ID_OFFSET: - self._config.ICMP_PAYLOAD_OFFSET - ]) - - reply = ICMPReply( + self._ICMP_ID_OFFSET: + self._ICMP_PAYLOAD_OFFSET]) + + return ICMPReply( source=source, id=id, sequence=sequence, type=type, code=code, bytes_received=bytes_received, - time=reply_time) - - return reply + time=current_time) def send(self, request): ''' - Send a request to a host. + Send an ICMP request message over the network to a remote host. This operation is non-blocking. Use the `receive` method to get the reply. :type request: ICMPRequest - :param request: The ICMP request you have created. + :param request: The ICMP request you have created. If the + socket is used in non-privileged mode on a Linux system, + the identifier defined in the request will be replaced by + the kernel. :raises SocketBroadcastError: If a broadcast address is used and the corresponding option is not enabled on the socket @@ -245,35 +245,30 @@ :raises ICMPSocketError: If another error occurs while sending. ''' - if not self._socket: + if not self._sock: raise SocketUnavailableError - payload = request.payload - - if not payload: - payload = random_byte_message( - size=request.payload_size) + payload = request.payload or \ + random_byte_message(request.payload_size) packet = self._create_packet( id=request.id, sequence=request.sequence, payload=payload) - self._socket.ttl = request.ttl - self._socket.traffic_class = request.traffic_class - - request._time = time() - try: - # The packet is actually the Ethernet payload (without the - # IP header): the variable name will be changed in future - # versions - self._socket.send( - payload=packet, - address=request.destination, - port=0) - - self._last_request = request + self._set_ttl(request.ttl) + self._set_traffic_class(request.traffic_class) + + request._time = time() + self._sock.sendto(packet, (request.destination, 0)) + + # On Linux, the ICMP request identifier is replaced by the + # kernel with a random port number when a datagram socket + # is used (SOCK_DGRAM). So, we update the request created + # by the user to take this new identifier into account. + if not self._privileged and PLATFORM_LINUX: + request._id = self._sock.getsockname()[1] except PermissionError: raise SocketBroadcastError @@ -281,59 +276,72 @@ except OSError as err: raise ICMPSocketError(str(err)) - def receive(self): - ''' - Receive a reply from a host. + def receive(self, request=None, timeout=2): + ''' + Receive an ICMP reply message from the socket. This method can be called multiple times if you expect several - responses (as with a broadcast address). + responses as with a broadcast address. + + :type request: ICMPRequest, optional + :param request: The ICMP request to use to match the response. + By default, all ICMP packets arriving on the socket are + returned. + + :type timeout: int or float, optional + :param timeout: The maximum waiting time for receiving the + response in seconds. Default to 2. + + :rtype: ICMPReply + :returns: An `ICMPReply` object representing the response of + the desired destination or an upstream gateway. See the + `ICMPReply` class for details. :raises TimeoutExceeded: If no response is received before the - timeout defined in the request. - This exception is also useful for stopping a possible loop - in case of multiple responses. + timeout specified in parameters. :raises SocketUnavailableError: If the socket is closed. :raises ICMPSocketError: If another error occurs while receiving. - :rtype: ICMPReply - :returns: An `ICMPReply` object containing the reply of the - desired destination. - - See the `ICMPReply` class for details. - - ''' - if not self._socket: + ''' + if not self._sock: raise SocketUnavailableError - if not self._last_request: - raise TimeoutExceeded(0) - - request = self._last_request - - current_time = time() - self._socket.timeout = request.timeout - timeout = current_time + request.timeout + self._sock.settimeout(timeout) + time_limit = time() + timeout try: while True: - packet, address, port = self._socket.receive() - reply_time = time() - - if reply_time > timeout: + response = self._sock.recvfrom(1024) + current_time = time() + + packet = response[0] + source = response[1][0] + + if current_time > time_limit: raise socket.timeout - reply = self._read_reply( + # On Linux, the IP header is missing when a datagram + # socket is used (SOCK_DGRAM). To keep the same + # behavior on all operating systems including macOS + # which has this header, we add a padding of the size + # of the missing IP header. + if not self._privileged and PLATFORM_LINUX: + padding = b'\x00' * self._ICMP_HEADER_OFFSET + packet = padding + packet + + reply = self._parse_reply( packet=packet, - source=address, - reply_time=reply_time) - - if (reply and request.id == reply.id and + source=source, + current_time=current_time) + + if (reply and not request or + reply and request.id == reply.id and request.sequence == reply.sequence): return reply except socket.timeout: - raise TimeoutExceeded(request.timeout) + raise TimeoutExceeded(timeout) except OSError as err: raise ICMPSocketError(str(err)) @@ -343,29 +351,135 @@ Close the socket. It cannot be used after this call. ''' - if self._socket: - self._socket.close() - self._socket = None + if self._sock: + self._sock.close() + self._sock = None + + @property + def address(self): + ''' + The IP address from which the socket listens and sends packets. + Return `None` if the socket listens on all interfaces. + + ''' + return self._address + + @property + def is_privileged(self): + ''' + Indicate whether the socket is running in privileged mode. + Return a `boolean`. + + ''' + return self._privileged @property def is_closed(self): ''' - Indicate whether the socket is closed. Return a `boolean`. - - ''' - return self._socket is None + Indicate whether the socket is closed. + Return a `boolean`. + + ''' + return self._sock is None + + +# Echo Request and Echo Reply messages RFC 792 +# +# 0 1 2 3 +# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Type | Code | Checksum | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Identifier | Sequence Number | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Data ... +# +-+-+-+-+- +# +# ICMPv4 Error message RFC 792 +# +# 0 1 2 3 +# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Type | Code | Checksum | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Unused / Depends on the error | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Internet Header + 64 bits of Original Data Datagram | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ class ICMPv4Socket(ICMPSocket): ''' - Socket for sending and receiving ICMPv4 packets. - - :raises SocketPermissionError: If the permissions are insufficient + Class for sending and receiving ICMPv4 packets. + + :type address: str, optional + :param address: The IP address from which you want to listen and + send packets. By default, the socket listens on all interfaces. + + :type privileged: bool, optional + :param privileged: When this option is enabled, the socket fully + manages the exchanges and the structure of the ICMP packets. + Disable this option if you want to instantiate and use the + socket without root privileges and let the kernel handle ICMP + headers. Default to True. + Only available on Unix systems. Ignored on Windows. + + :raises SocketPermissionError: If the privileges are insufficient to create the socket. + :raises SocketAddressError: If the requested address cannot be + assigned to the socket. + :raises ICMPSocketError: If another error occurs while creating the + socket. ''' - def __init__(self): - super().__init__(ICMPv4Config) + _ICMP_HEADER_OFFSET = 20 + _ICMP_HEADER_REAL_OFFSET = 20 + + _ICMP_CODE_OFFSET = _ICMP_HEADER_OFFSET + 1 + _ICMP_CHECKSUM_OFFSET = _ICMP_HEADER_OFFSET + 2 + _ICMP_ID_OFFSET = _ICMP_HEADER_OFFSET + 4 + _ICMP_SEQUENCE_OFFSET = _ICMP_HEADER_OFFSET + 6 + _ICMP_PAYLOAD_OFFSET = _ICMP_HEADER_OFFSET + 8 + + _ICMP_ECHO_REQUEST = 8 + _ICMP_ECHO_REPLY = 0 + + def _create_socket(self, type): + ''' + Create and return a new socket. + + ''' + return socket.socket( + family=socket.AF_INET, + type=type, + proto=socket.IPPROTO_ICMP) + + def _set_ttl(self, ttl): + ''' + Set the time to live of every IP packet originating from this + socket. + + ''' + self._sock.setsockopt( + socket.IPPROTO_IP, + socket.IP_TTL, + ttl) + + def _set_traffic_class(self, traffic_class): + ''' + Set the DS Field (formerly TOS) of every IP packet originating + from this socket. + + Only available on Unix systems. Ignored on Windows. + + ''' + if PLATFORM_WINDOWS: + return + + self._sock.setsockopt( + socket.IPPROTO_IP, + socket.IP_TOS, + traffic_class) @property def broadcast(self): @@ -378,20 +492,288 @@ icmp_socket.broadcast = True ''' - return self._socket.broadcast + return self._sock.getsockopt( + socket.SOL_SOCKET, + socket.SO_BROADCAST) > 0 @broadcast.setter def broadcast(self, allow): - self._socket.broadcast = allow + self._sock.setsockopt( + socket.SOL_SOCKET, + socket.SO_BROADCAST, + allow) + + +# Echo Request and Echo Reply messages RFC 4443 +# +# 0 1 2 3 +# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Type | Code | Checksum | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Identifier | Sequence Number | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Data ... +# +-+-+-+-+- +# +# ICMPv6 Error message RFC 4443 +# +# 0 1 2 3 +# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Type | Code | Checksum | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Unused / Depends on the error | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Original packet without exceed the minimum IPv6 MTU | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +# Windows IPv6 compatibility +if PLATFORM_WINDOWS: socket.IPPROTO_IPV6 = 41 class ICMPv6Socket(ICMPSocket): ''' - Socket for sending and receiving ICMPv6 packets. - - :raises SocketPermissionError: If the permissions are insufficient + Class for sending and receiving ICMPv6 packets. + + :type address: str, optional + :param address: The IP address from which you want to listen and + send packets. By default, the socket listens on all interfaces. + + :type privileged: bool, optional + :param privileged: When this option is enabled, the socket fully + manages the exchanges and the structure of the ICMP packets. + Disable this option if you want to instantiate and use the + socket without root privileges and let the kernel handle ICMP + headers. Default to True. + Only available on Unix systems. Ignored on Windows. + + :raises SocketPermissionError: If the privileges are insufficient to create the socket. + :raises SocketAddressError: If the requested address cannot be + assigned to the socket. + :raises ICMPSocketError: If another error occurs while creating the + socket. ''' - def __init__(self): - super().__init__(ICMPv6Config) + _ICMP_HEADER_OFFSET = 0 + _ICMP_HEADER_REAL_OFFSET = 40 + + _ICMP_CODE_OFFSET = _ICMP_HEADER_OFFSET + 1 + _ICMP_CHECKSUM_OFFSET = _ICMP_HEADER_OFFSET + 2 + _ICMP_ID_OFFSET = _ICMP_HEADER_OFFSET + 4 + _ICMP_SEQUENCE_OFFSET = _ICMP_HEADER_OFFSET + 6 + _ICMP_PAYLOAD_OFFSET = _ICMP_HEADER_OFFSET + 8 + + _ICMP_ECHO_REQUEST = 128 + _ICMP_ECHO_REPLY = 129 + + def _create_socket(self, type): + ''' + Create and return a new socket. + + ''' + return socket.socket( + family=socket.AF_INET6, + type=type, + proto=socket.IPPROTO_ICMPV6) + + def _set_ttl(self, ttl): + ''' + Set the time to live of every IP packet originating from this + socket. + + ''' + self._sock.setsockopt( + socket.IPPROTO_IPV6, + socket.IPV6_UNICAST_HOPS, + ttl) + + def _set_traffic_class(self, traffic_class): + ''' + Set the Traffic Class field of every IP packet originating from + this socket. + + Only available on Unix systems. Ignored on Windows. + + ''' + if PLATFORM_WINDOWS: + return + + self._sock.setsockopt( + socket.IPPROTO_IPV6, + socket.IPV6_TCLASS, + traffic_class) + + +class BufferedSocket: + ''' + A wrapper for ICMP sockets that reads and classifies incoming ICMP + packets into a buffer, in real time. + + Useful if you want to send several ICMP packets consecutively + without waiting for a response between each sending. For this, an + internal thread is used. It stops automatically when you call the + `detach` or `close` method. + + This feature is experimental. There is no guarantee that it will be + retained in future versions. + + :type sock: ICMPSocket + :param sock: An ICMP socket. Once the wrapper instantiated, this + socket should no longer be used directly. + + ''' + def __init__(self, sock): + self._sock = sock + self._buffer = {} + + self._buffer_event = Event() + self._buffer_lock = Lock() + + self._thread = Thread(target=self._read_from_socket) + self._thread.start() + + def __enter__(self): + ''' + Return this object. + + ''' + return self + + def __exit__(self, type, value, traceback): + ''' + Call the `close` method. + + ''' + self.close() + + def __del__(self): + ''' + Call the `close` method. + + ''' + self.close() + + def _read_from_socket(self): + ''' + Internal thread which retrieves incoming ICMP packets from the + socket and adds them to a buffer. + + ''' + while self._sock: + try: + reply = self._sock.receive(timeout=1) + + with self._buffer_lock: + buffer_id = reply.id, reply.sequence + + if buffer_id not in self._buffer: + self._buffer[buffer_id] = [] + + self._buffer[buffer_id].append(reply) + self._buffer_event.set() + + except ICMPSocketError: + pass + + def send(self, request): + ''' + Send an ICMP request message over the network to a remote host. + + This operation is non-blocking. Use the `receive` method to get + the reply. + + :type request: ICMPRequest + :param request: The ICMP request you have created. If the + underlying socket is used in non-privileged mode on a Linux + system, the identifier defined in the request will be + replaced by the kernel. + + :raises SocketBroadcastError: If a broadcast address is used + and the corresponding option is not enabled on the + underlying socket (ICMPv4 only). + :raises SocketUnavailableError: If the socket is closed. + :raises ICMPSocketError: If another error occurs while sending. + + ''' + if not self._sock: + raise SocketUnavailableError + + self._sock.send(request) + + def receive(self, request, timeout=2): + ''' + Retrieve an ICMP reply message from the buffer. + + This method can be called multiple times if you expect several + responses as with a broadcast address. + + :type request: ICMPRequest + :param request: The ICMP request to use to match the response. + + :type timeout: int or float, optional + :param timeout: The maximum waiting time for receiving the + response in seconds. This parameter takes into account the + timestamp of the request. Default to 2. + + :rtype: ICMPReply + :returns: An `ICMPReply` object representing the response of + the desired destination or an upstream gateway. See the + `ICMPReply` class for details. + + :raises TimeoutExceeded: If no response is received before the + timeout specified in parameters. + + ''' + buffer_id = request.id, request.sequence + + while True: + with self._buffer_lock: + self._buffer_event.clear() + + if buffer_id in self._buffer: + reply = self._buffer[buffer_id].pop(0) + + if not self._buffer[buffer_id]: + del self._buffer[buffer_id] + + return reply + + remaining_time = request.time + timeout - time() + + if not self._buffer_event.wait(remaining_time): + raise TimeoutExceeded(timeout) + + def detach(self): + ''' + Detach the socket from the wrapper and return it. The wrapper + cannot be used after this call but the socket can be reused for + other purposes. + + ''' + sock = self._sock + + if self._sock: + self._sock = None + self._thread.join() + + return sock + + def close(self): + ''' + Close this object and the underlying socket. Both cannot be + used after this call. + + ''' + if self._sock: + self.detach().close() + + @property + def is_closed(self): + ''' + Indicate whether the wrapper is still operational or not. + Return a `boolean`. + + ''' + return self._sock is None diff --git a/icmplib/traceroute.py b/icmplib/traceroute.py index 736166a..d5c9f3d 100644 --- a/icmplib/traceroute.py +++ b/icmplib/traceroute.py @@ -2,9 +2,12 @@ icmplib ~~~~~~~ + A powerful library for forging ICMP packets and performing ping + and traceroute. + https://github.com/ValentinBELYN/icmplib - :copyright: Copyright 2017-2020 Valentin BELYN. + :copyright: Copyright 2017-2021 Valentin BELYN. :license: GNU LGPLv3, see the LICENSE for details. ~~~~~~~ @@ -32,38 +35,72 @@ from .utils import PID, resolve, is_ipv6_address -def traceroute(address, count=3, interval=0.05, timeout=2, id=PID, - traffic_class=0, max_hops=30, fast_mode=False, **kwargs): +def traceroute(address, count=2, interval=0.05, timeout=2, id=PID, + first_hop=1, max_hops=30, source=None, fast=False, **kwargs): ''' Determine the route to a destination host. The Internet is a large and complex aggregation of network hardware, connected together by gateways. Tracking the route one's packets - follow can be difficult. This function utilizes the IP protocol - time to live field and attempts to elicit an ICMP TIME_EXCEEDED - response from each gateway along the path to some host. + follow can be difficult. This function uses the IP protocol time to + live field and attempts to elicit an ICMP Time Exceeded response + from each gateway along the path to some host. + + This function requires root privileges to run. :type address: str - :param address: The destination IP address. - - :type count: int - :param count: (Optional) The number of ping to perform per hop. - - :type interval: int or float - :param interval: (Optional) The interval in seconds between sending - each packet. - - :type timeout: int or float - :param timeout: (Optional) The maximum waiting time for receiving - a reply in seconds. - - :type id: int - :param id: (Optional) The identifier of the request. Used to match - the reply with the request. In practice, a unique identifier is - used for every ping process. - - :type traffic_class: int - :param traffic_class: (Optional) The traffic class of packets. + :param address: The IP address, hostname or FQDN of the host to + reach. For deterministic behavior, prefer to use an IP address. + + :type count: int, optional + :param count: The number of ping to perform per hop. Default to 2. + + :type interval: int or float, optional + :param interval: The interval in seconds between sending each + packet. Default to 0.05. + + :type timeout: int or float, optional + :param timeout: The maximum waiting time for receiving a reply in + seconds. Default to 2. + + :type id: int, optional + :param id: The identifier of ICMP requests. Used to match the + responses with requests. In practice, a unique identifier + should be used for every traceroute process. By default, the + identifier corresponds to the PID. + + :type first_hop: int, optional + :param first_hop: The initial time to live value used in outgoing + probe packets. Default to 1. + + :type max_hops: int, optional + :param max_hops: The maximum time to live (max number of hops) used + in outgoing probe packets. Default to 30. + + :type source: str, optional + :param source: The IP address from which you want to send packets. + By default, the interface is automatically chosen according to + the specified destination. + + :type fast: bool, optional + :param fast: When this option is enabled and an intermediate router + has been reached, skip to the next hop rather than perform + additional requests. The `count` parameter then becomes the + maximum number of requests in the event of no response. + Default to False. + + Advanced (**kwags): + + :type payload: bytes, optional + :param payload: The payload content in bytes. A random payload is + used by default. + + :type payload_size: int, optional + :param payload_size: The payload size. Ignored when the `payload` + parameter is set. Default to 56. + + :type traffic_class: int, optional + :param traffic_class: The traffic class of ICMP packets. Provides a defined level of service to packets by setting the DS Field (formerly TOS) or the Traffic Class field of IP headers. Packets are delivered with the minimum priority by @@ -71,28 +108,21 @@ Intermediate routers must be able to support this feature. Only available on Unix systems. Ignored on Windows. - :type max_hops: int - :param max_hops: (Optional) The maximum time to live (max number of - hops) used in outgoing probe packets. - - :type fast_mode: bool - :param fast_mode: (Optional) When this option is enabled and an - intermediate router has been reached, skip to the next hop - rather than perform additional requests. The `count` parameter - then becomes the maximum number of requests in case of no - responses. - - :param **kwargs: (Optional) Advanced use: arguments passed to - `ICMPRequest` objects. - :rtype: list of Hop :returns: A list of `Hop` objects representing the route to the - desired host. The list is sorted in ascending order according - to the distance (in terms of hops) that separates the remote - host from the current machine. - - :raises SocketPermissionError: If the permissions are insufficient - to create a socket. + desired destination. The list is sorted in ascending order + according to the distance, in terms of hops, that separates the + remote host from the current machine. Gateways that do not + respond to requests are not added to this list. + + :raises NameLookupError: If you pass a hostname or FQDN in + parameters and it does not exist or cannot be resolved. + :raises SocketPermissionError: If the privileges are insufficient + to create the socket. + :raises SocketAddressError: If the source address cannot be + assigned to the socket. + :raises ICMPSocketError: If another error occurs. See the + `ICMPv4Socket` or `ICMPv6Socket` class for details. Usage:: @@ -102,18 +132,18 @@ >>> for hop in hops: ... if last_distance + 1 != hop.distance: - ... print('Some routers are not responding') + ... print('Some gateways are not responding') ... ... print(f'{hop.distance} {hop.address} {hop.avg_rtt} ms') ... ... last_distance = hop.distance ... - 1 10.0.0.1 5.196 ms - 2 194.149.169.49 7.552 ms - 3 194.149.166.54 12.21 ms - * Some routers are not responding - 5 212.73.205.22 22.15 ms - 6 1.1.1.1 13.59 ms + 1 10.0.0.1 5.196 ms + 2 194.149.169.49 7.552 ms + 3 194.149.166.54 12.21 ms + * Some gateways are not responding + 5 212.73.205.22 22.15 ms + 6 1.1.1.1 13.59 ms See the `Hop` class for details. @@ -121,12 +151,12 @@ address = resolve(address) if is_ipv6_address(address): - socket = ICMPv6Socket() + sock = ICMPv6Socket(source) else: - socket = ICMPv4Socket() - - ttl = 1 + sock = ICMPv4Socket(source) + + ttl = first_hop host_reached = False hops = [] @@ -144,16 +174,14 @@ destination=address, id=id, sequence=sequence, - timeout=timeout, ttl=ttl, - traffic_class=traffic_class, **kwargs) try: - socket.send(request) + sock.send(request) packets_sent += 1 - reply = socket.receive() + reply = sock.receive(request, timeout) reply.raise_for_status() host_reached = True @@ -171,7 +199,7 @@ min_rtt = min(round_trip_time, min_rtt) max_rtt = max(round_trip_time, max_rtt) - if fast_mode: + if fast: break if packets_received: @@ -190,6 +218,6 @@ ttl += 1 - socket.close() + sock.close() return hops diff --git a/icmplib/utils.py b/icmplib/utils.py index 7e1cc37..f396cdf 100644 --- a/icmplib/utils.py +++ b/icmplib/utils.py @@ -2,9 +2,12 @@ icmplib ~~~~~~~ + A powerful library for forging ICMP packets and performing ping + and traceroute. + https://github.com/ValentinBELYN/icmplib - :copyright: Copyright 2017-2020 Valentin BELYN. + :copyright: Copyright 2017-2021 Valentin BELYN. :license: GNU LGPLv3, see the LICENSE for details. ~~~~~~~ @@ -25,13 +28,18 @@ ''' import socket + from sys import platform from os import getpid from re import match from random import choices +from .exceptions import NameLookupError + PID = getpid() +PLATFORM_LINUX = platform == 'linux' +PLATFORM_MACOS = platform == 'darwin' PLATFORM_WINDOWS = platform == 'win32' @@ -40,54 +48,62 @@ Generate a random byte sequence of the specified size. ''' - bytes_available = ( + sequence = choices( b'abcdefghijklmnopqrstuvwxyz' b'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - b'1234567890' - ) + b'1234567890', k=size) - return bytes( - choices(bytes_available, k=size) - ) + return bytes(sequence) -def resolve(name): +def resolve(name, family=None): ''' Resolve a hostname or FQDN into an IP address. If several IP addresses are available, only the first one is returned. - This function searches for IPv4 addresses first for compatibility - reasons before searching for IPv6 addresses. + :type name: str + :param name: A hostname or a Fully Qualified Domain Name (FQDN). + If you pass an IP address, no lookup is done. The same address + is returned. - If no address is found, the name specified in parameter is - returned so as not to impact the operation of other functions. This - behavior may change in future versions of icmplib. + :type family: int, optional + :param family: The address family. Can be set to `4` for IPv4 or + `6` for IPv6 addresses. By default, this function searches for + IPv4 addresses first for compatibility reasons (A DNS lookup) + before searching for IPv6 addresses (AAAA DNS lookup). + + :raises NameLookupError: If the requested name does not exist or + cannot be resolved. ''' if is_ipv4_address(name) or is_ipv6_address(name): return name - try: - return socket.getaddrinfo( - host=name, - port=None, - family=socket.AF_INET, - type=socket.SOCK_DGRAM - )[0][4][0] + if family is None or family == 4: + try: + return socket.getaddrinfo( + host=name, + port=None, + family=socket.AF_INET, + type=socket.SOCK_DGRAM + )[0][4][0] - except OSError: - pass + except OSError: + pass - try: - return socket.getaddrinfo( - host=name, - port=None, - family=socket.AF_INET6, - type=socket.SOCK_DGRAM - )[0][4][0] + if family is None or family == 6: + try: + return socket.getaddrinfo( + host=name, + port=None, + family=socket.AF_INET6, + type=socket.SOCK_DGRAM + )[0][4][0] - except OSError: - return name + except OSError: + pass + + raise NameLookupError(name) def is_ipv4_address(address): @@ -96,10 +112,8 @@ Return a `boolean`. ''' - return match( - r'^([0-9]{1,3}[.]){3}[0-9]{1,3}$', - address - ) is not None + pattern = r'^([0-9]{1,3}[.]){3}[0-9]{1,3}$' + return match(pattern, address) is not None def is_ipv6_address(address): diff --git a/media/icmplib-logo-2.0.png b/media/icmplib-logo-2.0.png new file mode 100755 index 0000000..00eef83 Binary files /dev/null and b/media/icmplib-logo-2.0.png differ diff --git a/setup.cfg b/setup.cfg index 3618ec1..cf3efb3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = icmplib - version = 1.2.2 + version = 2.1.1 description = Easily forge ICMP packets and make your own ping and traceroute. keywords = pure, implementation, icmp, sockets, ping, multiping, traceroute, ipv4, ipv6, python3 diff --git a/setup.py b/setup.py index 571c07a..9639e04 100644 --- a/setup.py +++ b/setup.py @@ -2,9 +2,12 @@ icmplib ~~~~~~~ + A powerful library for forging ICMP packets and performing ping + and traceroute. + https://github.com/ValentinBELYN/icmplib - :copyright: Copyright 2017-2020 Valentin BELYN. + :copyright: Copyright 2017-2021 Valentin BELYN. :license: GNU LGPLv3, see the LICENSE for details. ~~~~~~~