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