前言

在这篇Android on Linux(在Linux主机上运行Android可执行程序)文章中,我们在Linux主机上运行Android可执行程序时是使用了sudo来执行,也就是需要root权限。

现在使用的需求是在要服务器上运行,而服务器此时是在git用户,而不是在root用户。可以在运行前从git用户切到root用户,但是这样需要输入root用户的密码。同样使用sudo来执行,也需要密码。或者将此用户的sudo设置为免密码,但这样显得就不是很安全。有没有更好的方案呢?

本文将探讨的是,使用Linux的服务(service),启动一个daemon进程,客户端(git用户)将请求发送到服务端(root用户),在daemon进程中来执行需要特权的程序,同时再将执行结果返回到客户端。从而实现在无特权用户执行特权程序。

探索过程

docker中提到的方法

在使用docker中,我们默认是需要使用sudo的,但是可以免去sudo,在docker的安装文档Post-installation steps for Linux中写到:

Manage Docker as a non-root user

The Docker daemon binds to a Unix socket instead of a TCP port. By default that Unix socket is owned by the user root and other users can only access it using sudo. The Docker daemon always runs as the root user.

If you don’t want to preface the docker command with sudo, create a Unix group called docker and add users to it. When the Docker daemon starts, it creates a Unix socket accessible by members of the docker group.

Warning
The docker group grants privileges equivalent to the root user. For details on how this impacts security in your system, see Docker Daemon Attack Surface.

Note:
To run Docker without root privileges, see Run the Docker daemon as a non-root user (Rootless mode).
Rootless mode is currently available as an experimental feature.

这里主要提到两点:
1、Docker daemon在root用户运行,如果想免sudo,可以创建docker用户组,并将当前用户加入其中。
2、无root特权运行Doker,使用Rootless mode,在非root用户下运行Docker daemon。

这里我们不研究第2点的方式,主要看第1点给我们提到的思路。在root用户下开启一个daemon进程,其它用户的进程与daemon进程通讯,这样就可以避免使用sudo了。

daemon、service和Systemd

什么是 daemon 与服务(service)文章中,介绍了daemon与service。

简单说,你启动一个程序,这个程序提供你一些功能,那么这个程序就是 daemon,程序运行后提供的功能就是 service

说到Linux service就要提到init,在如何查看 Linux 中所有正在运行的服务中提到:

init(初始化initialization的简称)是在系统启动期间运行的第一个进程。init 是一个守护进程,它将持续运行直至关机。

大多数 Linux 发行版都使用如下的初始化系统之一:
System V 是更老的初始化系统
Upstart 是一个基于事件的传统的初始化系统的替代品
systemd 是新的初始化系统,它已经被大多数最新的 Linux 发行版所采用

我们使用的ubuntu服务器默认也是使用的systemd,在Systemd 入门教程:命令篇中提到:

Systemd为系统的启动和管理提供一套完整的解决方案。
Systemd 可以管理所有系统资源。不同的资源统称为 Unit(单位)。Unit 一共分成12种。其中就包含我们需要的Service unit:系统服务。

对于那些支持 Systemd 的软件,安装的时候,会自动在/usr/lib/systemd/system目录添加一个配置文件。
配置文件主要放在 /usr/lib/systemd/system 目录,也可能在 /etc/systemd/system 目录。

下面我们来添加一个简单Service Unit:test.service文件,(start_test_service.shstop_test_service.sh脚本后续会实现)。

[Unit]
Description=Service for test.service

[Service]
ExecStart=/opt/start_test_service.sh
Restart=on-failure
ExecStop=/opt/stop_test_service.sh

[Install]
WantedBy=multi-user.target

Service Unit中的介绍:

[Unit]区块通常是配置文件的第一个区块,用来定义 Unit 的元数据,以及配置与其他 Unit 的关系。Description:简短描述。

[Service]区块用来 Service 的配置,只有 Service 类型的 Unit 才有这个区块。
ExecStart:启动当前服务的命令
Restart:定义何种情况 Systemd 会自动重启当前服务,可能的值包括always(总是重启)、on-success、on-failure、on-abnormal、on-abort、on-watchdog
ExecStop:停止当前服务时执行的命令

[Install]通常是配置文件的最后一个区块,用来定义如何启动,以及是否开机启动。
WantedBy:它的值是一个或多个 Target,当前 Unit 激活时(enable)符号链接会放入/etc/systemd/system目录下面以 Target 名 + .wants后缀构成的子目录中

Systemd 入门教程:实战篇中,介绍了如何开启服务,开机自启。

设置开机启动以后,软件并不会立即启动,必须等到下一次开机。如果想现在就运行该软件,那么要执行systemctl start命令。systemctlSystemd 的主命令,用于管理系统。

$ sudo systemctl start test.service

执行上面的命令以后,有可能启动失败,因此要用systemctl status命令查看一下该服务的状态。
终止正在运行的服务,需要执行systemctl stop命令。

