From 088e3ddaa81b95d9d0ab926fbba976e4b1e29d22 Mon Sep 17 00:00:00 2001 From: Andrej Komelj <23163209+akomelj@users.noreply.github.com> Date: Sat, 12 Jul 2025 13:28:12 +0200 Subject: [PATCH] feat: specify device to bind ICMP socket to --- pythonping/__init__.py | 18 +++++++++++++----- pythonping/executor.py | 4 ++-- pythonping/network.py | 7 ++++--- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/pythonping/__init__.py b/pythonping/__init__.py index 2b3c29e..11226c7 100644 --- a/pythonping/__init__.py +++ b/pythonping/__init__.py @@ -1,3 +1,4 @@ +import socket import sys from random import randint from . import network, executor, payload_provider @@ -21,7 +22,8 @@ def ping(target, out=sys.stdout, match=False, source=None, - out_format='legacy'): + out_format='legacy', + device=None): """Pings a remote host and handles the responses :param target: The remote hostname or IP address to ping @@ -51,8 +53,12 @@ def ping(target, 8.8.8.8 with 1000 bytes and reply is truncated to only the first 74 of request payload with packet identifiers the same in request and reply) :type match: bool - :param repr_format: How to __repr__ the response. Allowed: legacy, None - :type repr_format: str + :param source: Source IP for ICMP packets + :type source: str + :param out_format: How to __repr__ the response. Allowed: legacy, None + :type out_format: str + :param device: Name of the network interface to use (to bind ICMP socket to). + :type device: str :return: List with the result of each ping :rtype: executor.ResponseList""" provider = payload_provider.Repeat(b'', 0) @@ -64,9 +70,11 @@ def ping(target, if not payload: payload = random_text(size) provider = payload_provider.Repeat(payload, count) - options = () + options = [] if df: - options = network.Socket.DONT_FRAGMENT + options.append(network.Socket.DONT_FRAGMENT) + if device: + options.append((socket.SOL_SOCKET, socket.SO_BINDTODEVICE, device.encode('utf-8') + b'\0')) # Fix to allow for pythonping multithreaded usage; # no need to protect this loop as no one will ever surpass 0xFFFF amount of threads diff --git a/pythonping/executor.py b/pythonping/executor.py index 085c6b1..aa53622 100644 --- a/pythonping/executor.py +++ b/pythonping/executor.py @@ -268,7 +268,7 @@ def __iter__(self): class Communicator: """Instance actually communicating over the network, sending messages and handling responses""" - def __init__(self, target, payload_provider, timeout, interval, socket_options=(), seed_id=None, + def __init__(self, target, payload_provider, timeout, interval, socket_options=None, seed_id=None, verbose=False, output=sys.stdout, source=None, repr_format=None): """Creates an instance that can handle communication with the target device @@ -281,7 +281,7 @@ def __init__(self, target, payload_provider, timeout, interval, socket_options=( :param interval: Interval to wait between pings, in seconds :type interval: int :param socket_options: Options to specify for the network.Socket - :type socket_options: tuple + :type socket_options: list[tuple[int, int, int | Buffer] :param seed_id: The first ICMP packet ID to use :type seed_id: Union[None, int] :param verbose: Flag to enable verbose mode, defaults to False diff --git a/pythonping/network.py b/pythonping/network.py index 080b9b0..15d3ef2 100644 --- a/pythonping/network.py +++ b/pythonping/network.py @@ -8,7 +8,7 @@ class Socket: PROTO_LOOKUP = {"icmp": socket.IPPROTO_ICMP, "tcp": socket.IPPROTO_TCP, "udp": socket.IPPROTO_UDP, "ip": socket.IPPROTO_IP, "raw": socket.IPPROTO_RAW} - def __init__(self, destination, protocol, options=(), buffer_size=2048, source=None): + def __init__(self, destination, protocol, options=None, buffer_size=2048, source=None): """Creates a network socket to exchange messages :param destination: Destination IP address @@ -16,7 +16,7 @@ def __init__(self, destination, protocol, options=(), buffer_size=2048, source=N :param protocol: Name of the protocol to use :type protocol: str :param options: Options to set on the socket - :type options: tuple + :type options: list[tuple[int, int, int | Buffer] :param source: Source IP to use - implemented in future releases :type source: Union[None, str] :param buffer_size: Size in bytes of the listening buffer for incoming packets (replies) @@ -31,7 +31,8 @@ def __init__(self, destination, protocol, options=(), buffer_size=2048, source=N self.socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, self.protocol) self.source = source if options: - self.socket.setsockopt(*options) + for option in options: + self.socket.setsockopt(*option) # Implementing a version of socket.getprotobyname for this library since built-in is not thread safe # for python 3.5, 3.6, and 3.7: