Pythonの標準ライブラリに学ぶIPv6備忘録

# はじめに
~~タイトルの後半部分を言いたかっただけです~~
こんにちは。株式会社インティメート・マージャーの十文字です。

IPv6の仕様って何度見ても覚えられないですよね。
ネットワークエンジニアやインフラエンジニアならまだしも、普段バックエンド開発をしている身としてはIPv6に触れる機会というのは少なく、毎回部分的に調べて満足してしまっています。

そこで今回は、IPv6の仕様を調べてまとめていこうと思ったのですが、私のような初学者にはRFCの仕様はとっつきづらかったため、今回はPythonの標準ライブラリである`ipaddress`のコードを読みながらIPv6について学びつつ、理解を深めていこうと思います。

https://docs.python.org/ja/3/library/ipaddress.html

# `ipaddress` ライブラリ
今回はこの `ipaddress` ライブラリ内の `IPv6Address` というクラスを見ていくのですが、その前にライブラリ全体の構成を見てこいうと思います。

`ipaddress` ライブラリは以下のようになっており、リストだと分かりづらいのでクラス図も貼っておきます。
- _IPAddressBase
- _BaseAddress(_IPAddressBase)
- _BaseNetwork(_IPAddressBase)
- _BaseV4
- IPv4Address(_BaseV4, _BaseAddress)
- IPv4Interface(IPv4Address)
- IPv4Network(_BaseV4, _BaseNetwork)
- _BaseV6
- IPv6Address(_BaseV6, _BaseAddress)
- IPv6Interface(IPv6Address)
- IPv6Network(_BaseV6, _BaseNetwork)

```mermaid
classDiagram
    class _IPAddressBase {
    }
    class _BaseAddress {
    }
    class _BaseNetwork {
    }
    class _BaseV4 {
    }
    class IPv4Address {
    }
    class IPv4Interface {
    }
    class IPv4Network {
    }
    class _BaseV6 {
    }
    class IPv6Address {
    }
    class IPv6Interface {
    }
    class IPv6Network {
    }

    _IPAddressBase <|-- _BaseAddress
    _IPAddressBase <|-- _BaseNetwork

    _BaseV4 <|-- IPv4Address
    _BaseV4 <|-- IPv4Network
    IPv4Address <|-- IPv4Interface

    _BaseV6 <|-- IPv6Address
    _BaseV6 <|-- IPv6Network
    IPv6Address <|-- IPv6Interface

    _BaseAddress <|-- IPv4Address
    _BaseNetwork <|-- IPv4Network
    _BaseAddress <|-- IPv6Address
    _BaseNetwork <|-- IPv6Network
```

このままではまだ複雑なのでここでは `Address` 関連のもののみを見ていくことにします。
すると `_BaseV6` と `IPv6Address` クラスを見れば大体IPv6の特徴が掴めそうということが分かります。

- _IPAddressBase
    - _BaseAddress(_IPAddressBase)
- _BaseV4
    - IPv4Address(_BaseV4, _BaseAddress)
- _BaseV6
    - IPv6Address(_BaseV6, _BaseAddress)

```mermaid
classDiagram
    class _IPAddressBase {
    }
    class _BaseAddress {
    }
    class _BaseV4 {
    }
    class IPv4Address {
    }
    class _BaseV6 {
    }
    class IPv6Address {
    }

    _IPAddressBase <|-- _BaseAddress
    _BaseV4 <|-- IPv4Address
    _BaseV6 <|-- IPv6Address

    _BaseAddress <|-- IPv4Address
    _BaseAddress <|-- IPv6Address

```

# IPv6Address
IPv4とIPv6の一番大きな違いとして使用可能なIPアドレスの数の違いがあります。

```ipaddress.py
IPV4LENGTH = 32
IPV6LENGTH = 128
```

https://github.com/python/cpython/blob/3.13/Lib/ipaddress.py#L16-L17

また、表現方法も異なるため、その辺の違いが `_BaseV4` と `_BaseV6` にそれぞれ実装されています。
（表現方法や省略方法についてはここでは割愛します）

ということでここからは `IPv6Address` を見ていくことにします。

## `IPv6Address` プロパティ
`IPv6Address` には以下のプロパティが定義されています。

