mirror of
https://github.com/nmap/nmap.git
synced 2025-12-06 04:31:29 +00:00
from a supplied range, not simply copying over the address used to specify the range. Specifically in case of CIDR notation, the supplied address may be any address in the range, such as "192.168.1.10/24". Closes #1285
876 lines
29 KiB
Lua
876 lines
29 KiB
Lua
---
|
|
-- Utility functions for manipulating and comparing IP addresses.
|
|
--
|
|
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
|
|
|
|
local math = require "math"
|
|
local stdnse = require "stdnse"
|
|
local string = require "string"
|
|
local table = require "table"
|
|
local type = type
|
|
local ipairs = ipairs
|
|
local tonumber = tonumber
|
|
local unittest = require "unittest"
|
|
|
|
|
|
_ENV = stdnse.module("ipOps", stdnse.seeall)
|
|
|
|
local pack = string.pack
|
|
local unpack = string.unpack
|
|
|
|
---
|
|
-- Checks to see if the supplied IP address is part of a non-routable
|
|
-- address space.
|
|
--
|
|
-- The non-Internet-routable address spaces known to this function are
|
|
-- * IPv4 Loopback (RFC3330)
|
|
-- * IPv4 Private Use (RFC1918)
|
|
-- * IPv4 Link Local (RFC3330)
|
|
-- * IPv4 IETF Protocol Assignments (RFC 5736)
|
|
-- * IPv4 TEST-NET-1, TEST-NET-2, TEST-NET-3 (RFC 5737)
|
|
-- * IPv4 Network Interconnect Device Benchmark Testing (RFC 2544)
|
|
-- * IPv4 Reserved for Future Use (RFC 1112, Section 4)
|
|
-- * IPv4 Multicast Local Network Control Block (RFC 3171, Section 3)
|
|
-- * IPv6 Unspecified and Loopback (RFC3513)
|
|
-- * IPv6 Site-Local (RFC3513, deprecated in RFC3879)
|
|
-- * 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 True or false (or <code>nil</code> in case of an error).
|
|
-- @return String error message in case of an error or
|
|
-- String non-routable address containing the supplied IP address.
|
|
isPrivate = function( ip )
|
|
local err
|
|
|
|
ip, err = expand_ip( ip )
|
|
if err then return nil, err end
|
|
|
|
if ip:match( ":" ) then
|
|
|
|
local is_private
|
|
local ipv6_private = { "::/127", "FC00::/7", "FE80::/10", "FEC0::/10" }
|
|
|
|
for _, range in ipairs( ipv6_private ) do
|
|
is_private, err = ip_in_range( ip, range )
|
|
if is_private == true then
|
|
return true, range
|
|
end
|
|
if err then
|
|
return nil, err
|
|
end
|
|
end
|
|
|
|
elseif ip:sub(1,3) == '10.' then
|
|
|
|
return true, '10/8'
|
|
|
|
elseif ip:sub(1,4) == '127.' then
|
|
|
|
return true, '127/8'
|
|
|
|
elseif ip:sub(1,8) == '169.254.' then
|
|
|
|
return true, '169.254/16'
|
|
|
|
elseif ip:sub(1,4) == '172.' then
|
|
|
|
local p, e = ip_in_range(ip, '172.16/12')
|
|
if p == true then
|
|
return true, '172.16/12'
|
|
else
|
|
return p, e
|
|
end
|
|
|
|
elseif ip:sub(1,4) == '192.' then
|
|
|
|
if ip:sub(5,8) == '168.' then
|
|
return true, '192.168/16'
|
|
elseif ip:match('^192%.[0][0]?[0]?%.[0][0]?[0]?%.') then
|
|
return true, '192.0.0/24'
|
|
elseif ip:match('^192%.[0][0]?[0]?%.[0]?[0]?2') then
|
|
return true, '192.0.2/24'
|
|
end
|
|
|
|
elseif ip:sub(1,4) == '198.' then
|
|
|
|
if ip:match('^198%.[0]?18%.') or ip:match('^198%.[0]?19%.') then
|
|
return true, '198.18/15'
|
|
elseif ip:match('^198%.[0]?51%.100%.') then
|
|
return true, '198.51.100/24'
|
|
end
|
|
|
|
elseif ip:match('^203%.[0][0]?[0]?%.113%.') then
|
|
|
|
return true, '203.0.113/24'
|
|
|
|
elseif ip:match('^224%.[0][0]?[0]?%.[0][0]?[0]?%.') then
|
|
|
|
return true, '224.0.0/24'
|
|
|
|
elseif ip:match('^24[0-9]%.') or ip:match('^25[0-5]%.') then
|
|
|
|
return true, '240.0.0/4'
|
|
|
|
end
|
|
|
|
return false, nil
|
|
|
|
end
|
|
|
|
|
|
|
|
---
|
|
-- Converts the supplied IPv4 address into a DWORD value.
|
|
--
|
|
-- For example, the address a.b.c.d becomes (((a*256+b)*256+c)*256+d).
|
|
--
|
|
-- Note: IPv6 addresses are not supported. Currently, numbers in NSE are
|
|
-- limited to 10^14, and consequently not all IPv6 addresses can be
|
|
-- represented. Consider using <code>ip_to_str</code> for IPv6 addresses.
|
|
-- @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 <code>nil</code>
|
|
-- in case of an error).
|
|
-- @return 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, err = {}
|
|
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
|
|
|
|
---
|
|
-- Converts the supplied IPv4 address from a DWORD value into a dotted string.
|
|
--
|
|
-- For example, the address (((a*256+b)*256+c)*256+d) becomes a.b.c.d.
|
|
--
|
|
--@param ip DWORD representing an IPv4 address.
|
|
--@return The string representing the address.
|
|
fromdword = function( ip )
|
|
if type( ip ) ~= "number" then
|
|
stdnse.debug1("Error in ipOps.fromdword: Expected 32-bit number.")
|
|
return nil
|
|
end
|
|
return string.format("%d.%d.%d.%d", unpack("BBBB", pack(">I4", ip)))
|
|
end
|
|
|
|
---
|
|
-- Separates the supplied IP address into its constituent parts and
|
|
-- returns them as a table of numbers.
|
|
--
|
|
-- For example, the address 139.104.32.123 becomes { 139, 104, 32, 123 }.
|
|
-- @usage
|
|
-- local a, b, c, d;
|
|
-- local t, err = ipOps.get_parts_as_number( "139.104.32.123" )
|
|
-- if t then a, b, c, d = table.unpack( t ) end
|
|
-- @param ip String representing an IPv4 or IPv6 address. Shortened notation
|
|
-- is permitted.
|
|
-- @return Array of numbers for each part of the supplied IP address (or
|
|
-- <code>nil</code> in case of an error).
|
|
-- @return String error message in case of an error.
|
|
get_parts_as_number = function( ip )
|
|
local err
|
|
|
|
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.
|
|
--
|
|
-- When comparing addresses from different families,
|
|
-- IPv4 addresses will sort before IPv6 addresses.
|
|
-- @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:
|
|
-- <code>"eq"</code>, <code>"ge"</code>, <code>"le"</code>,
|
|
-- <code>"gt"</code> or <code>"lt"</code> (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
|
|
-- ...
|
|
-- end
|
|
-- @return True or false (or <code>nil</code> in case of an error).
|
|
-- @return 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
|
|
|
|
local err ={}
|
|
left, err[#err+1] = ip_to_str( left )
|
|
right, err[#err+1] = ip_to_str( right )
|
|
if #err > 0 then
|
|
return nil, table.concat( err, " " )
|
|
end
|
|
|
|
-- by prepending the length, IPv4 (length 4) sorts before IPv6 (length 16)
|
|
left = pack("s1", left)
|
|
right = pack("s1", right)
|
|
|
|
if ( op == "eq" ) then
|
|
return ( left == right )
|
|
elseif ( op == "ne" ) then
|
|
return ( left ~= right )
|
|
elseif ( op == "le" ) then
|
|
return ( left <= right )
|
|
elseif ( op == "ge" ) then
|
|
return ( left >= right )
|
|
elseif ( op == "lt" ) then
|
|
return ( left < right )
|
|
elseif ( op == "gt" ) then
|
|
return ( left > right )
|
|
end
|
|
|
|
return nil, "Error in ipOps.compare_ip: Invalid Operator."
|
|
end
|
|
|
|
--- Sorts a table of IP addresses
|
|
--
|
|
-- An in-place sort using <code>table.sort</code> to sort by IP address.
|
|
-- Sorting non-IP addresses is likely to result in a bad sort and possibly an infinite loop.
|
|
--
|
|
-- @param ips The table of IP addresses to sort
|
|
-- @param op The comparison operation to use. Default: "lt" (ascending)
|
|
ip_sort = function (ips, op)
|
|
op = op or "lt"
|
|
return table.sort(ips, function(a, b) return compare_ip(a, op, b) end)
|
|
end
|
|
|
|
---
|
|
-- Checks whether the supplied IP address is within the supplied range of IP
|
|
-- addresses.
|
|
--
|
|
-- The address and the range must both 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.
|
|
-- <code>"192.168.1.1 - 192.168.255.255"</code> or
|
|
-- <code>"2001:0A00::/23"</code>).
|
|
-- @usage
|
|
-- if ipOps.ip_in_range( "192.168.1.1", "192/8" ) then ... end
|
|
-- @return True or false (or <code>nil</code> in case of an error).
|
|
-- @return 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.
|
|
-- @param family String representing the address family to expand to. Only
|
|
-- affects IPv4 addresses when "inet6" is provided, causing the function to
|
|
-- return an IPv4-mapped IPv6 address.
|
|
-- @usage
|
|
-- local ip = ipOps.expand_ip( "2001::" )
|
|
-- @return String representing a fully expanded IPv4 or IPv6 address (or
|
|
-- <code>nil</code> in case of an error).
|
|
-- @return String error message in case of an error.
|
|
expand_ip = function( ip, family )
|
|
local err
|
|
|
|
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.gmatch( 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
|
|
if family == "inet6" then
|
|
return ( table.concat( { 0,0,0,0,0,"ffff",
|
|
stdnse.tohex( 256*octets[1]+octets[2] ),
|
|
stdnse.tohex( 256*octets[3]+octets[4] )
|
|
}, ":" ) )
|
|
else
|
|
return ( table.concat( octets, "." ) )
|
|
end
|
|
end
|
|
|
|
if family ~= nil and family ~= "inet6" then
|
|
return nil, "Error in ipOps.expand_ip: Cannot convert IPv6 address to IPv4"
|
|
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.gmatch( 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
|
|
-- <code>nil</code> in case of an error).
|
|
-- @return String representing the last address in the supplied range (or
|
|
-- <code>nil</code> in case of an error).
|
|
-- @return 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 ip, prefix = range:match("^%s*([%x:.]+)/(%d+)%s*$")
|
|
if ip then
|
|
return get_first_last_ip(ip, prefix)
|
|
end
|
|
local first, last = range:match("^%s*([%x:.]+)%s*%-%s*([%x:.]+)%s*$")
|
|
if not first then
|
|
return nil, nil, "Error in ipOps.get_ips_from_range: The range supplied could not be interpreted."
|
|
end
|
|
|
|
local err
|
|
first, err = expand_ip(first)
|
|
if not err then
|
|
last, err = expand_ip(last)
|
|
end
|
|
if not err then
|
|
local af = function (ip) return ip:find(":") and 6 or 4 end
|
|
if af(first) ~= af(last) then
|
|
err = "Error in ipOps.get_ips_from_range: First IP address is of a different address family to last IP address."
|
|
end
|
|
end
|
|
if err then
|
|
return nil, nil, err
|
|
end
|
|
return first, last
|
|
end
|
|
|
|
---
|
|
-- Calculates the first and last IP addresses of a range of addresses,
|
|
-- given an IP address in the range and a prefix length for that range
|
|
-- @param ip String representing an IPv4 or IPv6 address. Shortened notation
|
|
-- is permitted.
|
|
-- @param prefix Number or a string representing a decimal number corresponding
|
|
-- to a prefix length.
|
|
-- @usage
|
|
-- first, last = ipOps.get_first_last_ip( "192.0.0.0", 26)
|
|
-- @return String representing the first IP address of the range denoted by
|
|
-- the supplied parameters (or <code>nil</code> in case of an error).
|
|
-- @return String representing the last IP address of the range denoted by
|
|
-- the supplied parameters (or <code>nil</code> in case of an error).
|
|
-- @return String error message in case of an error.
|
|
get_first_last_ip = function(ip, prefix)
|
|
local err
|
|
ip, err = ip_to_bin(ip)
|
|
if err then return nil, nil, err end
|
|
|
|
prefix = tonumber(prefix)
|
|
if not prefix or prefix ~= math.floor(prefix) or prefix < 0 or prefix > #ip then
|
|
return nil, nil, "Error in ipOps.get_first_last_ip: Invalid prefix."
|
|
end
|
|
|
|
local net = ip:sub(1, prefix)
|
|
local first = bin_to_ip(net .. ("0"):rep(#ip - prefix))
|
|
local last = bin_to_ip(net .. ("1"):rep(#ip - prefix))
|
|
return first, last
|
|
end
|
|
|
|
---
|
|
-- Calculates the first 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 Number or a string representing a decimal number corresponding
|
|
-- to a prefix length.
|
|
-- @usage
|
|
-- first = ipOps.get_first_ip( "192.0.0.0", 26 )
|
|
-- @return String representing the first IP address of the range denoted by the
|
|
-- supplied parameters (or <code>nil</code> in case of an error).
|
|
-- @return String error message in case of an error.
|
|
get_first_ip = function(ip, prefix)
|
|
local first, last, err = get_first_last_ip(ip, prefix)
|
|
return first, err
|
|
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 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 <code>nil</code> in case of an error).
|
|
-- @return String error message in case of an error.
|
|
get_last_ip = function(ip, prefix)
|
|
local first, last, err = get_first_last_ip(ip, prefix)
|
|
return last, err
|
|
end
|
|
|
|
---
|
|
-- Converts an IP address into an opaque string (big-endian)
|
|
-- @param ip String representing an IPv4 or IPv6 address.
|
|
-- @param family (optional) Address family to convert to. "ipv6" converts IPv4
|
|
-- addresses to IPv4-mapped IPv6.
|
|
-- @usage
|
|
-- opaque = ipOps.ip_to_str( "192.168.3.4" )
|
|
-- @return 4- or 16-byte string representing IP address (or <code>nil</code>
|
|
-- in case of an error).
|
|
-- @return String error message in case of an error
|
|
ip_to_str = function( ip, family )
|
|
local err
|
|
|
|
ip, err = expand_ip( ip, family )
|
|
if err then return nil, err end
|
|
|
|
local t = {}
|
|
|
|
if not ip:match( ":" ) then
|
|
-- ipv4 string
|
|
for octet in string.gmatch( ip, "%d+" ) do
|
|
t[#t+1] = pack("B", octet)
|
|
end
|
|
else
|
|
-- ipv6 string
|
|
for hdt in string.gmatch( ip, "%x+" ) do
|
|
t[#t+1] = pack( ">I2", tonumber(hdt, 16) )
|
|
end
|
|
end
|
|
|
|
|
|
return table.concat( t )
|
|
end
|
|
|
|
---
|
|
-- Converts an opaque string (big-endian) into an IP address
|
|
--
|
|
-- @param ip Opaque string representing an IP address. If length 4, then IPv4
|
|
-- is assumed. If length 16, then IPv6 is assumed.
|
|
-- @return IP address in readable notation (or <code>nil</code> in case of an
|
|
-- error)
|
|
-- @return String error message in case of an error
|
|
str_to_ip = function (ip)
|
|
if #ip == 4 then
|
|
return ("%d.%d.%d.%d"):format(unpack("BBBB", ip))
|
|
elseif #ip == 16 then
|
|
local full = ("%x:%x:%x:%x:%x:%x:%x:%x"):format(unpack((">I2"):rep(8), ip))
|
|
full = full:gsub(":[:0]+", "::", 1) -- Collapse the first (should be longest?) series of :0:
|
|
full = full:gsub("^0::", "::", 1) -- handle special case of ::1
|
|
return full
|
|
else
|
|
return nil, "Invalid length"
|
|
end
|
|
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 <code>nil</code> in case of an error).
|
|
-- @return String error message in case of an error.
|
|
ip_to_bin = function( ip )
|
|
local err
|
|
|
|
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.gmatch( ip, "%d+" ) do
|
|
t[#t+1] = stdnse.tohex( tonumber(octet) )
|
|
end
|
|
mask = "00"
|
|
else
|
|
-- ipv6 string
|
|
for hdt in string.gmatch( ip, "%x+" ) do
|
|
t[#t+1] = hdt
|
|
end
|
|
mask = "0000"
|
|
end
|
|
|
|
-- padding
|
|
for i, v in ipairs( t ) do
|
|
t[i] = mask:sub( 1, # mask - # v ) .. v
|
|
end
|
|
|
|
return hex_to_bin( table.concat( t ) )
|
|
|
|
end
|
|
|
|
|
|
|
|
---
|
|
-- Converts a string of 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 <code>nil</code> in
|
|
-- case of an error).
|
|
-- @return 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
|
|
|
|
local af
|
|
if # binstring == 32 then
|
|
af = 4
|
|
elseif # binstring == 128 then
|
|
af = 6
|
|
else
|
|
return nil, "Error in ipOps.bin_to_ip: Expected exactly 32 or 128 binary digits."
|
|
end
|
|
|
|
local t = {}
|
|
if af == 6 then
|
|
local pattern = string.rep( "[01]", 16 )
|
|
for chunk in string.gmatch( 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.gmatch( binstring, pattern ) do
|
|
t[#t+1] = tonumber( chunk, 2 ) .. ""
|
|
end
|
|
return table.concat( t, "." )
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local bin_lookup = {
|
|
[0]="0000", "0001", "0010", "0011", "0100", "0101", "0110", "0111",
|
|
"1000", "1001", "1010", "1011", "1100", "1101", "1110", "1111",
|
|
}
|
|
---
|
|
-- Converts a string of hexadecimal digits into the corresponding string of
|
|
-- binary digits.
|
|
--
|
|
-- Each hex digit results in four bits.
|
|
-- @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
|
|
-- <code>nil</code> in case of an error).
|
|
-- @return String error message in case of an error.
|
|
hex_to_bin = function( hex )
|
|
if type( hex ) ~= "string" then
|
|
return nil, "Error in ipOps.hex_to_bin: Expected string"
|
|
end
|
|
|
|
local status, result = pcall( string.gsub, hex, ".", function(nibble)
|
|
local n = bin_lookup[tonumber(nibble, 16)]
|
|
if n then
|
|
return n
|
|
else
|
|
error("Error in ipOps.hex_to_bin: Expected string representing a hexadecimal number.")
|
|
end
|
|
end)
|
|
if status then
|
|
return result
|
|
end
|
|
return status, result
|
|
end
|
|
|
|
---
|
|
-- Convert a CIDR subnet mask to dotted decimal notation.
|
|
--
|
|
-- @param subnet CIDR string representing the subnet mask.
|
|
-- @usage
|
|
-- local netmask = ipOps.cidr_to_subnet( "/16" )
|
|
-- @return Dotted decimal representation of the suppliet subnet mask (e.g. "255.255.0.0")
|
|
cidr_to_subnet = function( subnet )
|
|
local bits = subnet:match("/(%d%d)$")
|
|
if not bits then return nil end
|
|
return fromdword((0xFFFFFFFF >> tonumber(bits)) ~ 0xFFFFFFFF)
|
|
end
|
|
|
|
---
|
|
-- Convert a dotted decimal subnet mask to CIDR notation.
|
|
--
|
|
-- @param subnet Dotted decimal string representing the subnet mask.
|
|
-- @usage
|
|
-- local cidr = ipOps.subnet_to_cidr( "255.255.0.0" )
|
|
-- @return CIDR representation of the supplied subnet mask (e.g. "/16").
|
|
subnet_to_cidr = function( subnet )
|
|
local dword, err = todword(subnet)
|
|
if not dword then return nil, err end
|
|
return "/" .. tostring(32 - (math.tointeger(math.log((dword ~ 0xFFFFFFFF) + 1, 2))))
|
|
end
|
|
|
|
--Ignore the rest if we are not testing.
|
|
if not unittest.testing() then
|
|
return _ENV
|
|
end
|
|
|
|
test_suite = unittest.TestSuite:new()
|
|
test_suite:add_test(unittest.is_true(isPrivate("192.168.123.123")), "192.168.123.123 is private")
|
|
test_suite:add_test(unittest.is_false(isPrivate("1.1.1.1")), "1.1.1.1 is not private")
|
|
test_suite:add_test(unittest.equal(todword("65.66.67.68"),0x41424344), "todword")
|
|
test_suite:add_test(unittest.equal(todword("127.0.0.1"),0x7f000001), "todword")
|
|
test_suite:add_test(unittest.equal(fromdword(0xffffffff),"255.255.255.255"), "fromdword")
|
|
test_suite:add_test(unittest.equal(fromdword(0x7f000001),"127.0.0.1"), "fromdword")
|
|
test_suite:add_test(unittest.equal(str_to_ip("\x01\x02\x03\x04"),"1.2.3.4"), "str_to_ip (ipv4)")
|
|
test_suite:add_test(unittest.equal(str_to_ip("\0\x01\xbe\xef\0\0\0\0\0\0\x02\x03\0\0\0\x01"),"1:beef::203:0:1"), "str_to_ip (ipv6)")
|
|
test_suite:add_test(unittest.equal(str_to_ip(("\0"):rep(15) .. "\x01"),"::1"), "str_to_ip (ipv6)")
|
|
test_suite:add_test(function()
|
|
local parts, err = get_parts_as_number("8.255.0.1")
|
|
if parts == nil then return false, err end
|
|
if parts[1] == 8 and parts[2] == 255 and parts[3] == 0 and parts[4] == 1 then
|
|
return true
|
|
end
|
|
return false, string.format("Expected {8, 255, 0, 1}, got {%d, %d, %d, %d}", table.unpack(parts))
|
|
end, "get_parts_as_number")
|
|
|
|
do
|
|
local low_ip4 = "192.168.1.10"
|
|
local high_ip4 = "192.168.10.1"
|
|
local low_ip6 = "2001::DEAD:0:0:9"
|
|
local high_ip6 = "2001::DEAF:0:0:9"
|
|
for _, op in ipairs({
|
|
{low_ip4, "eq", low_ip4, unittest.is_true, "IPv4"},
|
|
{low_ip6, "eq", low_ip6, unittest.is_true, "IPv6"},
|
|
{high_ip4, "eq", low_ip4, unittest.is_false, "IPv4"},
|
|
{high_ip6, "eq", low_ip6, unittest.is_false, "IPv6"},
|
|
{low_ip4, "eq", low_ip6, unittest.is_false, "mixed"},
|
|
{low_ip4, "ne", low_ip4, unittest.is_false, "IPv4"},
|
|
{low_ip6, "ne", low_ip6, unittest.is_false, "IPv6"},
|
|
{high_ip4, "ne", low_ip4, unittest.is_true, "IPv4"},
|
|
{high_ip6, "ne", low_ip6, unittest.is_true, "IPv6"},
|
|
{low_ip4, "ne", low_ip6, unittest.is_true, "mixed"},
|
|
{low_ip4, "ge", low_ip4, unittest.is_true, "IPv4, equal"},
|
|
{low_ip6, "ge", low_ip6, unittest.is_true, "IPv6, equal"},
|
|
{high_ip4, "ge", low_ip4, unittest.is_true, "IPv4"},
|
|
{high_ip6, "ge", low_ip6, unittest.is_true, "IPv6"},
|
|
{low_ip4, "ge", high_ip4, unittest.is_false, "IPv4"},
|
|
{low_ip6, "ge", high_ip6, unittest.is_false, "IPv6"},
|
|
{low_ip6, "ge", low_ip4, unittest.is_true, "mixed"},
|
|
{low_ip4, "ge", low_ip6, unittest.is_false, "mixed"},
|
|
{low_ip4, "le", low_ip4, unittest.is_true, "IPv4, equal"},
|
|
{low_ip6, "le", low_ip6, unittest.is_true, "IPv6, equal"},
|
|
{high_ip4, "le", low_ip4, unittest.is_false, "IPv4"},
|
|
{high_ip6, "le", low_ip6, unittest.is_false, "IPv6"},
|
|
{low_ip4, "le", high_ip4, unittest.is_true, "IPv4"},
|
|
{low_ip6, "le", high_ip6, unittest.is_true, "IPv6"},
|
|
{low_ip6, "le", low_ip4, unittest.is_false, "mixed"},
|
|
{low_ip4, "le", low_ip6, unittest.is_true, "mixed"},
|
|
{low_ip4, "gt", low_ip4, unittest.is_false, "IPv4, equal"},
|
|
{low_ip6, "gt", low_ip6, unittest.is_false, "IPv6, equal"},
|
|
{high_ip4, "gt", low_ip4, unittest.is_true, "IPv4"},
|
|
{high_ip6, "gt", low_ip6, unittest.is_true, "IPv6"},
|
|
{low_ip4, "gt", high_ip4, unittest.is_false, "IPv4"},
|
|
{low_ip6, "gt", high_ip6, unittest.is_false, "IPv6"},
|
|
{low_ip6, "gt", low_ip4, unittest.is_true, "mixed"},
|
|
{low_ip4, "gt", low_ip6, unittest.is_false, "mixed"},
|
|
{low_ip4, "lt", low_ip4, unittest.is_false, "IPv4, equal"},
|
|
{low_ip6, "lt", low_ip6, unittest.is_false, "IPv6, equal"},
|
|
{high_ip4, "lt", low_ip4, unittest.is_false, "IPv4"},
|
|
{high_ip6, "lt", low_ip6, unittest.is_false, "IPv6"},
|
|
{low_ip4, "lt", high_ip4, unittest.is_true, "IPv4"},
|
|
{low_ip6, "lt", high_ip6, unittest.is_true, "IPv6"},
|
|
{low_ip6, "lt", low_ip4, unittest.is_false, "mixed"},
|
|
{low_ip4, "lt", low_ip6, unittest.is_true, "mixed"},
|
|
}) do
|
|
test_suite:add_test(op[4](compare_ip(op[1], op[2], op[3])),
|
|
string.format("compare_ip(%s, %s, %s) (%s)", op[1], op[2], op[3], op[5]))
|
|
end
|
|
end
|
|
|
|
do
|
|
for _, h in ipairs({
|
|
{"a", "1010"},
|
|
{"aa", "10101010"},
|
|
{"12", "00010010"},
|
|
{"54321", "01010100001100100001"},
|
|
{"123error", false},
|
|
{"", ""},
|
|
{"bad 123", false},
|
|
}) do
|
|
test_suite:add_test(unittest.equal(hex_to_bin(h[1]), h[2]))
|
|
end
|
|
end
|
|
|
|
do
|
|
for _, op in ipairs({
|
|
{"192.168.13.1", "192/8", unittest.is_true, "IPv4 CIDR"},
|
|
{"193.168.13.1", "192/8", unittest.is_false, "IPv4 CIDR"},
|
|
{"192.168.13.0", "192.168.13.128/24", unittest.is_true, "IPv4 CIDR"},
|
|
{"193.168.13.0", "192.168.13.128/24", unittest.is_false, "IPv4 CIDR"},
|
|
{"2001:db8::9", "2001:db8/32", unittest.is_true, "IPv6 CIDR"},
|
|
{"2001:db7::9", "2001:db8/32", unittest.is_false, "IPv6 CIDR"},
|
|
{"2001:db8::9", "2001:db8::1:0/32", unittest.is_true, "IPv6 CIDR"},
|
|
{"2001:db7::9", "2001:db8::1:0/32", unittest.is_false, "IPv6 CIDR"},
|
|
{"192.168.13.1", "192.168.10.33-192.168.80.80", unittest.is_true, "IPv4 range"},
|
|
{"193.168.13.1", "192.168.1.1 - 192.168.5.0", unittest.is_false, "IPv4 range"},
|
|
{"2001:db8::9", "2001:db8::1-2001:db8:1::1", unittest.is_true, "IPv6 range"},
|
|
{"2001:db8::9", "2001:db8:10::1-2001:db8:11::1", unittest.is_false, "IPv6 range"},
|
|
{"193.168.1.1", "192.168.1.1 - 2001:db8::1", unittest.is_nil, "mixed"},
|
|
{"2001:db8::1", "192.168.1.1 - 2001:db8::1", unittest.is_nil, "mixed"},
|
|
}) do
|
|
test_suite:add_test(op[3](ip_in_range(op[1], op[2])),
|
|
string.format("ip_in_range(%s, %s) (%s)", op[1], op[2], op[4]))
|
|
end
|
|
end
|
|
|
|
do
|
|
for _, op in ipairs({
|
|
{"192.168", nil, "192.168.0.0", "IPv4 trunc"},
|
|
{"192.0.2.3", nil, "192.0.2.3", "IPv4"},
|
|
{"192.168", "inet6", "0:0:0:0:0:ffff:c0a8:0", "IPv4 trunc to IPv6"},
|
|
{"2001:db8::9", nil, "2001:db8:0:0:0:0:0:9", "IPv6"},
|
|
{"::ffff:192.0.2.128", "inet6", "0:0:0:0:0:ffff:c000:280", "IPv4-mapped to IPv6"},
|
|
-- TODO: Perhaps we should support extracting IPv4 from IPv4-mapped addresses?
|
|
--{"::ffff:192.0.2.128", "inet4", "192.0.2.128", "IPv4-mapped to IPv4"},
|
|
--{"::ffff:c000:0280", "inet4", "192.0.2.128", "IPv4-mapped to IPv4"},
|
|
}) do
|
|
test_suite:add_test(unittest.equal(expand_ip(op[1], op[2]), op[3]),
|
|
string.format("expand_ip(%s, %s) (%s)", op[1], op[2], op[4]))
|
|
end
|
|
test_suite:add_test(unittest.is_nil(expand_ip("2001:db8::1", "ipv4")),
|
|
"IPv6 to IPv4")
|
|
end
|
|
test_suite:add_test(unittest.equal(cidr_to_subnet("/16"), "255.255.0.0"), "cidr_to_subnet")
|
|
test_suite:add_test(unittest.equal(subnet_to_cidr("255.255.0.0"), "/16"), "subnet_to_cidr")
|
|
|
|
return _ENV;
|