情報セキュリティのログ調査では、入手したIPアドレスに情報を付加することが少なくありません。この際、付加する対象のデータベースのキーがCIDRブロックなことがあります。この記事では、IPアドレスからAS番号を引くという仮設例をもとに、IPアドレスをCIDRブロックにマッチさせる手法を取り上げます。
まず、CIDRブロックとAS番号とが組になったデータが必要です。「IP2Location™ LITE IP-ASN Database」や「Free IP address to ASN database」でテキストファイルをダウンロードすることもできますが、astoolsパッケージを用いるのが最も簡単でしょう。astools::routeviews_latest()は、最新のCAIDA RouteViewsを入手してデータフレームを返します。
当日のRouteViewsを入手してfeatherフォーマットで出力し、データフレームを確認します。
library(tidyverse)
library(arrow)
library(astools)
file_name <- paste0("routeviews_", format(Sys.Date(), "%Y-%m-%d"), ".feather")
file_path <- paste0("data/", file_name)
if(!file.exists(file_path)){
if(!dir.exists("data")){dir.create("data")}
astools::routeviews_latest() %>%
arrow::write_feather(file_path)
}
rv_df <- arrow::read_feather(file_path)
rv_df
## # A tibble: 880,411 x 6
## cidr asn minimum_ip maximum_ip min_numeric max_numeric
## <chr> <chr> <chr> <chr> <dbl> <dbl>
## 1 1.0.0.0/24 13335 1.0.0.0 1.0.0.255 16777216 16777471
## 2 1.0.4.0/22 56203 1.0.4.0 1.0.7.255 16778240 16779263
## 3 1.0.4.0/24 56203 1.0.4.0 1.0.4.255 16778240 16778495
## 4 1.0.5.0/24 56203 1.0.5.0 1.0.5.255 16778496 16778751
## 5 1.0.6.0/24 56203 1.0.6.0 1.0.6.255 16778752 16779007
## 6 1.0.7.0/24 56203 1.0.7.0 1.0.7.255 16779008 16779263
## 7 1.0.16.0/24 2519 1.0.16.0 1.0.16.255 16781312 16781567
## 8 1.0.64.0/18 18144 1.0.64.0 1.0.127.255 16793600 16809983
## 9 1.0.128.0/17 23969 1.0.128.0 1.0.255.255 16809984 16842751
## 10 1.0.128.0/18 23969 1.0.128.0 1.0.191.255 16809984 16826367
## # ... with 880,401 more rows
CIDRで引くためには、iptoolsパッケージが有用です。ランダムにIPアドレスを5つとり、それらのAS番号を求めてみます。
rv_trie <- astools::as_asntrie(rv_df)
library(iptools)
set.seed(1234)
ips <- tibble(ip_addr = iptools::ip_random(5))
ips %>%
mutate(asn = iptools::ip_to_asn(rv_trie, ip_addr))
## # A tibble: 5 x 2
## ip_addr asn
## <chr> <chr>
## 1 15.83.158.3 <NA>
## 2 79.104.227.188 <NA>
## 3 77.196.196.9 15557
## 4 79.139.186.45 25513
## 5 109.121.175.196 206129
CIDRルックアップには基数探索の利用が有効です。その基数木を作っている部分が、astools::as_asntrie()にあたります。
実は私はasntoolsパッケージの存在を最近まで知らなかったので、基数木を手で作っていました。こんな感じで作れます。
asn_tbl <- tribble(
~cidr, ~asn,
"31.224.0.0/11", "3320",
"61.94.24.0/21", "17974",
"106.244.0.0/14", "3786",
"113.24.0.0/14", "4134",
"114.46.0.0/16", "3462") %>%
separate(cidr, c("ip", "mask"), "/") %>%
mutate(prefix = str_sub(iptools::ip_to_binary_string(ip), 1, mask))
asn_tbl
## # A tibble: 5 x 4
## ip mask asn prefix
## <chr> <chr> <chr> <chr>
## 1 31.224.0.0 11 3320 00011111111
## 2 61.94.24.0 21 17974 001111010101111000011
## 3 106.244.0.0 14 3786 01101010111101
## 4 113.24.0.0 14 4134 01110001000110
## 5 114.46.0.0 16 3462 0111001000101110
ここまでで、CIDRブロックからプレフィックスを切り出しました。このプレフィックスをキーにして基数木を作ります。triebeardパッケージはiptoolsが依存しているので、iptoolsと一緒に読み込まれます。
asn_trie <- triebeard::trie(asn_tbl$prefix, asn_tbl$asn)
CIDRブロックのマッチをAS番号以外にも使用したいことがあるかもしれません。たとえばWhois情報です。ip_to_asn()関数の値の部分はAS番号である必要はないので、Whois情報でも同じ要領で使えます。基数木の長所は、探索範囲が大きくなっても所要時間があまり変わらないことです。Whois情報だと200万行を超えますから、長所が生きてきます。
むかし私は、AS番号を引くために自作のルックアップ関数を使っていました。自作関数だと1つのIPアドレスあたり20 msほどかかってしまい、わずか1,000個のマッチで20秒になります。これは実用に耐えないと思って調べ始め、基数木を知り、そこからBob Rudios氏(asntoolsやiptoolsの作者)のブログ記事「Slaying CIDR Orcs with Triebeard (a.k.a. fast trie-based ‘IPv4-in-CIDR’ lookups in R)」に辿り着きました。
基数木を使った場合、私の環境において1,000個のマッチにかかる平均時間は6.39 msでした。ほとんど一瞬と言ってよく、十分に快適です。