Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test Isolation using linux namespaces #277

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0300f03
WIP Test Isolation using linux namespaces
sloretz May 10, 2023
45dc291
exec from command line
sloretz May 17, 2023
407365a
Bring up loopback interface
sloretz May 17, 2023
eadaff4
Move linux namespace creation to separate library
sloretz May 17, 2023
01b3f35
Rename some things
sloretz May 18, 2023
6f1e200
grammar
sloretz May 18, 2023
48f2b79
ros_cc_test isolated by default
sloretz May 19, 2023
5093486
WIP CPython extension for isolation
sloretz May 19, 2023
288000b
First commit where Python tests seem to work
sloretz May 31, 2023
7892d86
Remove unused file
sloretz May 31, 2023
4958e7f
linters
sloretz May 31, 2023
7499342
lint
sloretz May 31, 2023
2a22e42
Add python_dev repo
sloretz May 31, 2023
8f015db
Try to fix one CI job by updating apt package lists
sloretz Jun 20, 2023
f279ccf
Move where we apt-get update to later
sloretz Jun 20, 2023
9e2ecdf
Add debugging step to find missing shared library
sloretz Jun 20, 2023
f7c2c29
fixes rosdep linter
adityapande-1995 Jul 17, 2023
9c5ffce
Fixed CI error
adityapande-1995 Jul 17, 2023
4106361
Cleanup
adityapande-1995 Jul 17, 2023
e03cfb2
Removed apt update step in workflow
adityapande-1995 Jul 20, 2023
430fed7
sandbox_debug flag to CI
adityapande-1995 Jul 20, 2023
bab9094
Update workflow to save space
adityapande-1995 Jul 24, 2023
31c102b
Bazel clean workspace
adityapande-1995 Jul 24, 2023
454ef51
Removed sandbox debug and added bazel clean without expunge
adityapande-1995 Jul 24, 2023
3432e81
Added a test case for network_isolation
adityapande-1995 Aug 2, 2023
fb9703d
Typo
adityapande-1995 Aug 2, 2023
a34408a
Linter
adityapande-1995 Aug 2, 2023
68672d8
Merge remote-tracking branch 'upstream/main' into sloretz__isolate_wi…
adityapande-1995 Sep 27, 2023
21fe89b
Free up disk space
adityapande-1995 Sep 27, 2023
8ce8456
Added a readme
adityapande-1995 Oct 10, 2023
ea13063
readme minor edits
adityapande-1995 Oct 10, 2023
e47cfbc
readme minor edits
adityapande-1995 Oct 10, 2023
8d84a10
Added suggestions
ahcorde Mar 22, 2024
3744256
Merge pull request #1 from ahcorde/ahcorde/sloretz__isolate_with_linu…
adityapande-1995 Mar 22, 2024
bed993e
Merge remote-tracking branch 'upstream/main' into sloretz__isolate_wi…
adityapande-1995 Mar 22, 2024
c2b9823
suggestions to network isolation
ahcorde Mar 25, 2024
d000c8b
make linters happy
ahcorde Mar 25, 2024
c6777f5
Merge pull request #2 from ahcorde/ahcorde/suggestion_network_isolation
adityapande-1995 Mar 25, 2024
ad1131b
Update bazelized_drake_ros.yml
adityapande-1995 Mar 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/bazelized_drake_ros.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
run: rosdep update
- name: Install bazel_ros2_rules dependencies
# TODO(sloretz) make bazel_ros2_rules/setup/install_prereqs.sh
run: sudo apt install python3 python3-toposort
run: sudo apt --no-install-recommends install python3 python3-toposort
- name: Download Drake
run: bazel build @drake//:module_py
working-directory: drake_ros
Expand All @@ -44,14 +44,17 @@ jobs:
# TODO(sloretz) can't use rosdep here because we need the archive downloaded,
# but can't download the archive with bazel until we have the dependencies installed
# so bazel_ros2_rules can inspect it.
run: sudo apt-get install -y libconsole-bridge-dev
run: sudo apt --no-install-recommends install -y libconsole-bridge-dev
# CI for drake_ros.
- name: Build drake_ros
run: bazel build //...
working-directory: drake_ros
- name: Test drake_ros
run: bazel test //...
working-directory: drake_ros
- name: Clean up drake_ros
run: bazel clean
working-directory: drake_ros
# CI for drake_ros_examples.
- name: Install additional ROS dependencies for drake_ros_examples
run: yes | sudo ./setup/install_prereqs.sh
Expand Down
4 changes: 4 additions & 0 deletions bazel_ros2_rules/WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ http_archive(
load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace")

bazel_skylib_workspace()

load("//deps:defs.bzl", "add_bazel_ros2_rules_dependencies")

add_bazel_ros2_rules_dependencies()
33 changes: 33 additions & 0 deletions bazel_ros2_rules/network_isolation/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
cc_library(
name = "network_isolation_cc",
srcs = ["network_isolation.cc"],
hdrs = ["network_isolation.h"],
visibility = ["//visibility:public"],
)

cc_binary(
name = "isolate",
srcs = ["isolate.cc"],
visibility = ["//visibility:public"],
deps = [
":network_isolation_cc",
],
)

# Create a CPython extension
cc_binary(
name = "network_isolation_py.so",
srcs = ["network_isolation_py.cc"],
linkshared = True,
linkstatic = True,
deps = [
":network_isolation_cc",
"@python_dev//:headers",
],
)

py_library(
name = "network_isolation_py",
data = [":network_isolation_py.so"],
visibility = ["//visibility:public"],
)
35 changes: 35 additions & 0 deletions bazel_ros2_rules/network_isolation/isolate.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#include <unistd.h>

#include <iostream>
#include <vector>

#include "network_isolation.h"

void die(const char * message) {
std::cerr << "isolate: " << message << ".\n";
exit(-1);
}

int main(int argc, char ** argv) {
if (argc < 2) {
die("shim must be given a command to execute");
}

if (!network_isolation::create_linux_namespaces()) {
die("Failed to fully create isolated environment");
}

// Copy to a new array that terminates with a null pointer at the end.
std::vector<char *> new_argv;
for (int i = 1; i < argc; ++i) {
new_argv.push_back(argv[i]);
}
new_argv.push_back(nullptr);

// Exec a new process - should never return!
execv(new_argv.at(0), &new_argv.at(0));

perror("execv");
die("Call to execv failed");
return -1;
}
87 changes: 87 additions & 0 deletions bazel_ros2_rules/network_isolation/network_isolation.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#include "network_isolation.h"

#include <errno.h>
#include <sched.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <sys/types.h>
#include <ifaddrs.h>
#include <string.h>

#include <iostream>

namespace network_isolation {

void error(const char * message)
{
std::cerr << "create_linux_namesapces: "
<< message << ":" << strerror(errno) << "\n";
}

bool create_linux_namespaces()
{
int result = unshare(CLONE_NEWUSER | CLONE_NEWNET | CLONE_NEWIPC);

if (result != 0) {
error("failed to call unshare");
return false;
}

// Assert there is exactly one network interface
struct ifaddrs *ifaddr;

if (-1 == getifaddrs(&ifaddr)) {
error("could not get network interfaces");
return false;
}
if (nullptr == ifaddr) {
error("there are no network interfaces");
return false;
}
if (nullptr != ifaddr->ifa_next) {
error("there are multiple network interfaces");
return false;
}

// Need a socket to do ioctl stuff on
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if( fd < 0 ){
error("could not open a socket");
freeifaddrs(ifaddr);
return false;
}

struct ifreq ioctl_request;

// Check what flags are set on the interface
strncpy(ioctl_request.ifr_name, ifaddr->ifa_name, IFNAMSIZ);
int err = ioctl(fd, SIOCGIFFLAGS, &ioctl_request);
if (0 != err) {
freeifaddrs(ifaddr);
error("failed to get interface flags");
return false;
}

// Expecting a loopback interface.
if (!(ioctl_request.ifr_flags & IFF_LOOPBACK)) {
error("the only interface is not a loopback interface");
freeifaddrs(ifaddr);
return false;
}

// Enable multicast
ioctl_request.ifr_flags |= IFF_MULTICAST;
// Bring up interface
ioctl_request.ifr_flags |= IFF_UP;

err = ioctl(fd, SIOCSIFFLAGS, &ioctl_request);
if (0 != err) {
error("failed to set interface flags");
freeifaddrs(ifaddr);
return false;
}

freeifaddrs(ifaddr);
return true;
}
} // namespace network_isolation
22 changes: 22 additions & 0 deletions bazel_ros2_rules/network_isolation/network_isolation.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

namespace network_isolation {
/// Creates linux namespaces suitable for isolating ROS 2 traffic.
///
/// The new namespaces are:
/// * A new user namespace to avoid needing CAP_SYS_ADMIN to create
/// network and IPC namespaces
/// * A new network namespace to prevent cross-talk via the network
/// * A new IPC namespaces to prevent cross-talk via shared memory
///
/// It also configures network namespace to enable ROS 2 traffic.
/// At the end of a successful call the current process will be in
/// the created namespaces.
/// Depending on what part of the process fails, an unsuccessful
/// call may also leave the current process in new namespaces.
/// There is no way to undo this.
///
/// \return true iff the namespaces were created successfully.
bool create_linux_namespaces();

} // namespace network_isolation
35 changes: 35 additions & 0 deletions bazel_ros2_rules/network_isolation/network_isolation_py.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>

#include "network_isolation/network_isolation.h"

extern "C" {
static PyObject *
create_linux_namespaces(PyObject *, PyObject *)
{
if (!network_isolation::create_linux_namespaces()) {
Py_RETURN_FALSE;
}
Py_RETURN_TRUE;
}

static PyMethodDef methods[] = {
{"create_linux_namespaces", &create_linux_namespaces, METH_NOARGS,
"Isolate the current process using linux namespaces."},
{NULL, NULL, 0, NULL} /* sentinel */
};

static PyModuleDef module = {
PyModuleDef_HEAD_INIT,
.m_name = "network_isolation_py",
.m_doc = "Tools to isolate network traffic on linux.",
.m_size = -1,
methods,
};

PyMODINIT_FUNC
PyInit_network_isolation_py(void)
{
return PyModule_Create(&module);
}
}
6 changes: 6 additions & 0 deletions bazel_ros2_rules/ros2/ros_cc.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ def ros_cc_test(
cc_binary_rule = native.cc_binary,
cc_library_rule = native.cc_library,
cc_test_rule = native.cc_test,
network_isolation = False,
**kwargs):
"""
Builds a C/C++ test and wraps it with a shim that will inject the minimal
Expand Down Expand Up @@ -211,6 +212,7 @@ def ros_cc_test(
name = shim_name,
target = ":" + noshim_name,
env_changes = shim_env_changes,
network_isolation = network_isolation,
**shim_kwargs
)

Expand All @@ -220,4 +222,8 @@ def ros_cc_test(
deps = ["@bazel_ros2_rules//ros2:dload_shim_cc"],
tags = ["nolint"] + kwargs.get("tags", []),
)
if network_isolation:
kwargs["deps"].append(
"@bazel_ros2_rules//network_isolation:network_isolation_cc",
)
cc_test_rule(name = name, **kwargs)
6 changes: 6 additions & 0 deletions bazel_ros2_rules/ros2/ros_py.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ def ros_py_test(
rmw_implementation = None,
py_binary_rule = native.py_binary,
py_test_rule = native.py_test,
network_isolation = False,
**kwargs):
"""
Builds a Python test and wraps it with a shim that will inject the minimal
Expand Down Expand Up @@ -176,6 +177,7 @@ def ros_py_test(
name = shim_name,
target = ":" + noshim_name,
env_changes = shim_env_changes,
network_isolation = network_isolation,
**shim_kwargs
)

Expand All @@ -186,4 +188,8 @@ def ros_py_test(
deps = ["@bazel_ros2_rules//ros2:dload_shim_py"],
tags = ["nolint"] + kwargs.get("tags", []),
)
if network_isolation:
kwargs["deps"].append(
"@bazel_ros2_rules//network_isolation:network_isolation_py",
)
py_test_rule(name = name, **kwargs)
1 change: 1 addition & 0 deletions bazel_ros2_rules/ros2/tools/dload.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def get_dload_shim_attributes():
cfg = "target",
),
"env_changes": attr.string_list_dict(),
"network_isolation": attr.bool(default = False),
}

