close
close

first Drop

Com TW NOw News 2024

news

IPv4 Components in APL | R-bloggers

At a recent Meetup focused on APL, someone posed the challenge of breaking down the components of an IPv4 address using an APL language. That inspired me to learn more about how that works in general and how I could do the processing in APL myself.

The person who posed the challenge had tackled it using J, which I’m only vaguely familiar with, but it gave me a chance to learn a bit more about it. It’s not all that different from the Dyalog APL I’m more familiar with; it uses a standard ASCII input with many of the same ideas – for example, determining whether a year is a leap year or not, as I recently explored

In Dyalog APL:

⍝ Dyalog APL
      leapyear ← {(80∨⍵) > 50∨⍵}
      years ← 1890+⍳30
      (leapyear years) ⌿ years
1892 1896 1904 1908 1912 1916 1920

⍝ or tacit
      leapyear ← 80∘∨ > 50∘∨

compared to J:

NB. J
   leapyear =: {{ (80 +. y) > (50 +. y) }}
   years =: 1890 + i.31
   (leapyear years) # years
1892 1896 1904 1908 1912 1916 1920

NB. or tatic
   leapyear =: 80&+. > 50&+.

The connection between these two is quite easy to see.

I don’t think we went through the J-solution for splitting up the components of an IPv4 address, but I did take a stab at a Dyalog APL solution during the meeting. We went through that and I’ve made improvements to it since then.

The problem as stated was – given an IPv4 address, for example ‘192.0.2.63’ and a subnet mask in CIDR notation (for example /24), can we identify the different network components?

This is a fun problem because it potentially involves arrays – maybe we should start with what this means. I’m not an expert in this myself, but explaining things is a great way to learn more, so feel free to correct me at any time.

I started with this guide that I to know There is already an error in one of the images. Can you find it?

An IPv4 address consists of four octets separated by dots, with each number representing 8 bits (hence ‘octet’), which in binary means 8 1s or 0s for a maximum value of 255

192 = 11000000
    = 1x(2^8) + 1x(2^7) + 0x(2^6) + 0x(2^5) + ... 
    = 256 + 128 + 0

So we have four of these sets of 8 binary values ​​that represent an address.

The subnet mask is described by the CIDR block and essentially represents how many 1s are at the beginning of an address, so if the mask is ‘255.0.0.0’ then it would be

11111111 00000000 00000000 00000000

what is 8 1s, so it would be /8. In the same way /26 would have 26 1s and converting from binary to decimal would produce a mask of ‘255,255,255,192’.

So, given an address and a CIDR block, what is the mask?

First we need to convert our address from a string to an array of binary digits. One way to partition a string by a character in APL is

      '.'(≠⊆⊢)'192.0.2.63'
192  0  2  63      

and we can convert this array of strings to numbers using ‘eval’

      ⍎¨'.'(≠⊆⊢)'192.0.2.63'
192 0 2 63

Converting to binary in APL is as simple as ‘decoding’ with a base of 8 2S

      2 2 2 2 2 2 2 2 ⊤ ⍎¨'.'(≠⊆⊢)'192.0.2.63'
1 0 0 0
1 0 0 0
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 1 1
0 0 0 1

but of course we can write all those things 2s with either (8⍴2) (a value of ‘reform’ 2 up to length 8) or (8/2) (‘repeat’ 2 8 times) so

      (8⍴2)⊤⍎¨'.'(≠⊆⊢)'192.0.2.63'
1 0 0 0
1 0 0 0
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 1 1
0 0 0 1

That gives us the binary sequences for each of the octets as columns of an array. It’s a lot to type out each time, though, so we can create a function that takes a proper argument

      asbin←{(8/2)⊤⍎¨'.'(≠⊆⊢)⍵}
      asbin '192.0.2.63'
1 0 0 0
1 0 0 0
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 1 1
0 0 0 1

Of course, if we wanted to go the other way and view this as an IP address made up of octets, we could “paste” the values ​​(converted back to integers) with dots in between using

      asoct←2∘⊥
      asip←{∊(⍕¨⍵),¨'.' '.' '.' ''}

