889 lines
29 KiB
Lua
889 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 stringaux = require "stringaux"
|
|
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 = stringaux.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",
|
|
["1"]="0001",
|
|
["2"]="0010",
|
|
["3"]="0011",
|
|
["4"]="0100",
|
|
["5"]="0101",
|
|
["6"]="0110",
|
|
["7"]="0111",
|
|
["8"]="1000",
|
|
["9"]="1001",
|
|
["a"]="1010",
|
|
["b"]="1011",
|
|
["c"]="1100",
|
|
["d"]="1101",
|
|
["e"]="1110",
|
|
["f"]="1111",
|
|
}
|
|
setmetatable(bin_lookup, {
|
|
__index = function()
|
|
error("Error in ipOps.hex_to_bin: Expected string representing a hexadecimal number.")
|
|
end
|
|
})
|
|
---
|
|
-- 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, string.lower(hex), ".", bin_lookup)
|
|
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;
|