summaryrefslogtreecommitdiff
path: root/src/daemon.cpp
blob: f91cd6656b5731b1d4453e7ef390f99c189aa94c (plain)
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
/* An implementation of a SPICE streaming agent
 *
 * \copyright
 * Copyright 2016-2017 Red Hat Inc. All rights reserved.
 */

/* This file implement the daemon part required to
 * avoid permission issues.
 * The daemon will listen to a socket for clients.
 * Clients simply will send informations about new display.
 * Daemon will monitor current terminal and launch a proper
 * agent with information passed.
 */

#include <config.h>
#include "daemon.hpp"
#include "eventfd.hpp"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <syslog.h>
#include <poll.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/prctl.h>
#include <fcntl.h>
#include <pwd.h>
#include <grp.h>
#include <systemd/sd-daemon.h>
#include <string>
#include <map>
#include <stdexcept>

/*
 * There are 3 "roles" for the agent:
 * - main agent;
 * - daemon
 * - helper
 * The role of the agent is to handle a given graphical session
 * capturing and sending video stream to SPICE server.
 * The role of the daemon is to listen to information from helpers
 * collecting Unix session information from the helpers and from
 * system and managing agents.
 * The helper just send session information to the daemon. These
 * information are used to be able to connect to the display server
 * (like X).
 * The agent is a child (forked) of the daemon.
 * This schema is used for different reasons:
 * - the daemon can be run as root having access to the streaming
 *   device file;
 * - the daemon can control the live of the agent making possible to
 *   switch between sessions;
 * - running agents directly launched from a graphical session cause
 *   some issue with SELinux, launching outside a Unix session allows
 *   the process to have less restrictions.
 */

/*
 * The protocol between helper and daemon is pretty simple.
 * Helper connects to daemon and send all information needed to
 * connect to the display server.
 * The helper send a single message which is pretty small (should be at
 * most 1K) through a Unix socket.
 * This:
 * - allows to pass credentials;
 * - a single small message prevent the helper to consume memory on the
 *   daemon side;
 * - allows dynamic activation using SystemD;
 * - writing the client is really easy and can be written in a script
 *   language.
 *
 * Message is:
 * - 1 byte. Version. Has to be 1;
 * - a set of strings, each:
 *   - 1 byte type field, currently:
 *     1) DISPLAY environment;
 *     2) XAUTHORITY environment;
 *   - 1 byte for length
 *   - data
 * - DISPLAY content. The DISPLAY should be local like ":1";
 * - XAUTHORITY environment content (a filename).
 *
 * Note: Linux allows to read the peer credentials (user/group) and
 * PID which we need. If we would need to extent to other systems
 * (like *BSD/Mac OS X) these allows to pass these informations using
 * an ancilliary message and SCM_CREDS (Linux also has a similar
 * SCM_CREDENTIALS).
 */

static const char daemon_socket_name[] = "@spice-streaming-agent-daemon";

struct TerminalInfo
{
    TerminalInfo() = default;
    TerminalInfo(const uint8_t *msg, size_t msg_len);

    std::string display;
    std::string xauthority;
    uid_t uid;
};

/**
 * Parse a message from a client.
 * Throw an exception if the message content is invalid.
 */
TerminalInfo::TerminalInfo(const uint8_t *msg, size_t msg_len)
{
    if (msg_len < 1 || msg[0] != 1) {
        throw std::runtime_error("Invalid message header");
    }

    auto msg_end = msg + msg_len;
    ++msg;
    while (msg_end - msg >= 2) {
        uint8_t type = *msg++;
        uint8_t len = *msg++;
        if (msg_end - msg < len) {
            throw std::runtime_error("Invalid field header");
        }

        switch (type) {
        case 1:
            display = std::string((const char*) msg, len);
            break;
        case 2:
            xauthority = std::string((const char*) msg, len);
            break;
        default:
            throw std::runtime_error("Invalid field");
        }

        msg += len;
    }
    if (msg != msg_end) {
        throw std::runtime_error("Message not terminated correctly");
    }
}

static void api_err(const char *msg)
{
    syslog(LOG_ERR, "%s: %m", msg);
    exit(1);
}

/**
 * Get terminal number from PID.
 * This function is supposed to be quite low level.
 * We don't log any error to avoid possibles DoS.
 * The caller can use errno.
 * @returns terminal or -1 if error
 */