The first of these creates a “curryed” (partially applied) ‘decode’ with radix 2while the second ‘format’ the values ​​into the given pattern, so

      asoct asbin '192.0.2.63'
192 0 2 63

      asip asoct asbin '192.0.2.63'
192.0.2.63

Cool, then we can do this back and forth.

The subnet mask is a series of 1is filled with 32 values ​​with 0s which we can write as

      mask←{⍉4 8 ⍴ 1=⍸⍵ 0 (32-⍵)}

which creates a 4×8 array of values ​​filled with the appropriate number of 1S

      mask 26
1 1 1 1
1 1 1 1
1 1 1 0
1 1 1 0
1 1 1 0
1 1 1 0
1 1 1 0
1 1 1 0

With this new feature we can also view this subnet mask

      asoct mask 26
255 255 255 192

      asip asoct mask 26
255.255.255.192

The ‘network address’ for this address is found by a bitwise AND between this mask and the IP address, and APL has a built-in ‘and’

      (mask 26) ∧ asbin '192.0.2.63'
1 0 0 0
1 0 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 0

      asip asoct (mask 26) ∧ asbin '192.0.2.63'
192.0.2.0

The ‘broadcast address’ is found by a bitwise OR between the reversed of the mask and the IP address

      (~mask 26) ∨ asbin '192.0.2.63'
1 0 0 0
1 0 0 0
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 1 1
0 0 0 1

      asip asoct (~mask 26) ∨ asbin '192.0.2.63'
192.0.2.63

Looking at what we have so far, we can write some functions

      asip←{∊(⍕¨⍵),¨'.' '.' '.' ''}
      asoct←2∘⊥
      mask←{⍉4 8 ⍴ 1=⍸⍵ 0 (32-⍵)}
      smask←{asip asoct mask ⍵}
      asbin←{(8/2)⊤⍎¨'.'(≠⊆⊢)⍵}
      netaddr←{asip asoct (mask ⍺) ∧ asbin ⍵}
      bcast←{asip asoct (~mask ⍺) ∨ asbin ⍵}

and try it at a few different addresses

      ip←'192.168.0.1'
      smask 8
255.0.0.0

      26 netaddr ip
192.168.0.0

      26 bcast ip
192.168.0.63

      ip←'142.250.70.174'
      
      16 netaddr ip
142.250.0.0

      16 bcast ip
142.250.255.255

Cool!

We can also calculate the number of hosts that can be assigned, since that is just 2 to the power of the number of host bits (non-network bits), minus the network and broadcast addresses.

      nhosts←{¯2+2*(32-⍵)}
      nhosts 26
62

We could list the entire range of host IPs, except we need to compensate for the network and broadcast addresses. Time to create some utilities

      netutil←{asoct (mask ⍺) ∧ asbin ⍵}
      butil←{asoct (~mask ⍺) ∨ asbin ⍵}

      bcast1←{x←⍺ butil ⍵ ⋄ x(4)←x(4)-1 ⋄ asip x}
      netaddr1←{x←⍺ netutil ⍵ ⋄ x(4)←x(4)+1 ⋄ asip x}
      
      iprange←{n←⍺ netaddr1 ⍵ ⋄ b←⍺ bcast1 ⍵ ⋄ n,'-',b}
      
      26 iprange '192.0.2.63'
192.0.2.1-192.0.2.62

That seems like a good set of tools – and a great opportunity to learn how Dyalog APL wraps things up in namespaces. One way is to write the functions to a file, e.g. SubnetCalc.dyalog as

:Namespace SubnetCalc
    asip←{∊(⍕¨⍵),¨'.' '.' '.' ''}
    asoct←{2⊥⍵}
    mask←{⍉4 8 ⍴ 1=⍸⍵ 0 (32-⍵)}
    nhosts←{¯2+2*(32-⍵)}
    smask←{asip asoct mask ⍵}
    asbin←{(8/2)⊤⍎¨'.'(≠⊆⊢)⍵}
    netutil←{asoct (mask ⍺)∧asbin ⍵}
    netaddr←{asip ⍺ netutil ⍵}
    netaddr1←{x←⍺ netutil ⍵ ⋄ x(4)←x(4)+1 ⋄ asip x}
    butil←{asoct (~mask ⍺)∨asbin ⍵}
    bcast←{asip ⍺ butil ⍵}
    bcast1←{x←⍺ butil ⍵ ⋄ x(4)←x(4)-1 ⋄ asip x}
    iprange←{n←⍺ netaddr1 ⍵ ⋄ b←⍺ bcast1 ⍵ ⋄ n,'-',b}
