以 root 身份连接到用户 dbus

Connecting to user dbus as root

如果我们正常打开一个python解释器,输入以下内容:

import dbus
bus = dbus.SessionBus()
bus.list_names()

我们看到了用户会话 dbus 上的所有服务。现在假设我们想在同一个脚本中做一些 root-only 的事情来确定通过 dbus 传递的信息,所以我们 运行 解释器与 sudo python 和 运行 同样的事情,我们只查看 root 用户会话 dbus 上的项目的简短列表,并尝试使用 get_object 连接到用户 dbus 上的任何内容相应地产生未找到的错误。

到目前为止我已经尝试插入

import os

os.seteuid(int(os.environ['SUDO_UID']))

但这只会让 SessionBus() 给出 org.freedesktop.DBus.Error.NoReply 所以这可能是无稽之谈。有没有办法以超级用户身份使用 python dbus 绑定连接到用户的 dbus 服务?

您可以设置 DBUS_SESSION_BUS_ADDRESS 环境变量来选择您要连接的 dbus 会话。

不正确的权限(即缺少 seteuid)导致立即 NoReply,并且未定义 DBUS_SESSION_BUS_ADDRESS 响应为 Using X11 for dbus-daemon autolaunch was disabled at compile time, set your DBUS_SESSION_BUS_ADDRESS instead

这是我使用的测试代码:

import os
import dbus

# become user
uid = os.environ["SUDO_UID"]
print(f"I'm {os.geteuid()}, becoming {uid}")
os.seteuid(int(uid))

# set the dbus address
os.environ["DBUS_SESSION_BUS_ADDRESS"] = f"unix:path=/run/user/{uid}/bus"

bus = dbus.SessionBus()
print(bus.list_names())

# I'm 0, becoming 1000
# dbus.Array([dbus.String('org.freedesktop.DBus'), dbus.String('org.fr .. <snip>

我对 DBus 知之甚少,但这个问题让我很好奇。

TL;DR:使用 dbus.bus.BusConnection 和目标用户的套接字地址,seteuid 以获得访问权限。

第一个问题:DBus 为会话总线连接到什么套接字?

$ cat list_bus.py 
import dbus
print(dbus.SessionBus().list_names())
$ strace -o list_bus.trace python3 list_bus.py
$ grep ^connect list_bus.trace 
connect(3, {sa_family=AF_UNIX, sun_path="/run/user/1000/bus"}, 20) = 0

也许它依赖于环境变量?知道了!

$ env|grep /run/user/1000/bus
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus

从 root 帐户跟踪行为,它似乎不知道要连接到的地址。谷歌搜索变量名称让我进入 D-Bus Specification,部分“Well-known 消息总线实例”。

第二个问题:我们可以直接连接到套接字而不需要 D-Bus 库猜测正确的地址吗? dbus-python tutorial 状态:

For special purposes, you might use a non-default Bus, or a connection which isn’t a Bus at all, using some new API added in dbus-python 0.81.0.

查看 changelog,这似乎指的是这些:

Bus has a superclass dbus.bus.BusConnection (a connection to a bus daemon, but without the shared-connection semantics or any deprecated API) for the benefit of those wanting to subclass bus daemon connections

让我们试试看:

$ python3
Python 3.9.2 (default, Feb 28 2021, 17:03:44) 
>>> from dbus.bus import BusConnection
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
145

root 权限如何?

# python3
>>> from dbus.bus import BusConnection
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3/dist-packages/dbus/bus.py", line 124, in __new__
    bus = cls._new_for_bus(address_or_type, mainloop=mainloop)
dbus.exceptions.DBusException: org.freedesktop.DBus.Error.NoReply: Did not
  receive a reply. Possible causes include: the remote application did not send
  a reply, the message bus security policy blocked the reply, the reply timeout
  expired, or the network connection was broken.
>>> import os
>>> os.seteuid(1000)
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
143

所以这回答了问题:使用BusConnection而不是SessionBus并明确指定地址,结合seteuid获得访问权限.

奖励:以 root 身份连接而不使用 seteuid

我还是想知道是否可行 以 root 用户身份直接访问总线,而无需诉诸 seteuid。后 一些搜索查询,我发现 a systemd ticket 加上这句话:

dbus-daemon is the component enforcing access ... (but you can drop an xml policy file in, to make it so).

这让我 askubuntu question 讨论如何修改站点本地会话总线策略。