static int get_terminal(pid_t pid)
{
    char fn[128];
    sprintf(fn, "/proc/%u/stat", pid);

    // use C style FILE, the kernel document this file syntax using
    // scanf format specification
    FILE *f = fopen(fn, "re");
    if (!f) {
        return -1;
    }

    char line[1024*2];
    if (fgets(line, sizeof(line), f) == NULL) {
        fclose(f);
        return -1;
    }
    fclose(f);

    int terminal = -1, tty = -1;
    const char *end_exename = strstr(line, ") ");
    if (end_exename && sscanf(end_exename+2, "%*c %*d %*d %*d %d", &tty) > 0) {
        // check tty is a physical one (just looks at major/minor)
        int major = tty >> 8;
        int minor = tty & 0xff;
        if (major == 4 && minor > 0 && minor < 64) {
            terminal = minor;
        }
    }
    return terminal;
}

static bool check_xauthority(const std::string& fn, uid_t uid)
{
    // TODO timeout on check, could have passed a weird NFS
    // impersonate uid
    // file must be present
    // file must be small
    // read file
    // check for keys (memmem)
    //   MIT-MAGIC-COOKIE-1
    //   XDM-AUTHORIZATION-1
    //   MIT-KERBEROS-5
    //   SUN-DES-1
    return true;
}

/**
 * Check if a given file descriptor is the daemon socket we should
 * accept requests from.
 * In case the daemon is launched form inetd or systemd the file
 * descriptor is inherited from the parent.
 */
static bool fd_is_agent(int fd)
{
    // must be a socket
    struct stat st;
    if (fstat(fd, &st) != 0) {
        return false;
    }
    if ((st.st_mode & S_IFMT) != S_IFSOCK) {
        return false;
    }

    // must have our address
    struct sockaddr_un address;
    socklen_t len = sizeof(address);
    if (getsockname(fd, (sockaddr *) &address, &len) != 0) {
        return false;
    }
    if (address.sun_family != AF_UNIX) {
        return false;
    }
    if (address.sun_path[0] != 0) {
        return false;
    }
    address.sun_path[0] = '@';
    if (len != SUN_LEN(&address) || strcmp(address.sun_path, daemon_socket_name) != 0) {
        return false;
    }

    // TODO must be in listening, but this is mainly a paranoia check,
    // the file descriptor can come only from a trusted source

    return true;
}

class Daemon
{
public:
    Daemon(const char *stream_port_name);
    ~Daemon();
    void loop(int &streamfd);
private:
    void remove_client(unsigned n);
    void data_from_client(unsigned n);
    void handle_new_fd(int fd);
    void handle_fd_events(unsigned n, unsigned events);
    bool check_agent(int &streamfd);

    enum { max_clients = 63 };
    enum {
        LISTEN_FD,
        WAKEUP_FD,
        VT_FD,
        FIXED_FDS
    };
    spice::streaming_agent::EventFD wakeup_loop;
    struct pollfd fds[max_clients+FIXED_FDS];
    struct pollfd * const client_fds = fds+FIXED_FDS;
    unsigned num_clients = 0;
    pid_t child_pid = -1;
    int working_tty = -1;
    const char *stream_port_name;

    std::map<int, TerminalInfo> terminal_info;

    static void handle_sigchild(int);
    static Daemon *current;
};

Daemon *Daemon::current = nullptr;

Daemon::Daemon(const char *stream_port_name):
    stream_port_name(stream_port_name)
{
    int ret;

    fds[WAKEUP_FD].fd = wakeup_loop.raw_fd();
    fds[WAKEUP_FD].events = POLLIN;

    int fd;
    if (fd_is_agent(0)) {
        // inetd style socket passed
        fd = 0;
    } else if (fd_is_agent(SD_LISTEN_FDS_START)) {
        // systemd style socket passed
        fd = SD_LISTEN_FDS_START;
    } else {
        // open socket
        fd = socket(PF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0);
        if (fd < 0) {
            api_err("Unable to open daemon socket");
        }

        struct sockaddr_un address;
        memset(&address, 0, sizeof(address));
        address.sun_family = AF_UNIX;
        strcpy(address.sun_path, daemon_socket_name);
        int len = SUN_LEN(&address);
        address.sun_path[0] = 0;
        ret = bind(fd, (struct sockaddr *)&address, len);
        if (ret < 0) {
            // TODO remove fd leak
            api_err("Unable to bind daemon socket");
        }
        // listen to socket
        ret = listen(fd, 5);
        if (ret < 0) {
            // TODO remove fd leak
            api_err("Unable to listen to daemon socket");
        }
    }

    fds[LISTEN_FD].fd = fd;

    // detect TTY changes
    fd = open("/sys/class/tty/tty0/active", O_RDONLY|O_CLOEXEC);
    if (fd < 0) {
        api_err("Unable to open TTY change file");
    }
    fds[VT_FD].fd = fd;
    fds[VT_FD].events = POLLPRI;
}

