0%

Linux UDS与FD迁移技术

Unix Domain Sockets

UDS(Unix Domain Sockets)在 Linux 上表现为一个文件,相比较于普通 socket 监听在端口上,一个进程也可以监听在一个 UDS 文件上,比如 /tmp/xjjdog.sock

由于通过这个文件进行数据传输,并不需要走网卡等物理设备,所以通过 UDS 传输数据,速度是非常快的

UDS 主要用于同一主机上的进程间通信

Socket 网络通信原理

之前已经分析过了 Socket 的网络通信原理与内核对协议栈的处理

大致的流程如下:

  • 驱动通知网卡有一个新的描述符(有一个即将网络包到达网卡)
  • 网卡从 RX ring buffer 中取出描述符,从而获知缓冲区的地址和大小
  • 网卡收到新的数据包
  • 网卡将新数据包直接通过 DMA 写到内核堆中 sk_buffer 结构体里,并使用中断来通知内核
    • RX ring buffer:网络栈接收数据环形缓存区
    • DMA:Direct Memory Access 直接存储器访问,外部设备不通过CPU而直接与系统内存交换数据的接口技术
  • 内核调用 netif_receive_skb 来处理 sk_buffer 中的数据包
  • 对于网络协议,内核会遍历协议容器 ptype_base 中的所有协议 packet_type,匹配成功后调用 packet_type->func 完成相应的处理
  • 最后把处理的结果输出到各个目标进程中

Socket 本地通信原理

Unix Domain Sockets 也使用了差不多的原理,但差别如下:

  • UDS 不需要 IP:Port,而是通过一个文件名来表示
  • 使用 UDS 时,socket 函数的 domain 参数固定为 AF_UNIX
  • UDS 中使用 sockaddr_un 来表示 sockaddr 结构体

剩下的操作就和 socket 接口的那一套东西一样了

Msg 消息队列

之前也分析过了信息队列的底层实现

消息队列,是消息的链接表,存放在内核中,一个消息队列由一个标识符(即ID)来标识

  • 消息队列的标识符 key 键,它的基本类型是 key_t,使用 ftok 函数可以生成一个 key_t
  • 两个无关的进程,可以通过唯一标识符 key 来找到对应的 msg

msgget 会在内核堆空间中创建一个 msg_queue 结构体,用于表示一个消息队列的头

msgsndmsgrcv 都依靠 msg_msg 结构体,用于表示在这个消息队列中的各个消息主体

这两者通过链表相关联:

1
msg_queue -> msg_msg -> msg_msg -> msg_msg ...

但现在我又了解到了信息队列的一个特殊用途:用于传递 Socket 信息

FD 迁移技术

FD 迁移技术可以把一个进程所挂载的连接(socket),转移到另外一个进程之上

主要是通过如下两个系统调用:

1
2
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssizt_t sendmsg(int sockfd, struct msghdr *msg, int flags);
  • 这个两个函数都需要传入一个 sockfd,并依赖 msghdr 结构体来传递信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct msghdr
{
void *msg_name; /* Address to send to/receive from. */
socklen_t msg_namelen; /* Length of address data. */

struct iovec *msg_iov; /* Vector of data to send/receive into. */
int msg_iovlen; /* Number of elements in the vector. */

void *msg_control; /* Ancillary data (eg BSD filedesc passing). */
socklen_t msg_controllen; /* Ancillary data buffer length. */

int msg_flags; /* Flags in received message. */
};
  • 其中的 msg_control 条目有需要指向 cmsghdr 结构体:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct cmsghdr
{
size_t cmsg_len; /* Length of data in cmsg_data plus length
of cmsghdr structure.
!! The type should be socklen_t but the
definition of the kernel is incompatible
with this. */
int cmsg_level; /* Originating protocol. */
int cmsg_type; /* Protocol specific type. */
#if __glibc_c99_flexarr_available
__extension__ unsigned char __cmsg_data __flexarr; /* Ancillary data. */
#endif
};
  • 成员变量 cmsg_type 有3个类型:
    • SCM_RIGHTS
    • SCM_CREDENTIALS
    • SCM_SECURITY
  • 其中,SCM_RIGHTS 就是我们所需要的,它允许我们从一个进程,发送一个文件句柄到另外一个进程

