aboutsummaryrefslogtreecommitdiffstats

inet - an IP address mangling library

inet is meant to make it simple and fun to do IP address calculations, subnet math and similar IP address related arithmetics.

local inet = require 'inet'

-- get first address of the 3rd /64 in a /56
inet('2001:db8::/56') / 64 * 3 + 1  -- returns inet('2001:db8:0:3::1/64')

-- get last /64 in a /56
inet('2001:db8::/56') * 1 / 64 * -1 -- returns inet('2001:db8:0:ff::/64')

All examples on this page, are tested when make test is run.

Dependencies

  • Lua version 5.2, 5.3 or 5.4 beta
  • LPeg - Parsing Expression Grammars For Lua

Install

You can install lua-inet in several ways, after all it is just a pure Lua library.

From git

System-wide

$ sudo -i
# apt install lua-lpeg
# git clone https://git.2e8.dk/lua-inet /opt/lua-inet
# mkdir -p /usr/local/share/lua/5.3
# ln -s /opt/lua-inet/lua/inet /usr/local/share/lua/5.3/inet

Local user

Clone this repo to somewhere, then include this repo's lua/ in your package.path, with something like this:

package.path = '/PATH/TO/lua/?.lua;/PATH/TO/lua/?/init.lua;'..package.path

See also the LUA_INIT environment variable.

LuaRocks

If you want you can also use LuaRocks:

$ luarocks install inet

API

inet module

API Description
inet(...) Parse address and build inet4 or inet6 table
inet.is(foo) is foo an inet* table?
inet.is4(foo) is foo an inet4 table?
inet.is6(foo) is foo an inet6 table?
inet.is_set(foo) is set table?
inet.set() get new empty set instance.
inet.mixed_networks IPv6 mixed notation set
inet.lpeg LPeg patterns
inet.version API version (currently 1)

IPv6 mixed notation configuration

inet.mixed_networks can be used to configure which IPv6 networks should use mixed notation, ie. last 32 bits formatted as IPv4, as per RFC 5952 section 5.

Initially the set contains these well-known networks:

inet.mixed_networks:list() -- returns {
  inet('::ffff:0:0/96'), -- RFC 5156
  inet('64:ff9b::/96'),  -- RFC 6052
}

Common inet* API

Operator Description
+ Addition
- Subtract
/ Change mask (absolute)
^ Change mask (relative)
* Move network
< is less than
<= is less than or equal
== equals
>= is greater or equal
> is greater than
~= not equals
# number of network bits
:contains() contains
:network() extract network part of address
tostring(net) convert to network
:ipstring() ip as string without prefix
:cidrstring() format CIDR notation
:netmask() generate netmask as an address
:hostmask() generate hostmask as an address
:flip() flip the least significant network bit
:bits() return the address bits in a table
:subnets() return the amount of /n subnets
:family() return the address family (number)
:full_family() return the address family (string)

Additional inet6 methods

inet6 has these additional methods:

Operator Description
:ipstring4() string formatted in mixed notation
:ipstring6() string formatted in standard notation

set API

API Description
set:list() list networks in set
set:add() add network to set
set:remove() remove network from set
set:contains() is network contained in set?
set:flush() empty the set

Creating

There is a multitude of different ways to create inet* instances.

-- IPv4
inet('192.0.2.0')     -- returns inet('192.0.2.0/32')
inet('192.0.2.0', 24) -- returns inet('192.0.2.0/24')
inet({192,0,2,0}, 24) -- returns inet('192.0.2.0/24')
inet(3221225985, 32)  -- returns inet('192.0.2.1')

-- IPv6
inet('2001:db8::')     -- returns inet('2001:db8::/128')
inet('2001:db8::', 56) -- returns inet('2001:db8::/56')

-- its possible to wrap inet instances
inet(inet('192.0.2.0/24')) -- returns inet('192.0.2.0/24')
inet(inet('2001:db8::'))   -- returns inet('2001:db8::')

-- when wrapped additional mask takes precedence
inet(inet('192.0.2.0/32'), 24)   -- returns inet('192.0.2.0/24')
inet(inet('2001:db8::/128'), 64) -- returns inet('2001:db8::/64')

