The RIP package for working with IP addresses in R
I’ve just committed a package for working with IP addresses in R on github. It’s called RIP and you can get it here.
The RIP 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 :
- 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. RIP IP objects were designed to behave as much as possible as R vectors but there are some pitfalls. Please read the caveat section at the end of this post.
The easiest way to install the package is probably the install_github() from package remotes :
You can also manually download the package source from the github repo and install it with the install.packages() command provided the stringr is already installed.
## install.packages("stringr") ## install.packages("/path/to/directory/RIP_0.5.1.tar.gz", repos = NULL, type="source")
stringr is the only package dependence.
Before we get started, first note that a few (network-related only like host()) functions need OS specific system calls. As a consequence, those functions are only available for POSIX compliant OS at the moment.
The RIP 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(RIP) ## 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, for the sake of completeness, 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 RIP 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 RIP 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 addresses 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 <- 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 RIP 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 <- c( host4 = -1062731772L)
The IP package also provides methods for IP* lookup and DNS resolution. But we’ll cover that in another post.
As shown by the previous examples, the RIP 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 RIP 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 ` /`. But I’m not sure there is a more general way to block every irrelevant inherited methods.
And there are cases where there doesn’t seem to be any solution like non-generic functions and, hence, no method signature check. For instance, the match() function is not generic. Therefore, calling it with IP* objects as arguments will not give what you want by default. I’ve tried to fix this by making match() generic but unfortunately, this only works in userland. This means that any base R functions that call match() on an IP* object will give a wrong answer (except in a few cases).
For instance, consider merging two data.frame on IP* objects. merge.data.frame() relies on the match() function to match by variables. Therefore, merging on one IP* objects will fail. In that case, you need to first manually convert the IP* by variable to character. But note that merging on two or more variable including IP* object will work because R converts multiple by arguments to character.
So, in a nutshell, when calling a non-RIP defined methods on any IP* object, either convert to character or first check the result before further processing.
Please refer to the RIP manual for further details.