summaryrefslogtreecommitdiff
path: root/minecproxy/main.c
diff options
context:
space:
mode:
authorDavid Härdeman <david@hardeman.nu>2020-06-23 20:56:22 +0200
committerDavid Härdeman <david@hardeman.nu>2020-06-23 20:56:22 +0200
commitea053d96f7e89e053d4af8d39b04c5428760345f (patch)
tree8182ca73675ad3933b0f38cb48a99c69101309b4 /minecproxy/main.c
parent8c27290245b7bcc7cd2f72f3b4a7562294b43bbe (diff)
Big renaming, move some more functionality to shared lib
Diffstat (limited to 'minecproxy/main.c')
-rw-r--r--minecproxy/main.c741
1 files changed, 741 insertions, 0 deletions
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 <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 "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 <config.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;
+
+#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 <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.
+ */
+ 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 : "<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 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);
+}