TenForward

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

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

LXC 3.0 新機能の予習

ここ最近、新バージョンリリース時と、ドキュメント(man pages)に更新があったときに翻訳する以外、新しい機能について全く調査していませんでした。

なんとなく見てると LXC 3.0 が近いようですので、どう変わるのかをまとめてみます。

cgroup ドライバの整理

これまで LXC には cgroup 関連のドライバが 3 つ含まれていました。

  • cgfs
  • cgmanager
  • cgfsng

cgfs ドライバはもっとも古くからあるドライバです。今や cgroup は /sys/fs/cgroup にマウントされますが、昔は特にマウントする場所は決まっておらず、/dev/cgroup にマウントしたり、/cgroup にマウントしたりしていました。どこにマウントされるかわからないファイルシステムを検出するロジックなど、今や不要になったロジックが多く含まれています。

cgfs ドライバの機能は cgfsng でカバーされていますので、cgfs ドライバは削除されるようです。

cgmanager ドライバは Ubuntu 14.04 のあたりに導入された cgmanager を使って cgroup を管理するためのドライバでした。カーネルに cgroup namespace が導入された今となっては、cgmanager は不要で、すでに廃止予定のプロジェクトになっていますので、cgmanager ドライバも廃止されます。

テンプレートの整理

LXC 2.x までは、様々なディストリビューション用のコンテナイメージを作成するために、シェルスクリプトで書かれたテンプレートが各ディストリビューションごとに用意されていました。

LXC 3.0 では、ディストリビューション依存のテンプレートが削除されるようです。ただ、バサッと切り捨てるのではなく、lxc-templatesというプロジェクトに分離されました。

LXC プロジェクト配下には、新たに distrobuilder という、イメージを作成するための Go 言語によるツールの開発が始まっています。

distrobuilder

distrobuilder は、

という機能を持っているようです。

現時点では、各ディストリビューションが準備している

  • alpine イメージ(alpine-minirootfs-*.tar.gz)
  • arch イメージ(archlinux-bootstrap-*-x86_64.tar.gz)
  • CentOS イメージ(iso イメージ?)
  • Ubuntu イメージ(ubuntu-base--base-.tar.gz)

を使用してイメージを作成する機能と、

  • debootstrap

を使用してイメージを作成する機能があるようです。

各種言語バインディングの分離

LXC のソースアーカイブに含まれていた、

は独立したリポジトリに分離されるようです。

pam_cgfs の LXC への移動

ユーザログイン時に、ユーザ用の cgroup を作成するための pam モジュールとして pam_cgfs が LXCFS で開発されていましたが、LXC 3.0 からは LXC ツリー配下で開発されるようです。

cgroup v2 サポート

cgroup v2 がサポートされたようです。


他にもあるでしょうけど、とりあえずこのあたりで。

参考

GnuPGで鍵取得しようとするとdirmngrに繋がらないと怒られる

単なるメモ。Plamo-7.0 開発中環境でのお話。

gnupg 2.1.19 までは大丈夫なんだけど、gnupg 2.1.23、2.2.0、2.2.1 にすると、dirmngr がうまく動かない… (2.1.20 〜 22 は作ってないので知らない)。

$ gpg --recv-keys ()
gpg: connecting dirmngr at '/home/karma/.gnupg/S.dirmngr' failed: IPC connect呼び出しに失敗しました
gpg: 鍵サーバからの受信に失敗しました: dirmngrがありません

となる。この時、dirmngr は起動しているけど、

tcp        0      1 10.200.200.232:45602    127.0.0.1:9050          SYN_SENT    27056/dirmngr

のように、どうやら Tor に接続に行っている模様。これコンパイル時に無効化できんの? もしくはデフォルト使わないってできんの?

もちろん dirmngr.conf で使わないように設定すれば使わない。でも Plamo では、システムワイドで dirmngr 起動するわけではなく、ユーザごとに起動することになるので、ユーザの dirmngr.conf にいちいち書かなければならない。

$ cat ~/.gnupg/dirmngr.conf 
no-use-tor

いや、GnuPG 詳しくないから知らんけど

マニュアルには The default is to use Tor if it is available on startup or after reloading dirmngr. と書いてある。でも Tor なんて入ってないと思うんだけど…

GnuPGのdirmngrのコード見ると、dirmngr/server.c

/* This function returns true if a Tor server is running.  The status
 * is cached for the current connection.  */
