はじめに
前回で、Kirkstoneではdo_fetch以外のタスクからは原則ネットワークアクセスができなくなっていることについて調査した。
Release 4.0 (kirkstone)には下記のように記載があり、カーネル機能を使用しているとのことだったが、具体的にどのような機能が使用されているかはドキュメントには見当たらなかった。
Network access from tasks is now disabled by default on kernels which support this feature (on most recent distros such as CentOS 8 and Debian 11 onwards). This means that tasks accessing the network need to be marked as such with the network flag.
興味が湧いたので調べてみた。
bitbakeの実装
該当箇所の特定
poky/bitbake
ディレクトリ配下でnetwork
をgrepしてみる。するとbitbake-worker
というスクリプトに怪しい行を見つけた。
$ grep -n -r 'network' . ... (snip) ... ./bin/bitbake-worker:264: if not the_data.getVarFlag(taskname, 'network', False): ... (snip) ...
この行事体は、tasknameで指定されたタスクにnetwork
というフラグが立っているかをチェックしているだけだが、
この付近でネットワークアクセスをチェックしているということが推測できる。
if not the_data.getVarFlag(taskname, 'network', False): if bb.utils.is_local_uid(uid): logger.debug("Attempting to disable network for %s" % taskname) bb.utils.disable_network(uid, gid) else: logger.debug("Skipping disable network for %s since %s is not a local uid." % (taskname, uid))
bb.utils.disable_network(uid, gid)
が怪しい。
bb.utils.disable_networkの実装
これが本体と仮定して実装を眺めてみる。この関数は./lib/bb/utils.py
で実装されている。
def disable_network(uid=None, gid=None): """ Disable networking in the current process if the kernel supports it, else just return after logging to debug. To do this we need to create a new user namespace, then map back to the original uid/gid. """ libc = ctypes.CDLL('libc.so.6') # From sched.h # New user namespace CLONE_NEWUSER = 0x10000000 # New network namespace CLONE_NEWNET = 0x40000000 if uid is None: uid = os.getuid() if gid is None: gid = os.getgid() ret = libc.unshare(CLONE_NEWNET | CLONE_NEWUSER) if ret != 0: logger.debug("System doesn't suport disabling network without admin privs") return with open("/proc/self/uid_map", "w") as f: f.write("%s %s 1" % (uid, uid)) with open("/proc/self/setgroups", "w") as f: f.write("deny") with open("/proc/self/gid_map", "w") as f: f.write("%s %s 1" % (gid, gid))
処理の流れやログから判断すると、この部分がネットワーク制限および、カーネルがサポートしているかをチェックしている部分になる。
ret = libc.unshare(CLONE_NEWNET | CLONE_NEWUSER) if ret != 0: logger.debug("System doesn't suport disabling network without admin privs") return
libcのunshare関数を呼び出している。
仕組みとしてはタスクを実行するのプロセスのユーザーとネットワークについて名前空間を分離するということをしているようだ。 詳しいわけではないのでざっくりの理解だが、 名前空間を分離することで、分離されたネットワークに対してアクセスすることになるため、 プログラムからは見かけ上、正常にネットワークアクセスできるが、実際には外にパケットが飛んでいかないため、接続エラーになるようなイメージ。
実際に前回試した時のエラーが下記のような感じとなっている。
| Cloning into 'helloworld'... | fatal: unable to access 'https://github.com/mickey-happygolucky/helloworld.git/': Could not resolve host: github.com | Cloning into 'helloworld'... | fatal: unable to access 'https://github.com/mickey-happygolucky/helloworld.git/': Could not resolve host: github.com | Cloning into 'helloworld'... | fatal: unable to access 'https://github.com/mickey-happygolucky/helloworld.git/': Could not resolve host: github.com | -- Had to git clone more than once: | 3 times. | CMake Error at /home/mickey/work/yocto/kirkstone/build/tmp/work/core2-64-poky-linux/example/0.1-r0/build/helloworld/tmp/helloworld-gitclone.cmake:31 (message): | Failed to clone repository: | 'https://github.com/mickey-happygolucky/helloworld.git' (...snip...) Summary: 1 task failed: /home/mickey/work/yocto/kirkstone/build/meta-test/recipes-example/example/example_0.1.bb:do_compile
おそらくその理解で問題ないだろう。
使用されるカーネル機能
libc.unshare(CLONE_NEWNET | CLONE_NEWUSER)
によってどのようなカーネル機能が必要となるかを調べる。
google先生に聞いたらドンピシャですごいテストコードが出てきた。
# Requires at least kernel configuration options: # CONFIG_NAMESPACES=y # CONFIG_NET_NS=y # CONFIG_UTS_NS=y def IfPossibleEnterNewNetworkNamespace(): """Instantiate and transition into a fresh new network namespace if possible.""" sys.stdout.write('Creating clean namespace... ') try: UnShare(CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWNET) except OSError as err: print('failed: %s (likely: no privs or lack of kernel support).' % err) return False
UnShare
もbitbakeのコードのlibc.unshare(CLONE_NEWNET | CLONE_NEWUSER)
と同じようにlibcのunshare関数を呼び出すだけのものと推測できる。
コメントのカーネルコンフィグと、UnShareに渡しているフラグはおそらく1:1になっているので、CLONE_NEWNET
に対応するカーネル機能はCONFIG_NET_NS=y
と読み取ることができる。
Ubuntuカーネルの対応状況
Ubuntu 20.04のカーネルはこの機能に対応しているのか確認してみる。
$ grep 'CONFIG_NET_NS' /boot/config-$(uname -r) CONFIG_NET_NS=y CONFIG_NET_NSH=m
対応していることがわかる。
既に修正済みだが前回の記事で、下記のように記載してしまった。
Ubuntu 20.04の環境では、全く問題なくビルドできてしまった。
これは実際には間違いで、Ubuntu 20.04 + CROPS の環境では問題なくビルドができたということだった。
CROPSはDockerコンテナを使用しているということが大きな違いとなる。
なぜUbuntu 20.04カーネルを使用していてもCROPS経由だとこの機能をすり抜けたのか。これはunshareに原因がある。
Dockerコンテナは既に名前空間が分離された状態になるため、コンテナ上でunshareを実行した場合、ホスト上で実行した場合と結果が変わることがある。(筆者は変わらないケースを確認していないが。)
ホスト上とコンテナ上でのunshareの挙動の違い
まず下記のようなプログラムを作成して挙動を確認する。
#include <iostream> #include <sched.h> int main() { auto ret = ::unshare(CLONE_NEWNET|CLONE_NEWUSER); std::cout << "unshare: ret=" << ret << std::endl; return 0; }
main.cppとして保存して下記のようにコンパイルする。
$ g++ ./main.cpp -O -o call_unshare
ホスト上での実行結果
$ ./call_unshare unshare: ret = 0
問題なく0(成功)が返る。
Dockerコンテナ上での実行
$ docker run --rm -it -v "$(pwd):/work" ubuntu:20.04 bash -c '/work/call_unshare' unshare: ret = -1
想定した通りに-1(失敗)が返る。
bitbakeは下記の処理によって、コンテナ上で実行される場合、システムが機能をサポートしていないとみなしてしまう。
ret = libc.unshare(CLONE_NEWNET | CLONE_NEWUSER) if ret != 0: logger.debug("System doesn't suport disabling network without admin privs") return
ある意味抜け穴。