diff --git a/icmplib/models.py b/icmplib/models.py index c055fdf..bc19527 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 5ba1efc..e813d96 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 130f55a..21a26a4 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..5dd8528 --- /dev/null +++ b/test/pmtu.py @@ -0,0 +1,91 @@ +import argparse +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}") + + 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_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=interval, + timeout=timeout, + 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("❌") + + test_size = lower + int((upper - lower) / 2) + + return MTU + + +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 args.hosts: + mtu = findmtu(host, verbose=args.verbose, debug=args.debug) + if mtu: + print(f"host: {host} MTU: {mtu}") + else: + print("{host} is unreachable")