static int
is_tor_running (ctrl_t ctrl)
{
  /* Check whether we can connect to the proxy.  */

  if (!ctrl || !ctrl->server_local)
    return 0; /* Ooops.  */

  if (!ctrl->server_local->tor_state)
    {
      assuan_fd_t sock;

      sock = assuan_sock_connect_byname (NULL, 0, 0, NULL, ASSUAN_SOCK_TOR);
      if (sock == ASSUAN_INVALID_FD)
        ctrl->server_local->tor_state = -1; /* Not running.  */
      else
        {
          assuan_sock_close (sock);
          ctrl->server_local->tor_state = 1; /* Running.  */
        }
    }
  return (ctrl->server_local->tor_state > 0);
}

こんな関数がある。ここで tor_state が 1 になってる?

libassuan を見ると、src/assuan-socket.c 内に

_assuan_sock_connect_byname (assuan_context_t ctx, const char *host,
                             unsigned short port, int reserved,
                             const char *credentials, unsigned int flags)
{
  assuan_fd_t fd;
  unsigned short socksport;

  if ((flags & ASSUAN_SOCK_TOR))
    socksport = TOR_PORT;
  else if ((flags & ASSUAN_SOCK_SOCKS))
    socksport = SOCKS_PORT;
  else
    {
      gpg_err_set_errno (ENOTSUP);
      return ASSUAN_INVALID_FD;
    }

  if (host && !*host)
    {
      /* Error out early on an empty host name.  See below.  */
      gpg_err_set_errno (EINVAL);
      return ASSUAN_INVALID_FD;
    }

  fd = _assuan_sock_new (ctx, AF_INET, SOCK_STREAM, 0);
  if (fd == ASSUAN_INVALID_FD)
    return fd;

  /* For HOST being NULL we pass an empty string which indicates to
     socks5_connect to stop midway during the proxy negotiation.  Note
     that we can't pass NULL directly as this indicates IP address
     mode to the called function.  */
  if (socks5_connect (ctx, fd, socksport,
                      credentials, host? host:"", port, NULL, 0))
    {
      int save_errno = errno;
      assuan_sock_close (fd);
      gpg_err_set_errno (save_errno);
      return ASSUAN_INVALID_FD;
    }

  return fd;
}

なんて関数がある。よくわからんw

いや、違うな。dirmngr に Tor のオプションを指定すると、オプションが --use-tor--no-use-tor かに関わらず dirmngr はすぐに起動するけど、何も指定しないと起動後 127.0.0.1:9050 へのアクセスをして、だいぶタイムアウトを待った後に、使わない設定で起動するな。dirmngr.conf を準備して、ちゃんと設定しろ、ということかな。なんて不便なソフトウェアだ。

$ strace dirmngr --server --homedir /home/karma/.gnupg -vvv --debug-all
  :(snip)
write(2, "dirmngr[27628]: enabled debug fl"..., 108dirmngr[27628]: enabled debug flags: x509 crypto memory cache memstat hashing ipc dns network lookup extprog) = 108
write(2, "\n", 1
)                       = 1
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(9050), sin_addr=inet_addr("127.0.0.1")}, 16
  :(↑でだんまり)
  :(snip)

うん、接続を延々待ってるわ。これで失敗したあとにちゃんと dirmngr は起動して、クライアントからのリクエストに応える。不便すぎて涙出るな。

Linux 4.10 で入った Overlayfs の redirect_dir 機能の動きを軽く追ってみた

ちょっと前にリリースされたカーネルですが、4.10 で overlayfs に変更が入っていましたのでちょっと調べてみました。

Overlayfs についてはこちらをどうぞ (ちょっと古い記事なのでカーネルにマージされる前の仕様も説明しています)。

それとコンテナ勉強会のこの発表資料

カーネルの変更と準備

4.10の関連するコミットは以下。

lowerdir側に存在するディレクトリを移動した場合の改良ってところでしょうか。相変わらずパッチも短くて見やすくて変更が追いやすそうですね (あまりちゃんと見てませんが)。

以下は 4.13-rc5 カーネルで試しています。

カーネルの config で Overlayfs: turn on redirect dir feature by default を “Y” にするとデフォルトで有効になるようです (CONFIG_OVERLAY_FS_REDIRECT_DIR)。

従来の動き

まずは従来の動きにするように redirect_dir=off というオプションを与えます。

$ mkdir lower upper work overlay # overlayfs用のディレクトリの作成
$ mkdir lower/lowerdir upper/upperdir # 下層、上層それぞれにディレクトリ作成
$ touch lower/lowerdir/lowfile upper/upperdir/upfile # ディレクトリ内にファイル作成
$ sudo mount -t overlay \
> -o lowerdir=lower,upperdir=upper,workdir=work,redirect_dir=off
> overlayfs overlay/ # マウント

重ね合わされた overlay ディレクトリの中身はこんな風になります。普通の動きですね。

