やりたいこと
-
PPPoE
接続ではなくIPoE
接続している自宅LAN上のラズパイをインターネット経由でアクセスできるようにしたい。- プロバイダから割り当てられるWAN側のIPv4のアドレスは他のユーザと共用しており、v4のアドレスを使ってサービスを公開するのは難しいです。
- IPv6であれば自宅LAN内に接続したマシンにグローバルユニークアドレス(GUA)が割り当てられるので、外部からLAN内のラズパイにアクセスするが可能になります。
- ラズパイ上で動かすサービスはDockerのコンテナで動かしており、このサービスからインターネット経由で別のサービスのAPIを呼び出したい。
「外部にラズパイを公開する」なんてセキュリティ的に聞いただけでも恐ろしい感じがするかもしれませんが、適切にセキュリティ対策を行えば特に問題ないと考えます。
インターネット接続に関する前提
- ルータ + ONU(終端装置)やHGW(ホームゲートウェイ)などを使って自宅LANからインターネットへ接続しているものとします。
- 従来の
PPPoE
接続ではなくIPoE
接続でIPv6を利用するものとします。(IPoEに関する説明はここでは行いません)
ご注意
- ラズパイを外部へ公開する場合は、必ずルータ、ラズパイのOS/ネットワーク、コンテナ周りのセキュリティに関する設定を適切に行ってください。
- IPv6でラズパイを外部へ公開するので、外部からはIPv6でしかラズパイ上のサービスを呼び出すことができません。
サービスを呼び出すクライアントもIPv6を使うことになります。 - サービスを動かすDockerコンテナもIPv6を使うので、コンテナ側からインターネット経由で呼び出す外部サービスもIPv6で呼び出せる必要があります。
私は自分のサービスでWikipediaのAPIを呼び出しているのですが、WikipediaはIPv6でAPIを呼び出すことができました。(世の中の有名なサービスはIPv6でも接続できるものが多いかと思います)
環境例
今回は以下のような環境で試しました。
- ラズパイ: Model 4B(メモリ:8GB)
- OS: Raspbian OS(Buster)
- Docker: バージョンは
20.10.2
とします。- Experimental機能の
ip6tables
を使います。
なお、最新バージョン20.10.8
ではExperimentalではなくなっているかもしれませんが
私の環境では20.10.2
を使っているのでこのバージョンを例に挙げておきます。
- Experimental機能の
手順の概要
- 1.ルータのパケットフィルタリング設定
- WAN側、LAN側のパケットフィルタリング設定を行います。
- 2.ラズパイのネットワーク設定
- IPv6のアドレス割り当て、DNSサーバ、フォワーディングの設定を行います。
- 3.ラズパイのファイアウォール設定
- ufwを使った設定を行います。
- 4.ラズパイのDocker設定
- 5.docker-composenのサンプル
- サンプルでDjangoのコンテナを動かしてみます。
- 6.dockerのネットワーク確認
- サンプルで作成したdockerネットワークの確認を行います。
- 7.動作確認
- 外部からコンテナへのアクセス
- コンテナから外部へのアクセス
- 8.ダイナミックDNSへの登録
1.ルータ等のパケットフィルタリング設定
- ご自身のルータやHGW等にパケットフィルタリングの機能があると思いますので、ルータ等の管理画面でパケットフィルタリングの設定を確認してください。ここでは例としてデフォルトゲートウェイのIPv6が以下のものであるとします。
2xxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
このアドレスはグローバルユニークアドレス(GUA)であるため、セキュリティを考慮してマスクしてあります。
ルータの管理画面等でルータのLAN側のIPv6アドレスを確認した上で、ご自身の環境にあったものを設定してください。
-
私は市販のルータとONUを使っているのですが、プロバイダからレンタルするHGWを利用されている場合、デフォルトでIPv6のパケットフィルタリングが適切に設定されていないものもあるとのことです。必ずHGW側でケットフィルタリングの設定を行ってください。
-
今回はサンプルとしてdocker-composeを使ってDjangoのコンテナを8080番ポートで起動させるので、もしサンプルを実行される場合は8080番のポートを開いてください。
2.ラズパイのネットワーク設定
/etc/dhcpcd.conf
にラズパイに割り当てるIPアドレスとDNSサーバのIPアドレスを設定します。
- IPアドレスとDNSサーバの設定を行います。私は有線でラズパイとルータを接続しており、ネットワークインターフェースは
eth0
を使っています。ここでは/etc/dhcpcd.conf
でeth0
のv6アドレスとDNSサーバのv6アドレスを設定しますが、適宜インターフェースはご自身の環境に合わせて指定してください。
いきなり最初からラズパイで利用できるIPv6のアドレス割り当てるのも難しいと思いますので、
私はルータから自動的に割り当てられたv6のアドレスを確認してからそのアドレスを割り当て増田。
今回は以下のアドレスを割り当てるものとします。(ここでもセキュリティを考慮してマスクしてあります)
# ラズパイに割り当てるIPv6のアドレス
2yyy:yyyy:yyyy:yyyy:yyyy:yyyy:yyyy:yyyy/64
- 次にDNSサーバはGoogleのIPv6で公開されているものとデフォルトゲートウェイのものを指定します。
ここではIPv6の2001:4860:4860::8888
(GoogleのプライマリDNS)、2001:4860:4860::8844
(GoogleのセカンダリDNS)、2xxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx(デフォルトゲートウェイ)の順番で設定しておきます。
# dhcpcd.confの設定
interface eth0
static ip6_address=2yyy:yyyy:yyyy:yyyy:yyyy:yyyy:yyyy:yyyy/64
static routers=2xxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
static domain_name_servers=2001:4860:4860::8888 2001:4860:4860::8844 2xxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
次にコンテナからラズパイ経由でインターネットに出るためのパケットフォワードの設定を行います。
- ラズパイ上でIPv6のパケットのフォワーディングの設定を行います。
今回はDockerのコンテナ上で動かしているサービス側からインターネット経由で別サービスのAPIを呼び出したいので、
/etc/sysctl.conf
の以下2つのipv6に関するforwardingを有効にしておきます。
デフォルトだと以下のように該当する2行がコメントアウトされていると思います。
# 変更前のsysctl.conf
#net.ipv6.conf.all.forwarding=0
#net.ipv6.conf.default.forwarding=0
以下のようにコメントを外して0を1に変更します。
# 変更後のsysctl.conf
net.ipv6.conf.all.forwarding=1
net.ipv6.conf.default.forwarding=1
この設定を行うことでDockerコンテナの中からIPv6のパケットがラズパイ経由でインターネットへ出ることができるようになります。
3.ラズパイのファイアウォール設定
- 次にファイアウォールの設定を行います。私はufwを使っているのでここではufwを使って設定します。
(ufw以外のツールを使っている方はご自身の環境に応じた設定を行ってください) - 今回はラズパイから出ていったパケットに関連するパケットのみがラズパイへ入って来れるようフォワードの設定を行います。
[ufwのforward設定]
私の環境でufwの設定を確認したらデフォルトでパケットのフォワードに関しては全てDROPするようになっていました。
# /etc/default/ufwの抜粋
DEFAULT_FORWARD_POLICY="DROP"
IPv6に関する設定を行うため/etc/ufw/before6.rules
の末尾にあるCOMMITの上に以下を追加します。
# IPv6に関するufwの設定追加
...
-A ufw6-before-forward -i eth0 -j ACCEPT
-A ufw6-before-forward -m state --state RELATED,ESTABLISHED -j ACCEPT
# don't delete the 'COMMIT' line or these rules won't be processed
COMMIT
設定を変更したら以下の通りufwの設定をリロードしてください。
# ufwのリロード
sudo ufw reload
4.ラズパイのDocker設定
- 次にDocker周りの設定を行います。DockerはデフォルトだとIPv6が無効になっているため有効にする必要があります。
今回は/etc/docker/daemon.json
を以下の通り設定します。
{
"ipv6": true,
"fixed-cidr-v6": "2001:db8:1::/64",
"experimental": true,
"ip6tables": true
}
-
"fixed-cidr-v6": "2001:db8:1::/64"
の設定はDockerの公式サイトにあるサンプルと同じものです。ご自身の環境に合わせて適宜変えてしまって大丈夫です。 -
"ip6tables": true
ですが、これはIPv6に関するNATの設定を自動的に行うためのものです。
IPv4の場合、-p
や--publish
などでホスト上(今回だとラズパイ上)にコンテナのポートを割り当てた際はDockerがデフォルトでNATの設定をしてくれます。しかし、IPv6の場合はデフォルトではNATの設定をしてくれません。"ip6table": true
をセットすれば自動的にIPv6のNAT設定が行われるようになります。
なお、このパラメータはDockerバージョン20.10.8ではexperimentalであるため、"experimental": true
も併せて設定ししておきます。 - docker.jsonを設定したらDockerデーモンを再起動してください。
5.docker-composeのサンプル
次にdocker-composeを使って実際にDjangoのコンテナを動かしてIPv6まわりの確認をしてみます。
ここではサンプルとして以下のようなdocker-compose.yml
を作成し、テスト用のDjangoのコンテナ(django-test)とIPv6のテスト用ネットワーク(ipv6-test)を用意します。
なお、今回はDjangoのコンテナを使っていますがRailsやSpringBootといった他のフレームワークのコンテナでも同じ話となります。
# docker-compose.ymlのサンプル
version: "3.1"
services:
django-test:
build: .
command: python3 manage.py runserver [::]:8080
volumes:
- .:/code
networks:
- ipv6-test
ports:
- 8080:8080
networks:
ipv6-test:
enable_ipv6: true
driver: bridge
ipam:
driver: default
config:
- subnet: 2001:1000:1000::/64
gateway: 2001:1000:1000::1
- 上の
networks
の部分でenable_ipv6: true
を指定しないとコンテナにIPv6のアドレスが割り当てられません。 -
ipam
のconfig
でサブネットとゲートウェイの設定を行いっています。
サブネットですが、先ほどdocker.json
のfixed-cidr-v6
で設定した内容("2001:db8:1::/64"
)と同じものをセットすると、ネットワーク作成時に以下のようなエラーが発生します。
Creating network "docker-test_ipv6-test" with driver "bridge"
ERROR: Pool overlaps with other one on this address space
なので、ここではサンプルで2001:1000:1000::/64
を設定しています。
- 次にDjangoのプロジェクトを生成するため以下を実行します。
(https://docs.docker.jp/compose/django.html を参考にしました)
# Djangoのプロジェクト作成
docker-compose run web django-admin.py startproject composeexample .
プロジェクトが作成されたら、composeexample/setting.py
を開いてALLOWED_HOSTS
に
/etc/dhcpcd.d
で設定したラズパイのIPv6のアドレスをセットします。(ここではセキュリティ上マスクして記載します)
# ALLOWED_HOSTSの設定
ALLOWED_HOSTS = ['[2yyy:yyyy:yyyy:yyyy:yyyy:yyyy:yyyy:yyyy]']
次にdjango-compose
でコンテナを起動します。
# コンテナの起動
docker-compose up -d
サービスを起動させたらdocker-compose ps
でコンテナの一覧を見てみます。
# docker-compose ps
Name Command State Ports
-------------------------------------------------------------------------------------------------------------
docker-test_django-test_1 python3 manage.py runserve ... Up 0.0.0.0:8080->8080/tcp,:::8080->8080/tcp
django-testのコンテナはpython3 manage.py runserver [::]:8080
というように、IPアドレスにv6の[::]
を指定して起動しています。この場合、IPv6とIPv4の両方でdjangoのサーバはLISTENします。(IPv6の[::]
はIPv4でいうところの"0.0.0.0"に相当します)
6.Dockerのネットワーク確認
- 次に上で作成されたネットワーク(ipv6-test)を
docker network inspect
で確認してみます。
# 作成されたネットワークの確認
docker network inspect docker-test_ipv6-test
結果は以下のようになりました。
[
{
"Name": "docker-test_ipv6-test",
"Id": "aa9292514cc7130a1a70b7e07409f08c62273fa96d62d574b7b1320cec177f4b",
"Created": "2021-10-01T05:55:42.889737347+09:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": true,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.20.0.0/16",
"Gateway": "172.20.0.1"
},
{
"Subnet": "2001:1000:1000::/64",
"Gateway": "2001:1000:1000::1"
}
]
},
"Internal": false,
"Attachable": true,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"b603d5fbc5fd70b0a71e88ec420a113da2f5292dd7aebc25223d5c4d6dc6522a": {
"Name": "docker-test_django-test_1",
"EndpointID": "94f080c14ef708fc91ec6d509aadbaa7dd0226ad32f6a40cc16717ccb1361ab5",
"MacAddress": "02:42:ac:14:00:02",
"IPv4Address": "172.20.0.2/16",
"IPv6Address": "2001:1000:1000::2/64"
}
},
"Options": {
"com.docker.network.enable_ipv6": "true"
},
"Labels": {
"com.docker.compose.network": "ipv6-test",
"com.docker.compose.project": "docker-test",
"com.docker.compose.version": "1.29.2"
}
}
]
Containers
のところでDjangoのコンテナ(Name
がdocker-test_django-test_1
となっている部分)に対してIPv6のアドレス(IPV6Address
が2001:1000:1000::2/64
となっている部分)が割り当てられているのが確認できます。また、IPv4のアドレスは自動的に172.20.0.2/16
が割り当てられています。
次にdocker exec
でDjangoのコンテナに入りip a
でIPアドレスを確認してみます。
# IPアドレスの確認
docker exec -it docker-test_django-test_1 /bin/bash
# ip aでIPアドレスを表示します
root@b603d5fbc5fd:/code# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
36: eth0@if37: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:14:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.20.0.2/16 brd 172.20.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 2001:1000:1000::2/64 scope global nodad
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe14:2/64 scope link
valid_lft forever preferred_lft forever
上でinet6 2001:1000:1000::2/64 scope global nodad
と表示されました。
docker network inspect
で確認したものと同じアドレスになっていることがわかります。
[ip6tablesの確認]
次に4で設定した"ip6tables": true
が正しく機能しているか確認してみます。
以下の通りip6tables
コマンドをラズパイ上で実行します。
sudo ip6tables -t nat -L -n
以下のような結果が表示されました。
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all ::/0 ::/0 ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all 2001:1000:1000::/64 ::/0
MASQUERADE all 2001:db8:1::/64 ::/0
MASQUERADE tcp 2001:1000:1000::2 2001:1000:1000::2 tcp dpt:8080
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DOCKER all ::/0 !::1 ADDRTYPE match dst-type LOCAL
Chain DOCKER (2 references)
target prot opt source destination
RETURN all ::/0 ::/0
RETURN all ::/0 ::/0
RETURN all ::/0 ::/0
DNAT tcp ::/0 ::/0 tcp dpt:8080 to:[2001:1000:1000::2]:8080
以下2行の設定からIPv6に関するNATの設定が正しく行われていることがわかります。
MASQUERADE tcp 2001:1000:1000::2 2001:1000:1000::2 tcp dpt:8080
DNAT tcp ::/0 ::/0 tcp dpt:8080 to:[2001:1000:1000::2]:8080
7.動作確認
[外部からラズパイ上のコンテナへのアクセス]
- 試しにブラウザを開いて
http://[2yyy:yyyy:yyyy:yyyy:yyyy:yyyy:yyyy:yyyy]:8080
にアクセスしてみてください。正しくアクセスできると以下のようなHTMLがブラウザに表示されると思います。
It worked!
Congratulations on your first Django-powered page.
Next, start your first app by running python manage.py startapp [app_label].
You're seeing this message because you have DEBUG = True in your Django settings file and you haven't configured any URLs. Get to work!
なお、ラズパイと同じLAN内にあるマシンから確認した場合、インターネット経由のアクセスではなくなるので、可能であればインターネット経由で確認した方が「外部からラズパイ上のサービスへアクセスできるようにする」という今回の趣旨の確認になると思います。
もし上のDjangoコンテナからHTMLが返ってこない場合、ルータのパケットフィルタリングやラズパイのネットワーク設定等を見直してください。
[コンテナ内部から外部へのアクセス]
- 次にDjangoのコンテナ内部からラズパイ、ルータを経由して自宅LANの外部へアクセスできるか確認してみます。
試しに以下の通りDjangoのコンテナに入ってping6
でGoogleのDNSサーバへアクセスしてみます。
問題なく接続できれば以下のような結果が返ってきます。
docker exec -it docker-test_django-test_1 /bin/bash
root@809786c94601:/code# ping6 -c 3 dns.google
PING dns.google(dns.google (2001:4860:4860::8844)) 56 data bytes
64 bytes from dns.google (2001:4860:4860::8844): icmp_seq=1 ttl=114 time=8.01 ms
64 bytes from dns.google (2001:4860:4860::8844): icmp_seq=2 ttl=114 time=5.53 ms
64 bytes from dns.google (2001:4860:4860::8844): icmp_seq=3 ttl=114 time=4.94 ms
次に以下の通りコンテナ内部からcurlで外部のWebサイトへアクセスしてみます。
試しにここでは日本語版WikipediaのメインページへIPv6でアクセスしみましょう。
# IPv6で接続するので`-6`スイッチをつけます
root@809786c94601:/code# curl -6 https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8
HTMLのレスポンスが返ってきたら、問題なくIPv6でWikipediaへアクセスできています。
8.DynamicDNSへの独自ドメインとIPv6の登録
Dockerコンテナで動かしているサービスを外部に公開する場合、通常はhttpsで公開すると思います。なので、Nginxなどでリバースプロキシを立てて証明書をインストールして運用することになると思います。
以下のような手順で独自ドメインと証明書を取得し、独自ドメインとIPv6アドレスをDDNSに登録すれば良いです。
- No-IPのようなサービスでフリーの独自ドメインを取得する。
- Let's EncryptやZeroSSLといった無料の証明書サービスにて独自ドメインの証明書を発行する。
- ラズパイに割り当てたIPv6のアドレスをNo-IPのようなDynamicDNSへ登録する。
No-IPはドメイン更新(手動)が必要ですが、無料で使えるので私は普段よく利用しています。
まとめ
今回はラズパイを使って以下のことを確認しました。
- IPoE接続しているLAN内のラズパイに対して外部からIPv6でアクセスする。
- Dockerコンテナからインターネット上のサービスに対してIPv6でアクセスする。
なお、以下の点にご注意下さい。
- ラズパイを外部へ公開する場合、セキュリティ対策をきちんと行ってください。
- ラズパイ上のサービスをIPv6で公開する場合、サービスを呼び出すクライアントもIPv6に対応している必要があります。
- 世の中で公開されているサービスによってはIPv6に対応していないものもあります。
参考資料
[Docker]
- https://docs.docker.com/config/daemon/ipv6/
- https://blog.iphoting.com/blog/2021/02/10/ipv6-docker-docker-compose-and-shorewall6-ip6tables/
[IPv6]
[DDNS]