refactor code and add a readme
This commit is contained in:
parent
75aa2b0fbf
commit
902db39029
3 changed files with 292 additions and 80 deletions
70
README.md
70
README.md
|
@ -1,2 +1,72 @@
|
||||||
# lockings
|
# 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. |
|
||||||
|
|
|
@ -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
222
lockings
Executable 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)))
|
Loading…
Reference in a new issue