From ea053d96f7e89e053d4af8d39b04c5428760345f Mon Sep 17 00:00:00 2001 From: David Härdeman Date: Tue, 23 Jun 2020 20:56:22 +0200 Subject: Big renaming, move some more functionality to shared lib --- minecproxy/main.c | 741 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 741 insertions(+) create mode 100644 minecproxy/main.c (limited to 'minecproxy/main.c') diff --git a/minecproxy/main.c b/minecproxy/main.c new file mode 100644 index 0000000..bbe3fad --- /dev/null +++ b/minecproxy/main.c @@ -0,0 +1,741 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "main.h" +#include "signal-handler.h" +#include "uring.h" +#include "config-parser.h" +#include "server.h" +#include "server-config.h" +#include "announce.h" +#include "systemd.h" +#include "igmp.h" +#include "idle.h" +#include "ptimer.h" +#include + +/* 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; + +#define ANSI_RED "\x1B[0;31m" +#define ANSI_GREEN "\x1B[0;32m" +#define ANSI_YELLOW "\x1B[0;33m" +#define ANSI_BLUE "\x1B[0;34m" +#define ANSI_MAGENTA "\x1B[0;35m" +#define ANSI_GREY "\x1B[0;38;5;245m" +#define ANSI_NORMAL "\x1B[0m" + +static void +msg(enum debug_lvl lvl, const char *fmt, va_list ap) +{ + static bool first = true; + static bool use_colors = false; + static bool sd_daemon = false; + const char *color; + const char *sd_lvl; + + assert_return(lvl != 0 && !empty_str(fmt) && ap); + + while (first) { + int fd; + const char *e; + + first = false; + + /* assume we're not launched by systemd when daemonized */ + if (daemonize) { + sd_daemon = false; + use_colors = false; + break; + } + + if (log_file) { + sd_daemon = false; + use_colors = false; + break; + } + + if (getenv("NO_COLOR")) { + sd_daemon = false; + use_colors = false; + break; + } + + fd = fileno(stderr); + if (fd < 0) { + /* Umm... */ + sd_daemon = true; + use_colors = false; + break; + } + + if (!isatty(fd)) { + sd_daemon = true; + use_colors = false; + break; + } + + /* systemd wouldn't normally set TERM */ + e = getenv("TERM"); + if (!e) { + sd_daemon = true; + use_colors = false; + break; + } + + if (streq(e, "dumb")) { + sd_daemon = false; + use_colors = false; + break; + } + + sd_daemon = false; + use_colors = true; + } + + switch (lvl) { + case DBG_ERROR: + sd_lvl = SD_ERR; + color = use_colors ? ANSI_RED : NULL; + 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 = use_colors ? ANSI_GREY : NULL; + break; + } + + if (sd_daemon) + fprintf(stderr, sd_lvl); + else if (color) + fprintf(stderr, color); + + vfprintf(log_file ? log_file : stderr, fmt, ap); + + if (color) + fprintf(stderr, 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); +} + +__attribute__((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(cfg); + xfree(cfg->igmp_iface); + cfg->igmp_iface = NULL; + exiting = true; + /* The cfg struct is free:d in main() */ +} + +enum mcfg_keys { + MCFG_KEY_INVALID = 0, + MCFG_KEY_IGMP, + MCFG_KEY_IGMP_IFACE, + MCFG_KEY_ANN_INTERVAL, + MCFG_KEY_PROXY_CONN_INTERVAL, + MCFG_KEY_PROXY_CONN_ATTEMPTS, + MCFG_KEY_SOCKET_DEFER, + MCFG_KEY_SOCKET_FREEBIND, + MCFG_KEY_SOCKET_KEEPALIVE, + MCFG_KEY_SOCKET_IPTOS, + MCFG_KEY_SOCKET_NODELAY, +}; + +struct cfg_key_value_map mcfg_key_map[] = { + { + .key_name = "igmp", + .key_value = MCFG_KEY_IGMP, + .value_type = CFG_VAL_TYPE_BOOL, + }, { + .key_name = "igmp_iface", + .key_value = MCFG_KEY_IGMP_IFACE, + .value_type = CFG_VAL_TYPE_STRING, + }, { + .key_name = "announce_interval", + .key_value = MCFG_KEY_ANN_INTERVAL, + .value_type = CFG_VAL_TYPE_UINT16, + }, { + .key_name = "proxy_connection_interval", + .key_value = MCFG_KEY_PROXY_CONN_INTERVAL, + .value_type = CFG_VAL_TYPE_UINT16, + }, { + .key_name = "proxy_connection_attempts", + .key_value = MCFG_KEY_PROXY_CONN_ATTEMPTS, + .value_type = CFG_VAL_TYPE_UINT16, + }, { + .key_name = "socket_defer", + .key_value = MCFG_KEY_SOCKET_DEFER, + .value_type = CFG_VAL_TYPE_BOOL, + }, { + .key_name = "socket_freebind", + .key_value = MCFG_KEY_SOCKET_FREEBIND, + .value_type = CFG_VAL_TYPE_BOOL, + }, { + .key_name = "socket_keepalive", + .key_value = MCFG_KEY_SOCKET_KEEPALIVE, + .value_type = CFG_VAL_TYPE_BOOL, + }, { + .key_name = "socket_iptos", + .key_value = MCFG_KEY_SOCKET_IPTOS, + .value_type = CFG_VAL_TYPE_BOOL, + }, { + .key_name = "socket_nodelay", + .key_value = MCFG_KEY_SOCKET_NODELAY, + .value_type = CFG_VAL_TYPE_BOOL, + }, { + .key_name = NULL, + .key_value = MCFG_KEY_INVALID, + .value_type = CFG_VAL_TYPE_INVALID, + } +}; + +static void +cfg_read() +{ + FILE *cfgfile; + const char *path; + char buf[4096]; + char *pos = buf; + size_t rd = 0; + size_t r; + + assert_return(cfg); + + if (cfg->cfg_file) + path = cfg->cfg_file; + else + path = DEFAULT_MAIN_CFG_FILE; + + cfgfile = fopen(path, "re"); + if (!cfgfile) { + /* ENOENT is only an error with an explicitly set path */ + if (errno == ENOENT && !cfg->cfg_file) + return; + else if (errno == ENOENT) + die("main config file (%s) missing", path); + else + die("fopen(%s): %m", path); + } + + debug(DBG_CFG, "opened main config file (%s)", path); + + while (rd < sizeof(buf)) { + r = fread(pos, 1, sizeof(buf) - rd - 1, cfgfile); + if (r == 0) + break; + rd += r; + pos += r; + } + + if (rd == 0) + die("main config file (%s) zero size", path); + + if (rd >= sizeof(buf)) + die("main config file (%s) too large", path); + + fclose(cfgfile); + *pos = '\0'; + pos = buf; + + if (!config_parse_header(path, "mcproxy", &pos)) + die("main config file (%s) invalid", path); + + while (true) { + int key; + const char *keyname; + struct cfg_value value; + + if (!config_parse_line(path, &pos, mcfg_key_map, + &key, &keyname, &value)) + break; + + if (key == MCFG_KEY_INVALID) + die("main config file (%s) invalid", path); + + 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_ANN_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 (%s) invalid", path); + } + } +} + +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, + } +}; + +__attribute__((noreturn)) static void +usage(int argc, char **argv, bool invalid) +{ + if (invalid) + info("Invalid option(s)"); + + info("Usage: %s [OPTIONS]\n" + "\n" + "Valid options:\n" + " -c, --cfgdir=DIR\tlook for configuration files in DIR\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", + argv ? argv[0] : "mcproxy"); + + 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); + list_init(&cfg->servers); + + cfg->cfg_dir = DEFAULT_CFG_DIR; + 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; + static struct option long_options[] = { + { "cfgdir", required_argument, 0, 'c' }, + { "cfgfile", 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 } + }; + + c = getopt_long(argc, argv, ":c:C:u:Dl:hvd:", + long_options, &option_index); + if (c == -1) + break; + + switch (c) { + case 'c': + cfg->cfg_dir = optarg; + break; + + case 'C': + cfg->cfg_file = 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(argc, argv, true); + + debug_mask |= DBG_VERBOSE; + debug_mask |= debug_category_str[i].val; + break; + + case 'h': + usage(argc, argv, false); + + default: + usage(argc, argv, true); + } + + } + + if (optind < argc) + usage(argc, argv, 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. + */ + if (chdir(cfg->cfg_dir)) + die("chdir(%s): %m", cfg->cfg_dir); + + 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 4 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); +} -- cgit v1.2.3