2022-01-21

一、 D-Bus简介

D-Bus是一种高效、易用的进程间通信方式.
D-Bus分为两种:system bus(系统总线),用于系统(Linux)与用户程序之间进行通信和消息的传递;session bus(会话总线),用于用户程序之间进行通信.

D-Feet是一个查看和调用D-Bus接口的图形化工具.UOS下安装命令sudo apt install d-feet.
D-Bus官网
D-Feet的使用
D-Feet Wiki

二. 背景介绍

1. libsysted sd-bus简介

一个轻量级的D-Bus库. 我们用它来演示如何开发一个简答的系统D-Bus程序.
编译依赖:pkg-config libsystemd-dev  libsystemd0 
编译时头文件和动态库: 运行 `pkg-config --cflags --libs libsystemd` 获得.

2. D-bus接口org.freedesktop.PolicyKit1.Authority

org.freedesktop.PolicyKit1 是polkit的系统dbus接口.
通过此dbus接口,可以验证权限,并通过与用户交互来获取权限.
下面是一个通过D-Feet使用org.freedesktop.PolicyKit1.Authority CheckAuthorization dbus方法来验证进程deepin-devicemanager是否具备org.gnome.gparted行为的权限的例子.
(1) 通过pkaction查看权限

pkaction --action-id org.gnome.gparted --verbose    

可以知道 org.gnome.gparted 需要 auth_admin,即管理员权限.

(2) 打开一个应用程序.这里打开了deepin-devicemanager,其pid为8798.

(3) D-Feet调用org.freedesktop.PolicyKit1.Authority CheckAuthorization接口

("unix-process", {"pid":GLib.Variant('u', 4574), "start-time":GLib.Variant('t', 0)}),
"org.gnome.gparted",
{},
1, 
"timeout=600"

[图片上传失败...(image-d3d50b-1642718751398)]
polkit会弹出认证窗口来与用户交互,获取授权.
若用户输入正确密码,返回 (True, False, {}),表示进程deepin-devicemanager拥有了执行org.gnome.gparted action的权限.
若用户关闭验证框,返回 (False, False, {'polkit.dismissed': 'true'}),标示获取权限失败.

详见org.freedesktop.PolicyKit1.Authority Interface

3. UOS系统白名单机制

UOS系统通过白名单机制放行一些系统级的可执行程序,白名单中的程序拥有较高的权限,被允许直接放行.

二. 一个系统D-Bus示例程序,调用此D-Bus接口时会对调用者进行鉴权

1. 自定义一个polkit action

action对应了用户执行操作的polkit权限要求,添加的基本步骤如下
(1)根据要开发的系统dbus功能,新建policy文件.
policy文件是xml格式,例如actions/net.poettering.Calculator.policy.
填入图标、action id,描述,polkit窗口消息等.
最主要是定义default值

allow_any:      任意的客户端都可以
allow_inactive: 远程交互客户端 (SSH, VNC, etc.)
allow_active:   直接登录的TTY或X display客户端

每个allow_***可以设置为以下的值:

表头 表头
no 不通过
yes 通过
auth_self 以当前session用户身份来验证
auth_admin 以管理员身份来验证
auth_self_keep 以当前session用户身份来验证,但仅保持一定时间验证有效 (例如5分钟无需在此验证)
auth_admin_keep 以管理员用户身份来验证,但仅保持一定时间验证有效 (例如5分钟无需在此验证)

(2) 复制policy文件到 /usr/share/polkit-1/actions.
(3) policy文件添加/usr/share/polkit-1/actions后使用pkaction检查是否生效.

pkaction --action-id net.poettering.Calculator --verbose

同样的,你可以通过D-Feet调用org.freedesktop.PolicyKit1 D-Bus接口看下添加的action效果.

2. D-Bus设置

system.d/net.poettering.Calculator.conf复制到 /etc/dbus-1/system.d/文件夹下,允许任何程序调用net.poettering.Calculator D-Bus接口

3. 系统D-Bus 程序

<details>
<summary>展开查看 代码test.c</summary>
<pre><code>
#include <stdio.h>

