CUBE SUGAR CONTAINER

技術系のこと書きます。

IPv4 アドレスの値段

今回は IPv4 アドレスの値段や、その売買に関する動向について調べてみた。

TL; DR

  • IPv4 アドレスは売買できる
  • オークションにおける IPv4 アドレスの売買単価は値上がり傾向にある
  • 2018 年 11 月 1 日現在、IPv4 アドレス一つあたり 17 ~ 20 USD ほどで取引されている
  • 近年は取引も活発になっている傾向にある

IPv4 アドレスの在庫枯渇と売買について

IPv4 アドレスの在庫が枯渇したというニュースが話題になってから、ずいぶんと時間が経ったように思う。 実際のところ IANA の中央在庫が枯渇したのは 2011 年 2 月 3 日なので、実に 7 年以上が経っている。

IANAにおけるIPv4アドレス在庫枯渇、およびJPNICの今後のアドレス分配について - JPNIC

IPv4 アドレスの在庫は、階層構造になったインターネットレジストリが管理している。 階層構造において下流に位置する組織は、上流の組織に対して必要に応じて割り当てを申請して、在庫の一部を受け取る。 しかし、在庫が枯渇してしまった上流の組織からは、新規の割り当てが制限される。 ちなみに、日本のインターネットレジストリである JPNIC の在庫は 2011 年 4 月 15 日に枯渇している。

IPv4アドレスの在庫枯渇に関して - JPNIC

新規の割り当てを受けられない状況において、ネットワーク事業者は既存の割り当て済みアドレスの利用効率を上げるなど、いくつかの対策が考えられる。 しかし、現実問題として既存の割り当て済みアドレスだけではどうにもならないようなケースも存在する。 例えば新たに IaaS のようなサービスを始めたり、設備を大幅に増強するような局面においては IPv4 アドレスが大量に必要となる恐れがある。

そのような場合の救済策として、他の事業者に割り当てられたアドレスを一定の条件下で譲り受ける (移転する) ことが可能になっている。 これは、過去に割り当てを受けたものの、既に不要となった IPv4 アドレスが企業などで死蔵されているケースなどがあるため。 もちろん、慈善事業ではないのでアドレスの移転を受ける場合には基本的に有償になると考えられる。 ただし、補足しておくとアドレスの移転ポリシーについては管轄のインターネットレジストリによって色々と事情が異なる。

そうした状況において、アドレスを買いたい事業者と売りたい事業者を仲介するブローカーも登場している。 前置きが長くなったけど、今回扱うのはそんなブローカーの一つである以下のサイト。 ここでは売却したい IPv4 アドレスのブロックがオークションにかけられている。

www.ipv4auctions.com

上記のサイトには、過去に売却された IPv4 アドレスのブロックに関する情報が記載されている。 そこで、今回はそれをスクレイピングして可視化してみることにした。 このサイトにおける売却は全世界のアドレス移転のごく一部を見ているにすぎないとはいえ、傾向については読み取れるんじゃないかと。

IPv4 アドレスの価格の推移

早速だけど、まずは IPv4 アドレスの価格の推移から。 横軸に売却された日付、縦軸に単一アドレスあたりの価格 (米ドル) をプロットした散布図にしてみた。 点の色は、中央在庫を管理している IANA の直下に位置する RIR (Regional Internet Registry: 地域インターネットレジストリ) を表している。

f:id:momijiame:20181031215956p:plain

2014 ~ 2016 年頃までは、だいたい 7 ~ 10 USD ほどで売却されていたアドレスが、最近では 17 ~ 20 USD 近くまで値上がりしていることが分かる。 仮にこの価格が下がるとすれば、もしかすると IPv4 アドレスが不要になりつつある兆しかもしれない。 他の要因ももちろんあるとはいえ IPv4 のインターネットが縮小期に入っている可能性はある。

また、このサイトでの売却は、北アメリカを管轄している ARIN (American Registry for Internet Numbers) のアドレスブロックで活発なようだ。 北アメリカはインターネット発祥の地とも言えるので、やはり初期に大きく割り当てられて死蔵されているアドレスブロックも多かったりするんだろうか?

売却されたアドレスの数・回数の推移

続いては、売却された IPv4 アドレスの数や回数の推移を時系列でプロットしてみた。

まず、以下は売却された IPv4 アドレスの数を /24 のブロック (232-24 = 256 個) 単位で時系列の棒グラフにしている。

f:id:momijiame:20181031220016p:plain

