diff --git a/nselib/ipOps.lua b/nselib/ipOps.lua index 2447e2a4d..d9cd6593c 100644 --- a/nselib/ipOps.lua +++ b/nselib/ipOps.lua @@ -1,46 +1,473 @@ ---- General IP Operations. --- @copyright See nmaps COPYING for licence +--- Provides functions for manipulating and comparing IP addresses. The functions reside inside the ipOps namespace. +-- @copyright See Nmap License: http://nmap.org/book/man-legal.html -module(... or "ipOps",package.seeall) +local type = type +local table = table +local string = string +local ipairs = ipairs +local tonumber = tonumber + +local stdnse = require "stdnse" + +module ( "ipOps" ) + + + +--- +-- Checks to see if the supplied IP address is part of the following non-internet-routable address spaces: +-- IPv4 Loopback (RFC3330), +-- IPv4 Private Use (RFC1918), +-- IPv4 Link Local (RFC3330), +-- IPv6 Unspecified and Loopback (RFC3513), +-- IPv6 Unique Local Unicast (RFC4193), +-- IPv6 Link Local Unicast (RFC4291) +-- @param ip String representing an IPv4 or IPv6 address. Shortened notation is permitted. +-- @usage local is_private = ipOps.isPrivate( "192.168.1.1" ) +-- @return Boolean True or False (or nil in case of an error). +-- @return Nil (or String error message in case of an error). +isPrivate = function( ip ) + + ip, err = expand_ip( ip ) + if err then return nil, err end + + local ipv4_private = { "10/8", "127/8", "169.254/16", "172.15/12", "192.168/16" } + local ipv6_private = { "::/127", "FC00::/7", "FE80::/10" } + local t, is_private = {} + if ip:match( ":" ) then + t = ipv6_private + else + t = ipv4_private + end + + for _, range in ipairs( t ) do + is_private, err = ip_in_range( ip, range ) + -- return as soon as is_private is true or err + if is_private then return true end + if err then return nil, err end + end + return false ---- Checks whether an IP address, provided as a string in dotted-quad --- notation, is part of the non-routed private IP address space, --- as described in RFC 1918. These addresses are the well-known --- 10.0.0.0/8, 192.168.0.0/16 and 172.16.0.0/12 networks. --- @param ip Dotted-Quad IP address. --- @return boolean Is private IP -isPrivate = function(ip) - local a, b - a, b = get_parts_as_number(ip) - if a == 10 then - return true - elseif a == 172 and (b>15 and b<32) then - return true - elseif a == 192 and b == 168 then - return true - end - return false end ---- Returns the IP address as DWORD value (i.e. the IP becomes --- (((a*256+b)*256+c)*256+d) ) --- @param ip Dotted-Quad IP address. --- @return IP Address as a DWORD value. -todword = function(ip) - local a, b, c, d - a,b,c,d = get_parts_as_number(ip) - return (((a*256+b))*256+c)*256+d + + +--- +-- Converts the supplied IPv4 address into a DWORD value. +-- i.e. the address becomes (((a*256+b)*256+c)*256+d). +-- Note: Currently, numbers in NSE are limited to 10^14, consequently not all IPv6 addresses can be represented in base 10. +-- @param ip String representing an IPv4 address. Shortened notation is permitted. +-- @usage local dword = ipOps.todword( "73.150.2.210" ) +-- @return Number corresponding to the supplied IP address (or nil in case of an error). +-- @return Nil (or String error message in case of an error). +todword = function( ip ) + + if type( ip ) ~= "string" or ip:match( ":" ) then + return nil, "Error in ipOps.todword: Expected IPv4 address." + end + + local n, ret = {} + n, err = get_parts_as_number( ip ) + if err then return nil, err end + + ret = (((n[1]*256+n[2]))*256+n[3])*256+n[4] + + return ret + end ---- Returns 4 numbers corresponding to the fields in dotted-quad notation. --- For example, ipOps.get_parts_as_number("192.168.1.1") returns 192,168,1,1. --- @param ip Dotted-Quad IP address. --- @return Four numbers in the IP address. -get_parts_as_number = function(ip) - local a,b,c,d = string.match(ip, "(%d+)%.(%d+)%.(%d+)%.(%d+)") - a = tonumber(a); - b = tonumber(b); - c = tonumber(c); - d = tonumber(d); - return a,b,c,d + + +--- +-- Separates the supplied IP address into its constituent parts and returns them as a table of decimal numbers. +-- (e.g. the address 139.104.32.123 becomes { 139, 104, 32, 123 } ) +-- @usage local a, b, c, d; +-- local t, err = get_parts_as_number( "139.104.32.123" ); +-- if t then a, b, c, d = unpack( t ) end +-- @param ip String representing an IPv4 or IPv6 address. Shortened notation is permitted. +-- @return Array-style Table containing decimal numbers for each part of the supplied IP address (or nil in case of an error). +-- @return Nil (or String error message in case of an error). +get_parts_as_number = function( ip ) + + ip, err = expand_ip( ip ) + if err then return nil, err end + + local pattern, base + if ip:match( ":" ) then + pattern = "%x+" + base = 16 + else + pattern = "%d+" + base = 10 + end + local t = {} + for part in string.gmatch(ip, pattern) do + t[#t+1] = tonumber( part, base ) + end + + return t + +end + + + +--- +-- Compares two IP addresses (from the same address family). +-- @param left String representing an IPv4 or IPv6 address. Shortened notation is permitted. +-- @param op A comparison operator which may be one of the following strings: "eq", "ge", "le", "gt" or "lt" (respectively ==, >=, <=, >, <). +-- @param right String representing an IPv4 or IPv6 address. Shortened notation is permitted. +-- @usage if ipOps.compare_ip( "2001::DEAD:0:0:0", "eq", "2001:0:0:0:DEAD::" ) then ... +-- @return Boolean True or False (or nil in case of an error). +-- @return Nil (or String error message in case of an error). +compare_ip = function( left, op, right ) + + if type( left ) ~= "string" or type( right ) ~= "string" then + return nil, "Error in ipOps.compare_ip: Expected IP address as a string." + end + + if ( left:match( ":" ) and not right:match( ":" ) ) or ( not left:match( ":" ) and right:match( ":" ) ) then + return nil, "Error in ipOps.compare_ip: IP addresses must be from the same address family." + end + + if op == "lt" or op == "le" then + left, right = right, left + elseif op ~= "eq" and op ~= "ge" and op ~= "gt" then + return nil, "Error in ipOps.compare_ip: Invalid Operator." + end + + local err ={} + left, err[#err+1] = ip_to_bin( left ) + right, err[#err+1] = ip_to_bin( right ) + if #err > 0 then + return nil, table.concat( err, " " ) + end + + if string.len( left ) ~= string.len( right ) then + -- shouldn't happen... + return nil, "Error in ipOps.compare_ip: Binary IP addresses were of different lengths." + end + + -- equal? + if ( op == "eq" or op == "le" or op == "ge" ) and left == right then + return true + elseif op == "eq" then + return false + end + + -- starting from the leftmost bit, subtract the bit in right from the bit in left + local compare + for i = 1, string.len( left ), 1 do + compare = tonumber( string.sub( left, i, i ) ) - tonumber( string.sub( right, i, i ) ) + if compare == 1 then + return true + elseif compare == -1 then + return false + end + end + return false + +end + + + +--- +-- Checks whether the supplied IP address is within the supplied Range of IP addresses if they belong to the same address family. +-- @param ip String representing an IPv4 or IPv6 address. Shortened notation is permitted. +-- @param range String representing a range of IPv4 or IPv6 addresses in first-last or cidr notation (e.g. "192.168.1.1 - 192.168.255.255" or "2001:0A00::/23"). +-- @usage if ipOps.ip_in_range( "192.168.1.1", "192/8" ) then ... +-- @return Boolean True or False (or nil in case of an error). +-- @return Nil (or String error message in case of an error). +ip_in_range = function( ip, range ) + + local first, last, err = get_ips_from_range( range ) + if err then return nil, err end + ip, err = expand_ip( ip ) + if err then return nil, err end + if ( ip:match( ":" ) and not first:match( ":" ) ) or ( not ip:match( ":" ) and first:match( ":" ) ) then + return nil, "Error in ipOps.ip_in_range: IP address is of a different address family to Range." + end + + err = {} + local ip_ge_first, ip_le_last + ip_ge_first, err[#err+1] = compare_ip( ip, "ge", first ) + ip_le_last, err[#err+1] = compare_ip( ip, "le", last ) + if #err > 0 then + return nil, table.concat( err, " " ) + end + + if ip_ge_first and ip_le_last then + return true + else + return false + end + +end + + + +--- +-- Expands an IP address supplied in shortened notation. +-- Serves also to check the well-formedness of an IP address. +-- Note: IPv4in6 notated addresses will be returned in pure IPv6 notation unless the IPv4 portion +-- is shortened and does not contain a dot - in which case the address will be treated as IPv6. +-- @param ip String representing an IPv4 or IPv6 address in shortened or full notation. +-- @usage local ip = ipOps.expand_ip( "2001::" ) +-- @return String representing a fully expanded IPv4 or IPv6 address (or nil in case of an error). +-- @return Nil (or String error message in case of an error). +expand_ip = function( ip ) + + if type( ip ) ~= "string" or ip == "" then + return nil, "Error in ipOps.expand_ip: Expected IP address as a string." + end + + local err4 = "Error in ipOps.expand_ip: An address assumed to be IPv4 was malformed." + + if not ip:match( ":" ) then + -- ipv4: missing octets should be "0" appended + if ip:match( "[^\.0-9]" ) then + return nil, err4 + end + local octets = {} + for octet in string.gfind( ip, "%d+" ) do + if tonumber( octet, 10 ) > 255 then return nil, err4 end + octets[#octets+1] = octet + end + if #octets > 4 then return nil, err4 end + while #octets < 4 do + octets[#octets+1] = "0" + end + return ( table.concat( octets, "." ) ) + end + + if ip:match( "[^\.:%x]" ) then + return nil, ( err4:gsub( "IPv4", "IPv6" ) ) + end + + -- preserve :: + ip = string.gsub(ip, "::", ":z:") + + -- get a table of each hexadectet + local hexadectets = {} + for hdt in string.gfind( ip, "[\.z%x]+" ) do + hexadectets[#hexadectets+1] = hdt + end + + -- deal with IPv4in6 (last hexadectet only) + local t = {} + if hexadectets[#hexadectets]:match( "[\.]+" ) then + hexadectets[#hexadectets], err = expand_ip( hexadectets[#hexadectets] ) + if err then return nil, ( err:gsub( "IPv4", "IPv4in6" ) ) end + t = stdnse.strsplit( "[\.]+", hexadectets[#hexadectets] ) + for i, v in ipairs( t ) do + t[i] = tonumber( v, 10 ) + end + hexadectets[#hexadectets] = stdnse.tohex( 256*t[1]+t[2] ) + hexadectets[#hexadectets+1] = stdnse.tohex( 256*t[3]+t[4] ) + end + + -- deal with :: and check for invalid address + local z_done = false + for index, value in ipairs( hexadectets ) do + if value:match( "[\.]+" ) then + -- shouldn't have dots at this point + return nil, ( err4:gsub( "IPv4", "IPv6" ) ) + elseif value == "z" and z_done then + -- can't have more than one :: + return nil, ( err4:gsub( "IPv4", "IPv6" ) ) + elseif value == "z" and not z_done then + z_done = true + hexadectets[index] = "0" + local bound = 8 - #hexadectets + for i = 1, bound, 1 do + table.insert( hexadectets, index+i, "0" ) + end + elseif tonumber( value, 16 ) > 65535 then + -- more than FFFF! + return nil, ( err4:gsub( "IPv4", "IPv6" ) ) + end + end + + -- make sure we have exactly 8 hexadectets + if #hexadectets > 8 then return nil, ( err4:gsub( "IPv4", "IPv6" ) ) end + while #hexadectets < 8 do + hexadectets[#hexadectets+1] = "0" + end + + return ( table.concat( hexadectets, ":" ) ) + +end + + + +--- +-- Returns the first and last IP addresses in the supplied range of addresses. +-- @param range String representing a range of IPv4 or IPv6 addresses in either cidr or first-last notation. +-- @usage first, last = ipOps.get_ips_from_range( "192.168.0.0/16" ) +-- @return String representing the first address in the supplied range (or nil in case of an error). +-- @return String representing the last address in the supplied range (or nil in case of an error). +-- @return Nil (or String error message in case of an error). +get_ips_from_range = function( range ) + + if type( range ) ~= "string" then + return nil, nil, "Error in ipOps.get_ips_from_range: Expected a range as a string." + end + + local first, last, prefix + if range:match( "/" ) then + first, prefix = range:match( "([%x%d:\.]+)/(%d+)" ) + elseif range:match( "-" ) then + first, last = range:match( "([%x%d:\.]+)%s*\-%s*([%x%d:\.]+)" ) + end + + local err = {} + if first and ( last or prefix ) then + first, err[#err+1] = expand_ip( first ) + else + return nil, nil, "Error in ipOps.get_ips_from_range: The range supplied could not be interpreted." + end + if last then + last, err[#err+1] = expand_ip( last ) + elseif first and prefix then + last, err[#err+1] = get_last_ip( first, prefix ) + end + + if first and last then + if ( first:match( ":" ) and not last:match( ":" ) ) or ( not first:match( ":" ) and last:match( ":" ) ) then + return nil, nil, "Error in ipOps.get_ips_from_range: First IP address is of a different address family to last IP address." + end + return first, last + else + return nil, nil, table.concat( err, " " ) + end + +end + + + +--- +-- Calculates the last IP address of a range of addresses given an IP address in the range and prefix length for that range. +-- @param ip String representing an IPv4 or IPv6 address. Shortened notation is permitted. +-- @param prefix Decimal number or a string representing a decimal number corresponding to a Prefix length. +-- @usage last = ipOps.get_last_ip( "192.0.0.0", 26 ) +-- @return String representing the last IP address of the range denoted by the supplied parameters (or nil in case of an error). +-- @return Nil (or String error message in case of an error). +get_last_ip = function( ip, prefix ) + + local first, err = ip_to_bin( ip ) + if err then return nil, err end + + prefix = tonumber( prefix ) + if not prefix or ( prefix < 0 ) or ( prefix > string.len( first ) ) then + return nil, "Error in ipOps.get_last_ip: Invalid prefix length." + end + + local hostbits = string.sub( first, prefix + 1 ) + hostbits = string.gsub( hostbits, "0", "1" ) + local last = string.sub( first, 1, prefix ) .. hostbits + last, err = bin_to_ip( last ) + if err then return nil, err end + return last + +end + + + +--- +-- Converts an IP address into a string representing the address as binary digits. +-- @param ip String representing an IPv4 or IPv6 address. Shortened notation is permitted. +-- @usage bit_string = ipOps.ip_to_bin( "2001::" ) +-- @return String representing the supplied IP address as 32 or 128 binary digits (or nil in case of an error). +-- @return Nil (or String error message in case of an error). +ip_to_bin = function( ip ) + + ip, err = expand_ip( ip ) + if err then return nil, err end + + local t, mask = {} + + if not ip:match( ":" ) then + -- ipv4 string + for octet in string.gfind( ip, "%d+" ) do + t[#t+1] = stdnse.tohex( octet ) + end + mask = "00" + else + -- ipv6 string + for hdt in string.gfind( ip, "%x+" ) do + t[#t+1] = hdt + end + mask = "0000" + end + + -- padding + for i, v in ipairs( t ) do + t[i] = mask:sub( 1, string.len( mask ) - string.len( v ) ) .. v + end + + return hex_to_bin( table.concat( t ) ) + +end + + + +--- +-- Converts a string representing binary digits into an IP address. +-- @param binstring String representing an IP address as 32 or 128 binary digits. +-- @usage ip = ipOps.bin_to_ip( "01111111000000000000000000000001" ) +-- @return String representing an IP address (or nil in case of an error). +-- @return Nil (or String error message in case of an error). +bin_to_ip = function( binstring ) + + if type( binstring ) ~= "string" or binstring:match( "[^01]+" ) then + return nil, "Error in ipOps.bin_to_ip: Expected string of binary digits." + end + + if string.len( binstring ) == 32 then + af = 4 + elseif string.len( binstring ) == 128 then + af = 6 + else + return nil, "Error in ipOps.bin_to_ip: Expected exactly 32 or 128 binary digits." + end + + t = {} + if af == 6 then + local pattern = string.rep( "[01]", 16 ) + for chunk in string.gfind( binstring, pattern ) do + t[#t+1] = stdnse.tohex( tonumber( chunk, 2 ) ) + end + return table.concat( t, ":" ) + end + + if af == 4 then + local pattern = string.rep( "[01]", 8 ) + for chunk in string.gfind( binstring, pattern ) do + t[#t+1] = tonumber( chunk, 2 ) .. "" + end + return table.concat( t, "." ) + end + +end + + + +--- +-- Converts a string representing a hexadecimal number into a string representing that number as binary digits. +-- Each hex digit results in four bits - this function is really just a wrapper around stdnse.tobinary(). +-- @param hex String representing a hexadecimal number. +-- @usage bin_string = ipOps.hex_to_bin( "F00D" ) +-- @return String representing the supplied number in binary digits (or nil in case of an error). +-- @return Nil (or String error message in case of an error). +hex_to_bin = function( hex ) + + if type( hex ) ~= "string" or hex == "" or hex:match( "[^%x]+" ) then + return nil, "Error in ipOps.hex_to_bin: Expected string representing a hexadecimal number." + end + + local t, mask, binchar = {}, "0000" + for hexchar in string.gfind( hex, "%x" ) do + binchar = stdnse.tobinary( tonumber( hexchar, 16 ) ) + t[#t+1] = mask:sub( 1, string.len( mask ) - string.len( binchar ) ) .. binchar + end + return table.concat( t ) + end