- `scope_id`*
- `packed`（バイナリ表現）
- `is_multicast`（マルチキャストアドレス）
- `is_reserved`（予約済みアドレス）
- `is_link_local`*（リンクローカルアドレス）
- `is_site_local`*, **（サイトローカルアドレス）
- `is_private`（プライベートアドレス）
- `is_global`（グローバルアドレス）
- `is_unspecified`（未定義アドレス）
- `is_loopback`（ループバックアドレス）
- `ipv4_mapped`***（IPv4-mappedアドレス）
- `teredo`*
- `sixtofour`*

*: `IPv4Address` には無いプロパティ
**: 「サイトローカルアドレス」は廃止されています
***: `IPv4Address` に `ipv6_mapped` がある

### IPv6のアドレスアーキテクチャ関連
IPv6もIPv4同様、IPアドレスの先頭のビットパターンでIPアドレスの種類を区別します。
上記のプロパティを見たところ、`is_` で始まるプロパティがそれらを区別するためのプロパティのようです。

以下に実装の一部を載せます。
（IPv4-mappedアドレスの際の実装は省きます）

- `is_multicast`（マルチキャストアドレス）
    - ```py
        @property
        def is_multicast(self):
            """Test if the address is reserved for multicast use.
    
            Returns:
                A boolean, True if the address is a multicast address.
                See RFC 2373 2.7 for details.
    
            """
            return self in IPv6Network('ff00::/8')
        ```
- `is_reserved`（予約済みアドレス）
    - ```py
        @property
        def is_reserved(self):
            """Test if the address is otherwise IETF reserved.
    
            Returns:
                A boolean, True if the address is within one of the
                reserved IPv6 Network ranges.
    
            """
            return any(self in x for x in [
                IPv6Network('::/8'), IPv6Network('100::/8'),
                IPv6Network('200::/7'), IPv6Network('400::/6'),
                IPv6Network('800::/5'), IPv6Network('1000::/4'),
                IPv6Network('4000::/3'), IPv6Network('6000::/3'),
                IPv6Network('8000::/3'), IPv6Network('A000::/3'),
                IPv6Network('C000::/3'), IPv6Network('E000::/4'),
                IPv6Network('F000::/5'), IPv6Network('F800::/6'),
                IPv6Network('FE00::/9'),
            ])
        ```
- `is_link_local`（リンクローカルアドレス）
    - ```py
        @property
        def is_link_local(self):
            """Test if the address is reserved for link-local.
    
            Returns:
                A boolean, True if the address is reserved per RFC 4291.
    
            """
            return self in IPv6Network('fe80::/10')
        ```
- `is_site_local`（サイトローカルアドレス）
    - ```py
        @property
        def is_site_local(self):
            """Test if the address is reserved for site-local.
    
            Note that the site-local address space has been deprecated by RFC 3879.
            Use is_private to test if this address is in the space of unique local
            addresses as defined by RFC 4193.
    
            Returns:
                A boolean, True if the address is reserved per RFC 3513 2.5.6.
    
            """
            return self in IPv6Network('fec0::/10')
        ```
- `is_private`（プライベートアドレス）
    - ```py
        @property
        @functools.lru_cache()
        def is_private(self):
            """``True`` if the address is defined as not globally reachable by
            iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
            (for IPv6) with the following exceptions:
    
            * ``is_private`` is ``False`` for ``100.64.0.0/10``
            * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
                semantics of the underlying IPv4 addresses and the following condition holds
                (see :attr:`IPv6Address.ipv4_mapped`)::
    
                    address.is_private == address.ipv4_mapped.is_private
    
            ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
            IPv4 range where they are both ``False``.
            """
            # Not globally reachable address blocks listed on
            # https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
            _private_networks = [
                IPv6Network('::1/128'),
                IPv6Network('::/128'),
                IPv6Network('::ffff:0:0/96'),
                IPv6Network('64:ff9b:1::/48'),
                IPv6Network('100::/64'),
                IPv6Network('2001::/23'),
                IPv6Network('2001:db8::/32'),
                # IANA says N/A, let's consider it not globally reachable to be safe
                IPv6Network('2002::/16'),
                IPv6Network('fc00::/7'),
                IPv6Network('fe80::/10'),
            ]
            _private_networks_exceptions = [
                IPv6Network('2001:1::1/128'),
                IPv6Network('2001:1::2/128'),
                IPv6Network('2001:3::/32'),
                IPv6Network('2001:4:112::/48'),
                IPv6Network('2001:20::/28'),
                IPv6Network('2001:30::/28'),
            ]
            return (
                any(self in net for net in _private_networks)
                and all(self not in net for net in _private_networks_exceptions)
            )
        ```