include <stdlib.h>

include <errno.h>

include <systemd/sd-bus.h>

include <stdio.h>

include <unistd.h>

include <stdlib.h>

include <errno.h>

include <systemd/sd-bus.h>

define MAX_BUF_SIZE 1024

define SECURITY_WHILTELIST_FILE "/var/lib/deepin/deepin_security_verify.whitelist"

/* 检查程序是否在白名单中
读取白名单文件中的列表并比对
*/
static int has_in_whitelist(const char *path)
{
int ret = 0;
char buf[MAX_BUF_SIZE] = {0};

FILE *fr = fopen(SECURITY_WHILTELIST_FILE, "r");
if (!fr) {
    return ret;
}

while (!feof(fr)) {
    memset(buf, 0, MAX_BUF_SIZE);
    char* c = fgets(buf, MAX_BUF_SIZE, fr);
    if (c == NULL) {
        break;
    }

    // trim '\n'
    size_t len = strlen(buf);
    if (buf[len-1] == '\n') {
        buf[len-1] = '\0';
    }
    if (strcmp(buf, path) == 0) {
        ret = 1;
        break;
    }
}

fclose(fr);
return ret;

}

/* 根据pid 获取 发送进程的路径,检查路径是否在白名单中
进程路径是/proc/<进程id>/exe指向的路径
/
static int get_sender_path(pid_t pid, char
path)
{
int r;
sd_bus_creds *creds = NULL;
ssize_t len, buflen;

char exe_path[256] = {0};

snprintf(exe_path, 256, "/proc/%d/exe", pid);

len = readlink(exe_path, path, 256);

if( len < 1 )
{
    return -1;
}

return len;

}

static int check_path(char* path)
{
return has_in_whitelist(path);
}

/* 从sender的bus message获取pid
调用者信息中包含pid等信息,找到相关接口获取
*/
static pid_t get_sender_pid(sd_bus_message *m)
{
int r;
pid_t pid = -1;
sd_bus_creds *creds = NULL;

r = sd_bus_query_sender_creds(m, SD_BUS_CREDS_PID, &creds);
if (r < 0)
{
    fprintf(stderr, "sd_bus_query_sender_creds failed: %s, creds = %p\n", strerror(-r), creds);
    return 0;
}

r = sd_bus_creds_get_pid(creds, &pid);
if (r < 0)
{
    fprintf(stderr, "sd_bus_creds_get_pid failed: %s, creds = %p\n ", strerror(-r), creds);
    return 0;
}

fprintf(stderr, "sd_bus_creds_get_pid: %d\n", pid);

return pid;

}

/* 调用org.freedesktop.PolicyKit1.Authority dbus 进行polkit鉴权
如果没有权限 通过polkit agent与用户交互,获取授权
*/
static int check_pid(pid_t pid)
{
sd_bus_error error = SD_BUS_ERROR_NULL;
sd_bus_message *m = NULL;
sd_bus *bus = NULL;
const char *path;
int r;

unsigned is_authorized = 0;
unsigned is_challenge = 0;
int detail_size;
int a;
const char* s1;
const char* s2;

/*获取一个系统bus connection  */
r = sd_bus_open_system(&bus);
if (r < 0) 
{
    fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r));
    goto finish;
}

