Module:IP explained

-- 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

nil or (0 < pos and pos <= self.n)) then self.n = self.n - 1 return table.remove(self, pos) endend

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

4 then for i, s in ipairs(octets) do if s:match('^%d+$') then local num = tonumber(s) if 0 <= num and num <= 255 then if num > 0 and s:match('^0') then -- A redundant leading zero is for an IP in octal. return nil end octets[i] = num else return nil end else return nil end end local parts = Collection.new for i = 1, 3, 2 do parts:add(octets[i] * 256 + octets[i+1]) end return setmetatable(parts, RawIP) end return nilend

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

8 then for i, s in ipairs(parts) do if s

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

2 and V4 or V6end

function RawIP:isIPv4 -- Return true if this is an IPv4 representation, and false otherwise. return self.n

2end

function RawIP:isIPv6 -- Return true if this is an IPv6 representation, and false otherwise. return self.n

8end

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]

0 then if zstart then zcount = zcount + 1 else zstart = i zcount = 1 end else if zcount and zcount > 1 then if not z1 or zcount > z2 - z1 + 1 then z1 = zstart z2 = zstart + zcount - 1 end end zstart = nil zcount = nil end end local parts = Collection.new for i = 1, 8 do if z1 and z1 <= i and i <= z2 then if i

z1 then if z1

1 or z2

8 then if z1

1 and z2

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

2 then return self:_makeIPv4String else return self:_makeIPv6String endend

function RawIP:__lt(obj) if self.n

obj.n then for i = 1, self.n do if self[i] ~= obj[i] then return self[i] < obj[i] end end return false end return self.n < obj.nend

function RawIP:__eq(obj) if self.n

obj.n then for i = 1, self.n do if self[i] ~= obj[i] then return false end end return true end return falseend

---------------------------------------------------------------------------------- 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)

'table' and val[dataKey] ~= nil end

validateIPAddress = makeValidationFunction('IPAddress', isIPAddressObject)

-- Metamethods that don't need upvalues local function ipEquals(ip1, ip2) return ip1[dataKey].rawIP

ip2[dataKey].rawIP end

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)

'table', 'rawIP was type ' .. type(rawIP) .. '; expected type table')

-- 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

'string' then subnet = makeSubnet(subnet) elseif 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)

'table' and val[uniqueKey] ~= nil end

-- 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

'string' then ip = makeIPAddress(ip) elseif tp

'table' then validateIPAddress('containsIP', 1, ip) else checkTypeMulti('containsIP', 1, ip,) end if self:getVersion

ip:getVersion then return self:getPrefix <= ip and ip <= self:getHighestIP end return false end

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

'string' then subnet = makeSubnet(subnet) elseif tp

'table' then validateSubnet('overlapsSubnet', 1, subnet) else checkTypeMulti('overlapsSubnet', 1, subnet,) end if self:getVersion

subnet:getVersion then return (subnet:getHighestIP >= self:getPrefix and subnet:getPrefix <= self:getHighestIP ) end return false end

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

0 or not rhs:find('^0')) then -- The right-hand side is a number between 0 and 32 (for IPv4) -- or 0 and 128 (for IPv6) and doesn't have any leading zeroes. local base = RawIP.newFromIP(lhs) if base then -- The left-hand side is a valid IP address. local prefix = base:getPrefix(n) if base

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]

rhs[2] then return lhs[1] < rhs[1] end return lhs[2] < rhs[2] end ) local pos = self.n while pos > 1 do for i = pos - 1, 1, -1 do local ip1 = self[i][2] local ip2 = ip1:getNextIP if ip2 < ip1 then ip2 = ip1 -- don't wrap around end if self[pos][1] > ip2 then break end ip1 = self[i][1] ip2 = self[pos][1] self[i] = self:remove(pos) pos = pos - 1 if pos <= 1 then break end end pos = pos - 1 endend

---------------------------------------------------------------------------------- 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

V4 or 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

'string' then ip = makeIPAddress(ip) elseif 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

'string' then subnet = makeSubnet(subnet) elseif 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

'string' then ip = makeIPAddress(ip) elseif tp

'table' then validateIPAddress('containsIP', 1, ip) else checkTypeMulti('containsIP', 1, ip,) end if self:getVersion

ip:getVersion then for _, item in ipairs(self.addresses) do if item

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

'string' then subnet = makeSubnet(subnet) elseif tp

'table' then validateSubnet('overlapsSubnet', 1, subnet) else checkTypeMulti('overlapsSubnet', 1, subnet,) end if self:getVersion

subnet:getVersion then for _, item in ipairs(self.addresses) do if subnet:containsIP(item) then return true, item end end for _, item in ipairs(self.subnets) do if subnet:overlapsSubnet(item) then return true, item end end end return falseend

---------------------------------------------------------------------------------- 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('[:!"#&\'+,%-;<=>?[%]_

]', ' ') for hit in text:gmatch('%S+') do if hit:match('^%d+%.%d+[%.%d/]+$') then local _, n = hit:gsub('%.', '.') if n >= 3 then self:_store(hit) end end end return selfend

---------------------------------------------------------------------------------- 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)

'table' then local mt = getmetatable(val) if mt

IPv4Collection or mt

IPv6Collection then return true end end return false end

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('[!"#&\'+,%-;<=>?[%]_

]', ' ') for position, hit in line:gmatch('(%S+)') do local ip = hit:match('^([:%x]+)/?%d*$') if ip then local _, n = ip:gsub(':', ':') if n >= 2 then self:_store(hit, position

1) end end end end return self endend

return pronounced as /dress = IPAddress, Subnet = Subnet, IPv4Collection = IPv4Collection, IPv6Collection = IPv6Collection,/