diff options
-rw-r--r-- | examples/minecserver@.service | 37 | ||||
-rw-r--r-- | examples/server.properties | 10 | ||||
-rw-r--r-- | minecctl/minecctl.c | 6 | ||||
-rw-r--r-- | minecctl/minecctl.h | 6 | ||||
-rw-r--r-- | minecctl/misc-commands.c | 352 | ||||
-rw-r--r-- | shared/utils.h | 15 |
6 files changed, 341 insertions, 85 deletions
diff --git a/examples/minecserver@.service b/examples/minecserver@.service index e14ed81..bb72c5b 100644 --- a/examples/minecserver@.service +++ b/examples/minecserver@.service @@ -1 +1,36 @@ -This is an example service +[Unit] +Description=Minecraft Server %i +#Documentation= https://url https://url man:abc +Wants=network-online.target +After=network-online.target +ConditionFileNotEmpty=%E/minecproxy/config/%i.mcserver +ConditionPathExists=%E/minecproxy/servers/%i/eula.txt +ConditionPathExists=%E/minecproxy/servers/%i/server.properties +ConditionPathExists=%E/minecproxy/servers/%i/server.jar + +[Service] +Type=exec +ExecStart=/usr/bin/java -Xmx1024M -Xms1024M -jar server.jar --nogui +ExecStop=-/usr/bin/minecctl -c %E/minecproxy/config/ -f stop %i +# Optional: this will autogenerate new servers on the fly +# if this is used, comment out the ConditionPathExists checks above +#ExecStartPre=/usr/bin/minecctl new %i +TimeoutStopSec=120 +KillSignal=SIGCONT +Restart=on-failure +WorkingDirectory=%E/minecproxy/servers/%i +Nice=5 +LimitCORE=0 +NoNewPrivileges=true +KeyringMode=private +PrivateUsers=true +ProtectSystem=strict +ReadWritePaths=%E/minecproxy/servers/%i +PrivateDevices=true +ProtectKernelTunables=true +ProtectControlGroups=true +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +RestrictNamespaces=true +LockPersonality=true +RestrictSUIDSGID=true +PrivateTmp=true diff --git a/examples/server.properties b/examples/server.properties index f992c40..dddb380 100644 --- a/examples/server.properties +++ b/examples/server.properties @@ -1 +1,9 @@ -This is an example server.properties file +#Minecraft server properties +# This is a partial file, it will be replaced with a +# fleshed out version the first time the Minecraft +# server is executed. +motd=A Minecraft Server +server-port=25565 +enable-rcon=true +rcon.port=25575 +rcon.password=secret diff --git a/minecctl/minecctl.c b/minecctl/minecctl.c index 3b1da80..344d3d7 100644 --- a/minecctl/minecctl.c +++ b/minecctl/minecctl.c @@ -497,6 +497,12 @@ static void parse_cmdline(struct cfg *cfg, int argc, char *const *argv) int main(int argc, char *const *argv) { struct cfg cfg = { + .listen_port_min = 20000, + .listen_port_max = 23999, + .mc_port_min = 24000, + .mc_port_max = 27999, + .rcon_port_min = 28000, + .rcon_port_max = 31999, .servers = LIST_HEAD_INIT(cfg.servers), }; bool success = false; diff --git a/minecctl/minecctl.h b/minecctl/minecctl.h index 46c616a..5778d13 100644 --- a/minecctl/minecctl.h +++ b/minecctl/minecctl.h @@ -12,6 +12,12 @@ struct cfg { char *rcon_addrstr; char *mc_addrstr; char **commands; + uint16_t listen_port_min; + uint16_t listen_port_max; + uint16_t mc_port_min; + uint16_t mc_port_max; + uint16_t rcon_port_min; + uint16_t rcon_port_max; bool force_stop; bool default_set; bool server_list_loaded; diff --git a/minecctl/misc-commands.c b/minecctl/misc-commands.c index 34cbedf..f3ddff2 100644 --- a/minecctl/misc-commands.c +++ b/minecctl/misc-commands.c @@ -7,6 +7,8 @@ #include <fcntl.h> #include <string.h> #include <errno.h> +#include <sys/random.h> +#include <inttypes.h> #include "shared/utils.h" #include "minecctl.h" @@ -35,10 +37,11 @@ static bool create_link(int dfd, const char *source, const char *target) return true; } -static bool write_cfg_file(int dfd, const char *name, unsigned char *content, +static bool write_cfg_file(int dfd, const char *name, + const unsigned char *content, size_t len) { - int fd; + _cleanup_close_ int fd; ssize_t done = 0, r; bool rv = true; @@ -61,13 +64,12 @@ static bool write_cfg_file(int dfd, const char *name, unsigned char *content, done += r; } - close(fd); return rv; } static bool find_user_service(int xfd, const char *service) { - int dfd; + _cleanup_close_ int dfd = -1; /* FIXME: Make this a macro, make paths #defines */ char sub_path[STRLEN("systemd/user/") + strlen(service) + 1]; char etc_path[STRLEN("/etc/systemd/user/") + strlen(service) + 1]; @@ -102,10 +104,8 @@ static bool find_user_service(int xfd, const char *service) if (faccessat(dfd, sub_path, R_OK, 0) == 0) { info("User service %s already installed in " "$XDG_DATA_HOME/systemd/user/", service); - close(dfd); return true; } - close(dfd); } if (access(usr_path, R_OK) == 0) { @@ -120,118 +120,98 @@ static bool find_user_service(int xfd, const char *service) static bool create_user_service(int xfd, const char *service, unsigned char *content, size_t len) { - int sfd, ufd = -1; - bool rv = false; + _cleanup_close_ int sfd = -1; + _cleanup_close_ int ufd = -1; if (find_user_service(xfd, service)) return true; sfd = open_subdir(xfd, "systemd", true); if (sfd < 0) - goto out; + return false; ufd = open_subdir(sfd, "user", true); if (ufd < 0) - goto out; + return false; if (!write_cfg_file(ufd, service, content, len)) - goto out; + return false; info("Created user service file $XDG_CONFIG_HOME/systemd/user/%s", service); - rv = true; - -out: - if (ufd >= 0) - close(ufd); - if (sfd >= 0) - close(sfd); - return rv; + return true; } static bool create_server_cfg(int sfd, int cfd, const char *name, - const char *filename) + const char *filename, + const unsigned char *properties, + size_t properties_len) { - int efd = -1; - bool rv = false; + _cleanup_close_ int efd = -1; efd = open_subdir(sfd, name, true); if (efd < 0) { error("Failed to create configuration directory \"%s\"", name); - goto out; + return false; } if (!write_cfg_file(efd, "eula.txt", ___examples_eula_txt, ___examples_eula_txt_len)) - goto out; + return false; if (!write_cfg_file(efd, "server.properties", - ___examples_server_properties, - ___examples_server_properties_len)) - goto out; + properties, properties_len)) + return false; if (!create_link(efd, "server.jar", "../server.jar")) - goto out; + return false; if (!write_cfg_file(cfd, filename, ___examples_example_mcserver, ___examples_example_mcserver_len)) - goto out; - - rv = true; - -out: - if (efd >= 0) - close(efd); + return false; - return rv; + return true; } static bool create_config_tree(int xfd) { - int mfd = -1, sfd = -1, cfd = -1; - bool rv = false; + _cleanup_close_ int mfd = -1; + _cleanup_close_ int sfd = -1; + _cleanup_close_ int cfd = -1; mfd = open_subdir(xfd, "minecproxy", true); if (mfd < 0) - goto out; + return false; sfd = open_subdir(mfd, "servers", true); if (sfd < 0) - goto out; + return false; if (!write_cfg_file(sfd, "README.TXT", ___examples_README_TXT, ___examples_README_TXT_len)) - goto out; + return false; cfd = open_subdir(mfd, "config", true); if (cfd < 0) - goto out; + return false; if (!write_cfg_file(cfd, "minecproxy.conf", ___examples_minecproxy_conf, ___examples_minecproxy_conf_len)) - goto out; + return false; - if (!create_server_cfg(sfd, cfd, "example", "example.mcserver")) - goto out; + if (!create_server_cfg(sfd, cfd, "example", "example.mcserver", + ___examples_server_properties, + ___examples_server_properties_len)) + return false; info("Created configuration in $XDG_CONFIG_HOME/minecproxy"); - rv = true; - -out: - if (mfd >= 0) - close(mfd); - if (sfd >= 0) - close(sfd); - if (cfd >= 0) - close(cfd); - return rv; + return true; } bool do_init(struct cfg *cfg) { - int xfd; - bool rv = false; + _cleanup_close_ int xfd = -1; if (cfg->dir_path) { xfd = open(cfg->dir_path, O_PATH | O_CLOEXEC | O_DIRECTORY); @@ -241,32 +221,27 @@ bool do_init(struct cfg *cfg) xfd = open_xdg_cfg_dir(true); if (xfd < 0) - goto out; + return false; if (!create_config_tree(xfd)) - goto out; + return false; if (cfg->dir_path) { info("Option -c used, not creating systemd services"); - rv = true; - goto out; + return true; } if (!create_user_service(xfd, "minecserver@.service", ___examples_minecserver__service, ___examples_minecserver__service_len)) - goto out; + return false; if (!create_user_service(xfd, "minecproxy.service", ___examples_minecproxy_service, ___examples_minecproxy_service_len)) - goto out; + return false; - rv = true; -out: - if (xfd >= 0) - close(xfd); - return rv; + return true; } static bool valid_name(const char *name) @@ -300,12 +275,204 @@ static bool valid_name(const char *name) return true; } +/* FIXME: Share this */ +static inline char tohex(uint8_t val) +{ + static const char hex[] = "0123456789abcdef"; + + return hex[val & 0x0f]; +} + +static bool generate_random_password(char *buf, size_t len) +{ + size_t written; + ssize_t r; + const char *s; + char *d; + + if (len < 3) + return false; + + char rnd[(len - 1) / 2]; + + for (written = 0; written < sizeof(rnd); written += r) { + r = getrandom(rnd + written, sizeof(rnd) - written, 0); + if (r < 0) { + if (errno == EAGAIN || errno == EINTR) + continue; + + error("getrandom failure: %m"); + return false; + } + } + + for (s = rnd, d = buf; s < (rnd + sizeof(rnd)); s++) { + *(d++) = tohex(*s >> 4); + *(d++) = tohex(*s); + } + *d = '\0'; + + info("Hex = %s", buf); + return true; +} + +static unsigned char *create_mc_properties(const char *name, uint16_t mc_port, + uint16_t rcon_port, + const char *rcon_password, + size_t *len) +{ + char hexrnd[16 + 1]; + char *properties; + int r; + + if (!rcon_password) { + if (!generate_random_password(hexrnd, sizeof(hexrnd))) { + error("Failed to generate random password"); + return NULL; + } + } + + /* FIXME: server-ip= */ + r = asprintf(&properties, + "#Minecraft server properties\n" + "# This is a partial file, it will be replaced with a\n" + "# fleshed out version the first time the Minecraft\n" + "# server is executed.\n" + "motd=%s\n" + "server-port=%" PRIu16 "\n" + "enable-rcon=true\n" + "rcon.port=%" PRIu16 "\n" + "rcon.password=%s", + name, mc_port, rcon_port, + rcon_password ? rcon_password : hexrnd); + if (r < 0) { + error("asprintf failed: %m"); + return NULL; + } + + if (len) + *len = r; + return (unsigned char *)properties; +} + +static uint16_t get_port(struct list_head *list) +{ + struct saddr *saddr; + + if (!list || list_empty(list)) + return 0; + + list_for_each_entry(saddr, list, list) { + switch (saddr->st.ss_family) { + case AF_INET: + if (saddr->in4.sin_addr.s_addr == htonl(INADDR_LOOPBACK) || + saddr->in4.sin_addr.s_addr == htonl(INADDR_ANY) || + saddr->in4.sin_addr.s_addr == htonl(INADDR_BROADCAST)) + return htons(saddr->in4.sin_port); + break; + case AF_INET6: + if (!memcmp(&saddr->in6.sin6_addr, &in6addr_any, + sizeof(saddr->in6.sin6_addr)) || + !memcmp(&saddr->in6.sin6_addr, &in6addr_loopback, + sizeof(saddr->in6.sin6_addr))) + return htons(saddr->in6.sin6_port); + break; + } + } + + return 0; +} + +static bool saddr_port_match(struct list_head *list, uint16_t port) +{ + struct saddr *a; + + list_for_each_entry(a, list, list) { + switch (a->st.ss_family) { + case AF_INET: + if (a->in4.sin_port == port) + return true; + break; + + case AF_INET6: + if (a->in6.sin6_port == port) + return true; + break; + + default: + break; + } + } + + return false; +} + +static bool select_free_ports(struct cfg *cfg, uint16_t *listen_port, + uint16_t *mc_port, uint16_t *rcon_port) +{ + uint16_t lport = cfg->listen_port_min; + uint16_t mport = cfg->mc_port_min; + uint16_t rport = cfg->rcon_port_min; + struct server *server; + bool used; + + for (lport = cfg->listen_port_min, + mport = cfg->mc_port_min, + rport = cfg->rcon_port_min; + lport <= cfg->listen_port_max && + mport <= cfg->mc_port_max && + rport <= cfg->rcon_port_max; + lport++, mport++, rport++) { + + used = false; + + list_for_each_entry(server, &cfg->servers, list) { + if (!server->scfg.filename) + continue; + + if (saddr_port_match(&server->scfg.locals, lport)) { + used = true; + break; + } + + if (saddr_port_match(&server->scfg.remotes, mport)) { + used = true; + break; + } + + if (saddr_port_match(&server->scfg.rcons, rport)) { + used = true; + break; + } + } + + if (!used ) { + if (listen_port && *listen_port == 0) + *listen_port = lport; + + if (mc_port && *mc_port == 0) + *mc_port = mport; + + if (rcon_port && *rcon_port == 0) + *rcon_port = rport; + + return true; + } + } + + return false; +} + bool do_new(struct cfg *cfg) { const char *name = cfg->commands[0]; struct server *server; - int sfd = -1; - bool rv = false; + struct server *defserver = NULL; + _cleanup_free_ unsigned char *properties = NULL; + size_t properties_len; + _cleanup_close_ int sfd = -1; + uint16_t rcon_port = 0, mc_port = 0; + const char *rcon_password = NULL; if (!valid_name(name)) return false; @@ -316,33 +483,52 @@ bool do_new(struct cfg *cfg) server_load_all_known(cfg); if (!cfg->dir) - goto out; + return false; list_for_each_entry(server, &cfg->servers, list) { - if (streq(server->name, name)) { + if (!server->scfg.filename) { + defserver = server; + continue; + } + + if (streq(server->scfg.filename, filename)) { error("Server \"%s\" already exists", name); - goto out; + return false; } } + if (defserver) { + /* FIXME: check list empty and tailor error msg */ + rcon_port = get_port(&defserver->scfg.rcons); + mc_port = get_port(&defserver->scfg.remotes); + rcon_password = defserver->scfg.rcon_password; + } else if (cfg->rcon_password) + rcon_password = cfg->rcon_password; + + if (!select_free_ports(cfg, NULL, &mc_port, &rcon_port)) { + error("Failed to find a free port"); + return false; + } + sfd = open_subdir(dirfd(cfg->dir), "../servers", false); if (sfd < 0) { error("Failed to open configuration directory \"servers\""); - goto out; + return false; } - if (!create_server_cfg(sfd, dirfd(cfg->dir), name, filename)) - goto out; - - info("Created server configuration %s", name); + properties = create_mc_properties(name, mc_port, rcon_port, + rcon_password, &properties_len); + if (!properties) + return false; - rv = true; + error("Created config file (%zu):\n%s", properties_len, properties); -out: - if (sfd >= 0) - close(sfd); + if (!create_server_cfg(sfd, dirfd(cfg->dir), name, filename, properties, + properties_len)) + return false; - return rv; + info("Created server configuration %s", name); + return true; } bool do_list(struct cfg *cfg) diff --git a/shared/utils.h b/shared/utils.h index 9404323..c47f9a5 100644 --- a/shared/utils.h +++ b/shared/utils.h @@ -10,6 +10,7 @@ #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> +#include <unistd.h> extern unsigned debug_mask; @@ -127,4 +128,18 @@ static inline bool strcaseeq(const char *a, const char *b) #define DIV_ROUND_UP(n, d) (((n) + (d)-1) / (d)) +#define _cleanup_(x) __attribute__((cleanup(x))) + +static inline void closep(int *fd) +{ + if (*fd && *fd >= 0) + close(*fd); +} +#define _cleanup_close_ _cleanup_(closep) + +static inline void freep(void *p) { + xfree(*(void**)p); +} +#define _cleanup_free_ _cleanup_(freep) + #endif |