我が家のネットワークが不調すぎるのでRasberry PiでIPv6ルーターを自作した


背景

我が家のネットワークの調子が悪い。

ミーティング中やゲーム中に突如回線落ちする。ひどいときは日に3回ほどネットが切断される。どうやら利用しているルーターの負荷が高まると勝手に再起動し、その間に通信ができないということがわかった。

しょうがないので、ちょっと高めのIPv6対応ルーターを導入することにした。
東京住まいということもあり、PPPoE通信すると夜は画像すら満足にダウンロードできないレベルで遅い。IPoE接続(v6プラス)のルーターは必須だ。

Amazonでルーターを購入し利用しはじめて数日、新ルーターは性能が低いのかIPoEでも速度がでないことに気がついた。ネットの調子悪いのか?と思い昔のルーターに戻して通信したら爆速。まじか、せっかく高い金払ったのに通信遅かったら意味ないじゃん。

こうなったら、ルーターを自分で作るしか無い。
Rasberry Pi4を使って自分でルーターを作ることに決めた。

構築するネットワーク

構築するネットワークは以下の通り。

我が家のISPは@Niftyで、IPv6プラスオプションを申し込んでいる。

IPv6のインターネット接続は簡単、単にイーサネットケーブルを直接ONUに繋げばよい。
ただ、自宅内のすべての端末がグローバルネットワークにダイレクト接続されるのはセキュリティの面で怖い。そのため、自宅内はIPv6プライベートネットワーク(fd00::/64)を組み、ルーターがIPマスカレードしてグローバルなネットワークにでていく構成とした。
IPv6の経路構築とIP割当は(DHCPv6ではなく)RAで行う。DNSの通知も今回RAにまかせることとした。

問題はIPv4ネットワークへの接続だ。IPv4 インターネットを利用するためには、JPNE(IPoE提供事業者)のBR(Border Relay)にトンネルを張り、途中までIPv6で通信しBRでIPv4にでていく必要がある。JPNEの場合、MAP-E方式でIPv4 over IPv6を実現しているとのこと。
自宅内のIPv4はプライベートネットワーク(10.0.0.0/8) を組み、IPアドレスの配布とDNS通知はDHCPで行う。
プライベートネットワークから外部に通信する際は、ルーターが一度通信を受け止め、BRへのトンネルを経由してインターネットとの通信を行う。

Rasberry Piのセットアップ

