Skip to content

Commit

Permalink
Merge pull request python-wheel-build#477 from tiran/run-network-isol…
Browse files Browse the repository at this point in the history
…ation

Fix: enable loopback device in network isolation
  • Loading branch information
mergify[bot] authored Oct 15, 2024
2 parents c3946a9 + 0ab9db0 commit 80b8563
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 9 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ jobs:
default: true
override: true

- name: Disable AppArmor restriction for unprivileged user namespaces
if: matrix.os == 'ubuntu-latest'
run: sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0

- name: Check unshare is working
if: matrix.os == 'ubuntu-latest'
run: unshare -rn echo "unshare works"

- name: Install dependencies
run: python -m pip install tox

Expand Down Expand Up @@ -116,6 +124,14 @@ jobs:
default: true
override: true

- name: Disable AppArmor restriction for unprivileged user namespaces
if: matrix.os == 'ubuntu-latest'
run: sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0

- name: Check unshare is working
if: matrix.os == 'ubuntu-latest'
run: unshare -rn echo "unshare works"

- name: Install dependencies
run: python -m pip install tox

Expand Down
15 changes: 8 additions & 7 deletions src/fromager/external_commands.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import logging
import os
import pathlib
import shlex
import shutil
import subprocess
import sys
import typing
from io import TextIOWrapper

logger = logging.getLogger(__name__)

HERE = pathlib.Path(__file__).absolute().parent

NETWORK_ISOLATION: list[str] | None
if sys.platform == "linux":
NETWORK_ISOLATION = ["unshare", "--net", "--map-current-user"]
# runner script with `unshare -rn` + `ip link set lo up`
NETWORK_ISOLATION = [str(HERE / "run_network_isolation.sh")]
else:
NETWORK_ISOLATION = None

Expand All @@ -22,11 +25,8 @@ def network_isolation_cmd() -> typing.Sequence[str]:
Raises ValueError when network isolation is not supported
Returns: command list to run a process with network isolation
"""
if sys.platform == "linux":
unshare = shutil.which("unshare")
if unshare is not None:
return [unshare, "--net", "--map-current-user"]
raise ValueError("Linux system without 'unshare' command")
if NETWORK_ISOLATION:
return NETWORK_ISOLATION
raise ValueError(f"unsupported platform {sys.platform}")


Expand Down Expand Up @@ -106,6 +106,7 @@ def run(
# isolation problem and change the exception type to make it easier
# for the caller to recognize that case.
for substr in [
"connection refused",
"network unreachable",
"Network is unreachable",
]:
Expand Down
25 changes: 25 additions & 0 deletions src/fromager/run_network_isolation.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env -S unshare -rn /bin/bash
#
# Run command with network isolation (CLONE_NEWNET) and set up loopback
# interface in the new network namespace. This is somewhat similar to
# Bubblewrap `bwrap --unshare-net --dev-bind / /`, but works in an
# unprivilged container. The user is root inside the new namespace and mapped
# to the euid/egid if the parent namespace.
#
# Ubuntu 24.04: needs `sysctl kernel.apparmor_restrict_unprivileged_userns=0`
# to address `unshare: write failed /proc/self/uid_map: Operation not permitted`.
#

set -e
set -o pipefail

if [ "$#" -eq 0 ]; then
echo "$0 command" >&2
exit 2
fi

# bring loopback up
ip link set lo up

# replace with command
exec "$@"
7 changes: 5 additions & 2 deletions tests/test_external_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,19 @@ def test_external_commands_network_isolation(
)


NETWORK_ISOLATION_ERROR: Exception | None = None
try:
external_commands.detect_network_isolation()
except Exception:
except Exception as err:
NETWORK_ISOLATION_ERROR = err
SUPPORTS_NETWORK_ISOLATION: bool = False
else:
SUPPORTS_NETWORK_ISOLATION = True


@pytest.mark.skipif(
not SUPPORTS_NETWORK_ISOLATION, reason="network isolation is not supported"
not SUPPORTS_NETWORK_ISOLATION,
reason=f"network isolation is not supported: {NETWORK_ISOLATION_ERROR}",
)
def test_external_commands_network_isolation_real():
with pytest.raises(external_commands.NetworkIsolationError) as e:
Expand Down

0 comments on commit 80b8563

Please sign in to comment.