/* 调用系统dbus接口方法 */
r = sd_bus_call_method(
    bus,
    "org.freedesktop.PolicyKit1",             /* service to contact */
    "/org/freedesktop/PolicyKit1/Authority",  /* object path */
    "org.freedesktop.PolicyKit1.Authority",   /* interface name */
    "CheckAuthorization",                     /* method name */
    &error,              /* object to return error in */
    &m,                  /* return message on success */
    "(sa{sv})sa{ss}us",  
        /* 后面参数的类型 
        CheckAuthorization (Struct of (String, Dict of {String, Variant}) subject, String action_id, Dict of {String, String} details, UInt32 flags, String cancellation_id)    
        
        struct 就用() 扩起来
        Dict 用{} 括起来, 后面需要多加一个size值
        Variant ‘v’,需要加一个type
        可以对照着d-feet的method 参数填写
        详见https://manpages.debian.org/testing/libsystemd-dev/sd_bus_message_read.3.en.html
        */       
    "unix-process",     
    2,      // dict的个数,即{sv}的个数
    "pid",
    "u",    // Variant的type int64
    pid,    // Variant的value
    "start-time",
    "t",    // glib vriant的type uint64
    0,      
    "net.poettering.Calculator", // 权限action名称
    0,  // dict的个数,即{ss}的个数
    0x00000001,  /* 
        一般设置为1,表示如果pid没有权限,则尝试通过polkit提权 
        The AuthorityFeatures Flags
        {
            None                 = 0x00000000,
            AllowUserInteraction = 0x00000001
        }
        Flags used in the CheckAuthorization() method.

        None : No flags set.

        AllowUserInteraction : If the Subject can obtain the authorization through authentication, and an authentication agent is available, then attempt to do so. Note, this means that the CheckAuthorization() method will block while the user is being asked to authenticate.
    */
    "" //cancellation_id,一般不用,设置为空
    );

if (r < 0) 
{
    fprintf(stderr, "Failed to Authority: %s\n", error.message);
    goto finish;
}

/*  读取dubs信息  
    (Struct of (Boolean, Boolean, Dict of {String, String}) result)
    因为dict是变长的,需要循环读取
    首先进入结构体(直接读取会返回失败)

    systemd-241.8/src/systemd/sd-bus-protocol.h 

    SD_BUS_TYPE_BYTE             = 'y',
    SD_BUS_TYPE_BOOLEAN          = 'b',
    SD_BUS_TYPE_INT16            = 'n',
    SD_BUS_TYPE_UINT16           = 'q',
    SD_BUS_TYPE_INT32            = 'i',
    SD_BUS_TYPE_UINT32           = 'u',
    SD_BUS_TYPE_INT64            = 'x',
    SD_BUS_TYPE_UINT64           = 't',
    SD_BUS_TYPE_DOUBLE           = 'd',
    SD_BUS_TYPE_STRING           = 's',
    SD_BUS_TYPE_OBJECT_PATH      = 'o',
    SD_BUS_TYPE_SIGNATURE        = 'g',
    SD_BUS_TYPE_UNIX_FD          = 'h',
    SD_BUS_TYPE_ARRAY            = 'a',
    SD_BUS_TYPE_VARIANT          = 'v',
    SD_BUS_TYPE_STRUCT           = 'r',
    SD_BUS_TYPE_STRUCT_BEGIN     = '(',
    SD_BUS_TYPE_STRUCT_END       = ')',
    SD_BUS_TYPE_DICT_ENTRY       = 'e',
    SD_BUS_TYPE_DICT_ENTRY_BEGIN = '{',
    SD_BUS_TYPE_DICT_ENTRY_END   = '}'
*/

/*读取调用结果
*/
r = sd_bus_message_enter_container(m, 'r', "bba{ss}");
if (r < 0)
{
    printf("sd_bus_message_enter_container failed.\n");
    goto finish;
}

//读取前两个bool
r = sd_bus_message_read(m, "bb", &is_authorized, &is_challenge);
if( r < 0)
{
    printf("sd_bus_message_read failed.\n");
    goto finish;
}

printf("sd_bus_message_read is_authorized = %s, is_challenge = %s",is_authorized?"true":"false"\
        ,is_challenge?"true":"false");
//
r = sd_bus_message_enter_container(m, 'a', "{ss}");

// 变长数组循环读取
while(1)
{
    r = sd_bus_message_read(m, "{ss}", &s1, &s2);
    if (r <= 0) {
        break;
    }

    printf(" %s=%s", s1, s2);
}

sd_bus_message_exit_container(m);
printf(".\n");

r = sd_bus_message_exit_container(m);

finish:
/*关闭系统dbus连接
*/
sd_bus_error_free(&error);
sd_bus_message_unref(m);
sd_bus_unref(bus);