:EndNamespace

(noting that I should use explicit defuns instead of just implicit calls) and then load that into the RIDE editor session with

⎕FIX '/path/to/project/SubnetCalc.dyalog'

and give it a shorter name if desired

'ip' ⎕NS SubnetCalc

Now I can call my functions even faster

      ip.smask 26
255.255.255.192

      google←'142.250.70.174'

      26 ip.iprange google
142.250.70.129-142.250.70.190

Dyalog recently announced its own package infrastructure, Tatin, which may come as a surprise to those more familiar with newer languages, but it’s actually one of the First package ecosystems for an APL language I know. I want to find out if my ‘toy’ package is too simplistic to share, or if it’s worth learning the tricks of the trade. Right now all the packages in that system are internally sourced, but presumably it would be opened up to external users once it stabilizes.

This was all a lot of fun and I learned a lot. How would I do this in another language? Well, there’s almost always an R package for something, and sure enough, there’s an {ipaddress} package on CRAN that has all of this functionality plus more, although it seems to rely on compiling some C++ code to do it.

library(ipaddress)

ip 
## (1) 192.0.2.0
broadcast_address(ipn)               # c.f. bcast
## 
## (1) 192.0.2.63
netmask(ipn)                         # c.f. smask
## 
## (1) 255.255.255.192
hostmask(ipn)
## 
## (1) 0.0.0.63
range(hosts(ipn))                    # c.f. iprange
## 
## (1) 192.0.2.1  192.0.2.62
is_within(ip, ipn)
## (1) TRUE

One of the advantages of the APL approach, I think, is that you can see exactly what the function does. Often there is no point in naming a function, because any useful name you give it tends to contain more characters than the actual implementation. If you dig even a little bit into this package, it is not immediately clear where the processing is happening. Sometimes I worry that we are adding too many layers of increasingly high abstractions, but I realize that sometimes we can gain a lot from it.

I wouldn’t use my APL code in production. There are no checks or error handling, but building it helped me understand what’s going on between all those ones and zeros.

As always, if you have any comments, suggestions, or improvements, feel free to do so in the comments section below. You can also reach me on Mastodon.

