af-graft を LXC コンテナで試してみた
Twitter でコンテナ用の面白い仕組みを見つけたので試してみました。中身ほぼゼロです。
コンテナの中で開いたsocketがhost stackに生えるという不思議socketです。論文はこちら https://t.co/YyP67mDPt2 実装はこちら https://t.co/IaKO5RFfkG
— upa (@upaa) 24 July 2018
論文も公開されているようですね。
README を見る限りは普通に LXC で使えそうなのでロクにドキュメント読まず(コラ)やってみました。
とりあえず Ubuntu 16.04 ホストを準備します(AWS 上に準備しました)。そして LXC をインストールします。こういうときは LXD でなく設定ファイルちょいちょいといじって起動させられる LXC 便利。
host:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 16.04.5 LTS
Release: 16.04
Codename: xenial
host:~$ sudo apt install lxc
コンテナを作ります。
host:~$ lxc-create -t download -n c1 -- -d ubuntu -r xenial -a amd64 host:~$ sudo egrep '(lxc.net*|mount)' /var/lib/lxc/c1/config lxc.network.type = empty lxc.mount.entry = /home/ubuntu/af-graft home/ubuntu/af-graft none bind,relative,create=dir 0 0
lxc.network.type
(LXC 3.0ではlxc.net.0.type
)はempty
でループバックのみlxc.mount.entry
はホスト側でコンパイルしたバイナリをコンテナ内でもそのまま使いかかったので書いてるだけで特に重要ではありませんCAP_NET_ADMIN
が必要ですが、LXC コンテナではデフォルト許可なので capability はいじらず
ってところです。
ここでコンテナを起動します。コンテナには Apache がインストールされていて、コンテンツとして簡単な index.html
を準備しました。
host:~$ sudo lxc-start -n c1 host:~$ sudo lxc-attach -n c1 root@c1:~# root@c1:~# cat /var/www/html/index.html <h1>c1</h1>
ここからが本番。エンドポイントを作成します。
root@c1:~# af-graft/iproute2-4.10.0/ip/ip graft add nx4 type ipv4 addr 0.0.0.0 port 80 root@c1:~# af-graft/iproute2-4.10.0/ip/ip graft add nx6 type ipv6 addr :: port 80 root@c1:~# af-graft/iproute2-4.10.0/ip/ip graft show nx4 type ipv4 addr 0.0.0.0 port 80 nx6 type ipv6 addr :: port 80
はい、できましたね。apache 起動してみましょう。
root@c1:~# LD_PRELOAD=af-graft/tools/libgraft-hijack.so GRAFT_CONV_PAIRS="0.0.0.0:80=nx4 :::80=nx6" apachectl start libgraft-hijack.so:466:socket(): overwrite family 10 with AF_GRAFT (4) libgraft-hijack.so:597:setsockopt(): wrap setsockopt() level=1, optname=15 libgraft-hijack.so:466:socket(): overwrite family 2 with AF_GRAFT (4) libgraft-hijack.so:466:socket(): overwrite family 10 with AF_GRAFT (4) libgraft-hijack.so:597:setsockopt(): wrap setsockopt() level=1, optname=2 libgraft-hijack.so:597:setsockopt(): wrap setsockopt() level=1, optname=9 libgraft-hijack.so:597:setsockopt(): wrap setsockopt() level=1, optname=15 libgraft-hijack.so:540:bind(): convert bind :::80 to nx6
起動しました。試しに netstat してみると
root@c1:~# netstat -napt Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
何もありません。
では、アクセスしてみましょう。
- ホストから
host:~$ curl http://localhost/ <h1>c1</h1>
- Internet 経由で
$ curl http://afgrafttest.ten-forward.ws/ <h1>c1</h1>
ま、動いて当たり前なのでしょうが、試してみましたってことでご報告。論文なんかにパフォーマンス比較してありますが、手元でも時間があればやってみたいですね。macvlan とか ipvlan との比較も含めて(論文で何と比較してるのかは見てない)。
cgroup v2 の nsdelegate オプション(2)〜 cgroup namespace 作成後の cgroup root を権限委譲の境界として扱う
cgroup v2 の nsdelegate
オプション第 2 弾です。
前回紹介したのは、cgroup namespace を作ると cgroup root を超えたプロセスの移動が禁止されるという機能でした。
これはユーザに関わらず関係するお話でしたが、今回は一般ユーザで namespace(つまりコンテナ)を作ったときのお話です。
私の連載の第 40 回 で紹介したとおり、cgroup v1 に比べて cgroup v2 は一般ユーザでプロセスを移動できる条件が厳しくなりました。ただ、これは実際のユースケースに合っている、というのは連載で書いたとおりです。
一般ユーザに cgroup ツリーを権限委譲する際には、権限委譲する root cgroup に当たる cgroup には、
という権限を与えました。
cgroup 内でコントローラを使えるようにしていても、権限委譲の root cgroup 内のコントローラ関連のファイルには権限は与えません。コンテナに与えるリソースを定義するのはあくまでホストの管理者であって、コンテナの管理者が自身に与えられたリソースを変更できるとマズいから、というのは納得の行くところだと思います。
これを実現するために、前述のようにホスト側の管理者がパーミッションの調整を行っていたわけですが、nsdelegate
オプションをつけると、これが自動的に行われます。
nsdelegate
の有無で処理がどう変わるか見てみましょう。処理は
- cgroup v2 をマウントする(オプション有無)
cgroup.subtree_control
にコントローラを登録- cgroup 作成
- cgroup ディレクトリとそれ以下のファイルに所有権を与える
- cgroup にシェルの PID 登録
- cgroup namespace 作成
- 作成した cgroup のコントローラを制御するファイル(ここでは
pids.max
)に制限値を書き込む
という処理をやってみました。
nsdelegate
を付けない場合
では、まず普通に cgroup v2 をマウントして処理をすすめましょう。
# mount -t cgroup2 cgroup2 /sys/fs/cgroup/ (1) # echo "+io +pids +memory" > /sys/fs/cgroup/cgroup.subtree_control (2) # mkdir /sys/fs/cgroup/test01 (3) # chown karma /sys/fs/cgroup/test01 (4) # chown karma /sys/fs/cgroup/test01/* (4) $ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs (5) 4013 $ unshare --user --cgroup --map-root-user (6) root@plamo70b2:~# ls -l /sys/fs/cgroup/test01 (権限を確認) : (snip) -rw-r--r-- 1 root nogroup 0 9月 7日 01:03 pids.max
このように pids.max
には書き込み権がありますね。
一応 cgroup root にいるか確認します。
# cat /proc/$$/cgroup 0::/
ここで pids.max
に制限値として 50
を書き込みます。
# echo 50 > /sys/fs/cgroup/test01/pids.max (7) # cat /sys/fs/cgroup/test01/pids.max 50
登録できました。普通の cgroup v2 のコントローラ操作ですね。
nsdelegate
を付けた場合
nsdelegate
をオプションで与える以外は同じ操作をしてみます。
# mount -t cgroup2 -o nsdelegate cgroup2 /sys/fs/cgroup/ (1) # echo "+io +pids +memory" > /sys/fs/cgroup/cgroup.subtree_control (2) # mkdir /sys/fs/cgroup/test01 (3) # chown karma /sys/fs/cgroup/test01 (4) # chown karma /sys/fs/cgroup/test01/* (4) $ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs (5) 4013 $ unshare --user --cgroup --map-root-user (6) # ls -l /sys/fs/cgroup/test01/ : (snip) -rw-r--r-- 1 root nogroup 0 9月 7日 01:25 pids.max
先ほどと同様に書き込み権はあります。
cgroup root にいることも確認しておきます。
# cat /proc/$$/cgroup 0::/
先ほどと同様に制限値を書き込みます。
# echo 50 > /sys/fs/cgroup/test01/pids.max (7) -bash: echo: 書き込みエラー: 許可されていない操作です
今回は先ほどと違ってエラーになりました。
このように権限委譲した場合、その root cgroup には限定的な権限のみを与えるのが nsdelegate
のもうひとつの機能です。
つまり cgroup namespace (つまりコンテナ)を権限委譲の境界とし、その root cgroup (コンテナの root cgroup)には限定的な権限を与え、それ以下の cgroup のみ権限委譲対象のユーザ管理とするわけです。
LXDでコンテナごとに異なるユーザ権限でコンテナを起動する
$ lxc version Client version: 3.0.1 Server version: 3.0.1
な環境の Ubuntu 18.04 上で試してます。
以下で何の前提もなく書いてるサブ ID のお話は、私の連載
に書いていますのでそちらをどうぞ。
LXD でコンテナを起動すると、デフォルトでは非特権コンテナが起動します。
$ lxc launch ubuntu:18.04 c1 Creating c1 Starting c1 $ lxc list +------+---------+--------------------+-----------------------------------------------+------------+-----------+ | NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS | +------+---------+--------------------+-----------------------------------------------+------------+-----------+ | c1 | RUNNING | 10.56.2.183 (eth0) | fd42:dce3:4ae3:1cb3:216:3eff:fe25:e3c3 (eth0) | PERSISTENT | 0 | +------+---------+--------------------+-----------------------------------------------+------------+-----------+
誰の権限で起動しているか確認してみます。
$ ps auxf : (snip) root 13607 0.0 0.1 530556 7344 ? Ss 13:31 0:00 [lxc monitor] /var/lib/lxd/containers c1 100000 13626 0.1 0.2 159436 8816 ? Ss 13:31 0:00 \_ /sbin/init : (snip)
このように uid: 100000 のユーザで起動しています。
ここでもうひとつコンテナを起動します。
$ lxc launch ubuntu:18.04 c2 Creating c2 Starting c2 $ ps aux : (snip) root 14507 0.0 0.1 529148 7264 ? Ss 13:34 0:00 [lxc monitor] /var/lib/lxd/containers c2 100000 14532 1.4 0.2 77420 8660 ? Ss 13:34 0:00 /sbin/init : (snip)
こちらも uid: 100000 のユーザで起動します。
このようにデフォルトでは同一ホスト上で起動するコンテナはすべて同じユーザ権限で起動します。マルチテナントで、コンテナごとにユーザが異なる場合、セキュリティを考えると異なるユーザ権限でコンテナが実行されている方が望ましいでしょう。
LXD ではこれを制御する設定 security.idmap.isolated
があります。デフォルトではこれは false
ですので、コンテナもしくはプロファイルで true
としておくと、異なる uid/gid の範囲を使ってコンテナを起動します。
この辺りは、公式の userns-idmap あたりに詳しいです。
/etc/subuid, /etc/subgid の設定
デフォルトではコンテナには 65536 個の ID を割り当てるので、Ubuntu でユーザを登録すると、subuid/subgid として 65536 個の ID を使えるように設定されています(LXDはコンテナをrootユーザから起動するのでrootに対して)。
$ cat /etc/sub{u,g}id | grep root root:100000:65536 root:100000:65536
十分なサブ ID が確保されていない場合、
Error: Failed container creation: Not enough uid/gid available for the container.
みたいなエラーでコンテナを作成できません。
コンテナごとに異なる ID の範囲を割り当てる場合、コンテナごとに 65536 必要ですから、sub{u,g}id でも必要な個数設定しておく必要があります。
とりあえずサブ ID はユーザごとに異なっている必要もありませんので、ケチケチ設定する必要もないので、適当に 200000 個くらい設定しましょう。(LXD がどういう計算しているかわからないけど、コンテナ 2 つ起動するから 131072 とか設定すると上記エラーが出ます)
$ cat /etc/sub{u,g}id | grep root root:100000:200000 root:100000:200000
このあと、LXD を再起動しておきます。(サブ ID のマップを起動時に読み込むから)
$ sudo systemctl restart lxd
もう一つ気をつけること。ドキュメントにも書かれていないと思います。この辺りで発言があります。
/etc/sub{u,g}id に設定する ID の個数ですが、設定した最初の 65536 個(security.idmap.size
の設定)は security.idmap.isolated
を設定していないコンテナ専用です。
つまり先の例のように設定した場合、100000 〜 165535 までの ID は security.idmap.isolated
を設定していないコンテナが使いますので、security.idmap.isolated
を設定したコンテナを起動したい場合は、さらに 65536 個の ID が必要です。
ですので、ID の個数として /etc/sub{u,g}id に 65536 を設定してある環境で、ひとつもコンテナが起動していないところに、security.idmap.isolated
を設定したコンテナを起動しようとしても失敗します。
少なくとも 131073 以上の設定が必要です。まあ、ケチることはないので先の例のように多めに確保しておきましょう。
profile の設定
default
プロファイルの設定を変えてもいいですが、同じ ID 範囲で起動するコンテナを起動したい場合のためにそれは置いておいて、新しいプロファイルを作ります。
$ lxc profile copy default secure $ lxc profile set secure security.idmap.isolated true $ lxc profile show secure config: security.idmap.isolated: "true" : (snip)
コンテナの起動
$ lxc launch ubuntu:18.04 c1 --profile=secure Creating c1 Starting c1 $ lxc launch ubuntu:18.04 c2 --profile=secure Creating c2 Starting c2 $ ps aux : (snip) root 17072 0.0 0.1 604544 7324 ? Ss 14:03 0:00 [lxc monitor] /var/lib/lxd/containers c1 165536 17093 0.2 0.2 159472 8892 ? Ss 14:03 0:00 \_ /sbin/init : (snip) root 17935 0.0 0.1 529148 7196 ? Ss 14:03 0:00 [lxc monitor] /var/lib/lxd/containers c2 231072 17953 0.2 0.2 159420 8840 ? Ss 14:03 0:00 \_ /sbin/init : (snip)
はい、違うユーザ権限で起動していますね。
cgroup v2 の nsdelegate オプション(1)〜 namespace 外へのプロセス移動の禁止
cgroup v2がカーネルに導入された時点では、cgroup v2にはマウントオプションはありませんでした。
しかし、4.13 で nsdelegate
というオプションが導入されました。これは現時点でも cgroup v2 唯一のマウントオプションです。
このオプションは初期の namespace でマウントするときにのみ指定できます。それ以外の namespace では無視されます。
cgroup namespace については、私の連載 の 第34回 で説明しました。ここでは、cgroup namespace で cgroup ツリーを独立させた後でも、cgroup namespace 内の /
(root)を超えてプロセスを移動できました。
これはある意味コンテナ内のプロセスを、別のコンテナに移動させるという意味になります。
普通にコンテナを起動すると、mount namespace を分離させますので、他のコンテナのファイルシステムは見えないはずです。したがってこのようなコンテナをまたいだプロセスの移動はできないはずです。しかし、何らかの理由で別の cgroup 階層が見えるような場合は移動ができることになります。このような操作は通常は行わないケースがほとんどで、禁止したいケースがほとんどであると思います。
nsdelegate を使うとこれを禁止できます。
試してみましょう。以下は Plamo 7.0(4.14.44 kernel)で試しています(sysvinit バンザイ!)。
namespace をまたいだプロセスの移動
nsdelegate がないとき
オプションを指定せずに cgroup v2 をマウントします。これは、私の連載の第34回で説明しています。詳しくはそちらをどうぞ。
# mount -t cgroup2 cgroup2 /sys/fs/cgroup/
test01
と test02
cgroup を作成します。
# mkdir /sys/fs/cgroup/test0{1,2}
現在のシェルの PID を test01
に登録します。
# echo $$ | tee /sys/fs/cgroup/test01/cgroup.procs 4213
unshare で cgroup namespace を作成してシェルを起動します。
# unshare --cgroup -- /bin/bash
起動したシェルは親 cgroup と同じ cgroup に属することになるので、namespace 作成時点で親と自身が cgroup の /
(root)にいることになります。namespace 作成時点にいる cgroup が namespace 内では root となる、これが cgroup namespace でした。
# echo $$ 4284 # cat /proc/4213/cgroup 0::/ # cat /proc/4284/cgroup 0::/
親の namespace から見ると /test01
cgroup にいることになっています。ちゃんと namespace として働いているのがわかりますね。
parent namespace # cat /proc/4213/cgroup 0::/test01 parent namespace # cat /proc/4284/cgroup 0::/test01
ここでおもむろに現在のシェルの PID を test01
と同じレベルの別階層にある test02
に登録します。
# echo $$ > /sys/fs/cgroup/test02/cgroup.procs
すると /
(root)の一つ上の test02
を表す /../test02
という cgroup に属することになっています。/test01
が root になっているので、同じレベルの別階層だとこうなるのはわかりますね。
# cat /proc/$$/cgroup 0::/../test02
nsdelegate があるとき
nsdelegate 機能を使うには cgroup v2 をマウントする際に nsdelegate オプションを指定します。既に cgroup v2 がマウント済みの場合は -o remount,nsdelegate
と指定して再マウントすれば使えるようになります。
# mount -t cgroup2 -o nsdelegate cgroup2 /sys/fs/cgroup/
先ほどと同様に test01
、test02
cgroup を作りましょう。
# mkdir /sys/fs/cgroup/test0{1,2}
そして、test01
にシェルのプロセスを登録し、所属する cgroup を確認します。
# echo $$ > /sys/fs/cgroup/test01/cgroup.procs # unshare --cgroup /bin/bash # cat /proc/$$/cgroup 0::/
test01
が root cgroup になりました。ここまではさきほどと同じですね。
ここで先程と同じようにシェルの PID を test02
に移動させてみましょう。
# echo $$ > /sys/fs/cgroup/test02/cgroup.procs bash: echo: write error: No such file or directory
エラーになりました。nsdelegate
を指定して cgroup v2 をマウントすると、このように namespace の境界(root)をまたいで、別階層の cgroup にプロセスを移動できません(ENOENT
が返ります)。
nsdelegate には他にも重要な機能がありますので、次回にでも。
LXD コンテナに物理NICを直接与える
あまり役に立たないメモです。
コンテナホスト上でコンテナを起動する場合、ホスト上にブリッジを作成し、そこにアタッチする veth インターフェースを接続する場合が多いかと思います。
しかし、ベアメタル上に物理 NIC が多数あったり、SR-IOV で物理 NIC 上で仮想的な NIC が多数作成できる場合は、LXD コンテナに直接 NIC をアタッチできます(ただし、最新の LXD は SR-IOV 対応していたのでこのブログエントリは関係ないかも? < 良く知らない)。
コンテナホスト上にブリッジを作成し、veth でアタッチしてコンテナを起動する場合、コンテナに割り当てる profile は default
として、veth でホスト上のブリッジにアタッチするデバイスが定義されています(環境依存です)。
ホスト上の NIC を直接割り当てる場合はこのような profile は無駄ですので、一旦デバイスとして nic が存在しないコンテナ用 profile を作成します。これは default プロファイルをコピーすれば良いでしょう。
LXD 用に、ネットワークは定義しないプロファイル nonet
を定義しましょう。default
(lxd init
時に設定されそうなので環境依存だと思う)をコピーして lxc profile edit nonet
とかやるとエディタで編集できます。
$ lxc profile copy default nonet $ lxc profile edit nonet $ lxc profile show nonet config: {} description: Default LXD profile devices: root: path: / pool: default type: disk name: nonet used_by: []
コンテナを作ります。
$ lxc init ubuntu:18.04 --profile=nonet Creating the container Container name is: still-hamster $ lxc list +---------------+---------+------+------+------------+-----------+ | NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS | +---------------+---------+------+------+------------+-----------+ | still-hamster | STOPPED | | | PERSISTENT | 0 | +---------------+---------+------+------+------------+-----------+
ネットワーク設定のないコンテナが作られます。
$ lxc config show still-hamster | grep devices devices: {}
ここで、ホスト上にあるけど使われていない物理 NIC を確認してみましょう。次のようなアドレスも割当らず、UP していないインターフェース eth1
がありました。この eth1
が接続されているネットワークには DHCP サーバがあり、そこからアドレスがもらえるとします。
$ ip a : (snip) 3: eth1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000 link/ether 0e:9a:02:d4:20:38 brd ff:ff:ff:ff:ff:ff
これを先ほど作成したコンテナ still-hamster
に割り当てます。
$ lxc config device add still-hamster eth0 nic nictype=physical name=eth0 parent=eth1 Device eth0 added to still-hamster
これでコンテナ内に eth0
という名前で、ホスト上の eth1
が割り当たります。
$ lxc config show still-hamster : (snip) devices: eth0: name: eth0 nictype: physical parent: eth1 type: nic : (snip)
コンテナを起動します。
$ lxc start still-hamster $ lxc list +---------------+---------+--------------------+------+------------+-----------+ | NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS | +---------------+---------+--------------------+------+------------+-----------+ | still-hamster | RUNNING | 10.7.11.252 (eth0) | | PERSISTENT | 0 | +---------------+---------+--------------------+------+------------+-----------+
起動してアドレスも割当たっています。ちなみに Ubuntu:18.04
イメージでは、DHCP でアドレスをもらえる設定がなされています。
$ lxc exec still-hamster -- cat /etc/netplan/50-cloud-init.yaml : (snip) network: version: 2 ethernets: eth0: dhcp4: true
コンテナ内でルーティングテーブルを見てみると次のようにちゃんと割当たっていることがわかります。
$ lxc exec still-hamster -- ip route show table main default via 10.7.0.1 dev eth0 proto dhcp src 10.7.11.252 metric 100 10.7.0.0/16 dev eth0 proto kernel scope link src 10.7.11.252 10.7.0.1 dev eth0 proto dhcp scope link src 10.7.11.252 metric 100
ちなみに、コンテナが起動した状態で eth1
を確認してみると、eth1
はコンテナの namespace に移動していますので、ホスト上からは見えません。
$ ip a | grep eth1
$
ここでは LXD での例を示しましたが、LXC でも設定ファイルにネットワークで物理インターフェースを指定することで同じことができます。