上記を見ると、売却される IPv4 アドレスの数は近年増加傾向にあることが分かる。

また、同様に売却の件数についても月ごとにプロットしてみた。

f:id:momijiame:20181031220023p:plain

こちらも近年は増加傾向にあることが見て取れる。

売却されたブロックサイズの推移

続いては、売却されたアドレスブロックのサイズについても月ごとに集計してプロットしてみた。 月単位での CIDR 長の最大値 (max)、最小値 (min)、平均値 (mean)、中央値 (median) を示している。

f:id:momijiame:20181031220006p:plain

これについては、時系列での傾向は特に見受けられなかった。 最も小さなブロックのサイズが期間を通じて /24 で固定なのは、それより小さいと BGP の経路フィルタで落とされる可能性が高いためだろうか?

ブロックサイズごとの価格の違い

ブロックサイズについて見たところで、次はブロックサイズと価格の関係をプロットしてみる。

f:id:momijiame:20181117192921p:plain

/17 だけやけに安いのは、そもそもオークションに出品された件数が少なく、それも初期に固まっているためだろう。 それ以外のブロックサイズについては、サイズごとに極端な傾向は見られない。 ただ、/20 ~ /24 の間に、ブロックサイズが大きいほどアドレス単価の中央値が安くなる傾向が見られるのは意外だった。 それというのも、ブロックサイズは大きい方が使い勝手が良いため、大きいほど高いのかと思っていた。 ブロックサイズが大きくなるほど取引にかかる総額も大きくなるため、価格が抑制されやすいという可能性はあるかもしれない。 あるいは、価格が上昇すると共に単に大きめのブロックサイズが出品されにくくなっているという疑似相関だろうか。

まとめ

今回は IPv4 アドレスのオークションサイトのデータをスクレイピングしてグラフとして可視化してみた。 最近は IPv4 アドレスの値段が値上がり傾向にあることが分かった。

備考

今回スクレイピングと可視化に使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.13.6
BuildVersion:   17G65
$ python -V          
Python 3.6.7

スクリプトを動作させるのに必要となるパッケージは次の通り。

$ pip install pandas lxml matplotlib tqdm percache

以下はスクレイピングと可視化に用いたスクリプト。

IPv4 アドレスの価格の推移

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import datetime
import time

import percache
from tqdm import tqdm
import pandas as pd
from matplotlib import pyplot as plt


def _fetch(year):
    """オークションの履歴をスクレイピングする"""
    base_url = 'https://www.ipv4auctions.com/customer/account/previous?year={yyyy}'
    url = base_url.format(yyyy=year)
    dfs = pd.read_html(url)
    return dfs[1]


# スクレイピングした内容をローカルのディスクにキャッシュする
cache = percache.Cache('df-cache')


@cache
def auction_history(start_year, stop_year, fetch_interval=2):
    """各年の情報を結合して一枚のデータフレームにする"""
    dfs = []
    years = range(start_year, stop_year + 1)
    for year in tqdm(years):
        year_df = _fetch(year)
        dfs.append(year_df)
        # サービスに負荷をかけないようにインターバルをかける
        time.sleep(fetch_interval)

    # データフレームを結合する
    return pd.concat(dfs, axis=0)


def filter_outlier(df_):
    """データフレームから外れ値を取り除く"""
    # ブロックサイズが適切にパースできるものだけに絞る
    prefix_series = df_['BLOCK'].apply(lambda x: x[1:])
    return df_[prefix_series.str.isnumeric()]


def sold_region(df_):
    """取引された地域をパースする"""
    def parse(x):
        # 末尾にカンマの入ったデータが混ざっていたので取り除く
        return x[:-1] if x[-1] == ',' else x
    return df_.REGION.apply(parse)


def sold_date(df_):
    """取引された日付をパースする"""
    def parse(x):
        datetime_ = datetime.datetime.strptime(x, '%Y-%m-%d')
        return datetime.date(datetime_.year, datetime_.month, datetime_.day)
    return df_['SOLD DATE'].apply(parse)


def sold_yyyymm(df_):
    """取引された年・月をパースする"""
    def to_str(dt):
        return dt.strftime('%Y-%m')
    dt_series = pd.to_datetime(df_['SOLD DATE'])
    return dt_series.apply(to_str)


def price_per_addr(df_):
    """取引された価格をパースする"""
    def parse(x):
        ex_comma_x = x.replace(',', '')
        return float(ex_comma_x[1:])
    return df_['PRICE PER ADDRESS'].apply(parse)


