CUBE SUGAR CONTAINER

技術系のこと書きます。

Linux の UTS Namespace について

Linux のコンテナ仮想化を構成する要素の 1 つに、カーネルの Namespace (名前空間) という機能がある。 Namespace には色々とあるけど、今回はホスト名と NIS (Network Information Service) 1 ドメイン名を隔離する仕組みを提供している UTS Namespace について扱ってみる。

具体的には、unshare(1) と unshare(2) を使ってホスト名が隔離される様子を観察する。 unshare(2) というのは Namespace を操作するためのシステムコール。 そして、unshare(1) は同名のシステムコールを利用したコマンドラインツールになっている。

使った環境は次のとおり。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04.3 LTS"
$ uname -rm
5.4.0-92-generic aarch64
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

もくじ

下準備

まずは、事前に必要なパッケージをインストールしておく。 unshare(1) を使うために util-linux を、C のコードをビルドするために build-essential を入れておく。

$ sudo apt-get update
$ sudo apt-get install -y util-linux build-essential

unshare(1) を使って UTS Namespace を使ってみる

まずは unshare(1) から UTS Namespace を使ってみよう。 あらかじめ、システムのホスト名と NIS ドメイン名を確認しておく。

$ hostname
focal
$ domainname
(none)

上記から、ホスト名は focal で、NIS ドメイン名は設定されていないことが分かる。

現在の Namespace の情報も確認しておこう。 procfs の内容から、今は 4026531838 という識別子の UTS Namespace であることが確認できる。

$ file /proc/$$/ns/uts
/proc/949/ns/uts: symbolic link to uts:[4026531838]

続いて、unshare(1) を使って新しく UTS Namespace を作って bash を立ち上げる。

$ sudo unshare --uts bash

procfs の内容から、UTS Namespace の識別子が 4026532128 に変わったことが分かる。

# file /proc/$$/ns/uts
/proc/1068/ns/uts: symbolic link to uts:[4026532128]

ここで hostname(1) と domainname(1) を使ってホスト名と NIS ドメイン名を変更してみよう。

# hostname host.example.com
# domainname example

次のとおり、ちゃんと変更された。

# hostname
host.example.com
# domainname
example

しかし、上記の変更はあくまで新しく作られた UTS Namespace 上での操作に過ぎないはず。 exit して、元のシェルに戻ってみよう。

# exit
exit

UTS Namespace の識別子は、もちろん元に戻る。

$ file /proc/$$/ns/uts
/proc/949/ns/uts: symbolic link to uts:[4026531838]

ホスト名と NIS ドメイン名も元に戻っている。 つまり、ホスト名と NIS ドメイン名は、ちゃんと UTS Namespace によって隔離されていた。

$ hostname
focal
$ domainname
(none)

unshare(2) を使って UTS Namespace を使ってみる

続いては unshare(2) を使って UTS Namespace を使ってみよう。 今度は C のコードを使うことになるけど、長くなるのでホスト名とドメイン名で分けることにする。 といっても、両者は呼び出すシステムコールが {set,get}hostname(2) になるか {set,get}domainname(2) になるか位しか違いはない。

ホスト名を変更する

まずはホスト名から。 コメントに処理の説明は書いてあるけど、unshare(1) で UTS Namespace を新しく作ってから sethostname(2) と gethostname(2) を発行している。 そして、最終的には bash を起動している。

#define _GNU_SOURCE
  
#include <sched.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    // UTS Namespace を作成する
    if (unshare(CLONE_NEWUTS) != 0) {
        fprintf(stderr, "Failed to create a new UTS namespace: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // sethostname(2) でホスト名を変更する
    char const hostname[] = "host.example.com";
    if (sethostname(hostname, strlen(hostname)) != 0) {
        fprintf(stderr, "Failed to set hostname: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // gethostname(2) でホスト名を確認する
    char hostname_buf[254];
    if (gethostname(hostname_buf, sizeof(hostname_buf) / sizeof(hostname_buf[0])) != 0) {
        fprintf(stderr, "Failed to get hostname: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }
    printf("gethostname: %s\n", hostname_buf);

    // execvp(3) でシェルを起動する
    char* const args[] = {"bash", NULL};
    if (execvp(args[0], args) != 0) {
        fprintf(stderr, "Failed to exec \"%s\": %s\n", args[0], strerror(errno));
        exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS;
}

上記をコンパイルする。

$ gcc -Wall example.c 

できたバイナリを実行する。

$ sudo ./a.out 
gethostname: host.example.com

sethostname(2) で変更されたホスト名が gethostname(2) で取得できている。

また、立ち上がったシェルを hostname(1) を発行しても、ちゃんとホスト名が変更されていることがわかる。

# hostname
host.example.com

procfs で確認できる UTS Namespace の識別子もちゃんと変わっている。 どうも識別子は使い回されてるっぽいけど。

# file /proc/$$/ns/uts
/proc/1288/ns/uts: symbolic link to uts:[4026532128]

もちろん、シェルから抜ければホスト名は元に戻る。

# exit
exit
$ hostname
focal

NIS ドメイン名を変更する

同様に NIS ドメイン名でも試してみる。 基本的にさっきのコードで発行するシステムコールが setdomainname(2) と getdomainname(2) に変わっただけ。

#define _GNU_SOURCE
  
#include <sched.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    // UTS Namespace を作成する
    if (unshare(CLONE_NEWUTS) != 0) {
        fprintf(stderr, "Failed to create a new UTS namespace: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // setdomainname(2) でNIS ドメイン名を変更する
    char const hostname[] = "example";
    if (setdomainname(hostname, strlen(hostname)) != 0) {
        fprintf(stderr, "Failed to set NIS domain name: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // getdomainname(2) で NIS ドメイン名を確認する
    char hostname_buf[64];
    if (getdomainname(hostname_buf, sizeof(hostname_buf) / sizeof(hostname_buf[0])) != 0) {
        fprintf(stderr, "Failed to get NIS domain name: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }
    printf("getdomainname: %s\n", hostname_buf);

    // execvp(3) でシェルを起動する
    char* const args[] = {"bash", NULL};
    if (execvp(args[0], args) != 0) {
        fprintf(stderr, "Failed to exec \"%s\": %s\n", args[0], strerror(errno));
        exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS;
}

上記をコンパイルする。

$ gcc -Wall example.c 

できたバイナリを実行する。

$ sudo ./a.out 
getdomainname: example

ちゃんと NIS ドメイン名が変更されている。

立ち上がったシェルで domainnname(1) を実行しても、変更されていることがわかる。

# domainname
example

procfs で確認できる UTS Namespace の識別子もちゃんと変わっている。

# file /proc/$$/ns/uts
/proc/1338/ns/uts: symbolic link to uts:[4026532128]

もちろん、シェルから抜ければホスト名は元に戻る。

# exit
exit
$ domainname
(none)

ばっちり。

まとめ

今回は UTS Namespace を使ってホスト名と NIS ドメイン名の変更が隔離される様子を観察した。


  1. 使ったことがないけどディレクトリサービスの一種らしい