diff options
Diffstat (limited to 'minecproxy/minecproxy.c')
-rw-r--r-- | minecproxy/minecproxy.c | 693 |
1 files changed, 693 insertions, 0 deletions
diff --git a/minecproxy/minecproxy.c b/minecproxy/minecproxy.c new file mode 100644 index 0000000..7448943 --- /dev/null +++ b/minecproxy/minecproxy.c @@ -0,0 +1,693 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#include <stdio.h> +#include <stdlib.h> +#include <stdarg.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <stdbool.h> +#include <getopt.h> +#include <systemd/sd-daemon.h> +#include <cap-ng.h> +#include <pwd.h> +#include <grp.h> +#include <sys/time.h> +#include <sys/resource.h> + +#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 <someuser>)"); + + 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 : "<unknown>"); + 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); +} |