xrdp源码解析

11/9/2023

xrdp是一款开源的rdp server框架,另外还提供了VCN和RDP的代理程序。主要针对 GNU/Linux 操作系统。x86(包括 x86-64)和 ARM 处理器。其他架构的处理器尚且不成熟。某些组件(例如 xorgxrdp 和 RemoteFX 编解码器)使用 SIMD 指令针对 x86 进行了特殊优化。因此在 x86 处理器上运行 xrdp (硬件加速)将获得完全加速的体验。

# 概览

他的项目结构下面这样图解释的很清楚:

image-20231108143728303

# xrdp模块分解

xrdp总体上分为三个服务,xrdp、xrdp-sesman(session manager)、xrdp-chansrv(channel server)。了解这三个服务的主要作用对我们阅读xrdp源码有很大的用处。正所谓会当凌绝顶,一览众山小。

# xrdp

xrdp首先会有一个mainloop来监听客户端的连接。我们直接看下面这张图:

image-20231108152727755

看左半部分,从xrdp_process_main_loop开始,可以看到这个函数中延伸了三个函数,分别是:

**xrdp_rdp_incomming、xrdp_porecess_data_in、xrdp_wm_check_wait_objs。**可以说xrdp的半壁江山都在这三个函数中,他们被一个大的mainloop包裹着。

/*****************************************************************************/
int xrdp_process_main_loop(struct xrdp_process *self) {
  int robjs_count;
  int wobjs_count;
  int cont;
  int timeout = 0;
  tbus robjs[32];
  tbus wobjs[32];
  tbus term_obj;

  DEBUG(("xrdp_process_main_loop"));
  self->status = 1;
  self->server_trans->extra_flags = 0;
  self->server_trans->header_size = 0;
  self->server_trans->no_stream_init_on_data_in = 1;
  self->server_trans->trans_data_in = xrdp_process_data_in;
  self->server_trans->callback_data = self;
  init_stream(self->server_trans->in_s, 8192 * 4);
  self->session = libxrdp_init((tbus)self, self->server_trans);
  self->server_trans->si = &(self->session->si);
  self->server_trans->my_source = XRDP_SOURCE_CLIENT;
  /* this callback function is in xrdp_wm.c */
  self->session->callback = callback;
  /* this function is just above */
  self->session->is_term = xrdp_is_term;

  if (libxrdp_process_incoming(self->session) == 0) {
    init_stream(self->server_trans->in_s, 32 * 1024);

    term_obj = g_get_term_event();
    cont = 1;

    while (cont) {
      /* build the wait obj list */
      timeout = -1;
      robjs_count = 0;
      wobjs_count = 0;
      robjs[robjs_count++] = term_obj;
      robjs[robjs_count++] = self->self_term_event;
      xrdp_wm_get_wait_objs(self->wm, robjs, &robjs_count, wobjs, &wobjs_count,
                            &timeout);
      trans_get_wait_objs_rw(self->server_trans, robjs, &robjs_count, wobjs,
                             &wobjs_count, &timeout);
      /* wait */
      if (g_obj_wait(robjs, robjs_count, wobjs, wobjs_count, timeout) != 0) {
        /* error, should not get here */
        g_sleep(100);
      }

      if (g_is_wait_obj_set(term_obj)) /* term */
      {
        break;
      }

      if (g_is_wait_obj_set(self->self_term_event)) {
        break;
      }

      if (xrdp_wm_check_wait_objs(self->wm) != 0) {
        break;
      }

      if (trans_check_wait_objs(self->server_trans) != 0) {
        break;
      }
    }
    /* send disconnect message if possible */
    libxrdp_disconnect(self->session);
  } else {
    g_writeln("xrdp_process_main_loop: libxrdp_process_incoming failed");
    /* this will try to send a disconnect,
       maybe should check that connection got far enough */
    libxrdp_disconnect(self->session);
  }
  /* Run end in module */
  xrdp_process_mod_end(self);
  libxrdp_exit(self->session);
  self->session = 0;
  self->status = -1;
  g_set_wait_obj(self->done_event);
  return 0;
}
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
77
78
79
80
81
82

# xrdp_rdp_incomming

这个函数就是来接受客户端连接的第一个步骤、它主要是进行RDP协议层面的交互,包括密钥协商、能力协商、rdp通道等等。这是xrdp处理客户端的前置部分、只包含协商并不包含具体的文件、快速路径、等等数据的解析处理。

# xrdp_porecess_data_in

这个函数就是具体接受rdp协议的各种数据的函数了。