def main():
    # データ取得・描画範囲
    YEAR_RANGE = (2014, 2018)

    # オークションの履歴を取得する
    df = auction_history(YEAR_RANGE[0], YEAR_RANGE[-1])

    # 外れ値を取り除く
    df = filter_outlier(df)

    # 可視化に使うデータを取り出す
    df = df.assign(sold_date=sold_date(df))
    df = df.assign(price_per_addr=price_per_addr(df))
    df = df.assign(sold_region=sold_region(df))
    df = df.assign(yyyymm=sold_yyyymm(df))

    # 地域ごとに集計・可視化する
    fig, ax = plt.subplots()
    regions = pd.unique(df.sold_region)
    for region in regions:
        region_df = df[df.sold_region == region]
        ax.plot_date(region_df.sold_date, region_df.price_per_addr, label=region)

    ax.set_title('IPv4 auction price graph')
    ax.legend()
    ax.set_ylabel('price per address (USD)')
    ax.set_xlabel('sold date')
    plt.show()


if __name__ == '__main__':
    main()

売却されたアドレスの数の推移

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time

import percache
from tqdm import tqdm
import pandas as pd
from matplotlib import pyplot as plt


def _fetch(year):
    """オークションの履歴をスクレイピングする"""
    base_url = 'https://www.ipv4auctions.com/customer/account/previous?year={yyyy}'
    url = base_url.format(yyyy=year)
    dfs = pd.read_html(url)
    return dfs[1]


# スクレイピングした内容をローカルのディスクにキャッシュする
cache = percache.Cache('df-cache')


@cache
def auction_history(start_year, stop_year, fetch_interval=2):
    """各年の情報を結合して一枚のデータフレームにする"""
    dfs = []
    years = range(start_year, stop_year + 1)
    for year in tqdm(years):
        year_df = _fetch(year)
        dfs.append(year_df)
        # サービスに負荷をかけないようにインターバルをかける
        time.sleep(fetch_interval)

    # データフレームを結合する
    return pd.concat(dfs, axis=0)


def class_c_blocks(df_):
    """取引された /24 ブロックの数を数える"""
    def parse(x):
        return 2 ** (24 - int(x[1:]))
    return df_['BLOCK'].apply(parse)


def filter_outlier(df_):
    """データフレームから外れ値を取り除く"""
    # ブロックサイズが適切にパースできるものだけに絞る
    prefix_series = df_['BLOCK'].apply(lambda x: x[1:])
    return df_[prefix_series.str.isnumeric()]


def sold_yyyymm(df_):
    """取引された年・月をパースする"""
    def to_str(dt):
        return dt.strftime('%Y-%m')
    dt_series = pd.to_datetime(df_['SOLD DATE'])
    return dt_series.apply(to_str)


def region(df_):
    """取引された地域をパースする"""
    def parse(x):
        # 末尾にカンマの入ったデータが混ざっていたので取り除く
        return x[:-1] if x[-1] == ',' else x
    return df_.REGION.apply(parse)


def main():
    # データ取得・描画範囲
    YEAR_RANGE = (2014, 2018)

    # オークションの履歴を取得する
    df = auction_history(YEAR_RANGE[0], YEAR_RANGE[-1])

    # 外れ値を取り除く
    df = filter_outlier(df)

    # 必要なデータを取り出す
    df = df.assign(blocks=class_c_blocks(df))
    df = df.assign(yyyymm=sold_yyyymm(df))
    df = df.assign(region=region(df))

    # 集計する
    pivot_df = df.pivot_table(index=['yyyymm'], columns=['region'], values=['blocks'], aggfunc='sum', fill_value=0)

    # 可視化する
    fig, ax = plt.subplots()
    for _, data in pivot_df.items():
        region_name = data.name[1]
        ax.bar(data.index, data, label=region_name)

    ax.set_title('Number of /24 blocks sold in the auction')
    ax.legend()
    ax.set_ylabel('sold /24 blocks')
    ax.set_xlabel('month')
    ax.xaxis.set_ticks(['{y}-06'.format(y=y) for y in range(YEAR_RANGE[0], YEAR_RANGE[-1] + 1)])
    plt.show()


if __name__ == '__main__':
    main()

売却された回数の推移

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time

import percache
from tqdm import tqdm
import pandas as pd
from matplotlib import pyplot as plt


