The IP package for working with IP addresses in R

I wrote an R package to handle Internet Protocol (IP) versions 4 and 6 addresses. It’s called IP and it is now available from CRAN.

The package is based on on the ip4r PostgreSQL extension. It provides a wide array of methods for working with both IPv4 and IPv6 addresses and ranges such as :

  • IP addresses and range parsing and validation
  • vectorized operations such as arithmetic, logical and bitwise operations
  • IP matching and lookup
  • (reverse) DNS lookup and whois databases query

Code is mostly C for increased performances. The IP package also provides an experimental support for AVX2 operations. Please refer to the Package installation section to enable AVX2 support.

What follows will highlight some features of the package. But before we get started, please note that a few (network-related only like host()) functions need OS specific system calls. As a consequence, some functions are only available for POSIX compliant OS at the moment. In addition, the IP package depends on the idn library for internationalized domain name (IDN) support. Again, please refer to the Package installation for more information.

Also, as we’re about to see, IP objects are designed to behave as much as possible as R vectors. But there are some pitfalls. Please make sure you’ve read the caveat section at the end of this post.

Package installation

As stated in the introduction, some features of the IP package depends on your environment and may therefore not be available or need to be enabled manually. This section deals with enabling the following components :

  • IDN support
  • AX2 operation support

The IP package depends on the idn library for handling Unicode characters in domain name through punycode.

On Windows, the required static libraries are downloaded from this repo.

On other platforms (-eg: GNU/Linux, Mac), the libidn needs to be installed beforehand.

## Debian, Ubuntu,…
apt-get install libidn*-dev
## Fedora, Redhat
yum install libidn-devel-*

Otherwise, the IP package will be compiled without IDN support.

AVX2 support needs to be enabled manually using configure args 

install.packages("IP", configure.args="--enable-avx2")

Of course, you’ll need a CPU that supports Intel®’s AVX2 instruction set (which is very likely to be the case with modern hardware).

You can use the ip.capabilities() to check whether IDN and AVX2 support are available :

> ip.capabilities()
AVX2  IDN 
TRUE TRUE 

Getting started

The IP package provides six different IP classes :

  • the IPv4 class (for IP version 4 addresses)
  • the IPv4r class (for IP version 4 addresses ranges)
  • the IPv6 class (for IP version 6 addresses)
  • the IPv6r class (for IP version 6 addresses ranges)
  • the IP class (for both kind of addresses)
  • the IPr class (for IP both kind of addresses ranges)

Let’s start with IPv4 input. Calling the ipv4() and ipv4r() functions creates IPv4 and IPv4r objects respectively from strings :

##
library(IP)
## IPv4
ipv4("192.168.0.0")
## IPv4 range using CIDR notation
ipv4r("192.168.0.0/16")
## same thing using dash notation
ipv4r("192.168.0.0-192.168.255.255")
## same thing using an IPv4 object and an integer giving the number of addresses in the range
ipv4r(ipv4("192.168.0.0"), as.integer(2L^16 -1) )

Likewise, the ipv6() and ipv6r() functions creates IPv6 and IPv6r objects :

## IPv6
ipv6("fe80::")
## IPv6 range using CIDR notation
ipv6r("fe80::/10")
## same thing using dash notation
ipv6r("fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff")
## same thing but overflows at the moment for argument larger than 2^64
ipv6r(ipv6("fe80::"), 2^118 -1 )

From the last example, we can see that when input fails for any reason, the IP value is NA

And, IP and IPr objects are created as follow :

## IP
(x <- ip(c("192.168.0.0", "fe80::") ) )
##
ip.version(x)
##
ip(ipv4(c("192.168.0.0", NA)), ipv6(c(NA, "fe80::")) )
##
ip(ipv4("192.168.0.0"), ipv6("fe80::"), append=T) 
## IP range using CIDR notation
ipr(c("192.168.0.0/16","fe80::/10") )
## same thing using dash notation
ipr(c( "192.168.0.0-192.168.255.255", "fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff") )

The IP package also provides direct input from integers. Note that input values are treated as unsigned integers :

##
ipv4(1L)
## this is really 4294967295
x<- ipv4(-1L)
##  
x > ipv4(0L)
##
ipv4(NA_integer_)
## same thing for IPv6
ipv6(1L)
##
x<- ipv6(-1L)
##
x > ipv6(0L)
##
ipv6(NA_integer_)

The dash notation for addresses ranges gives greater flexibility over CIDR notations as it enables input of arbitrary ranges :

##
ipv4r("192.168.0.0-192.168.0.9")
##
ipv4r("192.168.0.10-192.168.0.19")

Also, some methods are specific to certain classes. For instance, the IP package defines getters for address ranges :

## 
x <- ipv4r("192.168.0.0/16")
## this also works for IPv6r and IPr objects
lo(x) ## low end
hi(x) ## high end

as well as for IP objects :

##
x <- ip(c("192.168.0.0", "fe80::") )
##
ipv4(x) ## IPv4 part
ipv6(x) ## IPv6 part
##
ipv4(x, drop=T)
ipv6(x, drop=T)