-- various error examples
inet('192.0.2.0/24', 32)  -- returns nil, 'multiple masks supplied'
inet('2001:db8::/64', 56) -- returns nil, 'multiple masks supplied'
inet('foobar')            -- returns nil, 'parse error'
inet('foo::bar')          -- returns nil, 'parse error'
inet('192.0.2.0', 33)     -- returns nil, 'invalid mask'
inet('2001:db8::', 129)   -- returns nil, 'invalid mask'

Usable in LPeg patterns

Internally inet uses LPeg to parse IP adresses, but the LPeg patterns are also available for embedding into your own LPeg patterns.

local lpeg = require 'lpeg'
local P = lpeg.P
local http_host_v6 = P('[') * inet.lpeg.ipv6 * P(']')
local http_host = http_host_v6 + inet.lpeg.ipv4
local my_http_uri = P('https://') * http_host * P('/') * -1

my_http_uri:match('https://192.0.2.0/')     -- returns inet('192.0.2.0')
my_http_uri:match('https://[2001:db8::1]/') -- returns inet('2001:db8::1')

inet.lpeg.ipv6:match('2001:db8::/64')       -- returns inet('2001:db8::/64')
inet.lpeg.ipv4:match('192.0.2.0/24')        -- returns inet('192.0.2.0/24')
inet.lpeg.ip:match('2001:db8::/64')         -- returns inet('2001:db8::/64')
inet.lpeg.ip:match('192.0.2.0/24')          -- returns inet('192.0.2.0/24')

Mangling

All of the inet* mangling operators and methods returns a new instance, and does not modify the original instance.

foo + bar

Addition

inet('192.0.2.0') + 24    -- returns inet('192.0.2.24')
inet('2001:db8::/64') + 5 -- returns inet('2001:db8::5/64')

-- mixed networks special:
inet('::ffff:0.0.0.0/96') + inet('192.0.2.24') -- returns inet('::ffff:192.0.2.24')
inet('192.0.2.24') + inet('::ffff:0.0.0.0/96') -- returns inet('::ffff:192.0.2.24')

foo - bar

Subtraction

inet('2001:db8::5/64') - 5 -- returns inet('2001:db8::/64')

inet('192.0.2.24') - inet('192.0.2.0') -- returns 24

inet('2001:db8::5/64') - inet('2001:db8::') -- returns 5

-- by calling the operator method directly additional debuging info are available:
inet('2001:db8::5/64') - inet('ffff::') -- returns nil
inet('2001:db8::5/64'):__sub(inet('ffff::'))
-- returns nil, 'out of range', { -57342, 3512, 0, 0, 0, 0, 0, 5 }

-- mixed networks special:
inet('::ffff:192.0.2.24') - inet('::ffff:0.0.0.0/96') -- returns inet('192.0.2.24')

foo / bar

Change mask (absolute)

inet('2001:db8::/32') / 64  -- returns inet('2001:db8::/64')
inet('2001:db8::1/32') / 64 -- returns inet('2001:db8::1/64')

foo ^ bar

Change mask (relative)

inet('2001:db8::/64')  ^ -8 -- returns inet('2001:db8::/56')
inet('2001:db8::2/48') ^  8 -- returns inet('2001:db8::2/56')

foo * bar

Move network

inet('2001:db8::/64')   *   1 -- returns inet('2001:db8:0:1::/64')
inet('2001:db8:1::/64') * -16 -- returns inet('2001:db8:0:fff0::/64')

foo:network()

Reset the host bits.

inet('192.0.2.4/24'):network() -- returns inet('192.0.2.0/24')

foo:netmask()

Build an IP address mask with the netmask of foo.

inet('192.0.2.0/24'):netmask() -- returns inet('255.255.255.0')
inet('2001:db8::/52'):netmask() -- returns inet('ffff:ffff:ffff:f000::')
inet('2001:db8::/56'):netmask() -- returns inet('ffff:ffff:ffff:ff00::')
inet('2001:db8::/64'):netmask() -- returns inet('ffff:ffff:ffff:ffff::')

foo:hostmask()

Build an IP address mask with the netmask of foo.