def _fetch(year):
    """オークションの履歴をスクレイピングする"""
    base_url = 'https://www.ipv4auctions.com/customer/account/previous?year={yyyy}'
    url = base_url.format(yyyy=year)
    dfs = pd.read_html(url)
    return dfs[1]


# スクレイピングした内容をローカルのディスクにキャッシュする
cache = percache.Cache('df-cache')


@cache
def auction_history(start_year, stop_year, fetch_interval=2):
    """各年の情報を結合して一枚のデータフレームにする"""
    dfs = []
    years = range(start_year, stop_year + 1)
    for year in tqdm(years):
        year_df = _fetch(year)
        dfs.append(year_df)
        # サービスに負荷をかけないようにインターバルをかける
        time.sleep(fetch_interval)

    # データフレームを結合する
    return pd.concat(dfs, axis=0)


def class_c_blocks(df_):
    """取引された /24 ブロックの数を数える"""
    def parse(x):
        return 2 ** (24 - int(x[1:]))
    return df_['BLOCK'].apply(parse)


def filter_outlier(df_):
    """データフレームから外れ値を取り除く"""
    # ブロックサイズが適切にパースできるものだけに絞る
    prefix_series = df_['BLOCK'].apply(lambda x: x[1:])
    return df_[prefix_series.str.isnumeric()]


def sold_yyyymm(df_):
    """取引された年・月をパースする"""
    def to_str(dt):
        return dt.strftime('%Y-%m')
    dt_series = pd.to_datetime(df_['SOLD DATE'])
    return dt_series.apply(to_str)


def region(df_):
    """取引された地域をパースする"""
    def parse(x):
        # 末尾にカンマの入ったデータが混ざっていたので取り除く
        return x[:-1] if x[-1] == ',' else x
    return df_.REGION.apply(parse)


def main():
    # データ取得・描画範囲
    YEAR_RANGE = (2014, 2018)

    # オークションの履歴を取得する
    df = auction_history(YEAR_RANGE[0], YEAR_RANGE[-1])

    # 外れ値を取り除く
    df = filter_outlier(df)

    # 必要なデータを取り出す
    df = df.assign(blocks=class_c_blocks(df))
    df = df.assign(yyyymm=sold_yyyymm(df))
    df = df.assign(region=region(df))

    # 集計する
    pivot_df = df.pivot_table(index=['yyyymm'], columns=['region'], values=['blocks'], aggfunc='count', fill_value=0)

    # 可視化する
    fig, ax = plt.subplots()
    for _, data in pivot_df.items():
        region_name = data.name[1]
        ax.bar(data.index, data, label=region_name)

    ax.set_title('Number of sold counts in the auction')
    ax.legend()
    ax.set_ylabel('sold count')
    ax.set_xlabel('month')
    ax.xaxis.set_ticks(['{y}-06'.format(y=y) for y in range(YEAR_RANGE[0], YEAR_RANGE[-1] + 1)])
    plt.show()


if __name__ == '__main__':
    main()

売却されたブロックサイズの推移

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time

import percache
from tqdm import tqdm
import pandas as pd
import matplotlib.pyplot as plt


def _fetch(year):
    """オークションの履歴をスクレイピングする"""
    base_url = 'https://www.ipv4auctions.com/customer/account/previous?year={yyyy}'
    url = base_url.format(yyyy=year)
    dfs = pd.read_html(url)
    return dfs[1]


# スクレイピングした内容をローカルのディスクにキャッシュする
cache = percache.Cache('df-cache')


@cache
def auction_history(start_year, stop_year, fetch_interval=2):
    """各年の情報を結合して一枚のデータフレームにする"""
    dfs = []
    years = range(start_year, stop_year + 1)
    for year in tqdm(years):
        year_df = _fetch(year)
        dfs.append(year_df)
        # サービスに負荷をかけないようにインターバルをかける
        time.sleep(fetch_interval)

    # データフレームを結合する
    return pd.concat(dfs, axis=0)


def block_size(df_):
    """取引された IPv4 アドレスのブロックサイズをパースする"""
    def parse(x):
        return int(x[1:])
    return df_['BLOCK'].apply(parse)


def filter_outlier(df_):
    """データフレームから外れ値を取り除く"""
    # ブロックサイズが適切にパースできるものだけに絞る
    prefix_series = df_['BLOCK'].apply(lambda x: x[1:])
    return df_[prefix_series.str.isnumeric()]


def sold_yyyymm(df_):
    """取引された年・月をパースする"""
    def to_str(dt):
        return dt.strftime('%Y-%m')
    dt_series = pd.to_datetime(df_['SOLD DATE'])
    return dt_series.apply(to_str)