只是为了玩玩,我在一个终端中 运行 这个:

$ cp /usr/share/dbus-1/session.conf session.conf
$ (edit session.conf to modify the include for local customization)
$ diff /usr/share/dbus-1/session.conf session.conf
50c50
<   <include ignore_missing="yes">/etc/dbus-1/session-local.conf</include>
---
>   <include ignore_missing="yes">session-local.conf</include>
$ cat > session-local.conf
<busconfig>
  <policy context="mandatory">
    <allow user="root"/>
  </policy>
</busconfig>
$ dbus-daemon --config-file session.conf --print-address
unix:abstract=/tmp/dbus-j0r67hLIuh,guid=d100052e45d06f248242109262325b98
$ dbus-daemon --config-file session.conf --print-address
unix:abstract=/tmp/dbus-j0r67hLIuh,guid=d100052e45d06f248242109262325b98

在另一个终端中,我无法以 root 用户身份连接到此总线:

# python3
Python 3.9.2 (default, Feb 28 2021, 17:03:44) 
>>> from dbus.bus import BusConnection
>>> address = "unix:abstract=/tmp/dbus-j0r67hLIuh,guid=d100052e45d06f248242109262325b98"
>>> BusConnection(address).list_names()
dbus.Array([dbus.String('org.freedesktop.DBus'), dbus.String(':1.0')], signature=dbus.Signature('s'))

在全局安装 session-local.conf 时,这还应该能够访问系统上的 所有 会话总线:

# cp session-local.conf /etc/dbus-1/session-local.conf
# kill -HUP 1865   # reload config of my users session dbus-daemon
# python3
>>> from dbus.bus import BusConnection
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
143

并且有效 - 现在 root 可以连接到任何会话总线而无需诉诸 seteuid。别忘了

# rm /etc/dbus-1/session-local.conf 

如果您的 root 用户不需要此功能。

Bluehorn 的回答对我很有帮助。我没有足够的声誉来支持他的回答,但我想我会分享我的解决方案。我才学习 python(仅来自 shell 脚本编写),所以如果这确实是错误的并且恰好在我的系统上工作,请告诉我,哈哈。

这些是我编写的用于监视 cpu 温度和控制 linux 中的风扇速度的守护程序的部分,因此它需要 root 权限。不确定如果 运行 作为普通用户在多个用户登录时它会如何工作。我猜它不会...


import os, pwd
from dbus import SessionBus, Interface
from dbus.bus import BusConnection

# Subclassing dbus.Interface because why not

class Dbus(Interface):
    def __init__(self, uid):
        method = 'org.freedesktop.Notifications'
        path = '/' + method.replace('.', '/')
        if os.getuid() == uid:
            obj = SessionBus().get_object( method, path )
        else:
            os.seteuid(uid)
            obj = BusConnection( "unix:path=/run/user/" + str(uid) + "/bus" )
            obj.get_object( method, path )

        super().__init__(obj, method)

# Did this so my notifications would work
# when running as root or non root

class DbusNotificationHandler:

    app_icon = r"/path/to/my/apps/icon.png"
    name     = "MacFanD"

    def __init__(self):
        loggedIn, users = [ os.getlogin() ], []
        for login in pwd.getpwall():
            if login.pw_name in loggedIn:
                users.append( login.pw_uid )

        self.users = []
        for i in users:
            self.users.append( Dbus(i) )

    def notification(self, msg, mtype=None):
        if not isinstance(msg, list) or len(msg) < 2:
            raise TypeError("Expecting a list of 2 for 'msg' parameter")

        hint = {}

        if mtype == 'temp':
            icon = 'dialog-warning'
            hint['urgency'] = 2
            db_id = 498237618
            timeout = 0
        elif mtype == 'warn':
            icon = 'dialog-warning'
            hint['urgency'] = 2
            db_id = 0
            timeout = 5000
        else:
            icon = self.app_icon
            hint['urgency'] = 1
            db_id = 0
            timeout = 5000

        for db in self.users:
            db.Notify( self.name, db_id, icon, msg[0], msg[1:], [], hint, timeout )

handler = DbusNotificationHandler()
notify = handler.notification

msg = [ "Daemon Started", "Daemon is now running - %s"%os.getpid() ]
notify(msg)

temp = "95 Celsius"
msg = [ "High Temp Warning", "CPU temperature has reached %s"%temp ]
notify(msg, 'warn')