Table of Contents

モチベーション

OSI参照モデルで役割を把握しておく

OSI参照モデルを覚える必要は無いと思うが,どのレイヤーがどのような役割を行っているのかを理解しておく必要はあると思う.

レイヤー 呼称
L7 アプリケーション層 HTTP,FTP,DNS,SNMP
L6 プレゼンテーション層 HTTP,FTP,DNS,SNMP
L5 セッション層 HTTP,FTP,DNS,SNMP,TCP
L4 トランスポート層 TCP,UDP
L3 インターネット層 IP,ICMP,ARP
L2 リンク層 Ethernet,Wi-Fi
L1 物理層 UTP,STP

L2はブリッジ周辺

L3はルーター周辺

L4はソケット周辺

L7,L6,L5はアプリケーション周辺

CRUD HTTPメソッド
Create POST/PUT
Read GET
Update PUT
Delete DELETE

つまり,サーバー/クライアントアプリケーションはL7,L6,L5,L4らへん

以下はC言語での実装例であるが,基本的にはどのサーバー/クライアントアプリケーションもL7,L6,L5,L4らへんを実装したものである.

サーバー側

クライアント側

ifconfigとかiptablesとかで見ているのはL3,L2らへん

ここからが本題.L2,L3は物理層であるL1に依存しているため,基本的に,L1-L2-L3は一対一に対応している.しかし,同一のホスト内で異なるIPアドレスを持ったアプリケーションを実行するためには,L3(ルーター)を複数用意する必要があり,場合によっては,L3をまとめるL2(ブリッジ)も複数用意する必要がある.これを実現するには,複数のL2,L3を仮想的に用意できる機能であるnetnsを用いる.

Network Namespace(netns)でホスト内のネットワークを分離する

Network Namespace(netns)とは,ルーティングテーブルやブリッジなどのホスト内のネットワークスタックを仮想的に分離するLinuxカーネルの機能である.同一のホスト上でプロセス毎にネットワークスタックを設定することができ,分離された空間をNamespaceと呼ぶ.

Network Namespaceをつくってみる

まず,ifconfigにより,eth0やloなどのインターフェースを確認することができ,ethXは物理的なインターフェースであり,ホスト外からみたIPアドレスを指す.また,loは仮想的なインターフェースであり,ホストからみた自身のIPアドレス(一般的には127.0.0.0/8)を指す.

ip netns add <name>により.任意のnamespaceをホストに定義することができる.

$ sudo ip netns add mynamespace
$ ip netns list
mynamespace

ip netns exec <namespace> <command>により,namespaceと<command>を紐付けることができる.これにより,<command>はホストのネットワークから隔離された状態で実行される.

$ sudo ip netns exec mynamespace sh

# ifconfig <=== この時点ではactiveなインターフェースは存在しない

# ping 127.0.0.1 <=== loへのpingも通らない
connect: Network is unreachable

