ynetd

Small server for binding programs to tcp ports
git clone https://git.sinitax.com/yx7/ynetd
Log | Files | Refs | sfeed.txt

ynetd.c (10404B)


      1/*
      2 *
      3 * Copyright (c) 2015-2024 Lorenz Panny
      4 *
      5 * This is ynetd version 2024.02.17.
      6 * Check for newer versions at https://yx7.cc/code.
      7 * Please report bugs to lorenz@yx7.cc.
      8 *
      9 * This program is released under the MIT license; see license.txt.
     10 *
     11 */
     12
     13#define _GNU_SOURCE
     14#include <stdlib.h>
     15#include <stdio.h>
     16#include <string.h>
     17#include <stdbool.h>
     18#include <time.h>
     19
     20#include <unistd.h>
     21#include <pwd.h>
     22#include <grp.h>
     23#include <signal.h>
     24#include <sys/socket.h>
     25#include <netinet/in.h>
     26#include <arpa/inet.h>
     27#include <sys/wait.h>
     28#include <sys/resource.h>
     29
     30__attribute__((noreturn)) void version()
     31{
     32    printf("This is ynetd version 2024.02.17.\n");
     33    exit(0);
     34}
     35
     36__attribute__((noreturn)) void help(int st)
     37{
     38    bool tty = isatty(fileno(stdout));
     39
     40    printf("\n");
     41    printf("    %synetd: a minimalistic inetd%s\n",
     42            tty ? "\x1b[32m" : "", tty ? "\x1b[0m" : "");
     43    printf("    ---------------------------\n\n");
     44    printf("    %sinvocation:%s ynetd [$opts] $cmd\n\n",
     45            tty ? "\x1b[33m" : "", tty ? "\x1b[0m" : "");
     46
     47    printf("    %sflags:%s\n",
     48            tty ? "\x1b[33m" : "", tty ? "\x1b[0m" : "");
     49    printf("-h              "
     50            "this help text\n");
     51    printf("-a $addr        "
     52            "IP address to bind to (default :: and 0.0.0.0)\n");
     53    printf("-p $port        "
     54            "TCP port to bind to (default 1024)\n");
     55    printf("-u $user        "
     56            "username (default current)\n");
     57    printf("-d $dir         "
     58            "working directory (default user's home if -u else current)\n");
     59    printf("-sh [yn]        "
     60            "invoke /bin/sh to execute $cmd? (default y)\n");
     61    printf("-si [yn]        "
     62            "use socket as stdin? (default y)\n");
     63    printf("-so [yn]        "
     64            "use socket as stdout? (default y)\n");
     65    printf("-se [yn]        "
     66            "use socket as stderr? (default n)\n");
     67    printf("-lt $lim        "
     68            "limit cpu time in seconds (default unchanged)\n");
     69    printf("-lm $lim        "
     70            "limit amount of memory in bytes (default unchanged)\n");
     71    printf("-lp $lim        "
     72            "limit number of processes (default unchanged)\n");
     73    printf("-rn $val        "
     74            "set process priority (default unchanged)\n");
     75    printf("$cmd            "
     76            "command\n");
     77    printf("                NOTE: $cmd is executed relative to $dir!\n");
     78    printf("                      if in doubt, use absolute paths only.\n");
     79    printf("\n");
     80    exit(st);
     81}
     82
     83#define die(S) do { perror(S); exit(-1); } while (0)
     84
     85struct config {
     86    struct {
     87        bool set;
     88        uid_t uid;
     89        gid_t gid;
     90    } ids;
     91
     92    int family;
     93    union {
     94        struct in6_addr ipv6;
     95        struct in_addr ipv4;
     96    } addr;
     97    in_port_t port;
     98
     99    char *cmd;
    100    char *dir;
    101    bool shell;
    102    bool in, out, err;
    103    struct {
    104        bool set;
    105        rlim_t lim;
    106    } cpu, mem, proc;
    107    struct {
    108        bool set;
    109        int val;
    110    } nice;
    111};
    112
    113void parse_args(size_t argc, char **argv, struct config *cfg)
    114{
    115    struct passwd spw, *pw;
    116    char pwbuf[0x100];
    117
    118    /* note: to avoid copying all the strings from argv[] to cfg,
    119     * we only write pointers to the arguments into cfg. since only
    120     * main() calls this function, these references are guaranteed
    121     * to stay valid for the lifetime of the program. */
    122
    123#define ARG_YESNO(S, L, V) \
    124    else if (!strcmp(argv[i], (S)) || !strcmp(argv[i], (L))) { \
    125        if (++i >= argc) \
    126            help(1); \
    127        if (argv[i][1] || (*argv[i] != 'y' && *argv[i] != 'n')) \
    128            help(1); \
    129        (V) = *argv[i++] == 'y'; \
    130    }
    131
    132#define ARG_NUM(S, L, V, P) \
    133    else if (!strcmp(argv[i], (S)) || !strcmp(argv[i], (L))) { \
    134        if (++i >= argc) \
    135            help(1); \
    136        (V) = strtol(argv[i++], NULL, 10); \
    137        if (P) \
    138            * (bool *) (P) = true; \
    139    }
    140
    141    for (size_t i = 1; i < argc; ) {
    142        if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
    143            help(0);
    144        }
    145        else if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--version")) {
    146            version();
    147        }
    148        ARG_YESNO("-sh", "--shell", cfg->shell)
    149        ARG_YESNO("-si", "--stdin", cfg->in)
    150        ARG_YESNO("-so", "--stdout", cfg->out)
    151        ARG_YESNO("-se", "--stderr", cfg->err)
    152        else if (!strcmp(argv[i], "-a") || !strcmp(argv[i], "--addr")) {
    153            if (++i >= argc)
    154                help(1);
    155            if (1 == inet_pton(AF_INET6, argv[i], &cfg->addr.ipv6))
    156                cfg->family = AF_INET6;
    157            else if (1 == inet_pton(AF_INET, argv[i], &cfg->addr.ipv4))
    158                cfg->family = AF_INET;
    159            else
    160                die("inet_pton");
    161            ++i;
    162        }
    163        ARG_NUM("-p", "--port", cfg->port, NULL)
    164        else if (!strcmp(argv[i], "-u") || !strcmp(argv[i], "--user")) {
    165            if (++i >= argc)
    166                help(1);
    167            if (getpwnam_r(argv[i++], &spw, pwbuf, sizeof(pwbuf), &pw) || !pw)
    168                die("getpwnam_r");
    169            cfg->ids.uid = pw->pw_uid;
    170            cfg->ids.gid = pw->pw_gid;
    171            cfg->ids.set = true;
    172            if (!cfg->dir) {
    173                /* note: pw->pw_dir is local, so we need to copy it. */
    174                /* note: ideally we should free() this, but it will
    175                 * exist until the parent dies anyway. */
    176                cfg->dir = strdup(pw->pw_dir);
    177            }
    178        }
    179        else if (!strcmp(argv[i], "-d") || !strcmp(argv[i], "--dir")) {
    180            if (++i >= argc)
    181                help(1);
    182            cfg->dir = argv[i++];
    183        }
    184        ARG_NUM("-lt", "--limit-time", cfg->cpu.lim, &cfg->cpu.set)
    185        ARG_NUM("-lm", "--limit-memory", cfg->mem.lim, &cfg->mem.set)
    186        ARG_NUM("-lp", "--limit-processes", cfg->proc.lim, &cfg->proc.set)
    187        ARG_NUM("-rn", "--renice", cfg->nice.val, &cfg->nice.set)
    188        else if (!cfg->cmd) {
    189            cfg->cmd = argv[i++];
    190        }
    191        else {
    192            help(1);
    193        }
    194    }
    195
    196#undef ARG_YESNO
    197#undef ARG_NUM
    198
    199    if (!cfg->cmd)
    200        help(1);
    201}
    202
    203int bind_listen(struct config const cfg)
    204{
    205    int const one = 1;
    206    int lsock;
    207    union {
    208       struct sockaddr_in6 ipv6;
    209       struct sockaddr_in ipv4;
    210    } addr = {0};
    211    socklen_t addr_len;
    212
    213    if (0 > (lsock = socket(cfg.family, SOCK_STREAM, 0)))
    214        die("socket");
    215
    216    if (setsockopt(lsock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)))
    217        die("setsockopt");
    218
    219    switch (cfg.family) {
    220    case AF_INET6:
    221        addr.ipv6.sin6_family = cfg.family;
    222        addr.ipv6.sin6_addr = cfg.addr.ipv6;
    223        addr.ipv6.sin6_port = htons(cfg.port);
    224        addr_len = sizeof(addr.ipv6);
    225        break;
    226    case AF_INET:
    227        addr.ipv4.sin_family = cfg.family;
    228        addr.ipv4.sin_addr = cfg.addr.ipv4;
    229        addr.ipv4.sin_port = htons(cfg.port);
    230        addr_len = sizeof(addr.ipv4);
    231        break;
    232    default:
    233        fprintf(stderr, "bad address family?!\n");
    234        exit(-1);
    235    }
    236
    237    if (bind(lsock, (struct sockaddr *) &addr, addr_len))
    238        die("bind");
    239
    240    if (listen(lsock, 16))
    241        die("listen");
    242
    243    return lsock;
    244}
    245
    246void handle_connection(struct config const cfg, int sock)
    247{
    248    struct rlimit rlim;
    249
    250    /* set resource limits */
    251    if (cfg.cpu.set) {
    252        rlim.rlim_cur = rlim.rlim_max = cfg.cpu.lim;
    253        if (0 > setrlimit(RLIMIT_CPU, &rlim))
    254            die("setrlimit");
    255    }
    256    if (cfg.mem.set) {
    257        rlim.rlim_cur = rlim.rlim_max = cfg.mem.lim;
    258#ifndef RLIMIT_AS
    259        if (0 > setrlimit(RLIMIT_DATA, &rlim))
    260#else
    261        if (0 > setrlimit(RLIMIT_AS, &rlim))
    262#endif
    263            die("setrlimit");
    264    }
    265    if (cfg.proc.set) {
    266        rlim.rlim_cur = rlim.rlim_max = cfg.proc.lim;
    267        if (0 > setrlimit(RLIMIT_NPROC, &rlim))
    268            die("setrlimit");
    269    }
    270
    271    /* renice */
    272    if (cfg.nice.set && setpriority(PRIO_PROCESS, 0, cfg.nice.val))
    273        die("setpriority");
    274
    275    /* drop privileges */
    276    if (cfg.ids.set) {
    277        if (setgroups(0, NULL))
    278            die("setgroups");
    279        if (setgid(cfg.ids.gid))
    280            die("setgid");
    281        if (setuid(cfg.ids.uid))
    282            die("setuid");
    283    }
    284
    285    /* change working directory */
    286    if (cfg.dir && chdir(cfg.dir))
    287        die("chdir");
    288
    289    /* duplicate socket to stdio */
    290    if (cfg.in && fileno(stdin) != dup2(sock, fileno(stdin)))
    291        die("dup2");
    292    if (cfg.out && fileno(stdout) != dup2(sock, fileno(stdout)))
    293        die("dup2");
    294    if (cfg.err && fileno(stderr) != dup2(sock, fileno(stderr)))
    295        die("dup2");
    296    if (close(sock))
    297        die("close");
    298
    299    /* FIXME does nobody care about the environment? */
    300
    301    /* execute command */
    302    if (cfg.shell) {
    303        execle("/bin/sh", "sh", "-c", cfg.cmd, NULL, NULL);
    304        die("execle");
    305    }
    306    else {
    307        /* FIXME support more arguments? */
    308        execle(cfg.cmd, cfg.cmd, NULL, NULL);
    309        die("execle");
    310    }
    311}
    312
    313int main(int argc, char **argv)
    314{
    315    pid_t pid;
    316    int lsock, sock;
    317    struct sigaction sigact;
    318
    319    /* configuration options */
    320    struct config cfg = {
    321        .ids = {.set = false},
    322
    323        .family = AF_INET6,
    324        .addr = {.ipv6 = in6addr_any},
    325        .port = 1024,
    326
    327        .cmd = NULL,
    328        .dir = NULL,
    329        .shell = true,
    330        .in = true, .out = true, .err = false,
    331        .cpu = {.set = false}, .mem = {.set = false}, .proc = {.set = false},
    332        .nice = {.set = false},
    333    };
    334
    335    /* "parse" arguments */
    336    parse_args(argc, argv, &cfg);
    337
    338    /* do not turn dead children into zombies */
    339    memset(&sigact, 0, sizeof(sigact));
    340    sigact.sa_flags = SA_NOCLDWAIT | SA_NOCLDSTOP;
    341    if (sigaction(SIGCHLD, &sigact, 0))
    342        die("sigaction");
    343
    344    /* set up listening socket */
    345    lsock = bind_listen(cfg);
    346
    347    /* accept loop */
    348    while (1) {
    349
    350        if (0 > (sock = accept(lsock, NULL, NULL)))
    351            continue;
    352
    353        if ((pid = fork())) {
    354            /* parent */
    355            /* note: if the fork failed, we just drop the connection
    356             * and continue as usual, so we don't catch that case. */
    357            if (close(sock))
    358                die("close");
    359            continue;
    360        }
    361
    362        /* child */
    363        if (close(lsock))
    364            die("close");
    365
    366        /* detach from terminal */
    367        if (0 > setsid())
    368            die("setsid");
    369
    370        handle_connection(cfg, sock);
    371
    372    }
    373}
    374