if( r > 0 && !is_authorized )
{
    return -1;
}

return r;

}

/*
检测sender的权限
如果sender path在白名单中 返回成功
如果不在白名单 则polkit鉴权
*/
static int check_auth(sd_bus_message *m)
{
int r;
char path[256] = {0};

pid_t pid = get_sender_pid(m);

if(pid < 0)
{
    fprintf(stderr, "get_sender_pid failed. pid = %d \n", pid);
    return pid;
}

r = get_sender_path(pid, path);
if(r > 0)
{
    r = check_path(path);
    fprintf(stderr, "check_path result. r = %d \n", r);
}

if(r > 0)
{
    return r;
}

r = check_pid(pid);

return r;

}

/* dbus method调用在这里被处理
*/
static int method_multiply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
{
int64_t x, y;
int r;

/*
在调用方法前进行白名单验证和polkit鉴权
失败直接返回错误
*/
r = check_auth(m);
if( r < 0)
{
    fprintf(stderr, "check_auth failed: %s\n", strerror(-r));
    return sd_bus_reply_method_return(m, "xs", 0, &"auth failed");
}

/* 读取传入参数 */
r = sd_bus_message_read(m, "xx", &x, &y);
if (r < 0) 
{
    fprintf(stderr, "Failed to parse parameters: %s\n", strerror(-r));
    return r;
}

/* 返回dbus结果 */
return sd_bus_reply_method_return(m, "xs", x * y, &"success");

}

/* vtable定义dubs object的方法、信号、属性等
这里定义了一个乘法的method
*/
static const sd_bus_vtable calculator_vtable[] =
{
SD_BUS_VTABLE_START(0),
SD_BUS_METHOD("Multiply", "xx", "xs", method_multiply, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_VTABLE_END
};

int main(int argc, char *argv[])
{
sd_bus_slot *slot = NULL;
sd_bus *bus = NULL;
int r;

/* 1. 获取一个系统bus connection 
*/
r = sd_bus_open_system(&bus);
if (r < 0) 
{
    fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r));
    goto finish;
}

/* 2. 安装bus object,设置 bus路径和接口,绑定bus 方法
 */
r = sd_bus_add_object_vtable(bus,
                            &slot,
                            "/net/poettering/Calculator",  /* object path */
                            "net.poettering.Calculator",   /* interface name */
                            calculator_vtable,
                            NULL);
if (r < 0) 
{
    fprintf(stderr, "Failed to issue method call: %s\n", strerror(-r));
    goto finish;
}

/* 3. 设置服务名称 供客户查找和调用
*/
r = sd_bus_request_name(bus, "net.poettering.Calculator", 0);
if (r < 0) 
{
    fprintf(stderr, "Failed to acquire service name: %s\n", strerror(-r));
    goto finish;
}

/*
   4. 循环等待并处理bus请求事件 
*/
while(1) 
{
    /* 处理bus请求 实际的流程在方法中处理 
    */
    r = sd_bus_process(bus, NULL);
    if (r < 0) 
    {
        fprintf(stderr, "Failed to process bus: %s\n", strerror(-r));
        goto finish;
    }

    if (r > 0)
        continue;

    /* 等待下一个事件 */
    r = sd_bus_wait(bus, (uint64_t) -1);
    if (r < 0) 
    {
        fprintf(stderr, "Failed to wait on bus: %s\n", strerror(-r));
        goto finish;
    }
}

finish:
/* 服务退出时 关闭获取到的系统bus连接
*/
sd_bus_slot_unref(slot);
sd_bus_unref(bus);

return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;

}
</code></pre>
</details>