def do_dload_shim(ctx, template, to_list):
Expand Down
27 changes: 25 additions & 2 deletions bazel_ros2_rules/ros2/tools/dload_cc.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,19 @@ load(
"get_dload_shim_attributes",
)

_ISOLATE_IMPORT = '#include "network_isolation/network_isolation.h"'
_ISOLATE_CALL_OR_RETURN = """\
if (!network_isolation::create_linux_namespaces()) {{
return -1;
}}
"""

_REEXEC_TEMPLATE = """\
#include "ros2/tools/dload_shim.h"
CC_ISOLATE_IMPORT

int main(int argc, const char * argv[]) {{
CC_ISOLATE_CALL
const char * executable_path = "{executable_path}";
std::vector<const char *> names = {names};
std::vector<std::vector<const char *>> actions = {actions};
Expand All @@ -24,12 +33,23 @@ int main(int argc, const char * argv[]) {{
}}
"""

def _resolve_isolation(template, network_isolation):
isolate_import = ""
isolate_call = ""
if network_isolation:
isolate_import = _ISOLATE_IMPORT
isolate_call = _ISOLATE_CALL_OR_RETURN
template = template.replace("CC_ISOLATE_IMPORT", isolate_import)
template = template.replace("CC_ISOLATE_CALL", isolate_call)
return template