def region(df_):
    """取引された地域をパースする"""
    def parse(x):
        # 末尾にカンマの入ったデータが混ざっていたので取り除く
        return x[:-1] if x[-1] == ',' else x
    return df_.REGION.apply(parse)


def main():
    # データ取得・描画範囲
    YEAR_RANGE = (2014, 2018)

    # オークションの履歴を取得する
    df = auction_history(YEAR_RANGE[0], YEAR_RANGE[-1])

    # 外れ値を取り除く
    df = filter_outlier(df)

    # 取引された日付と価格を取得する
    df = df.assign(block_size=block_size(df))
    df = df.assign(yyyymm=sold_yyyymm(df))
    df = df.assign(region=region(df))

    # 集計する
    pivot_df = df.pivot_table(index=['yyyymm'], values=['block_size'], aggfunc=['max', 'mean', 'median', 'min'], fill_value=0)

    # 可視化する
    fig, ax = plt.subplots()
    for _, data in pivot_df.items():
        agg_type = data.name[0]
        ax.plot(data.index, data, label=agg_type)

    ax.legend()
    ax.set_ylabel('cidr')
    ax.set_xlabel('month')
    ax.xaxis.set_ticks(['{y}-06'.format(y=y) for y in range(YEAR_RANGE[0], YEAR_RANGE[-1] + 1)])
    plt.show()


if __name__ == '__main__':
    main()
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time

import percache
from tqdm import tqdm
import pandas as pd
from matplotlib import pyplot as plt


def _fetch(year):
    """オークションの履歴をスクレイピングする"""
    base_url = 'https://www.ipv4auctions.com/customer/account/previous?year={yyyy}'
    url = base_url.format(yyyy=year)
    dfs = pd.read_html(url)
    return dfs[1]


# スクレイピングした内容をローカルのディスクにキャッシュする
cache = percache.Cache('df-cache')


@cache
def auction_history(start_year, stop_year, fetch_interval=2):
    """各年の情報を結合して一枚のデータフレームにする"""
    dfs = []
    years = range(start_year, stop_year + 1)
    for year in tqdm(years):
        year_df = _fetch(year)
        dfs.append(year_df)
        # サービスに負荷をかけないようにインターバルをかける
        time.sleep(fetch_interval)

    # データフレームを結合する
    return pd.concat(dfs, axis=0)


def filter_outlier(df_):
    """データフレームから外れ値を取り除く"""
    # ブロックサイズが適切にパースできるものだけに絞る
    prefix_series = df_['BLOCK'].apply(lambda x: x[1:])
    return df_[prefix_series.str.isnumeric()]


def price_per_addr(df_):
    """取引された価格をパースする"""
    def parse(x):
        ex_comma_x = x.replace(',', '')
        return float(ex_comma_x[1:])
    return df_['PRICE PER ADDRESS'].apply(parse)


def sold_block_size(df_):
    """取引されたブロックのサイズ"""
    def parse(x):
        return int(x[1:])
    return df_['BLOCK'].apply(parse)


def main():
    # データ取得・描画範囲
    YEAR_RANGE = (2014, 2018)

    # オークションの履歴を取得する
    df = auction_history(YEAR_RANGE[0], YEAR_RANGE[-1])

    # 外れ値を取り除く
    df = filter_outlier(df)

    # 可視化に使うデータを取り出す
    df = df.assign(price_per_addr=price_per_addr(df))
    df = df.assign(sold_block_size=sold_block_size(df))

    # ブロックサイズごとに集計・可視化する
    fig, ax = plt.subplots()
    sizes = sorted(pd.unique(df.sold_block_size))
    block_size_series = [df[df.sold_block_size == size].price_per_addr for size in sizes]
    ax.boxplot(block_size_series, labels=sizes)

    ax.set_title('IPv4 auction price graph')
    ax.set_ylabel('price per address (USD)')
    ax.set_xlabel('sold block size')
    plt.show()


if __name__ == '__main__':
    main()

いじょう。

マスタリングTCP/IP 入門編 第5版

マスタリングTCP/IP 入門編 第5版

  • 作者: 竹下隆史,村山公保,荒井透,苅田幸雄
  • 出版社/メーカー: オーム社
  • 発売日: 2012/02/25
  • メディア: 単行本(ソフトカバー)
  • 購入: 4人 クリック: 34回
  • この商品を含むブログ (37件) を見る