From 792496a3afbd91149ae2cfe061f0bfa7e677b3ee Mon Sep 17 00:00:00 2001 From: Andrew Cooks Date: Fri, 17 Feb 2023 20:29:21 +1000 Subject: [PATCH 1/3] add support for PMTU discovery --- icmplib/models.py | 26 +++++++++++++++-- icmplib/ping.py | 14 +++++++++ icmplib/sockets.py | 65 ++++++++++++++++++++++++++++++++++++++++++ test/pmtu.py | 71 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 test/pmtu.py diff --git a/icmplib/models.py b/icmplib/models.py index 0478440..38851eb 100644 --- a/icmplib/models.py +++ b/icmplib/models.py @@ -70,12 +70,20 @@ class ICMPRequest: Intermediate routers must be able to support this feature. Only available on Unix systems. Ignored on Windows. + :type pmtudisc_opt: str, optional + :param pmtudisc_opt: The Path MTU Discovery strategy. + Options are: + do - prohibit fragmentation, even local one, + want - do PMTU discovery and fragment locally when packet size is large), or + dont - do not set DF flag. + ''' __slots__ = '_destination', '_id', '_sequence', '_payload', \ - '_payload_size', '_ttl', '_traffic_class', '_time' + '_payload_size', '_ttl', '_traffic_class', '_time', \ + '_pmtudisc_opt' def __init__(self, destination, id, sequence, payload=None, - payload_size=56, ttl=64, traffic_class=0): + payload_size=56, ttl=64, traffic_class=0, pmtudisc_opt='do'): if payload: payload_size = len(payload) @@ -88,6 +96,7 @@ def __init__(self, destination, id, sequence, payload=None, self._ttl = ttl self._traffic_class = traffic_class self._time = 0 + self._pmtudisc_opt = pmtudisc_opt def __repr__(self): return f'' @@ -162,6 +171,19 @@ def time(self): ''' return self._time + @property + def pmtudisc_opt(self): + ''' + The Path MTU Discovery strategy. + + Options are: + do - default; prohibit fragmentation, even local one, + want - do PMTU discovery and fragment locally when packet size is large), or + dont - do not set DF flag. + + ''' + return self._pmtudisc_opt + class ICMPReply: ''' diff --git a/icmplib/ping.py b/icmplib/ping.py index fcd916d..d054dec 100644 --- a/icmplib/ping.py +++ b/icmplib/ping.py @@ -101,6 +101,13 @@ def ping(address, count=4, interval=1, timeout=2, id=None, source=None, Intermediate routers must be able to support this feature. Only available on Unix systems. Ignored on Windows. + :type pmtudisc_opt: str, optional + :param pmtudisc_opt: The Path MTU discovery strategy. + Options are: + do - prohibit fragmentation, even local one, + want - do PMTU discovery and fragment locally when packet size is large), or + dont - do not set DF flag. + :rtype: Host :returns: A `Host` object containing statistics about the desired destination. @@ -233,6 +240,13 @@ async def async_ping(address, count=4, interval=1, timeout=2, id=None, Intermediate routers must be able to support this feature. Only available on Unix systems. Ignored on Windows. + :type pmtudisc_opt: str, optional + :param pmtudisc_opt: The Path MTU discovery strategy. + Options are: + do - prohibit fragmentation, even local one, + want - do PMTU discovery and fragment locally when packet size is large), or + dont - do not set DF flag. + :rtype: Host :returns: A `Host` object containing statistics about the desired destination. diff --git a/icmplib/sockets.py b/icmplib/sockets.py index a2a428e..803c116 100644 --- a/icmplib/sockets.py +++ b/icmplib/sockets.py @@ -35,6 +35,28 @@ from .utils import PLATFORM_LINUX, PLATFORM_MACOS, PLATFORM_WINDOWS +socket.IP_MTU_DISCOVER = 10 +socket.IPV6_MTU_DISCOVER = 23 + +# IP_MTU_DISCOVER values +socket.IP_PMTUDISC_DONT = 0 # Never send DF frames +socket.IP_PMTUDISC_WANT = 1 # Use per route hints +socket.IP_PMTUDISC_DO = 2 # Always DF +socket.IP_PMTUDISC_PROBE = 3 # Ignore dst pmtu + +# IPV6_MTU_DISCOVER values +socket.IPV6_PMTUDISC_DONT = 0 +socket.IPV6_PMTUDISC_WANT = 1 +socket.IPV6_PMTUDISC_DO = 2 +socket.IPV6_PMTUDISC_PROBE = 3 + +PMTUDISC_VAL = { + 'dont' : [socket.IP_PMTUDISC_DONT, socket.IPV6_PMTUDISC_DONT], + 'want' : [socket.IP_PMTUDISC_WANT, socket.IPV6_PMTUDISC_WANT], + 'do' : [socket.IP_PMTUDISC_DO, socket.IPV6_PMTUDISC_DO], + 'probe': [socket.IP_PMTUDISC_PROBE, socket.IPV6_PMTUDISC_PROBE] +} + class ICMPSocket: ''' Base class for ICMP sockets. @@ -146,6 +168,15 @@ def _set_traffic_class(self, traffic_class): ''' raise NotImplementedError + def _set_pmtudisc_opt(self, pmtudisc_opt): + ''' + Set the PMTU Discovery strategy. In IPv4, this is the DF flag. + In IPv6, this is a socket option. + Must be overridden. + + ''' + raise NotImplementedError + def _checksum(self, data): ''' Compute the checksum of an ICMP packet. Checksums are used to @@ -270,6 +301,7 @@ def send(self, request): self._set_ttl(request.ttl) self._set_traffic_class(request.traffic_class) + self._set_pmtudisc_opt(request.pmtudisc_opt) request._time = time() self._sock.sendto(packet, sock_destination) @@ -516,6 +548,20 @@ def _set_traffic_class(self, traffic_class): socket.IP_TOS, traffic_class) + def _set_pmtudisc_opt(self, pmtudisc_opt): + ''' + Set the DF flag in the IPv4 header of every packet originating + from this socket. + + ''' + if not PLATFORM_LINUX: + raise NotImplementedError + + self._sock.setsockopt( + socket.IPPROTO_IP, + socket.IP_MTU_DISCOVER, + PMTUDISC_VAL[pmtudisc_opt][0]) + @property def broadcast(self): ''' @@ -654,6 +700,25 @@ def _set_traffic_class(self, traffic_class): socket.IPV6_TCLASS, traffic_class) + def _set_pmtudisc_opt(self, pmtudisc_opt): + ''' + Set the DONTFRAG socket option for every packet originating + from this socket. + + ''' + if not PLATFORM_LINUX: + raise NotImplementedError + + self._sock.setsockopt( + socket.IPPROTO_IPV6, + socket.IPV6_MTU_DISCOVER, + PMTUDISC_VAL[pmtudisc_opt][1]) + + self._sock.setsockopt( + socket.IPPROTO_IPV6, + socket.IPV6_DONTFRAG, + 1 if pmtudisc_opt in ['do', 'want', 'probe'] else 0) + class AsyncSocket: ''' diff --git a/test/pmtu.py b/test/pmtu.py new file mode 100644 index 0000000..f8680b7 --- /dev/null +++ b/test/pmtu.py @@ -0,0 +1,71 @@ +import ipaddress +from icmplib import ping + + +def findmtu(host, verbose=False, debug=False): + """ + Find the PMTU to the specified host. + Searches the range + Host is an IPv4 or IPv6 address. + """ + + # These headers get added to the ping payload to get to MTU + SIZE_ICMP_HDR = 8 + SIZE_IPV4_HDR = 20 + SIZE_IPV6_HDR = 40 + + # These Ethernet headers are not included in the MTU. + # Subtract them from the size-on-wire to get MTU. + SIZE_ETH2_HDR = 14 + + if isinstance(ipaddress.ip_address(host), ipaddress.IPv6Address): + size_ip_hdr = SIZE_IPV6_HDR + else: + size_ip_hdr = SIZE_IPV4_HDR + + lower = 900 + upper = 10000 + MTU = lower + + if verbose: + print(f"\nhost: {host}") + + while upper - lower > 1: + test_size = lower + int((upper - lower) / 2) + test_MTU = test_size + SIZE_ICMP_HDR + size_ip_hdr + if verbose: + if debug: + s = f" - MTU search range: {upper}-{lower} - {test_MTU+SIZE_ETH2_HDR} bytes on wire " + else: + s = "" + print( + f"checking frame size {test_MTU} {s}", + end="", + flush=True, + ) + result = ping( + host, + count=2, + interval=0.5, + timeout=1, + payload_size=test_size, + pmtudisc_opt="do", + ) + if result.is_alive: + lower = test_size + MTU = test_MTU + if verbose: + print("✅") + else: + upper = test_size + if verbose: + print("❌") + + return MTU + + +hosts = ["10.0.0.1", "fc00::1", "10.1.0.0", "fc00:1::2", "10.1.0.1", "fc00:1::1"] + +for host in hosts: + mtu = findmtu(host, verbose=False, debug=False) + print(f"host: {host} MTU: {mtu}") From 634162dd952ba490ed98ad2ab0c40632c13af4f2 Mon Sep 17 00:00:00 2001 From: Andrew Cooks Date: Sat, 11 Mar 2023 07:25:37 +1000 Subject: [PATCH 2/3] fix pmtu test command line arg parsing --- test/pmtu.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/test/pmtu.py b/test/pmtu.py index f8680b7..5bbfc8e 100644 --- a/test/pmtu.py +++ b/test/pmtu.py @@ -1,3 +1,4 @@ +import argparse import ipaddress from icmplib import ping @@ -64,8 +65,15 @@ def findmtu(host, verbose=False, debug=False): return MTU -hosts = ["10.0.0.1", "fc00::1", "10.1.0.0", "fc00:1::2", "10.1.0.1", "fc00:1::1"] +parser = argparse.ArgumentParser() +parser.add_argument("-v", "--verbose", help="verbose output", action="store_true") +parser.add_argument("-d", "--debug", help="debug output", action="store_true") +parser.add_argument("hosts", help="one or more hosts to test", nargs='+') +args = parser.parse_args() -for host in hosts: - mtu = findmtu(host, verbose=False, debug=False) - print(f"host: {host} MTU: {mtu}") +for host in args.hosts: + mtu = findmtu(host, verbose=args.verbose, debug=args.debug) + if mtu: + print(f"host: {host} MTU: {mtu}") + else: + print("{host} is unreachable") From 37de083248aed9cb5a62a181c37612291516a206 Mon Sep 17 00:00:00 2001 From: Andrew Cooks Date: Sat, 11 Mar 2023 07:27:12 +1000 Subject: [PATCH 3/3] optimise pmtu test - only discover pmtu for reachable hosts - pick a starting test point closer to the more probable MTU - tune the interval and timeout based on discovered RTT --- test/pmtu.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/test/pmtu.py b/test/pmtu.py index 5bbfc8e..5dd8528 100644 --- a/test/pmtu.py +++ b/test/pmtu.py @@ -31,8 +31,18 @@ def findmtu(host, verbose=False, debug=False): if verbose: print(f"\nhost: {host}") + result = ping(host) + if not result.is_alive: + return 0 + + timeout = 1.5 * result.max_rtt / 1E3 + interval = 0.5 * result.max_rtt / 1E3 + if verbose: + print(result) + print(f'PMTU discovery timeout: {timeout}, interval: {interval}') + + test_size = 1700 while upper - lower > 1: - test_size = lower + int((upper - lower) / 2) test_MTU = test_size + SIZE_ICMP_HDR + size_ip_hdr if verbose: if debug: @@ -47,8 +57,8 @@ def findmtu(host, verbose=False, debug=False): result = ping( host, count=2, - interval=0.5, - timeout=1, + interval=interval, + timeout=timeout, payload_size=test_size, pmtudisc_opt="do", ) @@ -62,6 +72,8 @@ def findmtu(host, verbose=False, debug=False): if verbose: print("❌") + test_size = lower + int((upper - lower) / 2) + return MTU