def _to_cc_list(collection):
"""Turn collection into a C++ aggregate initializer expression."""
return "{" + ", ".join(collection) + "}"

def _dload_cc_reexec_impl(ctx):
return do_dload_shim(ctx, _REEXEC_TEMPLATE, _to_cc_list)
template = _resolve_isolation(_REEXEC_TEMPLATE, ctx.attr.network_isolation)
return do_dload_shim(ctx, template, _to_cc_list)

dload_cc_reexec = rule(
doc = """\
Expand All @@ -52,10 +72,12 @@ dload_cc_reexec = rule(

_LDWRAP_TEMPLATE = """\
#include "ros2/tools/dload_shim.h"
CC_ISOLATE_IMPORT

extern "C" int __real_main(int argc, char** argv);

extern "C" int __wrap_main(int argc, char** argv) {{
CC_ISOLATE_CALL
std::vector<const char*> names = {names};
std::vector<std::vector<const char*>> actions = {actions};
bazel_ros2_rules::ApplyEnvironmentActions(argv[0], names, actions);
Expand All @@ -64,7 +86,8 @@ extern "C" int __wrap_main(int argc, char** argv) {{
"""

def _dload_cc_ldwrap_impl(ctx):
return do_dload_shim(ctx, _LDWRAP_TEMPLATE, _to_cc_list)
template = _resolve_isolation(_LDWRAP_TEMPLATE, ctx.attr.network_isolation)
return do_dload_shim(ctx, template, _to_cc_list)

dload_cc_ldwrap = rule(
doc = """\
Expand Down
Loading
Loading