/* SPDX-License-Identifier: GPL-2.0 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "minecproxy.h" #include "signal-handler.h" #include "uring.h" #include "shared/config-parser.h" #include "server.h" #include "server-config.h" #include "announce.h" #include "shared/systemd.h" #include "igmp.h" #include "idle.h" #include "ptimer.h" #include "config.h" #include "minecproxy-config-options.h" /* Global */ struct cfg *cfg = NULL; bool exiting = false; /* Local */ static bool daemonize = false; static FILE *log_file = NULL; static const char *log_file_path = NULL; static void set_logging_type(bool *use_colors, bool *sd_daemon) { int fd; const char *e; /* assume we're not launched by systemd when daemonized */ if (daemonize) { *sd_daemon = false; *use_colors = false; return; } if (log_file) { *sd_daemon = false; *use_colors = false; return; } if (getenv("NO_COLOR")) { *sd_daemon = false; *use_colors = false; return; } fd = fileno(stderr); if (fd < 0) { /* Umm... */ *sd_daemon = true; *use_colors = false; return; } if (!isatty(fd)) { *sd_daemon = true; *use_colors = false; return; } /* systemd wouldn't normally set TERM */ e = getenv("TERM"); if (!e) { *sd_daemon = true; *use_colors = false; return; } if (streq(e, "dumb")) { *sd_daemon = false; *use_colors = false; return; } *sd_daemon = false; *use_colors = true; } static void msg(enum debug_lvl lvl, const char *fmt, va_list ap) { static bool first = true; static bool sd_daemon; const char *color; const char *sd_lvl; assert_return(lvl != 0 && !empty_str(fmt) && ap); if (first) { bool use_colors; set_logging_type(&use_colors, &sd_daemon); if (use_colors) enable_colors(); first = false; } switch (lvl) { case DBG_ERROR: sd_lvl = SD_ERR; color = ansi_red; break; case DBG_VERBOSE: sd_lvl = SD_INFO; color = NULL; break; case DBG_INFO: sd_lvl = SD_NOTICE; color = NULL; break; default: sd_lvl = SD_DEBUG; color = ansi_grey; break; } if (sd_daemon) fprintf(stderr, "%s", sd_lvl); else if (color) fprintf(stderr, "%s", color); vfprintf(log_file ? log_file : stderr, fmt, ap); if (color) fprintf(stderr, "%s", ansi_normal); } void __debug(enum debug_lvl lvl, const char *fmt, ...) { va_list ap; assert_return(lvl != 0 && !empty_str(fmt)); va_start(ap, fmt); msg(lvl, fmt, ap); va_end(ap); } _noreturn_ void __die(const char *fmt, ...) { va_list ap; if (!empty_str(fmt)) { va_start(ap, fmt); msg(DBG_ERROR, fmt, ap); va_end(ap); } else error("fmt not set"); sd_notifyf(0, "STATUS=Error, shutting down"); exit(EXIT_FAILURE); } static void cfg_free(struct uring_task *task) { struct cfg *xcfg = container_of(task, struct cfg, task); assert_return(task && xcfg == cfg); debug(DBG_SIG, "called"); systemd_delete(); xfree(cfg->igmp_iface); cfg->igmp_iface = NULL; xfree(cfg->data_real_path); cfg->data_real_path = NULL; xfree(cfg->cfg_real_path); cfg->cfg_real_path = NULL; if (cfg->data_dir) { closedir(cfg->data_dir); cfg->data_dir = NULL; } if (cfg->cfg_dir) { closedir(cfg->cfg_dir); cfg->cfg_dir = NULL; } exiting = true; /* The cfg struct is free:d in main() */ } static void cfg_read() { char buf[4096]; char *pos; size_t off; size_t r; unsigned lineno; _cleanup_close_ int fd = -1; _cleanup_fclose_ FILE *cfgfile = NULL; assert_return(cfg); fd = openat(dirfd(cfg->cfg_dir), MINECPROXY_CFG_FILE, O_RDONLY | O_CLOEXEC | O_NOCTTY); if (fd < 0) return; cfgfile = fdopen(fd, "re"); if (!cfgfile) return; fd = -1; debug(DBG_CFG, "opened main config file %s/%s", cfg->cfg_real_path, MINECPROXY_CFG_FILE); for (off = 0; off < sizeof(buf); off += r) { r = fread(buf + off, 1, sizeof(buf) - off, cfgfile); if (r == 0) break; } if (off >= sizeof(buf) - 1) die("main config file %s/%s too large", cfg->cfg_real_path, MINECPROXY_CFG_FILE); buf[off] = '\0'; pos = buf; if (!config_parse_header(MINECPROXY_CFG_HEADER, &pos, &lineno)) die("main config file %s/%s missing/invalid header", cfg->cfg_real_path, MINECPROXY_CFG_FILE); while (true) { int key; const char *keyname; struct cfg_value value; const char *error; if (!config_parse_line(MINECPROXY_CFG_FILE, &pos, mcfg_key_map, &key, &keyname, &value, false, &lineno, &error)) break; if (key == MCFG_KEY_INVALID) die("main config file %s/%s invalid: line %u: %s", cfg->cfg_real_path, MINECPROXY_CFG_FILE, lineno, error); debug(DBG_CFG, "main cfg: key %s", keyname); switch (key) { case MCFG_KEY_IGMP: cfg->do_igmp = value.boolean; break; case MCFG_KEY_IGMP_IFACE: cfg->igmp_iface = xstrdup(value.str); if (!cfg->igmp_iface) die("xstrdup: %m"); break; case MCFG_KEY_ANNOUNCE_INTERVAL: cfg->announce_interval = value.uint16; break; case MCFG_KEY_PROXY_CONN_INTERVAL: cfg->proxy_connection_interval = value.uint16; break; case MCFG_KEY_PROXY_CONN_ATTEMPTS: cfg->proxy_connection_attempts = value.uint16; break; case MCFG_KEY_SOCKET_DEFER: cfg->socket_defer = value.boolean; break; case MCFG_KEY_SOCKET_FREEBIND: cfg->socket_freebind = value.boolean; break; case MCFG_KEY_SOCKET_KEEPALIVE: cfg->socket_keepalive = value.boolean; break; case MCFG_KEY_SOCKET_IPTOS: cfg->socket_iptos = value.boolean; break; case MCFG_KEY_SOCKET_NODELAY: cfg->socket_nodelay = value.boolean; break; case MCFG_KEY_INVALID: default: die("main config file invalid"); } } } /* clang-format off */ const struct { const char *name; unsigned val; } debug_category_str[] = { { .name = "config", .val = DBG_CFG, },{ .name = "refcount", .val = DBG_REF, },{ .name = "malloc", .val = DBG_MALLOC, },{ .name = "announce", .val = DBG_ANN, },{ .name = "signal", .val = DBG_SIG, },{ .name = "uring", .val = DBG_UR, },{ .name = "server", .val = DBG_SRV, },{ .name = "proxy", .val = DBG_PROXY, },{ .name = "rcon", .val = DBG_RCON, },{ .name = "idle", .val = DBG_IDLE, },{ .name = "igmp", .val = DBG_IGMP, },{ .name = "systemd", .val = DBG_SYSD, },{ .name = "dns", .val = DBG_DNS, },{ .name = "timer", .val = DBG_TIMER, },{ .name = NULL, .val = 0, } }; /* clang-format on */ _noreturn_ static void usage(bool invalid) { if (invalid) info("Invalid option(s)"); info("Usage: %s [OPTIONS]\n" "\n" "Valid options:\n" " -c, --cfgdir=DIR\tuse DIR for configuration files\n" " -C, --datadir=DIR\tuse DIR for server data\n" " -u, --user=USER\trun as USER\n" " -D, --daemonize\trun in daemon mode (disables stderr output)\n" " -l, --logfile=FILE\tlog to FILE instead of stderr\n" " -h, --help\t\tprint this information\n" " -v, --verbose\t\tenable verbose logging\n" " -d, --debug=CATEGORY\tenable debugging for CATEGORY\n" "\t\t\t(use \"list\" to see available categories,\n" "\t\t\t or \"all\" to enable all categories)\n", program_invocation_short_name); exit(invalid ? EXIT_FAILURE : EXIT_SUCCESS); } static void cfg_init(int argc, char **argv) { int c; unsigned i; assert_die(argc > 0 && argv, "invalid arguments"); cfg = zmalloc(sizeof(*cfg)); if (!cfg) die("malloc: %m"); uring_task_init(&cfg->task, "main", NULL, cfg_free); INIT_LIST_HEAD(&cfg->servers); cfg->cfg_path = NULL; cfg->cfg_real_path = NULL; cfg->cfg_dir = NULL; cfg->data_path = NULL; cfg->data_real_path = NULL; cfg->data_dir = NULL; cfg->announce_interval = DEFAULT_ANNOUNCE_INTERVAL; cfg->proxy_connection_interval = DEFAULT_PROXY_CONN_INTERVAL; cfg->proxy_connection_attempts = DEFAULT_PROXY_CONN_ATTEMPTS; cfg->socket_defer = DEFAULT_SOCKET_DEFER; cfg->socket_freebind = DEFAULT_SOCKET_FREEBIND; cfg->socket_keepalive = DEFAULT_SOCKET_KEEPALIVE; cfg->socket_iptos = DEFAULT_SOCKET_IPTOS; cfg->socket_nodelay = DEFAULT_SOCKET_NODELAY; cfg->uid = geteuid(); cfg->gid = getegid(); while (true) { int option_index = 0; /* clang-format off */ static struct option long_options[] = { { "cfgdir", required_argument, 0, 'c' }, { "datadir", required_argument, 0, 'C' }, { "user", required_argument, 0, 'u' }, { "daemonize", no_argument, 0, 'D' }, { "logfile", required_argument, 0, 'l' }, { "help", no_argument, 0, 'h' }, { "verbose", no_argument, 0, 'v' }, { "debug", required_argument, 0, 'd' }, { 0, 0, 0, 0 } }; /* clang-format on */ c = getopt_long(argc, argv, ":c:C:u:Dl:hvd:", long_options, &option_index); if (c == -1) break; switch (c) { case 'c': cfg->cfg_path = optarg; break; case 'C': cfg->data_path = optarg; break; case 'v': debug_mask |= DBG_VERBOSE; break; case 'D': daemonize = true; break; case 'l': log_file_path = optarg; break; case 'u': { struct passwd *pwd; errno = 0; pwd = getpwnam(optarg); if (!pwd) { if (errno == 0) errno = ESRCH; if (errno == ESRCH) die("failed to find user %s", optarg); else die("failed to find user %s (%m)", optarg); } debug(DBG_CFG, "asked to execute with uid %ji gid %ji", (intmax_t)pwd->pw_uid, (intmax_t)pwd->pw_gid); cfg->uid = pwd->pw_uid; cfg->gid = pwd->pw_gid; break; } case 'd': if (strcaseeq(optarg, "all")) { debug_mask = ~0; break; } else if (strcaseeq(optarg, "list")) { info("Debug categories:"); info(" * all"); for (i = 0; debug_category_str[i].name; i++) info(" * %s", debug_category_str[i].name); exit(EXIT_FAILURE); } for (i = 0; debug_category_str[i].name; i++) { if (strcaseeq(optarg, debug_category_str[i].name)) break; } if (!debug_category_str[i].name) usage(true); debug_mask |= DBG_VERBOSE; debug_mask |= debug_category_str[i].val; break; case 'h': usage(false); default: usage(true); } } if (optind < argc) usage(true); } static void cfg_apply() { if (cfg->uid == 0 || cfg->gid == 0) /* This catches both -u root and running as root without -u */ die("Execution as root is not supported (use -u )"); capng_clear(CAPNG_SELECT_BOTH); if (capng_updatev(CAPNG_ADD, CAPNG_EFFECTIVE | CAPNG_PERMITTED, CAP_NET_RAW, CAP_NET_BIND_SERVICE, -1)) die("capng_updatev failed"); if (geteuid() != cfg->uid) { if (capng_change_id(cfg->uid, cfg->gid, CAPNG_DROP_SUPP_GRP | CAPNG_CLEAR_BOUNDING)) die("capng_change_id failed"); } else { /* * This can fail if any of the caps are lacking, but it'll * be re-checked later. */ capng_apply(CAPNG_SELECT_BOTH); setgroups(0, NULL); } if (daemonize) { if (daemon(1, 0) < 0) die("daemon() failed: %m"); } if (log_file_path) { log_file = fopen(log_file_path, "ae"); if (!log_file) die("fopen(%s) failed: %m", log_file_path); } /* * Do this after caps have been dropped to make sure we're not * accessing a directory we should have permissions to. */ cfg->cfg_dir = open_cfg_dir(cfg->cfg_path, &cfg->cfg_real_path); if (!cfg->cfg_dir) die("Unable to open configuration directory"); cfg->data_dir = open_data_dir(cfg->data_path, &cfg->data_real_path); if (!cfg->data_dir) die("Unable to open server directory"); if (fchdir(dirfd(cfg->data_dir))) die("Unable to chdir to server directory: %m"); if (debug_enabled(DBG_VERBOSE)) { char *wd; wd = get_current_dir_name(); verbose("Working directory: %s", wd ? wd : ""); free(wd); } } void dump_tree() { struct server *server; if (!debug_enabled(DBG_REF)) return; info("\n\n"); info("Dumping Tree"); info("============"); uring_task_refdump(&cfg->task); uring_refdump(); signal_refdump(); ptimer_refdump(); idle_refdump(); igmp_refdump(); announce_refdump(); server_cfg_monitor_refdump(); list_for_each_entry(server, &cfg->servers, list) server_refdump(server); info("============"); info("\n\n"); } int main(int argc, char **argv) { struct server *server; unsigned server_count; struct rlimit old_rlimit; debug_mask = DBG_ERROR | DBG_INFO; cfg_init(argc, argv); cfg_apply(); cfg_read(); /* * In the splice case we use 6 fds per proxy connection... */ if (prlimit(0, RLIMIT_NOFILE, NULL, &old_rlimit) == 0) { struct rlimit new_rlimit; new_rlimit.rlim_cur = old_rlimit.rlim_max; new_rlimit.rlim_max = old_rlimit.rlim_max; if (prlimit(0, RLIMIT_NOFILE, &new_rlimit, NULL) == 0) debug(DBG_MALLOC, "prlimit(NOFILE): %u/%u -> %u/%u", (unsigned)old_rlimit.rlim_cur, (unsigned)old_rlimit.rlim_max, (unsigned)new_rlimit.rlim_cur, (unsigned)new_rlimit.rlim_cur); } uring_init(); ptimer_init(); igmp_init(); /* Drop CAP_NET_RAW (if we have it), only used for igmp */ capng_clear(CAPNG_SELECT_BOTH); if (capng_update(CAPNG_ADD, CAPNG_EFFECTIVE | CAPNG_PERMITTED, CAP_NET_BIND_SERVICE)) die("capng_update failed"); if (capng_apply(CAPNG_SELECT_BOTH)) { /* Try clearing all caps, shouldn't fail */ capng_clear(CAPNG_SELECT_BOTH); if (capng_apply(CAPNG_SELECT_BOTH)) die("capng_apply failed"); } signal_init(); server_cfg_monitor_init(); announce_init(); if (!cfg->igmp) announce_start(0); idle_init(); uring_task_put(&cfg->task); server_count = 0; list_for_each_entry(server, &cfg->servers, list) server_count++; sd_notifyf(0, "READY=1\n" "STATUS=Running, %u server configurations loaded\n" "MAINPID=%lu", server_count, (unsigned long)getpid()); info("mcproxy (%s) started, %u server configurations loaded", VERSION, server_count); uring_event_loop(); verbose("Exiting"); xfree(cfg); cfg = NULL; if (debug_enabled(DBG_MALLOC)) debug_resource_usage(); fflush(stdout); fflush(stderr); exit(EXIT_SUCCESS); }