refactor code and add a readme

This commit is contained in:
hippoz 2022-06-06 20:45:02 +03:00
parent 75aa2b0fbf
commit 902db39029
No known key found for this signature in database
GPG key ID: 7C52899193467641
3 changed files with 292 additions and 80 deletions

View file

@ -1,2 +1,72 @@
# lockings
lockings is a simple sandbox manager written in Python that uses [bubblewrap](https://github.com/containers/bubblewrap) and [xdg-dbus-proxy](https://github.com/flatpak/xdg-dbus-proxy). It allows the creation of sandboxes using a JSON file, with a simple permission system. It generates "launcher" shell scripts which launch the sandboxed program, thus increasing customizability.
## quick start
Clone the repository on your machine and run the `lockings` script. Keep reading below to learn how to use it.
Lockings itself has no dependencies aside from Python, however, the launcher scripts generated by it require `bubblewrap`, and, if using the dbus-proxy permission, `xdg-dbus-proxy` as well.
## configuration
Configuration is done through a JSON file located at `~/.config/lockings/config.json`. If you'd like, you can use a different path by passing it as an argument (`lockings ~/some/path/config.json`).
The default configuration looks like this:
```json
{
"sandboxes_path": "/home/user/.sandboxes",
"scripts_path": "/home/user/.local/bin",
"sandboxes": []
}
```
The `sandboxes_path` is the directory where the sandboxed "jail" home folders will be created. Each program will see its own "artificial" home folder.
The `scripts_path` is the directory where the automatically generated shell scripts will be dropped by lockings. These shell scripts launch the sandboxed program - lockings itself never actually launches anything.
The `sandboxes` property is an array of an object that follows the following structure:
```
{
"name": "name of the sandbox",
"bin": "the binary of the program which you want to sandbox",
"permissions": [ list of permissions, check out the "permissions" section below for more info ]
}
```
Here is an example configuration which has a sandbox for Mozilla Firefox:
```json
{
"sandboxes_path": "/home/user/.sandboxes",
"scripts_path": "/home/user/.local/bin",
"sandboxes": [
{
"name": "firefox",
"bin": "firefox",
"permissions": [
"net",
"x11",
"sys",
"dbus-proxy"
]
}
]
}
```
## permissions
Currently, the following permissions are implemented:
| Permission | Description |
| --- | --- |
| net | Network permission |
| x11 | Exposes the x11 socket. Please note that [x11 is a weak point for sandboxing, and thus wayland is recommended](https://wiki.archlinux.org/title/Bubblewrap#Sandboxing_X11) |
| sys | Exposes some subdirectories in `/sys` and `/dev/dri`. Especially useful for hardware accelerated programs. |
| microphone | Exposes the microphone in `/dev/snd` |
| share:[permission]:[host path]:[sandbox path] | Binds [host path] from the host to [sandbox path] in the sandbox. If [permission] is `r`, it will be bound as read-only. Useful for sharing files with the sandbox |
| dbus-proxy | Enables `xdg-dbus-proxy` and exposes the bus to the host. **This permission is required for all subsequent dbus-proxy- permissions.** |
| dbus-proxy-talk | Set the talk policy for the given name. Read the `xdg-dbus-proxy` manpage for more information. |
| dbus-proxy-own | Set the own policy for the given name. Read the `xdg-dbus-proxy` manpage for more information. |
| dbus-proxy-see | Set the see policy for the given name. Read the `xdg-dbus-proxy` manpage for more information. |
| dbus-proxy-call | Set the call policy for the given name. Read the `xdg-dbus-proxy` manpage for more information. |
| dbus-proxy-broadcast | Set the broadcast policy for the given name. Read the `xdg-dbus-proxy` manpage for more information. |

View file

@ -1,80 +0,0 @@
def generate_bwrap_command(user, app_home_path, app_binary, permissions):
command = [
"/usr/bin/bwrap",
"--ro-bind", "/bin", "/bin",
"--ro-bind", "/usr/bin", "/usr/bin",
"--ro-bind", "/lib", "/lib",
"--ro-bind-try", "/lib32", "/lib32",
"--ro-bind-try", "/lib64", "/lib64",
"--ro-bind", "/usr/lib", "/usr/lib",
"--ro-bind-try", "/usr/local/lib", "/usr/local/lib",
"--ro-bind", "/usr/share", "/usr/share",
"--ro-bind-try", "/usr/local/share", "/usr/local/share",
"--ro-bind", "/usr/include", "/usr/include",
"--ro-bind", "/etc", "/etc",
"--ro-bind-try", f"{app_home_path}/.machine-id", "/etc/machine-id",
"--ro-bind-try", f"{app_home_path}/.machine-id", "/var/lib/dbus/machine-id",
"--ro-bind", "/var/lib", "/var/lib",
"--tmpfs", "/var/lib/dbus",
"--dev", "/dev",
"--bind", app_home_path, f"/home/{user}",
"--proc", "/proc",
"--tmpfs", "/tmp",
"--tmpfs", "/var/tmp",
"--tmpfs", "/var/cache",
"--tmpfs", "/run",
"--symlink", "/run", "/var/run",
"--chdir", f"/home/{user}",
"--setenv", "HOME", f"/home/{user}",
"--setenv", "SHELL", "/sbin/nologin",
"--unsetenv", "SUDO_USER",
"--unsetenv", "SUDO_UID",
"--unsetenv", "SUDO_GID",
"--unsetenv", "SUDO_COMMAND",
"--unsetenv", "OLDPWD",
"--unsetenv", "MAIL",
"--unshare-all",
"--new-session",
"--cap-drop", "all",
]
for p in permissions:
if p == "network":
command.append("--share-net")
elif p == "x11":
command.extend([
"--ro-bind-try", f"/home/{user}/.Xauthority", f"/home/{user}/.Xauthority",
"--ro-bind-try", "/tmp/.X11-unix", "/tmp/.X11-unix"
])
elif p == "sys":
command.extend([
"--ro-bind", "/sys/dev", "/sys/dev",
"--ro-bind", "/sys/devices", "/sys/devices",
"--ro-bind", "/sys/class", "/sys/class",
"--ro-bind", "/sys/bus", "/sys/bus",
"--ro-bind", "/sys/fs/cgroup", "/sys/fs/cgroup",
"--dev-bind", "/dev/dri", "/dev/dri",
])
elif p == "microphone":
command.extend([
"--dev-bind-try", "/dev/snd", "/dev/snd",
])
else:
raise RuntimeError(f"unknown permission '{p}'")
command.append(app_binary)
return command
bash_command = generate_command(
"hippoz",
"/home/hippoz/sandbox",
"/bin/bash",
[
"x11",
"network",
"microphone",
"sys"
]
)
print(" ".join(bash_command))

222
lockings Executable file
View file

@ -0,0 +1,222 @@
#!/usr/bin/env python3
from pathlib import Path
import sys
import os
import json
class Sandbox:
def __init__(self, user, directory, permissions, binary, sandbox_id):
self.user = user
self.directory = directory
self.permissions = permissions
self.binary = binary
self.sandbox_id = sandbox_id
self.using_dbus_proxy = False
def dbus_proxy_path(self):
return '$XDG_RUNTIME_DIR/lockings-dbus_' + self.sandbox_id
def dbus_system_proxy_path(self):
return '$XDG_RUNTIME_DIR/lockings-sysdbus_' + self.sandbox_id
def _generate_bwrap_command(self):
home = f"/home/{self.user}"
command = [
"env -i /usr/bin/bwrap",
"--ro-bind", "/bin", "/bin",
"--ro-bind", "/usr/bin", "/usr/bin",
"--ro-bind", "/usr/local/bin", "/usr/local/bin",
"--ro-bind", "/lib", "/lib",
"--ro-bind-try", "/lib32", "/lib32",
"--ro-bind-try", "/lib64", "/lib64",
"--ro-bind", "/usr/lib", "/usr/lib",
"--ro-bind-try", "/usr/local/lib", "/usr/local/lib",
"--ro-bind", "/usr/share", "/usr/share",
"--ro-bind-try", "/usr/local/share", "/usr/local/share",
"--ro-bind", "/usr/include", "/usr/include",
"--ro-bind", "/etc", "/etc",
"--ro-bind", f"{self.directory}/.machine-id", "/etc/machine-id",
"--ro-bind", f"{self.directory}/.machine-id", "/var/lib/dbus/machine-id",
"--ro-bind", "/var/lib", "/var/lib",
"--dev", "/dev",
"--bind", self.directory, home,
"--proc", "/proc",
"--tmpfs", "/tmp",
"--tmpfs", "/var/tmp",
"--tmpfs", "/var/cache",
"--tmpfs", "/run",
"--symlink", "/run", "/var/run",
"--chdir", home,
"--setenv", "HOME", home,
"--setenv", "PATH", "$PATH",
"--setenv", "SHELL", "/sbin/nologin",
"--setenv", "XDG_RUNTIME_DIR", "/run/user/$UID",
"--unshare-all",
"--new-session",
"--cap-drop", "all",
]
if self.using_dbus_proxy:
command.extend([
"--ro-bind-try", self.dbus_proxy_path(), self.dbus_proxy_path(),
"--ro-bind-try", self.dbus_system_proxy_path(), "/run/dbus/system_bus_socket",
"--setenv", "DBUS_SESSION_BUS_ADDRESS", f"unix:path={self.dbus_proxy_path()}",
])
for p in self.permissions:
if p == "net":
command.append("--share-net")
elif p == "x11":
command.extend([
"--ro-bind-try", f"/home/{self.user}/.Xauthority", f"/home/{self.user}/.Xauthority",
"--ro-bind-try", "/tmp/.X11-unix", "/tmp/.X11-unix",
"--setenv", "DISPLAY", "$DISPLAY"
])
elif p == "sys":
command.extend([
"--ro-bind", "/sys/dev", "/sys/dev",
"--ro-bind", "/sys/devices", "/sys/devices",
"--ro-bind", "/sys/class", "/sys/class",
"--ro-bind", "/sys/bus", "/sys/bus",
"--ro-bind", "/sys/fs/cgroup", "/sys/fs/cgroup",
"--dev-bind", "/dev/dri", "/dev/dri",
])
elif p == "microphone":
command.extend([
"--dev-bind-try", "/dev/snd", "/dev/snd",
])
elif p.startswith("share:"):
# example: `share:rw:/home/user/Documents:/home/user/Documents`
parts = p.split(":", 4)
if len(parts) != 4:
raise RuntimeError("invalid ro-bind expression, expected 4 parts separated by colon")
switch = "--bind"
if (parts[1] == "r"):
switch = "--ro-bind"
command.extend([
switch, parts[2], parts[3]
])
command.append(self.binary)
return command
def _generate_xdg_dbus_proxy_command(self):
if "dbus-proxy" not in self.permissions:
return []
self.using_dbus_proxy = True
command = [
"/usr/bin/xdg-dbus-proxy",
"${DBUS_SESSION_BUS_ADDRESS}",
self.dbus_proxy_path(),
"--filter",
]
for p in self.permissions:
elements = p.split(':', 1)
if len(elements) != 2:
continue
op = elements[0]
arg = elements[1]
if op == "dbus-proxy-talk":
command.append(f"--talk={arg}")
elif op == "dbus-proxy-own":
command.append(f"--own={arg}")
elif op == "dbus-proxy-see":
command.append(f"--see={arg}")
elif op == "dbus-proxy-call":
command.append(f"--call={arg}")
elif op == "dbus-proxy-broadcast":
command.append(f"--broadcast={arg}")
command.extend([
"unix:path=/run/dbus/system_bus_socket",
self.dbus_system_proxy_path(),
"--filter",
])
command.append("&")
return command
def _generate_script_header(self):
return f"#!/bin/sh\n\n# This script was automatically generated by lockings. Parameters:\n# user: {self.user}\n# directory: {self.directory}\n# permissions: {' '.join(self.permissions)}\n# binary: {self.binary}\n# sandbox_id: {self.sandbox_id}\n"
def generate_script(self):
script = [
self._generate_script_header(),
" ".join(self._generate_xdg_dbus_proxy_command()),
" ".join(self._generate_bwrap_command()),
]
return "\n\n".join(script)
def write_to_file(self, file: Path):
file.touch(exist_ok=True, mode=0o770)
file.write_text(self.generate_script())
def load_from_json(path: Path):
with path.open("r") as f:
config = json.load(f)
sandboxes = Path(config["sandboxes_path"])
scripts = Path(config["scripts_path"])
script_filename_prefix = config.get("script_filename_prefix", "")
sandboxes.mkdir(exist_ok=True, parents=True)
scripts.mkdir(exist_ok=True, parents=True)
for s in config["sandboxes"]:
sandbox = Sandbox(
os.environ.get("USER"),
str(sandboxes / s["name"]),
s["permissions"],
s["bin"],
s["name"]
)
(sandboxes / s["name"]).mkdir(exist_ok=True)
(sandboxes / s["name"] / ".machine-id").touch(exist_ok=True)
sandbox.write_to_file(scripts / f"{script_filename_prefix}{s['name']}")
def main(argv, argc):
if argc > 1:
input_file = Path(argv[1])
if input_file.exists():
load_from_json(input_file)
else:
print("lockings: fatal: input file does not exist", file=sys.stderr)
return 1
else:
config_path = os.environ.get("XDG_CONFIG_HOME")
home_directory = os.environ.get("HOME")
if not home_directory:
print("lockings: fatal: HOME environment variable is not set", file=sys.stderr)
return 1
if not config_path:
config_path = f"{home_directory}/.config"
config_path = Path(config_path)
if not config_path.exists():
print("lockings: fatal: config home does not exist, make sure the XDG_CONFIG_HOME or HOME environment variables are properly configured", file=sys.stderr)
return 1
program_config_folder = config_path / "lockings"
program_config_folder.mkdir(exist_ok=True, parents=True)
json_file_path = program_config_folder / "config.json"
if not json_file_path.exists():
print("lockings: note: config file does not exist, creating one right now", file=sys.stderr)
json_file_path.touch()
json_file_path.write_text(json.dumps({
"sandboxes_path": f"{home_directory}/.sandboxes",
"scripts_path": f"{home_directory}/.local/bin",
"sandboxes": []
}, indent=4))
load_from_json(json_file_path)
if __name__ == "__main__":
sys.exit(main(sys.argv, len(sys.argv)))