summaryrefslogtreecommitdiffstats
path: root/scripts/path-mtu.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/path-mtu.nse')
-rw-r--r--scripts/path-mtu.nse399
1 files changed, 399 insertions, 0 deletions
diff --git a/scripts/path-mtu.nse b/scripts/path-mtu.nse
new file mode 100644
index 0000000..6dc441c
--- /dev/null
+++ b/scripts/path-mtu.nse
@@ -0,0 +1,399 @@
+local ipOps = require "ipOps"
+local math = require "math"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Performs simple Path MTU Discovery to target hosts.
+
+TCP or UDP packets are sent to the host with the DF (don't fragment) bit set
+and with varying amounts of data. If an ICMP Fragmentation Needed is received,
+or no reply is received after retransmissions, the amount of data is lowered
+and another packet is sent. This continues until (assuming no errors occur) a
+reply from the final host is received, indicating the packet reached the host
+without being fragmented.
+
+Not all MTUs are attempted so as to not expend too much time or network
+resources. Currently the relatively short list of MTUs to try contains
+the plateau values from Table 7-1 in RFC 1191, "Path MTU Discovery".
+Using these values significantly cuts down the MTU search space. On top
+of that, this list is rarely traversed in whole because:
+* the MTU of the outgoing interface is used as a starting point, and
+* we can jump down the list when an intermediate router sending a "can't fragment" message includes its next hop MTU (as described in RFC 1191 and required by RFC 1812)
+]]
+
+---
+-- @usage
+-- nmap --script path-mtu target
+--
+-- @output
+-- Host script results:
+-- |_path-mtu: 1492 <= PMTU < 1500
+--
+-- Host script results:
+-- |_path-mtu: PMTU == 1006
+
+author = "Kris Katterjohn"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"safe", "discovery"}
+
+
+local IPPROTO_ICMP = packet.IPPROTO_ICMP
+local IPPROTO_TCP = packet.IPPROTO_TCP
+local IPPROTO_UDP = packet.IPPROTO_UDP
+
+-- Number of times to retransmit for no reply before dropping to
+-- another MTU value
+local RETRIES = 1
+
+-- RFC 1191, Table 7-1: Plateaus. Even the massive MTU values are
+-- here since we skip down the list based on the outgoing interface
+-- so its no harm.
+local MTUS = {
+ 65535,
+ 32000,
+ 17914,
+ 8166,
+ 4352,
+ 2002,
+ 1492,
+ 1006,
+ 508,
+ 296,
+ 68
+}
+
+-- Find the index in MTUS{} to use based on the MTU +new+. If +new+ is in
+-- between values in MTUS, then insert it into the table appropriately.
+local searchmtu = function(cidx, new)
+ if new == 0 then
+ return cidx
+ end
+
+ while cidx <= #MTUS do
+ if new >= MTUS[cidx] then
+ if new ~= MTUS[cidx] then
+ table.insert(MTUS, cidx, new)
+ end
+ return cidx
+ end
+ cidx = cidx + 1
+ end
+ return cidx
+end
+
+local dport = function(ip)
+ if ip.ip_p == IPPROTO_TCP then
+ return ip.tcp_dport
+ elseif ip.ip_p == IPPROTO_UDP then
+ return ip.udp_dport
+ end
+end
+
+local sport = function(ip)
+ if ip.ip_p == IPPROTO_TCP then
+ return ip.tcp_sport
+ elseif ip.ip_p == IPPROTO_UDP then
+ return ip.udp_sport
+ end
+end
+
+-- Checks how we should react to this packet
+local checkpkt = function(reply, orig)
+ local ip = packet.Packet:new(reply, reply:len())
+
+ if ip.ip_p == IPPROTO_ICMP then
+ if ip.icmp_type ~= 3 then
+ return "recap"
+ end
+ -- Port Unreachable
+ if ip.icmp_code == 3 then
+ local is = ip.buf:sub(ip.icmp_offset + 9)
+ local ip2 = packet.Packet:new(is, is:len())
+
+ -- Check sent packet against ICMP payload
+ if ip2.ip_p ~= IPPROTO_UDP or
+ ip2.ip_p ~= orig.ip_p or
+ ip2.ip_bin_src ~= orig.ip_bin_src or
+ ip2.ip_bin_dst ~= orig.ip_bin_dst or
+ sport(ip2) ~= sport(orig) or
+ dport(ip2) ~= dport(orig) then
+ return "recap"
+ end
+
+ return "gotreply"
+ end
+ -- Frag needed, DF set
+ if ip.icmp_code == 4 then
+ local val = ip:u16(ip.icmp_offset + 6)
+ return "nextmtu", val
+ end
+ return "recap"
+ end
+
+ if ip.ip_p ~= orig.ip_p or
+ ip.ip_bin_src ~= orig.ip_bin_dst or
+ ip.ip_bin_dst ~= orig.ip_bin_src or
+ dport(ip) ~= sport(orig) or
+ sport(ip) ~= dport(orig) then
+ return "recap"
+ end
+
+ return "gotreply"
+end
+
+-- This is all we can use since we can get various protocols back from
+-- different hosts
+local check = function(layer3)
+ local ip = packet.Packet:new(layer3, layer3:len())
+ return ip.ip_bin_dst
+end
+
+-- Updates a packet's info and calculates checksum
+local updatepkt = function(ip)
+ if ip.ip_p == IPPROTO_TCP then
+ ip:tcp_set_sport(math.random(0x401, 0xffff))
+ ip:tcp_set_seq(math.random(1, 0x7fffffff))
+ ip:tcp_count_checksum()
+ elseif ip.ip_p == IPPROTO_UDP then
+ ip:udp_set_sport(math.random(0x401, 0xffff))
+ ip:udp_set_length(ip.ip_len - ip.ip_hl * 4)
+ ip:udp_count_checksum()
+ end
+ ip:ip_count_checksum()
+end
+
+-- Set up packet header and data to satisfy a certain MTU
+local setmtu = function(pkt, mtu)
+ if pkt.ip_len < mtu then
+ pkt.buf = pkt.buf .. string.rep("\0", mtu - pkt.ip_len)
+ else
+ pkt.buf = pkt.buf:sub(1, mtu)
+ end
+
+ pkt:ip_set_len(mtu)
+ pkt.packet_length = mtu
+ updatepkt(pkt)
+end
+
+local basepkt = function(proto)
+ local ibin = stdnse.fromhex(
+ "4500 0014 0000 4000 8000 0000 0000 0000 0000 0000"
+ )
+ local tbin = stdnse.fromhex(
+ "0000 0000 0000 0000 0000 0000 6002 0c00 0000 0000 0204 05b4"
+ )
+ local ubin = stdnse.fromhex(
+ "0000 0000 0800 0000"
+ )
+
+ if proto == IPPROTO_TCP then
+ return ibin .. tbin
+ elseif proto == IPPROTO_UDP then
+ return ibin .. ubin
+ end
+end
+
+-- Creates a Packet object for the given proto and port
+local genericpkt = function(host, proto, port)
+ local pkt = basepkt(proto)
+ local ip = packet.Packet:new(pkt, pkt:len())
+
+ ip:ip_set_bin_src(host.bin_ip_src)
+ ip:ip_set_bin_dst(host.bin_ip)
+
+ ip:set_u8(ip.ip_offset + 9, proto)
+ ip.ip_p = proto
+
+ ip:ip_set_len(pkt:len())
+
+ if proto == IPPROTO_TCP then
+ ip:tcp_parse(false)
+ ip:tcp_set_dport(port)
+ elseif proto == IPPROTO_UDP then
+ ip:udp_parse(false)
+ ip:udp_set_dport(port)
+ end
+
+ updatepkt(ip)
+
+ return ip
+end
+
+local ipproto = function(p)
+ if p == "tcp" then
+ return IPPROTO_TCP
+ elseif p == "udp" then
+ return IPPROTO_UDP
+ end
+ return -1
+end
+
+-- Determines how to probe
+local getprobe = function(host)
+ local combos = {
+ { "tcp", "open" },
+ { "tcp", "closed" },
+ -- udp/open probably only happens when Nmap sends proper
+ -- payloads, which doesn't happen in here
+ { "udp", "closed" }
+ }
+ local proto = nil
+ local port = nil
+
+ for _, c in ipairs(combos) do
+ port = nmap.get_ports(host, nil, c[1], c[2])
+ if port then
+ proto = c[1]
+ break
+ end
+ end
+
+ return proto, port
+end
+
+-- Sets necessary probe data in registry
+local setreg = function(host, proto, port)
+ host.registry['pathmtuprobe'] = {
+ ['proto'] = proto,
+ ['port'] = port
+ }
+end
+
+hostrule = function(host)
+ if not nmap.is_privileged() then
+ nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {}
+ if not nmap.registry[SCRIPT_NAME].rootfail then
+ stdnse.verbose1("not running for lack of privileges.")
+ end
+ nmap.registry[SCRIPT_NAME].rootfail = true
+ return nil
+ end
+
+ if nmap.address_family() ~= 'inet' then
+ stdnse.debug1("is IPv4 compatible only.")
+ return false
+ end
+ if not (host.interface and host.interface_mtu) then
+ return false
+ end
+ local proto, port = getprobe(host)
+ if not (proto and port) then
+ return false
+ end
+ setreg(host, proto, port.number)
+ return true
+end
+
+action = function(host)
+ local m, r
+ local gotit = false
+ local mtuset
+ local sock = nmap.new_dnet()
+ local pcap = nmap.new_socket()
+ local proto = host.registry['pathmtuprobe']['proto']
+ local port = host.registry['pathmtuprobe']['port']
+ local saddr = ipOps.str_to_ip(host.bin_ip_src)
+ local daddr = ipOps.str_to_ip(host.bin_ip)
+ local try = nmap.new_try()
+ local status, pkt, ip
+
+ try(sock:ip_open())
+
+ try = nmap.new_try(function() sock:ip_close() end)
+
+ pcap:pcap_open(host.interface, 104, false, "dst host " .. saddr .. " and (icmp or (" .. proto .. " and src host " .. daddr .. " and src port " .. port .. "))")
+
+ -- Since we're sending potentially large amounts of data per packet,
+ -- simply bump up the host's calculated timeout value. Most replies
+ -- should come from routers along the path, fragmentation reassembly
+ -- times isn't an issue and the large amount of data is only traveling
+ -- in one direction; still, we want a response from the target so call
+ -- it 1.5*timeout to play it safer.
+ pcap:set_timeout(1.5 * host.times.timeout * 1000)
+
+ m = searchmtu(1, host.interface_mtu)
+
+ mtuset = MTUS[m]
+
+ local pkt = genericpkt(host, ipproto(proto), port)
+
+ while m <= #MTUS do
+ setmtu(pkt, MTUS[m])
+
+ r = 0
+ status = false
+ while true do
+ if not status then
+ if not sock:ip_send(pkt.buf, host) then
+ -- Got a send error, perhaps EMSGSIZE
+ -- when we don't know our interface's
+ -- MTU. Drop an MTU and keep trying.
+ break
+ end
+ end
+
+ local test = pkt.ip_bin_src
+ local status, length, _, layer3 = pcap:pcap_receive()
+ while status and test ~= check(layer3) do
+ status, length, _, layer3 = pcap:pcap_receive()
+ end
+
+ if status then
+ local t, v = checkpkt(layer3, pkt)
+ if t == "gotreply" then
+ gotit = true
+ break
+ elseif t == "recap" then
+ elseif t == "nextmtu" then
+ if v == 0 then
+ -- Router didn't send its
+ -- next-hop MTU. Just drop
+ -- a level.
+ break
+ end
+ -- Lua's lack of a continue statement
+ -- for loop control sucks, so dec m
+ -- here as it's inc'd below. Ugh.
+ m = searchmtu(m, v) - 1
+ mtuset = v
+ break
+ end
+ else
+ if r >= RETRIES then
+ break
+ end
+ r = r + 1
+ end
+ end
+
+ if gotit then
+ break
+ end
+
+ m = m + 1
+ end
+
+ pcap:close()
+ sock:ip_close()
+
+ if not gotit then
+ if nmap.debugging() > 0 then
+ return "Error: Unable to determine PMTU (no replies)"
+ end
+ return
+ end
+
+ if MTUS[m] == mtuset then
+ return "PMTU == " .. MTUS[m]
+ elseif m == 1 then
+ return "PMTU >= " .. MTUS[m]
+ else
+ return "" .. MTUS[m] .. " <= PMTU < " .. MTUS[m - 1]
+ end
+end