$ find overlay/
overlay/
overlay/lowerdir
overlay/lowerdir/lowfile
overlay/upperdir
overlay/upperdir/upfile

lowerdir を移動しましょう。

$ cd overlay
$ mv lowerdir lowerdir2
$ ls -F
lowerdir2/  upperdir/

ここで下層の lowerdir と上層の upperdir の中を覗いてみます。

$ find lower/
lower/
lower/lowerdir
lower/lowerdir/lowfile

Overlayfs は下層側は変化しませんので、これは当たり前。

$ ls -l upper/
合計 8,192
c--------- 1 root  root   0, 0  81617:32 lowerdir
drwxr-xr-x 2 karma users 4,096  81617:29 lowerdir2/
drwxr-xr-x 2 karma users 4,096  81617:29 upperdir/

Overlayfs では、削除されたファイルやディレクトリは特別なデバイスファイルになるのでした。lowerdir は削除された状態になっており、新たに移動先の lowerdir2 が上層に作成されていますね。lowerdir2 内には、元々下層の lowerdir 内に存在していたファイルがコピーされています。

$ ls -l upper/lowerdir2/
合計 0
-rw-r--r-- 1 karma users 0  81617:29 lowfile

ディレクトリ内に多数のファイルやディレクトリがある場合は時間がかかりそうです。

改良後の動き

4.10 で導入された “redirect on rename-dir” という機能を使ってみましょう。

$ sudo mount -t overlay \
> -o lowerdir=lower,upperdir=upper,workdir=work,redirect_dir=on \
> overlayfs overlay

ここでは明示的に redirect_dir=on としていますが、カーネルの config でこの機能を “Y” にしてあれば、自動的にオンになりますので、これは不要です。

$ find overlay/
overlay/
overlay/lowerdir
overlay/lowerdir/lowfile
overlay/upperdir
overlay/upperdir/upfile

先ほどと同じように下層と上層が重ね合わされた状態になっています。この状態で先ほどと同じようにディレクトリを移動してみましょう。

$ cd overlay/
$ ls
lowerdir/  upperdir/
$ mv lowerdir lowerdir2
$ ls 
lowerdir2/  upperdir/

はい、移動されました。それでは下層と上層のディレクトリ内を覗いてみましょう。

$ ls -l upper/
合計 12,288
c--------- 1 root  root   0, 0  81617:35 lowerdir
drwxr-xr-x 2 karma users 4,096  81617:34 lowerdir2/
drwxr-xr-x 2 karma users 4,096  81617:34 upperdir/

これは先ほどと同じですね。移動前の lowerdir が削除された状態になっており、新たに lowerdir2 が作成されています。この lowerdir2 内を覗いてみましょう。

$ ls -l upper/lowerdir2/
合計 0
$ ls overlay/lowerdir2/
lowfile

おや? 先ほどは内部のファイルもコピーされていましたが、今回はファイルが存在しません。にもかかわらず、重ね合わせたディレクトリにはちゃんとファイルが存在しますね。下層側を見てみると、

$ ls -l lower/lowerdir/
合計 0
-rw-r--r-- 1 karma users 0  81617:34 lowfile

ファイルが存在しますが、これは従来と変わりありません。

ディレクトリを移動した場合は、中身のファイルは元の下層側のファイルを参照しているようですね。参照先はどうやって調べているのでしょう?

先に紹介したコミットにも、Documentation/filesystems/overlayfs.txt にも書かれてあります。

2. If the "redirect_dir" feature is enabled, then the directory will be
   copied up (but not the contents).  Then the "trusted.overlay.redirect"
   extended attribute is set to the path of the original location from the
   root of the overlay.  Finally the directory is moved to the new
   location.

調べてみましょう。

$ sudo getfattr -n trusted.overlay.redirect upper/lowerdir2
# file: upper/lowerdir2
trusted.overlay.redirect="lowerdir"

ここに参照先が保存されていますね。

階層が深くなった時は “he path of the original location from the root of the overlay” ってことだから、マウントポイントからのパスが保存されているのかと思ったら、単なるディレクトリ名でした。まあ追えるからかな。

$ sudo getfattr -n trusted.overlay.redirect upper/lowerdir2/hogehoge2
# file: upper/lowerdir2/hogehoge2
trusted.overlay.redirect="hogehoge"

ちなみに redirect_dir 機能を使って操作された overlayfs (の upperdir) を、redirect_dir 機能をサポートしていないカーネルではマウントできませんので注意しましょう (あまりないか…)。

おまけ

4.10 では、redirect 機能の他に、ディレクトリの移動時の変更がもうひとつ加えられています。xfs の時に効果を発揮するのでしょうか。