Daemon::~Daemon()
{
    // close file descriptors
    // this is executed also on the main streaming agent
    for (unsigned n = 0; n < num_clients+FIXED_FDS; ++n) {
        if (n == WAKEUP_FD) {
            continue;
        }
        close(fds[n].fd);
    }
}

void Daemon::remove_client(unsigned n)
{
    close(client_fds[n].fd);
    client_fds[n].fd = -1;
    if (n != num_clients-1) {
        client_fds[n] = client_fds[num_clients-1];
    }
    --num_clients;
}

// check message, should contain a DISPLAY and XAUTHORITY
// callback when data are received
void Daemon::data_from_client(unsigned n)
{
    const int fd = client_fds[n].fd;

    // get message, the protocol specify a single message
    char msg_buffer[1024*4];
    iovec iov[1];
    iov[0].iov_base = msg_buffer;
    iov[0].iov_len = sizeof(msg_buffer);
    msghdr msg;
    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;
    ssize_t msg_len = recvmsg(fd, &msg, MSG_CMSG_CLOEXEC|MSG_DONTWAIT);
    if (msg_len < 0 && errno == EAGAIN) {
        return;
    }
    if (msg_len < 0 && errno == EWOULDBLOCK) {
        return;
    }
    if (msg_len <= 0) {
        remove_client(n);
        return;
    }

    // get credentials (uid+pid)
    // the uid is used to be able to run the streaming agent with
    // correct user so we should get it from a secure source
    ucred cred;
    socklen_t cred_length = sizeof(cred);
    if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &cred_length) < 0) {
        remove_client(n);
        return;
    }

    // get tty terminal from process
    // Don't trust too much the helper sending information, the
    // terminal cannot be changed easily from normal accounts, get
    // this information directly from the kernel
    int num_terminal = get_terminal(cred.pid);

    // send an ack to the helper, we got all the information
    // The helper can now terminate
    remove_client(n);

    if (num_terminal < 0) {
        return;
    }

    // parse message, should contain data and credentials
    TerminalInfo info;
    try {
        info = TerminalInfo((const uint8_t *) msg_buffer, msg_len);
    }
    catch (std::runtime_error& err) {
        // avoid DoS on the logs just ignoring the error
        return;
    }

    // check Xauthority using the uid passed
    // TODO check only name?
    // TODO check just on child to avoid possible DoS
    if (!check_xauthority(info.xauthority, cred.uid)) {
        return;
    }

    // set final informations
    info.uid = cred.uid;
    terminal_info[num_terminal] = info;
}

void Daemon::handle_new_fd(int fd)
{
    // limit number of clients
    if (num_clients >= max_clients) {
        close(fd);
        return;
    }

    // append to loop handlers
    client_fds[num_clients].fd = fd;
    client_fds[num_clients].events = POLLIN;
    client_fds[num_clients].revents = 0;
    ++num_clients;

    // TODO timeout for data
}

void Daemon::handle_fd_events(unsigned n, unsigned events)
{
    if (events == POLLIN) {
        data_from_client(n);
        return;
    }
    // delete client if other events
    if (events) {
        remove_client(n);
    }
}

static int current_tty = -1;

static void handle_vt_change(int fd)
{
    char vt_name[128];
    for (;;) {
        auto len = read(fd, vt_name, sizeof(vt_name));
        if (len < 0 && errno == EINTR) {
            continue;
        }
        if (len < 0) {
            // TODO error
            return;
        }

        unsigned tty_num;
        if (sscanf(vt_name, "tty%u", &tty_num) == 1 && tty_num < 64) {
            current_tty = tty_num;
        }
        lseek(fd, 0, SEEK_SET);
        break;
    }
}

