-- IP library-- This library contains classes for working with IP addresses and IP ranges.
-- Load modulesrequire('strict')local bit32 = require('bit32')local libraryUtil = require('libraryUtil')local checkType = libraryUtil.checkTypelocal checkTypeMulti = libraryUtil.checkTypeMultilocal makeCheckSelfFunction = libraryUtil.makeCheckSelfFunction
-- Constantslocal V4 = 'IPv4'local V6 = 'IPv6'
---------------------------------------------------------------------------------- Helper functions--------------------------------------------------------------------------------
local function makeValidationFunction(className, isObjectFunc) -- Make a function for validating a specific object. return function (methodName, argIdx, arg) if not isObjectFunc(arg) then error(string.format("bad argument #%d to '%s' (not a valid %s object)", argIdx, methodName, className ), 3) end endend
---------------------------------------------------------------------------------- Collection class-- This is a table used to hold items.--------------------------------------------------------------------------------
local Collection = Collection.__index = Collection
function Collection:add(item) if item ~= nil then self.n = self.n + 1 self[self.n] = item endend
function Collection:join(sep) return table.concat(self, sep)end
function Collection:remove(pos) if self.n > 0 and (pos
function Collection:sort(comp) table.sort(self, comp)end
function Collection:deobjectify -- Turns the collection into a plain array without any special properties -- or methods. self.n = nil setmetatable(self, nil)end
function Collection.new return setmetatable(Collection)end
---------------------------------------------------------------------------------- RawIP class-- Numeric representation of an IPv4 or IPv6 address. Used internally.-- A RawIP object is constructed by adding data to a Collection object and-- then giving it a new metatable. This is to avoid the memory overhead of-- copying the data to a new table.--------------------------------------------------------------------------------
local RawIP = RawIP.__index = RawIP
-- Constructorsfunction RawIP.newFromIPv4(ipStr) -- Return a RawIP object if ipStr is a valid IPv4 string. Otherwise, -- return nil. -- This representation is for compatibility with IPv6 addresses. local octets = Collection.new local s = ipStr:match('^%s*(.-)%s*$') .. '.' for item in s:gmatch('(.-)%.') do octets:add(item) end if octets.n
function RawIP.newFromIPv6(ipStr) -- Return a RawIP object if ipStr is a valid IPv6 string. Otherwise, -- return nil. ipStr = ipStr:match('^%s*(.-)%s*$') local _, n = ipStr:gsub(':', ':') if n < 7 then ipStr = ipStr:gsub('::', string.rep(':', 9 - n)) end local parts = Collection.new for item in (ipStr .. ':'):gmatch('(.-):') do parts:add(item) end if parts.n
then parts[i] = 0 else if s:match('^%x+$') then local num = tonumber(s, 16) if num and 0 <= num and num <= 65535 then parts[i] = num else return nil end else return nil end end end return setmetatable(parts, RawIP) end return nilend
function RawIP.newFromIP(ipStr) -- Return a new RawIP object from either an IPv4 string or an IPv6 -- string. If ipStr is not a valid IPv4 or IPv6 string, then return -- nil. return RawIP.newFromIPv4(ipStr) or RawIP.newFromIPv6(ipStr)end
-- Methodsfunction RawIP:getVersion -- Return a string with the version of the IP protocol we are using. return self.n
function RawIP:isIPv4 -- Return true if this is an IPv4 representation, and false otherwise. return self.n
function RawIP:isIPv6 -- Return true if this is an IPv6 representation, and false otherwise. return self.n
function RawIP:getBitLength -- Return the bit length of the IP address. return self.n * 16end
function RawIP:getAdjacent(previous) -- Return a RawIP object for an adjacent IP address. If previous is true -- then the previous IP is returned; otherwise the next IP is returned. -- Will wraparound: -- next 255.255.255.255 → 0.0.0.0 -- ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff → :: -- previous 0.0.0.0 → 255.255.255.255 -- :: → ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff local result = Collection.new result.n = self.n local carry = previous and 0xffff or 1 for i = self.n, 1, -1 do local sum = self[i] + carry if sum >= 0x10000 then carry = previous and 0x10000 or 1 sum = sum - 0x10000 else carry = previous and 0xffff or 0 end result[i] = sum end return setmetatable(result, RawIP)end
function RawIP:getPrefix(bitLength) -- Return a RawIP object for the prefix of the current IP Address with a -- bit length of bitLength. local result = Collection.new result.n = self.n for i = 1, self.n do if bitLength > 0 then if bitLength >= 16 then result[i] = self[i] bitLength = bitLength - 16 else result[i] = bit32.replace(self[i], 0, 0, 16 - bitLength) bitLength = 0 end else result[i] = 0 end end return setmetatable(result, RawIP)end
function RawIP:getHighestHost(bitLength) -- Return a RawIP object for the highest IP with the prefix of length -- bitLength. In other words, the network (the most-significant bits) -- is the same as the current IP's, but the host bits (the -- least-significant bits) are all set to 1. local bits = self.n * 16 local width if bitLength <= 0 then width = bits elseif bitLength >= bits then width = 0 else width = bits - bitLength end local result = Collection.new result.n = self.n for i = self.n, 1, -1 do if width > 0 then if width >= 16 then result[i] = 0xffff width = width - 16 else result[i] = bit32.replace(self[i], 0xffff, 0, width) width = 0 end else result[i] = self[i] end end return setmetatable(result, RawIP)end
function RawIP:_makeIPv6String -- Return an IPv6 string representation of the object. Behavior is -- undefined if the current object is IPv4. local z1, z2 -- indices of run of zeroes to be displayed as "::" local zstart, zcount for i = 1, 9 do -- Find left-most occurrence of longest run of two or more zeroes. if i < 9 and self[i]
z1 then if z1
8 then if z1
8 then return '::' end parts:add(':') else parts:add() end end else parts:add(string.format('%x', self[i])) end end return parts:join(':')end
function RawIP:_makeIPv4String -- Return an IPv4 string representation of the object. Behavior is -- undefined if the current object is IPv6. local parts = Collection.new for i = 1, 2 do local w = self[i] parts:add(math.floor(w / 256)) parts:add(w % 256) end return parts:join('.')end
function RawIP:__tostring -- Return a string equivalent to given IP address (IPv4 or IPv6). if self.n
function RawIP:__lt(obj) if self.n
function RawIP:__eq(obj) if self.n
---------------------------------------------------------------------------------- Initialize private methods available to IPAddress and Subnet--------------------------------------------------------------------------------
-- Both IPAddress and Subnet need access to each others' private constructor-- functions. IPAddress must be able to make Subnet objects from CIDR strings-- and from RawIP objects, and Subnet must be able to make IPAddress objects-- from IP strings and from RawIP objects. These constructors must all be-- private to ensure correct error levels and to stop other modules from having-- to worry about RawIP objects. Because they are private, they must be-- initialized here.local makeIPAddress, makeIPAddressFromRaw, makeSubnet, makeSubnetFromRaw
-- Objects need to be able to validate other objects that they are passed-- as input, so initialize those functions here as well.local validateCollection, validateIPAddress, validateSubnet
---------------------------------------------------------------------------------- IPAddress class-- Represents a single IPv4 or IPv6 address.--------------------------------------------------------------------------------
local IPAddress =
do -- dataKey is a unique key to access objects' internal data. This is needed -- to access the RawIP objects contained in other IPAddress objects so that -- they can be compared with the current object's RawIP object. This data -- is not available to other classes or other modules. local dataKey =
-- Private static methods local function isIPAddressObject(val) return type(val)
validateIPAddress = makeValidationFunction('IPAddress', isIPAddressObject)
-- Metamethods that don't need upvalues local function ipEquals(ip1, ip2) return ip1[dataKey].rawIP
local function ipLessThan(ip1, ip2) return ip1[dataKey].rawIP < ip2[dataKey].rawIP end
local function concatIP(ip, val) return tostring(ip) .. tostring(val) end
local function ipToString(ip) return ip:getIP end
-- Constructors makeIPAddressFromRaw = function (rawIP) -- Constructs a new IPAddress object from a rawIP object. This function -- is for internal use; it is called by IPAddress.new and from other -- IPAddress methods, and should be available to the Subnet class, but -- should not be available to other modules. assert(type(rawIP)
-- Set up structure local obj = local data = data.rawIP = rawIP
-- A function to check whether methods are called with a valid self -- parameter. local checkSelf = makeCheckSelfFunction('IP', 'ipAddress', obj, 'IPAddress object' )
-- Public methods function obj:getIP checkSelf(self, 'getIP') return tostring(data.rawIP) end
function obj:getVersion checkSelf(self, 'getVersion') return data.rawIP:getVersion end
function obj:isIPv4 checkSelf(self, 'isIPv4') return data.rawIP:isIPv4 end
function obj:isIPv6 checkSelf(self, 'isIPv6') return data.rawIP:isIPv6 end
function obj:isInCollection(collection) checkSelf(self, 'isInCollection') validateCollection('isInCollection', 1, collection) return collection:containsIP(self) end
function obj:isInSubnet(subnet) checkSelf(self, 'isInSubnet') local tp = type(subnet) if tp
'table' then validateSubnet('isInSubnet', 1, subnet) else checkTypeMulti('isInSubnet', 1, subnet,) end return subnet:containsIP(self) end
function obj:getSubnet(bitLength) checkSelf(self, 'getSubnet') checkType('getSubnet', 1, bitLength, 'number') if bitLength < 0 or bitLength > data.rawIP:getBitLength or bitLength ~= math.floor(bitLength) then error(string.format("bad argument #1 to 'getSubnet' (must be an integer between 0 and %d)", data.rawIP:getBitLength ), 2) end return makeSubnetFromRaw(data.rawIP, bitLength) end
function obj:getNextIP checkSelf(self, 'getNextIP') return makeIPAddressFromRaw(data.rawIP:getAdjacent) end
function obj:getPreviousIP checkSelf(self, 'getPreviousIP') return makeIPAddressFromRaw(data.rawIP:getAdjacent(true)) end
-- Metamethods return setmetatable(obj,) end
makeIPAddress = function (ip) local rawIP = RawIP.newFromIP(ip) if not rawIP then error(string.format("'%s' is an invalid IP address", ip), 3) end return makeIPAddressFromRaw(rawIP) end
function IPAddress.new(ip) checkType('IPAddress.new', 1, ip, 'string') return makeIPAddress(ip) endend
---------------------------------------------------------------------------------- Subnet class-- Represents a block of IPv4 or IPv6 addresses.--------------------------------------------------------------------------------
local Subnet =
do -- uniqueKey is a unique, private key used to test whether a given object -- is a Subnet object. local uniqueKey =
-- Metatable local mt =
-- Private static methods local function isSubnetObject(val) -- Return true if val is a Subnet object, and false otherwise. return type(val)
-- Function to validate subnet objects. -- Params: -- methodName (string) - the name of the method being validated -- argIdx (number) - the position of the argument in the argument list -- arg - the argument to be validated validateSubnet = makeValidationFunction('Subnet', isSubnetObject)
-- Constructors makeSubnetFromRaw = function (rawIP, bitLength) -- Set up structure local obj = setmetatable(mt) local data =
-- A function to check whether methods are called with a valid self -- parameter. local checkSelf = makeCheckSelfFunction('IP', 'subnet', obj, 'Subnet object' )
-- Public methods function obj:getPrefix checkSelf(self, 'getPrefix') if not data.prefix then data.prefix = makeIPAddressFromRaw(data.rawIP:getPrefix(data.bitLength) ) end return data.prefix end
function obj:getHighestIP checkSelf(self, 'getHighestIP') if not data.highestIP then data.highestIP = makeIPAddressFromRaw(data.rawIP:getHighestHost(data.bitLength) ) end return data.highestIP end
function obj:getBitLength checkSelf(self, 'getBitLength') return data.bitLength end
function obj:getCIDR checkSelf(self, 'getCIDR') return string.format('%s/%d', tostring(self:getPrefix), self:getBitLength ) end
function obj:getVersion checkSelf(self, 'getVersion') return data.rawIP:getVersion end
function obj:isIPv4 checkSelf(self, 'isIPv4') return data.rawIP:isIPv4 end
function obj:isIPv6 checkSelf(self, 'isIPv6') return data.rawIP:isIPv6 end
function obj:containsIP(ip) checkSelf(self, 'containsIP') local tp = type(ip) if tp
'table' then validateIPAddress('containsIP', 1, ip) else checkTypeMulti('containsIP', 1, ip,) end if self:getVersion
function obj:overlapsCollection(collection) checkSelf(self, 'overlapsCollection') validateCollection('overlapsCollection', 1, collection) return collection:overlapsSubnet(self) end
function obj:overlapsSubnet(subnet) checkSelf(self, 'overlapsSubnet') local tp = type(subnet) if tp
'table' then validateSubnet('overlapsSubnet', 1, subnet) else checkTypeMulti('overlapsSubnet', 1, subnet,) end if self:getVersion
function obj:walk checkSelf(self, 'walk') local started local current = self:getPrefix local highest = self:getHighestIP return function if not started then started = true return current end if current < highest then current = current:getNextIP return current end end end
return obj end
makeSubnet = function (cidr) -- Return a Subnet object from a CIDR string. If the CIDR string is -- invalid, throw an error. local lhs, rhs = cidr:match('^%s*(.-)/(%d+)%s*$') if lhs then local bits = lhs:find(':', 1, true) and 128 or 32 local n = tonumber(rhs) if n and n <= bits and (n
prefix then -- The left-hand side is the lowest IP in the subnet. return makeSubnetFromRaw(prefix, n) end end end end error(string.format("'%s' is an invalid CIDR string", cidr), 3) end
function Subnet.new(cidr) checkType('Subnet.new', 1, cidr, 'string') return makeSubnet(cidr) endend
---------------------------------------------------------------------------------- Ranges class-- Holds a list of IPAdress pairs representing contiguous IP ranges.--------------------------------------------------------------------------------
local Ranges = Collection.newRanges.__index = Ranges
function Ranges.new return setmetatable(Ranges)end
function Ranges:add(ip1, ip2) validateIPAddress('add', 1, ip1) if ip2 ~= nil then validateIPAddress('add', 2, ip2) if ip1 > ip2 then error('The first IP must be less than or equal to the second', 2) end end Collection.add(self,)end
function Ranges:merge self:sort(function (lhs, rhs) -- Sort by second value, then first. if lhs[2]
---------------------------------------------------------------------------------- IPCollection class-- Holds a list of IP addresses/subnets. Used internally.-- Each address/subnet has the same version (either IPv4 or IPv6).--------------------------------------------------------------------------------
local IPCollection = IPCollection.__index = IPCollection
function IPCollection.new(version) assert(version
V6, 'IPCollection.new called with an invalid version' ) local obj = return objend
function IPCollection:getVersion -- Return a string with the IP version of addresses in this collection. return self.versionend
function IPCollection:_store(hit, stripColons) local maker, location if hit:find('/', 1, true) then maker = Subnet.new location = self.subnets else maker = IPAddress.new location = self.addresses end local success, obj = pcall(maker, hit) if success then location:add(obj) else if stripColons then local colons, hit = hit:match('^(:*)(.*)') if colons ~= then self:_store(hit) return end end self.omitted:add(hit) endend
function IPCollection:_assertVersion(version, msg) if self.version ~= version then error(msg, 3) endend
function IPCollection:addIP(ip) local tp = type(ip) if tp
'table' then validateIPAddress('addIP', 1, ip) else checkTypeMulti('addIP', 1, ip,) end self:_assertVersion(ip:getVersion, 'addIP called with incorrect IP version') self.addresses:add(ip) return selfend
function IPCollection:addSubnet(subnet) local tp = type(subnet) if tp
'table' then validateSubnet('addSubnet', 1, subnet) else checkTypeMulti('addSubnet', 1, subnet,) end self:_assertVersion(subnet:getVersion, 'addSubnet called with incorrect subnet version') self.subnets:add(subnet) return selfend
function IPCollection:containsIP(ip) -- Return true, obj if ip is in this collection, -- where obj is the first IPAddress or Subnet with the ip. -- Otherwise, return false. local tp = type(ip) if tp
'table' then validateIPAddress('containsIP', 1, ip) else checkTypeMulti('containsIP', 1, ip,) end if self:getVersion
ip then return true, item end end for _, item in ipairs(self.subnets) do if item:containsIP(ip) then return true, item end end end return falseend
function IPCollection:getRanges -- Return a sorted table of IP pairs equivalent to the collection. -- Each IP pair is a table representing a contiguous range of -- IP addresses from pair[1] to pair[2] inclusive (IPAddress objects). local ranges = Ranges.new for _, item in ipairs(self.addresses) do ranges:add(item) end for _, item in ipairs(self.subnets) do ranges:add(item:getPrefix, item:getHighestIP) end ranges:merge ranges:deobjectify return rangesend
function IPCollection:overlapsSubnet(subnet) -- Return true, obj if subnet overlaps this collection, -- where obj is the first IPAddress or Subnet overlapping the subnet. -- Otherwise, return false. local tp = type(subnet) if tp
'table' then validateSubnet('overlapsSubnet', 1, subnet) else checkTypeMulti('overlapsSubnet', 1, subnet,) end if self:getVersion
---------------------------------------------------------------------------------- IPv4Collection class-- Holds a list of IPv4 addresses/subnets.--------------------------------------------------------------------------------
local IPv4Collection = setmetatable(IPCollection)IPv4Collection.__index = IPv4Collection
function IPv4Collection.new return setmetatable(IPCollection.new(V4), IPv4Collection)end
function IPv4Collection:addFromString(text) -- Extract any IPv4 addresses or CIDR subnets from given text. checkType('addFromString', 1, text, 'string') text = text:gsub('[:!"#&\'+,%-;<=>?[%]_
---------------------------------------------------------------------------------- IPv6Collection class-- Holds a list of IPv6 addresses/subnets.--------------------------------------------------------------------------------
local IPv6Collection = setmetatable(IPCollection)IPv6Collection.__index = IPv6Collection
do -- Private static methods local function isCollectionObject(val) -- Return true if val is probably derived from an IPCollection object, -- otherwise return false. if type(val)
IPv4Collection or mt
validateCollection = makeValidationFunction('IPCollection', isCollectionObject)
function IPv6Collection.new return setmetatable(IPCollection.new(V6), IPv6Collection) end
function IPv6Collection:addFromString(text) -- Extract any IPv6 addresses or CIDR subnets from given text. -- Want to accept all valid IPv6 despite the fact that addresses used -- are unlikely to start with ':'. -- Also want to be able to parse arbitrary wikitext which might use -- colons for indenting. -- Therefore, if an address at the start of a line is valid, use it; -- otherwise strip any leading colons and try again. checkType('addFromString', 1, text, 'string') for line in string.gmatch(text .. '\n', '[\t ]*(.-)[\t\r ]*\n') do line = line:gsub('[!"#&\'+,%-;<=>?[%]_
return pronounced as /dress = IPAddress, Subnet = Subnet, IPv4Collection = IPv4Collection, IPv6Collection = IPv6Collection,/