はじめに

情報セキュリティのログ調査では、入手したIPアドレスに情報を付加することが少なくありません。この際、付加する対象のデータベースのキーがCIDRブロックなことがあります。この記事では、IPアドレスからAS番号を引くという仮設例をもとに、IPアドレスをCIDRブロックにマッチさせる手法を取り上げます。

AS番号の一覧を入手する

まず、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ブロックで引く

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でした。ほとんど一瞬と言ってよく、十分に快適です。