inet('192.0.2.0/24'):hostmask()   -- returns inet('0.0.0.255')
inet('2001:db8::/64'):hostmask()  -- returns inet('::ffff:ffff:ffff:ffff')
inet('2001:db8::/116'):hostmask() -- returns inet('::fff')
inet('2001:db8::/112'):hostmask() -- returns inet('::ffff')

foo:flip()

Flip the least significant network bit, to find the complimentary network.

inet('192.0.2.0/26'):flip()  -- returns inet('192.0.2.64/26')
inet('192.0.2.64/26'):flip() -- returns inet('192.0.2.0/26')
inet('192.0.2.0/25'):flip()  -- returns inet('192.0.2.128/25')

Tests

<, <=, >= and >

Compares inet instances according to the sort order.

inet('192.0.2.0/26') < inet('192.0.2.64/26') -- returns true
inet('192.0.2.0/24') < inet('192.0.2.0/26') -- returns true
inet('192.0.2.0/26') < inet('192.0.2.1/26')  -- returns true
inet('192.0.2.0/26') < inet('2001:db8::1/64')  -- returns true
inet('2001:db8::1/64') < inet('192.0.2.0/26')  -- returns false

== and ~=

Checks if two inet instances are of the same family, address and mask, or not.

inet('192.0.2.0/24') == inet('192.0.2.0/24')  -- returns true
inet('192.0.2.0/24') ~= inet('192.0.2.0/24')  -- returns false
inet('192.0.2.0/24') == inet('192.0.2.0/26')  -- returns false
inet('192.0.2.0/24') == inet('192.0.2.1/24')  -- returns false
inet('192.0.2.0/24') == inet('2001:db8::')    -- returns false

#foo

Returns the amount of significant network bits.

#inet('192.0.2.0/24')  -- returns 24
#inet('2001:db8::/48') -- returns 48

foo:contains(bar)

:contains() tests for subnet inclusion. It considers only the network parts of the two addresses, ignoring any host part, and determine whether one network part is a subnet of the other.

inet('192.0.2.0/24'):contains(inet('192.0.2.64/26')) -- returns true
inet('192.0.2.0/24'):contains(inet('192.0.2.0/26'))  -- returns true
inet('192.0.2.0/24'):contains(inet('192.0.2.0/24'))  -- returns false
inet('192.0.2.64/26'):contains(inet('192.0.2.0/24')) -- returns false

Text representation

inet6 implements RFC 5952 providing a standardized textual representation of IPv6 addresses.

tostring(foo)

String representation of foo. If foo represents a host address, then just the address is returned, otherwise CIDR notation is used.

tostring(inet('192.0.2.0/24')) -- returns '192.0.2.0/24'
tostring(inet('192.0.2.0/32')) -- returns '192.0.2.0'

For IPv6, if the network is contained by inet.mixed_networks, then mixed notation is used.

foo:cidrstring(foo)

Like tostring(foo), but always return the address in CIDR notation, as specified in RFC 4632.

inet('192.0.2.0/32'):cidrstring() -- returns '192.0.2.0/32'

foo:ipstring()

Like tostring(foo), but always returns the only the IP address, and not the mask.

inet('192.0.2.0/24'):ipstring() -- returns '192.0.2.0'

foo:ipstring4()

Like foo:ipstring(), but always uses mixed notation.

inet('2001:db8::c000:218'):ipstring()  -- returns '2001:db8::c000:218'
inet('2001:db8::c000:218'):ipstring4() -- returns '2001:db8::192.0.2.24'

foo:ipstring6()

Like tostring(foo), but never uses mixed notation.

inet('::ffff:192.0.2.24'):ipstring()  -- returns '::ffff:192.0.2.24'
inet('::ffff:192.0.2.24'):ipstring6() -- returns '::ffff:c000:218'

foo:bits(n)

Return the IP as a table with elements of n bits each.

Valid values for n are 1, 2, 4, 8, 16 or 32.

inet('192.0.2.24'):bits(32) -- returns { 3221226008 }
inet('192.0.2.24'):bits(16) -- returns { 49152, 536 }
inet('192.0.2.24'):bits(8) -- returns { 192, 0, 2, 24 }
inet('192.0.2.24'):bits(4) -- returns { 12, 0, 0, 0, 0, 2, 1, 8 }
inet('192.0.2.24'):bits(1) -- returns
  { 1, 1, 0, 0,  0, 0, 0, 0,   0, 0, 0, 0,  0, 0, 0, 0,
    0, 0, 0, 0,  0, 0, 1, 0,   0, 0, 0, 1,  1, 0, 0, 0 }

