1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-06 04:31:29 +00:00
Files
nmap/nselib/ipOps.lua
nnposter 57f9a46f73 Changes ipOps.get_ips_from_range() behavior to return true first IP address
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
2018-08-05 20:45:08 +00:00

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;