因此,依靠 sendmsgrecvmsg 两个系统调用就可以实现 FD 句柄的传输

  • 依靠 sendmsg 函数,将 FD 句柄发送到另外一个进程
  • 依靠 recvmsg 函数,接收这部分数据,然后将其还原成 cmsghdr 结构体,然后我们就可以从 cmsg_data 中获取句柄列表
  • 其实 FD 句柄在某个进程里只是一个引用,真正的 FD 句柄是放在内核中的,所谓的迁移,只不过是把一个指针,从一个进程中去掉,再加到另外一个进程中罢了

测试案例如下:

  • 发送 FD 句柄:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <sys/socket.h>

#define handle_error(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)

static void send_fd(int socket, int *fds, int n) // send fd by socket
{
struct msghdr msg = {0};
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(n * sizeof(int))], dup[256];
memset(buf, '\0', sizeof(buf));
struct iovec io = { .iov_base = &dup, .iov_len = sizeof(dup) };

msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);

cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(n * sizeof(int));

memcpy ((int *) CMSG_DATA(cmsg), fds, n * sizeof (int));

if (sendmsg (socket, &msg, 0) < 0)
handle_error ("Failed to send message");
}

int main(int argc, char *argv[]) {
int sfd, fds[2];
struct sockaddr_un addr;

if (argc != 3) {
fprintf (stderr, "Usage: %s <file-name1> <file-name2>\n", argv[0]);
exit (1);
}

sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1)
handle_error ("Failed to create socket");

memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/fd-pass.socket", sizeof(addr.sun_path) - 1);

fds[0] = open(argv[1], O_RDONLY);
if (fds[0] < 0)
handle_error ("Failed to open file 1 for reading");
else
fprintf (stdout, "Opened fd %d in parent\n", fds[0]);

fds[1] = open(argv[2], O_RDONLY);
if (fds[1] < 0)
handle_error ("Failed to open file 2 for reading");
else
fprintf (stdout, "Opened fd %d in parent\n", fds[1]);

if (connect(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
handle_error ("Failed to connect to socket");

send_fd (sfd, fds, 2);

exit(EXIT_SUCCESS);
}
  • 接收 FD 句柄:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <sys/socket.h>

#define handle_error(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)

static int * recv_fd(int socket, int n) {
int *fds = malloc (n * sizeof(int));
struct msghdr msg = {0};
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(n * sizeof(int))], dup[256];
memset(buf, '\0', sizeof(buf));
struct iovec io = { .iov_base = &dup, .iov_len = sizeof(dup) };

msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);

if (recvmsg (socket, &msg, 0) < 0)
handle_error ("Failed to receive message");

cmsg = CMSG_FIRSTHDR(&msg);

memcpy (fds, (int *) CMSG_DATA(cmsg), n * sizeof(int));

return fds;
}

int main(int argc, char *argv[]) {
ssize_t nbytes;
char buffer[256];
int sfd, cfd, *fds;
struct sockaddr_un addr;

sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1)
handle_error ("Failed to create socket");

if (unlink ("/tmp/fd-pass.socket") == -1 && errno != ENOENT)
handle_error ("Removing socket file failed");

memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/fd-pass.socket", sizeof(addr.sun_path) - 1);

if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
handle_error ("Failed to bind to socket");

if (listen(sfd, 5) == -1)
handle_error ("Failed to listen on socket");

cfd = accept(sfd, NULL, NULL);
if (cfd == -1)
handle_error ("Failed to accept incoming connection");

fds = recv_fd (cfd, 2);

for (int i=0; i<2; ++i) {
fprintf (stdout, "Reading from passed fd %d\n", fds[i]);
while ((nbytes = read(fds[i], buffer, sizeof(buffer))) > 0)
write(1, buffer, nbytes);
*buffer = '\0';
}

if (close(cfd) == -1)
handle_error ("Failed to close client socket");

return 0;
}
  • 结果:
1
2
3
exp ./send flag flag2
Opened fd 4 in parent
Opened fd 5 in parent
1
2
3
4
5
exp ./read           
Reading from passed fd 5
flag{yhellow}
Reading from passed fd 6
flag{yhellow}
  • send 程序中打开的两个文件 flag flag2 被迁移到了 read 程序中