Skip to content

Commit 8f03842

Browse files
carzhgvisor-bot
authored andcommitted
Add support for custom network modes in Python SandboxExec.
PiperOrigin-RevId: 960325662
1 parent 31f6978 commit 8f03842

2 files changed

Lines changed: 99 additions & 3 deletions

File tree

sandboxexec/sandbox/python/gvisor/sandbox.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def __init__(
3939
runtime_dir: Optional[str] = None,
4040
sandbox_id: Optional[str] = None,
4141
enable_networking: bool = True,
42+
network: Optional[str] = None,
4243
):
4344
"""Initializes and starts a new sandbox.
4445
@@ -48,11 +49,24 @@ def __init__(
4849
sandbox_id: Specific sandbox ID. If not set, a unique ID is generated
4950
automatically.
5051
enable_networking: Whether networking is enabled inside the sandbox.
52+
network: The networking mode for runsc (e.g. "none", "sandbox", "host").
53+
Specifying this overrides enable_networking.
5154
5255
Raises:
5356
Error: If sandbox creation fails.
57+
ValueError: If an invalid network mode is provided.
5458
"""
59+
if network is not None and network not in ("none", "sandbox", "host"):
60+
raise ValueError(
61+
f"Invalid network mode '{network}'. Valid options are 'none',"
62+
" 'sandbox', 'host', or None."
63+
)
64+
5565
self._enable_networking = enable_networking
66+
self._network = network
67+
self._is_network_enabled = (
68+
network != "none" if network is not None else enable_networking
69+
)
5670
self._runtime_dir = ""
5771
self._owns_runtime_dir = False
5872
self._id = ""
@@ -74,7 +88,11 @@ def __init__(
7488
self._id = sandbox_id or self._generate_id()
7589

7690
try:
77-
if os.geteuid() != 0 and self._enable_networking:
91+
if (
92+
os.geteuid() != 0
93+
and self._is_network_enabled
94+
and self._network != "host"
95+
):
7896
raise Error("enabling networking requires running as root")
7997

8098
self._state_dir = os.path.join(self._runtime_dir, "state")
@@ -101,8 +119,12 @@ def __init__(
101119
args = ["--root", self._state_dir]
102120
if os.geteuid() != 0:
103121
args.append("--ignore-cgroups")
104-
if not self._enable_networking:
122+
123+
if self._network is not None:
124+
args.append(f"--network={self._network}")
125+
elif not self._enable_networking:
105126
args.append("--network=none")
127+
106128
args.extend(["run", "--bundle", self._bundle_dir, "--detach", self._id])
107129

108130
# We must use a file for stderr because runsc run with --detach spawns a
@@ -188,7 +210,7 @@ def _create_bundle(self) -> str:
188210
]
189211
if os.geteuid() != 0:
190212
namespaces.append({"type": "user"})
191-
if self._enable_networking:
213+
if self._is_network_enabled and self._network != "host":
192214
namespaces.append({"type": "network"})
193215

194216
mounts = [

sandboxexec/sandbox/python/tests/sandbox_test.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,80 @@ def side_effect(*args, **_kwargs):
252252
sandbox.Sandbox(enable_networking=False)
253253
self.assertIn("failed to create sandbox via subprocess", str(ctx.exception))
254254

255+
def test_invalid_networking_mode(self): # pylint: disable=unused-argument
256+
with self.assertRaises(ValueError) as ctx:
257+
sandbox.Sandbox(network="invalid-net")
258+
self.assertIn(
259+
"Invalid network mode 'invalid-net'. Valid options are 'none',"
260+
" 'sandbox', 'host', or None.",
261+
str(ctx.exception),
262+
)
263+
264+
@mock.patch("os.geteuid", return_value=0)
265+
@mock.patch("subprocess.run")
266+
def test_network_modes(self, mock_run, mock_geteuid): # pylint: disable=unused-argument
267+
mock_run.return_value = mock.Mock(returncode=0)
268+
for net in ["none", "sandbox", "host"]:
269+
mock_run.reset_mock()
270+
sb = sandbox.Sandbox(network=net)
271+
args = mock_run.call_args_list[0][0][0]
272+
if net != "none":
273+
self.assertIn(f"--network={net}", args)
274+
else:
275+
self.assertIn("--network=none", args)
276+
sb.close()
277+
278+
@mock.patch("os.geteuid", return_value=0)
279+
@mock.patch("subprocess.run")
280+
def test_network_none_overrides_enable_networking_true(
281+
self, mock_run, mock_geteuid
282+
): # pylint: disable=unused-argument
283+
mock_run.return_value = mock.Mock(returncode=0)
284+
sb = sandbox.Sandbox(enable_networking=True, network="none")
285+
args = mock_run.call_args_list[0][0][0]
286+
self.assertIn("--network=none", args)
287+
config_path = os.path.join(sb.bundle_dir, "config.json")
288+
with open(config_path, "r") as f:
289+
spec = json.load(f)
290+
namespaces = spec.get("linux", {}).get("namespaces", [])
291+
namespace_types = {ns.get("type") for ns in namespaces}
292+
self.assertNotIn("network", namespace_types)
293+
sb.close()
294+
295+
@mock.patch("os.geteuid", return_value=1000)
296+
def test_network_sandbox_nonroot_raises_error(
297+
self, mock_geteuid
298+
): # pylint: disable=unused-argument
299+
with self.assertRaises(sandbox.Error) as ctx:
300+
sandbox.Sandbox(network="sandbox")
301+
self.assertIn(
302+
"enabling networking requires running as root", str(ctx.exception)
303+
)
304+
305+
def test_network_mode_none_real_sandbox(self):
306+
"""Verifies starting a real sandbox with network='none' without mocks."""
307+
with sandbox.Sandbox(network="none") as sb:
308+
stdout, _ = sb.exec("echo", "hello network none")
309+
self.assertEqual(stdout.strip(), "hello network none")
310+
config_path = os.path.join(sb.bundle_dir, "config.json")
311+
with open(config_path, "r") as f:
312+
spec = json.load(f)
313+
namespaces = spec.get("linux", {}).get("namespaces", [])
314+
namespace_types = {ns.get("type") for ns in namespaces}
315+
self.assertNotIn("network", namespace_types)
316+
317+
def test_network_mode_host_real_sandbox(self):
318+
"""Verifies starting a real sandbox with network='host' without mocks."""
319+
with sandbox.Sandbox(network="host") as sb:
320+
stdout, _ = sb.exec("echo", "hello network host")
321+
self.assertEqual(stdout.strip(), "hello network host")
322+
config_path = os.path.join(sb.bundle_dir, "config.json")
323+
with open(config_path, "r") as f:
324+
spec = json.load(f)
325+
namespaces = spec.get("linux", {}).get("namespaces", [])
326+
namespace_types = {ns.get("type") for ns in namespaces}
327+
self.assertNotIn("network", namespace_types)
328+
255329
def test_find_runsc_not_found(self):
256330
old_runsc_path = os.environ.get("RUNSC_PATH")
257331
if "RUNSC_PATH" in os.environ:

0 commit comments

Comments
 (0)