[已解决] 如何使用 xdg-dbus-proxy 代理沙盒中的 dbus

以 qq 为例,我使用了这个命令

xdg-dbus-proxy unix:path=/run/user/1000/bus /run/user/1000/bus4qq --talk=org.freedesktop.portal.* --talk=org.freedesktop.Notifications --talk=org.kde.StatusNotifierWatcher --talk=org.freedesktop.impl.* -- --talk=org.freedesktop.DBus --filter

然后在 bwrap 沙盒中将 –ro-bind /run/user/1000/bus4qq /run/user/1000/bus

这样一来,qq可以正常启动,也可以在托盘显示图标

但是不能打开任何文件和网址

我使用了 archwiki 中提到的 snapd-xdg-open,在沙盒中运行了一个bash来测试,不能打开网址。会提示 error: GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown: org.freedesktop.DBus.Error.ServiceUnknown

但是我又重新运行了 xdg-dbus-proxy ,这次去掉了 –filter,也就是说我把所有的 dbus 对象都给了沙盒

重启沙盒,运行 bash ,用 snapd-xdg-open 打开网址,这次可以在我沙盒外的浏览器打开了

请问该怎么解决

为什么要手工操作而不是直接使用高级沙盒比如flatpak/portable?

flatpak 太大了。portable 印象不好

看标题标记为已解决,想问一下是如何解决的?

另外 discourse 论坛有把一个评论标记为解决方案的功能的

首先是找到了这样一篇 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()
```

确实,这应该是比较合理的方案,让沙箱内能够访问 OpenURI portal,然后用能调用 portal 的 flatpak-xdg-open 来提供 xdg-open 兼容层。具体的脚本我没仔细看但这个思路是没错的。

感觉就是重新发明portable…

portable 很好用,隔离的更彻底,也更规整
但是在我的用法上不太灵活