summaryrefslogtreecommitdiff
path: root/minecproxy/minecproxy.c
diff options
context:
space:
mode:
Diffstat (limited to 'minecproxy/minecproxy.c')
-rw-r--r--minecproxy/minecproxy.c693
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);
+}