bool Daemon::check_agent(int &streamfd)
{
    syslog(LOG_DEBUG, "tty working %d current %d", working_tty, current_tty);

    pid_t pid;
    int status;
    while ((pid=waitpid(-1, &status, WNOHANG)) != -1 && pid != 0) {
        if (pid == child_pid) {
            child_pid = -1;
        }
    }

    // try to open streamfd if not already opened
    if (child_pid == -1 && streamfd < 0) {
        // open device as soon as possible to make sure is not busy
        streamfd = open(stream_port_name, O_RDWR|O_CLOEXEC);
        if (streamfd < 0) {
            api_err("Unable to open streaming device");
        }
    }

    // TODO kill the child if it doesn't exit after a given timeout?

    // TODO execute this part also on main loop
    // when should we try again ?
    if (working_tty == current_tty && child_pid != -1) {
        return false;
    }

    syslog(LOG_DEBUG, "trace %d", __LINE__);

#if 0
    // TODO check that tty we are going to use is still a graphic
    // terminal
     int arg;

          /* To be used as the fd in ioctl(). */
	  if ((fd = open("/dev/console", O_NOCTTY)) == ERROR) {
	     perror("open");
	     exit(ERROR);
	  }

	  printf("w00w00!\n\n");

          if ((ioctl(fd, KDGETMODE, &arg)) == ERROR) {
	     perror("ioctl");
	     close(fd);
	     exit(ERROR);
	  }
#endif

    // can we handle a new TTY ?
    if (terminal_info.find(current_tty) == terminal_info.end()) {
        return false;
    }

    // TODO check if the terminal is still valid and a graphic one

    syslog(LOG_DEBUG, "trace %d child pid %d", __LINE__, (int) child_pid);
    // TODO who clear TTY when reset ?
    // TODO switch to text ?

    if (child_pid != -1) {
        return false;
    }

    syslog(LOG_DEBUG, "trace %d", __LINE__);
    // save pid, manage only one agent
    child_pid = fork();
    switch (child_pid) {
    case -1:
        // TODO
        return false;
    case 0:
        // child
        child_pid = -1;
        break;
    default:
        // parent
        close(streamfd);
        streamfd = -1;
        return false;
    }

    // we are the child here, we return to do the stream work

    // we don't want to outlive the daemon process
    prctl(PR_SET_PDEATHSIG, SIGKILL);

    // TODO handle cases where the child die launching it again
    uid_t uid = terminal_info[current_tty].uid;
    syslog(LOG_DEBUG, "trace %d uid %d", __LINE__, (int) uid);
    passwd *pw = getpwuid(uid);
    if (!pw) {
        api_err("Unable to retrieve user information");
    }
    if (setgid(pw->pw_gid) != 0) {
        api_err("Unable to set group");
    }
    if (initgroups(pw->pw_name, pw->pw_gid) != 0) {
        api_err("Unable to set group list");
    }
    if (setuid(uid) != 0) {
        api_err("Unable to set user");
    }

    setenv("DISPLAY", terminal_info[current_tty].display.c_str(), 1);
    setenv("XAUTHORITY", terminal_info[current_tty].xauthority.c_str(), 1);

    // TODO check if the VM is running on a text terminal we don't
    // keep spawning new childs

    // TODO why on the child? this is doing nothing!
    working_tty = current_tty;
    return true;
}

void Daemon::loop(int &streamfd)
{
    // ignore pipe, prevent signal handling data from file descriptors
    signal(SIGPIPE, SIG_IGN);
    current = this;
    signal(SIGCHLD, handle_sigchild);

    // poll for new events
    while (true) {
        // check if we need to execute the agent
        // this should be done here so if poll get a EINTR
        // for a closed child we check again
        if (check_agent(streamfd)) {
            break;
        }

        // limit clients
        if (num_clients >= max_clients) {
            fds[0].events = 0;
        } else {
            fds[0].events = POLLIN;
        }
        if (poll(fds, num_clients+FIXED_FDS, -1) < 0) {
            // TODO errors
            continue;
        }
        wakeup_loop.ack();
        if ((fds[LISTEN_FD].revents & POLLIN) != 0) {
            // accept
            int new_fd = accept(fds[LISTEN_FD].fd, NULL, NULL);
            if (new_fd < 0) {
                continue;
            }
            handle_new_fd(new_fd);
        }
        if ((fds[VT_FD].revents & POLLPRI) != 0) {
            handle_vt_change(fds[VT_FD].fd);
        }
        for (unsigned n = num_clients; n-- > 0; ) {
            if (client_fds[n].revents) {
                handle_fd_events(n, client_fds[n].revents);
            }
        }
    }

    signal(SIGCHLD, SIG_DFL);
    signal(SIGPIPE, SIG_DFL);
    current = nullptr;
}

void Daemon::handle_sigchild(int)
{
    current->wakeup_loop.signal();
}

void run_daemon(const char *stream_port_name, int &streamfd)
{
    streamfd = -1;

    Daemon daemon(stream_port_name);
    daemon.loop(streamfd);
}