自宅ネットワークを構築する


# 前書き

今年日本に引っ越したとき、ソフトバンクの光回線を契約しました。私はいつも自宅のコンピューターをサーバーとして使用しています。

自宅サーバーを使用するシナリオは次のとおりです。


* Plexユーザーとして、自宅でも屋外でも、Plexを使用していつでも音楽を聴いたり、ビデオを視聴したりできます。

* どこにいても、MacBookから自宅のLinuxサーバーにSSHしてコードをコンパイルし、プログラムを実行できます。

* どこにいても、自宅のコンピューターで実行されているさまざまなサービスにアクセスできます。例えば、AgentGPT、 Stable Diffusion、bliss、cockpit、qBittorrentなどです。

# はじめに

まず、ソフトバンク光がグローバルIPv4アドレスを提供しているかどうかを確認する必要があります。幸いなことに、管理画面からわかるように、BBユニットにはグローバルIPv4アドレスが割り当てられています。

グローバルIPv4アドレスができたので、光BBユニットの『IPアドレス／DHCPサーバの設定』と『ポート転送設定』画面にNATを設定して、パブリック ネットワークからサーバーにトラフィックを転送します。

そうすれば、屋外からグローバルIPv4アドレスで自宅のコンピュータをアクセスできます。

**ただし、最適化の余地はまだあります。**

# DDNS

IPアドレスでを使用してサービスをアクセスすることは、いい方法ではありません。理由の一つは、IPアドレスを覚えにくいとこ、もう一つはIPアドレスがいつでも変更される可能性があることです。

通常、サービスにアクセスするためにドメイン名を使用しますが、そのドメイン名は取得したIPアダレスにバインドされています。家庭用回線はほとんど動的IPv4アドレスを割り当てられます。固定IPアドレスとは異なり、動的IPはいつでも変更されできます。

現在のIPアドレスを定期的にチェックする Python コードを作成しました。現在のドメイン名にバインドされているIPアドレスと異なる場合は、すぐに新しいIPアドレスがバインドされます。

```python:main.py
#!/usr/bin/env python3

import os
import time
import socket

import requests
from google.cloud import dns

# pip install google-cloud-dns requests

# don't forget this file!
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "/gcp-service-account.json" 

GET_IP_API = "https://domains.google.com/checkip"
GCP_PROJECT_ID = "project-XXXXXX"
DEFAULT_TTL = 60

DOMAINS = {
    "aaa.com.": [
        "home.aaa.com.",
        "*.home.aaa.com.",
    ],
    "bbb.me.": [
        "bbb.me.",
    ],
    "ccc.net.": [
        "ccc.net.",
        "*.ccc.net.",
    ],
}


def get_family4():
    return socket.AF_INET


def get_family6():
    return socket.AF_INET6


def get_current_ip(get_family):
    requests.packages.urllib3.util.connection.allowed_gai_family = get_family
    r = requests.get(GET_IP_API)
    return r.text.strip()


def get_current_ipv4():
    return get_current_ip(get_family4)


def get_current_ipv6():
    return get_current_ip(get_family6)


def get_ips():
    return get_current_ipv4(), get_current_ipv6()


def set_ip(zone, domain, rtype, ip):
    records = [
        record for record in zone.list_resource_record_sets() if record.name == domain and record.record_type == rtype
    ]

    if len(records) == 0:
        add_record(zone, domain, rtype, ip)
    else:
        matched = False
        for record in records:
            if len(record.rrdatas) != 1 or record.rrdatas[0] != ip:
                del_changes = zone.changes()
                del_changes.delete_record_set(record)
                del_changes.create()
            else:
                matched = True
        if not matched:
            add_record(zone, domain, rtype, ip)


def add_record(zone, domain, rtype, ip):
    add_changes = zone.changes()
    add_changes.add_record_set(zone.resource_record_set(domain, rtype, DEFAULT_TTL, [ip]))
    add_changes.create()


def main():
    client = dns.Client(project=GCP_PROJECT_ID)

    while True:
        for zone in client.list_zones():
            for root, domains in DOMAINS.items():
                if zone.dns_name == root:
                    ipv4, ipv6 = get_ips()
                    for domain in domains:
                        set_ip(zone, domain, "A", ipv4)
                        set_ip(zone, domain, "AAAA", ipv6)

        time.sleep(120)


if __name__ == "__main__":
    main()
```


