TenForward

技術ブログ。はてなダイアリーから移転しました

af-graft を LXC コンテナで試してみた

Twitter でコンテナ用の面白い仕組みを見つけたので試してみました。中身ほぼゼロです。

論文も公開されているようですね。

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 の有無で処理がどう変わるか見てみましょう。処理は

  1. cgroup v2 をマウントする(オプション有無)
  2. cgroup.subtree_control にコントローラを登録
  3. cgroup 作成
  4. cgroup ディレクトリとそれ以下のファイルに所有権を与える
  5. cgroup にシェルの PID 登録
  6. cgroup namespace 作成
  7. 作成した 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  9701: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  9701: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/

test01test02 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/

先ほどと同様に test01test02 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 を定義しましょう。defaultlxd 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 でも設定ファイルにネットワークで物理インターフェースを指定することで同じことができます。