# ip link set lo up <=== loをupする
# ping 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.028 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.032 ms
^C
--- 127.0.0.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1005ms
rtt min/avg/max/mdev = 0.028/0.030/0.032/0.002 ms
#
# ifconfig
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536 <=== loが立ち上がる
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 4  bytes 336 (336.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 4  bytes 336 (336.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

# exit

ホストとnamespaceをつなげるためには,仮想的なインターフェースであるVirtual Ethernetのペアを用意する必要がある.

$ sudo ip link add type veth <=== ホストでveth0/1のペアを用意する.ここではVirtual Ethernetのニックネームをvethとした.
$ ip link
...
117: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 36:07:ad:3a:ea:0e brd ff:ff:ff:ff:ff:ff
118: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 96:70:77:07:c0:98 brd ff:ff:ff:ff:ff:ff
    veth0/1のペアが用意された.

$ sudo ip link set veth1 netns mynamespace <=== mynamespaceにveth1を持っていく
$ ip link
...
117: veth0@if118: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 36:07:ad:3a:ea:0e brd ff:ff:ff:ff:ff:ff link-netnsid 2
    veth1が見えなくなった.

$ ip address show veth0
117: veth0@if118: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 36:07:ad:3a:ea:0e brd ff:ff:ff:ff:ff:ff link-netnsid 2
    veth0にはIPアドレスが設定されていないようだ

$ sudo ip addr add 172.19.0.1/24 dev veth0 <=== veth0にIPアドレスを設定する
$ ip address show veth0
117: veth0@if118: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 36:07:ad:3a:ea:0e brd ff:ff:ff:ff:ff:ff link-netnsid 2
    inet 172.19.0.1/24 scope global veth0 <=== IPアドレスが設定された
       valid_lft forever preferred_lft foreve

$ sudo ip link set veth0 up <=== veth0をactiveにする
$ ip address show veth0
117: veth0@if118: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state LOWERLAYERDOWN group default qlen 1000
    link/ether 36:07:ad:3a:ea:0e brd ff:ff:ff:ff:ff:ff link-netnsid 2
    inet 172.19.0.1/24 scope global veth0
       valid_lft forever preferred_lft forever
    <NO-CARRIER,BROADCAST,MULTICAST,UP>によるとUPしたらしい

$ sudo ip netns exec mynamespace bash <=== mynamespace内でbashを立ち上げる
# ifconfig
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 20  bytes 1504 (1.5 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 20  bytes 1504 (1.5 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

# ip link 
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
118: veth1@if117: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 <=== mynamespace内にveth1がつくられている
    link/ether 96:70:77:07:c0:98 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    veth0の片割れがnamespace内で見つかった

# sudo ip addr add 172.19.0.2/24 dev veth1 <=== veth1にIPアドレスを設定する
# ip addr
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
118: veth1@if117: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 96:70:77:07:c0:98 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.19.0.2/24 scope global veth1 <=== veth1にIPアドレスが設定された
       valid_lft forever preferred_lft forever

# ip link set veth1 up
# ifconfig
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 36  bytes 2672 (2.6 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 36  bytes 2672 (2.6 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

veth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500 <=== veth1がactiveなインターフェースとして認識された
        inet 172.19.0.2  netmask 255.255.255.0  broadcast 0.0.0.0
        inet6 fe80::9470:77ff:fe07:c098  prefixlen 64  scopeid 0x20<link>
        ether 96:70:77:07:c0:98  txqueuelen 1000  (Ethernet)
        RX packets 6  bytes 516 (516.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 6  bytes 516 (516.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

# ping 172.19.0.1
PING 172.19.0.1 (172.19.0.1) 56(84) bytes of data.
64 bytes from 172.19.0.1: icmp_seq=1 ttl=64 time=0.073 ms <=== ホスト側のveth0との疎通がとれている
64 bytes from 172.19.0.1: icmp_seq=2 ttl=64 time=0.070 ms
^C
--- 172.19.0.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1010ms
rtt min/avg/max/mdev = 0.070/0.071/0.073/0.008 ms

ここまでで,ホスト側からnamespace内のveth1への疎通は取れるようになる.しかし,veth1から172.19.0.0/24以外には出られない. なぜなら,namespace内のルーティングテーブルには,デフォルトルート(デフォルトゲートウェイ)が設定されていないので,172.19.0.0/24以外の行き先を解決することができないからである. したがって,namespace内でデフォルトルートを設定することで,namespaceから172.19.0.0/24以外に行くことができる.

$ sudo ip netns exec mynamespace bash
# ip route
172.19.0.0/24 dev veth1 proto kernel scope link src 172.19.0.2

# ping 172.17.0.1
connect: Network is unreachable <=== ホスト側には予め用意した172.17.0.1があるはずだが見つからない

# ip route add default via 172.19.0.1 <=== 172.19.0.1をmynamespaceのデフォルトルートにする
# ip route
default via 172.19.0.1 dev veth1
172.19.0.0/24 dev veth1 proto kernel scope link src 172.19.0.2

# ping 172.17.0.1
PING 172.17.0.1 (172.17.0.1) 56(84) bytes of data.
64 bytes from 172.17.0.1: icmp_seq=1 ttl=64 time=0.047 ms <=== ホスト側の172.17.0.1が見つかった
64 bytes from 172.17.0.1: icmp_seq=2 ttl=64 time=0.054 ms
^C
--- 172.17.0.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1013ms
rtt min/avg/max/mdev = 0.047/0.050/0.054/0.007 ms

ここまでの設定によって,namespaceとホストを行き来することが可能になるが,ホストのeth0からWAN側へは出られない. なぜなら,NATの設定をしていないので,IPアドレスの変換が行われないからである. したがって,ホストのiptableにNATの設定を行うことで,namespaceからWANに行くことが可能になる.

# ping 185.XXX.XXX.XXX
PING 185.XXX.XXX.XXX (185.XXX.XXX.XXX) 56(84) bytes of data.
^C
--- 185.XXX.XXX.XXX ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2032ms <=== mynamespaceからWANに出られない

$ sudo iptables --table nat --append POSTROUTING --source 172.19.0.0/24 --jump MASQUERADE
$ sudo iptables -vnL -t nat

...
Chain POSTROUTING (policy ACCEPT 1 packets, 59 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 MASQUERADE  all  --  *      *       172.19.0.0/24        0.0.0.0/0 
    各インターフェースから出ようとする172.19.0.0/24からきたパケットのIPアドレスを,出ようとしたインターフェースのIPアドレスに変換するようにする.
...

# ping 185.XXX.XXX.XXX
PING 185.XXX.XXX.XXX (185.XXX.XXX.XXX) 56(84) bytes of data.
64 bytes from 185.XXX.XXX.XXX: icmp_seq=1 ttl=58 time=2.22 ms
64 bytes from 185.XXX.XXX.XXX: icmp_seq=2 ttl=58 time=1.04 ms
64 bytes from 185.XXX.XXX.XXX: icmp_seq=3 ttl=58 time=1.14 ms
^C
--- 185.XXX.XXX.XXX ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2006ms <=== mynamespaceからWANに出られた
rtt min/avg/max/mdev = 1.049/1.474/2.227/0.533 ms

一般に,同一のネットワークIDを持つVirtual Ethernetは一つのブリッジでまとめておく.

$ sudo ip link add mybridge type bridge <=== mybridgeというブリッジを追加する
$ sudo ip link
117: veth0@if118: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 36:07:ad:3a:ea:0e brd ff:ff:ff:ff:ff:ff link-netnsid 2
119: mybridge: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 <=== mybridgeが追加された
    link/ether da:09:90:31:b9:c3 brd ff:ff:ff:ff:ff:ff

$ sudo ip link set dev veth0 master mybridge <=== mybridgeにveth0を接続する
$ sudo ip link show master mybridge
117: veth0@if118: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master mybridge state UP mode DEFAULT group default qlen 1000
    link/ether 36:07:ad:3a:ea:0e brd ff:ff:ff:ff:ff:ff link-netnsid 2
    mybridgeにveth0が接続された

$ sudo ip link set mybridge up <=== mybridgeをupする
$ sudo ip link
117: veth0@if118: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master mybridge state UP mode DEFAULT group default qlen 1000
    link/ether 36:07:ad:3a:ea:0e brd ff:ff:ff:ff:ff:ff link-netnsid 2
119: mybridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 <=== mybridgeがupした
    link/ether 36:07:ad:3a:ea:0e brd ff:ff:ff:ff:ff:ff

$ sudo ip addr del 172.19.0.1/24 dev veth0 <=== veth0に割り当てているルートアドレスを消す
$ sudo ip addr add 172.19.0.1/24 dev mybridge <=== mybridgeにルートアドレスを設定する

# ping 172.17.0.1
PING 172.17.0.1 (172.17.0.1) 56(84) bytes of data.
64 bytes from 172.17.0.1: icmp_seq=1 ttl=64 time=0.061 ms <=== mynamespaceから別のホストIDにアクセスできた
64 bytes from 172.17.0.1: icmp_seq=2 ttl=64 time=0.079 ms
64 bytes from 172.17.0.1: icmp_seq=3 ttl=64 time=0.092 ms
^C
--- 172.17.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2044ms
rtt min/avg/max/mdev = 0.061/0.077/0.092/0.014 ms

ここまでで,eth0(ホスト)-bridge-veth(ホスト側)-veth(プロセス側)という構成を実現できた.

コンテナがデフォルトで構成するネットワーク

ここまでやってきたnetnsによるL2,L3の仮想化作業をDockerは自動で行っている.Dockerサービスはdocker0というDockerブリッジをデフォルトで用意する.

$ docker network ls | grep bridge
dcec008f4804        bridge              bridge              local

$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "dcec008f4804...
        "Created": "2020-07-04T02:51:36.795932003+09:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

$ ifconfig docker0
docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        inet6 ...  prefixlen 64  scopeid 0x20<link>
        ether ...  txqueuelen 0  (Ethernet)
        RX packets 33329  bytes 4334088 (4.3 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 155842  bytes 269698810 (269.6 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

ここで,docker exec -it <container> bashなどでコンテナを立ち上げると,docker0ブリッジにvethがつくられ,ホストからvethの片割れを確認することができる.

$ bridge link show
116: veth7cd7c37 state UP @if115: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master docker0 state forwarding priority 32 cost 2

$ ifconfig veth7cd7c37
veth7cd7c37: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet6 ...  prefixlen 64  scopeid 0x20<link>
        ether ...  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 12  bytes 976 (976.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

更に,立ち上げたコンテナプロセスのnamespaceに潜り込むと,コンテナ内のvethを確認することができる.

$ docker run -it --name hoge -p 8080:80 nginx bash

$ docker inspect hoge
...
            "Pid": 29942, <=== コンテナのPID
...

$ sudo ln -fs /proc/29942/ns/net /var/run/netns/container <=== コンテナ側のnetnsをホストで見れるようにsymbolic linkを張る

$ sudo ip netns exec container bash

# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.2  netmask 255.255.0.0  broadcast 172.17.255.255 <=== コンテナ内のvethが見れた.eth0にリネームされている.
        ether 02:42:ac:11:00:02  txqueuelen 0  (Ethernet)
        RX packets 15  bytes 1186 (1.1 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

コンテナへのパケットがiptablesによってどのようにフィルタされ,どのように外に出ていくかを知る

コンテナ用のネットワークをつくる

publishとexpose

docker run-p(publish)オプションを指定したり,docker-compose.ymlでportsを指定すると,NATテーブルが自動で書き換えられる.これにより,WANとコンテナ間のアクセスが実現する.一方,セキュリティの観点から,80/443番ポートはpublishしたいが,DBコンテナとサーバーコンテナ間のポートはpublishしたくないなどと言った場合は,それぞれのコンテナを立ち上げるときに--expose <port>を指定することで,exposeなportはホスト内のみで有効となる.docker-compose.ymlならEXPOSEがそれに該当する.

docker-composeで作成されるネットワーク

Kubernetesのネットワーク構成

あとで書く