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 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