- `is_global`（グローバルアドレス）
    - ```py
        @property
        def is_global(self):
            """``True`` if the address is defined as globally reachable by
            iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
            (for IPv6) with the following exception:
    
            For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
            semantics of the underlying IPv4 addresses and the following condition holds
            (see :attr:`IPv6Address.ipv4_mapped`)::
    
                address.is_global == address.ipv4_mapped.is_global
    
            ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
            IPv4 range where they are both ``False``.
            """
            return not self.is_private
        ```
- `is_unspecified`（未定義アドレス）
    - ```py
        @property
        def is_unspecified(self):
            """Test if the address is unspecified.
    
            Returns:
                A boolean, True if this is the unspecified address as defined in
                RFC 2373 2.5.2.
    
            """
            return self._ip == 0
        ```
- `is_loopback`（ループバックアドレス）
    - ```py
        @property
        def is_loopback(self):
            """Test if the address is a loopback address.
    
            Returns:
                A boolean, True if the address is a loopback address as defined in
                RFC 2373 2.5.3.
    
            """
            return self._ip == 1
        ```

一番気になったのは `is_private` プロパティです。
IPv6の文脈では、ローカルやグローバルという言葉はありますが、プライベートという言葉を使っている資料をほとんど見ないので見慣れないのですが、どうやらグローバルから到達出来ないアドレスのことを言っているようで、そのようなIPネットワークがいくつか定義されているようです。

そしてもう一つ、`is_site_local` プロパティですが、こちらはコメントにもある通り廃止されている（セキュリティ上の問題があった）ようで、調べてみると新たに「ユニークローカルアドレス」というアドレスに置き換わっており、`_private_networks` 内の `IPv6Network('fc00::/7')` として定義されています。

### その他
- `scope_id`*
    - ```py
        @property
        def scope_id(self):
            """Identifier of a particular zone of the address's scope.
    
            See RFC 4007 for details.
    
            Returns:
                A string identifying the zone of the address if specified, else None.
    
            """
            return self._scope_id
        ```
- `teredo`*
    - ```py
        @property
        def teredo(self):
            """Tuple of embedded teredo IPs.
    
            Returns:
                Tuple of the (server, client) IPs or None if the address
                doesn't appear to be a teredo address (doesn't start with
                2001::/32)
    
            """
            if (self._ip >> 96) != 0x20010000:
                return None
            return (IPv4Address((self._ip >> 64) & 0xFFFFFFFF),
                    IPv4Address(~self._ip & 0xFFFFFFFF))
        ```
- `sixtofour`*
    - ```py
        @property
        def sixtofour(self):
            """Return the IPv4 6to4 embedded address.
    
            Returns:
                The IPv4 6to4-embedded address if present or None if the
                address doesn't appear to contain a 6to4 embedded address.
    
            """
            if (self._ip >> 112) != 0x2002:
                return None
            return IPv4Address((self._ip >> 80) & 0xFFFFFFFF)
        ```

こちらもそれぞれあまりピンとこず、とくに「Teredo」という単語は初めて目にしました。
これも調べてみたところ、どうやら**Teredoトンネリング**という、経路上にIPv6を理解しない通信機器が存在していてもIPv4を用いて通信を行うことを可能にするためのIPv6移行技術に利用されるものでした。

「sixtofour」に関してはTeredo同様、**6to4トンネリング**というIPv6移行技術の一つで、IPv6パケットをIPv4パケットのペイロードとしてカプセル化するためのものでした。

そして「スコープID」は `RFC4007` に定義されているようで、こちらはリンクローカルアドレスの後ろにインターフェース名（ID）を `fe80::1%eth0` のように付与して利用する際に用いられるもので、どのインターフェースからパケットが送出されているか分かるようになっています。

# まとめ
ということで、今回はPythonの標準ライブラリである `ipaddress` の実装からIPv6に関する技術を見ていきました。
RFCの仕様ほど深い内容にはそこまで触れることは出来ませんでしたが、知識の浅い自分にとってはちょうどいい内容でした。

アドベントカレンダーも残ることあと2日ですが、残りの記事もぜひ楽しんでいってください。

https://qiita.com/advent-calendar/2024/intimatemerger