首先是找到了这样一篇 blog ,https://www.standingpad.org/posts/2023/08/sandboxing-time/
结合 archwiki 里提到的需要伪装成 flatpak 沙箱来欺骗门户
需要在 xdg-dbus-proxy 和所需程序的根目录都有一个 flatpak-info 文件,所以 dbus-proxy 也需要跑在沙盒里。两个bwrap实例。
用 grok 写了一个 python 脚本来实现这个过程,后来在实际使用中发现,仅仅有 flatpak-info 是不够的,还需要有 XDG_RUNTIME_DIR/.flatpak/APP_NAME-INSTANCE_ID,于是继续用 grok 改脚本
我知道 arch 社区并不建议使用 LLM 的输出,所以以下的内容我经过了反复的测试
这个脚本确实可以完成我所需要的功能:首先是 dbus 的过滤,但是不影响我需要使用的。然后是通过门户机制来让软件访问我没有通进沙盒的目录中的文件,这一步需要伪装成 flatpak。最后是使用本机的浏览器来打开沙盒内软件的网址,这个通过 archwiki 中提到的 snapd-xdg-open 实现了,但是后来找到了更好用的 flatpak-xdg-open ,它不仅可以转发对网址的打开,也可以向门户转发对一般文件的打开。
以下是我现在所使用的脚本,出自LLM,仅供参考
#!/usr/bin/env python3
import os
import subprocess
import tempfile
import time
import json
import signal
import sys
import argparse # 新增:用于解析命令行参数
from pathlib import Path
from datetime import datetime # 新增:用于生成唯一ID
# -----------------------------
# 🧩 基本配置
# -----------------------------
APP_NAME = "com.tencent.qq"
XDG_RUNTIME_DIR = Path(os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}"))
QQ_BIN = "/usr/bin/linuxqq"
# 新增:解析命令行参数,支持自定义实例ID
parser = argparse.ArgumentParser(description="Run QQ in sandbox with multi-instance support.")
parser.add_argument('--instance-id', type=str, help='Custom instance ID (optional)')
args = parser.parse_args()
# 新增:生成唯一实例ID(如果未指定,则用时间戳 + PID)
if args.instance_id:
INSTANCE_ID = args.instance_id
else:
INSTANCE_ID = f"instance-{datetime.now().strftime('%Y%m%d%H%M%S')}-{os.getpid()}"
# 修改:APP_FOLDER 和 instance_dir 包含 INSTANCE_ID 以避免冲突
APP_FOLDER = XDG_RUNTIME_DIR / "app" / f"{APP_NAME}-{INSTANCE_ID}"
instance_dir = XDG_RUNTIME_DIR / ".flatpak" / f"{APP_NAME}-{INSTANCE_ID}"
APP_FOLDER.mkdir(parents=True, exist_ok=True)
instance_dir.mkdir(parents=True, exist_ok=True)
flatpak_info = APP_FOLDER / "flatpak-info"
bwrapinfo_file = instance_dir / "bwrapinfo.json"
# -----------------------------
# 🧠 生成 .flatpak-info(更新 instance-id 为唯一值)
# -----------------------------
flatpak_info.write_text(
"[Application]\n"
f"name={APP_NAME}\n"
"\n[Instance]\n"
f"desktop-file={APP_NAME}.desktop\n"
f"instance-id={APP_NAME}-{INSTANCE_ID}\n", # 修改:使用唯一 instance-id
encoding="utf-8"
)
# -----------------------------
# 🧠 环境变量初始化
# -----------------------------
if "DBUS_SESSION_BUS_ADDRESS" not in os.environ:
os.environ["DBUS_SESSION_BUS_ADDRESS"] = f"unix:path={XDG_RUNTIME_DIR}/bus"
# -----------------------------
# 🚦 信号处理(优雅退出,并清理实例文件)
# -----------------------------
def cleanup(signum=None, frame=None):
try:
for proc in [p for p in processes if p.poll() is None]:
proc.terminate()
# 新增:可选清理实例特定目录(防止积累过多临时文件,如果需要保留可注释)
# import shutil
# shutil.rmtree(APP_FOLDER, ignore_errors=True)
# shutil.rmtree(instance_dir, ignore_errors=True)
except Exception:
pass
sys.exit(0)
signal.signal(signal.SIGINT, cleanup)
signal.signal(signal.SIGTERM, cleanup)
processes = []
# -----------------------------
# 🧱 启动 xdg-dbus-proxy(使用唯一的 APP_FOLDER/bus)
# -----------------------------
def set_up_dbus_proxy():
proxy_log = APP_FOLDER / "xdg-dbus-proxy.log"
args = [
"bwrap",
"--new-session",
"--symlink", "/usr/lib64", "/lib64",
"--ro-bind", "/usr/lib", "/usr/lib",
"--ro-bind", "/usr/lib64", "/usr/lib64",
"--ro-bind", "/usr/bin", "/usr/bin",
"--clearenv",
"--bind", str(XDG_RUNTIME_DIR), str(XDG_RUNTIME_DIR),
"--ro-bind", str(flatpak_info), "/.flatpak-info",
"--bind", str(instance_dir), str(instance_dir),
"--die-with-parent",
"--",
"xdg-dbus-proxy",
os.environ["DBUS_SESSION_BUS_ADDRESS"],
f"{APP_FOLDER}/bus", # 修改:使用唯一的 bus 路径
"--log",
"--filter",
"--talk=org.kde.StatusNotifierWatcher",
"--talk=org.freedesktop.portal.*",
]
with open(proxy_log, "wb") as log:
proc = subprocess.Popen(args, stdout=log, stderr=log)
processes.append(proc)
return proc
# -----------------------------
# 🚀 启动 QQ 沙盒环境(使用唯一的路径)
# -----------------------------
def run_app():
args = [
"bwrap",
"--unshare-all",
"--share-net",
"--new-session",
"--symlink", "/usr/lib", "/lib",
"--symlink", "/usr/lib64", "/lib64",
"--symlink", "/usr/bin", "/bin",
"--symlink", "/usr/bin", "/sbin",
"--ro-bind", "/usr/lib", "/usr/lib",
"--ro-bind", "/usr/lib64", "/usr/lib64",
"--ro-bind", "/usr/bin", "/usr/bin",
"--ro-bind", "/etc/resolv.conf", "/etc/resolv.conf",
"--ro-bind", "/etc/ssl", "/etc/ssl",
"--ro-bind", "/etc/ca-certificates", "/etc/ca-certificates",
"--ro-bind", "/etc/xdg", "/etc/xdg",
"--ro-bind", "/usr/share", "/usr/share",
"--ro-bind", "/opt/QQ", "/opt/QQ",
"--ro-bind", "/usr/lib/flatpak-xdg-utils/xdg-open", "/usr/bin/mpv",
"--ro-bind", "/usr/lib/flatpak-xdg-utils/xdg-open", "/usr/bin/xdg-open",
"--ro-bind", "/usr/lib/flatpak-xdg-utils/xdg-open", "/usr/bin/chromium",
"--ro-bind", "/usr/lib/flatpak-xdg-utils/xdg-open", "/usr/bin/chromium",
"--dev", "/dev",
"--dev-bind", "/dev/dri", "/dev/dri",
"--proc", "/proc",
"--ro-bind", "/sys/dev/char", "/sys/dev/char",
"--ro-bind", "/sys/devices", "/sys/devices",
"--bind", str(XDG_RUNTIME_DIR), str(XDG_RUNTIME_DIR),
"--ro-bind", str(XDG_RUNTIME_DIR / "wayland-0"), str(XDG_RUNTIME_DIR / "wayland-0"),
"--ro-bind", str(XDG_RUNTIME_DIR / "pipewire-0"), str(XDG_RUNTIME_DIR / "pipewire-0"),
"--ro-bind", str(XDG_RUNTIME_DIR / "pulse"), str(XDG_RUNTIME_DIR / "pulse"),
"--bind", str(XDG_RUNTIME_DIR / "doc"), str(XDG_RUNTIME_DIR / "doc"),
"--bind", f"{APP_FOLDER}/bus", f"{XDG_RUNTIME_DIR}/bus", # 修改:使用唯一的 bus 路径(绑定到相同的 /run/user/.../bus,但每个实例有自己的 bus 文件)
"--tmpfs", "/tmp",
"--bind", f"{Path.home()}/.config/QQ", f"{Path.home()}/.config/QQ", # 注意:如果需要每个实例独立配置,可修改为唯一路径,如 .config/QQ-{INSTANCE_ID}
"--bind", f"{Path.home()}/Downloads", f"{Path.home()}/Downloads",
"--ro-bind", str(flatpak_info), "/.flatpak-info",
"--bind", str(instance_dir), str(instance_dir),
"--setenv", "GTK_USE_PORTAL", "1",
"--setenv", "XDG_CURRENT_DESKTOP", "KDE:XFCE",
"--setenv", "XDG_SESSION_TYPE", "x11",
"--setenv", "GTK_IM_MODULE", "fcitx",
"--",
QQ_BIN,
]
proc = subprocess.Popen(args)
processes.append(proc)
# 写入 bwrapinfo.json(动态 child-pid)
bwrapinfo_file.write_text(json.dumps({
"app_id": APP_NAME,
"sandbox": True,
"child-pid": proc.pid
}), encoding="utf-8")
return proc
# -----------------------------
# 🪄 主流程
# -----------------------------
if __name__ == "__main__":
print(f"[+] Setting up sandbox for {APP_NAME} with instance ID: {INSTANCE_ID} ...")
proxy_proc = set_up_dbus_proxy()
time.sleep(0.2) # 给 dbus proxy 启动时间
app_proc = run_app()
print(f"[+] QQ started in sandbox (PID {app_proc.pid}, Instance ID: {INSTANCE_ID})")
app_proc.wait()
cleanup()
```