Rasberry Piは 4のModelB ( https://www.raspberrypi.org/products/raspberry-pi-4-model-b/ )を使うことにした。こいつをルーターにする。OSはUbuntu Server 20.04LTS。
Rasberry Pi はLANコネクタは1つしかないので、別途USB接続のLANアダプタを購入し接続した。これで eth0 と eth1 の2つができた。
今回はeth0をWANネットワークとし、eth1をLANネットワークとする。

Rasberry PiへのOSインストールなどは省略する。
ルーターにするために必要なパッケージはあらかじめ以下コマンドでインストールしておく。

# IPv6 Router Advertisement(RA) のために利用
$ sudo apt install radvd

# IPv4 DHCPのためにインストール
$ sudo apt install isc-dhcp-server

IPv6の世界へRasberry Piを接続する

まずは現状のnetplanを確認。
eth0はONUに直接つなぎ、IPv6の世界へ接続する。
eth1は何も接続していない。

# cat /etc/netplan/50-cloud-init.yaml

network:
    ethernets:
        eth0:
            dhcp4: true
            optional: true
    version: 2

この状態でping6をかけてみる。
2404:6800:4004:80b::200e は youtube.com だ。

ubuntu@rasp-server:~$ ping6 2404:6800:4004:80b::200e
PING 2404:6800:4004:80b::200e(2404:6800:4004:80b::200e) 56 data bytes
64 bytes from 2404:6800:4004:80b::200e: icmp_seq=1 ttl=115 time=7.31ms

無事IPv6での通信が行えた。

DNSが無いため名前解決はできない。そのため、ping6 youtube.com コマンドを入力してもレスポンスは返ってこない。

また、IPv4の通信も当然ながらできない。

ubuntu@rasp-server:~$ ping 163.44.168.xxx
ping: connect: Network is unreachable

ルーターIP割当とRA/DHCPの設定

次にnetplanを変更し、eth0にWAN用の設定をし、eth1にプライベートネットワークルーターとしてのIPを設定する。
eth0はRAを受け取り、グローバルなIPv6アドレスをもらうようにする。

eth1はプライベートネットワークのゲートウェイになるため、IPアドレスは固定化しておく。
DNSは自前でサービスを立てず、Googleの8.8.8.8を利用することにした。

$ cat /etc/netplan/50-cloud-init.yaml

network:
    ethernets:
        eth0:
            dhcp4: true
            dhcp6: false
            accept-ra: yes
            optional: true
        eth1:
            dhcp4: false
            dhcp6: false
            addresses:
                - 10.0.0.1/8
                - fd00::1/64
            gateway4: 10.0.0.1
            gateway6: fd00::1
            nameservers:
                addresses:
                     - 8.8.8.8
                     - 8.8.4.4
                     - 2001:4860:4860::8888
                     - 2001:4860:4860::8844
    version: 2

プライベートネットワーク内にIPv4を配るため、(IPv4の)DHCPの設定をする。
10.0.0.0/8 のネットワークをプライベートとして利用することとした。

$ sudo vim /etc/dhcp/dhcpd.conf

# option domain-name "example.org";
# option domain-name-servers ns1.example.org, ns2.example.org;
## 上記はいらないのでコメントアウト


# 以下有効にする
ddns-update-style none;


# If this DHCP server is the official DHCP server for the local
# network, the authoritative directive should be uncommented.
# 以下も有効にする
authoritative;

# Use this to send dhcp log messages to a different log file (you also
# have to hack syslog.conf to complete the redirection).
# ログも有効にする
log-facility local7;


# 10.0.0.0 のローカルネットワーク構築
subnet 10.0.0.0 netmask 255.0.0.0 {
  range 10.0.0.20 10.0.255.254;                  # DHCPとして割り当てるIPアドレスの範囲
  option routers 10.0.0.1;                       # ルーターのアドレス=自分のアドレス
  option broadcast-address 10.255.255.255;
  option domain-name-servers 8.8.8.8, 8.8.4.4;   # DNSはgoogleを使う
  default-lease-time 432000;                     # IPリース時間は長めに取る
  max-lease-time 864000;
}

プライベートネットワーク内のIPv6経路とアドレス割当のため、RAを設定する。以下のようにradvd.confを追加する。

$ cat /etc/radvd.conf

interface eth1 {             # インターフェースはeth1(LAN)
  AdvSendAdvert on;
  AdvManagedFlag off;        # Mフラグ Off
  AdvOtherConfigFlag off;    # Oフラグ Off
  MinRtrAdvInterval 30;
  MaxRtrAdvInterval 100;

  prefix fd00::/64 {        # fd00::/64 プライベートネットワークに通知
    AdvOnLink on;
    AdvRouterAddr on;
    AdvAutonomous on;
  };

  RDNSS 2001:4860:4860::8888 2001:4860:4860::8844   # GoogleのDNSを使う
  {
  };
};

ここまで構築したら、設定をすべて反映する

sudo netplan try

sudo systemctl enable isc-dhcp-server
sudo systemctl enable radvd

sudo reboot # 念の為サーバーを再起動

反映が終わったら、eth1を内部スイッチにつなぎ、他のPCと接続する。
今回はiMacをeth1側につなぎ、DHCPで割り振ったIPの更新を行った。

結果、以下のようにプライベートIPとDNSが振られていることがわかる。

IPは割り当てられているが、この状態でIPv6のインターネットにも接続はできない。

IPv6をフォワードさせる

プライベートネットワークから外のネットワークにでていくためには、ルーターがIPv4/IPv6をフォワードする必要がある。
そのため、/etc/sysctl.conf を以下のように修正しForward設定を有効化する。

# /etc/sysctl.conf

# Uncomment the next line to enable packet forwarding for IPv4
# IPv4をforwardするためにコメントアウトする
net.ipv4.ip_forward=1


# Uncomment the next line to enable packet forwarding for IPv6
#  Enabling this option disables Stateless Address Autoconfiguration
#  based on Router Advertisements for this host
# IPv6をforwardするためにコメントアウトする
net.ipv6.conf.all.forwarding=1


net.ipv6.conf.eth0.accept_ra = 2  # WAN側 RAを受け付ける
net.ipv6.conf.eth1.accept_ra = 0
net.ipv6.conf.all.disable_ipv6 = 0         # IPv6有効化
net.ipv6.conf.default.disable_ipv6 = 0

上記の設定を行ったら、以下コマンドで反映する

sudo sysctl -p

次にip6tablesを使い、fd00::/64 (ローカルネットワーク)ソースの通信をIPマスカレードしてWAN側に流す。すべての通信が素通りだと怖いので、ローカル発信の通信のみ許可する。このスクリプトを ipv6_forward.sh として保存する

#!/bin/bash

WAN_DEVICE=eth0
LAN_DEVICE=eth1
LOCAL_SOURCE="fd00::/64"

ip6tables -F

# すべての通信を素通りだと怖いので MARKをつけてLAN内のものからのみ許可する
ip6tables -t nat -A PREROUTING -i $LAN_DEVICE -j MARK --set-mark 1000
ip6tables -t filter -A FORWARD -p tcp -m mark --mark 1000 -j ACCEPT
ip6tables -t filter -A FORWARD -p udp -m mark --mark 1000 -j ACCEPT
ip6tables -t filter -A FORWARD -m mark --mark 1000 -j ACCEPT

# SSHはローカルネットワークからのみ許可
ip6tables -A INPUT -p tcp -s $LOCAL_SOURCE --dport 22 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 22 -j DROP

# fd00::/64 宛の通信は全部WANに流す
ip6tables -t nat -A POSTROUTING -s $LOCAL_SOURCE -o $WAN_DEVICE -j MASQUERADE
sudo chmod +x ./ipv6_forward.sh
sudo ./ipv6_forward.sh

しばらく時間をおいてから、iMacからcurlでyoutube.comに接続してみる。

[nishio@NPC] $ curl youtube.com -v                                                                                                                                                                                                                                                                                                                                      [~]
*   Trying 2404:6800:4004:81d::200e...
* TCP_NODELAY set
* Connected to youtube.com (2404:6800:4004:81d::200e) port 80 (#0)
> GET / HTTP/1.1
> Host: youtube.com
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Location: https://youtube.com/
< Content-Length: 0
< Date: Sun, 16 Aug 2020 09:38:36 GMT
< Content-Type: text/html
< Server: YouTube Frontend Proxy
< X-XSS-Protection: 0
<
* Connection #0 to host youtube.com left intact
* Closing connection 0

接続成功! しかし、YoutubeはIPv6に対応しているから見られるけど、あいかわらずIPv4のサイトは見られない状態。

Rasberry Pi ルーターを Map-EでIPv4の世界に接続する

問題のIPv4接続。これは一筋縄ではいかず、自分の契約しているISPや通信方式に依存した設定が必要となる。
JPNEのBRへの接続方法は非公開で、手軽に接続とはいかない。ただ、某大型掲示板に接続方法が記載されており(参考: https://gato.intaa.net/archives/13186 )、どこかの誰かが晒してくれたスクリプトを使い接続を行う。

まずはじめに、 http://ipv4.web.fc2.com/map-e.html にアクセスし、自分が取得できたIPv6アドレスを入力する。
するとCE、IPv4、PSIDが取得できるので、これを以下スクリプトに記載する。BRのアドレスは2chで拾った。

#!/bin/sh

BR='2404:9200:225:100::64'
CE='CEのアドレス'
IP4='自分のグローバルIPアドレス'
PSID='自分のPSID'
WANDEV='eth0'
LANDEV='eth1'
TUNDEV='ip6tunnel1'

ip -6 addr add $CE dev $WANDEV   # WAN側デバイスにCEアドレス割当
ip -6 tunnel add $TUNDEV mode ip4ip6 remote $BR local $CE dev $WANDEV encaplimit none # TunnelでBRに接続
ip link set dev $TUNDEV mtu 1460 # MTU設定
ip link set dev $TUNDEV up       # Tunnel有効化

# IPv4のデフォルトルートをトンネルしたDeviceにする
ip -4 route delete default
ip -4 route add default dev $TUNDEV

# Map-Eの転送許可追加
iptables -t nat -F

rule=1
while [ $rule -le 15  ] ; do
  mark=`expr $rule + 16`
  pn=`expr $rule - 1`
  portl=`expr $rule \* 4096 + $PSID \* 16`
  portr=`expr $portl + 15`

  # ローカルネットワークからの通信にマークをつけて転送許可
  iptables -t nat -A PREROUTING -i $LANDEV -m statistic --mode nth --every 15 --packet $pn -j MARK --set-mark $mark
  iptables -t nat -A OUTPUT -m statistic --mode nth --every 15 --packet $pn -j MARK --set-mark $mark

  iptables -t filter -A FORWARD -p icmp -m mark --mark $mark -j ACCEPT
  iptables -t filter -A FORWARD -p tcp -m mark --mark $mark -j ACCEPT
  iptables -t filter -A FORWARD -p udp -m mark --mark $mark -j ACCEPT

  # Map-Eのルールにしたがい通信を飛ばす このルールは自分の契約による
  iptables -t nat -A POSTROUTING -p icmp -o $TUNDEV -m mark --mark $mark -j SNAT --to $IP4:$portl-$portr
  iptables -t nat -A POSTROUTING -p tcp -o $TUNDEV -m mark --mark $mark -j SNAT --to $IP4:$portl-$portr
  iptables -t nat -A POSTROUTING -p udp -o $TUNDEV -m mark --mark $mark -j SNAT --to $IP4:$portl-$portr

  rule=`expr $rule + 1`
done

iptables -t mangle -o $TUNDEV --insert FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
sudo chmod +x ./ipv4_forward.sh
sudo ./ipv4_forward.sh

ここまで設定したら、しばらく時間をおいて再度iMacから通信をしてみる。

[nishio@NPC] $ curl densan-labs.net -v                                                                                                                                                                                                                                                                                                                                  [~]
*   Trying 163.44.168.152...
* TCP_NODELAY set
* Connected to densan-labs.net (163.44.168.152) port 80 (#0)
> GET / HTTP/1.1
> Host: densan-labs.net
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Server: nginx
< Date: Sun, 16 Aug 2020 09:57:44 GMT
< Content-Type: text/html
< Content-Length: 178
< Connection: keep-alive
< Location: https://densan-labs.net/
<
<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>
* Connection #0 to host densan-labs.net left intact
* Closing connection 0

IPv4で接続できた。

IPv6 で本当に通信できているか確認する

https://test-ipv6.com/index.html.ja_JPhttps://ipv6-test.com/ のサイトにアクセスすると、
通信がIPv6でできているか確認できる。
我が家のiMacから通信した結果は以下のようになった。

念の為、http://www.ipv6scanner.com/cgi-bin/main.py などでポートスキャンをかけ、余計なポートが空いていないかどうかのチェックもしておく。通信がすべてFilterされていれば安心だ。

これでルーターの完成。お疲れさまでした。

参考資料