Skip to content

gechandesu/netaddr

Repository files navigation

Network address processing library for V

netaddr supports IP (both IPv4 and IPv6) and EUI (EUI-48, EUI-64) addresses.

Features:

  • Parsing and validation of EUI, IP addresses and IP network addresses.
  • Converting addresses to/from different formats, e.g. various string representations, byte arrays, integers.
  • IP addresses and networks comparison.
  • IPv4-IPv6 interoperability.
  • IPv6 scopes support.
  • Parsing/creating Teredo (IPv4 over IPv6 tunneling) addresses.
  • Testing addresses and networks i.e. check is network intended for private use or not and many other tests.
  • Generating random EUI-48 (useful for virtual machines, etc).
  • Converting EUI to IPv6.
  • Calculating IP networks (both versions).
  • ...

Usage

IP address/network parsing and validation

Once you got an Ipv{4,6}Addr or Ipv{4,6}Net instance without errors — that's done, validation is passed. In the simplest case you can do:

if ip := netaddr.IpAddr.from_string('::1') {
	// address is valid
} else {
	// address is not valid
}

More concrete example that prints the address on success:

import netaddr

fn main() {
	addr := arguments()[1] or {
		panic('no such argument, specify an IP address')
	}
	ip := netaddr.IpAddr.from_string(addr) or {
		panic('${addr} is not valid IP address')
	}
	if ip is netaddr.Ipv4Net || ip is netaddr.Ipv6Net {
		panic('${ip} seems to be network, not a single host addresses')
	}
	println(addr)
}

Working with IP networks

Basic usage:

import netaddr

fn main() {
	network4 := netaddr.Ipv4Net.from_string('172.16.16.0/24')!
	network6 := netaddr.Ipv6Net.from_string('fe80:aaaa:bbbb:cccc::/64')!
	println(network4)
	println(network6)
}

The from_string() method of the Ipv4Net and Ipv6Net structs supports several different formats for network prefixes:

  • a single address without a prefix length will be considered as a network with a prefix of 32 or 128 depending on the IP version;
  • an address with an integer non-negative prefix length;
  • an address with a subnet mask;
  • an address with a host mask;
network := netaddr.Ipv4Net.from_string('203.0.113.99/0.0.0.255')!
assert network.network_address.str() == '203.0.113.0'
assert (network.host_address as netaddr.Ipv4Addr).str() == '203.0.113.99'

If host bits is set in the network address the optional host_address field will be filled with this host address. The network_address field always will contain the real network address. The host_address will equal none for single address "networks" such as 127.0.0.1/32, etc.

Iterating over network hosts

Ipv4Net and Ipv6Net has next() method that implements the V iterator mechanism which allow you use object in for loop in following maner:

network := netaddr.Ipv4Net.from_string('172.16.16.0/26')!
for host in network {
	// `host` is an Ipv4Addr instance
	if host == network.network_address || host == network.broadcast_address {
		continue
	}
	println(host)
}

Note that the iterator will iterate over all addresses in the network, including those that cannot be used as a host address: the network address and broadcast address. Exceptions are the networks with small prefixes: 31 (point-to-point) and 32 (single address) for IPv4, and 127 and 128 for IPv6 respectively.

If you just want to check is network contain some address use contains() method:

network := netaddr.Ipv4Net.from_string('172.16.0.0/26')!
addr := netaddr.Ipv4Addr.from_string('172.16.16.68')!
assert !network.contains(addr)

Networks intersection tests and subnetting

To choose the right prefix when planning a network, it is important to avoid overlapping network address spaces.

Check partial overlapping:

net_a := netaddr.Ipv4Net.from_string('100.64.0.0/22')!
net_b := netaddr.Ipv4Net.from_string('100.64.4.0/22')!
assert !net_a.overlaps(net_b)

Also you can check is the network a subnet or supernet of another one:

assert !net_a.is_subnet_of(net_b)
assert !net_a.is_supernet_of(net_b)

To split the network into equal prefixes, you can use the subnets() method:

network := netaddr.Ipv4Net.from_string('100.64.64.0/20')!
println(network)
mut subnets := []netaddr.Ipv4Net{}
for subnet in network.subnets(22)! {
	subnets << subnet
}
println(subnets)
// [100.64.64.0/22, 100.64.68.0/22, 100.64.72.0/22, 100.64.76.0/22]

IPv4-IPv6 interoperability

netaddr supports IP conversion between 4 and 6 versions in both directions.

The V REPL session below illustrates this:

>>> import netaddr
>>> ip4 := netaddr.Ipv4Addr.from_string('203.0.113.99')!
>>> ip4
203.0.113.99
>>> ip6 := ip4.ipv6()
>>> ip6
::ffff:203.0.113.99
>>> ip6.is_ipv4_mapped()
true
>>> ip6.is_ipv4_compat()
false
>>> ip6.ipv4()!
203.0.113.99
>>> ip4 == ip6.ipv4()!
true

IPv6 address cannot be converted to IPv4 if it is not the IPv4-mapped or IPv4-compatible per RFC 4291 Section 2.5.5.

Also several representation formats are supported:

>>> ip6.format(.dotted | .compact)
::ffff:203.0.113.99
>>> ip6.format(.dotted | .verbose)
0000:0000:0000:0000:0000:ffff:203.0.113.99
>>> ip6.format(.compact)
::ffff:cb00:7163
>>> ip6.format(.verbose)
0000:0000:0000:0000:0000:ffff:cb00:7163

Dealing with scoped IPv6 addresses

Ipv6Addr struct has optional zone_id field that contains the scope zone identifier if available. For example (V REPL session):

>>> ip6_scoped := netaddr.Ipv6Addr.from_string('fe80::d08e:6658:38bd:6391%wlan0')!
>>> ip6_scoped
fe80::d08e:6658:38bd:6391%wlan0
>>> ip6_scoped.zone_id
Option('wlan0')
>>> zone_id := ip6_scoped.zone_id as string
>>> zone_id
wlan0

For creating scoped address from big.Integer, u8, u16, etc use the optional zone_id parameter. e.g.:

// vfmt off
new := netaddr.Ipv6Addr.new(
	0xfe80, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x1234,
	zone_id: 'eth0'
)!
from_u8 := netaddr.Ipv6Addr.from_octets(
	[
		u8(0xfe), 0x80,
		0x0, 0x0,
		0x0, 0x0,
		0x0, 0x0,
		0x0, 0x0,
		0x0, 0x0,
		0x0, 0x0,
		0x12, 0x34
	]!,
	zone_id: 'eth0'
)!
// vfmt on
println(new) // fe80::1234%eth0
println(from_u8) // fe80::1234%eth0

Also you can create new IPv6 address with zone_id from existing Ipv6Addr instance:

>>> ip6 := netaddr.Ipv6Addr.from_string('fe80::d08e:6658:38bd:6391')!
>>> new_ip6 := ip6.with_scope('eth1')!
>>> new_ip6
fe80::d08e:6658:38bd:6391%eth1

Scoped IPv6 networks are supported, but Ipv6Net struct does not have own zone_id field, refer to it's network_address as follows:

>>> ip6net := netaddr.Ipv6Net.from_string('fe80::%eth1/64')!
>>> ip6net
fe80::%eth1/64
>>> ip6net.network_address.zone_id
Option('eth1')

Getting global unicast IPv6 from EUI-48

This is a slightly synthetic example that shows how you can automatically get a global unicast IPv6 address for a host given the network prefix.

// Known network prefix
network := netaddr.Ipv6Net.from_string('2001:0db8::/64')!
// Lets generate random EUI-48
eui := netaddr.Eui48.random()
// ipv6() method converts EUI-48 to Modified EUI-64 and appends it to prefix per RFC 4291
ip := eui.ipv6(network.network_address)!
println(ip) // 2001:db8::8429:6bff:fedc:ef8b

Note that using EUI in IPv6 address may cause security issues. See RFC 4941 for details.

License

netaddr is released under LGPL 3.0 or later license.

SPDX Lincese ID: LGPL-3.0-or-later.