Note that some methods only work for IPv4 and IPv6 objects and not for IP objects. Partly because some methods have not been implemented yet but mostly by design. Despite their similarities, IPv4 and IPv6 are different protocols. Therefore, at some point or another you’ll have to deal with them separately for instance when masking, sorting or matching addresses. In addition IP methods are a bit slower. And, despite fifteen years of IPv6 deployment, what you still get today is mostly IPv4 addresses in many circumstances anyway.

Working with IP addresses

IP* objects were designed to behave as much as possible like base R atomic vectors. Hence, you can input addresses from named vectors :

##
(x <- ipv4(c(
  router = '192.168.0.0'
  , host1  = '192.168.0.1'
)))
##
x
##
names(x)

Like any R vector, IP objects supports vector slicing :

##
x[1:2]
x[2:1]

and vector assignment :

##
x[3:4] <- c( host2  = '192.168.0.2', host3  = '192.168.0.3' )
##
x[5] <- c( host4  = -1062731772L)
##
data.frame(n=names(x), x=x)
##
x <- c(x, x+5)
##
names(x)[6:10] <- paste("host", 5:9, sep="")

Note that, when doing assignment, new values are automatically coerced.

In addition to vector slicing and assignment, the IP package also provides methods for

  • arithmetic : +, –
  • comparison : ==, >, <, >=, <=
  • bit manipulation : !, &, |, ^, %<<%, %>>%

Multiplication, division and modulo are not implemented yet. The !, &, | and ^ operators behave differently from their base R counterparts in that they perform bitwise operations much like in the C language :

  • ! : bitwise NOT (like C ~)
  • & : bitwise AND
  • | : bitwise OR
  • ^ : bitwise XOR
  • %<<% : left shift
  • %>>% right shift

In addition, the ipv4.netmask(n) and ipv4.hostmask(n) (and their corresponding IPv6 functions ipv6.netmask(n) and ipv6.hostmask(n)) returns a net and host mask respectively of size n.

##
x <- ipv4('192.168.0.0') 
##
((x + 1L) - ipv4(1L))==x
##
!ipv4.netmask(8)==ipv4.netmask(24)
##
ipv4r(x, x|ipv4.hostmask(16) )==ipv4r('192.168.0.0/16')
## recycle
ipv4(c(0L,1L)) + ipv4(0:5L)
##
ipv4(c(0L,1L)) < ipv4(0:5L)
## even if dimensions don't match
ipv4(c(0L,1L)) + ipv4(0:6L)

IP* arithmetic behaves like R integer arithmetic. Hence, overflow means NA :

## note: beware of operators precedence here : `+` > `!`
(!ipv4(0L) )+1L
.Machine$integer.max+1L

and so does any operation with NA

ipv4(c(NA,1L) ) == ipv4(1L)

There are no Summary (min(), max(),…) methods yet. But table() works :

##
x <- ipv4('192.168.0.0') + 0:4
x <- x[sample.int(length(x),length(x)*9, replace=T)]
##
table(x)

IPv6 and IP objects behave similarly and most of what precedes works for them (with some obvious modifications). There are some exceptions for IP objects because a few methods are still missing or do not apply like when assigning to an IP vector :

##
x=ip(c(
    router = '192.168.0.0'
    , host1  = '192.168.0.1'
))
## does not work yet
x[3:4] <- c( host2  = '192.168.0.2', host3  = '192.168.0.3' )
##  we need to convert to IP first
x[3:4] <- ip(c( host2  = '192.168.0.2', host3  = '192.168.0.3' ))
## does not work because we cannot tell the IP version
x[5] <- c( host4  = -1062731772L)

IP object can also be included in data.frames :

##
x = data.frame(ip = ipv4('0.0.0.0')+rep(1:2,2), x=1:4)
##
y = data.frame(ip = ipv4('0.0.0.0')+rep(2:1,2), x=1:4)
##
x[1:2,]
##
rbind(x, y)
##
merge(x,y, by='ip')

The IP package also provides methods for IP* lookup and DNS resolution. But we’ll cover that in another post.

A caveat

As shown by the previous examples, the IP package was designed to fit in the R environment as smoothly as possible. But there are some instances where this might fail and calling R functions with IP* as argument will not yield the expected result. Reason is that every IP* class inherits from an integer vector. So, by default, whenever R does not find a method for an IP* class, it looks for an inherited one for integer vectors.

For example, the IP package does not provide a `*` operator. In earlier versions, multiplication ended messing up the IP* object and further processing would raise something like an out-of-bound error. This has been fixed for a number of base R functions like `*` or ` /`. Unfortunately, I’m not aware of a more more generic way to block every irrelevant inherited methods because this is not a bug but a feature of inheritance in OO programming.

So, in a nutshell, when calling a non-IP defined methods on any IP* object, either convert to character or first check the result before further processing.

Please refer to the IP manual for further details.



Cite this blog post
Thomas Soubiran (2020, September 3). The IP package for working with IP addresses in R. NUMA. Retrieved May 21, 2024, from https://doi.org/10.58079/sgor

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Search OpenEdition Search

You will be redirected to OpenEdition Search