devtools::session_info()
## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value
##  version  R version 4.3.3 (2024-02-29)
##  os       Pop!_OS 22.04 LTS
##  system   x86_64, linux-gnu
##  ui       X11
##  language (EN)
##  collate  en_AU.UTF-8
##  ctype    en_AU.UTF-8
##  tz       Australia/Adelaide
##  date     2024-08-22
##  pandoc   3.2 @ /usr/lib/rstudio/resources/app/bin/quarto/bin/tools/x86_64/ (via rmarkdown)
## 
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package     * version date (UTC) lib source
##  blogdown      1.19    2024-02-01 (1) CRAN (R 4.3.3)
##  bookdown      0.36    2023-10-16 (1) CRAN (R 4.3.2)
##  bslib         0.6.1   2023-11-28 (3) CRAN (R 4.3.2)
##  cachem        1.0.8   2023-05-01 (3) CRAN (R 4.3.0)
##  callr         3.7.3   2022-11-02 (3) CRAN (R 4.2.2)
##  cli           3.6.1   2023-03-23 (1) CRAN (R 4.3.3)
##  crayon        1.5.2   2022-09-29 (3) CRAN (R 4.2.1)
##  devtools      2.4.5   2022-10-11 (1) CRAN (R 4.3.2)
##  digest        0.6.34  2024-01-11 (3) CRAN (R 4.3.2)
##  ellipsis      0.3.2   2021-04-29 (3) CRAN (R 4.1.1)
##  evaluate      0.23    2023-11-01 (3) CRAN (R 4.3.2)
##  fastmap       1.1.1   2023-02-24 (3) CRAN (R 4.2.2)
##  fs            1.6.3   2023-07-20 (3) CRAN (R 4.3.1)
##  glue          1.7.0   2024-01-09 (1) CRAN (R 4.3.3)
##  htmltools     0.5.7   2023-11-03 (3) CRAN (R 4.3.2)
##  htmlwidgets   1.6.2   2023-03-17 (1) CRAN (R 4.3.2)
##  httpuv        1.6.12  2023-10-23 (1) CRAN (R 4.3.2)
##  icecream      0.2.1   2023-09-27 (1) CRAN (R 4.3.2)
##  ipaddress   * 1.0.2   2023-12-01 (1) CRAN (R 4.3.3)
##  jquerylib     0.1.4   2021-04-26 (3) CRAN (R 4.1.2)
##  jsonlite      1.8.8   2023-12-04 (3) CRAN (R 4.3.2)
##  knitr         1.45    2023-10-30 (3) CRAN (R 4.3.2)
##  later         1.3.1   2023-05-02 (1) CRAN (R 4.3.2)
##  lifecycle     1.0.4   2023-11-07 (1) CRAN (R 4.3.3)
##  magrittr      2.0.3   2022-03-30 (1) CRAN (R 4.3.3)
##  memoise       2.0.1   2021-11-26 (3) CRAN (R 4.2.0)
##  mime          0.12    2021-09-28 (3) CRAN (R 4.2.0)
##  miniUI        0.1.1.1 2018-05-18 (1) CRAN (R 4.3.2)
##  pkgbuild      1.4.2   2023-06-26 (1) CRAN (R 4.3.2)
##  pkgload       1.3.3   2023-09-22 (1) CRAN (R 4.3.2)
##  prettyunits   1.2.0   2023-09-24 (3) CRAN (R 4.3.1)
##  processx      3.8.3   2023-12-10 (3) CRAN (R 4.3.2)
##  profvis       0.3.8   2023-05-02 (1) CRAN (R 4.3.2)
##  promises      1.2.1   2023-08-10 (1) CRAN (R 4.3.2)
##  ps            1.7.6   2024-01-18 (3) CRAN (R 4.3.2)
##  purrr         1.0.2   2023-08-10 (3) CRAN (R 4.3.1)
##  R6            2.5.1   2021-08-19 (1) CRAN (R 4.3.3)
##  Rcpp          1.0.11  2023-07-06 (1) CRAN (R 4.3.2)
##  remotes       2.4.2.1 2023-07-18 (1) CRAN (R 4.3.2)
##  rlang         1.1.4   2024-06-04 (1) CRAN (R 4.3.3)
##  rmarkdown     2.25    2023-09-18 (3) CRAN (R 4.3.1)
##  rstudioapi    0.15.0  2023-07-07 (3) CRAN (R 4.3.1)
##  sass          0.4.8   2023-12-06 (3) CRAN (R 4.3.2)
##  sessioninfo   1.2.2   2021-12-06 (1) CRAN (R 4.3.2)
##  shiny         1.7.5.1 2023-10-14 (1) CRAN (R 4.3.2)
##  stringi       1.8.3   2023-12-11 (3) CRAN (R 4.3.2)
##  stringr       1.5.1   2023-11-14 (3) CRAN (R 4.3.2)
##  urlchecker    1.0.1   2021-11-30 (1) CRAN (R 4.3.2)
##  usethis       3.0.0   2024-07-29 (1) CRAN (R 4.3.3)
##  vctrs         0.6.5   2023-12-01 (1) CRAN (R 4.3.3)
##  xfun          0.41    2023-11-01 (3) CRAN (R 4.3.2)
##  xtable        1.8-4   2019-04-21 (1) CRAN (R 4.3.2)
##  yaml          2.3.8   2023-12-11 (3) CRAN (R 4.3.2)
## 
##  (1) /home/jono/R/x86_64-pc-linux-gnu-library/4.3
##  (2) /usr/local/lib/R/site-library
##  (3) /usr/lib/R/site-library
##  (4) /usr/lib/R/library
## 
## ──────────────────────────────────────────────────────────────────────────────