├── CHANGELOG.md ├── requirements.txt ├── setup.py ├── release.py ├── README.md ├── LICENSE ├── mtrrt ├── example_runs └── scapy-mtr_2020-02-18_16-56-30.svg └── mtraceroute └── __init__.py /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 1.36.1 5 | ------ 6 | * Bump ipython from 8.1.1 to 8.10.0. 7 | * Fixed missing iface parameter in all sr() functions. 8 | 9 | 1.36.0 10 | ------ 11 | * Bump cryptography from 36.0.1 to 39.0.1. 12 | * Bump pillow from 9.0.1 to 9.3.0. 13 | 14 | 1.34.0 15 | ------ 16 | * Solved circular dependencies inside scapy. 17 | * Created as requirements.txt file. 18 | 19 | 1.30.2 20 | ------ 21 | * Fixed ASN discovery for graph clustering. 22 | * Updated example runs. 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appnope==0.1.2 2 | asttokens==2.0.5 3 | backcall==0.2.0 4 | cffi==1.15.0 5 | cryptography==39.0.1 6 | cycler==0.11.0 7 | decorator==5.1.1 8 | executing==0.8.3 9 | fonttools==4.29.1 10 | ipython==8.10.0 11 | jedi==0.18.1 12 | kiwisolver==1.3.2 13 | matplotlib==3.5.1 14 | matplotlib-inline==0.1.3 15 | netifaces==0.11.0 16 | numpy==1.22.2 17 | packaging==21.3 18 | parso==0.8.3 19 | pexpect==4.8.0 20 | pickleshare==0.7.5 21 | Pillow==9.3.0 22 | prompt-toolkit==3.0.28 23 | ptyprocess==0.7.0 24 | pure-eval==0.2.2 25 | pycparser==2.21 26 | Pygments==2.11.2 27 | pygraphviz==1.9 28 | pyparsing==3.0.7 29 | python-dateutil==2.8.2 30 | PyX==0.15 31 | scapy==2.4.5 32 | six==1.16.0 33 | stack-data==0.2.0 34 | traitlets==5.1.1 35 | wcwidth==0.2.5 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | #----------------------------------------------------------------------------- 3 | # A setup.py installation script modified for 'mtraceroute.py'. 4 | #----------------------------------------------------------------------------- 5 | """ 6 | A distutils Python setup file. For setuptools support see setup_egg.py. 7 | """ 8 | import os 9 | import sys 10 | 11 | from distutils.core import setup 12 | 13 | if os.path.exists('MANIFEST'): 14 | os.remove('MANIFEST') 15 | 16 | import release 17 | # 18 | #----------------------------------------------------------------------------- 19 | def main(): 20 | if sys.version_info[:2] < (3, 6): 21 | print("nstipgeolocate requires Python version 3.6.x or higher.") 22 | sys.exit(1) 23 | 24 | if sys.argv[-1] == 'setup.py': 25 | print("To install, run 'python3 setup.py install'") 26 | print() 27 | 28 | setup( 29 | name = release.name, 30 | version = release.version, 31 | description = release.description, 32 | keywords = release.keywords, 33 | download_url = release.download_url, 34 | author = release.author, 35 | author_email = release.author_email, 36 | url = release.url, 37 | packages = release.packages, 38 | package_data = release.package_data, 39 | license = release.license, 40 | long_description = release.long_description, 41 | scripts = release.scripts, 42 | platforms = release.platforms, 43 | classifiers = release.classifiers, 44 | ) 45 | # 46 | #----------------------------------------------------------------------------- 47 | if __name__ == "__main__": 48 | main() 49 | -------------------------------------------------------------------------------- /release.py: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | # Copyright (c) 2020, networksecuritytoolkit.org. All rights reserved. 3 | #----------------------------------------------------------------------------- 4 | 5 | import mtraceroute 6 | 7 | name = 'mtraceroute' 8 | 9 | version = '1.36.0' 10 | 11 | description = 'A python3 library for performing an enhanced scapy Multi-Traceroute (MTR) with resulting SVG visual' 12 | 13 | keywords = [ 14 | 'Networking', 'Systems Administration', 'Traceroute' 15 | ] 16 | 17 | download_url = 'https://github.com/rwhalb/mtraceroute' 18 | 19 | author = 'Ronald W. Henderson' 20 | 21 | author_email = 'rwhalb@verizon.net' 22 | 23 | url = 'http://www.networksecuritytoolkit.org/' 24 | 25 | packages = [ 26 | 'mtraceroute' 27 | ] 28 | 29 | package_data = { 30 | 'mtraceroute': [ 31 | ], 32 | } 33 | 34 | license = 'GPLv2 License' 35 | 36 | long_description = """ 37 | A python3 library for performing an enhanced scapy Multi-Traceroute (MTR) 38 | with resulting SVG visual. 39 | 40 | Features include running multiple queries with each target, display of 41 | Round Trip Time (RTT) calculations, selection of using 42 | Network Protocols: TCP, UDP and ICMP and with enhanced scapy 43 | SVG visual results and session packet capture output. 44 | 45 | This library is also used by the Network Security Toolkit (NST). 46 | The NST (Web User Interface) WUI provides key enhancements including 47 | a GUI options interface, an interactive MTR SVG graphic, 48 | NST IPv4 Address Tools integration, IPv4 Address Geolocation, 49 | MTR session Packet Capture, ASN lookup, 50 | MTR historical session selection and management, 51 | MTR SVG graphic editing, MTR session console output access 52 | and SVG Graphic image conversion. 53 | 54 | See: http://wiki.networksecuritytoolkit.org/nstwiki/index.php/HowTo_Use_The_Scapy:_Multi-Traceroute_(MTR) 55 | """ 56 | 57 | platforms = 'OS Independent' 58 | 59 | scripts = ['mtrrt'] 60 | 61 | classifiers = [ 62 | 'Development Status :: 5 - Production/Stable', 63 | 'Environment :: Console', 64 | 'Environment :: Plugins', 65 | 'Intended Audience :: Developers', 66 | 'Intended Audience :: Education', 67 | 'Intended Audience :: Information Technology', 68 | 'Intended Audience :: Science/Research', 69 | 'Intended Audience :: Network Administrators', 70 | 'Intended Audience :: Telecommunications Industry', 71 | 'License :: OSI Approved :: GPLv2 License', 72 | 'Natural Language :: English', 73 | 'Operating System :: OS Independent', 74 | 'Programming Language :: Python3', 75 | 'Topic :: Education :: Testing', 76 | 'Topic :: Internet', 77 | 'Topic :: Internet :: Log Analysis', 78 | 'Topic :: Software Development', 79 | 'Topic :: Software Development :: Libraries :: Python3 Modules', 80 | 'Topic :: System :: Networking', 81 | 'Topic :: System :: Operating System', 82 | 'Topic :: System :: Shells', 83 | 'Topic :: System :: Network Administration', 84 | 'Topic :: Utilities', 85 | ] 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mtraceroute 2 | 3 | ![Release: 1.36.0](https://img.shields.io/badge/Release-1.36.0-blue) ![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-blue.svg) ![Python3.7|3.8|3.9|3.10](https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9%20%7C%203.10-blue) 4 | --- 5 | A python3 library for performing an enhanced [scapy](https://github.com/secdev/scapy) Multi-Traceroute (MTR) 6 | with resulting SVG visual. 7 | 8 | Features include running multiple queries with each target, display of 9 | Round Trip Time (RTT) calculations, selection of using 10 | Network Protocols: TCP, UDP and ICMP and with enhanced scapy 11 | SVG visual results and session packet capture output. 12 | 13 | This library is also used by the [Network Security Toolkit](https://www.networksecuritytoolkit.org) (NST). 14 | The NST (Web User Interface) WUI provides key enhancements including 15 | a GUI options interface, an interactive MTR SVG graphic, 16 | NST IPv4 Address Tools integration, IPv4 Address Geolocation, 17 | MTR session Packet Capture, ASN lookup, 18 | MTR historical session selection and management, 19 | MTR SVG graphic editing, MTR session console output access 20 | and SVG Graphic image conversion. 21 | 22 | [Details found on the NST Wiki](https://wiki.networksecuritytoolkit.org/nstwiki/index.php/HowTo_Use_The_Scapy:_Multi-Traceroute_-_MTR) 23 | 24 | 25 | ## Installation 26 | ### Pipy 27 | 28 | ``` 29 | TODO 30 | pip3 install mtraceroute 31 | ``` 32 | 33 | ### Manual 34 | 35 | ``` 36 | python3.9 -m venv ./venv 37 | source ./venv/bin/activate 38 | pip3 install -r requirements.txt 39 | python3 setup.py install 40 | ``` 41 | 42 | ## Usage ## 43 | 44 | Use the python3 control script: "mtrrt" to help facilitate the creation of a 45 | Multi-Traceroute (MTR) session. 46 | 47 | ``` 48 | mtrrt --help 49 | 50 | *** The mtrrt script performs a Multi-Traceroute (MTR) using scapy - v1.36.0 *** 51 | 52 | mtrrt -t || --targets [-r || --retry ] [--timeout ] 53 | [--netproto ] [--stype [--sport ]] 54 | [-p || --dports ] 55 | [--minttl ] [--maxttl ] [--gateway ] 56 | [-g || --graphic ] [-s || --showpadding] [--privaddr] [--dotfile] 57 | [-f || --dirfile ] -i | --interface [--l3rawsocket] 58 | [--ptype [--payload ]] 59 | [-q || --nquery ] [-w || --wrpcap ] 60 | [-a || --asnresolver ] [ --vspread ] [--rtt] 61 | [--title ] [--ts ] [-v || --verbose <level>] [-h || --help] 62 | 63 | * Where <Target Host List> and <Destination Ports> are a comma separated string. The <Target Host List> can be in CIDR notation format. 64 | * Use the (--netproto) option to specify the MTR Network Protocol (Must be one of: "TCP" (Default), "UDP", or "ICMP"). 65 | * Use the (--stype) option to choose the source port type: "Random" (Default) or "Increment". 66 | * Use the (--sport) option to specify a source port (Default: 50000) for source port type: "Increment". 67 | If the source port type: "--stype Increment" is used, then the source port will be increased by one (1) for 68 | each packet sent out during an MTR session. 69 | * Use the (--ptype) option to choose the a packet payload type: "Disabled" (TCP Default) "RandStrTerm" (UDP Default), 70 | "RandStr" (ICMP Default), "ASCII" or "ASCII-Hex". 71 | * Use the (--payload) option for a ASCII string value (e.g., 'Data1: 56\n') for ptype: "ASCII". 72 | * Use the (--payload) option for a ASCII-Hex string value (e.g., '01fe44fFEf') for ptype: "ASCII-Hex". 73 | The "--payload ASCII-Hex" option must use 2 ASCII characters to represent one Hexadecimal byte: "f" => "0F" or "0f". 74 | * To add additional TCP destination service ports for tracerouting: "80,443" (Default: "80"). 75 | * Use the (-s || --showpadding) to display packets with padding as red triangles. 76 | * The (-a || --asnresolver) option can be: "Disabled", "All", "whois.cymru.com", "riswhois.ripe.net" or "whois.ra.net". 77 | * Use the (--privaddr) option to disable showing an associated AS Number bound box (cluster) 78 | on the Multi-Traceroute graph for a private IPv4 Address. 79 | * Use the (--timeout) option to limit the time waiting for a Hop packet response (Default: 2.0 seconds). 80 | * Use the (-q || --nquery) count for the number of traces to perform per service target (Default: 1). 81 | * The default graphic type is an SVG graphic: "svg". 82 | * The default directory file name for the resulting mtr graphic: "/tmp/graph.svg". 83 | * Use the (-f || --dirfile) option to change the resulting output directory: 84 | Example: "-f /var/nst/wuiout/scapy/graph.svg" - Output directory: "/var/nst/wuiout/scapy". 85 | * The default Network Interface will be used to send out the traceroute unless the (-i || --interface) option is used. 86 | * Use the (--gateway) option to override the detected gateway address. 87 | * Increase the verbosity output with a level of 1 or more (Default: 0). 88 | * Use the (--dotfile) option to dump the mtr DOT graph object to a file (.dot file) in the output directory. 89 | * Use the (--vspread) option to set the Vertical Separation in inches between nodes (Default: 0.75in). 90 | * Use the (--rtt) option to display Round-Trip Times (msec) on the graphic for each Hop along a Traceroute. 91 | * Use the (--title) option to override the default Title value: "Multi-Traceroute (MTR) Probe". 92 | * Use the (--ts) option to include the Title Time Stamp text below the Title Text. 93 | * Include internal mtr object variables in output at verbosity level: 8. 94 | * Include trace packet dump output at verbosity level: 9 (***Warning: A large text output may occur). 95 | 96 | *** Example: 97 | mtrrt -t "www.google.com,www.networksecuritytoolkit.org" -r 0 --timeout 3.5 --netproto "TCP" -p "80,443" --minttl 1 --maxttl 20 -q 2 -a "All" --vspread 0.60 --rtt -v 1; 98 | ``` 99 | 100 | ## Example Runs 101 | Shown are 3 use cases: A simple one, one more complex and a CIDR trace route. 102 | 103 | ### Simple Run 104 | A single TCP trace route from private host: 10.222.222.10 to target: [www.google.com](https://www.google.com) port: https (443). 105 | This run creates a resultant SVG trace route graphic and packet capture in directory: "/tmp". 106 | 107 | ``` 108 | mtrrt \ 109 | --targets 'www.google.com' \ 110 | --dirfile '/tmp/scapy-mtr_2020-02-18_16-56-30.svg' --nquery 1 --interface 'lan0' \ 111 | --netproto 'TCP' --dports '443' --wrpcap '/tmp/scapy-mtr_2020-02-18_16-56-30.pcap' \ 112 | --retry 0 --minttl 1 --maxttl 30 --asnresolver 'All' --verbose 1 \ 113 | --stype 'Random' --dotfile --rtt --privaddr --ptype 'Disabled' \ 114 | --vspread 0.75 --title 'Multi-Traceroute (MTR) Probe' --ts '2020-02-18 16:56:30' --timeout 2 2>&1; 115 | ``` 116 | 117 | ![Simple Run](example_runs/scapy-mtr_2020-02-18_16-56-30.svg) 118 | 119 | ### Complex Run 120 | A more complex TCP trace route from private host: 10.222.222.10 to targets: [www.google.com](https://www.google.com), [openwrt.org](https://openwrt.org) ports: http (80), https (443). Packets returned with padding are also shown. 121 | This run creates a resultant SVG trace route graphic and packet capture in directory: "/tmp". 122 | 123 | __Note: To zoom in on the graphic below just open the image in a new tab.__ 124 | 125 | ``` 126 | mtrrt \ 127 | --targets 'www.google.com,openwrt.org' \ 128 | --dirfile '/tmp/scapy-mtr_2020-02-18_17-00-04.svg' --nquery 3 --interface 'lan0' \ 129 | --netproto 'TCP' --dports '80, 443' --wrpcap '/tmp/scapy-mtr_2020-02-18_17-00-04.pcap' \ 130 | --retry 0 --minttl 1 --maxttl 30 --asnresolver 'All' --verbose 1 \ 131 | --stype 'Random' --dotfile --showpadding --rtt --privaddr --ptype 'Disabled' \ 132 | --vspread 0.75 --title 'Multi-Traceroute (MTR) Probe' --ts '2020-02-18 17:00:04' --timeout 2 2>&1; 133 | ``` 134 | 135 | ![Complex Run](example_runs/scapy-mtr_2020-02-18_17-00-04.svg) 136 | 137 | ### CIDR Run 138 | A TCP trace route from private host: 10.222.222.10 to [CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) targets: [www.google.com/30](https://www.google.com) port: https (443). The use of multiple queries (i.e., --nquery > 1) and CIDR address notation format can help expose load balancing tiers. 139 | This run creates a resultant SVG trace route graphic and packet capture in directory: "/tmp". 140 | 141 | __Note: To zoom in on the graphic below just open the image in a new tab.__ 142 | 143 | ``` 144 | mtrrt \ 145 | --targets 'www.google.com/30' \ 146 | --dirfile '/tmp/scapy-mtr_2020-02-18_17-04-55.svg' --nquery 3 --interface 'lan0' \ 147 | --netproto 'TCP' --dports '443' --wrpcap '/tmp/scapy-mtr_2020-02-18_17-04-55.pcap' \ 148 | --retry 0 --minttl 1 --maxttl 30 --asnresolver 'All' --verbose 1 \ 149 | --stype 'Random' --dotfile --rtt --privaddr --ptype 'Disabled' \ 150 | --vspread 0.75 --title 'Multi-Traceroute (MTR) Probe' --ts '2020-02-18 17:04:55' --timeout 2 2>&1; 151 | ``` 152 | 153 | ![Complex Run](example_runs/scapy-mtr_2020-02-18_17-04-55.svg) 154 | --- 155 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | <one line to give the program's name and a brief idea of what it does.> 294 | Copyright (C) <year> <name of author> 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | <signature of Ty Coon>, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /mtrrt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # 4 | # Script: mtrrt (Network Security Toolkit (NST) - Copyright: 2015, 2016, 2018, 2020) 5 | # 6 | # Description: Helper script to perform a scapy Multi-Traceroute (MTR) with resulting SVG visual. 7 | 8 | import os, sys, subprocess, getopt 9 | import binascii 10 | import netifaces 11 | from scapy.all import * 12 | from mtraceroute import * 13 | 14 | def usage(): 15 | version = "1.36.0" 16 | print('\n*** The mtrrt script performs a Multi-Traceroute (MTR) using scapy - v{v:s} ***\n'.format(v = version)) 17 | print('mtrrt -t || --targets <Target Host List> [-r || --retry <Retry>] [--timeout <Fractional Seconds>]') 18 | print(' [--netproto <Network Protocol>] [--stype <Type> [--sport <Source Port>]]') 19 | print(' [-p || --dports <Destination Service Ports>]') 20 | print(' [--minttl <Min TTL>] [--maxttl <Max TTL>] [--gateway <IPv4 Address>]') 21 | print(' [-g || --graphic <Graphic Type>] [-s || --showpadding] [--privaddr] [--dotfile]') 22 | print(' [-f || --dirfile <SVG Directory File Name>] -i | --interface <Interface Name> [--l3rawsocket]') 23 | print(' [--ptype <Type> [--payload <Payload>]]') 24 | print(' [-q || --nquery <Query Trace Count>] [-w || --wrpcap <pcap Directory File Name>]') 25 | print(' [-a || --asnresolver <ASN Resolver>] [ --vspread <Vertical Node Separation>] [--rtt]') 26 | print(' [--title <Title Text>] [--ts <Title Time Stamp>] [-v || --verbose <level>] [-h || --help]\n') 27 | print('* Where <Target Host List> and <Destination Ports> are a comma separated string. The <Target Host List> can be in CIDR notation format.') 28 | print('* Use the (--netproto) option to specify the MTR Network Protocol (Must be one of: "TCP" (Default), "UDP", or "ICMP").') 29 | print('* Use the (--stype) option to choose the source port type: "Random" (Default) or "Increment".') 30 | print('* Use the (--sport) option to specify a source port (Default: 50000) for source port type: "Increment".') 31 | print(' If the source port type: "--stype Increment" is used, then the source port will be increased by one (1) for') 32 | print(' each packet sent out during an MTR session.') 33 | print('* Use the (--ptype) option to choose the a packet payload type: "Disabled" (TCP Default) "RandStrTerm" (UDP Default),') 34 | print(' "RandStr" (ICMP Default), "ASCII" or "ASCII-Hex".') 35 | print("* Use the (--payload) option for a ASCII string value (e.g., \'Data1: 56\\n\') for ptype: \"ASCII\".") 36 | print("* Use the (--payload) option for a ASCII-Hex string value (e.g., \'01fe44fFEf\') for ptype: \"ASCII-Hex\".") 37 | print(' The "--payload ASCII-Hex" option must use 2 ASCII characters to represent one Hexadecimal byte: "f" => "0F" or "0f".') 38 | print('* To add additional TCP destination service ports for tracerouting: "80,443" (Default: "80").') 39 | print('* Use the (-s || --showpadding) to display packets with padding as red triangles.') 40 | print('* The (-a || --asnresolver) option can be: "Disabled", "All", "whois.cymru.com", "riswhois.ripe.net" or "whois.ra.net".') 41 | print('* Use the (--privaddr) option to disable showing an associated AS Number bound box (cluster)') 42 | print(' on the Multi-Traceroute graph for a private IPv4 Address.') 43 | print('* Use the (--timeout) option to limit the time waiting for a Hop packet response (Default: 2.0 seconds).') 44 | print('* Use the (-q || --nquery) count for the number of traces to perform per service target (Default: 1).') 45 | print('* The default graphic type is an SVG graphic: "svg".') 46 | print('* The default directory file name for the resulting mtr graphic: "/tmp/graph.svg".') 47 | print('* Use the (-f || --dirfile) option to change the resulting output directory:') 48 | print(' Example: "-f /var/nst/wuiout/scapy/graph.svg" - Output directory: "/var/nst/wuiout/scapy".') 49 | print('* The default Network Interface will be used to send out the traceroute unless the (-i || --interface) option is used.') 50 | print('* Use the (--gateway) option to override the detected gateway address.') 51 | print('* Increase the verbosity output with a level of 1 or more (Default: 0).') 52 | print('* Use the (--dotfile) option to dump the mtr DOT graph object to a file (.dot file) in the output directory.') 53 | print('* Use the (--vspread) option to set the Vertical Separation in inches between nodes (Default: 0.75in).') 54 | print('* Use the (--rtt) option to display Round-Trip Times (msec) on the graphic for each Hop along a Traceroute.') 55 | print('* Use the (--title) option to override the default Title value: "Multi-Traceroute (MTR) Probe".') 56 | print('* Use the (--ts) option to include the Title Time Stamp text below the Title Text.') 57 | print('* Include internal mtr object variables in output at verbosity level: 8.') 58 | print('* Include trace packet dump output at verbosity level: 9 (***Warning: A large text output may occur).\n') 59 | print('*** Example:') 60 | print('mtrrt -t "www.google.com,www.networksecuritytoolkit.org" -r 0 --timeout 3.5 --netproto "TCP" -p "80,443" --minttl 1 --maxttl 20 -q 2 -a "All" --vspread 0.60 --rtt -v 1;\n') 61 | 62 | def main(argv): 63 | targets = [] 64 | retry = -2 65 | timeout = 2.0 66 | netprotocol = "TCP" 67 | srcporttype = "Random" 68 | srcport = 50000 69 | dstports = [80] 70 | minttl = 1 71 | maxttl = 20 72 | payloadtype = None 73 | payload = "" 74 | verboselvl = 0 75 | graphictype = 'svg' 76 | dirfilenamebase = "/tmp/graph." 77 | dirfilename = "" 78 | nic = "" 79 | gateway = "" 80 | showpadding = 0 81 | nquery = 1 82 | asnresolver = "All" 83 | rasn = 1 84 | privaddr = 0 85 | tmppcapfile = "/tmp/_mtr.cap" 86 | pcapdirfilename = "" 87 | nstpcapfilename = "/capture_file.cap" 88 | nstpcaplogname = "/capture_file.log" 89 | dotfile = 0 90 | vspread = 0.75 91 | title = "Multi-Traceroute (MTR) Probe" 92 | timestamp = "" 93 | rtt = 0 94 | 95 | try: 96 | opts, args = getopt.getopt(argv, "hv:t:r:p:g:sq:f:i:w:a:", ["help", "verbose=", "targets=", "retry=", "timeout=", "dports=", "minttl=", "maxttl=", "graphic=", "showpadding", "nquery=", "dirfile=", "interface=", "wrpcap=", "privaddr", "asnresolver=", "dotfile", "vspread=", "title=", "ts=", "rtt", "l3rawsocket", "netproto=", "gateway=", "stype=", "sport=", "ptype=", "payload="]) 97 | except getopt.GetoptError: 98 | print('\n***ERROR*** An invalid command line argument was entered.') 99 | usage() 100 | sys.exit(1) 101 | for opt,arg in opts: 102 | if opt in ("-h", "--help"): 103 | usage() 104 | sys.exit(0) 105 | elif opt in ("-v", "--verbose"): 106 | verboselvl = int(arg) 107 | elif opt in ("-t", "--targets"): 108 | hl = arg 109 | targets = hl.split(',') 110 | elif opt in ("-r", "--retry"): 111 | rt = int(arg) 112 | retry = -rt # Set to negative for how many times to retry when no more packets are answered 113 | elif opt in ("--timeout"): 114 | timeout = float(arg) 115 | elif opt in ("--netproto"): 116 | netprotocol = arg.upper() 117 | elif opt in ("--stype"): 118 | srcporttype = arg.title() 119 | elif opt in ("--sport"): 120 | srcport = int(arg) 121 | if ((srcport < 0) or (srcport >= 2**16)): 122 | srcport = 50000 # Set to a default value if out of range 123 | elif opt in ("-p", "--dports"): 124 | dp = arg 125 | dps = dp.split(',') 126 | dstports = [int(p) for p in dps] # Use a list comprehension to convert port value from string to integer 127 | elif opt in ("--ptype"): 128 | payloadtype = arg 129 | elif opt in ("--payload"): 130 | payload = arg 131 | elif opt in ("--minttl"): 132 | minttl = int(arg) 133 | if (minttl <= 0): 134 | minttl = 1 135 | elif opt in ("--maxttl"): 136 | maxttl = int(arg) 137 | if (maxttl <= 0): 138 | maxttl = 20 139 | elif opt in ("-g", "--graphic"): 140 | graphictype = arg 141 | elif opt in ("-s", "--showpadding"): 142 | showpadding = 1 143 | elif opt in ("--asnresolver"): 144 | asnresolver = arg 145 | elif opt in ("--privaddr"): 146 | privaddr = 1 147 | elif opt in ("-q", "--nquery"): 148 | nquery = int(arg) 149 | if (nquery < 1): 150 | nquery = 1 151 | elif opt in ("-f", "--dirfile"): 152 | dirfilename = arg 153 | elif opt in ("-i", "--interface"): 154 | nic = arg 155 | elif opt in ("--gateway"): 156 | gateway = arg 157 | elif opt in ("-w", "--wrpcap"): 158 | pcapdirfilename = arg 159 | elif opt in ("--dotfile"): 160 | dotfile = 1 161 | elif opt in ("--vspread"): 162 | vspread = float(arg) 163 | elif opt in ("--title"): 164 | title = arg 165 | elif opt in ("--ts"): 166 | timestamp = arg 167 | elif opt in ("--rtt"): 168 | rtt = 1 169 | elif opt in ("--l3rawsocket"): 170 | conf.L3socket = L3RawSocket 171 | # 172 | # Auto file name cration... 173 | if (dirfilename == ""): 174 | dirfilename = dirfilenamebase + graphictype 175 | # 176 | # Range check Min/Max TTl counts... 177 | if (minttl > maxttl): 178 | maxttl = minttl 179 | # 180 | # Validate the Network Protocol value... 181 | plist = ["TCP", "UDP", "ICMP"] 182 | if not netprotocol in plist: 183 | print('\n***ERROR*** Option: "--netproto" (Network Protocol) must be one of: "TCP", "UDP" or "ICMP".\n') 184 | usage() 185 | sys.exit(2) 186 | # 187 | # Validate the Source Port type... 188 | slist = ["Random", "Increment"] 189 | if not srcporttype in slist: 190 | print('\n***ERROR*** Option: "--stype" (Source Port Type) must be one of: "Random" or "Increment".\n') 191 | usage() 192 | sys.exit(2) 193 | # 194 | # Default to payload type to it's default network protocol value if not found in list... 195 | pllist = ["Disabled", "RandStr", "RandStrTerm", "ASCII", "ASCII-Hex"] 196 | if payloadtype is None or (not payloadtype in pllist): 197 | if (netprotocol == "ICMP"): 198 | payloadtype = "RandStr" # ICMP: A random string payload to fill out the minimum packet size 199 | elif (netprotocol == "UDP"): 200 | payloadtype = "RandStrTerm" # UDP: A random string terminated payload to fill out the minimum packet size 201 | elif (netprotocol == "TCP"): 202 | payloadtype = "Disabled" # TCP: Disabled -> The minimum packet size satisfied - no payload required 203 | # 204 | # Create byte object for the payload... 205 | if (payloadtype == 'ASCII'): 206 | payload = bytes(payload, 'utf-8') 207 | payloadtype = "Custom" # Set custom payload type for mtr 208 | elif (payloadtype == 'ASCII-Hex'): 209 | # 210 | # Convert ASCII-Hex to a byte object with 'binascii.unhexlify()': 211 | try: 212 | payload = binascii.unhexlify(payload) 213 | payloadtype = "Custom" 214 | except: 215 | print('\n***ERROR*** Option: ASCII-Hex Payload error: "Non-Hexadecimal" or "Odd-length" payload.\n', sys.exc_info()[0]) 216 | usage() 217 | sys.exit(2) 218 | else: 219 | payload = b'' # Set empty byte object for non-custom payloads 220 | # 221 | # Determine the default Gateway IPv4 Address... 222 | # 223 | if (gateway == ''): 224 | gws = netifaces.gateways() 225 | defgw = gws['default'][netifaces.AF_INET] 226 | if (len(defgw) > 0): 227 | gateway = defgw[0] # Set the default Gateway IPv4 Address 228 | # 229 | # Check ASN resolver value... 230 | # 231 | # Set the Global config value: conf.AS_resolver to the desired ASN resolver... 232 | if asnresolver in ("Disabled"): 233 | rasn = 0 # Disable ASN resolving... 234 | elif asnresolver in ("All"): 235 | conf.AS_resolver = AS_resolver_multi() # Use all AS resolvers... 236 | rasn = 1 237 | elif asnresolver in ("whois.cymru.com"): 238 | conf.AS_resolver = AS_resolver_cymru() 239 | rasn = 1 240 | elif asnresolver in ("riswhois.ripe.net"): 241 | conf.AS_resolver = AS_resolver_riswhois() 242 | rasn = 1 243 | elif asnresolver in ("whois.ra.net"): 244 | conf.AS_resolver = AS_resolver_radb() 245 | rasn = 1 246 | else: 247 | print('\n***ERROR*** Option (--asnresolver) must be one of: "Disabled", "All",') 248 | print(' "whois.cymru.com", "riswhois.ripe.net" or "whois.ra.net".\n') 249 | usage() 250 | sys.exit(2) 251 | # 252 | # Target Host list: A Manditory argument... 253 | if (len(targets) == 0): 254 | print('\n***ERROR*** A target host list (-t <Target Host List>) is required.') 255 | usage() 256 | sys.exit(2) 257 | 258 | if (verboselvl >= 1): 259 | sp = 'stype = "{t:s}", '.format(t = srcporttype) 260 | if (srcporttype != 'Random'): 261 | sp += 'srcport = {p:d}, '.format(p = srcport) 262 | if (nic == ''): 263 | print('\nmtrc = mtr({a1:s}, retry = {a2:d}, timeout = {a3:.2f}, netproto = "{a4:s}", {a5:s}dport = {a6:s}, minttl = {a7:d}, maxttl = {a8:d}, nquery = {a9:d}, privaddr = {a10:d}, rasn = {a11:d}, gw = "{a12:s}", ptype = "{a13:s}", payload = {a14:s}, verbose = {a15:d})'.format(a1 = str(targets), a2 = retry, a3 = timeout, a4 = netprotocol, a5 = sp, a6 = str(dstports), a7 = minttl, a8 = maxttl, a9 = nquery, a10 = privaddr, a11 = rasn, a12 = gateway, a13 = payloadtype, a14 = repr(payload), a15 = verboselvl)) 264 | else: 265 | print('\nmtrc = mtr({a1:s}, retry = {a2:d}, timeout = {a3:.2f}, netproto = "{a4:s}", {a5:s}dport = {a6:s}, minttl = {a7:d}, maxttl = {a8:d}, nquery = {a9:d}, privaddr = {a10:d}, rasn = {a11:d}, gw = "{a12:s}", ptype = "{a13:s}", payload = {a14:s}, iface = "{a15:s}", verbose = {a16:d})'.format(a1 = str(targets), a2 = retry, a3 = timeout, a4 = netprotocol, a5 = sp, a6 = str(dstports), a7 = minttl, a8 = maxttl, a9 = nquery, a10 = privaddr, a11 = rasn, a12 = gateway, a13 = payloadtype, a14 = repr(payload), a15 = nic, a16 = verboselvl)) 266 | # 267 | # Run scapy mtr... 268 | try: 269 | if (nic == ''): 270 | mtrc = mtr(targets, retry = retry, timeout = timeout, netproto = netprotocol, stype = srcporttype, srcport = srcport, dport = dstports, minttl = minttl, maxttl = maxttl, nquery = nquery, privaddr = privaddr, rasn = rasn, gw = gateway, ptype = payloadtype, payload = payload, verbose = verboselvl) 271 | else: 272 | mtrc = mtr(targets, retry = retry, timeout = timeout, netproto = netprotocol, stype = srcporttype, srcport = srcport, dport = dstports, minttl = minttl, maxttl = maxttl, nquery = nquery, privaddr = privaddr, rasn = rasn, gw = gateway, ptype = payloadtype, payload = payload, iface = nic, verbose = verboselvl) 273 | except: 274 | print('\n**ERROR*** The scapy mtr (Multi-Traceroute) function failed. Use the verbose output option to help debug.') 275 | usage() 276 | sys.exit(3) 277 | 278 | if (verboselvl >= 1): 279 | tp = 0 280 | for ans in mtrc._res: 281 | tp += len(ans) 282 | tp *= 2 283 | print('\nTrace Send/Receive Packet Summary (Total: {p:d} pkts):'.format(p = tp)) 284 | print('=======================================================') 285 | print(mtrc._res) 286 | utp = 0 287 | for uans in mtrc._ures: 288 | utp += len(uans) 289 | print('\nTrace Unresponse Packet Summary (Total: {p:d} pkts):'.format(p = utp)) 290 | print('=======================================================') 291 | print(mtrc._ures) 292 | 293 | # 294 | # Dump packet details at verbosity level 9... 295 | if (verboselvl >= 9): 296 | for t in range(0, mtrc._nquery): 297 | rlen = len(mtrc._res[t]) 298 | if (rlen > 0): 299 | print('\nTrace Send/Receive Packet Details:') 300 | print('=======================================================') 301 | for i in range(0, rlen): 302 | print('-------------------------------------------------------') 303 | print('Trace Sent: {x:d} - mtrc._res[{t:d}][{r:d}][0]:'.format(x = (i + 1), t = t, r = i)) 304 | print('-------------------------------------------------------') 305 | mtrc._res[t][i][0].show() 306 | print('-------------------------------------------------------') 307 | print('Trace Received: {x:d} - mtrc._res[{t:d}][{r:d}][1]:'.format(x = (i + 1), t = t, r = i)) 308 | print('-------------------------------------------------------') 309 | mtrc._res[t][i][1].show() 310 | ulen = len(mtrc._ures[t]) 311 | if (ulen > 0): 312 | print('\nTrace Unresponse Packet Details:') 313 | print('=======================================================') 314 | for i in range(0, ulen): 315 | print('-------------------------------------------------------') 316 | print('Trace Sent: {x:d} - mtrc._ures[{t:d}][{u:d}]:'.format(x = (i + 1), t = t, u = i)) 317 | print('-------------------------------------------------------') 318 | mtrc._ures[t][i].show() 319 | 320 | # 321 | # Create SVG Graphic... 322 | try: 323 | if (verboselvl >= 1): 324 | print('\nNow generating the resulting scapy mtr {gt:s} graphic: "{df:s}"'.format(gt = graphictype.upper(), df = dirfilename)) 325 | print('\nmtrc.graph(format = "{gt:s}", target = "{tr:s}", padding = {pd:d}, vspread = {vs:.2f}, title = "{ti:s}", timestamp = "{ts:s}", rtt = {rtt:d})'.format(gt = graphictype, tr = dirfilename, pd = showpadding, vs = vspread, ti = title, ts = timestamp, rtt = rtt)) 326 | mtrc.graph(format = graphictype, target = dirfilename, padding = showpadding, vspread = vspread, title = title, timestamp = timestamp, rtt = rtt) 327 | except: 328 | print('\n**ERROR*** scapy mtr failed to produce a {gt:s} graphic.'.format(gt = graphictype.upper())) 329 | usage() 330 | sys.exit(4) 331 | # 332 | # Check to dump the DOT Digraph to file... 333 | try: 334 | if dotfile: 335 | basedirfilename, basefilenameext = os.path.splitext(dirfilename) 336 | dotdirfile = basedirfilename + '.dot' 337 | if (verboselvl >= 1): 338 | print('\nNow dumping the scapy mtr DOT Digraph (mtrc._graphdef) to file: "{df:s}"'.format(df = dotdirfile)) 339 | dg = open(dotdirfile, 'w') 340 | dg.write(mtrc._graphdef) 341 | dg.close() 342 | except: 343 | print('\n**ERROR*** scapy mtr failed to dump DOT file: "{df:s}"'.format(df = dotfile)) 344 | sys.exit(5) 345 | # 346 | # Check to dump traces to a pcap file... 347 | try: 348 | if (pcapdirfilename != ''): 349 | # 350 | # Get directory and file name components... 351 | basedirname = os.path.dirname(pcapdirfilename) 352 | nstcapturefile = basedirname + nstpcapfilename 353 | nstcapturelog = basedirname + nstpcaplogname 354 | basedirfilename, basefilenameext = os.path.splitext(pcapdirfilename) 355 | pcaplog = basedirfilename + '.plog' 356 | # 357 | # Remove any previous capture file, log and links... 358 | rmfiles = [nstcapturefile, nstcapturelog, pcapdirfilename, pcaplog] 359 | for f in rmfiles: 360 | try: 361 | os.unlink(f) 362 | except OSError: 363 | pass 364 | # 365 | # Dump a scapy create pcap file... 366 | if (verboselvl >= 1): 367 | print('\nNow dumping and time sorting the traces to pcap file: "{pc:s}"'.format(pc = pcapdirfilename)) 368 | # 369 | # To debug with pudb (RPM: python3-pudb) insert: import pudb; pudb.set_trace() 370 | spkts = [] # Sent packets 371 | rpkts = [] # Receive packets 372 | upkts = [] # Unanswered packets 373 | pkts = [] # All packets 374 | for ans in mtrc._res: # Dump all completed send/received packets 375 | spkts += [s[0] for s in ans] 376 | rpkts += [r[1] for r in ans] 377 | for sp in spkts: 378 | if hasattr(sp, "sent_time") and isinstance(sp.sent_time, float): 379 | sp.time = sp.sent_time # Get packet sent time (i.e., A sent answered packet) 380 | pkts = spkts + rpkts 381 | for uans in mtrc._ures: # Dump all unanswered packets 382 | upkts += [u[0] for u in uans] 383 | for up in upkts: 384 | if hasattr(up, "sent_time") and isinstance(up.sent_time, float): 385 | up.time = up.sent_time # Get packet sent time (i.e., A sent unanswered packet) 386 | pkts += upkts 387 | wrpcap(tmppcapfile, pkts) # Dump and create pcap file 388 | # 389 | # Reorder packets by their timestamp... 390 | vf = "&> /dev/null" 391 | if (verboselvl >= 8): 392 | vf = "" 393 | print("Time sorting pcap file: \"{rc:s}\"".format(rc = pcapdirfilename)) 394 | results = subprocess.check_output("/usr/sbin/reordercap \"{oc:s}\" \"{rc:s}\" {vf:s};".format(oc = tmppcapfile, rc = pcapdirfilename, vf = vf), stderr=subprocess.STDOUT, shell=True) 395 | print("{r:s}".format(r = results.decode("utf-8"))) 396 | if (verboselvl >= 8): 397 | vf = "-v" 398 | print("Removing temp pcap file: \"{oc:s}\"".format(oc = tmppcapfile)) 399 | results = subprocess.check_output("/usr/bin/rm -f {vf:s} \"{oc:s}\";".format(vf = vf, oc = tmppcapfile), stderr=subprocess.STDOUT, shell=True) 400 | print("{r:s}".format(r = results.decode("utf-8"))) 401 | # 402 | # Produce a pcap log file for Single-Tap Packet Capture... 403 | # 404 | # Get the Default IPv4 Address for Host... 405 | defaddr = subprocess.check_output("/usr/bin/getipaddr --default-address", shell=True) 406 | defaddr = defaddr.decode("utf-8") 407 | defaddr = defaddr.rstrip("\n") 408 | # 409 | # Get the default Interface if not provided... 410 | if (nic == ''): 411 | nic = subprocess.check_output("/usr/bin/getipaddr --default-netint", shell=True) 412 | nic = nic.decode("utf-8") 413 | nic = nic.rstrip("\n") 414 | pl = open(pcaplog, 'wt', encoding='utf-8') 415 | log = 'CAPHOST={ch:s}\n'.format(ch = defaddr) 416 | log += 'NETINT={ni:s}\n'.format(ni = nic) 417 | log += 'CAPAPPVER=mtraceroute (scapy - {v:s})\n'.format(v = conf.version) 418 | log += 'CAPTURETERMINATION=MTR Terminated\n' 419 | log += 'Note: Generated from a Scapy Multi-Traceroute (MTR) session.\n' 420 | pl.write(log) 421 | pl.close() 422 | # 423 | # Create links files for NST Single-Tap capture access... 424 | pcapbasename = os.path.basename(pcapdirfilename) 425 | logbasename = os.path.basename(pcaplog) 426 | # 427 | # s l s l 428 | links = {pcapbasename: nstcapturefile, logbasename: nstcapturelog} 429 | for s,l in links.items(): 430 | try: 431 | os.symlink(s, l) 432 | if (verboselvl >= 8): 433 | print("Creating symbolic link: {l:s} -> {s:s}".format(l = l, s = s)) 434 | except: 435 | print('\n**ERROR*** Cannot create symbolic link: {l:s} -> {s:s}'.format(l = l, s = s)) 436 | except: 437 | print('\n**ERROR*** scapy mtr failed to dump traces to pcap file: "{pc:s}"'.format(pc = pcapdirfilename)) 438 | sys.exit(7) 439 | # 440 | # Clean exit... 441 | sys.exit(0) 442 | # 443 | # Run this script by the interpreter if not being imported... 444 | if __name__ == "__main__": 445 | main(sys.argv[1:]) 446 | -------------------------------------------------------------------------------- /example_runs/scapy-mtr_2020-02-18_16-56-30.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" 3 | "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 4 | <!-- Generated by graphviz version 2.40.1 (0) 5 | --> 6 | <!-- Title: mtr Pages: 1 --> 7 | <svg width="324pt" height="1273pt" 8 | viewBox="0.00 0.00 324.00 1272.93" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 9 | <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1268.9291)"> 10 | <title>mtr 11 | 12 | cluster_15169 13 | 14 | 15 | AS: 15169 16 | [Google] 17 | 18 | 19 | 20 | 21 | cluster_172_217_10_228 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Target: www.google.com 31 | Hop Range (T1: 1 → 9) 32 | 33 | 34 | 35 | 36 | cluster_701 37 | 38 | 39 | AS: 701 40 | [UUNET, US] 41 | 42 | 43 | 44 | 45 | cluster_probe_Title 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | Multi-Traceroute (MTR) Probe 55 | 2020-02-18 16:56:30 56 | Target: www.google.com (172.217.10.228 → T1) 57 | 58 | 59 | 60 | 61 | cluster_default_gateway 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Default Gateway 71 | 72 | 73 | 74 | 75 | 76 | 72.14.208.130 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 72.14.208.130 86 | 87 | 88 | 89 | 90 | 91 | 108.170.225.6 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 108.170.225.6 101 | 102 | 103 | 104 | 105 | 106 | 72.14.208.130->108.170.225.6 107 | 108 | 109 | 110 | 111 | 112 | 113 |   12.572ms 114 | 115 | 116 | 117 | 118 | 119 | 172.253.70.15 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 172.253.70.15 129 | 130 | 131 | 132 | 133 | 134 | 108.170.225.6->172.253.70.15 135 | 136 | 137 | 138 | 139 | 140 | 141 |   12.024ms 142 | 143 | 144 | 145 | 146 | 147 | 172.217.10.228 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | Resolved Target 157 | 172.217.10.228 158 | 159 | T1 160 | 161 | https(443) SA 162 | 163 | 164 | 165 | 166 | 167 | 172.253.70.15->172.217.10.228:n 168 | 169 | 170 | 171 | 172 | 173 | 174 |   12.779ms 175 | 176 | 177 | 178 | 179 | 180 | 100.41.140.154 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 100.41.140.154 190 | 191 | 192 | 193 | 194 | 195 | Unk1 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | Unk1 205 | 206 | 207 | 208 | 209 | 210 | 100.41.140.154->Unk1 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 10.222.222.10 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | Probe: 10.222.222.10 229 | Network Interface: lan0 230 | 231 | TCP: https(443) 232 | 233 | T1 234 | 235 | 236 | 237 | 238 | 239 | 10.222.222.1 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 10.222.222.1 249 | 250 | 251 | 252 | 253 | 254 | 10.222.222.10:s->10.222.222.1 255 | 256 | 257 | 258 | 259 | 260 | 261 |   0.360ms 262 | 263 | 264 | 265 | 266 | 267 | Unk0 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | Unk0 277 | 278 | 279 | 280 | 281 | 282 | 10.222.222.1->Unk0 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | Unk0->100.41.140.154 292 | 293 | 294 | 295 | 296 | 297 | 298 |   1.045ms 299 | 300 | 301 | 302 | 303 | 304 | 140.222.1.43 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 140.222.1.43 314 | 315 | 316 | 317 | 318 | 319 | Unk1->140.222.1.43 320 | 321 | 322 | 323 | 324 | 325 | 326 |   7.083ms 327 | 328 | 329 | 330 | 331 | 332 | 140.222.1.43->72.14.208.130 333 | 334 | 335 | 336 | 337 | 338 | 339 |   11.602ms 340 | 341 | 342 | 343 | 344 | 345 | -------------------------------------------------------------------------------- /mtraceroute/__init__.py: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | # Copyright (c) 2020, 2023 networksecuritytoolkit.org (NST). 3 | # All rights reserved. 4 | #----------------------------------------------------------------------------- 5 | """A python3 library for performing an enhanced scapy Multi-Traceroute (MTR)""" 6 | """with resulting SVG visual. """ 7 | 8 | ########################## 9 | # Required Imports # 10 | ########################## 11 | import ipaddress 12 | import re 13 | from scapy.all import * 14 | import time 15 | 16 | 17 | ########################## 18 | # Classes From Kamene # 19 | ########################## 20 | class RandStringTerm(RandString): 21 | def __init__(self, size, term = b''): 22 | RandString.__init__(self, size) 23 | self.term = term 24 | def _fix(self): 25 | return RandString._fix(self) + self.term 26 | 27 | 28 | ########################## 29 | # Utilities From Kamene # 30 | ########################## 31 | @conf.commands.register 32 | def is_private_addr(x): 33 | """Returns True if the IPv4 Address is an RFC 1918 private address.""" 34 | paddrs = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'] 35 | found = False 36 | for ipr in paddrs: 37 | try: 38 | if ipaddress.ip_address(x) in ipaddress.ip_network(ipr): 39 | found = True 40 | continue 41 | except: 42 | break 43 | return found 44 | 45 | 46 | ########################## 47 | # Multi-Traceroute Class # 48 | ########################## 49 | class MTR: 50 | # 51 | # Initialize Multi-Traceroute Object Vars... 52 | def __init__(self, nquery=1, target=''): 53 | self._nquery = nquery # Number or traceroute queries 54 | self._ntraces = 1 # Number of trace runs 55 | self._iface = '' # Interface to use for trace 56 | self._gw = '' # Default Gateway IPv4 Address for trace 57 | self._netprotocol = 'TCP' # MTR network protocol to use for trace 58 | self._target = target # Session targets 59 | self._exptrg = [] # Expanded Session targets 60 | self._host2ip = {} # Target Host Name to IP Address 61 | self._ip2host = {} # Target IP Address to Host Name 62 | self._tcnt = 0 # Total Trace count 63 | self._tlblid = [] # Target Trace label IDs 64 | self._res = [] # Trace Send/Receive Response Packets 65 | self._ures = [] # Trace UnResponse Sent Packets 66 | self._ips = {} # Trace Unique IPv4 Addresses 67 | self._hops = {} # Traceroute Hop Ranges 68 | self._rt = [] # Individual Route Trace Summaries 69 | self._ports = {} # Completed Targets & Ports 70 | self._portsdone = {} # Completed Traceroutes & Ports 71 | self._rtt = {} # Round Trip Times (msecs) for Trace Nodes 72 | self._unknownlabel = incremental_label('"Unk%i"') 73 | self._asres = conf.AS_resolver # Initial ASN Resolver 74 | self._asns = {} # Found AS Numbers for the MTR session 75 | self._asds = {} # Associated AS Number descriptions 76 | self._unks = {} # Unknown Hops ASN IP boundaries 77 | self._graphdef = None 78 | self._graphasres = 0 79 | self._graphpadding = 0 80 | # 81 | # Get the protocol name from protocol integer value. 82 | # 83 | # proto - Protocol integer value. 84 | # 85 | # Returns a string value representing the given integer protocol. 86 | def get_proto_name(self, proto): 87 | ps = str(proto) 88 | if ps == '6': 89 | pt = 'tcp' 90 | elif ps == '17': 91 | pt = 'udp' 92 | elif ps == '1': 93 | pt = 'icmp' 94 | else: 95 | pt = str(proto) 96 | return pt 97 | 98 | # 99 | # Compute Black Holes... 100 | def get_black_holes(self): 101 | for t in range(0, self._ntraces): 102 | for rtk in self._rt[t]: 103 | trace = self._rt[t][rtk] 104 | k = trace.keys() 105 | for n in range(min(k), max(k)): 106 | if not n in trace: # Fill in 'Unknown' hops 107 | trace[n] = next(self._unknownlabel) 108 | if not rtk in self._portsdone: 109 | if rtk[2] == 1: # ICMP 110 | bh = "%s %i/icmp" % (rtk[1], rtk[3]) 111 | elif rtk[2] == 6: # TCP 112 | bh = "{ip:s} {dp:d}/tcp".format(ip=rtk[1], dp=rtk[3]) 113 | elif rtk[2] == 17: # UDP 114 | bh = '%s %i/udp' % (rtk[1], rtk[3]) 115 | else: 116 | bh = '%s %i/proto' % (rtk[1], rtk[2]) 117 | self._ips[rtk[1]] = None # Add the Blackhole IP to list of unique IP Addresses 118 | # 119 | # Update trace with Blackhole info... 120 | bh = '"{bh:s}"'.format(bh=bh) 121 | trace[max(k) + 1] = bh 122 | # 123 | # Detection for Blackhole - Failed target not set as last Hop in trace... 124 | for t in range(0, self._ntraces): 125 | for rtk in self._rt[t]: 126 | trace = self._rt[t][rtk] 127 | k = trace.keys() 128 | if (' ' not in trace[max(k)]) and (':' not in trace[max(k)]): 129 | if rtk[2] == 1: # ICMP 130 | bh = "%s %i/icmp" % (rtk[1], rtk[3]) 131 | elif rtk[2] == 6: # TCP 132 | bh = "{ip:s} {dp:d}/tcp".format(ip=rtk[1], dp=rtk[3]) 133 | elif rtk[2] == 17: # UDP 134 | bh = '%s %i/udp' % (rtk[1], rtk[3]) 135 | else: 136 | bh = '%s %i/proto' % (rtk[1], rtk[2]) 137 | self._ips[rtk[1]] = None # Add the Blackhole IP to list of unique IP Addresses 138 | # 139 | # Update trace with Blackhole info... 140 | bh = '"{bh:s}"'.format(bh=bh) 141 | trace[max(k) + 1] = bh 142 | 143 | # 144 | # Compute the Hop range for each trace... 145 | def compute_hop_ranges(self): 146 | n = 1 147 | for t in range(0, self._ntraces): 148 | for rtk in self._rt[t]: 149 | trace = self._rt[t][rtk] 150 | k = trace.keys() 151 | # 152 | # Detect Blackhole Endpoints... 153 | h = rtk[1] 154 | mt = max(k) 155 | if not ':' in trace[max(k)]: 156 | h = trace[max(k)].replace('"', '') # Add a Blackhole Endpoint (':' Char does not exist) 157 | if max(k) == 1: 158 | # 159 | # Special case: Max TTL set to 1... 160 | mt = 1 161 | else: 162 | mt = max(k) - 1 # Blackhole - remove Hop for Blackhole -> Host never reached 163 | hoplist = self._hops.get(h, []) # Get previous hop value 164 | hoplist.append([n, min(k), mt]) # Append trace hop range for this trace 165 | self._hops[h] = hoplist # Update mtr Hop value 166 | n += 1 167 | 168 | # 169 | # Get AS Numbers... 170 | def get_asns(self, privaddr=0): 171 | """Obtain associated AS Numbers for IPv4 Addreses. 172 | privaddr: 0 - Normal display of AS numbers, 173 | 1 - Do not show an associated AS Number bound box (cluster) on graph for a private IPv4 Address.""" 174 | ips = {} 175 | if privaddr: 176 | for k, v in self._ips.items(): 177 | if not is_private_addr(k): 178 | ips[k] = v 179 | else: 180 | ips = self._ips 181 | # 182 | # Special case for the loopback IP Address: 127.0.0.1 - Do not ASN resolve... 183 | if '127.0.0.1' in ips: 184 | del ips['127.0.0.1'] 185 | # 186 | # ASN Lookup... 187 | asnquerylist = dict.fromkeys(map(lambda x: x.rsplit(" ", 1)[0], ips)).keys() 188 | if self._asres is None: 189 | asnlist = [] 190 | else: 191 | try: 192 | asnlist = self._asres.resolve(*asnquerylist) 193 | except: 194 | pass 195 | for ip, asn, desc, in asnlist: 196 | if asn is None: 197 | continue 198 | # 199 | # If ASN is a string Convert to a number: (i.e., 'AS3257' => 3257) 200 | if type(asn) == str: 201 | asn = asn.upper() 202 | nasn = asn.replace('AS', '') 203 | else: 204 | nasn = asn 205 | try: # Make sure a number 206 | nasn = int(nasn) 207 | except: 208 | continue 209 | iplist = self._asns.get(nasn, []) # Get previous ASN value 210 | iplist.append(ip) # Append IP Address to previous ASN 211 | self._asns[nasn] = iplist # Store updated IP list 212 | self._asds[nasn] = desc # Append AS description 213 | 214 | # 215 | # Get the ASN for a given IP Address. 216 | # 217 | # ip - IP Address to get the ASN for. 218 | # 219 | # Return the ASN for a given IP Address if found. 220 | # A -1 is returned if not found. 221 | def get_asn_ip(self, ip): 222 | for a in self._asns: 223 | for i in self._asns[a]: 224 | if ip == i: 225 | return a 226 | return -1 227 | 228 | # 229 | # Guess Traceroute 'Unknown (Unkn) Hops' ASNs. 230 | # 231 | # Technique: Method to guess ASNs for Traceroute 'Unknown Hops'. 232 | # If the assign ASN for the known Ancestor IP is the 233 | # same as the known Descendant IP then use this ASN 234 | # for the 'Unknown Hop'. 235 | # Special case guess: If the Descendant IP is a 236 | # Endpoint Host Target the assign it to its 237 | # associated ASN. 238 | def guess_unk_asns(self): 239 | t = 1 240 | for q in range(0, self._ntraces): 241 | for rtk in self._rt[q]: 242 | trace = self._rt[q][rtk] 243 | tk = trace.keys() 244 | begip = endip = '' 245 | unklist = [] 246 | for n in range(min(tk), (max(tk) + 1)): 247 | if trace[n].find('Unk') == -1: 248 | # 249 | # IP Address Hop found... 250 | if len(unklist) == 0: 251 | # 252 | # No 'Unknown Hop' found yet... 253 | begip = trace[n] 254 | else: 255 | # 256 | # At least one Unknown Hop found - Store IP boundary... 257 | endip = trace[n] 258 | for u in unklist: 259 | idx = begip.find(':') 260 | if idx != -1: # Remove Endpoint Trace port info: '"162.144.22.85":T443' 261 | begip = begip[:idx] 262 | idx = endip.find(':') 263 | if idx != -1: 264 | endip = endip[:idx] 265 | # 266 | # u[0] - Unknown Hop name... 267 | # u[1] - Hop number... 268 | self._unks[u[0]] = [begip, endip, '{t:d}:{h:d}'.format(t=t, h=u[1])] 269 | # 270 | # Init var for new Unknown Hop search... 271 | begip = endip = '' 272 | unklist = [] 273 | else: 274 | # 275 | # 'Unknown Hop' found... 276 | unklist.append([trace[n], n]) 277 | t += 1 # Inc next trace count 278 | # 279 | # Assign 'Unknown Hop' ASN... 280 | for u in self._unks: 281 | bip = self._unks[u][0] 282 | bip = bip.replace('"', '') # Begin IP - Strip off surrounding double quotes (") 283 | basn = self.get_asn_ip(bip) 284 | if basn == -1: 285 | continue 286 | eip = self._unks[u][1] 287 | eip = eip.replace('"', '') 288 | easn = self.get_asn_ip(eip) 289 | if easn == -1: 290 | continue 291 | # 292 | # Append the 'Unknown Hop' to an ASN if 293 | # Ancestor/Descendant IP ASN match... 294 | if basn == easn: 295 | self._asns[basn].append(u.replace('"', '')) 296 | else: 297 | # 298 | # Special case guess: If the Descendant IP is 299 | # a Endpoint Host Target the assign it to its 300 | # associated ASN. 301 | for d in self._tlblid: 302 | if eip in d: 303 | self._asns[easn].append(u.replace('"', '')) 304 | break 305 | # 306 | # Make the DOT graph... 307 | def make_dot_graph(self, ASres=None, padding=0, vspread=0.75, title="Multi-Traceroute (MTR) Probe", timestamp="", rtt=1): 308 | import datetime 309 | import html 310 | if ASres is None: 311 | self._asres = conf.AS_resolver 312 | self._graphasres = ASres 313 | self._graphpadding = padding 314 | # 315 | # ASN box color generator... 316 | backcolorlist = colgen("60", "86", "ba", "ff") 317 | # 318 | # Edge (trace arrows) color generator... 319 | forecolorlist = colgen("a0", "70", "40", "20") 320 | # 321 | # Begin the DOT Digraph... 322 | s = "### Scapy Multi-Traceroute (MTR) DOT Graph Results ({t:s}) ###\n".format(t=datetime.datetime.now().isoformat(' ')) 323 | s += "\ndigraph mtr {\n" 324 | # 325 | # Define the default graph attributes... 326 | s += '\tgraph [bgcolor=transparent,ranksep={vs:.2f}];\n'.format(vs=vspread) 327 | # 328 | # Define the default node shape and drawing color... 329 | s += '\tnode [shape="ellipse",fontname="Sans-Serif",fontsize=11,color="black",gradientangle=270,fillcolor="white:#a0a0a0",style="filled"];\n' 330 | # 331 | # Combine Trace Probe Begin Points... 332 | # 333 | # k0 k1 k2 v0 v1 k0 k1 k2 v0 v1 334 | # Ex: bp = {('192.168.43.48',5555,''): ['T1','T3'], ('192.168.43.48',443,'https'): ['T2','T4']} 335 | bp = {} # ep -> A single services label for a given IP 336 | for d in self._tlblid: # k v0 v1 v2 v3 v4 v5 v6 v7 337 | for k, v in d.items(): # Ex: k: '162.144.22.87' v: ('T1', '192.168.43.48', '162.144.22.87', 6, 443, 'https', 'SA', '') 338 | p = bp.get((v[1], v[4], v[5])) 339 | if p == None: 340 | bp[(v[1], v[4], v[5])] = [v[0]] # Add new (TCP Flags / ICMP / Proto) and initial trace ID 341 | else: 342 | bp[(v[1], v[4], v[5])].append(v[0]) # Append additional trace IDs 343 | # 344 | # Combine Begin Point services... 345 | # k sv0 sv1 sv0 sv1 346 | # Ex bpip = {'192.168.43.48': [('T2|T4', 'https(443)'), ('T1|T3', '5555')]} 347 | bpip = {} # epip -> Combined Endpoint services label for a given IP 348 | for k, v in bp.items(): 349 | tr = '' 350 | for t in range(0, len(v)): 351 | if tr == '': 352 | tr += '{ts:s}'.format(ts=v[t]) 353 | else: 354 | tr += '|{ts:s}'.format(ts=v[t]) 355 | p = k[2] 356 | if p == '': # Use port number not name if resolved 357 | p = str(k[1]) 358 | else: 359 | p += '(' + str(k[1]) + ')' # Use both name and port 360 | if k[0] in bpip: 361 | bpip[k[0]].append((tr, p)) 362 | else: 363 | bpip[k[0]] = [(tr, p)] 364 | # 365 | # Create Endpoint Target Clusters... 366 | epc = {} # Endpoint Target Cluster Dictionary 367 | epip = [] # Endpoint IPs array 368 | oip = [] # Only Endpoint IP array 369 | epprb = [] # Endpoint Target and Probe the same IP array 370 | for d in self._tlblid: # Spin thru Target IDs 371 | for k, v in d.items(): # Get access to Target Endpoints 372 | h = k 373 | if v[6] == 'BH': # Add a Blackhole Endpoint Target 374 | h = '{bh:s} {bhp:d}/{bht:s}'.format(bh=k, bhp=v[4], bht=v[3]) 375 | elif v[1] == v[2]: # When the Target and host running the mtr session are 376 | epprb.append(k) # the same then append IP to list target and probe the same array 377 | epip.append(h) 378 | oip.append(k) 379 | # 380 | # Create unique arrays... 381 | uepip = set(epip) # Get a unique set of Endpoint IPs 382 | uepipo = set(oip) # Get a unique set of Only Endpoint IPs 383 | uepprb = set(epprb) # Get a unique set of Only IPs: Endpoint Target and Probe that are the same 384 | # 385 | # Now create unique endpoint target clusters.... 386 | for ep in uepip: 387 | # 388 | # Get Host only string... 389 | eph = ep 390 | f = ep.find(' ') 391 | if f >= 0: 392 | eph = ep[0:f] 393 | # 394 | # Build Traceroute Hop Range label... 395 | if ep in self._hops: # Is Endpoint IP in the Hops dictionary 396 | hr = self._hops[ep] 397 | elif eph in self._hops: # Is Host only endpoint in the Hops dictionary 398 | hr = self._hops[eph] 399 | else: 400 | continue # Not found in the Hops dictionary 401 | 402 | l = len(hr) 403 | if l == 1: 404 | hrs = "Hop Range (" 405 | else: 406 | hrs = "Hop Ranges (" 407 | c = 0 408 | for r in hr: 409 | hrs += 'T{s1:d}: {s2:d} → {s3:d}'.format(s1=r[0], s2=r[1], s3=r[2]) 410 | c += 1 411 | if c < l: 412 | hrs += ', ' 413 | hrs += ')' 414 | ecs = "\t\t### MTR Target Cluster ###\n" 415 | uep = ep.replace('.', '_') 416 | uep = uep.replace(' ', '_') 417 | uep = uep.replace('/', '_') 418 | gwl = '' 419 | if self._gw == eph: 420 | gwl = ' (Default Gateway)' 421 | ecs += '\t\tsubgraph cluster_{ep:s} {{\n'.format(ep=uep) 422 | ecs += '\t\t\ttooltip="MTR Target: {trg:s}{gwl:s}";\n'.format(trg=self._ip2host[eph], gwl=gwl) 423 | ecs += '\t\t\tcolor="darkgreen";\n' 424 | ecs += '\t\t\tfontsize=11;\n' 425 | ecs += '\t\t\tfontname="Sans-Serif";\n' 426 | ecs += '\t\t\tgradientangle=270;\n' 427 | ecs += '\t\t\tfillcolor="white:#a0a0a0";\n' 428 | ecs += '\t\t\tstyle="filled,rounded";\n' 429 | ecs += '\t\t\tpenwidth=2;\n' 430 | ecs += '\t\t\tlabel=<
Target: {h:s}{gwl:s}
{hr:s}
>;\n'.format(h=html.escape(self._ip2host[eph]), gwl=html.escape(gwl), hr=hrs) 431 | ecs += '\t\t\tlabelloc="b";\n' 432 | pre = '' 433 | if ep in uepprb: # Special Case: Separate Endpoint Target from Probe 434 | pre = '_' # when they are the same -> Prepend an underscore char: '_' 435 | ecs += '\t\t\t"{pre:s}{ep:s}";\n'.format(pre=pre, ep=ep) 436 | ecs += "\t\t}\n" 437 | # 438 | # Store Endpoint Cluster... 439 | epc[ep] = ecs 440 | # 441 | # Create ASN Clusters (i.e. DOT subgraph and nodes) 442 | s += "\n\t### ASN Clusters ###\n" 443 | cipall = [] # Array of IPs consumed by all ASN Cluster 444 | cepipall = [] # Array of IP Endpoints (Targets) consumed by all ASN Cluster 445 | for asn in self._asns: 446 | cipcur = [] 447 | s += '\tsubgraph cluster_{asn:d} {{\n'.format(asn=asn) 448 | s += '\t\ttooltip="AS: {asn:d} - [{asnd:s}]";\n'.format(asn=asn, asnd=self._asds[asn]) 449 | col = next(backcolorlist) 450 | s += '\t\tcolor="#{s0:s}{s1:s}{s2:s}";\n'.format(s0=col[0], s1=col[1], s2=col[2]) 451 | # 452 | # Fill in ASN Cluster the associated generated color using an 11.7% alpha channel value (30/256)... 453 | s += '\t\tfillcolor="#{s0:s}{s1:s}{s2:s}30";\n'.format(s0=col[0], s1=col[1], s2=col[2]) 454 | s += '\t\tstyle="filled,rounded";\n' 455 | s += '\t\tnode [color="#{s0:s}{s1:s}{s2:s}",gradientangle=270,fillcolor="white:#{s0:s}{s1:s}{s2:s}",style="filled"];\n'.format(s0=col[0], s1=col[1], s2=col[2]) 456 | s += '\t\tfontsize=10;\n' 457 | s += '\t\tfontname="Sans-Serif";\n' 458 | s += '\t\tlabel=<
AS: {asn:d}
[{des:s}]
>;\n'.format(asn=asn, des=html.escape(self._asds[asn])) 459 | s += '\t\tlabelloc="t";\n' 460 | s += '\t\tpenwidth=3;\n' 461 | for ip in self._asns[asn]: 462 | # 463 | # Only add IP if not an Endpoint Target... 464 | if not ip in uepipo: 465 | # 466 | # Spin thru all traces and only Add IP if not an ICMP Destination Unreachable node... 467 | for tr in range(0, self._ntraces): 468 | for rtk in self._rt[tr]: 469 | trace = self._rt[tr][rtk] 470 | k = trace.keys() 471 | for n in range(min(k), (max(k) + 1)): 472 | # 473 | # Check for not already added... 474 | if not ip in cipall: 475 | # 476 | # Add IP Hop - found in trace and not an ICMP Destination Unreachable node... 477 | if '"{ip:s}"'.format(ip=ip) == trace[n]: 478 | s += '\t\t"{ip:s}" [tooltip="Hop Host: {ip:s}"];\n'.format(ip=ip) 479 | cipall.append(ip) 480 | # 481 | # Special check for ICMP Destination Unreachable nodes... 482 | if ip in self._ports: 483 | for p in self._ports[ip]: 484 | if p.find('ICMP dest-unreach') >= 0: 485 | # 486 | # Check for not already added... 487 | uip = '{uip:s} 3/icmp'.format(uip=ip) 488 | if uip not in cipall: 489 | s += '\t\t"{uip:s}";\n'.format(uip=uip) 490 | cipall.append(uip) 491 | else: 492 | cipcur.append(ip) # Current list of Endpoints consumed by this ASN Cluster 493 | cepipall.append(ip) # Accumulated list of Endpoints consumed by all ASN Clusters 494 | # 495 | # Add Endpoint Cluster(s) if part of this ASN Cluster (Nested Clusters)... 496 | if len(cipcur) > 0: 497 | for ip in cipcur: 498 | for e in epc: # Loop thru each Endpoint Target Clusters 499 | h = e 500 | f = e.find(' ') # Strip off 'port/proto' 501 | if f >= 0: 502 | h = e[0:f] 503 | if h == ip: 504 | s += epc[e] 505 | s += "\t}\n" 506 | # 507 | # Add any Endpoint Target Clusters not consumed by an ASN Cluster (Stand-alone Cluster) 508 | # and not the same as the host running the mtr session... 509 | for ip in epc: 510 | h = ip 511 | f = h.find(' ') # Strip off 'port/proto' 512 | if f >= 0: 513 | h = ip[0:f] 514 | if not h in cepipall: 515 | for k, v in bpip.items(): # Check for target = host running the mtr session - Try to Add 516 | if k != h: # this Endpoint target to the Probe Target Cluster below. 517 | s += epc[ip] # Finally add the Endpoint Cluster if Stand-alone and 518 | # not running the mtr session. 519 | # 520 | # Probe Target Cluster... 521 | s += "\n\t### Probe Target Cluster ###\n" 522 | s += '\tsubgraph cluster_probe_Title {\n' 523 | p = '' 524 | for k, v in bpip.items(): 525 | p += ' {ip:s}'.format(ip=k) 526 | s += '\t\ttooltip="Multi-Traceroute (MTR) Probe: {ip:s}";\n'.format(ip=p) 527 | s += '\t\tcolor="darkorange";\n' 528 | s += '\t\tgradientangle=270;\n' 529 | s += '\t\tfillcolor="white:#a0a0a0";\n' 530 | s += '\t\tstyle="filled,rounded";\n' 531 | s += '\t\tpenwidth=3;\n' 532 | s += '\t\tfontsize=11;\n' 533 | s += '\t\tfontname="Sans-Serif";\n' 534 | # 535 | # Format Label including trace targets... 536 | tstr = '' 537 | for t in self._target: 538 | tstr += 'Target: {t:s} ('.format(t=t) 539 | # 540 | # Append resolve IP Addresses... 541 | l = len(self._host2ip[t]) 542 | c = 0 543 | for ip in self._host2ip[t]: 544 | tstr += '{ip:s} → '.format(ip=ip) 545 | # 546 | # Append all associated Target IDs... 547 | ti = [] 548 | for d in self._tlblid: # Spin thru Target IDs 549 | for k, v in d.items(): # Get access to Target ID (v[0]) 550 | if k == ip: 551 | ti.append(v[0]) 552 | lt = len(ti) 553 | ct = 0 554 | for i in ti: 555 | tstr += '{i:s}'.format(i=i) 556 | ct += 1 557 | if ct < lt: 558 | tstr += ', ' 559 | c += 1 560 | if c < l: 561 | tstr += ', ' 562 | tstr += ')' 563 | s += '\t\tlabel=<'.format(s0=html.escape(title)) 564 | if timestamp != "": 565 | s += ''.format(s0=timestamp) 566 | s += '{s0:s}
{s0:s}
{s0:s}
>;\n'.format(s0=tstr) 567 | s += '\t\tlabelloc="t";\n' 568 | for k, v in bpip.items(): 569 | s += '\t\t"{ip:s}";\n'.format(ip=k) 570 | # 571 | # Add in any Endpoint target that is the same as the host running the mtr session... 572 | for ip in epc: 573 | h = ip 574 | f = h.find(' ') # Strip off 'port/proto' 575 | if f >= 0: 576 | h = ip[0:f] 577 | for k, v in bpip.items(): # Check for target = host running the mtr session - Try to Add 578 | if k == h: # this Endpoint target to the Probe Target Cluster. 579 | s += epc[ip] 580 | s += "\t}\n" 581 | # 582 | # Default Gateway Cluster... 583 | s += "\n\t### Default Gateway Cluster ###\n" 584 | if self._gw != '': 585 | if self._gw in self._ips: 586 | if not self._gw in self._exptrg: 587 | s += '\tsubgraph cluster_default_gateway {\n' 588 | s += '\t\ttooltip="Default Gateway Host: {gw:s}";\n'.format(gw=self._gw) 589 | s += '\t\tcolor="goldenrod";\n' 590 | s += '\t\tgradientangle=270;\n' 591 | s += '\t\tfillcolor="white:#b8860b30";\n' 592 | s += '\t\tstyle="filled,rounded";\n' 593 | s += '\t\tpenwidth=3;\n' 594 | s += '\t\tfontsize=11;\n' 595 | s += '\t\tfontname="Sans-Serif";\n' 596 | s += '\t\tlabel=<
Default Gateway
>;\n' 597 | s += '\t\t"{gw:s}" [shape="diamond",fontname="Sans-Serif",fontsize=11,color="black",gradientangle=270,fillcolor="white:goldenrod",style="rounded,filled",tooltip="Default Gateway Host: {gw:s}"];\n'.format(gw=self._gw) 598 | s += "\t}\n" 599 | # 600 | # Build Begin Point strings... 601 | # Ex bps = '192.168.43.48" [shape="record",color="black",gradientangle=270,fillcolor="white:darkorange",style="filled",' 602 | # + 'label="192.168.43.48\nProbe|{http|{T1|T3}}|{https:{T4|T4}}"];' 603 | s += "\n\t### Probe Begin Traces ###\n" 604 | for k, v in bpip.items(): 605 | tr = '' 606 | for sv in v: 607 | if self._netprotocol == 'ICMP': 608 | if sv[1].find('ICMP') >= 0: 609 | ps = '{p:s} echo-request'.format(p=sv[1]) 610 | else: 611 | ps = 'ICMP({p:s}) echo-request'.format(p=sv[1]) 612 | else: 613 | ps = '{pr:s}: {p:s}'.format(pr=self._netprotocol, p=sv[1]) 614 | if tr == '': 615 | tr += '{{{ps:s}|{{{t:s}}}}}'.format(ps=ps, t=sv[0]) 616 | else: 617 | tr += '|{{{ps:s}|{{{t:s}}}}}'.format(ps=ps, t=sv[0]) 618 | bps1 = '\t"{ip:s}" [shape="record",color="black",gradientangle=270,fillcolor="white:darkorange",style="filled,rounded",'.format(ip=k) 619 | if self._iface != '': 620 | bps2 = 'label="Probe: {ip:s}\\nNetwork Interface: {ifc:s}|{tr:s}",tooltip="Begin Host Probe: {ip:s}"];\n'.format(ip=k, ifc=self._iface, tr=tr) 621 | else: 622 | bps2 = 'label="Probe: {ip:s}|{tr:s}",tooltip="Begin Host Probe: {ip:s}"];\n'.format(ip=k, tr=tr) 623 | s += bps1 + bps2 624 | # 625 | s += "\n\t### Target Endpoints ###\n" 626 | # 627 | # Combine Trace Target Endpoints... 628 | # 629 | # k0 k1 k2 v0 v1 v2 k0 k1 k2 v0 v1 v2 630 | # Ex: ep = {('162.144.22.87',80,'http'): ['SA','T1','T3'], ('10.14.22.8',443,'https'): ['SA','T2','T4']} 631 | ep = {} # ep -> A single services label for a given IP 632 | for d in self._tlblid: # k v0 v1 v2 v3 v4 v5 v6 v7 633 | for k, v in d.items(): # Ex: k: 162.144.22.87 v: ('T1', '10.222.222.10', '162.144.22.87', 6, 443, 'https', 'SA', '') 634 | if not v[6] == 'BH': # Blackhole detection - do not create Endpoint 635 | p = ep.get((k, v[4], v[5])) 636 | if p == None: 637 | ep[(k, v[4], v[5])] = [v[6], v[0]] # Add new (TCP Flags / ICMP type / Proto) and initial trace ID 638 | else: 639 | ep[(k, v[4], v[5])].append(v[0]) # Append additional trace IDs 640 | # 641 | # Combine Endpoint services... 642 | # k v v 643 | # k sv0 sv1 sv2 sv0 sv1 sv2 644 | # Ex epip = {'206.111.13.58': [('T8|T10', 'https', 'SA'), ('T7|T6', 'http', 'SA')]} 645 | epip = {} # epip -> Combined Endpoint services label for a given IP 646 | for k, v in ep.items(): 647 | tr = '' 648 | for t in range(1, len(v)): 649 | if tr == '': 650 | tr += '{ts:s}'.format(ts=v[t]) 651 | else: 652 | tr += '|{ts:s}'.format(ts=v[t]) 653 | p = k[2] 654 | if p == '': # Use port number not name if resolved 655 | p = str(k[1]) 656 | else: 657 | p += '(' + str(k[1]) + ')' # Use both name and port 658 | if k[0] in epip: 659 | epip[k[0]].append((tr, p, v[0])) 660 | else: 661 | epip[k[0]] = [(tr, p, v[0])] 662 | # 663 | # Build Endpoint strings... 664 | # Ex eps = '162.144.22.87" [shape=record,color="black",gradientangle=270,fillcolor="darkgreen:green",style=i"filled,rounded",' 665 | # + 'label="162.144.22.87\nTarget|{{T1|T3}|https SA}|{{T4|T4}|http SA}"];' 666 | for k, v in epip.items(): 667 | tr = '' 668 | for sv in v: 669 | if self._netprotocol == 'ICMP': 670 | ps = 'ICMP(0) echo-reply' 671 | else: 672 | ps = '{p:s} {f:s}'.format(p=sv[1], f=sv[2]) 673 | if tr == '': 674 | tr += '{{{{{t:s}}}|{ps:s}}}'.format(t=sv[0], ps=ps) 675 | else: 676 | tr += '|{{{{{t:s}}}|{ps:s}}}'.format(t=sv[0], ps=ps) 677 | pre = '' 678 | if k in uepprb: # Special Case: Separate Endpoint Target from Probe 679 | pre = '_' # when they are the same 680 | eps1 = '\t"{pre:s}{ip:s}" [shape="record",color="black",gradientangle=270,fillcolor="#00ff00:#005400",style="filled,rounded",'.format(pre=pre, ip=k) 681 | eps2 = 'label="Resolved Target\\n{ip:s}|{tr:s}",tooltip="MTR Resolved Target: {ip:s}"];\n'.format(ip=k, tr=tr) 682 | s += eps1 + eps2 683 | # 684 | # Blackholes... 685 | # 686 | # ***Note: Order matters: If a hop is both a Blackhole on one trace and 687 | # a ICMP destination unreachable hop on another, 688 | # it will appear in the dot file as two nodes in 689 | # both sections. The ICMP destination unreachable 690 | # hop node will take precedents and appear only 691 | # since it is defined last. 692 | s += "\n\t### Blackholes ###\n" 693 | bhhops = [] 694 | for d in self._tlblid: # k v0 v1 v2 v3 v4 v5 v6 v7 695 | for k, v in d.items(): # Ex: k: 162.144.22.87 v: ('T1', '10.222.222.10', '162.144.22.87', 'tcp', 5555, '', 'BH', 'I3') 696 | if v[6] == 'BH': # Blackhole detection 697 | # 698 | # If both a target blackhole and an ICMP packet hop, then skip creating this 699 | # node we be created in the 'ICMP Destination Unreachable Hops' section. 700 | if v[7] != 'I3': # ICMP destination not reached detection 701 | nd = '{b:s} {prt:d}/{pro:s}'.format(b=v[2], prt=v[4], pro=v[3]) 702 | if self._netprotocol == 'ICMP': 703 | bhh = '{b:s}
ICMP(0) echo-reply'.format(b=v[2]) 704 | else: 705 | bhh = nd 706 | # 707 | # If not already added... 708 | if bhh not in bhhops: 709 | lb = 'label=<{lh:s}
Failed Target>'.format(lh=bhh) 710 | s += '\t"{bh:s}" [{l:s},shape="doubleoctagon",color="black",gradientangle=270,fillcolor="white:red",style="filled,rounded",tooltip="Failed MTR Resolved Target: {b:s}"];\n'.format(bh=nd, l=lb, b=v[2]) 711 | bhhops.append(bhh) 712 | # 713 | # ICMP Destination Unreachable Hops... 714 | s += "\n\t### ICMP Destination Unreachable Hops ###\n" 715 | for d in self._ports: 716 | for p in self._ports[d]: 717 | if d in self._exptrg: 718 | # 719 | # Create Node: Target same as node that returns an ICMP packet... 720 | if p.find('ICMP dest-unreach') >= 0: 721 | unreach = 'ICMP(3): Destination' 722 | # 0 1 2 3 4 5 723 | # Ex ICMP ports: ' ICMP dest-unreach port-unreachable 17 53' 724 | icmpparts = p.split(' ') 725 | if icmpparts[3] == 'network-unreachable': 726 | unreach += '/Network' 727 | elif icmpparts[3] == 'host-unreachable': 728 | unreach += '/Host' 729 | elif icmpparts[3] == 'protocol-unreachable': 730 | unreach += '/Protocol' 731 | elif icmpparts[3] == 'port-unreachable': 732 | unreach += '/Port' 733 | protoname = self.get_proto_name(icmpparts[4]) 734 | protoport = '{pr:s}/{pt:s}'.format(pr=icmpparts[5], pt=protoname) 735 | lb = 'label=<{lh:s} {pp:s}
{u:s} Unreachable
Failed Target>'.format(lh=d, pp=protoport, u=unreach) 736 | s += '\t"{lh:s} {pp:s}" [{lb:s},shape="doubleoctagon",color="black",gradientangle=270,fillcolor="yellow:red",style="filled,rounded",tooltip="{u:s} Unreachable, Failed Resolved Target: {lh:s} {pp:s}"];\n'.format(lb=lb, pp=protoport, lh=d, u=unreach) 737 | else: 738 | # 739 | # Create Node: Target not same as node that returns an ICMP packet... 740 | if p.find('ICMP dest-unreach') >= 0: 741 | unreach = 'ICMP(3): Destination' 742 | if p.find('network-unreachable') >= 0: 743 | unreach += '/Network' 744 | elif p.find('host-unreachable') >= 0: 745 | unreach += '/Host' 746 | elif p.find('protocol-unreachable') >= 0: 747 | unreach += '/Protocol' 748 | elif p.find('port-unreachable') >= 0: 749 | unreach += '/Port' 750 | lb = 'label=<{lh:s} 3/icmp
{u:s} Unreachable>'.format(lh=d, u=unreach) 751 | s += '\t"{lh:s} 3/icmp" [{lb:s},shape="doubleoctagon",color="black",gradientangle=270,fillcolor="white:yellow",style="filled,rounded",tooltip="{u:s} Unreachable, Hop Host: {lh:s}"];\n'.format(lb=lb, lh=d, u=unreach) 752 | # 753 | # Padding check... 754 | if self._graphpadding: 755 | s += "\n\t### Nodes With Padding ###\n" 756 | pad = {} 757 | for t in range(0, self._ntraces): 758 | for _, rcv in self._res[t]: 759 | if rcv.src not in self._ports and rcv.haslayer(conf.padding_layer): 760 | p = rcv.getlayer(conf.padding_layer).load 761 | if p != "\x00" * len(p): 762 | pad[rcv.src] = None 763 | for sr in pad: 764 | lb = 'label=<
{r:s}
Padding>'.format(r=sr) 765 | s += '\t"{r:s}" [{l:s},shape="box3d",color="black",gradientangle=270,fillcolor="white:red",style="filled,rounded"];\n'.format(r=sr, l=lb) 766 | # 767 | # Draw each trace (i.e., DOT edge) for each number of queries... 768 | s += "\n\t### Traces ###\n" 769 | t = 0 770 | for q in range(0, self._ntraces): 771 | for rtk in self._rt[q]: 772 | s += "\t### T{tr:d} -> {r:s} ###\n".format(tr=(t + 1), r=repr(rtk)) 773 | col = next(forecolorlist) 774 | s += '\tedge [color="#{s0:s}{s1:s}{s2:s}"];\n'.format(s0=col[0], s1=col[1], s2=col[2]) 775 | # 776 | # Probe Begin Point (i.e., Begining of a trace)... 777 | for k, v in self._tlblid[t].items(): 778 | ptr = probe = v[1] 779 | s += '\t"{bp:s}":B{tr:s}:s -> '.format(bp=ptr, tr=v[0]) 780 | # 781 | # In between traces (i.e., Not at the begining or end of a trace)... 782 | trace = self._rt[q][rtk] 783 | tk = trace.keys() 784 | ntr = trace[min(tk)] 785 | # 786 | # Skip in between traces if there are none... 787 | if len(trace) > 1: 788 | lb = 'Trace: {tr:d}:{tn:d}, {lbp:s} -> {lbn:s}'.format(tr=(t + 1), tn=min(tk), lbp=ptr, lbn=ntr.replace('"', '')) 789 | if not 'Unk' in ntr: 790 | lb += ' (RTT: {prb:s} <-> {lbn:s} ({rtt:s}ms))'.format(prb=probe, lbn=ntr.replace('"', ''), rtt=self._rtt[t + 1][min(tk)]) 791 | if rtt: 792 | if not 'Unk' in ntr: 793 | llb = 'Trace: {tr:d}:{tn:d}, RTT: {prb:s} <-> {lbn:s} ({rtt:s}ms)'.format(tr=(t + 1), tn=min(tk), prb=probe, lbn=ntr.replace('"', ''), rtt=self._rtt[t + 1][min(tk)]) 794 | s += '{ntr:s} [label=<  {rtt:s}ms>,edgetooltip="{lb:s}",labeltooltip="{llb:s}"];\n'.format(ntr=ntr, rtt=self._rtt[t + 1][min(tk)], lb=lb, llb=llb) 795 | else: 796 | s += '{ntr:s} [edgetooltip="{lb:s}"];\n'.format(ntr=ntr, lb=lb) 797 | else: 798 | s += '{ntr:s} [edgetooltip="{lb:s}"];\n'.format(ntr=ntr, lb=lb) 799 | for n in range(min(tk) + 1, max(tk)): 800 | ptr = ntr 801 | ntr = trace[n] 802 | lb = 'Trace: {tr:d}:{tn:d}, {lbp:s} -> {lbn:s}'.format(tr=(t + 1), tn=n, lbp=ptr.replace('"', ''), lbn=ntr.replace('"', '')) 803 | if not 'Unk' in ntr: 804 | lb += ' (RTT: {prb:s} <-> {lbn:s} ({rtt:s}ms))'.format(prb=probe, lbn=ntr.replace('"', ''), rtt=self._rtt[t + 1][n]) 805 | if rtt: 806 | if not 'Unk' in ntr: 807 | llb = 'Trace: {tr:d}:{tn:d}, RTT: {prb:s} <-> {lbn:s} ({rtt:s}ms)'.format(tr=(t + 1), tn=n, prb=probe, lbn=ntr.replace('"', ''), rtt=self._rtt[t + 1][n]) 808 | # 809 | # Special check to see if the next and previous nodes are the same. 810 | # If yes use the DOT 'xlabel' attribute to spread out labels so that they 811 | # do not clash and 'forcelabel' so that they are placed. 812 | if ptr == ntr: 813 | s += '\t{ptr:s} -> {ntr:s} [xlabel=<  {rtt:s}ms>,forcelabel=True,edgetooltip="{lb:s}",labeltooltip="{llb:s}"];\n'.format(ptr=ptr, ntr=ntr, rtt=self._rtt[t + 1][n], lb=lb, llb=llb) 814 | else: 815 | s += '\t{ptr:s} -> {ntr:s} [label=<  {rtt:s}ms>,edgetooltip="{lb:s}",labeltooltip="{llb:s}"];\n'.format(ptr=ptr, ntr=ntr, rtt=self._rtt[t + 1][n], lb=lb, llb=llb) 816 | else: 817 | s += '\t{ptr:s} -> {ntr:s} [edgetooltip="{lb:s}"];\n'.format(ptr=ptr, ntr=ntr, lb=lb) 818 | else: 819 | s += '\t{ptr:s} -> {ntr:s} [edgetooltip="{lb:s}"];\n'.format(ptr=ptr, ntr=ntr, lb=lb) 820 | # 821 | # Enhance target Endpoint (i.e., End of a trace) replacement... 822 | for k, v in self._tlblid[t].items(): 823 | # 824 | # 01-12-2020: Limit test for max index (Fix for ISP Verizon FIOS TTL manipulation with ICMP packets) 825 | maxtk = max(tk) 826 | if maxtk not in self._rtt[t + 1]: 827 | maxtk -= 1 # Reduce max index by one 828 | ############### 829 | if v[6] == 'BH': # Blackhole detection - do not create Enhanced Endpoint 830 | # 831 | # Check for Last Hop / Backhole (Failed Target) match: 832 | lh = trace[max(tk)] 833 | lhicmp = False 834 | if lh.find(':I3') >= 0: # Is last hop and ICMP packet from target? 835 | lhicmp = True 836 | f = lh.find(' ') # Strip off 'port/proto' ''"100.41.207.244":I3' 837 | if f >= 0: 838 | lh = lh[0:f] 839 | f = lh.find(':') # Strip off 'proto:port' -> '"100.41.207.244 801/tcp"' 840 | if f >= 0: 841 | lh = lh[0:f] 842 | lh = lh.replace('"', '') # Remove surrounding double quotes ("") 843 | if k == lh: # Does Hop match final Target? 844 | # 845 | # Backhole last hop matched target: 846 | # 847 | # Check to skip in between traces... 848 | if len(trace) > 1: 849 | s += '\t{ptr:s} -> '.format(ptr=ntr) 850 | if lhicmp: 851 | # 852 | # Last hop is an ICMP packet from target and was reached... 853 | lb = 'Trace: {tr:d}:{tn:d}, {lbp:s} -> {lbn:s}'.format(tr=(t + 1), tn=max(tk), lbp=ntr.replace('"', ''), lbn=k) 854 | lb += ' (RTT: {prb:s} <-> {lbn:s} ({rtt:s}ms))'.format(prb=v[1], lbn=lh, rtt=self._rtt[t + 1][maxtk]) 855 | if rtt: 856 | llb = 'Trace: {tr:d}:{tn:d}, RTT: {prb:s} <-> {lbn:s} ({rtt:s}ms)'.format(tr=(t + 1), tn=max(tk), prb=v[1], lbn=k, rtt=self._rtt[t + 1][maxtk]) 857 | s += '"{bh:s} {bhp:d}/{bht:s}" [style="solid",label=<  {rtt:s}ms>,edgetooltip="{lb:s}",labeltooltip="{llb:s}"];\n'.format(bh=k, bhp=v[4], bht=v[3], rtt=self._rtt[t + 1][maxtk], lb=lb, llb=llb) 858 | else: 859 | s += '"{bh:s} {bhp:d}/{bht:s}" [style="solid",edgetooltip="{lb:s}"];\n'.format(bh=k, bhp=v[4], bht=v[3], lb=lb) 860 | else: 861 | # 862 | # Last hop is not ICMP packet from target (Fake hop - never reached - use dashed trace)... 863 | lb = 'Trace: {tr:d} - Failed MTR Resolved Target: {bh:s} {bhp:d}/{bht:s}'.format(tr=(t + 1), bh=k, bhp=v[4], bht=v[3]) 864 | s += '"{bh:s} {bhp:d}/{bht:s}" [style="dashed",label=<  T{tr:d}>,edgetooltip="{lb:s}",labeltooltip="{lb:s}"];\n'.format(bh=k, bhp=v[4], bht=v[3], tr=(t + 1), lb=lb) 865 | else: 866 | # 867 | # Backhole not matched (Most likely: 'ICMP (3) destination-unreached' 868 | # but last hop not equal to the target: 869 | # 870 | # Add this last Hop (This Hop is not the Target)... 871 | # 872 | # Check to skip in between traces... 873 | if len(trace) > 1: 874 | s += '\t{ptr:s} -> '.format(ptr=ntr) 875 | lb = 'Trace: {tr:d}:{tn:d}, {lbp:s} -> {lbn:s}'.format(tr=(t + 1), tn=max(tk), lbp=ntr.replace('"', ''), lbn=lh) 876 | lb += ' (RTT: {prb:s} <-> {lbn:s} ({rtt:s}ms))'.format(prb=v[1], lbn=lh, rtt=self._rtt[t + 1][maxtk]) 877 | llb = 'Trace: {tr:d}:{tn:d}, RTT: {prb:s} <-> {lbn:s} ({rtt:s}ms)'.format(tr=(t + 1), tn=max(tk), prb=v[1], lbn=lh, rtt=self._rtt[t + 1][maxtk]) 878 | if rtt: 879 | s += '"{lh:s} 3/icmp" [style="solid",label=<  {rtt:s}ms>,edgetooltip="{lb:s}",labeltooltip="{llb:s}"];\n'.format(lh=lh, rtt=self._rtt[t + 1][maxtk], lb=lb, llb=llb) 880 | else: 881 | s += '"{lh:s} 3/icmp" [style="solid",edgetooltip="{lb:s} 3/icmp",labeltooltip="{llb:s}"];\n'.format(lh=lh, lb=lb, llb=llb) 882 | # 883 | # Add the Failed Target (Blackhole - Fake hop - never reached - use dashed trace)... 884 | s += '\t"{lh:s} 3/icmp" -> '.format(lh=lh) 885 | lb = 'Trace: {tr:d} - Failed MTR Resolved Target: {bh:s} {bhp:d}/{bht:s}'.format(tr=(t + 1), bh=k, bhp=v[4], bht=v[3]) 886 | s += '"{bh:s} {bhp:d}/{bht:s}" [style="dashed",label=<  T{tr:d}>,edgetooltip="{lb:s}",labeltooltip="{llb:s}"];\n'.format(bh=k, bhp=v[4], bht=v[3], tr=(t + 1), lb=lb, llb=lb) 887 | 888 | else: # Enhanced Target Endpoint 889 | # 890 | # Check to skip in between traces... 891 | if len(trace) > 1: 892 | s += '\t{ptr:s} -> '.format(ptr=ntr) 893 | lb = 'Trace: {tr:d}:{tn:d}, {lbp:s} -> {lbn:s}'.format(tr=(t + 1), tn=max(tk), lbp=ntr.replace('"', ''), lbn=k) 894 | if not 'Unk' in k: 895 | lb += ' (RTT: {prb:s} <-> {lbn:s} ({rtt:s}ms))'.format(prb=v[1], lbn=k, rtt=self._rtt[t + 1][maxtk]) 896 | pre = '' 897 | if k in uepprb: # Special Case: Distinguish the Endpoint Target from Probe 898 | pre = '_' # when they are the same using the underscore char: '_'. 899 | if rtt: 900 | if not 'Unk' in k: 901 | llb = 'Trace: {tr:d}:{tn:d}, RTT: {prb:s} <-> {lbn:s} ({rtt:s}ms)'.format(tr=(t + 1), tn=max(tk), prb=v[1], lbn=k, rtt=self._rtt[t + 1][maxtk]) 902 | # 903 | # Check to remove label clashing... 904 | ntrs = ntr.replace('"', '') # Remove surrounding double quotes ("") 905 | if ntrs == k: 906 | s += '"{pre:s}{ep:s}":E{tr:s}:n [style="solid",xlabel=<  {rtt:s}ms>,forcelabel=True,edgetooltip="{lb:s}",labeltooltip="{llb:s}"];\n'.format(pre=pre, ep=k, tr=v[0], rtt=self._rtt[t + 1][maxtk], lb=lb, llb=llb) 907 | else: 908 | s += '"{pre:s}{ep:s}":E{tr:s}:n [style="solid",label=<  {rtt:s}ms>,edgetooltip="{lb:s}",labeltooltip="{llb:s}"];\n'.format(pre=pre, ep=k, tr=v[0], rtt=self._rtt[t + 1][maxtk], lb=lb, llb=llb) 909 | else: 910 | s += '"{pre:s}{ep:s}":E{tr:s}:n [style="solid",edgetooltip="{lb:s}"];\n'.format(pre=pre, ep=k, tr=v[0], lb=lb) 911 | else: 912 | s += '"{pre:s}{ep:s}":E{tr:s}:n [style="solid",edgetooltip="{lb:s}"];\n'.format(pre=pre, ep=k, tr=v[0], lb=lb) 913 | t += 1 # Next trace out of total traces 914 | # 915 | # Decorate Unknown ('Unkn') Nodes... 916 | s += "\n\t### Decoration For Unknown (Unkn) Node Hops ###\n" 917 | for u in self._unks: 918 | s += '\t{u:s} [tooltip="Trace: {t:s}, Unknown Hop: {u2:s}",shape="egg",fontname="Sans-Serif",fontsize=9,height=0.2,width=0.2,color="black",gradientangle=270,fillcolor="white:#d8d8d8",style="filled"];\n'.format(u=u, t=self._unks[u][2], u2=u.replace('"', '')) 919 | # 920 | # Create tooltip for standalone nodes... 921 | s += "\n\t### Tooltip for Standalone Node Hops ###\n" 922 | for k, v in self._ips.items(): 923 | if not k in cipall: 924 | if k != self._gw: 925 | if not k in cepipall: 926 | if not k in self._ports: 927 | found = False 928 | for tid in self._tlblid: 929 | if k in tid: 930 | found = True 931 | break 932 | if not found: 933 | s += '\t"{ip:s}" [tooltip="Hop Host: {ip:s}"];\n'.format(ip=k) 934 | # 935 | # End the DOT Digraph... 936 | s += "}\n" 937 | # 938 | # Store the DOT Digraph results... 939 | self._graphdef = s 940 | 941 | # 942 | # Graph the Multi-Traceroute... 943 | def graph(self, ASres=None, padding=0, vspread=0.75, title="Multi-Traceroute Probe (MTR)", timestamp="", rtt=1, **kargs): 944 | """x.graph(ASres=conf.AS_resolver, other args): 945 | ASres = None : Use AS default resolver => 'conf.AS_resolver' 946 | ASres = AS_resolver() : default whois AS resolver (riswhois.ripe.net) 947 | ASres = AS_resolver_cymru(): use whois.cymru.com whois database 948 | ASres = AS_resolver(server="whois.ra.net") 949 | 950 | padding: Show packets with padding as a red 3D-Box. 951 | vspread: Vertical separation between nodes on graph. 952 | title: Title text for the rendering graphic. 953 | timestamp: Title Time Stamp text to appear below the Title text. 954 | rtt: Display Round-Trip Times (msec) for Hops along trace edges. 955 | format: Output type (svg, ps, gif, jpg, etc.), passed to dot's "-T" option. 956 | figsize: w,h tuple in inches. See matplotlib documentation. 957 | target: Filename or redirect. 958 | prog: Which graphviz program to use.""" 959 | if self._asres is None: 960 | self._asres = conf.AS_resolver 961 | if (self._graphdef is None or # Remake the graph if there are any changes 962 | self._graphasres != self._asres or 963 | self._graphpadding != padding): 964 | self.make_dot_graph(ASres, padding, vspread, title, timestamp, rtt) 965 | 966 | return do_graph(self._graphdef, **kargs) 967 | 968 | 969 | ################################## 970 | # Multi-Traceroute Results Class # 971 | ################################## 972 | class MTracerouteResult(SndRcvList): 973 | def __init__(self, res=None, name="MTraceroute", stats=None): 974 | SndRcvList.__init__(self, res, name, stats) 975 | 976 | def show(self, ntrace): 977 | return self.make_table(lambda s, r: 978 | (s.sprintf("Trace: " + str(ntrace) + " - %IP.dst%:{TCP:tcp%ir,TCP.dport%}{UDP:udp%ir,UDP.dport%}{ICMP:ICMP}"), 979 | s.ttl, 980 | r.sprintf("%-15s,IP.src% {TCP:%TCP.flags%}{ICMP:%ir,ICMP.type%}"))) 981 | # 982 | # Get trace components... 983 | # 984 | # mtrc - Instance of a MTRC class 985 | # 986 | # nq - Traceroute query number 987 | def get_trace_components(self, mtrc, nq): 988 | ips = {} 989 | rt = {} 990 | rtt = {} 991 | trtt = {} 992 | ports = {} 993 | portsdone = {} 994 | trgttl = {} 995 | if len(self.res) > 0: 996 | # 997 | # Responses found... 998 | for s, r in self.res: 999 | s = s.getlayer(IP) or (conf.ipv6_enabled and s[IPv6]) or s 1000 | r = r.getlayer(IP) or (conf.ipv6_enabled and r[IPv6]) or r 1001 | # 1002 | # Make sure 'r.src' is an IP Address (e.g., Case where r.src = '24.97.150.188 80/tcp') 1003 | rs = r.src.split() 1004 | ips[rs[0]] = None 1005 | if TCP in s: 1006 | trace_id = (s.src, s.dst, 6, s.dport) 1007 | elif UDP in s: 1008 | trace_id = (s.src, s.dst, 17, s.dport) 1009 | elif ICMP in s: 1010 | trace_id = (s.src, s.dst, 1, s.type) 1011 | else: 1012 | trace_id = (s.src, s.dst, s.proto, 0) 1013 | trace = rt.get(trace_id, {}) 1014 | ttl = conf.ipv6_enabled and IPv6 in s and s.hlim or s.ttl 1015 | # 1016 | # Check for packet response types: 1017 | if not (ICMP in r and r[ICMP].type == 11) and not (conf.ipv6_enabled and IPv6 in r and ICMPv6TimeExceeded in r): 1018 | # 1019 | # Mostly: Process target reached or ICMP Unreachable... 1020 | if trace_id in portsdone: 1021 | # 1022 | # Special check for out or order response packets: If previous trace was determined 1023 | # done, but a ttl arrives with a lower value then process this response packet as the 1024 | # final ttl target packet. 1025 | if ttl >= trgttl[trace_id]: 1026 | continue # Next Send/Receive packet 1027 | else: 1028 | # 1029 | # Out of order response packet - process this packet as the possible 1030 | # final ttl target packet. 1031 | try: 1032 | if trgttl[trace_id] in trace: 1033 | del trace[trgttl[trace_id]] # Remove previous ttl target 1034 | except: 1035 | pass 1036 | portsdone[trace_id] = None 1037 | trgttl[trace_id] = ttl # Save potential target ttl packet 1038 | p = ports.get(r.src, []) 1039 | if TCP in r: 1040 | p.append(r.sprintf(" %TCP.sport% %TCP.flags%")) 1041 | trace[ttl] = r.sprintf('"%r,src%":T%ir,TCP.sport%') 1042 | elif UDP in r: 1043 | p.append(r.sprintf(" %UDP.sport%")) 1044 | trace[ttl] = r.sprintf('"%r,src%":U%ir,UDP.sport%') 1045 | elif ICMP in r: 1046 | if r[ICMP].type == 0: 1047 | # 1048 | # Process echo-reply... 1049 | p.append(r.sprintf(" ICMP %ICMP.type%")) 1050 | trace[ttl] = r.sprintf('"%r,src%":I%ir,ICMP.type%') 1051 | else: 1052 | # 1053 | # Format Ex: ' ICMP dest-unreach port-unreachable 17 53' 1054 | p.append(r.sprintf(" ICMP %ICMP.type% %ICMP.code% %ICMP.proto% %r,ICMP.dport%")) 1055 | trace[ttl] = r.sprintf('"%r,src%":I%ir,ICMP.type%') 1056 | else: 1057 | p.append(r.sprintf("{IP: IP %proto%}{IPv6: IPv6 %nh%}")) 1058 | trace[ttl] = r.sprintf('"%r,src%":{IP:P%ir,proto%}{IPv6:P%ir,nh%}') 1059 | ports[r.src] = p 1060 | else: 1061 | # 1062 | # Most likely a ICMP Time-Exceeded packet - Save Hop Host IP Address... 1063 | trace[ttl] = r.sprintf('"%r,src%"') 1064 | rt[trace_id] = trace 1065 | # 1066 | # Compute the Round Trip Time for this trace packet in (msec)... 1067 | rtrace = rtt.get(trace_id, {}) 1068 | crtt = (r.time - s.sent_time) * 1000 1069 | rtrace[ttl] = "{crtt:.3f}".format(crtt=crtt) 1070 | rtt[trace_id] = rtrace 1071 | else: 1072 | # 1073 | # No Responses found - Most likely target same as host running the mtr session... 1074 | # 1075 | # Create a 'fake' failed target (Blackhole) trace using the destination host 1076 | # found in unanswered packets... 1077 | for p in mtrc._ures[nq]: 1078 | ips[p.dst] = None 1079 | trace_id = (p.src, p.dst, p.proto, p.dport) 1080 | portsdone[trace_id] = None 1081 | if trace_id not in rt: 1082 | pt = mtrc.get_proto_name(p.proto) 1083 | # 1084 | # Set trace number to zero (0) (i.e., ttl = 0) for this special case: 1085 | # target = mtr session host - 'fake' failed target... 1086 | rt[trace_id] = {1: '"{ip:s} {pr:d}/{pt:s}"'.format(ip=p.dst, pr=p.dport, pt=pt)} 1087 | # 1088 | # Store each trace component... 1089 | mtrc._ips.update(ips) # Add unique IP Addresses 1090 | mtrc._rt.append(rt) # Append a new Traceroute 1091 | mtrc._ports.update(ports) # Append completed Traceroute target and port info 1092 | mtrc._portsdone.update(portsdone) # Append completed Traceroute with associated target and port 1093 | # 1094 | # Create Round Trip Times Trace lookup dictionary... 1095 | tcnt = mtrc._tcnt 1096 | for rttk in rtt: 1097 | tcnt += 1 1098 | trtt[tcnt] = rtt[rttk] 1099 | mtrc._rtt.update(trtt) # Update Round Trip Times for Trace Nodes 1100 | # 1101 | # Update the Target Trace Label IDs and Blackhole (Failed Target) detection... 1102 | # 1103 | # rtk0 rtk1 rtk2 rtk3 1104 | # Ex: {('10.222.222.10', '10.222.222.1', 6, 9980): {1: '"10.222.222.10":T9980'}} 1105 | for rtk in rt: 1106 | mtrc._tcnt += 1 # Compute the total trace count 1107 | # 1108 | # Derive flags from ports: 1109 | # Ex: {'63.117.14.247': [' http SA', ' https SA']} 1110 | prtflgs = ports.get(rtk[1], []) 1111 | found = False 1112 | for pf in prtflgs: 1113 | if mtrc._netprotocol == 'ICMP': 1114 | pat = '' # ICMP: Create reg exp pattern 1115 | else: 1116 | pat = '<[TU]{p:d}>'.format(p=rtk[3]) # TCP/UDP: Create reg exp pattern 1117 | match = re.search(pat, pf) # Search for port match 1118 | if match: 1119 | found = True 1120 | s = pf.split(' ') 1121 | if len(s) == 3: 1122 | pn = s[1] # Service Port name / ICMP 1123 | fl = s[2] # TCP Flags / ICMP Type / Proto 1124 | elif len(s) == 2: 1125 | pn = s[1] # Service Port name 1126 | fl = '' 1127 | else: 1128 | pn = '' 1129 | fl = '' 1130 | break 1131 | ic = '' # ICMP Destination not reachable flag 1132 | if not found: # Set Blackhole found - (fl -> 'BH') 1133 | # 1134 | # Set flag for last hop is a target and ICMP destination not reached flag set... 1135 | trace = rt[rtk] 1136 | tk = trace.keys() 1137 | lh = trace[max(tk)] 1138 | f = lh.find(':I3') # Is hop an ICMP destination not reached node? 1139 | if f >= 0: 1140 | lh = lh[0:f] # Strip off 'proto:port' -> '"100.41.207.244":I3' 1141 | lh = lh.replace('"', '') # Remove surrounding double quotes ("") 1142 | if lh in mtrc._exptrg: # Is last hop a target? 1143 | ic = 'I3' 1144 | pn = '' 1145 | fl = 'BH' 1146 | # 1147 | # Update the Target Trace Label ID: 1148 | # Ex: {'63.117.14.247': ('T2', '10.222.222.10', '162.144.22.87', 6, 443, 'https', 'SA', '')} 1149 | pt = mtrc.get_proto_name(rtk[2]) 1150 | tlid = {rtk[1]: ('T' + str(mtrc._tcnt), rtk[0], rtk[1], pt, rtk[3], pn, fl, ic)} 1151 | mtrc._tlblid.append(tlid) 1152 | 1153 | 1154 | #################### 1155 | # Multi-Traceroute # 1156 | #################### 1157 | @conf.commands.register 1158 | def mtr(target, dport=80, minttl=1, maxttl=30, stype="Random", srcport=50000, iface=None, l4=None, filter=None, timeout=2, verbose=None, gw=None, netproto="TCP", nquery=1, ptype=None, payload=b'', privaddr=0, rasn=1, **kargs): 1159 | """A Multi-Traceroute (mtr) command: 1160 | mtr(target, [maxttl=30,] [dport=80,] [sport=80,] [minttl=1,] [maxttl=1,] [iface=None] 1161 | [l4=None,] [filter=None,] [nquery=1,] [privaddr=0,] [rasn=1,] [verbose=conf.verb]) 1162 | 1163 | stype: Source Port Type: "Random" or "Increment". 1164 | srcport: Source Port. Default: 50000. 1165 | gw: IPv4 Address of the Default Gateway. 1166 | netproto: Network Protocol (One of: "TCP", "UDP" or "ICMP"). 1167 | nquery: Number of Traceroute queries to perform. 1168 | ptype: Payload Type: "Disable", "RandStr", "RandStrTerm" or "Custom". 1169 | payload: A byte object for each packet payload (e.g., b'\x01A\x0f\xff\x00') for ptype: 'Custom'. 1170 | privaddr: 0 - Default: Normal display of all resolved AS numbers. 1171 | 1 - Do not show an associated AS Number bound box (cluster) on graph for a private IPv4 Address. 1172 | rasn: 0 - Do not resolve AS Numbers - No graph clustering. 1173 | 1 - Default: Resolve all AS numbers. 1174 | retry: If positive, how many times to resend unanswered packets 1175 | if negative, how many times to retry when no more packets 1176 | are answered. 1177 | timeout: How much time to wait after the last packet has been sent.""" 1178 | # 1179 | # Initialize vars... 1180 | trace = [] # Individual trace array 1181 | # 1182 | # Range check number of query traces 1183 | if nquery < 1: 1184 | nquery = 1 1185 | # 1186 | # Create instance of an MTR class... 1187 | mtrc = MTR(nquery=nquery, target=target) 1188 | # 1189 | # Default to network protocol: "TCP" if not found in list... 1190 | plist = ["TCP", "UDP", "ICMP"] 1191 | netproto = netproto.upper() 1192 | if netproto not in plist: 1193 | netproto = "TCP" 1194 | mtrc._netprotocol = netproto 1195 | # 1196 | # Default to source type: "Random" if not found in list... 1197 | slist = ["Random", "Increment"] 1198 | stype = stype.title() 1199 | if stype not in slist: 1200 | stype = "Random" 1201 | if stype == "Random": 1202 | sport = RandShort() # Random 1203 | elif stype == "Increment": 1204 | if srcport != None: 1205 | sport = IncrementalValue(start=(srcport - 1), step=1, restart=65535) # Increment 1206 | # 1207 | # Default to payload type to it's default network protocol value if not found in list... 1208 | pllist = ["Disabled", "RandStr", "RandStrTerm", "Custom"] 1209 | if ptype is None or (not ptype in pllist): 1210 | if netproto == "ICMP": 1211 | ptype = "RandStr" # ICMP: A random string payload to fill out the minimum packet size 1212 | elif netproto == "UDP": 1213 | ptype = "RandStrTerm" # UDP: A random string terminated payload to fill out the minimum packet size 1214 | elif netproto == "TCP": 1215 | ptype = "Disabled" # TCP: Disabled -> The minimum packet size satisfied - no payload required 1216 | # 1217 | # Set trace interface... 1218 | if not iface is None: 1219 | mtrc._iface = iface 1220 | else: 1221 | mtrc._iface = conf.iface 1222 | # 1223 | # Set Default Gateway... 1224 | if not gw is None: 1225 | mtrc._gw = gw 1226 | # 1227 | # Set default verbosity if no override... 1228 | if verbose is None: 1229 | verbose = conf.verb 1230 | # 1231 | # Only consider ICMP error packets and TCP packets with at 1232 | # least the ACK flag set *and* either the SYN or the RST flag set... 1233 | filterundefined = False 1234 | if filter is None: 1235 | filterundefined = True 1236 | filter = "(icmp and (icmp[0]=3 or icmp[0]=4 or icmp[0]=5 or icmp[0]=11 or icmp[0]=12)) or (tcp and (tcp[13] & 0x16 > 0x10))" 1237 | # 1238 | # Resolve and expand each target... 1239 | ntraces = 0 # Total trace count 1240 | exptrg = [] # Expanded targets 1241 | for t in target: 1242 | # 1243 | # Use scapy's 'Net' function to expand target... 1244 | et = [ip for ip in iter(Net(t))] 1245 | exptrg.extend(et) 1246 | # 1247 | # Map Host Names to IP Addresses and store... 1248 | if t in mtrc._host2ip: 1249 | mtrc._host2ip[t].extend(et) 1250 | else: 1251 | mtrc._host2ip[t] = et 1252 | # 1253 | # Map IP Addresses to Host Names and store... 1254 | for a in et: 1255 | mtrc._ip2host[a] = t 1256 | # 1257 | # Store resolved and expanded targets... 1258 | mtrc._exptrg = exptrg 1259 | # 1260 | # Traceroute each expanded target value... 1261 | if l4 is None: 1262 | # 1263 | # Standard Layer: 3 ('TCP', 'UDP' or 'ICMP') tracing... 1264 | for n in range(0, nquery): # Iterate: Number of queries 1265 | for t in exptrg: # Iterate: Number of expanded targets 1266 | # 1267 | # Execute a traceroute based on network protocol setting... 1268 | if netproto == "ICMP": 1269 | # 1270 | # MTR Network Protocol: 'ICMP' 1271 | tid = 8 # Use a 'Type: 8 - Echo Request' packet for the trace: 1272 | id = 0x8888 # MTR ICMP identifier: '0x8888' 1273 | seq = IncrementalValue(start=(minttl - 2), step=1, restart=-10) # Use a Sequence number in step with TTL value 1274 | if filterundefined: 1275 | # 1276 | # Update Filter -> Allow for ICMP echo-request (8) and ICMP echo-reply (0) packet to be processed... 1277 | filter = "(icmp and (icmp[0]=8 or icmp[0]=0 or icmp[0]=3 or icmp[0]=4 or icmp[0]=5 or icmp[0]=11 or icmp[0]=12))" 1278 | # 1279 | # Check payload types: 1280 | if ptype == 'Disabled': 1281 | ip = IP(dst=[t], id=RandShort(), ttl=(minttl, maxttl)) 1282 | icmp = ICMP(type=tid, id=id, seq=seq) 1283 | ipicmp = ip / icmp 1284 | a, b = sr(ipicmp, iface=iface, timeout=timeout, filter=filter, verbose=verbose, **kargs) 1285 | else: 1286 | if ptype == 'RandStr': 1287 | # 1288 | # Use a random payload string to full out a minimum size PDU of 46 bytes for each ICMP packet: 1289 | # Length of 'IP()/ICMP()' = 28, Minimum Protocol Data Unit (PDU) is = 46 -> Therefore a 1290 | # payload of 18 octets is required. 1291 | pload = RandString(size=18) 1292 | elif ptype == 'RandStrTerm': 1293 | pload = RandStringTerm(size=17, term=b'\n') # Random string terminated 1294 | elif ptype == 'Custom': 1295 | pload = payload 1296 | # 1297 | # ICMP trace with payload... 1298 | ip = IP(dst=[t], id=RandShort(), ttl=(minttl, maxttl)) 1299 | icmp = ICMP(type=tid, id=id, seq=seq) 1300 | raw = Raw(load=pload) 1301 | ipicmpraw = ip / icmp / raw 1302 | a, b = sr(ipicmpraw, iface=iface, timeout=timeout, filter=filter, verbose=verbose, **kargs) 1303 | elif netproto == "UDP": 1304 | # 1305 | # MTR Network Protocol: 'UDP' 1306 | if filterundefined: 1307 | filter += " or udp" # Update Filter -> Allow for processing UDP packets 1308 | # 1309 | # Check payload types: 1310 | if ptype == 'Disabled': 1311 | ip = IP(dst=[t], id=RandShort(), ttl=(minttl, maxttl)) 1312 | udp = UDP(sport=sport, dport=dport) 1313 | ipudp = ip / udp 1314 | a, b = sr(ipudp, iface=iface, timeout=timeout, filter=filter, verbose=verbose, **kargs) 1315 | else: 1316 | if ptype == 'RandStr': 1317 | # 1318 | # Use a random payload string to full out a minimum size PDU of 46 bytes for each UDP packet: 1319 | # Length of 'IP()/UDP()' = 28, Minimum PDU is = 46 -> Therefore a payload of 18 octets is required. 1320 | pload = RandString(size=18) 1321 | elif ptype == 'RandStrTerm': 1322 | pload = RandStringTerm(size=17, term=b'\n') # Random string terminated 1323 | elif ptype == 'Custom': 1324 | pload = payload 1325 | # 1326 | # UDP trace with payload... 1327 | ip = IP(dst=[t], id=RandShort(), ttl=(minttl, maxttl)) 1328 | udp = UDP(sport=sport, dport=dport) 1329 | raw = Raw(load=pload) 1330 | ipudpraw = ip / udp / raw 1331 | a, b = sr(ipudpraw, iface=iface, timeout=timeout, filter=filter, verbose=verbose, **kargs) 1332 | else: 1333 | # 1334 | # Default MTR Network Protocol: 'TCP' 1335 | # 1336 | # Use some TCP options for the trace. Some firewalls will filter 1337 | # TCP/IP packets without the 'Timestamp' option set. 1338 | # 1339 | # Note: The minimum PDU size of 46 is statisfied with the use of TCP options. 1340 | # 1341 | # Use an integer encoded microsecond timestamp for the TCP option timestamp for each trace sequence. 1342 | uts = int(time.clock_gettime(time.CLOCK_REALTIME)) 1343 | opts = [('MSS', 1460), ('NOP', None), ('Timestamp', (uts, 0)), ('WScale', 7)] 1344 | seq = RandInt() # Use a start random TCP sequence number 1345 | # 1346 | # Check payload types: 1347 | if ptype == 'Disabled': 1348 | ip = IP(dst=[t], id=RandShort(), ttl=(minttl, maxttl)) 1349 | tcp = TCP(seq=seq, sport=sport, dport=dport, options=opts) 1350 | iptcp = ip / tcp 1351 | a, b = sr(iptcp, iface=iface, timeout=timeout, filter=filter, verbose=verbose, **kargs) 1352 | else: 1353 | if ptype == 'RandStr': 1354 | pload = RandString(size=32) # Use a 32 byte random string 1355 | elif ptype == 'RandStrTerm': 1356 | pload = RandStringTerm(size=32, term=b'\n') # Use a 32 byte random string terminated 1357 | elif ptype == 'Custom': 1358 | pload = payload 1359 | # 1360 | # TCP trace with payload... 1361 | ip = IP(dst=[t], id=RandShort(), ttl=(minttl, maxttl)) 1362 | tcp = TCP(seq=seq, sport=sport, dport=dport, options=opts) 1363 | raw = Raw(load=pload) 1364 | iptcpraw = ip / tcp / raw 1365 | a, b = sr(iptcpraw, iface=iface, timeout=timeout, filter=filter, verbose=verbose, **kargs) 1366 | # 1367 | # Create an 'MTracerouteResult' instance for each result packets... 1368 | trace.append(MTracerouteResult(res=a.res)) 1369 | mtrc._res.append(a) # Store Response packets 1370 | mtrc._ures.append(b) # Store Unresponse packets 1371 | if verbose: 1372 | trace[ntraces].show(ntrace=(ntraces + 1)) 1373 | print() 1374 | ntraces += 1 1375 | else: 1376 | # 1377 | # Custom Layer: 4 tracing... 1378 | filter = "ip" 1379 | for n in range(0, nquery): 1380 | for t in exptrg: 1381 | # 1382 | # Run traceroute... 1383 | a, b = sr(IP(dst=[t], id=RandShort(), ttl=(minttl, maxttl)) / l4, 1384 | iface=iface, timeout=timeout, filter=filter, verbose=verbose, **kargs) 1385 | trace.append(MTracerouteResult(res=a.res)) 1386 | mtrc._res.append(a) 1387 | mtrc._ures.append(b) 1388 | if verbose: 1389 | trace[ntraces].show(ntrace=(ntraces + 1)) 1390 | print() 1391 | ntraces += 1 1392 | # 1393 | # Store total trace run count... 1394 | mtrc._ntraces = ntraces 1395 | # 1396 | # Get the trace components... 1397 | # for n in range(0, ntraces): 1398 | for n in range(0, mtrc._ntraces): 1399 | trace[n].get_trace_components(mtrc, n) 1400 | # 1401 | # Compute any Black Holes... 1402 | mtrc.get_black_holes() 1403 | # 1404 | # Compute Trace Hop Ranges... 1405 | mtrc.compute_hop_ranges() 1406 | # 1407 | # Resolve AS Numbers... 1408 | if rasn: 1409 | mtrc.get_asns(privaddr) 1410 | # 1411 | # Try to guess ASNs for Traceroute 'Unkown Hops'... 1412 | mtrc.guess_unk_asns() 1413 | # 1414 | # Debug: Print object vars at verbose level 8... 1415 | if verbose == 8: 1416 | print("mtrc._target (User Target(s)):") 1417 | print("=======================================================") 1418 | print(mtrc._target) 1419 | print("\nmtrc._exptrg (Resolved and Expanded Target(s)):") 1420 | print("=======================================================") 1421 | print(mtrc._exptrg) 1422 | print("\nmtrc._host2ip (Target Host Name to IP Address):") 1423 | print("=======================================================") 1424 | print(mtrc._host2ip) 1425 | print("\nmtrc._ip2host (Target IP Address to Host Name):") 1426 | print("=======================================================") 1427 | print(mtrc._ip2host) 1428 | print("\nmtrc._res (Trace Response Packets):") 1429 | print("=======================================================") 1430 | print(mtrc._res) 1431 | print("\nmtrc._ures (Trace Unresponse Packets):") 1432 | print("=======================================================") 1433 | print(mtrc._ures) 1434 | print("\nmtrc._ips (Trace Unique IPv4 Addresses):") 1435 | print("=======================================================") 1436 | print(mtrc._ips) 1437 | print("\nmtrc._rt (Individual Route Traces):") 1438 | print("=======================================================") 1439 | print(mtrc._rt) 1440 | print("\nmtrc._rtt (Round Trip Times (msecs) for Trace Nodes):") 1441 | print("=======================================================") 1442 | print(mtrc._rtt) 1443 | print("\nmtrc._hops (Traceroute Hop Ranges):") 1444 | print("=======================================================") 1445 | print(mtrc._hops) 1446 | print("\nmtrc._tlblid (Target Trace Label IDs):") 1447 | print("=======================================================") 1448 | print(mtrc._tlblid) 1449 | print("\nmtrc._ports (Completed Targets & Ports):") 1450 | print("=======================================================") 1451 | print(mtrc._ports) 1452 | print("\nmtrc._portsdone (Completed Trace Routes & Ports):") 1453 | print("=======================================================") 1454 | print(mtrc._portsdone) 1455 | print("\nconf.L3socket (Layer 3 Socket Method):") 1456 | print("=======================================================") 1457 | print(conf.L3socket) 1458 | print("\nconf.AS_resolver Resolver (AS Resolver Method):") 1459 | print("=======================================================") 1460 | print(conf.AS_resolver) 1461 | print("\nmtrc._asns (AS Numbers):") 1462 | print("=======================================================") 1463 | print(mtrc._asns) 1464 | print("\nmtrc._asds (AS Descriptions):") 1465 | print("=======================================================") 1466 | print(mtrc._asds) 1467 | print("\nmtrc._unks (Unknown Hops IP Boundary for AS Numbers):") 1468 | print("=======================================================") 1469 | print(mtrc._unks) 1470 | print("\nmtrc._iface (Trace Interface):") 1471 | print("=======================================================") 1472 | print(mtrc._iface) 1473 | print("\nmtrc._gw (Trace Default Gateway IPv4 Address):") 1474 | print("=======================================================") 1475 | print(mtrc._gw) 1476 | 1477 | return mtrc 1478 | --------------------------------------------------------------------------------