(1) 获取系统dbus
使用sd_bus_open_system接口拿到系统dbus.
(2) 在系统dbus中安装自定义的dbus object,设置D-Bus路径,接口,方法
这立我们创建了一个method_multiply乘法方法,它的路径为/net/poettering/Calculator,接口为net.poettering.Calculator.
(3) 如果调用method_multiply方法,这里需要进行鉴权.
①首先获取请求dbus的进程pid
sd-dbus中的获取方法见 test.c get_sender_pid(sd_bus_message m)函数.
②接着检查进程的路径是否在白名单中.
根据进程pid获取进程的路径,具体实现参考 test.c check_path(char
path)函数.
检查进程path是否在白名单文件中,具体参考 test.c check_path(char* path)函数.
若进程路径在白名单中,则验证成功.
③polkit鉴权
调用org.freedesktop.PolicyKit1.Authority dbus接口,对进程进行认证.
sender进程如果拥有action的权限,认证通过.
sender进程如果没有action的权限,将调用polkit弹框与用户交互获取授权.
具体实现参考test.c check_pid(pid_t pid)函数.
sender进程如果具备权限或交互获取到了权限,则验证成功.
④未通过验证的dbus请求,返回失败.
(4)设置服务名称 供客户查找和调用

sd_bus_request_name(bus, "net.poettering.Calculator", 0)    

(5)循环等待并处理bus请求事件
(6)服务退出时 关闭获取到的系统bus连接
(7)编译 test.c 使用make或

gcc test.c -o test `pkg-config --cflags --libs libsystemd`

(8)由于是系统D-Bus,所以程序需要使用管理员权限运行

sudo ./test 

(9)D-Feet工具检查net.poettering.Calculator是否启用

你也可以使用Qt Dbus、dbus-glib或其他dbus库创建自己的dbus接口.

4. 客户端调用D-Bus接口

<details>
<summary>展开查看 代码test_client.c</summary>
<pre>

include <stdio.h>

include <stdlib.h>

include <errno.h>

include <systemd/sd-bus.h>

int main()
{
sd_bus_error error = SD_BUS_ERROR_NULL;
sd_bus_message *m = NULL;
sd_bus *bus = NULL;
const char *path;
int r;
</pre>
<code>

uint64_t result;
const char* s;

/* 1. 获取一个系统bus连接
    如果是用户dbus接口,使用 sd_bus_open_user 函数 获取 用户dbus连接
 */
r = sd_bus_open_system(&bus);
if (r < 0) 
{
    fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r));
    goto finish;
}

/* 2. 调用dbus接口方法 */
r = sd_bus_call_method(
    bus,
    "net.poettering.Calculator",   /* dubs 名称*/
    "/net/poettering/Calculator",  /* dbus结构体路径 */
    "net.poettering.Calculator",   /* 接口名称 */
    "Multiply",                    /* 方法 */
    &error,                        /* 返回错误信息保存 */
    &m,                            /* return message on success */
    "xx", 
        /* 输入参数类型 ’xx‘ 表示接下来的两个int64参数
        结构体用 () 
        disct用 {}
        glibvariant 用v标示
        可以对照着d-feet的method 参数填写
        详见 https://manpages.debian.org/testing/libsystemd-dev/sd_bus_message_read.3.en.html
        */
    2,
    3
    );

if (r < 0)
{
    fprintf(stderr, "Failed to issue method call: %s\n", error.message);
    goto finish;
}
/* 3.读取结果
*/
r = sd_bus_message_read(m, "xs", &result, &s);
if( r < 0)
{
    printf("sd_bus_message_read failed.\n");
    goto finish;
}

printf("sd_bus_message_read result = %s, %u.\n",s, result);

finish:
/*释放dbus连接
*/
sd_bus_error_free(&error);
sd_bus_message_unref(m);
sd_bus_unref(bus);

return r;

}
</code>

</details>
运行此客户端程序,将弹出polkit鉴权窗口,只有当输入正确密码方可执行。

三、 dbus接口使用polkit鉴权应用场景

当提供的服务、接口涉及安全,对调用者身份有验证要求时推荐使用这种方式.
以系统dbus中进行白名单检查、polkit鉴权的方式提供服务,保证了安全性.
调用者使用接口,不需要再单独进行鉴权等安全认证操作.

The new sd-bus API of systemd
polkit Reference Manual
Creating a D-Bus Service with dbus-python and Polkit Authentication
sd_bus_call_method Examples
sd-bus

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容