From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: passt.top; dmarc=none (p=none dis=none) header.from=gibson.dropbear.id.au Authentication-Results: passt.top; dkim=pass (2048-bit key; secure) header.d=gibson.dropbear.id.au header.i=@gibson.dropbear.id.au header.a=rsa-sha256 header.s=202408 header.b=KO4skE1i; dkim-atps=neutral Received: from mail.ozlabs.org (mail.ozlabs.org [IPv6:2404:9400:2221:ea00::3]) by passt.top (Postfix) with ESMTPS id DE3315A0282 for ; Mon, 26 Aug 2024 04:10:01 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gibson.dropbear.id.au; s=202408; t=1724638184; bh=uZHUrDg4ibxKSua+5g5Il1yFuqdjhvsQ/Ad8lAcdGNU=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=KO4skE1isfxFaVpl5WDnDXlywuNaNzTUMLOBERTYHbNgS2DeQ1ro5VHYEa69rWdPS UYoLtma4gVxi4E/rIotL6eqDNpqlcvlyEsZ5M18bIjHZhpihGnZpUakjJC94vUwePS ysyU8DVc6gXX8b723vSw4aC97kp1AvAvqij3kbukeZyvtsg5EYFEnqrLwGUJy9yn2F TwkCvUj3lxD3VfOez5+iyHU3yuENOayfzUlU/GBuJp3qlopXoimbOPLxdSAUJrKaAT 3wlV68TZUP4DPb90j0jTC0i2VYeOjKCb7jMPRSVkQSqTEZfSQTYwOmgiAFfhGhcdq4 qwHPU8O+nxjgg== Received: by gandalf.ozlabs.org (Postfix, from userid 1007) id 4WsYyw5h4xz4x8n; Mon, 26 Aug 2024 12:09:44 +1000 (AEST) From: David Gibson To: Stefano Brivio , passt-dev@passt.top Subject: [PATCH v3 08/15] tasst/unshare: Add helpers to run commands in Linux unshared namespaces Date: Mon, 26 Aug 2024 12:09:35 +1000 Message-ID: <20240826020942.545155-9-david@gibson.dropbear.id.au> X-Mailer: git-send-email 2.46.0 In-Reply-To: <20240826020942.545155-1-david@gibson.dropbear.id.au> References: <20240826020942.545155-1-david@gibson.dropbear.id.au> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Message-ID-Hash: XSMERXTVW2DCMBEWCXFIORGZL2YZBEI7 X-Message-ID-Hash: XSMERXTVW2DCMBEWCXFIORGZL2YZBEI7 X-MailFrom: dgibson@gandalf.ozlabs.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Cleber Rosa , David Gibson X-Mailman-Version: 3.3.8 Precedence: list List-Id: Development discussion and patches for passt Archived-At: Archived-At: List-Archive: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Use our existing nstool C helper, add python wrappers to easily run commands in various namespaces. Signed-off-by: David Gibson --- test/tasst/__main__.py | 1 + test/tasst/unshare.py | 166 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 test/tasst/unshare.py diff --git a/test/tasst/__main__.py b/test/tasst/__main__.py index 4ea4c593..9cba8985 100644 --- a/test/tasst/__main__.py +++ b/test/tasst/__main__.py @@ -17,6 +17,7 @@ import exeter MODULES = [ 'cmdsite', + 'unshare', ] diff --git a/test/tasst/unshare.py b/test/tasst/unshare.py new file mode 100644 index 00000000..15b760b5 --- /dev/null +++ b/test/tasst/unshare.py @@ -0,0 +1,166 @@ +#! /usr/bin/env python3 + +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright Red Hat +# Author: David Gibson + +""" +Test A Simple Socket Transport + +unshare.py - Create and run commands in Linux namespaces +""" + +import contextlib +import os +import subprocess +import tempfile +from typing import Any, Callable, Iterator + +import exeter + +from . import cmdsite + + +# FIXME: Can this be made more portable? +UNIX_PATH_MAX = 108 + +NSTOOL_BIN = './nstool' + + +class Unshare(cmdsite.CmdSite): + """A bundle of Linux namespaces managed by nstool""" + + sockpath: str + parent: cmdsite.CmdSite + _pid: int + + def __init__(self, name: str, sockpath: str, + parent: cmdsite.CmdSite = cmdsite.BUILD_HOST, + parent_priv: bool = False) -> None: + if len(sockpath) > UNIX_PATH_MAX: + raise ValueError( + f'Unix domain socket path "{sockpath}" is too long' + ) + + super().__init__(name) + self.sockpath = sockpath + self.parent = parent + self.parent_priv = parent_priv + self.parent.fg(NSTOOL_BIN, 'info', '-wp', self.sockpath, timeout=1) + + # PID of the nstool hold process as seen by another site which can + # see the nstool socket (important when using PID namespaces) + def relative_pid(self, relative_to: cmdsite.CmdSite) -> int | None: + cmd = [NSTOOL_BIN, 'info', '-p', self.sockpath] + relpid = int(relative_to.output(*cmd)) + if not relpid: + return None + return relpid + + def popen(self, *cmd: str, privilege: bool = False, + **kwargs: Any) -> subprocess.Popen[bytes]: + hostcmd = [NSTOOL_BIN, 'exec'] + if privilege: + hostcmd.append('--keep-caps') + hostcmd += [self.sockpath, '--'] + hostcmd += list(cmd) + return self.parent.popen(*hostcmd, privilege=self.parent_priv, + **kwargs) + + +@contextlib.contextmanager +def unshare(name: str, *opts: str, + parent: cmdsite.CmdSite = cmdsite.BUILD_HOST, + privilege: bool = False) -> Iterator[Unshare]: + # Create path for temporary nstool Unix socket + with tempfile.TemporaryDirectory() as tmpd: + sockpath = os.path.join(tmpd, name) + cmd = ['unshare'] + list(opts) + cmd += ['--', NSTOOL_BIN, 'hold', sockpath] + with parent.bg(*cmd, privilege=privilege) as holder: + try: + yield Unshare(name, sockpath, parent=parent, + parent_priv=privilege) + finally: + try: + parent.fg(NSTOOL_BIN, 'stop', sockpath) + finally: + try: + holder.run(timeout=0.1) + holder.kill() + finally: + try: + os.remove(sockpath) + except FileNotFoundError: + pass + + +def _userns_setup() -> Iterator[cmdsite.CmdSite]: + with unshare('usernetns', '-Ucn') as site: + yield site + + +def _nested_setup() -> Iterator[cmdsite.CmdSite]: + with unshare('userns', '-Uc') as userns: + with unshare('netns', '-n', parent=userns, privilege=True) as netns: + yield netns + + +def _pidns_setup() -> Iterator[cmdsite.CmdSite]: + with unshare('pidns', '-Upfn') as site: + yield site + + +def connect_site() -> Iterator[Unshare]: + with tempfile.TemporaryDirectory() as tmpd: + sockpath = os.path.join(tmpd, 'nons') + holdcmd = [NSTOOL_BIN, 'hold', sockpath] + with subprocess.Popen(holdcmd) as holder: + try: + yield Unshare("fakens", sockpath) + finally: + holder.kill() + os.remove(sockpath) + + +def selftests() -> None: + @exeter.test + def test_userns() -> None: + cmd = ['capsh', '--has-p=CAP_SETUID'] + status = cmdsite.BUILD_HOST.fg(*cmd, check=False) + assert status.returncode != 0 + with unshare('userns', '-Ucn') as ns: + ns.fg(*cmd, privilege=True) + + @exeter.test + def test_relative_pid() -> None: + with unshare('pidns', '-Upfn') as site: + # The holder is init (pid 1) within its own pidns + exeter.assert_eq(site.relative_pid(site), 1) + + def sockdir_cleanup(setup: Callable[[], Iterator[cmdsite.CmdSite]]) \ + -> None: + cm = contextlib.contextmanager(setup) + + def mess(sockpaths: list[str]) -> None: + with cm() as site: + while isinstance(site, Unshare): + sockpaths.append(site.sockpath) + site = site.parent + + sockpaths: list[str] = [] + mess(sockpaths) + assert sockpaths + for path in sockpaths: + assert not os.path.exists(os.path.dirname(path)) + + # General tests for all the nstool examples + for setup in [_userns_setup, _nested_setup, _pidns_setup]: + # Common cmdsite.CmdSite & NetSite tests + cmdsite.CmdSite.test(setup) + + exeter.register(f'{setup.__qualname__}|sockdir_cleanup', + sockdir_cleanup, setup) + + cmdsite.CmdSite.test(connect_site) -- 2.46.0