inet('2001:db8::42/64'):bits(32) -- returns { 536939960, 0, 0, 66 }
inet('::ffff:192.0.2.24'):bits(32) -- returns { 0, 0, 65535, 3221226008 }
inet('2001:db8::42/64'):bits(16) -- returns { 8193, 3512, 0, 0, 0, 0, 0, 66 }
inet('2001:db8::42/64'):bits(8) -- returns
  { 32, 1,  13, 184,  0, 0,  0, 0,  0, 0,  0, 0,  0, 0,  0, 66 }
inet('2001:db8::42/64'):bits(4) -- returns
  { 2, 0, 0, 1,  0, 13, 11, 8,   0, 0, 0, 0,  0, 0, 0, 0,
    0, 0, 0, 0,  0,  0,  0, 0,   0, 0, 0, 0,  0, 0, 4, 2 }

foo:subnets(n)

inet('192.0.2.0/24'):subnets(26) -- returns 4
inet('192.0.2.0/25'):subnets(26) -- returns 2
inet('192.0.2.0/26'):subnets(26) -- returns 1
inet('192.0.2.0/27'):subnets(26) -- returns 0.5
inet('192.0.2.0/28'):subnets(26) -- returns 0.25

inet('2001:db8::/48'):subnets(56) -- returns 256
inet('2001:db8::/56'):subnets(64) -- returns 256
inet('2001:db8::/48'):subnets(64) -- returns 65536
inet('2001:db8::/64'):subnets(56) -- returns 0.00390625

foo:family(n)

inet('192.0.2.0/24'):family()  -- returns 4
inet('2001:db8::/64'):family() -- returns 6

foo:full_family(n)

inet('192.0.2.0/24'):full_family()  -- returns 'ipv4'
inet('2001:db8::/64'):full_family() -- returns 'ipv6'

Sets

local foo = inet.set()

set:list()

List networks in set.

foo:list() -- returns {}

set:add(foo)

Add network to set.

foo:add(inet('2001:db8::/48')) -- returns true
foo:list() -- returns { inet('2001:db8::/48') }
foo:add(inet('2001:db8:1::/48')) -- returns true
foo:list() -- returns { inet('2001:db8::/47') }
foo:add(inet('192.0.2.0/24')) -- returns nil, 'invalid family'

set:remove(foo)

Remove network from set.

foo:remove(inet('2001:db8:1::/48')) -- returns true
foo:remove(inet('2001:db8:1::/48')) -- returns false
foo:list() -- returns { inet('2001:db8::/48') }

foo:remove(inet('2001:db8:0:4200::/56')) -- returns true
foo:list() -- returns {
  inet('2001:db8::/50'),
  inet('2001:db8:0:4000::/55'),
  inet('2001:db8:0:4300::/56'),
  inet('2001:db8:0:4400::/54'),
  inet('2001:db8:0:4800::/53'),
  inet('2001:db8:0:5000::/52'),
  inet('2001:db8:0:6000::/51'),
  inet('2001:db8:0:8000::/49'),
}

foo:add(inet('2001:db8:0:4200::/56')) -- returns true
foo:list() -- returns { inet('2001:db8::/48') }

set:contains(foo)

If the network is contained or equal to a network in the set, then the matching network will be returned, otherwise false is.

foo:contains(inet('2001:db8::'))           -- returns inet('2001:db8::/48')
foo:contains(inet('2001:db8::/32'))        -- returns false
foo:contains(inet('2001:db8::/48'))        -- returns inet('2001:db8::/48')
foo:contains(inet('2001:db8:1:2:3:4:5:6')) -- returns false

set:flush()

Empties the set.

foo:flush() -- returns true
foo:list()  -- returns {}

History

  • inet was brewed in Labitat in late 2014.
  • Since then it has been battle-tested in production at the danish ISP Fiberby.
  • In July 2019 inet was finally polished up and released to the world.

License

This project is licensed under GNU Lesser General Public License version 3 or later.