Skip to content

Commit

Permalink
On Linux, try to use Docker with gVisor if installed, else use Podman.
Browse files Browse the repository at this point in the history
gVisor is an open-source OCI-compliant container runtime.
It is a userspace reimplementation of the Linux kernel in a
memory-safe language.

It works by creating a sandboxed environment in which regular Linux
applications run, but their system calls are intercepted by gVisor.
gVisor then redirects these system calls and reinterprets them in
its own kernel. This means the host Linux kernel is isolated
from the sandboxed application, thereby providing protection against
Linux container escape attacks.

It also uses `seccomp-bpf` to provide a secondary layer of defense
against container escapes. Even if its userspace kernel gets
compromised, attackers would have to additionally have a Linux
container escape vector, and that exploit would have to fit within
the restricted `seccomp-bpf` rules that gVisor adds on itself.
  • Loading branch information
EtiennePerot committed Oct 9, 2023
1 parent bdf3f8b commit 0615e9b
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 24 deletions.
28 changes: 23 additions & 5 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ See instructions in [README.md](README.md#macos).
See instructions in [README.md](README.md#windows).

## Linux
On Linux, Dangerzone uses [Podman](https://podman.io/) instead of Docker Desktop for creating
an isolated environment. It will be installed automatically when installing Dangerzone.
On Linux, Dangerzone uses either [Docker](https://www.docker.com/) CLI (if
[gVisor](https://gvisor.dev) is installed as a runtime), or
[Podman](https://podman.io/) for creating an isolated environment. It will be
installed automatically when installing Dangerzone.

Dangerzone is available for:
- Ubuntu 23.04 (lunar)
Expand All @@ -26,9 +28,25 @@ Dangerzone is available for:
<summary><i>:memo: Expand this section if you are on Ubuntu 20.04 (Focal).</i></summary>
</br>

Dangerzone requires [Podman](https://podman.io/), which is not available
through the official Ubuntu Focal repos. To proceed with the Dangerzone
installation, you need to add an extra OpenSUSE repo that provides Podman to
Dangerzone requires either:

* [Docker](https://www.docker.com/) with [gVisor](https://gvisor.dev), or
* [Podman](https://podman.io/)

Neither of these are available through the official Ubuntu Focal repos.

For Docker with gVisor, follow the
[Docker installation instructions](https://docs.docker.com/engine/install/ubuntu/),
followed by the
[gVisor installation instructions](https://gvisor.dev/docs/user_guide/install/).
If all goes well, you should be able to run the following and observe the gVisor
kernel startup messages:

```bash
docker run --runtime=runsc --rm alpine:latest dmesg
```

For Podman, you need to add an extra OpenSUSE repo that provides Podman to
Ubuntu Focal users. You can follow the instructions below, which have been
copied from the [official Podman blog](https://podman.io/new/2021/06/16/new.html):

Expand Down
6 changes: 4 additions & 2 deletions dangerzone/gui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,15 +400,17 @@ def check_state(self) -> None:
if isinstance( # Sanity check
self.dangerzone.isolation_provider, Container
):
container_runtime = self.dangerzone.isolation_provider.get_runtime()
container_runtime_image_args = (
self.dangerzone.isolation_provider.get_runtime_args("image")
)
except NoContainerTechException as e:
log.error(str(e))
state = "not_installed"

else:
# Can we run `docker image ls` without an error
with subprocess.Popen(
[container_runtime, "image", "ls"],
container_runtime_image_args + ["ls"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
startupinfo=get_subprocess_startupinfo(),
Expand Down
61 changes: 46 additions & 15 deletions dangerzone/isolation_provider/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,54 @@ def __init__(self, enable_timeouts: bool) -> None:
self.enable_timeouts = 1 if enable_timeouts else 0
super().__init__()

@staticmethod
def get_gvisor_docker_runtime() -> Optional[str]:
if platform.system() != "Linux":
return None
if shutil.which("docker") is None:
return None
try:
return (
subprocess.check_output(
[
"docker",
"info",
"--format",
'{{range $i, $v := .Runtimes}}{{if eq $i "runsc"}}runsc{{end}}{{end}}',
],
text=True,
startupinfo=get_subprocess_startupinfo(),
)
or None
)
except Exception as e:
return None

@staticmethod
def get_runtime_name() -> str:
if platform.system() == "Linux":
runtime_name = "podman"
if Container.get_gvisor_docker_runtime() is not None:
return "docker"
return "podman"
else:
# Windows, Darwin, and unknown use docker for now, dangerzone-vm eventually
runtime_name = "docker"
return runtime_name
return "docker"

@staticmethod
def get_runtime() -> str:
def get_runtime_args(subcommand: str) -> List[str]:
container_tech = Container.get_runtime_name()
runtime = shutil.which(container_tech)
if runtime is None:
raise NoContainerTechException(container_tech)
return runtime
runtime_args: List[str] = []
subcommand_args: List[str] = []
if (
container_tech == "docker"
and subcommand in ("run", "create")
and Container.get_gvisor_docker_runtime() is not None
):
subcommand_args.extend(("--runtime", "runsc"))
return [runtime] + runtime_args + [subcommand] + subcommand_args

@staticmethod
def install() -> bool:
Expand All @@ -79,7 +111,7 @@ def install() -> bool:
log.info("Installing Dangerzone container image...")

p = subprocess.Popen(
[Container.get_runtime(), "load"],
Container.get_runtime_args("load"),
stdin=subprocess.PIPE,
startupinfo=get_subprocess_startupinfo(),
)
Expand Down Expand Up @@ -115,9 +147,8 @@ def is_container_installed() -> bool:
# See if this image is already installed
installed = False
found_image_id = subprocess.check_output(
[
Container.get_runtime(),
"image",
Container.get_runtime_args("image")
+ [
"list",
"--format",
"{{.ID}}",
Expand All @@ -137,7 +168,7 @@ def is_container_installed() -> bool:

try:
subprocess.check_output(
[Container.get_runtime(), "rmi", "--force", found_image_id],
Container.get_runtime_args("rmi") + ["--force", found_image_id],
startupinfo=get_subprocess_startupinfo(),
)
except:
Expand Down Expand Up @@ -207,7 +238,7 @@ def exec_container(
command: List[str],
extra_args: List[str] = [],
) -> int:
container_runtime = self.get_runtime()
container_runtime_args = self.get_runtime_args("run")

if self.get_runtime_name() == "podman":
security_args = ["--security-opt", "no-new-privileges"]
Expand All @@ -221,8 +252,8 @@ def exec_container(

prevent_leakage_args = ["--rm"]

args = (
["run", "--network", "none"]
run_args = (
["--network", "none"]
+ user_args
+ security_args
+ prevent_leakage_args
Expand All @@ -231,7 +262,7 @@ def exec_container(
+ command
)

args = [container_runtime] + args
args = container_runtime_args + run_args
return self.exec(document, args)

def _convert(
Expand Down Expand Up @@ -374,7 +405,7 @@ def get_max_parallel_conversions(self) -> int:
# For Windows and MacOS containers run in VM
# So we obtain the CPU count for the VM
n_cpu_str = subprocess.check_output(
[self.get_runtime(), "info", "--format", "{{.NCPU}}"],
self.get_runtime_args("info") + ["--format", "{{.NCPU}}"],
text=True,
startupinfo=get_subprocess_startupinfo(),
)
Expand Down
7 changes: 5 additions & 2 deletions tests/test_ocr.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
def test_ocr_ommisions() -> None:
# Create the command that will list all the installed languages in the container
# image.
runtime = Container.get_runtime()
command = [runtime, "run", Container.CONTAINER_NAME, "tesseract", "--list-langs"]
command = Container.get_runtime_args("run") + [
Container.CONTAINER_NAME,
"tesseract",
"--list-langs",
]

# Run the command, strip any extra whitespace, and remove the following first line
# from the result:
Expand Down

0 comments on commit 0615e9b

Please sign in to comment.