$ sudo systemctl stop test.service

Systemd 默认从目录/etc/systemd/system/读取配置文件。但是,里面存放的大部分文件都是符号链接,指向目录/usr/lib/systemd/system/,真正的配置文件存放在那个目录。
systemctl enable命令用于在上面两个目录之间,建立符号链接关系。

$ sudo systemctl enable test.service

如果配置文件里面设置了开机启动,systemctl enable命令相当于激活开机启动。
与之对应的,systemctl disable命令用于在两个目录之间,撤销符号链接关系,相当于撤销开机启动。

$ sudo systemctl disable test.service

至此我们知道了如何启动一个类似Docker daemon的守护进程,接下来我们看如何实现进程间通讯。

实现过程

进程间通信的几种方式

Linux进程间通信的几种方式介绍中,提到几种进程间通讯的方式:

1.管道:管道的实质是一个内核缓冲区,管道的作用正如其名,需要通信的两个进程在管道的两端,进程利用管道传递信息。管道对于管道两端的进程而言,就是一个文件,但是这个文件比较特殊,它不属于文件系统并且只存在于内存中。

管道依据是否有名字分为匿名管道和命名管道(有名管道),这两种管道有一定的区别。

匿名管道有几个重要的限制:

管道是半双工的,数据只能在一个方向上流动,A进程传给B进程,不能反向传递
管道只能用于父子进程或兄弟进程之间的通信,即具有亲缘关系的进程。
命名管道允许没有亲缘关系的进程进行通信。命名管道不同于匿名管道之处在于它提供了一个路径名与之关联,这样一个进程即使与创建有名管道的进程不存在亲缘关系,只要可以访问该路径,就能通过有名管道互相通信。

2.信号:信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,进程不必通过任何操作来等待信号的到达。信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件。

3.消息队列:消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识,并且允许一个或多个进程向它写入与读取消息

4.共享内存:使得多个进程可以可以直接读写同一块内存空间,是针对其他通信机制运行效率较低而设计的。

5.信号量:信号量实质上就是一个标识可用资源数量的计数器,它的值总是非负整数。而只有0和1两种取值的信号量叫做二进制信号量(或二值信号量),可用用来标识某个资源是否可用。

6.套接字:套接字是更为基础的进程间通信机制,与其他方式不同的是,套接字可用于不同机器之间的进程间通信。

service的实现

这里根据我们的需求,使用命名管道(FIFO),Using Named Pipes (FIFOs) with Bash中展示了bash中如何使用。

下面来看下root用户下,启动服务脚本start_test_service.sh

#!/bin/bash

# /opt/start_test_service.sh

service_pipe=/tmp/test_service_pipe
client_pipe=/tmp/test_client_pipe

trap "rm -f $service_pipe; rm -f $client_pipe" EXIT

if [[ ! -p $service_pipe ]]; then
    mkfifo $service_pipe
    chmod 666 $service_pipe
fi

if [[ ! -p $client_pipe ]]; then
    mkfifo $client_pipe
    chmod 666 $client_pipe
fi

while true; do
    if read line <$service_pipe; then
        if [[ "$line" == 'quit' ]]; then
            break
        elif [[ "$line" == 'start' ]]; then
            root_run_test.sh >$client_pipe
            echo 'quit' >$client_pipe
        fi
    fi
done

echo "test_service exiting"

start_test_service.sh的作用:
1、service_pipe管道接受客户端发送的请求,收到请求后执行root_run_test.sh,在root用户中完成需要使用root执行的脚本;
2、将执行的输出写入client_pipe管道,然后等待下一次请求。

服务停止时调用的脚本stop_test_service.sh

#!/bin/bash

# /opt/stop_test_service.sh

service_pipe=/tmp/test_service_pipe
client_pipe=/tmp/test_client_pipe

rm -f $service_pipe
rm -f $client_pipe

stop_test_service.sh的作用:
删除service_pipe管道和client_pipe管道。

客户端的实现

在git用户中执行的客户端test.sh脚本:

#!/bin/bash
# test.sh
service_pipe=/tmp/test_service_pipe
client_pipe=/tmp/test_client_pipe

if [[ ! -p $service_pipe ]]; then
    echo "service_pipe not exist."
    exit 1
fi

if [[ ! -p $client_pipe ]]; then
    echo "client_pipe not exist."
    exit 1
fi

echo 'start' >$service_pipe


while true; do
    if read line <$client_pipe; then
        if [[ "$line" == 'quit' ]]; then
            break
        fi
        echo $line
        ret=$line
    fi
done

test.sh的作用:
1、向service_pipe管道中写数据,发送请求;
2、循环从client_pipe管道中读取执行在root用户下执行的root_run_test.sh脚步的结果。

总结:至此我们完成了在非root用户下,与root用户下的服务的跨进程通讯,避免了运行程序时再需要root权限。