私のドメイン名は Google Cloud DNS でホストされています。これに該当する場合は、[このコード](https://gist.github.com/handsomecheung/6b809660ddc4d1d722b36e35b8818e9e)を参照してください。

# IPv6

ネットワーク全体がIPv6をサポートしている場合、自宅サーバーへのアクセスにIPv6を使用することは非常に良い選択です。**IPv6 はNATを使用しないため、NATに起因するさまざまな問題が発生しません。**
しかし、現在日本でIPv6がどれほど普及しているのか、またすべてのネットワークがIPv6をサポートしているのかどうかはわかりません。また、すべてのサービスが IPv6 をサポートしているわけではありません。したがって、IPv6に加えて、IPv4にも問題がないことが保証される必要があります。

# ヘアピンNAT

自宅ネットワーク上の任意のコンピュタで上記のコードを実行すると、屋外からドメイン名を介してサービスにアクセスできます。しかし、自宅のWi-Fiに接続している場合、ドメイン名を介してサービスにアクセスすることはできません（IPv4を使用している場合）。これは、光BBユニット（E-WMTA2.3）がヘアピンNAT機能をサポートしていないことが原因です。

ヘアピンNAT機能とは、NATを使用してインターネットに接続する構成で、NATの内側にある端末から同じNATの内側にある端末にNATの外側アドレスを指定して接続できるようにする機能です。NATループバック機能と呼ばれることもあります。 

その光BBユニットがカスタムドメイン名のマッピングをサポートしている場合は、NATヘアピンの問題が発生しないように、ドメイン名をサーバーのLAN IPアドレスにバインドできます。

残念ながら、この光BBユニットもカスタムドメイン名のマッピングをサポートしていません。


# NETGEAR WAX206 and OpenWRT

光BBユニットの問題を解決するために、市販ルーターのNETGEAR WAX206を購入しました。WAX206の内蔵ファームウェアがIPv6とカスタムドメイン名のマッピングをサポートしていないため、OpenWRTをインストールする必要があります。

WAX206にOpenWRTをインストールするプロセスは非常にスムーズでした。 OpenwrtはWAX206を公式サポートしているのはそのルーターを選択した理由でした。

OpenWRTにdnsmasqをインストールし、dnsmasqを設定してLAN IPをドメイン名にバインドしました。これにより、ローカルエリアネットワーク内でドメイン名にアクセスすると、そのドメインはLAN IPとして解析され、ヘアピンNATの問題を回避できます。


```:dnsmasq.conf
address=/server.home.aaa.com/192.168.37.11
address=/raspberrypi4b.home.aaa.com/192.168.37.12
```

自前でDNSサービス（dnsmasq）を利用する利点は、ヘアピンNATの問題を回避するだけでなく、LAN IPを使用してサービスにアクセスする方が効率的です。

![Network Topological Graph.small.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311989/a9f74350-0614-22e1-69e9-2723181107dd.png)


WAX206を光BBユニットの後ろに接続し、すべてのデバイスを有線またはWi-FでWAX206に接続します。 光BBユニットのWi-Fi機能が不要になったので、管理画面から『無線LAN機能』をオフにしました。

WAX206に接続されているすべてのデバイスは、デフォルトでWAX206によって提供されるDNSサーバーアドレス (dnsmasqサービス) を使用します。 dnsmasqはドメイン名に対応するLAN IPアドレスをクライアントに返すため、NATヘアピンの問題は発生しません。

ちなみに、最初は家にある数十台のデバイスが光BBユニットのWi-Fiを経由してインターネットに接続していましたが、頻繁に接続が切れることがありました。WAX206のWi-Fiに接続した後、接続切断の問題は一度も発生していません。具体的な原因は深く調査していません。

# Google Home

一部のデバイスはゲートウェイによって提供されるDNSアドレスを使用しません、Google Homeがそういうものです。

Plexで音楽を再生するときに、Google Homeでキャストすると問題が発生します。Google Home は、ネットワーク リクエストを行うときに固定DNS（Google Public DNS）アドレスを使用します。Google Public DNSはドメイン名に対応するWAN IPアドレスを返すため、NATヘアピン問題は依然として発生します。

この問題を解決するには多くの方法がありますが、最も簡単なのは、ルーター（WAX206）にiptabelsを使用して、すべてのクライアントのDNSクエリをリダイレクトすることです。

```
iptables -t nat -A PREROUTING -i eth1 -p udp --dport 53 -j DNAT --to 192.168.37.1
iptables -t nat -A PREROUTING -i eth1 -p tcp --dport 53 -j DNAT --to 192.168.37.1
```


# 終わりに

現在、自宅でも屋外でも、IPv4でもIPv6でも、スムーズに自宅サーバーにアクセスできるようになりました。

ソフトバンクの光BBユニットにはNATヘアピン機能とカスタムドメイン名のマッピング機能が備わっていないため、自宅サーバー環境の構築が難しく、新たにゲートウェイを追加する必要がありました。 ただし、機能拡張の観点からは、OpenWRTは光BBユニットよりも多くの機能を実現できます。

どなたかのお役に立てれば光栄です。