505 lines
13 KiB
Lua
505 lines
13 KiB
Lua
--
|
|
-- IP address manipulation library in Lua
|
|
--
|
|
-- @author leite (xico@simbio.se)
|
|
-- @license MIT
|
|
-- @copyright Simbiose 2015
|
|
|
|
local math, string, table, bit_available, bit =
|
|
require [[math]], require [[string]], require [[table]], pcall(require, 'bit')
|
|
|
|
-- yey! Lua 5.3 bitwise keywords available
|
|
if not bit_available and _VERSION == 'Lua 5.3' then
|
|
bit = assert(load([[return {
|
|
band = function (a, b) return a & b end,
|
|
bor = function (a, b) return a | b end,
|
|
rshift = function (a, b) return a >> b end,
|
|
lshift = function (a, b) return a << b end
|
|
}]]))()
|
|
end
|
|
|
|
local ip, modf, len, match, find, format, concat, insert, band, bor, rshift, lshift, type,
|
|
assert, error, tonumber, pcall, setmetatable =
|
|
{}, math.modf, string.len, string.match, string.find, string.format, table.concat, table.insert,
|
|
bit.band, bit.bor, bit.rshift, bit.lshift, type, assert, error, tonumber, pcall, setmetatable
|
|
|
|
local _octets, _octet, _part, _parts_with_octets, EMPTY, COLON, ZERO =
|
|
'^((0?[xX]?)[%da-fA-F]+)%.((0?[xX]?)[%da-fA-F]+)%.((0?[xX]?)[%da-fA-F]+)%.((0?[xX]?)[%da-fA-F]+)/?(%d*)$',
|
|
'^((0?[xX]?)[%da-fA-F]+)((/?)(%d*))$', '(:?)([^:/$]*)(:?)',
|
|
'^([:%dxXa-fA-F]-::?)(([%dxXa-fA-F]*)[%.%dxXa-fA-F]*)/?(%d*)$', '', ':', '0'
|
|
|
|
math, string, bit, table = nil, nil, nil, nil
|
|
|
|
-- IP special ranges
|
|
|
|
local special_ranges = {
|
|
ipv4 = {
|
|
{'unspecified', octets={0, 0, 0, 0}, _cidr=8},
|
|
{'broadcast', octets={255, 255, 255, 255}, _cidr=32},
|
|
{'multicast', octets={224, 0, 0, 0}, _cidr=4},
|
|
{'linkLocal', octets={169, 254, 0, 0}, _cidr=16},
|
|
{'loopback', octets={127, 0, 0, 0}, _cidr=8},
|
|
{
|
|
'private', {octets={10, 0, 0, 0}, _cidr=8}, {octets={172, 16, 0, 0}, _cidr=12},
|
|
{octets={192, 168, 0, 0}, _cidr=16}
|
|
}, {
|
|
'reserved', {octets={192, 0, 0, 0}, _cidr=24}, {octets={192, 0, 2, 0}, _cidr=24},
|
|
{octets={192, 88, 99, 0}, _cidr=24}, {octets={198, 51, 100, 0}, _cidr=24},
|
|
{octets={203, 0, 113, 0}, _cidr=24}, {octets={240, 0, 0, 0}, _cidr=4}
|
|
}
|
|
}, ipv6 = {
|
|
{'unspecified', parts={0, 0, 0, 0, 0, 0, 0, 0}, _cidr=128},
|
|
{'linkLocal', parts={0xfe80, 0, 0, 0, 0, 0, 0, 0}, _cidr=10},
|
|
{'multicast', parts={0xff00, 0, 0, 0, 0, 0, 0, 0}, _cidr=8},
|
|
{'loopback', parts={0, 0, 0, 0, 0, 0, 0, 1}, _cidr=128},
|
|
{'uniqueLocal', parts={0xfc00, 0, 0, 0, 0, 0, 0, 0}, _cidr=7},
|
|
{'ipv4Mapped', parts={0, 0, 0, 0, 0, 0xffff, 0, 0}, _cidr=96},
|
|
{'rfc6145', parts={0, 0, 0, 0, 0xffff, 0, 0, 0}, _cidr=96},
|
|
{'rfc6052', parts={0x64, 0xff9b, 0, 0, 0, 0, 0, 0}, _cidr=96},
|
|
{'6to4', parts={0x2002, 0, 0, 0, 0, 0, 0, 0}, _cidr=16},
|
|
{'teredo', parts={0x2001, 0, 0, 0, 0, 0, 0, 0}, _cidr=32},
|
|
{'reserved', parts={0x2001, 0xdb8, 0, 0, 0, 0, 0, 0}, _cidr=32}
|
|
}
|
|
}
|
|
|
|
-- assert ipv4 octets
|
|
--
|
|
-- @table octets
|
|
-- @return boolean, [string]
|
|
|
|
local function assert_ipv4 (octets)
|
|
if not(octets and type(octets) == 'table') then
|
|
return false, 'octets should be a table'
|
|
end
|
|
if not(#octets == 4) then
|
|
return false, 'ipv4 octet count should be 4'
|
|
end
|
|
if not((-1 < octets[1] and 256 > octets[1]) and (-1 < octets[2] and 256 > octets[2]) and
|
|
(-1 < octets[3] and 256 > octets[3]) and (-1 < octets[4] and 256 > octets[4])) then
|
|
return false, 'ipv4 octet is a byte'
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- assert ipv6 parts
|
|
--
|
|
-- @table parts
|
|
-- @return boolean, [string]
|
|
|
|
local function assert_ipv6 (parts)
|
|
if not(parts and type(parts) == 'table') then
|
|
return false, 'parts should be a table'
|
|
end
|
|
if not(#parts == 8) then
|
|
return false, 'ipv6 part count should be 8'
|
|
end
|
|
if not((-1 < parts[1] and 0x10000 > parts[1]) and (-1 < parts[2] and 0x10000 > parts[2]) and
|
|
(-1 < parts[3] and 0x10000 > parts[3]) and (-1 < parts[4] and 0x10000 > parts[4]) and
|
|
(-1 < parts[5] and 0x10000 > parts[5]) and (-1 < parts[6] and 0x10000 > parts[6]) and
|
|
(-1 < parts[7] and 0x10000 > parts[7]) and (-1 < parts[8] and 0x10000 > parts[8])) then
|
|
return false, 'ipv6 part should fit to two octets'
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- generic CIDR matcher
|
|
--
|
|
-- @table first
|
|
-- @table second
|
|
-- @number part_size
|
|
-- @number cidr_bits
|
|
-- @return boolean
|
|
|
|
local function match_cidr(first, second, part_size, cidr_bits)
|
|
assert(#first == #second, 'cannot match CIDR for objects with different lengths')
|
|
local part, shift = 0, 0
|
|
while cidr_bits > 0 do
|
|
part = part + 1
|
|
shift = part_size - cidr_bits
|
|
shift = shift < 0 and 0 or shift
|
|
if rshift(first[part], shift) ~= rshift(second[part], shift) then
|
|
return false
|
|
end
|
|
cidr_bits = cidr_bits - part_size
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- funct address named range matching
|
|
--
|
|
-- @table address
|
|
-- @table range_list
|
|
-- @string default_name
|
|
-- @return string
|
|
|
|
local function subnet_match(address, range_list, default_name)
|
|
local subnet = {}
|
|
for i = 1, #range_list do
|
|
subnet = range_list[i]
|
|
if #subnet == 1 then
|
|
if address:match(subnet) then
|
|
return subnet[1]
|
|
end
|
|
else
|
|
for j = 2, #subnet do
|
|
if address:match(subnet[j]) then
|
|
return subnet[1]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return default_name or 'unicast'
|
|
end
|
|
|
|
-- parse IP version 4
|
|
--
|
|
-- @string string
|
|
-- @table octets
|
|
-- @number cidr
|
|
-- @return boolean, [string]
|
|
|
|
local function parse_v4(string, octets, cidr)
|
|
local value, hex, _, __, _cidr = match(string, _octet)
|
|
|
|
if value then
|
|
value = tonumber(value, hex == ZERO and 8 or nil)
|
|
if value > 0xffffffff or value < 0 then
|
|
return false, 'address outside defined range'
|
|
end
|
|
octets[1], octets[2], octets[3], octets[4] =
|
|
band(rshift(value, 24), 0xff), band(rshift(value, 16), 0xff),
|
|
band(rshift(value, 8), 0xff), band(value, 0xff)
|
|
return tonumber(_cidr == EMPTY and 32 or _cidr)
|
|
end
|
|
|
|
local st, _st, nd, _nd, rd, _rd, th, _th, _cidr = match(string, _octets)
|
|
|
|
if not(st) then
|
|
return false, 'invalid ip address'
|
|
end
|
|
|
|
octets[1], octets[2], octets[3], octets[4] =
|
|
tonumber(st, _st == ZERO and 8 or nil), tonumber(nd, _nd == ZERO and 8 or nil),
|
|
tonumber(rd, _rd == ZERO and 8 or nil), tonumber(th, _th == ZERO and 8 or nil)
|
|
return tonumber(_cidr == EMPTY and (cidr and cidr or 32) or _cidr)
|
|
end
|
|
|
|
-- parse IP version 6
|
|
--
|
|
-- @string string
|
|
-- @table parts
|
|
-- @table octets
|
|
-- @number cidr
|
|
-- @return boolean, [string]
|
|
|
|
local function parse_v6(string, parts, octets, cidr)
|
|
local nd_sep, part, l_sep, count, double, length, index, last, string, octets_st, sep, _cidr =
|
|
'', '', false, 1, 0, 0, 0, 0, match(string, _parts_with_octets)
|
|
|
|
if not string or EMPTY == string then
|
|
return false, 'invalid ipv6 format'
|
|
end
|
|
|
|
if #octets_st == #sep then
|
|
string = string .. sep
|
|
else
|
|
local err, message = parse_v4(octets_st, octets)
|
|
if not err then
|
|
return err, message
|
|
end
|
|
end
|
|
|
|
_cidr, length, index, last, sep, part, nd_sep =
|
|
tonumber(_cidr==EMPTY and (cidr and cidr or 128) or _cidr), len(string), find(string, _part)
|
|
|
|
while index and index <= length do
|
|
if sep == COLON and nd_sep == COLON then
|
|
if part == EMPTY or l_sep then
|
|
if double > 0 then
|
|
return false, 'string is not formatted like ip address'
|
|
end
|
|
double = count
|
|
end
|
|
elseif sep == COLON or nd_sep == COLON then
|
|
if l_sep and sep == COLON then
|
|
if double > 0 then
|
|
return false, 'string is not formatted like ip address'
|
|
end
|
|
double = count
|
|
end
|
|
end
|
|
|
|
insert(parts, tonumber(part == EMPTY and '0' or part, 16))
|
|
|
|
l_sep, count, index, last, sep, part, nd_sep =
|
|
nd_sep == COLON, count + 1, find(string, _part, last + 1)
|
|
end
|
|
|
|
if #octets > 0 then
|
|
insert(parts, bor(lshift(octets[1], 8), octets[2]))
|
|
insert(parts, bor(lshift(octets[3], 8), octets[4]))
|
|
length = 7
|
|
else
|
|
length = 9
|
|
end
|
|
|
|
for index = 1, (length - count) do
|
|
insert(parts, double, 0)
|
|
end
|
|
|
|
return _cidr
|
|
end
|
|
|
|
-- ip metatable
|
|
|
|
local ip_metatable = {
|
|
|
|
-- set CIDR
|
|
--
|
|
-- @number cidr
|
|
-- @return metatable
|
|
|
|
cidr = function(self, cidr)
|
|
self._cidr = cidr
|
|
return self
|
|
end,
|
|
|
|
-- get address named range
|
|
--
|
|
-- @return string
|
|
|
|
range = function(self)
|
|
return subnet_match(self, special_ranges[self:kind()])
|
|
end,
|
|
|
|
-- get or match address kind
|
|
--
|
|
-- @string [kind]
|
|
-- @return string|boolean
|
|
|
|
kind = function(self, kind)
|
|
local _kind = #self.parts > 0 and 'ipv6' or (#self.octets > 0 and 'ipv4' or EMPTY)
|
|
if kind then
|
|
return kind == _kind
|
|
end
|
|
return _kind
|
|
end,
|
|
|
|
-- match two addresses
|
|
--
|
|
-- @table address
|
|
-- @number cidr
|
|
-- @return boolean
|
|
|
|
match = function(self, address, cidr)
|
|
if cidr and address._cidr then
|
|
address._cidr = cidr
|
|
end
|
|
return self.__eq(self, address)
|
|
end,
|
|
|
|
-- converts ipv4 to ipv4-mapped ipv6 address
|
|
--
|
|
-- @return string|nil
|
|
|
|
ipv4_mapped_address = function (self)
|
|
return self:kind('ipv4') and ip.parsev6('::ffff:' .. self:__tostring()) or nil
|
|
end,
|
|
|
|
-- check if it's a ipv4 mapped address
|
|
--
|
|
-- @return boolean
|
|
|
|
is_ipv4_mapped = function (self)
|
|
return self:range() == 'ipv4Mapped'
|
|
end,
|
|
|
|
-- converts ipv6 ipv4-mapped address to ipv4 address
|
|
--
|
|
-- @return metatable
|
|
|
|
ipv4_address = function (self)
|
|
assert(self:is_ipv4_mapped(), 'trying to convert a generic ipv6 address to ipv4')
|
|
local high, low = self.parts[7], self.parts[8]
|
|
return ip.v4({rshift(high, 8), band(high, 0xff), rshift(low, 8), band(low, 0xff)})
|
|
end,
|
|
|
|
-- IP table to string
|
|
--
|
|
-- @return string
|
|
|
|
__tostring = function(self)
|
|
if self:kind('ipv4') then
|
|
return concat(self.octets, '.')
|
|
end
|
|
|
|
local part, state, size, output = '', 0, #self.parts, {}
|
|
|
|
for i = 1, size do
|
|
part = format('%x', self.parts[i])
|
|
if 0 == state then
|
|
insert(output, (ZERO == part and EMPTY or part))
|
|
state = 1
|
|
elseif 1 == state then
|
|
if ZERO == part then
|
|
state = 2
|
|
else
|
|
insert(output, part)
|
|
end
|
|
elseif 2 == state then
|
|
if ZERO ~= part then
|
|
insert(output, EMPTY)
|
|
insert(output, part)
|
|
state = 3
|
|
end
|
|
else
|
|
insert(output, part)
|
|
end
|
|
end
|
|
|
|
if 2 == state then
|
|
insert(output, COLON)
|
|
end
|
|
|
|
return concat(output, COLON)
|
|
end,
|
|
|
|
-- compare two IP addresses
|
|
--
|
|
-- @table value
|
|
-- @return boolean
|
|
|
|
__eq = function(self, value)
|
|
if #self.parts > 0 then
|
|
assert(value.parts and #value.parts > 0, 'cannot match different address version')
|
|
return match_cidr(self.parts, value.parts, 16, value._cidr)
|
|
end
|
|
assert(value.octets and #value.octets > 0, 'cannot match different address version')
|
|
return match_cidr(self.octets, value.octets, 8, value._cidr)
|
|
end
|
|
}
|
|
|
|
ip_metatable.__index = ip_metatable
|
|
|
|
-- create new IP metatable
|
|
--
|
|
-- @table parts
|
|
-- @table octets
|
|
-- @number cidr
|
|
-- @return metatable
|
|
|
|
local function new (parts, octets, cidr)
|
|
return setmetatable({octets=octets, parts=parts, _cidr=cidr}, ip_metatable)
|
|
end
|
|
|
|
-- assert IP version 4 octets and create it's metatable
|
|
--
|
|
-- @table octets
|
|
-- @number cidr
|
|
-- @return metatable
|
|
|
|
function ip.v4 (octets, cidr)
|
|
local err, message = assert_ipv4(octets)
|
|
assert(err, message)
|
|
return new({}, octets, cidr or 32)
|
|
end
|
|
|
|
-- assert IP version 6 parts and create it's metatable
|
|
--
|
|
-- @table parts
|
|
-- @number cidr
|
|
-- @table [octets]
|
|
-- @return metatable
|
|
|
|
function ip.v6 (parts, cidr, octets)
|
|
local err, message = assert_ipv6(parts)
|
|
assert(err, message)
|
|
|
|
if octets and #octets > 0 then
|
|
err, message = assert_ipv4(octets)
|
|
assert(err, message)
|
|
end
|
|
|
|
return new(parts, octets or {}, cidr or 128)
|
|
end
|
|
|
|
-- parse string to IP version 4 metatable
|
|
--
|
|
-- @string string
|
|
-- @number [cidr]
|
|
-- @return metatable
|
|
|
|
function ip.parsev4 (string, cidr)
|
|
local octets, message = {}, ''
|
|
cidr, message = parse_v4(string, octets, cidr)
|
|
assert(cidr ~= false, message)
|
|
return ip.v4(octets, cidr)
|
|
end
|
|
|
|
-- parse string to IP version 6 metatable
|
|
--
|
|
-- @string string
|
|
-- @number [cidr]
|
|
-- @return metatable
|
|
|
|
function ip.parsev6 (string, cidr)
|
|
local parts, octets, message = {}, {}, ''
|
|
cidr, message = parse_v6(string, parts, octets, cidr)
|
|
assert(cidr ~= false, message)
|
|
return ip.v6(parts, cidr, octets)
|
|
end
|
|
|
|
-- check and parse string to IP metatable
|
|
--
|
|
-- @string string
|
|
-- @return metatable
|
|
|
|
function ip.parse (string)
|
|
if ip.isv6(string) then
|
|
return ip.parsev6(string)
|
|
elseif ip.isv4(string) then
|
|
return ip.parsev4(string)
|
|
end
|
|
error('the address has neither IPv6 nor IPv4 format')
|
|
end
|
|
|
|
-- check if string is a IP version 4 address
|
|
--
|
|
-- @string string
|
|
-- @boolean validate
|
|
-- @return boolean
|
|
|
|
function ip.isv4 (string, validate)
|
|
if validate then
|
|
local octets = {}
|
|
return parse_v4(string, octets) ~= false and assert_ipv4(octets)
|
|
end
|
|
return find(string, _octet) ~= nil or find(string, _octets) ~= nil
|
|
end
|
|
|
|
-- check if string is a IP version 6 address
|
|
--
|
|
-- @string string
|
|
-- @boolean validate
|
|
-- @return boolean
|
|
|
|
function ip.isv6 (string, validate)
|
|
if validate then
|
|
local octets, parts = {}, {}
|
|
return parse_v6(string, parts, octets) ~= false and assert_ipv6(parts)
|
|
end
|
|
return find(string, _parts_with_octets) ~= nil
|
|
end
|
|
|
|
-- check if IP address is valid
|
|
--
|
|
-- @string string
|
|
-- @return boolean
|
|
|
|
function ip.valid (string)
|
|
return pcall(ip.parse, string)
|
|
end
|
|
|
|
return ip
|