/*****************************************************************************/
static int
xrdp_process_loop(struct xrdp_process *self, struct stream *s)
{
    int rv;

    rv = 0;

    if (self->session != 0)
    {
        rv = libxrdp_process_data(self->session, s);

        if ((self->wm == 0) && (self->session->up_and_running) && (rv == 0))
        {
            DEBUG(("calling xrdp_wm_init and creating wm"));
            self->wm = xrdp_wm_create(self, self->session->client_info);
            /* at this point the wm(window manager) is create and wm::login_mode is
               zero and login_mode_event is set so xrdp_wm_init should be called by
               xrdp_wm_check_wait_objs */
        }
    }

    return rv;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

这个函数里面并没有while 逻辑,为什么说他是个mainloop?继续看上面那张图、可以看到这个函数在xrdp_process_main_loop之上,到这里xrdp的前半部分的架构逻辑大致都清楚了,在接受到数据后会以回调的形式传递到这个函数

最后所有的rdp协议消息都会给到这个函数来分发处理:

int EXPORT_CC
libxrdp_process_data(struct xrdp_session *session, struct stream *s)
{
  		  ......
        switch (code)
        {
            case -1:
                xrdp_caps_send_demand_active(rdp);
                session->up_and_running = 0;
                break;
            case 0:
                dead_lock_counter++;
                break;
            case PDUTYPE_CONFIRMACTIVEPDU:
                xrdp_caps_process_confirm_active(rdp, s);
                break;
            case PDUTYPE_DATAPDU:
                if (xrdp_rdp_process_data(rdp, s) != 0)
                {
                    DEBUG(("libxrdp_process_data returned non zero"));
                    cont = 0;
                    term = 1;
                }
                break;
            case 2: /* FASTPATH_INPUT_EVENT */
                if (xrdp_fastpath_process_input_event(rdp->sec_layer->fastpath_layer, s) != 0)
                {
                     DEBUG(("libxrdp_process_data returned non zero"));
                     cont = 0;
                     term = 1;
                }
                break;
            default:
                g_writeln("unknown in libxrdp_process_data: code= %d", code);
                dead_lock_counter++;
                break;
        }
  ......
}
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

# xrdp_wm_check_wait_objs

这个函数是主要处理xrdp的绘图窗口配置、初始化的函数。在分析这个函数之前我们需要看另一个函数xrdp_wm_login_mode_changed,这个方法其实是处理xrdp内部对于WM(window maneger)的认证的状态机的。其中又包含与sesman建立连接的状态。看其中的一部分代码:

if (xrdp_mm_connect(self->mm) == 0) {
  xrdp_wm_set_login_mode(self, 3); /* put the wm in connected mode */
  xrdp_wm_delete_all_children(self);
  self->dragging = 0;
}
1
2
3
4
5

可以看到xrdp_mm_connect调用成功后就将wm login mode设置为了3。至于xrdp_mm_connect函数内部、就是与sesman建立连接、认证,其中又用到了linux 的pam stack认证。前提是你配置了pamauth。libpam是linux提供的主机认证的开发库。sshd服务中也有这个配置。

UsePAM yes
1

UsePAM 设置为 yes 允许你配置 OpenSSH 服务器以使用 PAM 来进行身份验证和处理帐户及会话,但具体的行为将依赖于其他相关设置的配置。xrdp这里也是一样。至于pam认证的逻辑、是在sesman模块,这里只是简单的调用,具体的认证会放在xrdp-sesman讲解。

reply = access_control(pam_auth_username, pam_auth_password, pam_auth_sessionIP);
additionalError = getPAMAdditionalErrorInfo(reply, self);
if (additionalError && additionalError[0])
{
  xrdp_wm_log_msg(self->wm, LOG_LEVEL_INFO, "%s", additionalError);
}
......
1
2
3
4
5
6
7

在与sesman建立连接之前、xrdp将sesman的通道回调进行赋值。

    /* xrdp_mm_sesman_data_in is the callback that is called when data arrives
     */
    self->sesman_trans->trans_data_in = xrdp_mm_sesman_data_in;
    self->sesman_trans->header_size = 8;
    self->sesman_trans->callback_data = self;
1
2
3
4
5

这样在主mainloop中就可以一同处理这个连接了。建立连接成功后,xrdp模块向sesman开始发送login通知,并且将connected_state置为1。

    if (ok) {
      /* fully connect */
      xrdp_wm_log_msg(self->wm, LOG_LEVEL_INFO, "sesman connect ok");
      self->connected_state = 1;
      rv = xrdp_mm_send_login(self);
    }
......
1
2
3
4
5
6
7

如果sesman那边的逻辑处理成功、则会向xrdp模块发送login response消息,这时会在上面的回调(xrdp_mm_sesman_data_in)中进行后置处理。其实xrdp_mm_sesman_data_in里面仅仅处理了login response这一个消息而已。而且就算sesman拒绝了本次login、也同样会发送login response:

......
  switch (code) {
      /* even when the request is denied the reply will hold 3 as the command.
       */
    case 3:
      error = xrdp_mm_process_login_response(self, s);
      break;
    default:
      xrdp_wm_log_msg(self->wm, LOG_LEVEL_ERROR,
                      "Undefined reply code %d received from sesman", code);
      cleanup_sesman_connection(self);
      break;
  }
1
2
3
4
5
6
7
8
9
10
11
12
13

最后在xrdp_mm_process_login_response中进行与sesman连接的后置处理。

注意:对于绘图窗口的打开是在sesman里面,我们需要思考一问题,但是xrdp实际上需要绘图吗?其实xrdp本身是不需要进行绘图的,他是去拿xorg的绘图消息、然后转化成rdp的协议消息发送给客户端,这也就是为什么你在mstsc中能看到图像的原因,我们可以将xrdp理解为xorg到rdp协议的一个converter

当我得知这个理念之后我们就能理解xrdp中的xvnc/xorgxrdp的作用了。这样我们也能理解为什么xrdp主模块中为什么又个mm(module manager)的东西了,其实就是去显式得去load这些模块的客户端。直接上代码就清楚了,从xrdp_mm_process_login_response开始:

  if (ok) {
    self->display = display;
    xrdp_wm_log_msg(self->wm, LOG_LEVEL_INFO, "login successful for display %d",
                    display);

    if (xrdp_mm_setup_mod1(self) == 0) {
      if (xrdp_mm_setup_mod2(self, pguid) == 0) {
        xrdp_mm_get_value(self, "ip", ip, 255);
        xrdp_wm_set_login_mode(self->wm, 10);
        self->wm->dragging = 0;

        /* connect channel redir */
        if ((g_strcmp(ip, "127.0.0.1") == 0) || (ip[0] == 0)) {
          g_snprintf(port, 255, XRDP_CHANSRV_STR, display);
        } else {
          g_snprintf(port, 255, "%d", 7200 + display);
        }
        xrdp_mm_update_allowed_channels(self);
        xrdp_mm_connect_chansrv(self, ip, port);
      }
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

xrdp_mm_setup_mod1这个函数就是去加载这些模块的,设置处理回调等等。可以理解为mod1就是连接xserver的前置操作,真正进行socket连接的是在下面这个函数:xrdp_mm_setup_mod2,在xup中我们可以看到连接xserver的方法,(不过为什么连接失败重试了60次,不得不说xrdp的开发人员真是个奇葩)。

    while (1)
    {

        /* mod->server_msg(mod, "connecting...", 0); */

        error = -1;
        if (trans_connect(mod->trans, mod->ip, con_port, 3000) == 0)
        {
            LLOGLN(0, ("lib_mod_connect: connected to Xserver "
                   "(Xorg or X11rdp) sck %lld",
                   (long long) (mod->trans->sck)));
            error = 0;
        }

        if (error == 0)
        {
            break;
        }

        if (mod->server_is_term(mod))
        {
            break;
        }

        i++;

        if (i >= 60)
        {
            mod->server_msg(mod, "connection problem, giving up", 0);
            break;
        }

        g_sleep(500);
    }
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

之后在mainloop中就会触发xserver的消息通知然后根据不同的绘图操作发送给客户端了,这里举个例子:

/******************************************************************************/
/* return error */
static int
process_server_fill_rect(struct mod *mod, struct stream *s)
{
    int rv;
    int x;
    int y;
    int cx;
    int cy;

    in_sint16_le(s, x);
    in_sint16_le(s, y);
    in_uint16_le(s, cx);
    in_uint16_le(s, cy);
    rv = mod->server_fill_rect(mod, x, y, cx, cy);
    return rv;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这个方法就是在接收到xserver的矩形填充进行的处理操作。

这里我们需要问:客户端输入的内容在哪里处理的?处理剪贴板、键盘鼠标的部分哪里去了?,其实这一部分我们在上面已经提到了,只是没有将rdp协议细节到鼠标键盘的部分,如果单独说这一部分其实从rdp协议层面来说,这一部分确实应该单独拎出来,因为rdp协议的这部分数据是放在快速路径中处理的,不同于其他静态/动态虚拟通道。ok,我们来看一下这部分在哪里。

从上面提到的libxrdp_process_data出发、一路追踪到xrdp_fastpath_process_input_event:

int
xrdp_fastpath_process_input_event(struct xrdp_fastpath *self,
                                  struct stream *s)
{
    int i;
    int eventHeader;
    int eventCode;
    int eventFlags;

    /* process fastpath input events */
    for (i = 0; i < self->numEvents; i++)
    {
        if (!s_check_rem(s, 1))
        {
            return 1;
        }
        in_uint8(s, eventHeader);

        eventFlags = (eventHeader & 0x1F);
        eventCode = (eventHeader >> 5);

        switch (eventCode)
        {
            case FASTPATH_INPUT_EVENT_SCANCODE:
                if (xrdp_fastpath_process_EVENT_SCANCODE(self,
                                                         eventFlags,
                                                         s) != 0)
                {
                    return 1;
                }
                break;
            case FASTPATH_INPUT_EVENT_MOUSE:
                if (xrdp_fastpath_process_EVENT_MOUSE(self,
                                                      eventFlags,
                                                      s) != 0)
                {
                    return 1;
                }
                break;
            case FASTPATH_INPUT_EVENT_MOUSEX:
                if (xrdp_fastpath_process_EVENT_MOUSEX(self,
                                                       eventFlags,
                                                       s) != 0)
                {
                    return 1;
                }
                break;
            case FASTPATH_INPUT_EVENT_SYNC:
                if (xrdp_fastpath_process_EVENT_SYNC(self,
                                                     eventFlags,
                                                     s) != 0)
                {
                    return 1;
                }
                break;
            case FASTPATH_INPUT_EVENT_UNICODE:
                if (xrdp_fastpath_process_EVENT_UNICODE(self,
                                                        eventFlags,
                                                        s) != 0)
                {
                    return 1;
                }
                break;
            default:
                g_writeln("xrdp_fastpath_process_input_event: unknown "
                          "eventCode %d", eventCode);
                break;
        }
    }
    return 0;
}
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

最终我们发现,这条链路将所有的消息都统一发送给了一个回调函数:

        /* msg_type can be
          RDP_INPUT_SYNCHRONIZE - 0
          RDP_INPUT_SCANCODE - 4
          RDP_INPUT_MOUSE - 0x8001
          RDP_INPUT_MOUSEX - 0x8002 */
        /* call to xrdp_wm.c : callback */
        self->session->callback(self->session->id, msg,
                                param1, param2, param3, param4);
1
2
3
4
5
6
7
8

这个callback的实现是在xrdp_wm.c文件中,这个文件和什么有关系?sesman或者xup,OK那我们懂了,这些消息是通过这里的实现传递到了xorg客户端之中。

  if (self->mm->mod != 0) {
    if (self->mm->mod->mod_event != 0) {
      self->mm->mod->mod_event(self->mm->mod, 17, key_flags, device_flags,
                               key_flags, device_flags);
    }
  }
1
2
3
4
5
6

回到一开始我们说的xrdp_wm_check_wait_objs方法,这下我们就彻底明白这个函数的作用了,这个函数的目的就是打开并且处理xserver的消息并且转发给client。ok到这里整个xrdp的这个主模块就分析完了。接下来我们通过下面这张图来回顾这个xrdp主模块的前世今生:

image-20231109154101842

# xrdp-sesman

上一节我们把xrdp主模块的逻辑搞明白了,在开始分析sesman这个模块之前我先抛出几个疑问,带着这几个疑问,我们一点点洞悉sesman这个模块的功能。

  1. 是谁打开了桌面让xrdp主模块连接上去?
  2. xrdp主模块与sesman是如何通信的?
  3. xrdp作者为什么专门设计了session manager模块?

ok带着这三个问题我们来对sesman这个模块一探究竟。

我们在上一节中已经了解到,在客户端认证完成之后,xrdp主模块会与xorg建立socket连接,但是从未提及过这个xorg的桌面是谁打开的?是谁提供了桌面的交互能力?

在sesman模块中同样有一个manloop、大致架构和xrdp主模块极其相似,只是功能少许简单了许多。从sesman_main_loop出发,

/* we've got a connection, so we pass it to scp code */
LOG_DBG("new connection");
scp_process_start((void *)(tintptr)in_sck);
g_sck_close(in_sck);
1
2
3
4

接收到客户端的链接请求后,进入scp_process_start。这里会进行scp的协议部分,scp也就是session control protocol。可以理解为和sesman交互所需要遵守的协议,ok我们其实已经回答了第二个问题了,至于这个协议具体的协议细节,笔者这里不准备展开了,因为这个协议并不复杂,没必要展开分析。

值得注意的一个细节是这里的scp协议也是有版本的:

  scp_status = scp_vXs_accept(&scon, &(sdata));
  if (AsiaInfo_scp_hook(sdata) != 0) {
    scp_status = SCP_SERVER_STATE_VERSION_ERR;
  }

  switch (scp_status) {
  case SCP_SERVER_STATE_OK:
    if (sdata->version == 0) {
      /* starts processing an scp v0 connection */
      LOG_DBG("accept ok, go on with scp v0");
      scp_v0_process(&scon, sdata);
    } else {
      LOG_DBG("accept ok, go on with scp v1");
      /*LOG_DBG("user: %s\npass: %s",sdata->username, sdata->password);*/
      scp_v1_process(&scon, sdata);
    }
   ......
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

可以看到在解析了客户端的数据包之后,根据version是否是0来判断scp的版本。我们使用一些代码分析工具来查看我们内部使用的这个代码使用什么版本:

image-20231110144900946

可以看到我们这里使用的是V0版本,那么我后面进行分析的时候都以V0版本来描述。

access_login_allowed(s->username),这个方法便是sesman中的认证方法的第一步、他会校验sesman的各项配置文件,比如是否允许root账号登陆,是否授权了用户组等等。

  if ((0 == g_strncmp(user, "root", 5)) && (0 == g_cfg->sec.allow_root)) {
    log_message(LOG_LEVEL_WARNING,
                "ROOT login attempted, but root login is disabled");
    return 0;
  }

  if ((0 == g_cfg->sec.ts_users_enable) &&
      (0 == g_cfg->sec.ts_always_group_check)) {
    LOG_DBG("Terminal Server Users group is disabled, allowing authentication",
            1);
    return 1;
  }
1
2
3
4
5
6
7
8
9
10
11
12

然后为当前会话生成一个16位的随机ID

g_random((char *)guid, 16);
scp_session_set_guid(s, guid);
1
2

之后、就进入了xorg的阶段了,也就是这里,通过xorg指令来打开窗口,这就解释了我们的第一个疑问了

/* called with the main thread */
static int session_get_avail_display_from_chain(void) {
  int display;

  display = g_cfg->sess.x11_display_offset;

  while ((display - g_cfg->sess.x11_display_offset) <=
         g_cfg->sess.max_sessions) {
    if (!session_is_display_in_chain(display)) {
      if (!x_server_running_check_ports(display)) {
        return display;
      }
    }

    display++;
  }

  log_message(LOG_LEVEL_ERROR, "X server -- no display in range is available");
  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

至于对端口的校验、xorg环境监察等等细节这里不多做阐述。其实到这里,整个sesman模块的功能已经介绍完了,我们会发现这个模块好像也没有做什么事情,但是代码量却不小。回到最后一个问题,xrdp作者为什么设计sesman模块?

我个人觉得、可能是因为xrdp的作者希望引入其他与rdp协议无关的拓展、例如pam auth、例如xorg/xvnc的支持,又或者说为了项目耦合性的降低。总体而言我觉得sesman模块设计的是极其繁琐的,完全不需要提供这么大的篇幅来去做这些事情,我们可以去看freerdp的设计、如果你需要使用xfreerdp(xorg同理)、那你就必须启动一个X窗口,这件事情没有必要强制依赖在xrdp之中,可以交给用户去做或者提供一个脚本,强制耦合进代码里面是极其繁重的。对于PAM STACK,也完全可以写在rdp的认证回掉中,如果用户希望自定义认证,可以再去实现这个方法,强行引入sesman模块不仅需要设计通信协议、还无形的增加了服务器的负担。总而言之言而总之,设计宜简不宜繁。个人的想法而已,你也可以不同意我的想法。

# Xrdp-chansrv

当我第一眼看到这个模块的时候、我心里其实是崩溃的,我在想,xrdp不会是将静态/动态虚拟通道的处理专门传递到这个模块来进行控制吧?结果还真和我想的一样。

在我们介绍第一个主模块xrdp的时候就已经提到了对于这个模块的调用。在login repsonse的处理逻辑中:

 xrdp_mm_connect_chansrv(self, ip, port);
1
嘉宾
路文飞