diff options
| author | David Härdeman <david@hardeman.nu> | 2020-06-23 16:25:36 +0200 | 
|---|---|---|
| committer | David Härdeman <david@hardeman.nu> | 2020-06-23 16:25:36 +0200 | 
| commit | 8c27290245b7bcc7cd2f72f3b4a7562294b43bbe (patch) | |
| tree | 54bae7909a94bfc598df7b88d9794742daf0bb31 /mcserverproxy | |
| parent | 973ae757342b91e3e6aafd07e0c0a24af84aad98 (diff) | |
Split directories better
Diffstat (limited to 'mcserverproxy')
29 files changed, 7240 insertions, 0 deletions
| diff --git a/mcserverproxy/announce.c b/mcserverproxy/announce.c new file mode 100644 index 0000000..13ef423 --- /dev/null +++ b/mcserverproxy/announce.c @@ -0,0 +1,116 @@ +#include <inttypes.h> +#include <sys/socket.h> +#include <netinet/ip.h> +#include <arpa/inet.h> +#include <string.h> +#include <unistd.h> + +#include "main.h" +#include "uring.h" +#include "announce.h" +#include "server.h" +#include "ptimer.h" + +struct announce { +	uint64_t value; +	struct uring_task task; +	struct ptimer_task ptask; +	int mcast_fd; +}; + +static void +announce_cb(struct ptimer_task *ptask) +{ +	struct announce *announce = container_of(ptask, struct announce, ptask); +	struct server *server; + +	assert_return(ptask); +	assert_task_alive(DBG_ANN, &announce->task); + +	debug(DBG_ANN, "announcing servers"); +	list_for_each_entry(server, &cfg->servers, list) +		server_announce(server, announce->mcast_fd); +} + +static void +announce_free(struct uring_task *task) +{ +	struct announce *announce = container_of(task, struct announce, task); + +	assert_return(task); +	debug(DBG_ANN, "task %p, announce 0x%p, mcast_fd: %i", +	      task, announce, announce->mcast_fd); +	close(announce->mcast_fd); +	xfree(announce); +} + +void +announce_refdump() +{ +	assert_return_silent(cfg->announce); + +	uring_task_refdump(&cfg->announce->task); +} + +void +announce_delete() +{ +	assert_return_silent(cfg->announce); + +	debug(DBG_ANN, "called"); +	announce_stop(); +	uring_task_destroy(&cfg->announce->task); +	cfg->announce = NULL; +} + +void +announce_stop() +{ +	struct announce *announce = cfg->announce; + +	assert_return_silent(announce); + +	ptimer_del_task(&announce->ptask); +} + +void +announce_start(unsigned duration) +{ +	struct announce *announce = cfg->announce; +	unsigned times; + +	assert_return_silent(announce); + +	if (duration == 0) +		times = 0; +	else +		times = MAX(announce->ptask.times, +			    DIV_ROUND_UP(duration, cfg->announce_interval)); + +	announce->ptask.times = times; +	ptimer_add_task(&announce->ptask); +} + +void +announce_init() +{ +	struct announce *announce; +	int sfd; +  +	assert_return(!cfg->announce); +	assert_return_silent(cfg->announce_interval > 0); + +	announce = zmalloc(sizeof(*announce)); +	if (!announce) +		die("malloc: %m"); + +	sfd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); +	if (sfd < 0) +		die("socket: %m"); + +	uring_task_init(&announce->task, "announce", uring_parent(), announce_free); +	ptask_init(&announce->ptask, cfg->announce_interval, 0, announce_cb); +	announce->mcast_fd = sfd; +	cfg->announce = announce; +} + diff --git a/mcserverproxy/announce.h b/mcserverproxy/announce.h new file mode 100644 index 0000000..77a36f2 --- /dev/null +++ b/mcserverproxy/announce.h @@ -0,0 +1,14 @@ +#ifndef fooannouncehfoo +#define fooannouncehfoo + +void announce_refdump(); + +void announce_delete(); + +void announce_stop(); + +void announce_start(unsigned duration); + +void announce_init(); + +#endif diff --git a/mcserverproxy/config-parser.c b/mcserverproxy/config-parser.c new file mode 100644 index 0000000..ffed7f1 --- /dev/null +++ b/mcserverproxy/config-parser.c @@ -0,0 +1,490 @@ +#define _GNU_SOURCE +#include <stdio.h> +#include <ctype.h> +#include <string.h> +#include <stdbool.h> +#include <errno.h> +#include <unistd.h> +#include <arpa/inet.h> +#include <inttypes.h> + +#include "main.h" +#include "config-parser.h" + +static void +eat_whitespace_and_comments(char **pos) +{ +	assert_return(pos && *pos); + +	while (true) { +		while (isspace(**pos)) +			(*pos)++; + +		if (**pos != '#') +			return; + +		while (**pos != '\r' && **pos != '\n' && **pos != '\0') +			(*pos)++; +	} +} + +static char * +get_line(char **pos) +{ +	char *begin, *end; + +	assert_return(pos && *pos, NULL); + +	begin = *pos; +	while (isspace(*begin)) +		begin++; + +	if (*begin == '\0') +		return NULL; + +	end = begin; +	while (*end != '\n' && *end != '\0') +		end++; + +	if (*end == '\0') +		*pos = end; +	else +		*pos = end + 1; + +	while (isspace(*end)) { +		*end = '\0'; +		end--; +	} + +	return begin; +} + +static bool +dnslookup(const char *name, uint16_t port, struct cfg_value *rvalue, bool async) +{ +	struct sockaddr_in *in4; +	struct sockaddr_in6 *in6; +	struct dns_async tmp; +	struct dns_async *dns; +	int mode = async ? GAI_NOWAIT : GAI_WAIT; +	struct addrinfo *results = NULL, *ai; +	struct saddr *saddr = NULL; +	bool rv = false; +	int r; + +	assert_return(!empty_str(name) && strlen(name) < sizeof(dns->name) && port > 0 && rvalue, false); + +	if (async) { +		rvalue->type = CFG_VAL_TYPE_ASYNC_ADDRS; +		rvalue->dns_async = NULL; +		dns = zmalloc(sizeof(*dns)); +		if (!dns) { +			error("async DNS lookup of %s failed: %m", name); +			goto out; +		} +		debug(DBG_DNS, "doing async DNS lookup of %s: %p", name, dns); +	} else { +		memset(&tmp, 0, sizeof(tmp)); +		dns = &tmp; +		debug(DBG_DNS, "doing sync DNS lookup of %s", name); +	} + +	sprintf(dns->name, "%s", name);	 +	sprintf(dns->port, "%" PRIu16, port); + +	dns->req.ai_family = AF_UNSPEC; +	dns->req.ai_socktype = SOCK_STREAM; +	dns->req.ai_protocol = 0; +	dns->req.ai_flags = AI_NUMERICSERV; + +	dns->sev.sigev_notify = SIGEV_SIGNAL; +	dns->sev.sigev_signo = SIGUSR1; +	dns->sev.sigev_value.sival_ptr = dns; + +	dns->gcb.ar_name = dns->name; +	dns->gcb.ar_service = dns->port; +	dns->gcb.ar_request = &dns->req; + +	struct gaicb *gcbs[] = { &dns->gcb }; + +	r = getaddrinfo_a(mode, gcbs, ARRAY_SIZE(gcbs), &dns->sev); +	if (r != 0) { +		error("getaddrinfo(%s:%" PRIu16 "): %s", name, port, gai_strerror(r)); +		goto out; +	} + +	if (async) { +		rvalue->dns_async = dns; +		rv = true; +		goto out; +	} + +	results = dns->gcb.ar_result; + +	for (ai = results; ai; ai = ai->ai_next) { +		saddr = zmalloc(sizeof(*saddr)); +		if (!saddr) { +			error("sync DNS lookup of %s failed: %m", name); +			goto out; +		} + +		switch (ai->ai_family) { +		case AF_INET: +			in4 = (struct sockaddr_in *)ai->ai_addr; +			saddr_set_ipv4(saddr, in4->sin_addr.s_addr, in4->sin_port); +			error("addrstr: %s", saddr->addrstr); +			list_add(&saddr->list, &rvalue->saddrs); +			break; + +		case AF_INET6: +			in6 = (struct sockaddr_in6 *)ai->ai_addr; +			saddr_set_ipv6(saddr, &in6->sin6_addr, in6->sin6_port); +			error("addrstr: %s", saddr->addrstr); +			list_add(&saddr->list, &rvalue->saddrs); +			break; + +		default: +			error("getaddrinfo(%s:%s): unknown address family (%i)", +			      dns->name, dns->port, ai->ai_family); +			xfree(saddr); +			break; +		} +	} + +	rv = true; + +out: +	freeaddrinfo(results); +	return rv; +} + +static bool +strtosockaddrs(const char *str, struct cfg_value *rvalue, bool async) +{ +	struct saddr *saddr; +	uint16_t port; +	char *tmp; +	struct list_head *list; +	unsigned naddrs = 0; + +	assert_return(!empty_str(str) && rvalue, false); + +	rvalue->type = CFG_VAL_TYPE_ADDRS; +	list = &rvalue->saddrs; +	list_init(list); + +	if (*str == '[') { +		/* IPv6, [a:b:c...h]:p or [*]:p */ +		debug(DBG_CFG, "attempting to parse IPv6 addr (%s)", str); + +		str++; +		tmp = strchr(str, ']'); +		if (!tmp) +			goto error; +		*tmp = '\0'; + +		saddr = zmalloc(sizeof(*saddr)); +		if (!saddr) +			goto error; + +		/* early list_add to make sure saddr is free():d on error */ +		list_add(&saddr->list, list); + +		if (streq(str, "*")) +			saddr->in6.sin6_addr = in6addr_any; +		else if (inet_pton(AF_INET6, str, &saddr->in6.sin6_addr) <= 0) +			goto error; + +		tmp++; +		if (*tmp != ':') +			goto error; + +		tmp++; +		if (strtou16_strict(tmp, &port) < 0) +			goto error; + +		saddr_set_ipv6(saddr, NULL, htons(port)); +		naddrs++; + +	} else if (*str == '*') { +		/* IPv4, *:p */ +		debug(DBG_CFG, "attempting to parse IPv4 addr (%s)", str); + +		str++; +		if (*str != ':') +			goto error; + +		str++; +		if (strtou16_strict(str, &port) < 0) +			goto error; + +		saddr = zmalloc(sizeof(*saddr)); +		if (!saddr) +			goto error; + +		saddr_set_ipv4(saddr, INADDR_ANY, htons(port)); +		list_add(&saddr->list, list); +		naddrs++; + +	} else if ((tmp = strchr(str, ':'))) { +		/* IPv4, a.b.c.d:p or IPv4/6 hostname:p */ +		debug(DBG_CFG, "attempting to parse IPv4 addr or hostname (%s)", str); + +		*tmp = '\0'; +		tmp++; +		if (strtou16_strict(tmp, &port) < 0) +			goto error; + +		saddr = zmalloc(sizeof(*saddr)); +		if (!saddr) +			goto error; + +		if (inet_pton(AF_INET, str, &saddr->in4.sin_addr) > 0) { +			debug(DBG_CFG, "got an IPv4:port (%s:%" PRIu16 ")", str, port); +			saddr_set_ipv4(saddr, saddr->in4.sin_addr.s_addr, htons(port)); +			list_add(&saddr->list, list); +			naddrs++; +			goto success; +		}  + +		xfree(saddr); +		debug(DBG_CFG, "maybe got a hostname:port (%s:%" PRIu16 ")", str, port); +		if (!dnslookup(str, port, rvalue, async)) +			goto error; + +	} else if (strtou16_strict(tmp, &port) == 0) { +		/* Port */ +		debug(DBG_CFG, "attempting to parse a port number (%s)", str); + +		saddr = zmalloc(sizeof(*saddr)); +		if (!saddr) +			goto error; + +		saddr_set_ipv6(saddr, &in6addr_any, htons(port)); +		list_add(&saddr->list, list); +		naddrs++; + +		saddr = zmalloc(sizeof(*saddr)); +		if (!saddr) +			goto error; + +		saddr_set_ipv4(saddr, INADDR_ANY, htons(port)); +		list_add(&saddr->list, list); +		naddrs++; + +	} else { +		/* Unknown */ +		error("unable to parse address: %s", str); +		goto error; +	} + +success: +	switch (rvalue->type) { +	case CFG_VAL_TYPE_ADDRS: +		if (list_empty(list) || naddrs == 0) { +			error("empty address list"); +			return false; +		} + +		debug(DBG_CFG, "parsed to %u addresses", naddrs); +		return true; + +	case CFG_VAL_TYPE_ASYNC_ADDRS: +		debug(DBG_CFG, "looking up address asynchronously"); +		return true; + +	default: +		error("invalid rvalue type"); +		rvalue->type = CFG_VAL_TYPE_INVALID; +		break; +	} + +error: +	if (rvalue->type == CFG_VAL_TYPE_ADDRS && !list_empty(list)) { +		struct saddr *tmp; + +		list_for_each_entry_safe(saddr, tmp, list, list) { +			list_del(&saddr->list); +			xfree(saddr); +		} +	} +	return false; +} + +/* Returns true if there's data left to parse in buf */ +bool +config_parse_line(const char *filename, char **buf, +		  struct cfg_key_value_map *kvmap, int *rkey, +		  const char **rkeyname, struct cfg_value *rvalue) +{ +	char *line, *tmp, *key; +	int i; + +	assert_return(buf && *buf && kvmap && rkey && rkeyname && rvalue, false); + +	eat_whitespace_and_comments(buf); +	line = get_line(buf); +	if (!line) +		return false; + +	debug(DBG_CFG, "%s: parsing config line: %s", filename, line); + +	tmp = line; +	while (isspace(*tmp)) +		tmp++; + +	if (*tmp == '\0') +		goto error; + +	key = tmp; +	while (*tmp != '\0' && !isspace(*tmp)) +		tmp++; + +	if (*tmp == '\0') +		goto error; + +	*tmp = '\0'; +	tmp++; + +	while (isspace(*tmp)) +		tmp++; + +	if (*tmp != '=') +		goto error; + +	tmp++; +	while (isspace(*tmp)) +		tmp++; + +	if (*tmp == '\0') +		goto error; + +	for (i = 0; kvmap[i].key_name; i++) { +		if (!streq(kvmap[i].key_name, key)) +			continue; + +		switch (kvmap[i].value_type) { + +		case CFG_VAL_TYPE_STRING: +			rvalue->type = CFG_VAL_TYPE_STRING; +			rvalue->str = tmp; +			break; + +		case CFG_VAL_TYPE_UINT16: { +			uint16_t v; + +			if (strtou16_strict(tmp, &v) < 0) +				goto error; + +			rvalue->type = CFG_VAL_TYPE_UINT16; +			rvalue->uint16 = v; +			break; +		} + +		case CFG_VAL_TYPE_ADDRS: +			if (!strtosockaddrs(tmp, rvalue, false)) +				goto error; + +			if (rvalue->type != CFG_VAL_TYPE_ADDRS) { +				error("invalid type returned from strtosockaddrs"); +				goto error; +			} + +			if (list_empty(&rvalue->saddrs)) { +				error("empty address list"); +				goto error; +			} +			break; + +		case CFG_VAL_TYPE_ASYNC_ADDRS: +			if (!strtosockaddrs(tmp, rvalue, true)) +				goto error; + +			switch (rvalue->type) { +			case CFG_VAL_TYPE_ADDRS: +				if (list_empty(&rvalue->saddrs)) { +					error("empty address list"); +					goto error; +				} +				break; + +			case CFG_VAL_TYPE_ASYNC_ADDRS: +				if (!rvalue->dns_async) { +					error("dns_async not set"); +					goto error; +				} +				break; + +			default: +				error("invalid type returned from strtosockaddrs"); +				goto error; +			} + +			break; + +		case CFG_VAL_TYPE_BOOL: +			if (strcaseeq(tmp, "yes") || strcaseeq(tmp, "true")) { +				rvalue->type = CFG_VAL_TYPE_BOOL; +				rvalue->boolean = true; +			} else if (strcaseeq(tmp, "no") || strcaseeq(tmp, "false")) { +				rvalue->type = CFG_VAL_TYPE_BOOL; +				rvalue->boolean = false; +			} else { +				error("invalid boolean value (%s)", tmp); +				goto error; +			} +			break; + +		case CFG_VAL_TYPE_INVALID: +			/* fall through */ +		default: +			goto error; +		} + +		/* sanity check */ +		if ((rvalue->type != kvmap[i].value_type) && +		    ((kvmap[i].value_type != CFG_VAL_TYPE_ASYNC_ADDRS) && +		     (rvalue->type != CFG_VAL_TYPE_ADDRS))) { +			error("rvalue->type != kvmap->type"); +			goto error; +		} + +		*rkey = kvmap[i].key_value; +		*rkeyname = kvmap[i].key_name; +		return true; +	} + +error: +	/* FIXME: the line is already mangled here, a line number would be nice */ +	error("%s: invalid config line: %s", filename, line); +	rvalue->type = CFG_VAL_TYPE_INVALID; +	*rkey = 0; +	*rkeyname = NULL; +	return true; +} + +bool +config_parse_header(const char *filename, const char *title, char **buf) +{ +	char *line; + +	assert_return(!empty_str(filename) && !empty_str(title) && buf && *buf, false); + +	eat_whitespace_and_comments(buf); + +	line = get_line(buf); +	if (!line) { +		error("%s: missing header in configuration file", filename); +		return false; +	} else { +		char titlehdr[strlen(title) + 3]; + +		sprintf(titlehdr, "[%s]", title); +		if (!streq(line, titlehdr)) { +			error("%s: incorrect header in configuration file", filename); +			return false; +		} +	} + +	return true; +} diff --git a/mcserverproxy/config-parser.h b/mcserverproxy/config-parser.h new file mode 100644 index 0000000..3a117a3 --- /dev/null +++ b/mcserverproxy/config-parser.h @@ -0,0 +1,59 @@ +#ifndef fooconfigparserhfoo +#define fooconfigparserhfoo + +#define _GNU_SOURCE +#include <sys/types.h> +#include <sys/socket.h> +#include <netdb.h> +#include <signal.h> + +enum cfg_value_type { +	CFG_VAL_TYPE_INVALID, +	CFG_VAL_TYPE_STRING, +	CFG_VAL_TYPE_UINT16, +	CFG_VAL_TYPE_ADDRS, +	CFG_VAL_TYPE_ASYNC_ADDRS, +	CFG_VAL_TYPE_BOOL, +}; + +struct dns_async; + +typedef void (dns_cb_t)(struct dns_async *); + +struct dns_async { +	char name[FQDN_STR_LEN + 1]; +	char port[PORT_STR_LEN + 1]; +	struct addrinfo req; +	struct gaicb gcb; +	struct sigevent sev; +	dns_cb_t *cb; +	void *priv; +	struct list_head list; +}; + +struct cfg_key_value_map { +	const char *key_name; +	int key_value; +	enum cfg_value_type value_type; +}; + +struct cfg_value { +	enum cfg_value_type type; +	union { +		const char *str; +		uint16_t uint16; +		struct list_head saddrs; +		struct dns_async *dns_async; +		bool boolean; +	}; +}; + +bool config_parse_line(const char *filename, char **buf, +		       struct cfg_key_value_map *kvmap, +		       int *rkey, const char **rkeyname, +		       struct cfg_value *rvalue); + +bool config_parse_header(const char *filename, +			 const char *title, char **buf); + +#endif diff --git a/mcserverproxy/idle.c b/mcserverproxy/idle.c new file mode 100644 index 0000000..c49846d --- /dev/null +++ b/mcserverproxy/idle.c @@ -0,0 +1,378 @@ +#define _GNU_SOURCE +#include <inttypes.h> +#include <sys/socket.h> +#include <netinet/ip.h> +#include <arpa/inet.h> +#include <string.h> +#include <ctype.h> +#include <errno.h> + +#include "main.h" +#include "uring.h" +#include "server.h" +#include "idle.h" +#include "ptimer.h" + +struct idle { +	struct ptimer_task ptask; +	struct uring_task task; +}; + +static inline void +write_byte(char **pos, char byte) +{ +	assert_return(pos && *pos); + +	**pos = byte; +	(*pos)++; +} + +#define MC_HELO 0x00 +#define MC_NEXT_STATE_STATUS 0x01 +#define MC_GET_STATUS 0x00 +#define MC_VARINT_MAX_BYTES 5 +#define MC_STATUS_REPLY 0x00 +#define MC_UNDEFINED_VERSION -1 + +static inline void +write_varint(char **pos, int32_t orig) +{ +	assert_return(pos && *pos); + +	uint32_t val = (uint32_t)orig; + +	while (val) { +		**pos = val & 0x7f; +		val >>= 7; +		if (val > 0) +			**pos |= 0x80; +		(*pos)++; +	} +} + +/* + * return value: + * positive = varint parsed + * zero = need more bytes + * negative = error + */ +static inline int +read_varint(char **pos, size_t *remain, int32_t *res) +{ +	unsigned consumed; +	uint32_t val = 0; + +	assert_return(pos && *pos && remain && res, -1); + +	for (consumed = 1; consumed <= *remain; consumed++) { +		uint32_t tmp; + +		tmp = **pos & 0x7f; +		val += (tmp << (7 * (consumed - 1))); +		(*pos)++; + +		if (!(tmp & 0x80)) +			break; +	} + +	if (consumed > *remain) +		return 0; +	else if (consumed > MC_VARINT_MAX_BYTES) +		return -1; + +	*remain -= consumed; +	*res = (int32_t)val; +	return 1; +} + +static inline void +write_bytes(char **pos, const char *bytes, size_t n) +{ +	assert_return(pos && *pos && bytes && n > 0); + +	memcpy(*pos, bytes, n); +	*pos += n; +} + +static inline void +write_str(char **pos, const char *str) +{ +	size_t len; + +	assert_return(pos && *pos && !empty_str(str)); + +	len = strlen(str); +	write_varint(pos, len); +	write_bytes(pos, str, len); +} + +static inline void +write_cmd(char **pos, const char *begin, const char *end) +{ +	assert_return(pos && *pos && begin && end && end > begin); + +	write_varint(pos, end - begin); +	write_bytes(pos, begin, end - begin); +} + +static int +idle_check_handshake_complete(struct uring_task *task, int res) +{ +	size_t remain; +	char *pos; +	int32_t mclen; +	int r; + +	assert_return(task, -EINVAL); +	assert_task_alive_or(DBG_IDLE, task, return -EINTR); + +	remain = task->tbuf->len; +	pos = task->tbuf->buf; + +	r = read_varint(&pos, &remain, &mclen); +	if (r < 0) { +		error("failed to parse message length"); +		return -EINVAL; +	} else if (r == 0) { +		return 0; +	} else if (mclen < 2) { +		error("short MC message"); +		return -EINVAL; +	} + +	if (mclen < remain) { +		debug(DBG_IDLE, "short MC message - len: %" PRIi32 ", remain: %zu", +		      mclen, remain); +		return 0; +	}  + +	debug(DBG_IDLE, "Complete message"); +	return 1; +} + +#define ONLINE_NEEDLE "\"online\"" +static int +get_player_count(const char *pos, size_t remain) +{ +	/* +	 * Example JSON (line breaks added): +	 * {"description":{ +	 *	"text":"A Minecraft Server"}, +	 *	"players":{"max":20,"online":0}, +	 *	"version":{"name":"1.15.2","protocol":578} +	 * } +	 */ +	char *online; +	char *end; +	unsigned count; + +	assert_return(pos && remain > 0, -1); + +	online = memmem(pos, remain, ONLINE_NEEDLE, strlen(ONLINE_NEEDLE)); +	if (!online) { +		error("could not find online count in JSON"); +		return -1; +	} + +	remain -= (online - pos); + +	end = memchr(online, '}', remain); +	if (!end) { +		error("could not parse JSON (no end)"); +		return -1; +	} +	*end = '\0'; + +	if (sscanf(online, ONLINE_NEEDLE " : %u", &count) != 1) { +		error("could not parse JSON (online count)"); +		return -1; +	} + +	return count; +} + +static void +idle_check_handshake_reply(struct uring_task *task, int res) +{ +	struct server *server = container_of(task, struct server, idle_task); +	int32_t mclen; +	int32_t jsonlen; +	char *pos; +	size_t remain; +	int player_count = -1; +	int r; + +	assert_return(task); +	assert_task_alive(DBG_IDLE, task); + +	debug(DBG_IDLE, "res: %i", res); +	if (res < 0) +		goto out; + +	/* +	fprintf(stderr, "Received MC message (%i bytes):", res); +	for (int i = 0; i < res; i++) +		fprintf(stderr, "0x%02hhx ", idle->remotebuf[i]); +	fprintf(stderr, "n"); +	*/ + +	remain = server->idle_buf.len; +	pos = server->idle_buf.buf; + +	r = read_varint(&pos, &remain, &mclen); +	if (r <= 0 || mclen < 2 || mclen < remain) { +		/* Should not happen since the msg has been checked already */ +		error("invalid message"); +		goto out; +	} + +	debug(DBG_IDLE, "MC message - len: %" PRIi32 ", remain: %zu", +	      mclen, remain); + +	if (*pos != MC_STATUS_REPLY) { +		error("unknown server reply (0x%02hhx)", *pos); +		goto out; +	} + +	pos++; +	remain--; + +	r = read_varint(&pos, &remain, &jsonlen); +	if (r <= 0) { +		error("could not read JSON length"); +		goto out; +	} + +	debug(DBG_IDLE, "MC - json len: %" PRIi32 ", remain: %zu", +	      jsonlen, remain); + +	if (jsonlen < remain) { +		error("invalid JSON length"); +		goto out; +	} + +	/* +	fprintf(stderr, "JSON: "); +	for (int i = 0; i < jsonlen; i++) +		fprintf(stderr, "%c", pos[i]); +	*/ + +	player_count = get_player_count(pos, remain); + +out: +	uring_task_close_fd(task); +	server_set_active_players(server, player_count); +	return; +} + +static void +idle_check_handshake_sent(struct uring_task *task, int res) +{ +	assert_return(task); +	assert_task_alive(DBG_IDLE, task); + +	debug(DBG_IDLE, "sent %i bytes", res); +	if (res < 0) { +		uring_task_close_fd(task); +		return; +	} + +	uring_tbuf_read_until(task, +			      idle_check_handshake_complete, +			      idle_check_handshake_reply); +} + +void +idle_check_get_player_count(struct server *server, struct connection *conn) +{ +	char buf[1024]; +	char *pos; +	char *cmdbuf = server->idle_buf.buf; +	uint16_t port; +	char hostname[INET6_ADDRSTRLEN]; + +	assert_return(server && conn && server->idle_task.priv); + +	port = saddr_port(&conn->remote); +	saddr_addr(&conn->remote, hostname, sizeof(hostname)); + +	pos = buf; +	write_byte(&pos, MC_HELO); +	write_varint(&pos, MC_UNDEFINED_VERSION); +	write_str(&pos, hostname); +	write_byte(&pos, (port >> 8) & 0xff); +	write_byte(&pos, (port >> 0) & 0xff); +	write_byte(&pos, MC_NEXT_STATE_STATUS); +	write_cmd(&cmdbuf, buf, pos); + +	pos = buf; +	write_byte(&pos, MC_GET_STATUS); +	write_cmd(&cmdbuf, buf, pos); + +	server->idle_buf.len = (cmdbuf - server->idle_buf.buf); +	debug(DBG_IDLE, "sending MC message (%zu bytes)", server->idle_buf.len); + +	uring_tbuf_write(&server->idle_task, idle_check_handshake_sent); +} + +static void +idle_cb(struct ptimer_task *ptask) +{ +	struct idle *idle = container_of(ptask, struct idle, ptask); +	struct server *server; + +	assert_return(ptask); +	assert_task_alive(DBG_IDLE, &idle->task); + +	debug(DBG_IDLE, "timer fired"); + +	list_for_each_entry(server, &cfg->servers, list) +		server_idle_check(server); +} + +static void +idle_free(struct uring_task *task) +{ +	struct idle *idle = container_of(task, struct idle, task); + +	assert_return(task); +	debug(DBG_IDLE, "task %p, idle %p", task, idle); +	xfree(idle); +} + +void +idle_refdump() +{ +	assert_return_silent(cfg->idle); + +	uring_task_refdump(&cfg->idle->task); +} + +void +idle_delete() +{ +	assert_return(cfg->idle); + +	debug(DBG_IDLE, "closing fd %i", cfg->idle->task.fd); +	ptimer_del_task(&cfg->idle->ptask); +	uring_task_destroy(&cfg->idle->task); +	cfg->idle = NULL; +} + +void +idle_init() +{ +	struct idle *idle; + +	assert_return(!cfg->idle); + +	idle = zmalloc(sizeof(*idle)); +	if (!idle) +		die("malloc: %m"); + +	ptask_init(&idle->ptask, 60, 0, idle_cb); +	uring_task_init(&idle->task, "idle", uring_parent(), idle_free); +	ptimer_add_task(&idle->ptask); +	cfg->idle = idle; +} + diff --git a/mcserverproxy/idle.h b/mcserverproxy/idle.h new file mode 100644 index 0000000..d7e4ab0 --- /dev/null +++ b/mcserverproxy/idle.h @@ -0,0 +1,13 @@ +#ifndef fooidlehfoo +#define fooidlehfoo + +void idle_check_get_player_count(struct server *server, +				 struct connection *conn); + +void idle_refdump(); + +void idle_delete(); + +void idle_init(); + +#endif diff --git a/mcserverproxy/igmp.c b/mcserverproxy/igmp.c new file mode 100644 index 0000000..dc43a9f --- /dev/null +++ b/mcserverproxy/igmp.c @@ -0,0 +1,587 @@ +#include <unistd.h> +#include <string.h> +#include <stdint.h> +#include <sys/socket.h> +#include <linux/if_packet.h> +#include <net/ethernet.h> +#include <net/if.h> +#include <netinet/ip.h> +#include <linux/bpf.h> +#include <linux/filter.h> +#include <arpa/inet.h> +#include <errno.h> +#include <sys/ioctl.h> + +#include "main.h" +#include "uring.h" +#include "igmp.h" +#include "announce.h" + +struct igmp { +	struct uring_task task; +	struct uring_task_buf tbuf; +}; + +#define ETH_HDR_LEN		14 +#define IPV4_MIN_HDR_LEN	20 +#define IGMP_MIN_LEN		8 + +struct __attribute__((packed, scalar_storage_order("big-endian"))) ipv4_hdr { +	unsigned version:4; +	unsigned ihl:4; +	unsigned dscp:6; +	unsigned ecn:2; +	unsigned length:16; +	unsigned id:16; +	unsigned flags:3; +	unsigned fragment_offset:13; +	unsigned ttl:8; +	unsigned protocol:8; +	unsigned checksum:16; +	unsigned src:32; +	unsigned dst:32; +	unsigned options[]; +}; + +enum igmp_type { +	IGMP_MEMBERSHIP_QUERY		= 0x11, +	IGMP_V1_MEMBERSHIP_REPORT	= 0x12, +	IGMP_V2_MEMBERSHIP_REPORT	= 0x16, +	IGMP_V3_MEMBERSHIP_REPORT	= 0x22, +	IGMP_V2_LEAVE_GROUP		= 0x17 +}; + +union igmp_msg { +	struct __attribute__((packed, scalar_storage_order("big-endian"))) { +		enum igmp_type type:8; +		unsigned unknown:8; +		unsigned checksum:16; +	} common; + +	struct __attribute__((packed, scalar_storage_order("big-endian"))) { +		enum igmp_type type:8; +		unsigned resptime:8; +		unsigned checksum:16; +		unsigned addr:32; +	} v2; + +	struct __attribute__((packed, scalar_storage_order("big-endian"))) { +		enum igmp_type type:8; +		unsigned reserved1:8; +		unsigned checksum:16; +		unsigned reserved2:16; +		unsigned nrecs:16; +		uint8_t records[]; +	} v3; +}; + +enum igmp_v3_record_type { +	IGMP_V3_REC_MODE_IS_INCL	= 1, +	IGMP_V3_REC_MODE_IS_EXCL	= 2, +	IGMP_V3_REC_MODE_CH_INCL	= 3, +	IGMP_V3_REC_MODE_CH_EXCL	= 4 +}; + +union igmp_v3_record { +	struct __attribute__((packed, scalar_storage_order("big-endian"))) { +		enum igmp_v3_record_type type:8; +		unsigned auxlen:8; +		unsigned nsrcs:16; +		unsigned addr:32; +		uint32_t saddr[]; +	}; +}; + +static inline unsigned short +from32to16(unsigned int x) +{ +	/* add up 16-bit and 16-bit for 16+c bit */ +	x = (x & 0xffff) + (x >> 16); +	/* add up carry.. */ +	x = (x & 0xffff) + (x >> 16); +	return x; +} + +static unsigned int +do_csum(const unsigned char *buf, int len) +{ +	int odd; +	unsigned int result = 0; + +	assert_return(buf && len > 0, 0); + +	odd = 1 & (unsigned long)buf; +	if (odd) { +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +		result += (*buf << 8); +#else +		result = *buf; +#endif +		len--; +		buf++; +	} + +	if (len >= 2) { +		if (2 & (unsigned long)buf) { +			result += *(unsigned short *)buf; +			len -= 2; +			buf += 2; +		} +		if (len >= 4) { +			const unsigned char *end = buf + ((unsigned)len & ~3); +			unsigned int carry = 0; +			do { +				unsigned int w = *(unsigned int *)buf; +				buf += 4; +				result += carry; +				result += w; +				carry = (w > result); +			} while (buf < end); +			result += carry; +			result = (result & 0xffff) + (result >> 16); +		} +		if (len & 2) { +			result += *(unsigned short *)buf; +			buf += 2; +		} +	} + +	if (len & 1) +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +		result += *buf; +#else +		result += (*buf << 8); +#endif + +	result = from32to16(result); +	if (odd) +		result = ((result >> 8) & 0xff) | ((result & 0xff) << 8); + +	return result; +} + +static inline bool +csum_valid(const char *buf, size_t len) +{ +	assert_return(buf && len > 0, false); + +	return do_csum((unsigned const char *)buf, len) == 0xffff; +} + +static void +igmp_match() +{ +	debug(DBG_IGMP, "multicast request discovered"); +	/* +	 * IGMP messages are sent with approx 120-130 sec intervals, +	 * so set time to 5 minutes to allow some slack. +	 */ +	announce_start(5 * 60); +} + +static void +igmp_parse(struct igmp *igmp) +{ +	char *buf; +	size_t len; +	struct ipv4_hdr *hdr; +	size_t body_len; +	union igmp_msg *igmp_msg; + +	assert_return(igmp); + +	buf = igmp->task.tbuf->buf; +	len = igmp->task.tbuf->len; +	hdr = (struct ipv4_hdr *)buf; + +	if (len <= IPV4_MIN_HDR_LEN) +		return; + +	if (hdr->version != 4) +		return; + +	if (hdr->ihl * 4 < IPV4_MIN_HDR_LEN) +		return; + +	if (hdr->length < hdr->ihl * 4) +		return; + +	if (hdr->length != len) +		return; + +	if (hdr->fragment_offset > 0) +		return; + +	if (hdr->protocol != IPPROTO_IGMP) +		return; + +	if (!csum_valid(buf, hdr->ihl * 4)) +		return; + +	body_len = hdr->length - hdr->ihl * 4; +	igmp_msg = (union igmp_msg *)(buf + hdr->ihl * 4); + +	if (body_len < IGMP_MIN_LEN) +		return; + +	switch (igmp_msg->common.type) { +	case IGMP_V1_MEMBERSHIP_REPORT: +		debug(DBG_IGMP, "igmp_v1_membership_report"); +		/* fall through */ + +	case IGMP_V2_MEMBERSHIP_REPORT: { +		struct in_addr src; +		char src_str[INET_ADDRSTRLEN]; +		struct in_addr dst; +		char dst_str[INET_ADDRSTRLEN]; +		struct in_addr grp; +		char grp_str[INET_ADDRSTRLEN]; + +		src.s_addr = htonl(hdr->src); +		inet_ntop(AF_INET, &src, src_str, sizeof(src_str)); +		dst.s_addr = htonl(hdr->dst); +		inet_ntop(AF_INET, &dst, dst_str, sizeof(dst_str)); +		grp.s_addr = htonl(igmp_msg->v2.addr); +		inet_ntop(AF_INET, &grp, grp_str, sizeof(grp_str)); + +		debug(DBG_IGMP, "igmp_v2_membership_report %s -> %s (%s)", +		      src_str, dst_str, grp_str); + +		if (body_len != IGMP_MIN_LEN) { +			error("IGMPv2 invalid size"); +			break; +		} + +		if (!csum_valid((char *)igmp_msg, body_len)) { +			error("IGMPv2 invalid checksum"); +			break; +		} + +		debug(DBG_IGMP, "Inet addr: 0x%x", inet_addr("224.0.2.60")); +		debug(DBG_IGMP, "Inet addr: 0x%x", cinet_addr(224,0,2,60)); +		debug(DBG_IGMP, "Inet addr: 0x%x", chtobe32(cinet_addr(224,0,2,60))); +		if (htonl(hdr->dst) != cinet_addr(224,0,2,60)) { +			debug(DBG_IGMP, "IGMPv2 invalid dst addr"); +			break; +		} + +		if (htonl(igmp_msg->v2.addr) != cinet_addr(224,0,2,60)) { +			debug(DBG_IGMP, "IGMPv2 invalid grp addr"); +			break; +		} + +		igmp_match(); +		break; +	} + +	case IGMP_V3_MEMBERSHIP_REPORT: { +		char *pos = (char *)igmp_msg; +		struct in_addr src; +		char src_str[INET_ADDRSTRLEN]; +		struct in_addr dst; +		char dst_str[INET_ADDRSTRLEN]; + +		src.s_addr = htonl(hdr->src); +		inet_ntop(AF_INET, &src, src_str, sizeof(src_str)); +		dst.s_addr = htonl(hdr->dst); +		inet_ntop(AF_INET, &dst, dst_str, sizeof(dst_str)); + +		debug(DBG_IGMP, "igmp_v3_membership_report %s -> %s", +		      src_str, dst_str); + +		debug(DBG_IGMP, "IGMPv3" +		      " type: %x," +		      " reserved: %u," +		      " csum: %u," +		      " reserved: %u," +		      " nrecs: %u," +		      " size: %zu\n", +		      igmp_msg->v3.type, +		      igmp_msg->v3.reserved1, +		      igmp_msg->v3.checksum, +		      igmp_msg->v3.reserved2, +		      igmp_msg->v3.nrecs, +		      sizeof(igmp_msg->v3)); + +		if (!csum_valid(pos, body_len)) { +			error("IGMPv3 csum invalid"); +			break; +		} + +		if (htonl(hdr->dst) != cinet_addr(224,0,0,22)) { +			debug(DBG_IGMP, "IGMPv2 invalid dst addr"); +			break; +		} + +		body_len -= sizeof(igmp_msg->v3); +		pos += sizeof(igmp_msg->v3); + +		for (unsigned rec = 0; rec < igmp_msg->v3.nrecs; rec++) { +			union igmp_v3_record *record = (union igmp_v3_record *)pos; +			struct in_addr grp; +			char grp_str[INET_ADDRSTRLEN]; + +			if (body_len < sizeof(*record)) { +				error("IGMPv3 too short"); +				break; +			} + +			grp.s_addr = htonl(record->addr); +			inet_ntop(AF_INET, &grp, grp_str, sizeof(grp_str)); +			debug(DBG_IGMP, "received IGMPv3 record to %s", grp_str); + +			debug(DBG_IGMP, "IGMPv3 rec, " +			      " type: %u," +			      " auxlen: %u," +			      " nsrcs: %u," +			      " addr: %s," +			      " size: %zu bytes", +			      record->type, +			      record->auxlen, +			      record->nsrcs, +			      grp_str, +			      sizeof(*record)); + +			body_len -= sizeof(*record); +			pos += sizeof(*record); + +			if (body_len < record->nsrcs * sizeof(uint32_t) + record->auxlen) { +				error("IGMPv3 too short"); +				break; +			} + +			for (unsigned addr = 0; addr < record->nsrcs; addr++) { +				struct in_addr grp_src; +				char grp_src_str[INET_ADDRSTRLEN]; + +				grp_src.s_addr = htonl(record->saddr[addr]); +				inet_ntop(AF_INET, &grp_src, grp_src_str, sizeof(grp_src_str)); +				debug(DBG_IGMP, "received IGMPv3 record src %s", +				      grp_src_str); + +				body_len -= sizeof(record->saddr[addr]); +				pos += sizeof(record->saddr[addr]); +			} + +			/* Yes, EXCL, not INCL, see RFC3376 */ +			if ((htonl(record->addr) == cinet_addr(224,0,2,60)) && +			    ((record->type == IGMP_V3_REC_MODE_IS_EXCL) || +			     (record->type == IGMP_V3_REC_MODE_CH_EXCL))) +				igmp_match(); + +			body_len -= record->auxlen; +			pos += record->auxlen; +		} + +		break; +	} + +	case IGMP_MEMBERSHIP_QUERY: +		debug(DBG_IGMP, "igmp_membership_query"); +		break; + +	case IGMP_V2_LEAVE_GROUP: +		debug(DBG_IGMP, "igmp_v2_leave_group"); +		break; + +	default: +		debug(DBG_IGMP, "IGMP msg type %02hhx", igmp_msg->common.type); +		break; +	} + +	buf += hdr->length; +	len -= hdr->length; +} + +static void +igmp_read_cb(struct uring_task *task, int res) +{ +	struct igmp *igmp = container_of(task, struct igmp, task); + +	assert_return(task); +	assert_task_alive(DBG_IGMP, task); + +	debug(DBG_IGMP, "task %p, igmp %p, res %i", task, igmp, res); +	if (res < 0) { +		error("res: %i", res); +		return; +	} + +	task->tbuf->len = res; + +	if (task->saddr.storage.ss_family == AF_PACKET || +	    task->saddr.ll.sll_protocol == htons(ETH_P_IP)) +		igmp_parse(igmp); +	else +		debug(DBG_IGMP, "invalid packet type received"); + +	uring_tbuf_read(&igmp->task, igmp_read_cb); +} + +static void +igmp_free(struct uring_task *task) +{ +	struct igmp *igmp = container_of(task, struct igmp, task); + +	assert_return(task); +	debug(DBG_IGMP, "task %p, igmp %p", task, igmp); +	xfree(igmp); +} + +void +igmp_refdump() +{ +	assert_return_silent(cfg->igmp); + +	uring_task_refdump(&cfg->igmp->task); +} + +void +igmp_delete() +{ +	assert_return_silent(cfg->igmp); + +	debug(DBG_IGMP, "closing fd %i", cfg->igmp->task.fd); +	uring_task_destroy(&cfg->igmp->task); +	cfg->igmp = NULL; +} + +void +igmp_init() +{ +	static const struct sock_filter filter[] = { +		BPF_STMT(BPF_LD + BPF_W + BPF_LEN, 0),					/* A <- packet length */ +		BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, sizeof(struct iphdr), 1, 0),	/* A < sizeof(iphdr) */ +                BPF_STMT(BPF_RET + BPF_K, 0),						/* drop packet */ + +		BPF_STMT(BPF_LD + BPF_B + BPF_ABS, 0 /* iphdr[0] */),			/* A <- version + ihl */ +		BPF_STMT(BPF_ALU + BPF_RSH + BPF_K, 4),					/* A <- A >> 4 (version) */ +                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0x04, 1, 0),			/* A != 4 */ +                BPF_STMT(BPF_RET + BPF_K, 0),						/* drop packet */ + +		BPF_STMT(BPF_LD + BPF_B + BPF_ABS, offsetof(struct iphdr, protocol)),	/* A <- ip protocol */ +                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, IPPROTO_IGMP, 1, 0),		/* A != IPPROTO_IGMP */ +                BPF_STMT(BPF_RET + BPF_K, 0),						/* drop packet */ + +		BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(struct iphdr, daddr)),	/* A <- ip dst addr */ +                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, chtobe32(cinet_addr(224,0,2,60)), 2, 0),	/* A != 224.0.2.60 */ +                //BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0xe000023c, 2, 0),			/* A != 224.0.2.60 */ +                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, chtobe32(cinet_addr(224,0,0,22)), 1, 0),	/* A != 224.0.0.22 */ +                //BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0xe0000016, 1, 0),			/* A != 224.0.0.22 */ +                BPF_STMT(BPF_RET + BPF_K, 0),						/* drop packet */ + +		BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, 0 /* iphdr[0] */),			/* X <- pkt->ihl * 4 */ +		BPF_STMT(BPF_LD + BPF_IMM, 20),						/* A <- 20 */ +		BPF_JUMP(BPF_JMP + BPF_JGT + BPF_X, 0, 0, 1),				/* A > X */ +                BPF_STMT(BPF_RET + BPF_K, 0),						/* drop packet */ + +		BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(struct iphdr, tot_len)),	/* A <- ip tot_len */ +		BPF_JUMP(BPF_JMP + BPF_JGT + BPF_X, 0, 1, 0),				/* A <= ip->ihl * 4 */  +                BPF_STMT(BPF_RET + BPF_K, 0),						/* drop packet */ +		BPF_STMT(BPF_ALU + BPF_SUB + BPF_X, 0),					/* A <- A - X (bodylen) */ +		BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, 8, 1, 0),				/* A < 8 */ +                BPF_STMT(BPF_RET + BPF_K, 0),						/* drop packet */ + +		BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(struct iphdr, tot_len)),	/* A <- ip->tot_len */ +		BPF_STMT(BPF_MISC + BPF_TAX, 0),					/* X <- A */ +		BPF_STMT(BPF_LD + BPF_W + BPF_LEN, 0),					/* A <- packet length */ +		BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_X, 0, 1, 0),				/* A != ip->tot_len */ +                BPF_STMT(BPF_RET + BPF_K, 0),						/* drop packet */ + +                BPF_STMT(BPF_RET + BPF_K, (uint32_t) -1),				/* accept packet */ +        }; +        static const struct sock_fprog fprog = { +                .len = ARRAY_SIZE(filter), +                .filter = (struct sock_filter*) filter, +        }; +	struct sockaddr_ll addr = { +		.sll_family = AF_PACKET, +		.sll_ifindex = 0, +		.sll_pkttype = PACKET_MULTICAST, +	}; +	struct igmp *igmp; +	int sfd; +	int opt; + +	if (!cfg->do_igmp) { +		debug(DBG_IGMP, "igmp snooping disabled"); +		return; +	} + +	assert_return(!cfg->igmp); + +	igmp = zmalloc(sizeof(*igmp)); +	if (!igmp) +		return; + +	/* +	 * Kernel limitation, must be ETH_P_ALL, not ETH_P_IP or we won't get +	 * outgoing packets, https://lkml.org/lkml/1999/12/23/112 +	 */ +	sfd = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, htons(ETH_P_ALL)); +	if (sfd < 0) { +		if (errno == EACCES || errno == EPERM) +			info("Unable to do IGMP snooping, permission denied"); +		else +			error("socket: %m"); +		goto error; +	} + +	if (setsockopt(sfd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog)) < 0) { +		error("setsockopt(SO_ATTACH_FILTER): %m"); +		goto error; +	} + +	if (setsockopt(sfd, SOL_SOCKET, SO_LOCK_FILTER, &opt, sizeof(opt)) < 0) { +		error("setsockopt(SO_LOCK_FILTER): %m"); +		goto error; +	} + +	if (cfg->igmp_iface) { +		struct ifreq ifreq; +		int r; + +		r = snprintf(ifreq.ifr_name, sizeof(ifreq.ifr_name), +			     "%s", cfg->igmp_iface); +		if (r < 0 || r >= sizeof(ifreq.ifr_name)) +			die("invalid interface name: %s", cfg->igmp_iface); + +		if (ioctl(sfd, SIOCGIFINDEX, &ifreq) < 0) +			die("ioctl: %m"); + +		debug(DBG_IGMP, "using interface %s (%i)", +		      cfg->igmp_iface, ifreq.ifr_ifindex); + +		struct packet_mreq mreq = { +			.mr_ifindex = ifreq.ifr_ifindex, +			.mr_type = PACKET_MR_ALLMULTI +		}; + +		if (setsockopt(sfd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, +			       &mreq, sizeof(mreq)) < 0) { +			error("setsockopt(PACKET_ADD_MEMBERSHIP): %m"); +			goto error; +		} +	} + +	/* can't set .sll_protocol to htons(ETH_P_IP), see comment above */ +	if (bind(sfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { +		error("bind: %m"); +		goto error; +	} + +	debug(DBG_IGMP, "init successful, using fd %i", sfd); +	uring_task_init(&igmp->task, "igmp", uring_parent(), igmp_free); +	uring_task_set_fd(&igmp->task, sfd); +	uring_task_set_buf(&igmp->task, &igmp->tbuf); +	igmp->task.saddr.addrlen = sizeof(igmp->task.saddr.ll); +	uring_tbuf_recvmsg(&igmp->task, igmp_read_cb); +	 +	cfg->igmp = igmp; + +	return; + +error: +	close(sfd); +	xfree(igmp); +} diff --git a/mcserverproxy/igmp.h b/mcserverproxy/igmp.h new file mode 100644 index 0000000..80875b0 --- /dev/null +++ b/mcserverproxy/igmp.h @@ -0,0 +1,10 @@ +#ifndef fooigmphfoo +#define fooigmphfoo + +void igmp_refdump(); + +void igmp_delete(); + +void igmp_init(); + +#endif diff --git a/mcserverproxy/main.c b/mcserverproxy/main.c new file mode 100644 index 0000000..047e70e --- /dev/null +++ b/mcserverproxy/main.c @@ -0,0 +1,740 @@ +#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; +unsigned debug_mask = DBG_ERROR | DBG_INFO; + +/* 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; + +	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); +} diff --git a/mcserverproxy/main.h b/mcserverproxy/main.h new file mode 100644 index 0000000..256ddae --- /dev/null +++ b/mcserverproxy/main.h @@ -0,0 +1,175 @@ +#ifndef foomainhfoo +#define foomainhfoo + +#include <sys/socket.h> +#include <netinet/ip.h> + +struct cfg; + +#include "utils.h" + +extern struct cfg *cfg; +extern bool exiting; +extern unsigned debug_mask; + +enum debug_lvl { +	DBG_ERROR	= (0x1 << 1), +	DBG_INFO	= (0x1 << 2), +	DBG_VERBOSE	= (0x1 << 3), +	DBG_CFG		= (0x1 << 4), +	DBG_REF		= (0x1 << 5), +	DBG_MALLOC	= (0x1 << 6), +	DBG_ANN		= (0x1 << 7), +	DBG_SIG		= (0x1 << 8), +	DBG_UR		= (0x1 << 9), +	DBG_SRV		= (0x1 << 10), +	DBG_PROXY	= (0x1 << 11), +	DBG_RCON	= (0x1 << 12), +	DBG_IDLE	= (0x1 << 13), +	DBG_IGMP	= (0x1 << 14), +	DBG_SYSD	= (0x1 << 15), +	DBG_DNS		= (0x1 << 16), +	DBG_TIMER	= (0x1 << 17), +}; + +static inline bool +debug_enabled(enum debug_lvl lvl) +{ +	return !!(lvl & debug_mask); +} + +void __debug(enum debug_lvl lvl, const char *fmt, ...) __attribute__((format(printf, 2, 3))); + +#define __ifdebug(lvl, fmt, ...) 						\ +	do {									\ +		if (debug_enabled((lvl)))					\ +			__debug((lvl), fmt "\n"__VA_OPT__(,) __VA_ARGS__);	\ +	} while (0) + +#define debug(lvl, fmt, ...)	__ifdebug((lvl), "%s:%s:%i: " fmt,		\ +					  __FILE__, __func__, __LINE__ 		\ +					  __VA_OPT__(,) __VA_ARGS__) +#define verbose(fmt, ...)	__ifdebug(DBG_VERBOSE, fmt, __VA_ARGS__) +#define info(fmt, ...)		__ifdebug(DBG_INFO, fmt,  __VA_ARGS__) +#define error(fmt, ...)		__ifdebug(DBG_ERROR, "%s:%s:%i: " fmt,		\ +					  __FILE__, __func__, __LINE__ 		\ +					  __VA_OPT__(,) __VA_ARGS__) + +void __die(const char *fmt, ...) __attribute__((format(printf, 1, 2))); + +#define die(fmt, ...)								\ +	__die("%s:%s:%i: " fmt "\n",						\ +	      __FILE__, __func__, __LINE__					\ +	      __VA_OPT__(,) __VA_ARGS__) + +#define assert_log(expr, msg) 							\ +	((expr) ?								\ +	 (true) :								\ +	 (__debug(DBG_ERROR, "%s:%s:%i: assertion \"" msg "\" failed\n",	\ +		  __FILE__, __func__, __LINE__), false)) + +#define assert_return(expr, ...)						\ +	do {									\ +		if (!assert_log(expr, #expr))					\ +			return __VA_ARGS__;					\ +	} while (0) + +#define assert_return_silent(expr, ...)						\ +	do {									\ +		if (!(expr))							\ +			return __VA_ARGS__;					\ +	} while (0) + +#define assert_die(expr, msg)							\ +	do {									\ +		if (!assert_log(expr, #expr))					\ +			die(msg);						\ +	} while (0) + +#define assert_task_alive_or(lvl, t, cmd) 	\ +do {						\ +	if (!(t)) {				\ +		error("invalid task");		\ +		cmd;				\ +	}					\ +						\ +	if ((t)->dead) {			\ +		debug((lvl), "task dead");	\ +		cmd;				\ +	}					\ +} while(0) + +#define assert_task_alive(lvl, t) assert_task_alive_or((lvl), (t), return) + +void dump_tree(); + +struct uring_task; + +/* To save typing in all the function definitions below */ +typedef void (*utask_cb_t)(struct uring_task *, int res); +typedef int (*rutask_cb_t)(struct uring_task *, int res); + +struct uring_task_buf { +	char buf[4096]; +	size_t len; +	size_t done; +	struct iovec iov; +	struct msghdr msg; +}; + +struct uring_task { +	const char *name; +	unsigned refcount; +	int fd; +	struct uring_task *parent; +	void (*free)(struct uring_task *); +	bool dead; +	struct uring_task_buf *tbuf; + +	/* called once or repeatedly until is_complete_cb is satisfied */ +	utask_cb_t cb; + +	/* returns: 0 = not complete; < 0 = error; > 0 = complete */ +	rutask_cb_t is_complete_cb;  + +	/* called once tbuf processing is done */ +	utask_cb_t final_cb; + +	/* used for recvmsg/sendmsg */ +	struct saddr saddr; +	void *priv; +}; + +struct cfg { +	/* Options */ +	uid_t uid; +	gid_t gid; +	const char *cfg_dir; +	const char *cfg_file; +	bool do_igmp; +	char *igmp_iface; +	bool splice_supported; +	uint16_t announce_interval; +	uint16_t proxy_connection_interval; +	uint16_t proxy_connection_attempts; +	bool socket_defer; +	bool socket_freebind; +	bool socket_keepalive; +	bool socket_iptos; +	bool socket_nodelay; + +	/* Bookkeeping */ +	struct uring_ev *uring; +	struct server_cfg_monitor *server_cfg_monitor; +	struct signal_ev *signal; +	struct announce *announce; +	struct ptimer *ptimer; +	struct igmp *igmp; +	struct idle *idle; +	struct sd_bus *sd_bus; +	bool sd_bus_failed; +	struct uring_task task; +	struct list_head servers; +}; + +#endif diff --git a/mcserverproxy/meson.build b/mcserverproxy/meson.build new file mode 100644 index 0000000..e5fa7bc --- /dev/null +++ b/mcserverproxy/meson.build @@ -0,0 +1,36 @@ +mcproxy_sources = [ +	'main.c', +	'uring.c', +	'signal-handler.c', +	'server.c', +	'server-proxy.c', +	'server-config.c', +	'server-rcon.c', +	'announce.c', +	'config-parser.c', +	'idle.c', +	'ptimer.c', +	'igmp.c', +	'systemd.c', +	'utils.c' +] + +dep_liburing   = dependency('liburing') +dep_libsystemd = dependency('libsystemd') +dep_libcapng   = dependency('libcap-ng') + +mcproxy_deps = [ +	dep_liburing, +	dep_libsystemd, +	dep_libcapng, +	dep_config_h, +	dep_libshared, +] + +executable( +	'mcproxy', +	mcproxy_sources, +	link_args: [ '-lanl' ], +	dependencies: mcproxy_deps, +) + diff --git a/mcserverproxy/ptimer.c b/mcserverproxy/ptimer.c new file mode 100644 index 0000000..5f9cf5d --- /dev/null +++ b/mcserverproxy/ptimer.c @@ -0,0 +1,223 @@ +#include <inttypes.h> +#include <sys/timerfd.h> +#include <string.h> +#include <unistd.h> +#include <time.h> + +#include "main.h" +#include "uring.h" +#include "ptimer.h" + +struct ptimer { +	uint64_t value; +	time_t previous_time; +	struct uring_task task; +	unsigned task_count; +	struct list_head ptasks; +}; + +static void +ptimer_set(unsigned value, unsigned interval) +{ +	struct itimerspec tspec = { +		.it_interval = { +			.tv_sec  = interval, +			.tv_nsec = 0 +		}, +		.it_value = { +			.tv_sec  = value, +			.tv_nsec = 0 +		} +	}; + +	assert_return(cfg->ptimer && cfg->ptimer->task.fd >= 0); + +	if (timerfd_settime(cfg->ptimer->task.fd, 0, &tspec, NULL) != 0) +		error("timerfd_settime: %m"); +} + +static unsigned +gcd(unsigned a, unsigned b) +{ +	if (a == 0) +		return b; +	return gcd(b % a, a); +} + +static unsigned +array_gcd(unsigned arr[], unsigned n) +{ +	unsigned result = arr[0]; + +	for (unsigned i = 1; i < n; i++) +		result = gcd(arr[i], result); + +	return result; +} + +static void +ptimer_tick(struct ptimer *ptimer) +{ +	time_t now = time(NULL); +	unsigned diff = (unsigned)(now - ptimer->previous_time); +	struct ptimer_task *ptask, *ptmp; + +	debug(DBG_TIMER, "got a tick of %u secs", diff); +	list_for_each_entry_safe(ptask, ptmp, &ptimer->ptasks, list) { +		if (ptask->remain > diff) { +			ptask->remain -= diff; +			continue; +		} + +		debug(DBG_TIMER, "triggering ptask %p (times %u)", +		      ptask, ptask->times); + +		ptask->cb(ptask); +		ptask->remain = ptask->interval; + +		if (ptask->times == 0) +			continue; + +		ptask->times--; +		if (ptask->times == 0) { +			ptask->active = false; +			list_del(&ptask->list); +		} +	} + +	ptimer->previous_time = now; +} + +static void +ptimer_reconfig(struct ptimer *ptimer) +{ +	struct ptimer_task *ptask; +	unsigned i = 0; +	unsigned lowest = ~0; +	unsigned interval; + +	if (list_empty(&ptimer->ptasks)) { +		debug(DBG_TIMER, "no tasks"); +		ptimer_set(0, 0); +		return; +	} + +	unsigned intervals[ptimer->task_count]; + +	list_for_each_entry(ptask, &ptimer->ptasks, list) { +		if (ptask->remain < lowest) +			lowest = ptask->remain; +		intervals[i++] = ptask->interval; +	} + +	interval = array_gcd(intervals, i); + +	debug(DBG_TIMER, "lowest: %u, gcd: %u\n", lowest, interval); +	ptimer_set(lowest, interval); +} + +void +ptimer_del_task(struct ptimer_task *ptask) +{ +	struct ptimer *ptimer = cfg->ptimer; + +	assert_return(ptask && ptimer); +	assert_return_silent(ptask->active); +	assert_return(ptimer->task_count > 0); + +	list_del(&ptask->list); +	ptask->active = false; +	ptimer->task_count--; +	ptimer_tick(ptimer); +	ptimer_reconfig(ptimer); +	uring_task_put(&ptimer->task); +} + +void +ptimer_add_task(struct ptimer_task *ptask) +{ +	struct ptimer *ptimer = cfg->ptimer; + +	assert_return(ptask && ptask->interval > 0 && ptask->cb && ptimer); +	assert_return_silent(!ptask->active); + +	uring_task_get(&ptimer->task); +	ptask->active = true; +	ptask->remain = ptask->interval; +	ptimer_tick(ptimer); +	list_add(&ptask->list, &ptimer->ptasks); +	ptimer->task_count++; +	ptimer_reconfig(ptimer); +} + +void +ptimer_refdump() +{ +	assert_return(cfg->ptimer); + +	uring_task_refdump(&cfg->ptimer->task); +} + +static void +ptimer_free(struct uring_task *task) +{ +	struct ptimer *ptimer = container_of(task, struct ptimer, task); + +	assert_return(task); + +	debug(DBG_TIMER, "task %p, ptimer %p", task, ptimer); +	xfree(ptimer); +	cfg->ptimer = NULL; +} + +void +ptimer_delete() +{ +	assert_return(cfg->ptimer); + +	debug(DBG_TIMER, "closing fd %i", cfg->ptimer->task.fd); +	uring_task_destroy(&cfg->ptimer->task); +} + +static void +ptimer_cb(struct uring_task *task, int res) +{ +	struct ptimer *ptimer = container_of(task, struct ptimer, task); + +	assert_return(task); +	assert_task_alive(DBG_IGMP, task); + +	if (res != sizeof(ptimer->value)) { +		error("timerfd_read: res: %i, %m", res); +		return; +	} + +	ptimer_tick(ptimer); +	uring_read(&ptimer->task, &ptimer->value, sizeof(ptimer->value), ptimer_cb); +} + +void +ptimer_init() +{ +	struct ptimer *ptimer; +	int tfd; +  +	assert_return(!cfg->ptimer); + +	ptimer = zmalloc(sizeof(*ptimer)); +	if (!ptimer) +		die("malloc: %m"); + +	tfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC); +	if (tfd < 0) +		die("timerfd_create: %m"); + +	ptimer->task_count = 0; +	ptimer->previous_time = time(NULL); +	list_init(&ptimer->ptasks); +	uring_task_init(&ptimer->task, "ptimer", uring_parent(), ptimer_free); +	uring_task_set_fd(&ptimer->task, tfd); +	cfg->ptimer = ptimer; +	uring_read(&ptimer->task, &ptimer->value, sizeof(ptimer->value), ptimer_cb); +} + diff --git a/mcserverproxy/ptimer.h b/mcserverproxy/ptimer.h new file mode 100644 index 0000000..0b53590 --- /dev/null +++ b/mcserverproxy/ptimer.h @@ -0,0 +1,33 @@ +#ifndef fooptimerhfoo +#define fooptimerhfoo + +struct ptimer_task { +	unsigned interval; +	unsigned times; +	void (*cb)(struct ptimer_task *); +	bool active; +	unsigned remain; +	struct list_head list; +}; + +static inline void +ptask_init(struct ptimer_task *ptask, unsigned interval, +	   unsigned times, void(*cb)(struct ptimer_task *)) +{ +	ptask->interval = interval; +	ptask->times = times; +	ptask->cb = cb; +	ptask->active = false; +} + +void ptimer_del_task(struct ptimer_task *ptask); + +void ptimer_add_task(struct ptimer_task *ptask); + +void ptimer_refdump(); + +void ptimer_delete(); + +void ptimer_init(); + +#endif diff --git a/mcserverproxy/server-config.c b/mcserverproxy/server-config.c new file mode 100644 index 0000000..549cf16 --- /dev/null +++ b/mcserverproxy/server-config.c @@ -0,0 +1,580 @@ +#define _GNU_SOURCE +#include <stdio.h> +#include <ctype.h> +#include <string.h> +#include <stdbool.h> +#include <sys/inotify.h> +#include <dirent.h> +#include <errno.h> +#include <unistd.h> +#include <arpa/inet.h> +#include <inttypes.h> + +#include "main.h" +#include "uring.h" +#include "config-parser.h" +#include "server.h" +#include "server-config.h" + +static void +scfg_dns_cb(struct dns_async *dns, bool (*server_cb)(struct server *, struct saddr *)) +{ +	struct server *server; +	struct sockaddr_in *in4; +	struct sockaddr_in6 *in6; +	struct saddr *saddr; +	struct addrinfo *results = NULL, *ai; +	int r; + +	assert_return(dns && dns->priv && server_cb); + +	server = dns->priv; +	debug(DBG_DNS, "called, dns: %p, name: %s, server: %p, server->name: %s", +	      dns, dns->name, server, server->name); + +	r = gai_error(&dns->gcb); +	if (r == EAI_INPROGRESS) { +		/* This shouldn't happen, assume we'll get called again */ +		error("called with request in progress"); +		return; +	} else if (r == EAI_CANCELED) { +		/* The server must be in the process of going away */ +		goto out; +	} else if (r < 0) { +		error("DNS lookup of %s:%s failed: %s", +		      dns->name, dns->port, gai_strerror(r)); +		goto out; +	} + +	results = dns->gcb.ar_result; + +	for (ai = results; ai; ai = ai->ai_next) { +		saddr = zmalloc(sizeof(*saddr)); +		if (!saddr) { +			error("DNS lookup of %s:%s failed: %m", dns->name, dns->port); +			goto out; +		} + +		switch (ai->ai_family) { +		case AF_INET: +			in4 = (struct sockaddr_in *)ai->ai_addr; +			saddr_set_ipv4(saddr, in4->sin_addr.s_addr, in4->sin_port); +			server_cb(server, saddr); +			break; + +		case AF_INET6: +			in6 = (struct sockaddr_in6 *)ai->ai_addr; +			saddr_set_ipv6(saddr, &in6->sin6_addr, in6->sin6_port); +			server_cb(server, saddr); +			break; + +		default: +			error("getaddrinfo(%s:%s): unknown address family (%i)", +			      dns->name, dns->port, ai->ai_family); +			xfree(saddr); +			break; +		} +	} + +out: +	freeaddrinfo(results); +	list_del(&dns->list); +	xfree(dns); +	uring_task_put(&server->task); +	server_commit(server); +} + +static void +scfg_local_dns_cb(struct dns_async *dns) +{ +	assert_return(dns); + +	scfg_dns_cb(dns, server_add_local); +} + +static void +scfg_remote_dns_cb(struct dns_async *dns) +{ +	assert_return(dns); + +	scfg_dns_cb(dns, server_add_remote); +} + +static void +scfg_rcon_dns_cb(struct dns_async *dns) +{ +	assert_return(dns); + +	scfg_dns_cb(dns, server_add_rcon); +} + +enum scfg_keys { +	SCFG_KEY_INVALID = 0, +	SCFG_KEY_TYPE, +	SCFG_KEY_NAME, +	SCFG_KEY_PORT, +	SCFG_KEY_LOCAL, +	SCFG_KEY_REMOTE, +	SCFG_KEY_IDLE_TIMEOUT, +	SCFG_KEY_STOP_METHOD, +	SCFG_KEY_START_METHOD, +	SCFG_KEY_STOP_EXEC, +	SCFG_KEY_START_EXEC, +	SCFG_KEY_RCON, +	SCFG_KEY_RCON_PASSWORD, +	SCFG_KEY_SYSTEMD_SERVICE, +}; + +struct cfg_key_value_map scfg_key_map[] = { +	{ +		.key_name = "type", +		.key_value = SCFG_KEY_TYPE, +		.value_type = CFG_VAL_TYPE_STRING, +	}, { +		.key_name = "name", +		.key_value = SCFG_KEY_NAME, +		.value_type = CFG_VAL_TYPE_STRING, +	}, { +		.key_name = "port", +		.key_value = SCFG_KEY_PORT, +		.value_type = CFG_VAL_TYPE_UINT16, +	}, { +		.key_name = "local", +		.key_value = SCFG_KEY_LOCAL, +		.value_type = CFG_VAL_TYPE_ASYNC_ADDRS, +	}, { +		.key_name = "remote", +		.key_value = SCFG_KEY_REMOTE, +		.value_type = CFG_VAL_TYPE_ASYNC_ADDRS, +	}, { +		.key_name = "idle_timeout", +		.key_value = SCFG_KEY_IDLE_TIMEOUT, +		.value_type = CFG_VAL_TYPE_UINT16, +	}, { +		.key_name = "stop_method", +		.key_value = SCFG_KEY_STOP_METHOD, +		.value_type = CFG_VAL_TYPE_STRING, +	}, { +		.key_name = "start_method", +		.key_value = SCFG_KEY_START_METHOD, +		.value_type = CFG_VAL_TYPE_STRING, +	}, { +		.key_name = "stop_exec", +		.key_value = SCFG_KEY_STOP_EXEC, +		.value_type = CFG_VAL_TYPE_STRING, +	}, { +		.key_name = "start_exec", +		.key_value = SCFG_KEY_START_EXEC, +		.value_type = CFG_VAL_TYPE_STRING, +	}, { +		.key_name = "rcon", +		.key_value = SCFG_KEY_RCON, +		.value_type = CFG_VAL_TYPE_ASYNC_ADDRS, +	}, { +		.key_name = "rcon_password", +		.key_value = SCFG_KEY_RCON_PASSWORD, +		.value_type = CFG_VAL_TYPE_STRING, +	}, { +		.key_name = "systemd_service", +		.key_value = SCFG_KEY_SYSTEMD_SERVICE, +		.value_type = CFG_VAL_TYPE_STRING, +	}, { +		.key_name = NULL, +		.key_value = SCFG_KEY_INVALID, +		.value_type = CFG_VAL_TYPE_INVALID, +	} +}; + +static bool +handle_dns(struct server *server, const char *type, +	   struct cfg_value *value, dns_cb_t *async_cb, +	   bool (*sync_cb)(struct server *, struct saddr *)) +{ +	struct saddr *saddr, *tmp; +	struct dns_async *dns; + +	assert_return(server && type && value && async_cb && sync_cb, false); + +	switch (value->type) { +	case CFG_VAL_TYPE_ADDRS: +		debug(DBG_DNS, "%s: got immediate addrs", type); + +		list_for_each_entry_safe(saddr, tmp, &value->saddrs, list) { +			list_del(&saddr->list); +			sync_cb(server, saddr); +		} +		return true; + +	case CFG_VAL_TYPE_ASYNC_ADDRS: +		debug(DBG_DNS, "%s: doing async lookup of DNS record: %p", +		      type, value->dns_async); + +		dns = value->dns_async; +		dns->cb = async_cb; +		dns->priv = server; +		list_add(&dns->list, &server->dnslookups); +		uring_task_get(&server->task); +		return true; + +	default: +		return false; +	} +} + +static void +scfg_parse(struct server *server) +{ +	char *pos; + +	assert_return(server); + +	pos = server->tbuf.buf; + +	if (!config_parse_header(server->name, "server", &pos)) +		return; + +	while (true) { +		int key; +		const char *keyname; +		struct cfg_value value; + +		if (!config_parse_line(server->name, &pos, scfg_key_map, +				       &key, &keyname, &value)) +			break; + +		if (key == SCFG_KEY_INVALID) +			break; + +		debug(DBG_CFG, "%s: key %s", server->name, keyname); + +		switch (key) { + +		case SCFG_KEY_TYPE: +			if (streq(value.str, "proxy")) { +				if (!server_set_type(server, SERVER_TYPE_PROXY)) +					return; +			} else if (streq(value.str, "announce")) { +				if (!server_set_type(server, SERVER_TYPE_ANNOUNCE)) +					return; +			} +			break; + +		case SCFG_KEY_NAME: +			if (!server_set_pretty_name(server, value.str)) +				return; +			break; + +		case SCFG_KEY_PORT: +			if (!server_set_port(server, value.uint16)) +				return; +			break; + +		case SCFG_KEY_LOCAL: +			if (!handle_dns(server, "local", &value, +					scfg_local_dns_cb, server_add_local)) +				return; +			break; + +		case SCFG_KEY_REMOTE: +			if (!handle_dns(server, "remote", &value, +					scfg_remote_dns_cb, server_add_remote)) +				return; +			break; + +		case SCFG_KEY_IDLE_TIMEOUT: +			if (!server_set_idle_timeout(server, value.uint16)) +				return; +			break; + +		case SCFG_KEY_STOP_METHOD: +			if (streq(value.str, "exec")) { +				if (server_set_stop_method(server, SERVER_STOP_METHOD_EXEC)) +					break; +			} else if (streq(value.str, "rcon")) { +				if (server_set_stop_method(server, SERVER_STOP_METHOD_RCON)) +					break; +			} else if (streq(value.str, "systemd")) { +				if (server_set_stop_method(server, SERVER_STOP_METHOD_SYSTEMD)) +					break; +			} +			return; + +		case SCFG_KEY_START_METHOD: +			if (streq(value.str, "exec")) { +				if (server_set_start_method(server, SERVER_START_METHOD_EXEC)) +					break; +			} else if (streq(value.str, "systemd")) { +				if (server_set_start_method(server, SERVER_START_METHOD_SYSTEMD)) +					break; +			} +			return; + +		case SCFG_KEY_STOP_EXEC: +			if (!server_set_stop_exec(server, value.str)) +				return; +			break; + +		case SCFG_KEY_START_EXEC: +			if (!server_set_start_exec(server, value.str)) +				return; +			break; + +		case SCFG_KEY_RCON: +			if (!handle_dns(server, "rcon", &value, +					scfg_rcon_dns_cb, server_add_rcon)) +				return; +			break; + +		case SCFG_KEY_RCON_PASSWORD: +			if (!server_set_rcon_password(server, value.str)) +				return; +			break; + +		case SCFG_KEY_SYSTEMD_SERVICE: +			if (!server_set_systemd_service(server, value.str)) +				return; +			break; + +		case SCFG_KEY_INVALID: +		default: +			break; +		} +	} +} + +static void +scfg_read_cb(struct uring_task *task, int res) +{ +	struct server *server = container_of(task, struct server, task); + +	assert_return(task); +	assert_task_alive(DBG_CFG, task); + +	if (res <= 0) { +		error("error reading config file for %s: %s", +		      server->name, strerror(-res)); +		server_delete(server); +	} + +	debug(DBG_CFG, "%s: parsing cfg (%i bytes)", server->name, res); +	uring_task_close_fd(&server->task); +	scfg_parse(server); +	server_commit(server); +} + +static void  +scfg_open_cb(struct uring_task *task, int res) +{ +	struct server *server = container_of(task, struct server, task); + +	assert_return(task); +	assert_task_alive(DBG_CFG, task); + +	if (res < 0) { +		error("open(%s) failed: %s", server->name, strerror(-res)); +		server_delete(server); +		return; +	} + +	debug(DBG_CFG, "reading server cfg %s (fd %i)", server->name, res); +	uring_task_set_fd(&server->task, res); +	uring_tbuf_read_until_eof(&server->task, scfg_read_cb); +} + +static bool +scfg_valid_filename(const char *name) +{ +	const char *suffix; + +	if (empty_str(name)) +		return false; +	if (name[0] == '.') +		return false; +	if ((suffix = strrchr(name, '.')) == NULL) +		return false; +	if (!streq(suffix, ".server")) +		return false; + +	return true; +} + +struct server_cfg_monitor { +	struct uring_task task; +	char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event)))); +}; + +static void +scfgm_free(struct uring_task *task) +{ +	struct server_cfg_monitor *scfgm = container_of(task, +							struct server_cfg_monitor, +							task); + +	assert_return(task); + +	debug(DBG_CFG, "called"); +	xfree(scfgm); +	cfg->server_cfg_monitor = NULL; +} + +static void +inotify_event_dump(const struct inotify_event *event) +{ +	assert_return(event); + +	debug(DBG_CFG, "inotify event:"); +	debug(DBG_CFG, " * WD     : %i", event->wd); +	debug(DBG_CFG, " * Cookie : %" PRIu32, event->cookie); +	debug(DBG_CFG, " * Length : %" PRIu32, event->len); +	debug(DBG_CFG, " * Name   : %s", event->name); +	debug(DBG_CFG, " * Mask   : %" PRIu32, event->mask); +	if (event->mask & IN_ACCESS) +		debug(DBG_CFG, "\tIN_ACCESS"); +	else if(event->mask & IN_MODIFY) +		debug(DBG_CFG, "\tIN_MODIFY"); +	else if(event->mask & IN_ATTRIB) +		debug(DBG_CFG, "\tIN_ATTRIB"); +	else if(event->mask & IN_CLOSE_WRITE) +		debug(DBG_CFG, "\tIN_CLOSE_WRITE"); +	else if(event->mask & IN_CLOSE_NOWRITE) +		debug(DBG_CFG, "\tIN_CLOSE_NOWRITE"); +	else if(event->mask & IN_OPEN) +		debug(DBG_CFG, "\tIN_OPEN"); +	else if(event->mask & IN_MOVED_FROM) +		debug(DBG_CFG, "\tIN_MOVED_FROM"); +	else if(event->mask & IN_MOVED_TO) +		debug(DBG_CFG, "\tIN_MOVED_TO"); +	else if(event->mask & IN_CREATE) +		debug(DBG_CFG, "\tIN_CREATE"); +	else if(event->mask & IN_DELETE) +		debug(DBG_CFG, "\tIN_DELETE"); +	else if(event->mask & IN_DELETE_SELF) +		debug(DBG_CFG, "\tIN_DELETE_SELF"); +	else if(event->mask & IN_MOVE_SELF) +		debug(DBG_CFG, "\tIN_MOVE_SELF"); +	else if(event->mask & IN_UNMOUNT) +		debug(DBG_CFG, "\tIN_UNMOUNT"); +	else if(event->mask & IN_Q_OVERFLOW) +		debug(DBG_CFG, "\tIN_Q_OVERFLOW"); +	else if(event->mask & IN_IGNORED) +		debug(DBG_CFG, "\tIN_IGNORED"); +} + +static void +inotify_cb(struct uring_task *task, int res) +{ +	struct server_cfg_monitor *scfgm = container_of(task, +							struct server_cfg_monitor, +							task); +	const struct inotify_event *event; +	char *ptr; +	struct server *server; + +	assert_return(task); +	assert_task_alive(DBG_CFG, task); + +	if (res <= 0) { +		error("inotify_read: %i", res); +		return; +	} + +	for (ptr = scfgm->buf; ptr < scfgm->buf + res; ptr += sizeof(struct inotify_event) + event->len) { +		event = (const struct inotify_event *)ptr; + +		if (debug_enabled(DBG_CFG)) +			inotify_event_dump(event); + +		if (event->mask & (IN_IGNORED | IN_MOVE_SELF | IN_DELETE_SELF | IN_UNMOUNT)) +			die("configuration directory gone, exiting"); + +		if (event->mask & IN_Q_OVERFLOW) { +			error("inotify queue overflow"); +			continue; +		} + +		if (!scfg_valid_filename(event->name)) +			continue; + +		if (event->mask & (IN_MOVED_FROM | IN_DELETE)) +			server_delete_by_name(event->name); +		else if (event->mask & (IN_MOVED_TO | IN_CREATE | IN_CLOSE_WRITE)) { +			server = server_new(event->name); +			verbose("New server config file detected: %s", server->name); +			uring_openat(&server->task, server->name, scfg_open_cb); +		} else +			error("inotify: unknown event: 0x%08x", event->mask); +	} + +	uring_read(&scfgm->task, scfgm->buf, sizeof(scfgm->buf), inotify_cb); +} + +void +server_cfg_monitor_refdump() +{ +	assert_return_silent(cfg->server_cfg_monitor); + +	uring_task_refdump(&cfg->server_cfg_monitor->task); +} + +void +server_cfg_monitor_delete() +{ +	assert_return(cfg->server_cfg_monitor); + +	debug(DBG_CFG, "closing fd %i", cfg->server_cfg_monitor->task.fd); +	uring_task_destroy(&cfg->server_cfg_monitor->task); +	cfg->server_cfg_monitor = NULL; +} + +void +server_cfg_monitor_init() +{ +	int ifd; +	int iwd; +	struct server_cfg_monitor *scfgm; +	DIR *dir; +	struct dirent *dent; +	struct server *server; + +	assert_return(!cfg->server_cfg_monitor); + +	scfgm = zmalloc(sizeof(*scfgm)); +	if (!scfgm) +		die("malloc: %m"); + +	ifd = inotify_init1(IN_CLOEXEC); +	if (ifd < 0) +		die("inotify_init1: %m"); + +	/* ln = IN_CREATE, cp/vi/mv = IN_CREATE, IN_OPEN, IN_CLOSE_WRITE */ +	iwd = inotify_add_watch(ifd, ".", +				IN_CLOSE_WRITE | IN_DELETE | IN_CREATE | +				IN_DELETE_SELF | IN_MOVE_SELF | IN_MOVED_TO | +				IN_MOVED_FROM | IN_DONT_FOLLOW | +				IN_EXCL_UNLINK | IN_ONLYDIR ); +	if (iwd < 0) +		die("inotify_add_watch: %m"); + +	uring_task_init(&scfgm->task, "server-config-monitor", uring_parent(), scfgm_free); +	uring_task_set_fd(&scfgm->task, ifd); +	cfg->server_cfg_monitor = scfgm; +	uring_read(&scfgm->task, scfgm->buf, sizeof(scfgm->buf), inotify_cb); + +	dir = opendir("."); +	if (!dir) +		die("opendir(%s): %m", cfg->cfg_dir); + +	while ((dent = readdir(dir)) != NULL) { +		if (dent->d_type != DT_REG && dent->d_type != DT_UNKNOWN) +			continue; +		if (!scfg_valid_filename(dent->d_name)) +			continue; + +		server = server_new(dent->d_name); +		if (server) +			uring_openat(&server->task, server->name, scfg_open_cb); +	} + +	closedir(dir); +} + diff --git a/mcserverproxy/server-config.h b/mcserverproxy/server-config.h new file mode 100644 index 0000000..590dae0 --- /dev/null +++ b/mcserverproxy/server-config.h @@ -0,0 +1,10 @@ +#ifndef fooserverconfighfoo +#define fooserverconfighfoo + +void server_cfg_monitor_delete(); + +void server_cfg_monitor_refdump(); + +void server_cfg_monitor_init(); + +#endif diff --git a/mcserverproxy/server-proxy.c b/mcserverproxy/server-proxy.c new file mode 100644 index 0000000..4cbbb87 --- /dev/null +++ b/mcserverproxy/server-proxy.c @@ -0,0 +1,578 @@ +#define _GNU_SOURCE +#include <stdio.h> +#include <unistd.h> +#include <time.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <fcntl.h> +#include <unistd.h> + +#include "main.h" +#include "uring.h" +#include "ptimer.h" +#include "server.h" +#include "server-proxy.h" +#include "utils.h" + +static void +format_bytes(char *buf, size_t len, uint64_t val) +{ +	uint64_t tmp; +	const char *suffix = "B"; + +	assert_return(buf && len > 0); + +	tmp = val * 10; +	if (val > 1152921504606846976ULL) { +		tmp = val / 115292150460684697ULL; +		suffix= "EiB"; +	} else if (val > 1125899906842624ULL) { +		tmp /= 1125899906842624ULL; +		suffix = "PiB"; +	} else  if (val > 1099511627776ULL) { +		tmp /= 1099511627776ULL; +		suffix = "TiB"; +	} else if (val > 1073741824ULL) { +		tmp /= 1073741824ULL; +		suffix = "GiB"; +	} else if (val > 1048576) { +		tmp /= 1048576; +		suffix = "MiB"; +	} else if (val > 1024) { +		tmp /= 1024; +		suffix = "KiB"; +	} + +	snprintf(buf, len, "%lu.%lu %s", tmp / 10, tmp % 10, suffix); +} + +static void +format_time(char *buf, size_t len, time_t diff) +{ +	unsigned hh, mm, ss; + +	assert_return(buf && len > 0); + +	hh = diff / 3600; +	diff %= 3600; +	mm = diff / 60; +	diff %= 60; +	ss = diff; + +	snprintf(buf, len, "%02u:%02u:%02u", hh, mm, ss); +} + +static void +proxy_free(struct uring_task *task) +{ +	struct server_proxy *proxy = container_of(task, struct server_proxy, task); +	char cts[100]; +	char stc[100]; +	char duration[100]; + +	assert_return(task); + +	debug(DBG_PROXY, "server: %s, src: %s, dst: %s", +	      proxy->server->name, +	      proxy->client_conn.remote.addrstr, +	      proxy->server_conn.remote.addrstr); + +	if (proxy->begin > 0) { +		format_time(duration, sizeof(duration), time(NULL) - proxy->begin); +		format_bytes(cts, sizeof(cts), proxy->client_bytes); +		format_bytes(stc, sizeof(stc), proxy->server_bytes); + +		info("%s: proxy connection %s -> %s closed " +		     "(CtS: %s, StC: %s), duration %s", +		     proxy->server->name, +		     proxy->client_conn.remote.addrstr, +		     proxy->server_conn.remote.addrstr, +		     cts, stc, duration); +	} + +	list_del(&proxy->list); +	xfree(proxy); +} + +static void +proxy_client_free(struct uring_task *task) +{ +	struct server_proxy *proxy = container_of(task, struct server_proxy, clienttask); + +	assert_return(task); + +	debug(DBG_PROXY, "%s: client connection closed", proxy->server->name); +} + +static void +proxy_server_free(struct uring_task *task) +{ +	struct server_proxy *proxy = container_of(task, struct server_proxy, servertask); + +	assert_return(task); + +	debug(DBG_PROXY, "%s: server connection closed", proxy->server->name); +} + +void +proxy_delete(struct server_proxy *proxy) +{ +	debug(DBG_PROXY, "%s: shutting down proxy %p", proxy->server->name, proxy); + +	assert_return(proxy); + +	ptimer_del_task(&proxy->ptask); + +	if (cfg->splice_supported) { +		uring_close(&proxy->server->task, proxy->cpipe[PIPE_RD]); +		uring_close(&proxy->server->task, proxy->cpipe[PIPE_WR]); +		uring_close(&proxy->server->task, proxy->spipe[PIPE_RD]); +		uring_close(&proxy->server->task, proxy->spipe[PIPE_WR]); +	} + +	uring_task_set_fd(&proxy->servertask, proxy->sfd); +	uring_task_destroy(&proxy->servertask); +	uring_task_set_fd(&proxy->clienttask, proxy->cfd); +	uring_task_destroy(&proxy->clienttask); +	uring_task_destroy(&proxy->task); +} + +/* + * These four functions provide the fallback read-write mode + */ +static void proxy_client_read(struct uring_task *task, int res); + +static void +proxy_client_written(struct uring_task *task, int res) +{ +	struct server_proxy *proxy = container_of(task, struct server_proxy, clienttask); + +	assert_return(task); +	assert_task_alive(DBG_PROXY, task); + +	if (res <= 0) { +		debug(DBG_PROXY, "%s: res: %i", proxy->server->name, res); +		proxy_delete(proxy); +		return; +	} + +	proxy->client_bytes += res; +	uring_task_set_fd(&proxy->clienttask, proxy->cfd); +	uring_tbuf_read(task, proxy_client_read); +} + +static void +proxy_client_read(struct uring_task *task, int res) +{ +	struct server_proxy *proxy = container_of(task, struct server_proxy, clienttask); + +	assert_return(task); +	assert_task_alive(DBG_PROXY, task); + +	if (res <= 0) { +		debug(DBG_PROXY, "%s: res: %i", proxy->server->name, res); +		proxy_delete(proxy); +		return; +	} + +	uring_task_set_fd(&proxy->clienttask, proxy->sfd); +	uring_tbuf_write(task, proxy_client_written); +} + +static void proxy_server_read(struct uring_task *task, int res); + +static void +proxy_server_written(struct uring_task *task, int res) +{ +	struct server_proxy *proxy = container_of(task, struct server_proxy, servertask); + +	assert_return(task); +	assert_task_alive(DBG_PROXY, task); + +	if (res <= 0) { +		debug(DBG_PROXY, "%s: res: %i", proxy->server->name, res); +		proxy_delete(proxy); +		return; +	} + +	proxy->server_bytes += res; +	uring_task_set_fd(&proxy->servertask, proxy->sfd); +	uring_tbuf_read(&proxy->servertask, proxy_server_read); +} + +static void +proxy_server_read(struct uring_task *task, int res) +{ +	struct server_proxy *proxy = container_of(task, struct server_proxy, servertask); + +	assert_return(task); +	assert_task_alive(DBG_PROXY, task); + +	if (res <= 0) { +		debug(DBG_PROXY, "%s: res: %i", proxy->server->name, res); +		proxy_delete(proxy); +		return; +	} + +	uring_task_set_fd(&proxy->servertask, proxy->cfd); +	uring_tbuf_write(task, proxy_server_written); +} + +/* + * These four functions provide the splice fd->pipe->fd mode + */ +static void proxy_client_spliced_in(struct uring_task *task, int res); + +static void +proxy_client_spliced_out(struct uring_task *task, int res) +{ +	struct server_proxy *proxy = container_of(task, struct server_proxy, servertask); + +	assert_return(task); +	assert_task_alive(DBG_PROXY, task); + +	if (res <= 0) { +		debug(DBG_PROXY, "%s: res: %i", proxy->server->name, res); +		proxy_delete(proxy); +		return; +	} + +	uring_splice(task, proxy->cfd, proxy->cpipe[PIPE_WR], proxy_client_spliced_in); +} + +static void +proxy_client_spliced_in(struct uring_task *task, int res) +{ +	struct server_proxy *proxy = container_of(task, struct server_proxy, servertask); + +	assert_return(task); +	assert_task_alive(DBG_PROXY, task); + +	if (res <= 0) { +		debug(DBG_PROXY, "%s: res: %i", proxy->server->name, res); +		proxy_delete(proxy); +		return; +	} + +	uring_splice(task, proxy->cpipe[PIPE_RD], proxy->sfd, proxy_client_spliced_out); +} + +static void proxy_server_spliced_in(struct uring_task *task, int res); + +static void +proxy_server_spliced_out(struct uring_task *task, int res) +{ +	struct server_proxy *proxy = container_of(task, struct server_proxy, servertask); + +	assert_return(task); +	assert_task_alive(DBG_PROXY, task); + +	if (res <= 0) { +		debug(DBG_PROXY, "%s: res: %i", proxy->server->name, res); +		proxy_delete(proxy); +		return; +	} + +	uring_splice(task, proxy->sfd, proxy->spipe[PIPE_WR], proxy_server_spliced_in); +} + +static void +proxy_server_spliced_in(struct uring_task *task, int res) +{ +	struct server_proxy *proxy = container_of(task, struct server_proxy, servertask); + +	assert_return(task); +	assert_task_alive(DBG_PROXY, task); + +	if (res <= 0) { +		debug(DBG_PROXY, "%s: res: %i", proxy->server->name, res); +		proxy_delete(proxy); +		return; +	} + +	uring_splice(task, proxy->spipe[PIPE_RD], proxy->cfd, proxy_server_spliced_out); +} + +static void +proxy_connected_cb(struct connection *conn, bool connected) +{ +	struct server_proxy *proxy = container_of(conn, struct server_proxy, server_conn); + +	assert_return(conn); +	assert_task_alive(DBG_PROXY, &proxy->clienttask); +	assert_task_alive(DBG_PROXY, &proxy->servertask); + +	proxy->connecting = false; +	if (!connected) { +		error("%s: proxy connection to remote server failed", +		      proxy->server->name); +		if (!proxy->ptask.active) +			proxy_delete(proxy); +		return; +	} + +	ptimer_del_task(&proxy->ptask); + +	proxy->sfd = proxy->servertask.fd; +	verbose("%s: proxy connection %s -> %s opened", +		proxy->server->name, +		proxy->client_conn.remote.addrstr, +		proxy->server_conn.remote.addrstr); +	proxy->begin = time(NULL); + +	if (cfg->splice_supported) { +		debug(DBG_PROXY, "handling proxy connection with splice"); +		uring_splice(&proxy->clienttask, proxy->cfd, proxy->cpipe[PIPE_WR], proxy_client_spliced_in); +		uring_splice(&proxy->servertask, proxy->sfd, proxy->spipe[PIPE_WR], proxy_server_spliced_in); +	} else { +		debug(DBG_PROXY, "handling proxy connection with read-write"); +		uring_tbuf_read(&proxy->clienttask, proxy_client_read); +		uring_tbuf_read(&proxy->servertask, proxy_server_read); +	} +} + +void +proxy_refdump(struct server_proxy *proxy) +{ +	assert_return(proxy); + +	uring_task_refdump(&proxy->task); +	uring_task_refdump(&proxy->clienttask); +	uring_task_refdump(&proxy->servertask); +} + +static void +proxy_connect_timer_cb(struct ptimer_task *ptask) +{ +	struct server_proxy *proxy = container_of(ptask, struct server_proxy, ptask); + +	assert_return(ptask); + +	if (proxy->connecting) +		return; + +	proxy->connecting = true; +	connect_any(&proxy->servertask, &proxy->server->remotes, +		    &proxy->server_conn, proxy_connected_cb); +} + +struct server_proxy * +proxy_new(struct server *server, struct saddr *client, int fd) +{ +	struct server_proxy *proxy; + +	assert_return(server && client && fd > 0, NULL); + +	proxy = zmalloc(sizeof(*proxy)); +	if (!proxy) { +		error("malloc: %m"); +		goto out; +	} + +	if (cfg->splice_supported) { +		if (pipe2(proxy->cpipe, O_CLOEXEC) < 0) { +			error("pipe2: %m"); +			goto out_free; +		} + +		if (pipe2(proxy->spipe, O_CLOEXEC) < 0) { +			error("pipe2: %m"); +			goto out_close_cpipe; +		} +	} + +	proxy->sfd = -1; +	proxy->cfd = fd; +	proxy->server = server; +	uring_task_init(&proxy->task, "proxy", &server->task, proxy_free); + +	connection_set_local(&proxy->client_conn, fd); +	connection_set_remote(&proxy->client_conn, client); + +	uring_task_init(&proxy->clienttask, "proxy_client", &proxy->task, +			proxy_client_free); +	uring_task_set_buf(&proxy->clienttask, &proxy->clientbuf); +	uring_task_set_fd(&proxy->clienttask, fd); + +	uring_task_init(&proxy->servertask, "proxy_server", &proxy->task, +			proxy_server_free); +	uring_task_set_buf(&proxy->servertask, &proxy->serverbuf); + +	list_add(&proxy->list, &server->proxys); + +	if (server->state != SERVER_STATE_RUNNING) { +		if (server_start(server) && +		    cfg->proxy_connection_interval > 0 && +		    cfg->proxy_connection_attempts > 0) { +			ptask_init(&proxy->ptask, +				   cfg->proxy_connection_interval, +				   cfg->proxy_connection_attempts, +				   proxy_connect_timer_cb); +			ptimer_add_task(&proxy->ptask); +		} +	} + +	proxy->connecting = true; +	connect_any(&proxy->servertask, &server->remotes, +		    &proxy->server_conn, proxy_connected_cb); + +	return proxy; + +out_close_cpipe: +	uring_close(&server->task, proxy->cpipe[PIPE_RD]); +	uring_close(&server->task, proxy->cpipe[PIPE_WR]); +out_free: +	xfree(proxy); +out: +	return NULL; +} + +static void +local_accept(struct uring_task *task, int res) +{ +	struct server_local *local = container_of(task, struct server_local, task); +	struct server *server = container_of(task->parent, struct server, task); +	struct server_proxy *proxy; + +	assert_return(task); +	assert_task_alive(DBG_PROXY, task); + +	debug(DBG_PROXY, "task %p, res %i, server %s", task, res, server->name); + +	if (res < 0) { +		error("res: %i", res); +		goto out; +	} + +	saddr_set_addrstr(&local->client); + +	verbose("%s: incoming proxy connection: %s -> %s", +		server->name, local->client.addrstr, local->local.addrstr); + +	if (list_empty(&server->remotes)) { +		/* This shouldn't be possible, checked before opening local */ +		error("server->remotes empty!"); +		uring_close(&local->task, res); +		goto out; +	} + +	proxy = proxy_new(server, &local->client, res); +	if (!proxy) +		uring_close(&local->task, res); + +out: +	uring_accept(&local->task, &local->client, local_accept); +} + +bool +local_open(struct server_local *local) +{ +	int sfd; +	int option; +	int r; + +	assert_return(local && local->server, false); + +	sfd = socket(local->local.storage.ss_family, SOCK_STREAM | SOCK_CLOEXEC, 0); +	if (sfd < 0) { +		error("socket: %m"); +		goto error; +	} + +	option = true; +	if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option)) < 0) { +		error("setsockopt: %m"); +		goto error; +	} + +	/* The MC protocol expects the client to send data first */ +	if (cfg->socket_defer) { +		option = true; +		if (setsockopt(sfd, IPPROTO_TCP, TCP_DEFER_ACCEPT, &option, sizeof(option)) < 0) +			error("setsockopt: %m"); +	} + +	/* +	 * This has the advantage that interfaces don't need to be up but +	 * it means that cfg errors will not be caught. +	 */ +	if (cfg->socket_freebind) { +		option = true; +		if (setsockopt(sfd, IPPROTO_IP, IP_FREEBIND, &option, sizeof(option)) < 0) +			error("setsockopt: %m"); +	} + +	socket_set_low_latency(sfd); + +	r = bind(sfd, (struct sockaddr *)&local->local.storage, local->local.addrlen); +	if (r < 0) { +		error("bind: %m"); +		goto error; +	} + +	r = listen(sfd, 100); +	if (r < 0) { +		error("listen: %m"); +		goto error; +	} + +	uring_task_set_fd(&local->task, sfd); +	uring_accept(&local->task, &local->client, local_accept); +	return true; + +error: +	if (sfd >= 0) +		uring_close(&local->task, sfd); +	return false; +} + +void +local_refdump(struct server_local *local) +{ +	assert_return(local); + +	uring_task_refdump(&local->task); +} + +static void +local_free(struct uring_task *task) +{ +	struct server_local *local = container_of(task, struct server_local, task); + +	assert_return(task); + +	debug(DBG_PROXY, "task %p, local %p", task, local); +	list_del(&local->list); +	xfree(local); +} + +void +local_delete(struct server_local *local) +{ +	assert_return(local); + +	uring_task_destroy(&local->task); +} + +struct server_local * +local_new(struct server *server, struct saddr *saddr) +{ +	struct server_local *local; + +	assert_return(server && saddr, NULL); + +	local = zmalloc(sizeof(*local)); +	if (!local) { +		error("malloc: %m"); +		return NULL; +	} + +	debug(DBG_PROXY, "%s adding local: %s", server->name, saddr->addrstr); +	local->local = *saddr; +	local->server = server; +	uring_task_init(&local->task, "local", &server->task, local_free); +	xfree(saddr); +	return local; +} + diff --git a/mcserverproxy/server-proxy.h b/mcserverproxy/server-proxy.h new file mode 100644 index 0000000..ee3bda3 --- /dev/null +++ b/mcserverproxy/server-proxy.h @@ -0,0 +1,51 @@ +#ifndef fooserverproxyhfoo +#define fooserverproxyhfoo + +struct server_proxy { +	struct connection client_conn; +	struct uring_task_buf clientbuf; +	struct uring_task clienttask; +	uint64_t client_bytes; +	int cpipe[2]; +	int cfd; + +	struct connection server_conn; +	struct uring_task_buf serverbuf; +	struct uring_task servertask; +	uint64_t server_bytes; +	int spipe[2]; +	int sfd; +	 +	bool connecting; +	time_t begin; +	struct ptimer_task ptask; +	struct uring_task task; +	struct server *server; +	struct list_head list; +}; + +void proxy_refdump(struct server_proxy *proxy); + +void proxy_delete(struct server_proxy *proxy); + +struct server_proxy *proxy_new(struct server *server, struct saddr *client, +			       int fd); + +struct server_local { +	struct saddr local; +	struct saddr client; +	struct uring_task task; + +	struct server *server; +	struct list_head list; +}; + +bool local_open(struct server_local *local); + +void local_refdump(struct server_local *local); + +void local_delete(struct server_local *local); + +struct server_local *local_new(struct server *server, struct saddr *saddr); + +#endif diff --git a/mcserverproxy/server-rcon.c b/mcserverproxy/server-rcon.c new file mode 100644 index 0000000..1f8ef70 --- /dev/null +++ b/mcserverproxy/server-rcon.c @@ -0,0 +1,227 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/ip.h> +#include <arpa/inet.h> +#include <stdint.h> +#include <inttypes.h> +#include <errno.h> + +#include "main.h" +#include "uring.h" +#include "server.h" +#include "server-rcon.h" +#include "rcon-protocol.h" + +static int +rcon_packet_complete(struct uring_task *task, int res) +{ +	assert_return(task, -EINVAL); +	assert_task_alive_or(DBG_RCON, task, return -EINTR); + +	return rcon_protocol_packet_complete(task->tbuf->buf, task->tbuf->len); +} + +static bool +rcon_check_reply(struct uring_task *task, int res, int32_t *id, +		 int32_t *type, const char **msg) +{ +	struct server *server = container_of(task, struct server, rcon_task); +	const char *error; + +	if (res < 0) { +		error("rcon(%s): reading reply failed, res: %i", +		      server->name, res); +		goto error; +	} + +	if (!rcon_protocol_read_packet(task->tbuf->buf, task->tbuf->len, +				       id, type, msg, &error)) { +		error("rcon(%s): failed to parse packet: %s", +		      server->name, error); +		goto error; +	} + +	return true; + +error: +	uring_task_close_fd(task); +	return false; +} + +static void +rcon_stop_reply(struct uring_task *task, int res) +{ +	struct server *server = container_of(task, struct server, rcon_task); +	int32_t id; +	int32_t type; +	const char *msg; + +	assert_return(task); +	assert_task_alive(DBG_RCON, task); + +	if (!rcon_check_reply(task, res, &id, &type, &msg)) +		return; + +	if (id != 2) { +		error("rcon(%s): stop cmd failed, reply id (%" PRIi32 ")", +		      server->name, id); +		goto out; +	} else if (type != RCON_PACKET_RESPONSE) { +		error("rcon(%s): stop cmd failed, reply type (%" PRIi32 ")", +		      server->name, type); +		goto out; +	} + +	verbose("rcon(%s): stop command sent, reply: %s", server->name, msg); + +out: +	uring_task_close_fd(task); +} + +static void +rcon_stop_sent(struct uring_task *task, int res) +{ +	struct server *server = container_of(task, struct server, rcon_task); + +	assert_return(task); +	assert_task_alive(DBG_RCON, task); + +	if (res != task->tbuf->len) { +		error("rcon(%s): sending stop cmd failed, res: %i", +		      server->name, res); +		uring_task_close_fd(task); +		return; +	} + +	debug(DBG_RCON, "rcon(%s): stop cmd sent", server->name); +	uring_tbuf_read_until(task, rcon_packet_complete, rcon_stop_reply); +} + +static void +rcon_login_reply(struct uring_task *task, int res) +{ +	struct server *server = container_of(task, struct server, rcon_task); +	int32_t id; +	int32_t type; +	const char *msg; + +	assert_return(task); +	assert_task_alive(DBG_RCON, task); + +	if (!rcon_check_reply(task, res, &id, &type, &msg)) +		return; + +	if (id != 1) { +		error("rcon(%s): login failed, reply id (%" PRIi32 ")", +		      server->name, id); +		goto error; +	} else if (type == RCON_PACKET_LOGIN_FAIL) { +		error("rcon(%s): login failed, incorrect password", +		      server->name); +		goto error; +	} else if (type != RCON_PACKET_LOGIN_OK) { +		error("rcon(%s): login failed, reply type (%" PRIi32 ")", +		      server->name, type); +		goto error; +	} + +	debug(DBG_RCON, "rcon(%s): login successful", server->name); +	rcon_protocol_create_packet(task->tbuf->buf, sizeof(task->tbuf->buf), +				    &task->tbuf->len, 2, RCON_PACKET_COMMAND, +				    "stop"); +	uring_tbuf_write(task, rcon_stop_sent); +	return; + +error: +	uring_task_close_fd(task); +} + +static void +rcon_login_sent(struct uring_task *task, int res) +{ +	struct server *server = container_of(task, struct server, rcon_task); + +	assert_return(task); +	assert_task_alive(DBG_RCON, task); + +	if (res != task->tbuf->len) { +		error("rcon(%s): sending login failed, res: %i", +		      server->name, res); +		uring_task_close_fd(task); +		return; +	} + +	debug(DBG_RCON, "rcon(%s): login sent", server->name); +	uring_tbuf_read_until(task, rcon_packet_complete, rcon_login_reply); +} + +static void +rcon_connected_cb(struct connection *conn, bool connected) +{ +	struct server *server = container_of(conn, struct server, rcon_conn); + +	assert_return(conn); +	assert_task_alive(DBG_RCON, &server->rcon_task); + +	if (!connected) { +		error("rcon (%s): connection failed", server->name); +		return; +	} + +	rcon_protocol_create_packet(server->rcon_tbuf.buf, +				    sizeof(server->rcon_tbuf.buf), +				    &server->rcon_tbuf.len, 1, +				    RCON_PACKET_LOGIN, +				    server->rcon_password); +	uring_tbuf_write(&server->rcon_task, rcon_login_sent); +} + +static void +rcon_free(struct uring_task *task) +{ +	struct server *server = container_of(task, struct server, rcon_task); + +	assert_return(task); + +	debug(DBG_RCON, "task %p, server %s (%p)", task, server->name, server); +} + +void +rcon_stop(struct server *server) +{ +	assert_return(server && !list_empty(&server->rcons) && !empty_str(server->rcon_password)); +	assert_task_alive(DBG_RCON, &server->rcon_task); + +	connect_any(&server->rcon_task, &server->rcons, &server->rcon_conn, rcon_connected_cb); +} + +void +rcon_refdump(struct server *server) +{ +	assert_return(server); + +	uring_task_refdump(&server->rcon_task); +} + +void +rcon_delete(struct server *server) +{ +	assert_return(server); + +	debug(DBG_RCON, "closing fd %i", server->rcon_task.fd); +	uring_task_destroy(&server->rcon_task); +} + +void +rcon_init(struct server *server) +{ +	assert_return(server); + +	uring_task_init(&server->rcon_task, "rcon", &server->task, rcon_free); +	uring_task_set_buf(&server->rcon_task, &server->rcon_tbuf); +} + diff --git a/mcserverproxy/server-rcon.h b/mcserverproxy/server-rcon.h new file mode 100644 index 0000000..6625f25 --- /dev/null +++ b/mcserverproxy/server-rcon.h @@ -0,0 +1,12 @@ +#ifndef fooserverrconhfoo +#define fooserverrconhfoo + +void rcon_stop(struct server *server); + +void rcon_refdump(struct server *server); + +void rcon_delete(struct server *server); + +void rcon_init(struct server *server); + +#endif diff --git a/mcserverproxy/server.c b/mcserverproxy/server.c new file mode 100644 index 0000000..de42721 --- /dev/null +++ b/mcserverproxy/server.c @@ -0,0 +1,837 @@ +#define _GNU_SOURCE +#include <stdlib.h> +#include <string.h> +#include <arpa/inet.h> +#include <unistd.h> +#include <inttypes.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <sched.h> +#include <poll.h> +#include <errno.h> +#include <sched.h> + +#include "main.h" +#include "uring.h" +#include "ptimer.h" +#include "server.h" +#include "server-proxy.h" +#include "server-rcon.h" +#include "utils.h" +#include "config-parser.h" +#include "idle.h" +#include "systemd.h" + +static bool +set_property(struct server *server, char **property, const char *value) +{ +	assert_return(server && !*property && !empty_str(value), false); + +	*property = xstrdup(value); +	if (!*property) { +		error("strdup: %m"); +		return false; +	} + +	return true; +} + +void +server_refdump(struct server *server) +{ +	struct server_local *local; +	struct server_proxy *proxy; + +	assert_return(server); + +	uring_task_refdump(&server->task); +	uring_task_refdump(&server->exec_task); +	uring_task_refdump(&server->ann_task); +	uring_task_refdump(&server->idle_task); +	list_for_each_entry(local, &server->locals, list) +		local_refdump(local); +	list_for_each_entry(proxy, &server->proxys, list) +		proxy_refdump(proxy); +	rcon_refdump(server); +} + +static void +server_free(struct uring_task *task) +{ +	struct server *server = container_of(task, struct server, task); + +	assert_return(task); + +	debug(DBG_SRV, "freeing server %s (%p)", server->name, server); +	list_del(&server->list); +	xfree(server->pretty_name); +	xfree(server->start_exec); +	xfree(server->stop_exec); +	xfree(server->systemd_service); +	xfree(server->systemd_obj); +	xfree(server->rcon_password); +	xfree(server->name); +	xfree(server); +} + +void +server_delete(struct server *server) +{ +	struct server_local *local, *ltmp; +	struct server_proxy *proxy, *ptmp; +	struct saddr *remote; +	struct saddr *rcon; +	struct saddr *tmp; +	struct dns_async *dns, *dtmp; + +	assert_return(server); + +	verbose("Removing server %s", server->name); +	server->state = SERVER_STATE_DEAD; + +	rcon_delete(server); + +	list_for_each_entry_safe(local, ltmp, &server->locals, list) +		local_delete(local); + +	list_for_each_entry_safe(proxy, ptmp, &server->proxys, list) +		proxy_delete(proxy); + +	list_for_each_entry_safe(rcon, tmp, &server->rcons, list) { +		list_del(&rcon->list); +		xfree(rcon); +	} + +	list_for_each_entry_safe(remote, tmp, &server->remotes, list) { +		list_del(&remote->list); +		xfree(remote); +	} + +	list_for_each_entry_safe(dns, dtmp, &server->dnslookups, list) +		gai_cancel(&dns->gcb); +		 +	uring_task_destroy(&server->idle_task); +	uring_poll_cancel(&server->exec_task); +	uring_task_put(&server->exec_task); +	uring_task_destroy(&server->task); +	uring_task_put(&server->ann_task); +} + +void +server_delete_by_name(const char *name) +{ +	struct server *server; + +	assert_return(!empty_str(name)); + +	list_for_each_entry(server, &cfg->servers, list) { +		if (streq(server->name, name)) { +			server_delete(server); +			return; +		} +	} +} + +static void +server_dump(struct server *server) +{ +	struct server_local *local; +	struct saddr *remote; +	struct saddr *rcon; + +	assert_return(server); + +	verbose("Server %s:", server->name); +	switch (server->type) { +	case SERVER_TYPE_ANNOUNCE: +		verbose("  * Type: announce"); +		break; +	case SERVER_TYPE_PROXY: +		verbose("  * Type: proxy"); +		break; +	default: +		verbose("  * Type: unknown"); +		break; +	} +	verbose("  * Name: %s", server->pretty_name ? server->pretty_name : "<undefined>"); +	verbose("  * Announce port: %" PRIu16, server->announce_port);  + +	if (!list_empty(&server->locals)) { +		verbose("  * Local:"); +		list_for_each_entry(local, &server->locals, list) +			verbose("    * %s", local->local.addrstr); +	} + +	if (!list_empty(&server->remotes)) { +		verbose("  * Remote:"); +		list_for_each_entry(remote, &server->remotes, list) +			verbose("    * %s", remote->addrstr); +	} + +	if (!list_empty(&server->rcons)) { +		verbose("  * RCon:"); +		list_for_each_entry(rcon, &server->rcons, list) +			verbose("    * %s", rcon->addrstr); +	} + +	verbose(""); +} + +static void +server_exec_free(struct uring_task *task) +{ +	assert_return(task); + +	debug(DBG_SRV, "called"); +} + +#ifndef P_PIDFD +#define P_PIDFD 3 +#endif + +/* FIXME: update states */ +static void +server_exec_done(struct uring_task *task, int res) +{ +	struct server *server = container_of(task, struct server, exec_task); +	int r; +	siginfo_t info; + +	assert_return(task); +	assert_task_alive_or(DBG_SRV, task, goto out); +	/* Should we leave child processes running? */ + +	if (!(res & POLLIN)) { +		error("unexpected result: %i", res); +		goto out; +	} + +	r = waitid(P_PIDFD, server->exec_task.fd, &info, WEXITED); +        if (r < 0) { +                error("waitid: %m"); +		goto out; +        } + +	if (info.si_status == 0) +		debug(DBG_SRV, "command successfully executed"); +	else +		error("command failed: %i", info.si_status); + +out: +	uring_task_close_fd(&server->exec_task); +} + +static int +server_exec_child(void *ptr) +{ +	const char *cmd = ptr; + +	assert_return(ptr, EINVAL); + +	execl(cmd, cmd, NULL); +	return errno; +} + +#ifndef CLONE_PIDFD +#define CLONE_PIDFD 0x00001000 +#endif + +static bool +server_exec(struct server *server, const char *cmd) +{ +	char stack[4096]; /* Beautiful/horrible hack :) */ +	int pidfd; +	int r; + +	assert_return(server && cmd && server->exec_task.fd < 1, false); + +        r = clone(server_exec_child, stack + sizeof(stack), +                  CLONE_VM | CLONE_VFORK | CLONE_PIDFD | SIGCHLD, +                  (void *)cmd, &pidfd); +	if (r < 0) { +		error("clone: %m: %i", r); +		return false; +	} + +	uring_task_set_fd(&server->exec_task, pidfd); +	uring_poll(&server->exec_task, POLLIN, server_exec_done); +	return true; +} + +static bool +server_check_running(struct server *server) +{ +	assert_return(server, false); + +	/* FIXME: other methods, rcon? */ +	if (server->systemd_service) { +		verbose("%s: checking if systemd service is running", server->name); +		if (systemd_service_running(server)) { +			server->state = SERVER_STATE_RUNNING; +			return true; +		} else { +			server->state = SERVER_STATE_STOPPED; +			return false; +		} +	} + +	return false; +} + +bool +server_start(struct server *server) +{ +	assert_return(server, false); +	assert_task_alive_or(DBG_SRV, &server->task, return false); + +	switch (server->start_method) { + +	case SERVER_START_METHOD_EXEC: +		verbose("Starting server %s via external cmd", server->name); +		return server_exec(server, server->start_exec); + +	case SERVER_START_METHOD_SYSTEMD: +		verbose("Starting server %s via systemd (%s)", +			server->name, server->systemd_service); + +		if (systemd_service_start(server)) { +			server->state = SERVER_STATE_RUNNING; +			return true; +		} else +			return server_check_running(server); + +	case SERVER_START_METHOD_UNDEFINED: +	default: +		break; +	} + +	return false; +} + +bool +server_stop(struct server *server) +{ +	assert_return(server, false); +	assert_task_alive_or(DBG_SRV, &server->task, return false); + +	switch (server->stop_method) { + +	case SERVER_STOP_METHOD_EXEC: +		verbose("Stopping server %s via external cmd", server->name); +		return server_exec(server, server->stop_exec); + +	case SERVER_STOP_METHOD_SYSTEMD: +		verbose("Stopping server %s via systemd (%s)", +			server->name, server->systemd_service); +		if (systemd_service_stop(server)) { +			server->state = SERVER_STATE_STOPPED; +			return true; +		} else +			return server_check_running(server); + +	case SERVER_STOP_METHOD_RCON: +		verbose("Stopping server %s via rcon", server->name); +		rcon_stop(server); +		return true; + +	case SERVER_STOP_METHOD_UNDEFINED: +	default: +		break; +	} + +	return false; +} + +static void +server_idle_free(struct uring_task *task) +{ +	assert_return(task); + +	debug(DBG_ANN, "called"); +} + +void +server_set_active_players(struct server *server, int count) +{ +	assert_return(server); +	assert_task_alive(DBG_IDLE, &server->idle_task); + +	debug(DBG_IDLE, "%s: currently %i active players", +	      server->name, count); + +	if (count < 0) +		return; + +	server->state = SERVER_STATE_RUNNING; +	if (count > 0) +		server->idle_count = 0; +	else if (count == 0) +		server->idle_count++; + +	if (server->idle_count > server->idle_timeout) { +		verbose("stopping idle server %s", server->name); +		server_stop(server);  +	} +} + +static void +server_idle_connected_cb(struct connection *conn, bool connected) +{ +	struct server *server = container_of(conn, struct server, idle_conn); + +	assert_return(conn); +	assert_task_alive(DBG_IDLE, &server->idle_task); + +	if (!connected) { +		debug(DBG_IDLE, +		      "idle check connection to remote server (%s) failed", +		      server->name); +		server->idle_count = 0; +		server->state = SERVER_STATE_STOPPED; +		return; +	} + +	debug(DBG_IDLE, "connected to remote %s\n", conn->remote.addrstr); +	idle_check_get_player_count(server, conn); +} + +bool +server_idle_check(struct server *server) +{ +	assert_return(server, false); + +	if (server->state == SERVER_STATE_INIT || +	    server->state == SERVER_STATE_DEAD) +		return false; + +	if (server->idle_timeout < 1) +		return false; + +	if (list_empty(&server->remotes)) +		return false; + +	if (!list_empty(&server->proxys)) { +		server->idle_count = 0; +		return true; +	} + +	connect_any(&server->idle_task, &server->remotes, +		    &server->idle_conn, server_idle_connected_cb); +	return true; +} + +static void +server_announce_free(struct uring_task *task) +{ +	assert_return(task); + +	debug(DBG_ANN, "called"); +} + +static void +server_announce_cb(struct uring_task *task, int res) +{ +	struct server *server = container_of(task, struct server, ann_task); + +	assert_return(task); + +	if (res < 0) +		error("%s: failure %i", server->name, res); +	else if (res == server->ann_buf.len) +		debug(DBG_ANN, "%s: ok (%i)", server->name, res); +	else +		debug(DBG_ANN, "%s: unexpected result: %i", server->name, res); + +	uring_task_set_fd(&server->ann_task, -1); +} + +bool +server_announce(struct server *server, int fd) +{ +	assert_return(server && fd >= 0, false); + +	if (server->state == SERVER_STATE_INIT || +	    server->state == SERVER_STATE_DEAD) +		return false; + +	debug(DBG_ANN, "announcing server: %s", server->name); +	uring_task_set_fd(&server->ann_task, fd); +	uring_tbuf_sendmsg(&server->ann_task, server_announce_cb); +	return true; +} + +bool +server_commit(struct server *server) +{ +	struct server_local *local; +	uint16_t port; +	int r; + +	assert_return(server && server->name, false); +	assert_task_alive_or(DBG_SRV, &server->task, return false); + +	if (server->state != SERVER_STATE_INIT) { +		error("called in wrong state"); +		return false; +	} + +	if (!list_empty(&server->proxys)) { +		error("%s: proxys not empty?", server->name); +		return false; +	} + +	if (!list_empty(&server->dnslookups)) { +		debug(DBG_SRV, "called with pending DNS requests"); +		return true; +	} + +	if (server->stop_method == SERVER_STOP_METHOD_RCON && +	    list_empty(&server->rcons)) { +		error("%s: rcon stop method missing rcon address", +		      server->name);  +		return false; +	} + +	if (server->stop_method == SERVER_STOP_METHOD_RCON && +	    !server->rcon_password) { +		error("%s: rcon stop method missing rcon password", +		      server->name);  +		return false; +	} + +	if ((server->start_method == SERVER_START_METHOD_SYSTEMD || +	     server->stop_method == SERVER_STOP_METHOD_SYSTEMD) && +	    !server->systemd_service) { +		error("%s: systemd start/stop method missing systemd service", +		      server->name);  +		return false; +	} + +	if (server->systemd_service && !server->systemd_obj) { +		server->systemd_obj = systemd_object_path(server->systemd_service); +		if (!server->systemd_obj) { +			error("%s: failed to create systemd object path (%s)", +			      server->name, server->systemd_service); +			return false; +		} +	} + +	if (server->idle_timeout > 0 && +	    server->stop_method == SERVER_STOP_METHOD_UNDEFINED) { +		error("%s: idle_timeout set but missing stop method", server->name);  +		return false; +	} + +	switch (server->type) { +	case SERVER_TYPE_ANNOUNCE: +		if (server->announce_port < 1) { +			error("%s: missing announce port", server->name);  +			return false; +		} + +		if (server->start_method != SERVER_START_METHOD_UNDEFINED) { +			error("%s: can't set start_method for announce server", server->name); +			return false; +		} + +		if (!list_empty(&server->locals)) { +			error("%s: can't set local addresses for announce server", server->name); +			return false; +		} + +		if (!list_empty(&server->remotes)) { +			error("%s: can't set remote addresses for announce server", server->name); +			return false; +		} + +		break; + +	case SERVER_TYPE_PROXY: +		if (server->announce_port >= 1) { +			error("%s: can't set announce port for proxy server", server->name);  +			return false; +		} + +		if (list_empty(&server->locals)) { +			error("%s: missing local addresses for proxy server", server->name); +			return false; +		} + +		if (list_empty(&server->remotes)) { +			error("%s: missing remote addresses for proxy server", server->name); +			return false; +		} + +		list_for_each_entry(local, &server->locals, list) { +			port = saddr_port(&local->local); + +			if (port == 0) { +				error("%s: invalid local port", server->name); +				return false; +			} + +			if (server->announce_port < 1) +				server->announce_port = port; + +			if (server->announce_port != port) { +				error("%s: multiple local ports", server->name); +				return false; +			} +		} + +		if (server->announce_port < 1) { +			error("%s: can't determine which port to announce", server->name); +			return false; +		} + +		break; + +	default: +		error("%s: can't determine server type", server->name); +		return false; +	} + +	if (!server->pretty_name) { +		char *suffix; +		 +		suffix = strrchr(server->name, '.'); +		if (!suffix || suffix == server->name) { +			error("invalid server name: %s", server->name); +			return false; +		} + +		server->pretty_name = xstrndup(server->name, suffix - server->name); +		if (!server->pretty_name) { +			error("failed to create display name: %s", server->name); +			return false; +		} +	} + +	r = snprintf(server->ann_buf.buf, sizeof(server->ann_buf.buf), +		     "[MOTD]%s[/MOTD][AD]%" PRIu16 "[/AD]", +		     server->pretty_name, server->announce_port); +	if (r < 1 || r >= sizeof(server->ann_buf.buf)) { +		error("%s: unable to create announce msg: %i\n", server->name, r); +		return false; +	} +	server->ann_buf.len = r; + +	/* FIXME: config, dont reread config if server running, make sure fd is available before this is called */ +	server_dump(server); + +	list_for_each_entry(local, &server->locals, list) +		local_open(local); + +	server->state = SERVER_STATE_CFG_OK; + +	server_check_running(server); + +	debug(DBG_SRV, "success"); +	return true; +} + +bool +server_add_remote(struct server *server, struct saddr *remote) +{ +	assert_return(server && remote, false); +	assert_task_alive_or(DBG_SRV, &server->task, return false); + +	debug(DBG_SRV, "adding remote: %s", remote->addrstr); +	list_add(&remote->list, &server->remotes); +	return true; +} + +bool +server_add_local(struct server *server, struct saddr *saddr) +{ +	struct server_local *local; + +	assert_return(server && saddr, false); +	assert_task_alive_or(DBG_SRV, &server->task, return false); + +	local = local_new(server, saddr); +	if (!local) +		return false; + +	list_add(&local->list, &server->locals); +	return true; +} + +bool +server_add_rcon(struct server *server, struct saddr *rcon) +{ +	assert_return(server && rcon, false); +	assert_task_alive_or(DBG_SRV, &server->task, return false); + +	debug(DBG_SRV, "adding rcon: %s", rcon->addrstr); +	list_add(&rcon->list, &server->rcons); +	return true; +} + +bool +server_set_rcon_password(struct server *server, const char *password) +{ +	assert_return(server && !empty_str(password), false); + +	return set_property(server, &server->rcon_password, password); +} + +bool +server_set_systemd_service(struct server *server, const char *service) +{ +	const char *suffix; +	char *tmp; + +	assert_return(server && !empty_str(service) && !server->systemd_service, false); + +	suffix = strrchr(service, '.'); +	if (!suffix || !streq(suffix, ".service")) { +		tmp = zmalloc(strlen(service) + strlen(".service") + 1); +		if (tmp) +			sprintf(tmp, "%s.service", service); +	} else +		tmp = xstrdup(service); + +	if (!tmp) { +		error("malloc/strdup: %m"); +		return false; +	} + +	server->systemd_service = tmp; +	return true; +} + +bool +server_set_stop_method(struct server *server, +		       enum server_stop_method stop_method) +{ +	assert_return(server->stop_method == SERVER_STOP_METHOD_UNDEFINED && +		      stop_method != SERVER_STOP_METHOD_UNDEFINED, false); + +	server->stop_method = stop_method; +	return true; +} + +bool +server_set_start_method(struct server *server, +			enum server_start_method start_method) +{ +	assert_return(server->start_method == SERVER_START_METHOD_UNDEFINED && +		      start_method != SERVER_START_METHOD_UNDEFINED, false); + +	server->start_method = start_method; +	return true; +} + +bool +server_set_stop_exec(struct server *server, const char *cmd) +{ +	assert_return(server && !empty_str(cmd), false); + +	return set_property(server, &server->stop_exec, cmd); +} + +bool +server_set_start_exec(struct server *server, const char *cmd) +{ +	assert_return(server && !empty_str(cmd), false); + +	return set_property(server, &server->start_exec, cmd); +} + +bool +server_set_idle_timeout(struct server *server, uint16_t timeout) +{ +	assert_return(server && timeout > 0 && server->idle_timeout == 0, false); + +	server->idle_timeout = timeout; +	return true; +} + +bool +server_set_port(struct server *server, uint16_t port) +{ +	assert_return(server && port > 0 && server->announce_port == 0, false); + +	server->announce_port = htons(port); +	return true; +} + +bool +server_set_type(struct server *server, enum server_type type) +{ +	assert_return(server && type != SERVER_TYPE_UNDEFINED, false); + +	switch (type) { +	case SERVER_TYPE_ANNOUNCE: +		server->type = SERVER_TYPE_ANNOUNCE; +		break; +	case SERVER_TYPE_PROXY: +		server->type = SERVER_TYPE_PROXY; +		break; +	default: +		return false; +	} + +	return true; +} + +bool +server_set_pretty_name(struct server *server, const char *pretty_name) +{ +	assert_return(server && !empty_str(pretty_name), false); + +	return set_property(server, &server->pretty_name, pretty_name); +} + +struct server * +server_new(const char *name) +{ +	struct server *server; + +	assert_return(!empty_str(name), NULL); + +	list_for_each_entry(server, &cfg->servers, list) { +		if (!streq(name, server->name)) +			continue; +		error("attempt to add duplicate server: %s", name); +		return server; +	} + +	verbose("Adding server %s", name); +	server = zmalloc(sizeof(*server)); +	if (!server) { +		error("malloc: %m"); +		return NULL; +	} + +	server->state = SERVER_STATE_INIT; +	server->type = SERVER_TYPE_UNDEFINED; +	server->name = xstrdup(name); +	server->stop_method = SERVER_STOP_METHOD_UNDEFINED; +	server->start_method = SERVER_START_METHOD_UNDEFINED; +	server->idle_timeout = 0; + +	uring_task_init(&server->task, server->name, uring_parent(), server_free); +	uring_task_set_buf(&server->task, &server->tbuf); + +	uring_task_init(&server->ann_task, "announce", &server->task, server_announce_free); +	uring_task_set_buf(&server->ann_task, &server->ann_buf); +	saddr_set_ipv4(&server->ann_task.saddr, cinet_addr(224,0,2,60), htons(4445)); + +	uring_task_init(&server->exec_task, "exec", &server->task, server_exec_free); + +	uring_task_init(&server->idle_task, "idle", &server->task, server_idle_free); +	uring_task_set_buf(&server->idle_task, &server->idle_buf); + +	rcon_init(server); + +	list_init(&server->remotes); +	list_init(&server->locals); +	list_init(&server->proxys); +	list_init(&server->rcons); +	list_init(&server->dnslookups); +	list_add(&server->list, &cfg->servers); + +	return server; +} diff --git a/mcserverproxy/server.h b/mcserverproxy/server.h new file mode 100644 index 0000000..ff4c28e --- /dev/null +++ b/mcserverproxy/server.h @@ -0,0 +1,128 @@ +#ifndef fooserverhfoo +#define fooserverhfoo + +enum server_state { +	SERVER_STATE_INIT	= 0, +	SERVER_STATE_CFG_OK	= 1, +	SERVER_STATE_RUNNING	= 2, +	SERVER_STATE_STOPPED	= 3, +	SERVER_STATE_DEAD	= 4, +}; + +enum server_type { +	SERVER_TYPE_UNDEFINED, +	SERVER_TYPE_ANNOUNCE, +	SERVER_TYPE_PROXY, +}; + +enum server_stop_method { +	SERVER_STOP_METHOD_UNDEFINED, +	SERVER_STOP_METHOD_RCON, +	SERVER_STOP_METHOD_SYSTEMD, +	SERVER_STOP_METHOD_EXEC, +}; + +enum server_start_method { +	SERVER_START_METHOD_UNDEFINED, +	SERVER_START_METHOD_SYSTEMD, +	SERVER_START_METHOD_EXEC, +}; + +struct server { +	enum server_type type; +	char *name; +	char *pretty_name; +	uint16_t announce_port; +	struct list_head locals; +	struct list_head remotes; +	struct list_head proxys; +	struct list_head rcons; +	struct list_head dnslookups; +	enum server_state state; +	 +	enum server_stop_method stop_method; +	enum server_start_method start_method; + +	/* For calling external start/stop executables */ +	char *stop_exec; +	char *start_exec; +	struct uring_task exec_task; + +	/* For systemd services */ +	char *systemd_service; +	char *systemd_obj; + +	/* For rcon connections */ +	char *rcon_password; +	struct connection rcon_conn; +	struct uring_task rcon_task; +	struct uring_task_buf rcon_tbuf; + +	/* For announce messages */ +	struct uring_task ann_task; +	struct uring_task_buf ann_buf; + +	/* For checking idle status */ +	struct uring_task idle_task; +	struct connection idle_conn; +	struct uring_task_buf idle_buf; +	unsigned idle_timeout; +	unsigned idle_count; + +	/* For reading config files */ +	struct uring_task task; +	struct uring_task_buf tbuf; + +	struct list_head list; +}; + +void server_refdump(struct server *server); + +void server_delete(struct server *server); + +void server_delete_by_name(const char *name); + +bool server_start(struct server *server); + +bool server_stop(struct server *server); + +void server_set_active_players(struct server *server, int count); + +bool server_idle_check(struct server *server); + +bool server_announce(struct server *server, int fd); + +bool server_commit(struct server *server); + +bool server_add_remote(struct server *server, struct saddr *remote); + +bool server_add_local(struct server *server, struct saddr *saddr); + +bool server_add_rcon(struct server *server, struct saddr *rcon); + +bool server_set_rcon_password(struct server *server, const char *password); + +bool server_set_systemd_service(struct server *server, const char *service); + +bool server_set_stop_method(struct server *server, +			    enum server_stop_method stop_method); + +bool server_set_start_method(struct server *server, +			     enum server_start_method start_method); + +bool server_set_stop_exec(struct server *server, const char *cmd); + +bool server_set_start_exec(struct server *server, const char *cmd); + +bool server_set_idle_timeout(struct server *server, uint16_t timeout); + +bool server_set_port(struct server *server, uint16_t port); + +bool server_set_type(struct server *server, enum server_type type); + +bool server_set_pretty_name(struct server *server, const char *pretty_name); + +struct server *server_new(const char *name); + +#endif + diff --git a/mcserverproxy/signal-handler.c b/mcserverproxy/signal-handler.c new file mode 100644 index 0000000..67c2e0b --- /dev/null +++ b/mcserverproxy/signal-handler.c @@ -0,0 +1,195 @@ +#define _GNU_SOURCE +#include <sys/signalfd.h> +#include <signal.h> +#include <fcntl.h> +#include <unistd.h> +#include <systemd/sd-daemon.h> + +#include "main.h" +#include "signal-handler.h" +#include "uring.h" +#include "server.h" +#include "server-config.h" +#include "config-parser.h" +#include "igmp.h" +#include "announce.h" +#include "idle.h" +#include "ptimer.h" + +struct signal_ev { +	struct uring_task task; +	struct uring_task_buf tbuf; +	int pipe[2]; +}; + +static void +signal_delete() +{ +	assert_return(cfg->signal); + +	uring_task_destroy(&cfg->signal->task); +} + +static void +signalfd_read(struct uring_task *task, int res) +{ +	struct signal_ev *signal = container_of(task, struct signal_ev, task); +	struct server *server, *stmp; +	static unsigned count = 0; +	siginfo_t *si; + +	assert_return(task); +	assert_task_alive(DBG_SIG, task); + +	si = (siginfo_t *)task->tbuf->buf; +	if (res != sizeof(*si)) +		die("error in signalfd (%i)", res); + +	switch (si->si_signo) { +	case SIGUSR1: { +		struct dns_async *dns; + +		debug(DBG_SIG, "Got a SIGUSR1"); +		if (si->si_code != SI_ASYNCNL || !si->si_ptr) { +			error("SIGUSR1: unexpected values in siginfo"); +			goto out; +		} + +		dns = si->si_ptr; +		if (!dns->cb) { +			error("DNS callback not set"); +			goto out; +		} + +		debug(DBG_DNS, "DNS lookup complete, dns: %p, dns->cb: %p", +		      dns, dns->cb); +		dns->cb(dns); +		break; +	} + +	case SIGUSR2: +		debug(DBG_SIG, "got a SIGUSR2"); +		dump_tree(); +		break; + +	case SIGTERM: +		debug(DBG_SIG, "Got a SIGINT/SIGHUP"); +		verbose("got a signal to quit"); +		sd_notifyf(0, "STOPPING=1\nSTATUS=Received signal, exiting"); +		exit(EXIT_SUCCESS); +		break; + +	case SIGINT: +	case SIGHUP: +		count++; +		if (count > 5) { +			dump_tree(); +			exit(EXIT_FAILURE); +		} + +		verbose("got a signal to dump tree"); +		sd_notifyf(0, "STOPPING=1\nSTATUS=Received signal, exiting"); +		dump_tree(); +		signal_delete(); +		ptimer_delete(); +		igmp_delete(); +		announce_delete(); +		idle_delete(); +		server_cfg_monitor_delete(); +		list_for_each_entry_safe(server, stmp, &cfg->servers, list) +			server_delete(server); +		uring_delete(); +		return; + +	default: +		error("got an unknown signal: %i", si->si_signo); +		break; +	} + +out: +	uring_tbuf_read(&signal->task, signalfd_read); +} + +static void +hack_signal_handler(int signum, siginfo_t *si, void *ucontext) +{ +	ssize_t r; + +	assert_return(signum > 0 && si); + +	r = write(cfg->signal->pipe[PIPE_WR], si, sizeof(*si)); +	if (r != sizeof(*si)) +		error("write: %zi\n", r); + +} + +static void +signal_free(struct uring_task *task) +{ +	struct signal_ev *signal = container_of(task, struct signal_ev, task); + +	assert_return(task); + +	debug(DBG_SIG, "called"); +	close(signal->pipe[PIPE_WR]); +	cfg->signal = NULL; +	xfree(signal); +} + +void +signal_refdump() +{ +	assert_return(cfg->signal); + +	uring_task_refdump(&cfg->signal->task); +} + +void +signal_init() +{ +	//sigset_t mask; +	struct signal_ev *signal; + +	assert_return(!cfg->signal); + +	signal = zmalloc(sizeof(*signal)); +	if (!signal) +		die("malloc: %m"); + +	if (pipe2(signal->pipe, O_CLOEXEC) < 0) +		die("pipe2: %m"); + +	/* +	sigfillset(&mask); +	if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0) +		die("sigprocmask: %m"); + +	sfd = signalfd(-1, &mask, SFD_CLOEXEC); +	if (sfd < 0) +		die("signalfd: %m"); +	*/ + +	struct sigaction action; +	sigfillset(&action.sa_mask); +	action.sa_sigaction = hack_signal_handler; +	action.sa_flags = SA_SIGINFO; + +	sigaction(SIGINT, &action, NULL); +	sigaction(SIGHUP, &action, NULL); +	sigaction(SIGTERM, &action, NULL); +	sigaction(SIGUSR1, &action, NULL); +	sigaction(SIGUSR2, &action, NULL); + +	action.sa_handler = SIG_IGN; +	action.sa_flags = 0; +	sigaction(SIGPIPE, &action, NULL); + +	debug(DBG_SIG, "using pipe fds %i -> %i", +	      signal->pipe[PIPE_WR], signal->pipe[PIPE_RD]); +	uring_task_init(&signal->task, "signal", uring_parent(), signal_free); +	uring_task_set_fd(&signal->task, signal->pipe[PIPE_RD]); +	uring_task_set_buf(&signal->task, &signal->tbuf); +	cfg->signal = signal; +	uring_tbuf_read(&signal->task, signalfd_read); +} + diff --git a/mcserverproxy/signal-handler.h b/mcserverproxy/signal-handler.h new file mode 100644 index 0000000..e0140b3 --- /dev/null +++ b/mcserverproxy/signal-handler.h @@ -0,0 +1,8 @@ +#ifndef foosignalhfoo +#define foosignalhfoo + +void signal_refdump(); + +void signal_init(); + +#endif diff --git a/mcserverproxy/systemd.c b/mcserverproxy/systemd.c new file mode 100644 index 0000000..a44b0d8 --- /dev/null +++ b/mcserverproxy/systemd.c @@ -0,0 +1,219 @@ +#include <string.h> +#include <stdlib.h> +#include <systemd/sd-bus.h> + +#include "main.h" +#include "server.h" +#include "systemd.h" + +#define SYSTEMD_DBUS_SERVICE		"org.freedesktop.systemd1" +#define SYSTEMD_DBUS_INTERFACE		"org.freedesktop.systemd1.Unit" +#define SYSTEMD_DBUS_PATH_PREFIX	"/org/freedesktop/systemd1/unit/" + +static inline char +tohex(uint8_t val) +{ +	static const char hex[] = "0123456789abcdef"; + +	return hex[val & 0x0f]; +} + +/* + * Creates an escaped D-Bus object path for a given systemd service + * + * Escaping rules are documented here: + * https://dbus.freedesktop.org/doc/dbus-specification.html + * + * Essentially, everyting but a-z, A-Z, 0-9 is replaced by _xx where xx is + * the hexadecimal value of the character. + * + * Example: minecraft@world1.service -> minecraft_40world1_2eservice + */ +char * +systemd_object_path(const char *service) +{ +	char *r; +	char *d; +	const char *s; + +	assert_return(service && !empty_str(service), NULL); + +	r = zmalloc(strlen(SYSTEMD_DBUS_PATH_PREFIX) + strlen(service) * 3 + 1); +	if (!r) +		return NULL; + +	memcpy(r, SYSTEMD_DBUS_PATH_PREFIX, strlen(SYSTEMD_DBUS_PATH_PREFIX)); +	d = r + strlen(SYSTEMD_DBUS_PATH_PREFIX); + +	for (s = service; *s; s++) { +		if ((*s >= 'a' && *s <= 'z') || +		    (*s >= 'A' && *s <= 'Z') || +		    (*s >= '0' && *s <= '9')) { +			*(d++) = *s; +			continue; +		} + +		*(d++) = '_'; +		*(d++) = tohex(*s >> 4); +		*(d++) = tohex(*s); +	} + +	*d = '\0'; +	return r; +} + +void +systemd_delete() +{ +       	assert_return_silent(cfg->sd_bus); + +        sd_bus_unref(cfg->sd_bus); +	cfg->sd_bus = NULL; +} + +static sd_bus * +get_bus() +{ +	int r; + +	if (cfg->sd_bus_failed) +		return NULL; + +	if (!cfg->sd_bus) { +		r = sd_bus_open_user(&cfg->sd_bus); +		if (r < 0) { +			error("failed to connect to user system bus: %s", strerror(-r)); +			cfg->sd_bus_failed = true; +			return NULL; +		} +	} + +	return cfg->sd_bus; +} + +/* + * Check if a systemd service is running. + * + * This is equivalent to (assuming service minecraft@world1): + *   gdbus call --session  + *	--dest org.freedesktop.systemd1  + *	--object-path /org/freedesktop/systemd1/unit/minecraft_40world1_2eservice + *	--method org.freedesktop.DBus.Properties.Get + *		"org.freedesktop.systemd1.Unit"  + *		"ActiveState" + */ +bool +systemd_service_running(struct server *server) +{ +	sd_bus *bus = get_bus(); +        sd_bus_error error = SD_BUS_ERROR_NULL; +	char *status = NULL; +	bool running = false; +	int r; + +	assert_return(server && bus && server->systemd_service && server->systemd_obj, false); + +	r = sd_bus_get_property_string(bus, +				       SYSTEMD_DBUS_SERVICE, +				       server->systemd_obj, +				       SYSTEMD_DBUS_INTERFACE, +				       "ActiveState", +				       &error, +				       &status); +        if (r < 0) { +                error("failed to get status for service %s (%s): %s", +		      server->systemd_service, server->systemd_obj, error.message); +		goto out; +        } + +	if (streq(status, "active")) { +		running = true; +		debug(DBG_SYSD, "systemd service %s (%s) is active", +		      server->systemd_service, server->systemd_obj); +	} else +		debug(DBG_SYSD, "systemd service %s (%s) is not active", +		      server->systemd_service, server->systemd_obj); + +out: +	free(status); +        sd_bus_error_free(&error); +	return running; +} + +static bool +systemd_service_action(struct server *server, const char *action) +{ +        sd_bus_error error = SD_BUS_ERROR_NULL; +        sd_bus_message *m = NULL; +	sd_bus *bus = get_bus(); +	const char *path; +	bool performed = false; +	int r; + +	assert_return(server && bus && server->systemd_service && server->systemd_obj && action, false); + +        r = sd_bus_call_method(bus, +			       SYSTEMD_DBUS_SERVICE, +			       server->systemd_obj, +			       SYSTEMD_DBUS_INTERFACE, +			       action, +			       &error, +			       &m, +			       "s", +			       "fail"); +        if (r < 0) { +                error("failed to perform action %s on systemd service %s: %s", +		      action, server->systemd_service, error.message); +		goto out; +        } + +        r = sd_bus_message_read(m, "o", &path); +        if (r < 0) { +                error("failed to parse response message: %s", strerror(-r)); +		goto out; +        } + +        verbose("action %s queued for service %s", +		action, server->systemd_service); +	performed = true; + +out: +        sd_bus_error_free(&error); +        sd_bus_message_unref(m); +	return performed; +} + +/* + * Stop systemd service. + * + * This is equivalent to (assuming service minecraft@world1): + *   gdbus call --session  + *	--dest org.freedesktop.systemd1  + *	--object-path /org/freedesktop/systemd1/unit/minecraft_40world1_2eservice + *	--method org.freedesktop.systemd1.Unit.Stop "fail" + */ +bool +systemd_service_stop(struct server *server) +{ +	assert_return(server, false); + +	return systemd_service_action(server, "Stop"); +} + +/* + * Start systemd service. + * + * This is equivalent to (assuming service minecraft@world1): + *   gdbus call --session  + *	--dest org.freedesktop.systemd1  + *	--object-path /org/freedesktop/systemd1/unit/minecraft_40world1_2eservice + *	--method org.freedesktop.systemd1.Unit.Start "fail" + */ +bool +systemd_service_start(struct server *server) +{ +	assert_return(server, false); + +	return systemd_service_action(server, "Start"); +} + diff --git a/mcserverproxy/systemd.h b/mcserverproxy/systemd.h new file mode 100644 index 0000000..d455044 --- /dev/null +++ b/mcserverproxy/systemd.h @@ -0,0 +1,14 @@ +#ifndef foosystemdhfoo +#define foosystemdhfoo + +char *systemd_object_path(const char *service); + +void systemd_delete(); + +bool systemd_service_running(struct server *server); + +bool systemd_service_stop(struct server *server); + +bool systemd_service_start(struct server *server); + +#endif diff --git a/mcserverproxy/uring.c b/mcserverproxy/uring.c new file mode 100644 index 0000000..e979471 --- /dev/null +++ b/mcserverproxy/uring.c @@ -0,0 +1,759 @@ +#define _GNU_SOURCE +#include <liburing.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <errno.h> +#include <string.h> +#include <unistd.h> + +#include "main.h" +#include "uring.h" + +struct uring_ev { +	struct io_uring uring; +	struct io_uring_params uring_params; +	struct uring_task task; + +	/* for testing if the kernel supports splice */ +	int pipe[2]; +	int tfd; +}; + +enum cqe_type { +	CQE_TYPE_NORMAL		= 0x0, +	CQE_TYPE_CANCEL		= 0x1, +	CQE_TYPE_CLOSE		= 0x2, +	CQE_TYPE_POLL_CANCEL	= 0x3 +}; + +#define CQE_TYPE_PTR_MASK 0x3 + +uint64_t sqe_count = 0; +uint64_t cqe_count = 0; + +static struct io_uring_sqe * +get_sqe(struct uring_task *task) +{ +	struct io_uring_sqe *sqe; +	 +	assert_die(task, "invalid arguments"); + +	sqe = io_uring_get_sqe(&cfg->uring->uring); +	if (!sqe) { +		io_uring_submit(&cfg->uring->uring); +		sqe = io_uring_get_sqe(&cfg->uring->uring); +		if (!sqe) +			die("failed to get an sqe!"); +	} + +	sqe_count++; +	uring_task_get(task); +	return sqe; +} + +void +uring_task_refdump(struct uring_task *task) +{ +	char buf[4096]; +	struct uring_task *tmp; + +	assert_return(task); + +	if (!debug_enabled(DBG_REF)) +		return; + +	buf[0] = '\0'; +	for (tmp = task; tmp; tmp = tmp->parent) { +		size_t prefix; +		char *dst; + +		if (tmp->parent) +			prefix = strlen("->") + strlen(tmp->name); +		else +			prefix = strlen(tmp->name); + +		memmove(&buf[prefix], &buf[0], strlen(buf) + 1); + +		dst = &buf[0]; +		if (tmp->parent) { +			*dst++ = '-'; +			*dst++ = '>'; +		} + +		memcpy(dst, tmp->name, strlen(tmp->name)); +	} + +	info("%s (0x%p parent 0x%p free 0x%p fd %i ref %u)", +	     buf, task, task->parent, task->free, task->fd, +	     task->refcount); +} + +/* + * Similar to uring_task_put, but can be called from other tasks + * while the task is active. + */ +void +uring_task_destroy(struct uring_task *task) +{ +	assert_return(task); +	assert_return_silent(!task->dead); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	if (task->fd >= 0) { +		struct io_uring_sqe *sqe; +	 +		sqe = get_sqe(task); +		io_uring_prep_cancel(sqe, task, 0); +		io_uring_sqe_set_data(sqe, (void *)((uintptr_t)task | CQE_TYPE_CANCEL)); +	} + +	task->dead = true; +	uring_task_put(task); +} + +void +uring_task_put(struct uring_task *task) +{ +	struct uring_task *parent; + +	assert_return(task); + +	debug(DBG_REF, "task %s (%p), refcount %u -> %u", +	      task->name, task, task->refcount, task->refcount - 1); + +	task->refcount--; + +	if (task->refcount > 0) +		return; + +	if (task->refcount < 0) +		error("Negative refcount!"); + +	if (task->fd >= 0) { +		uring_task_close_fd(task); +		/* We'll be called again once the fd is closed */ +		return; +	} + +	parent = task->parent; +	if (task->free) +		task->free(task); + +	if (parent) { +		debug(DBG_REF, "putting parent %s (%p)", parent->name, parent); +		uring_task_put(parent); +	} +} + +void +uring_task_get(struct uring_task *task) +{ +	assert_return(task); + +	debug(DBG_REF, "task %s (%p), refcount %u -> %u", +	      task->name, task, task->refcount, task->refcount + 1); + +	if (task->refcount < 0) +		error("Negative refcount!"); + +	task->refcount++; +} + +void +uring_task_set_buf(struct uring_task *task, struct uring_task_buf *tbuf) +{ +	assert_return(task && tbuf); + +	debug(DBG_UR, "task %s (%p), buf %p, refcount %u", +	      task->name, task, tbuf, task->refcount); + +	/* iov_len and msg_namelen are set at send/receive time */ +	tbuf->iov.iov_base = tbuf->buf; +	tbuf->msg.msg_name = &task->saddr.storage; +	tbuf->msg.msg_iov = &tbuf->iov; +	tbuf->msg.msg_iovlen = 1; +	tbuf->msg.msg_control = NULL; +	tbuf->msg.msg_controllen = 0; +	tbuf->msg.msg_flags = 0; +	task->tbuf = tbuf; +} + +void +uring_task_set_fd(struct uring_task *task, int fd) +{ +	assert_return(task); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, fd, task->refcount); + +	task->fd = fd; +} + +void +uring_task_close_fd(struct uring_task *task) +{ +	assert_return(task); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	if (task->fd < 0) +		return; + +	uring_close(task, task->fd); +	task->fd = -1; +} + +struct uring_task * +uring_parent() +{ +	assert_die(cfg->uring, "invalid arguments"); + +	return &cfg->uring->task; +} + +void +uring_task_init(struct uring_task *task, const char *name, +		struct uring_task *parent, void (*free)(struct uring_task *)) +{ +	static bool first = true; + +	assert_die(task && !empty_str(name) && free, "invalid arguments"); + +	if (first) +		first = false; +	else +		assert_die(parent, "called without a parent task"); + +	task->refcount = 1; +	task->fd = -1; +	task->parent = parent; +	task->free = free; +	task->dead = false; +	task->name = name; +	task->tbuf = NULL; + +	if (task->parent) { +		debug(DBG_REF, "task %s (%p), refcount %u, " +		      "getting parent %s (%p), refcount %u", +		      task->name, task, task->refcount, +		      task->parent->name, task->parent, task->parent->refcount); +		uring_task_get(task->parent); +	} +} + +void +uring_close(struct uring_task *task, int fd) +{ +	struct io_uring_sqe *sqe; + +	assert_return(task && fd >= 0); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +       	sqe = get_sqe(task); +	io_uring_prep_close(sqe, fd); +	io_uring_sqe_set_data(sqe, (void *)((uintptr_t)task | CQE_TYPE_CLOSE)); +} + +static void +uring_tbuf_write_cb(struct uring_task *task, int res) +{ +	int r; + +	assert_return(task && task->tbuf && task->final_cb); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	if (res < 0) { +		r = res; +		goto finished; +	} + +	/* We wrote some more data */ +	task->tbuf->done += res; +	if (task->tbuf->done >= task->tbuf->len || res == 0) { +		r = task->tbuf->len; +		goto finished; +	} + +	uring_write(task, task->tbuf->buf + task->tbuf->done, +		    task->tbuf->len - task->tbuf->done, +		    uring_tbuf_write_cb); +	return; + +finished: +	task->final_cb(task, r); +} + +void +uring_tbuf_write(struct uring_task *task, utask_cb_t final_cb) +{ +	assert_return(task && task->fd >= 0 && task->tbuf && task->tbuf->len > 0); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	task->tbuf->done = 0; +	task->final_cb = final_cb; +	uring_write(task, &task->tbuf->buf, task->tbuf->len, uring_tbuf_write_cb); +} + +void +uring_write(struct uring_task *task, void *buf, size_t len, utask_cb_t cb) +{ +	struct io_uring_sqe *sqe; + +	assert_return(task && buf && len > 0 && cb && task->fd >= 0); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +       	sqe = get_sqe(task); +	task->cb = cb; +	io_uring_prep_write(sqe, task->fd, buf, len, 0); +	io_uring_sqe_set_data(sqe, task); +} + +static void +uring_tbuf_read_until_cb(struct uring_task *task, int res) +{ +	int r; + +	assert_return(task && task->tbuf && task->final_cb && task->is_complete_cb); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	if (res < 0) { +		r = res; +		goto finished; +	} + +	task->tbuf->len += res; +	r = task->is_complete_cb(task, res); +	if (r < 0) { +		r = res; +		goto finished; +	} else if (r > 0) { +		r = task->tbuf->len; +		goto finished; +	} + +	/* Need to read more */ +	if (task->tbuf->len >= sizeof(task->tbuf->buf)) { +		r = E2BIG; +		goto finished; +	} + +	uring_read_offset(task, task->tbuf->buf + task->tbuf->len, +			  sizeof(task->tbuf->buf) - task->tbuf->len, +			  task->tbuf->len, uring_tbuf_read_until_cb); +	return; + +finished: +	task->final_cb(task, r); +} + +void +uring_tbuf_read_until(struct uring_task *task, +		      rutask_cb_t is_complete_cb, utask_cb_t final_cb) +{ +	assert_return(task && task->fd >= 0 && task->tbuf && is_complete_cb && final_cb); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	task->tbuf->len = 0; +	task->is_complete_cb = is_complete_cb; +	task->final_cb = final_cb; +	uring_read(task, &task->tbuf->buf, sizeof(task->tbuf->buf), +		   uring_tbuf_read_until_cb); +} + +static int +uring_tbuf_eof(struct uring_task *task, int res) +{ +	assert_return(task && task->tbuf, -EINVAL); +	assert_task_alive_or(DBG_UR, task, return -EINTR); + +	if (task->tbuf->len + 1 >= sizeof(task->tbuf->buf)) +		return -E2BIG; + +	if (res > 0) +		return 0; + +	task->tbuf->buf[task->tbuf->len] = '\0'; +	task->tbuf->len++; +	return 1; +} + +void +uring_tbuf_read_until_eof(struct uring_task *task, +			  utask_cb_t final_cb) +{ +	assert_return(task && task->tbuf && final_cb); + +	uring_tbuf_read_until(task, uring_tbuf_eof, final_cb); +} + +static int +uring_tbuf_have_data(struct uring_task *task, int res) +{ +	assert_return(task, -EINVAL); + +	if (res < 0) +		return res; +	else +		return 1; +} + +void +uring_tbuf_read(struct uring_task *task, utask_cb_t final_cb) +{ +	assert_return(task && final_cb); + +	uring_tbuf_read_until(task, uring_tbuf_have_data, final_cb); +} + +void +uring_read_offset(struct uring_task *task, void *buf, size_t len, off_t offset, utask_cb_t cb) +{ +	struct io_uring_sqe *sqe; + +	assert_return(task && buf && len > 0 && task->fd >= 0); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	sqe = get_sqe(task); +	task->cb = cb; +	io_uring_prep_read(sqe, task->fd, buf, len, offset); +	io_uring_sqe_set_data(sqe, task); +} + +void +uring_openat(struct uring_task *task, const char *path, utask_cb_t cb) +{ +	struct io_uring_sqe *sqe; +        +	assert_return(task && !empty_str(path) && cb); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	sqe = get_sqe(task); +	task->cb = cb; +	io_uring_prep_openat(sqe, AT_FDCWD, path, O_RDONLY | O_CLOEXEC, 0); +	io_uring_sqe_set_data(sqe, task); +} + +void +uring_tbuf_recvmsg(struct uring_task *task, utask_cb_t cb) +{ +	struct io_uring_sqe *sqe; + +	assert_return(task && task->fd >= 0 && task->tbuf && cb); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	sqe = get_sqe(task); +	task->tbuf->done = 0; +	task->tbuf->len = 0; +	task->tbuf->iov.iov_len = sizeof(task->tbuf->buf); +	task->tbuf->msg.msg_namelen = task->saddr.addrlen; +	task->cb = cb; +	io_uring_prep_recvmsg(sqe, task->fd, &task->tbuf->msg, 0); +	io_uring_sqe_set_data(sqe, task); +} + +void +uring_tbuf_sendmsg(struct uring_task *task, utask_cb_t cb) +{ +	struct io_uring_sqe *sqe; + +	assert_return(task && task->fd >= 0 && task->tbuf && cb); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	sqe = get_sqe(task); +	task->tbuf->done = 0; +	task->tbuf->iov.iov_len = task->tbuf->len; +	task->tbuf->msg.msg_namelen = task->saddr.addrlen; +	task->cb = cb; +	io_uring_prep_sendmsg(sqe, task->fd, &task->tbuf->msg, 0); +	io_uring_sqe_set_data(sqe, task); +} + +void +uring_connect(struct uring_task *task, struct saddr *saddr, utask_cb_t cb) +{ +	struct io_uring_sqe *sqe; + +	assert_return(task && task->fd >= 0 && saddr && cb); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	sqe = get_sqe(task); +	task->cb = cb; +	io_uring_prep_connect(sqe, task->fd, (struct sockaddr *)&saddr->storage, saddr->addrlen); +	io_uring_sqe_set_data(sqe, task); +} + +void +uring_accept(struct uring_task *task, struct saddr *saddr, utask_cb_t cb) +{ +	struct io_uring_sqe *sqe; + +	assert_return(task && task->fd >= 0 && saddr && cb); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	sqe = get_sqe(task); +	saddr->addrlen = sizeof(saddr->storage); +	task->cb = cb; +	io_uring_prep_accept(sqe, task->fd, (struct sockaddr *)&saddr->storage, &saddr->addrlen, SOCK_CLOEXEC); +	io_uring_sqe_set_data(sqe, task); +} + +void +uring_splice(struct uring_task *task, int fd_in, int fd_out, utask_cb_t cb) +{ +	struct io_uring_sqe *sqe; +	 +	assert_return(task && fd_in >= 0 && fd_out >= 0 && cb); + +	debug(DBG_UR, "task %s (%p), fd_in %i, fd_out %i, refcount %u", +	      task->name, task, fd_in, fd_out, task->refcount); + +	sqe = get_sqe(task); +	task->cb = cb; +	io_uring_prep_splice(sqe, fd_in, -1, fd_out, -1, 4096, SPLICE_F_MOVE); +	io_uring_sqe_set_data(sqe, task); +} + +void +uring_poll(struct uring_task *task, short poll_mask, utask_cb_t cb) +{ +	struct io_uring_sqe *sqe; +	 +	assert_return(task && task->fd >= 0 && poll_mask && cb); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	sqe = get_sqe(task); +	task->cb = cb; +	io_uring_prep_poll_add(sqe, task->fd, poll_mask); +	io_uring_sqe_set_data(sqe, task); +} + +void +uring_poll_cancel(struct uring_task *task) +{ +	struct io_uring_sqe *sqe; +	 +	assert_return(task); + +	if (task->fd < 0) { +		/* not an error, no need to print error msg */ +		return; +	} + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	sqe = get_sqe(task); +	task->dead = true; +	io_uring_prep_poll_remove(sqe, task); +	io_uring_sqe_set_data(sqe, (void *)((uintptr_t)task | CQE_TYPE_POLL_CANCEL)); +} + +static void +uring_free(struct uring_task *task) +{ +	struct uring_ev *uring = container_of(task, struct uring_ev, task); + +	assert_return(task); + +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	io_uring_queue_exit(&uring->uring); +	cfg->uring = NULL; +	xfree(uring); +} + +void +uring_refdump() +{ +	assert_return(cfg->uring); + +	uring_task_refdump(&cfg->uring->task); +} + +void +uring_delete() +{ +	struct uring_task *task; + +	assert_return(cfg->uring); + +	task = &cfg->uring->task; +	debug(DBG_UR, "task %s (%p), fd %i, refcount %u", +	      task->name, task, task->fd, task->refcount); + +	uring_task_put(task); +} + +static void +uring_splice_test_cb(struct uring_task *task, int res) +{ +	struct uring_ev *uring = container_of(task, struct uring_ev, task); + +	assert_die(task && uring == cfg->uring, "splice test failed"); + +	uring_close(task, uring->tfd); +	uring_close(task, uring->pipe[PIPE_RD]); +	uring_close(task, uring->pipe[PIPE_WR]); + +	uring->tfd = -1; +	uring->pipe[PIPE_RD] = -1; +	uring->pipe[PIPE_WR] = -1; + +	if (res >= 0) { +		cfg->splice_supported = true; +		debug(DBG_UR, "splice supported"); +	} else if (res == -EINVAL) +		debug(DBG_UR, "splice not supported"); +	else  +		error("splice check failed: %i\n", res); +} + +void +uring_init() +{ +	struct uring_ev *uring; + +	assert_return(!cfg->uring); + +	uring = zmalloc(sizeof(*uring)); +	if (!uring) +		die("malloc: %m"); + +	if (io_uring_queue_init_params(4096, &uring->uring, &uring->uring_params) < 0) +		die("io_uring_queue_init_params"); + +	debug(DBG_UR, "uring initialized, features: 0x%08x", +	      uring->uring_params.features); + +	uring_task_init(&uring->task, "io_uring", &cfg->task, uring_free); +	cfg->uring = uring; + +	/* splice check, a bit convoluted, but seems to be no simpler way */ +	cfg->splice_supported = false; +	if (pipe2(uring->pipe, O_CLOEXEC) < 0) +		die("pipe2: %m"); +	uring->tfd = open("/dev/null", O_RDONLY | O_CLOEXEC | O_NOCTTY); +	if (uring->tfd < 0) +		die("open(\"/dev/null\"): %m"); +	uring_splice(&uring->task, uring->tfd, uring->pipe[PIPE_WR], uring_splice_test_cb); +} + +static inline void +uring_print_cqe(const char *type, struct uring_task *task, +		struct io_uring_cqe *cqe) +{ +	assert_return(!empty_str(type) && task && cqe); + +	debug(DBG_UR, "got CQE " +	      "(type: %s, res: %i (%s), task: %s (%p), fd: %i, cb: %p)", +	      type, +	      cqe->res, +	      cqe->res < 0 ? strerror(-cqe->res) : "ok", +	      task->name ? task->name : "<none>", +	      task, +	      task->fd, +	      task->cb); +} + +void +uring_event_loop() +{ +	while (true) { +		struct io_uring_cqe *cqe; +		unsigned nr, head; +		int r; + +		io_uring_submit(&cfg->uring->uring); +		r = io_uring_wait_cqe(&cfg->uring->uring, &cqe); +		if (r < 0) { +			if (errno == EINTR) +				continue; +			else +				die("io_uring_wait_cqe: %i", r); +		} + +		nr = 0; +		io_uring_for_each_cqe(&cfg->uring->uring, head, cqe) { +			struct uring_task *task = io_uring_cqe_get_data(cqe); +			bool do_cb; +			enum cqe_type cqe_type; + +			cqe_count++; + +			cqe_type = ((uintptr_t)task & CQE_TYPE_PTR_MASK); +			task = (void *)((uintptr_t)task & ~CQE_TYPE_PTR_MASK); + +			if (!task) +				die("null task"); + +			switch (cqe_type) { +			case CQE_TYPE_CANCEL: +				uring_print_cqe("cancel", task, cqe); +				do_cb = false; +				break; + +			case CQE_TYPE_CLOSE: +				uring_print_cqe("close", task, cqe); +				do_cb = false; +				break; + +			case CQE_TYPE_POLL_CANCEL: +				uring_print_cqe("poll_cancel", task, cqe); +				do_cb = false; +				break; + +			case CQE_TYPE_NORMAL: +				uring_print_cqe("standard", task, cqe); +				do_cb = true; +				break; + +			default: +				die("unknown CQE type"); +			} + +			if (do_cb && task->cb) +				task->cb(task, cqe->res); + +			uring_task_put(task); + +			if (exiting) +				return; + +			nr++; +		} + +		io_uring_cq_advance(&cfg->uring->uring, nr); +	} +} + diff --git a/mcserverproxy/uring.h b/mcserverproxy/uring.h new file mode 100644 index 0000000..9c33104 --- /dev/null +++ b/mcserverproxy/uring.h @@ -0,0 +1,73 @@ +#ifndef foouringhfoo +#define foouringhfoo + +extern uint64_t sqe_count; +extern uint64_t cqe_count; + +void uring_task_refdump(struct uring_task *task); + +void uring_task_destroy(struct uring_task *task); + +void uring_task_put(struct uring_task *task); + +void uring_task_get(struct uring_task *task); + +void uring_task_set_buf(struct uring_task *task, struct uring_task_buf *tbuf); + +void uring_task_set_fd(struct uring_task *task, int fd); + +void uring_task_close_fd(struct uring_task *task); + +struct uring_task *uring_parent(); + +void uring_task_init(struct uring_task *task, const char *name, +		     struct uring_task *parent, +		     void (*free)(struct uring_task *)); + +void uring_close(struct uring_task *task, int fd); + +void uring_tbuf_write(struct uring_task *task, utask_cb_t final_cb); + +void uring_write(struct uring_task *task, void *buf, size_t len, utask_cb_t cb); + +void uring_tbuf_read_until(struct uring_task *task, +			   rutask_cb_t is_complete_cb, utask_cb_t final_cb); + +void uring_tbuf_read_until_eof(struct uring_task *task, utask_cb_t final_cb); + +void uring_tbuf_read(struct uring_task *task, utask_cb_t final_cb); + +void uring_read_offset(struct uring_task *task, void *buf, +		       size_t len, off_t offset, utask_cb_t cb); + +static inline void +uring_read(struct uring_task *task, void *buf, size_t len, utask_cb_t cb) +{ +	uring_read_offset(task, buf, len, 0, cb); +} + +void uring_openat(struct uring_task *task, const char *path, utask_cb_t cb); + +void uring_tbuf_recvmsg(struct uring_task *task, utask_cb_t cb); + +void uring_tbuf_sendmsg(struct uring_task *task, utask_cb_t cb); + +void uring_connect(struct uring_task *task, struct saddr *saddr, utask_cb_t cb); + +void uring_accept(struct uring_task *task, struct saddr *saddr, utask_cb_t cb); + +void uring_splice(struct uring_task *task, int fd_in, int fd_out, utask_cb_t cb); + +void uring_poll(struct uring_task *task, short poll_mask, utask_cb_t cb); + +void uring_poll_cancel(struct uring_task *task); + +void uring_delete(); + +void uring_refdump(); + +void uring_init(); + +void uring_event_loop(); + +#endif diff --git a/mcserverproxy/utils.c b/mcserverproxy/utils.c new file mode 100644 index 0000000..eacc586 --- /dev/null +++ b/mcserverproxy/utils.c @@ -0,0 +1,430 @@ +#include <stdlib.h> +#include <errno.h> +#include <stdint.h> +#include <limits.h> +#include <arpa/inet.h> +#include <string.h> +#include <sys/types.h> +#include <dirent.h> +#include <fcntl.h> +#include <unistd.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <inttypes.h> + +#include "main.h" +#include "utils.h" +#include "uring.h" + +static unsigned total_malloc_count = 0; +static int malloc_count = 0; + +LIST_HEAD(malloc_list); + +struct allocation { +	const char *allocfn; +	const char *callerfn; +	int line; +	void *ptr; +	size_t size; +	struct list_head list; +}; + +static void +add_allocation(const char *allocfn, const char *callerfn, int line, void *ptr, size_t size) +{ +	struct allocation *a; + +	assert_die(!empty_str(allocfn) && !empty_str(callerfn) && line > 0 && ptr && size > 0, "invalid arguments"); + +	a = malloc(sizeof(*a)); +	if (!a) +		die("malloc: %m"); +	a->allocfn = allocfn; +	a->callerfn = callerfn; +	a->line = line; +	a->ptr = ptr; +	a->size = size; +	list_add(&a->list, &malloc_list); +	total_malloc_count++; +	malloc_count++; +	debug(DBG_MALLOC, "called from %s:%i - %s(%zu) = %p (%p)", +	      callerfn, line, allocfn, size, ptr, a); +} + +void * +__zmalloc(const char *fn, int line, size_t size) +{ +	void *ptr; + +	assert_die(!empty_str(fn) && line > 0 && size > 0, "invalid arguments"); + +	ptr = calloc(1, size); +	if (ptr) +		add_allocation("zmalloc", fn, line, ptr, size); +	return ptr; +} + +char * +__xstrdup(const char *fn, int line, const char *s) +{ +	char *ptr; + +	assert_die(!empty_str(fn) && line > 0 && !empty_str(s), "invalid arguments"); + +	ptr = strdup(s); +	if (ptr) +		add_allocation("xstrdup", fn, line, ptr, strlen(s) + 1); +	return ptr; +} + +char * +__xstrndup(const char *fn, int line, const char *s, size_t n) +{ +	char *ptr; + +	assert_die(!empty_str(fn) && line > 0 && !empty_str(s) && n > 0, "invalid arguments"); + +	ptr = strndup(s, n); +	if (ptr) +		add_allocation("xstrndup", fn, line, ptr, n); +	return ptr; +} + +void +__xfree(const char *fn, int line, void *ptr) +{ +	struct allocation *a, *tmp; +	unsigned delete_count = 0; + +	assert_die(!empty_str(fn) && line > 0, "invalid arguments"); + +	if (!ptr) +		return; +	free(ptr); +	malloc_count--; + +	debug(DBG_MALLOC, "called from %s:%i - %p", fn, line, ptr); + +	list_for_each_entry_safe(a, tmp, &malloc_list, list) { +		if (a->ptr == ptr) { +			list_del(&a->list); +			free(a); +			delete_count++; +		} +	} + +	if (delete_count != 1) { +		error("Delete count is %u for ptr 0x%p", delete_count, ptr); +		exit(EXIT_FAILURE); +	} +} + +void +debug_resource_usage() +{ +	struct allocation *a; +	DIR *dir; +	struct dirent *dent; +	char buf[4096]; +	ssize_t r; +	unsigned file_count = 0; + +	debug(DBG_MALLOC, "Still malloced %i (total %u)", +	      malloc_count, total_malloc_count); + +	list_for_each_entry(a, &malloc_list, list) { +		debug(DBG_MALLOC, "* Lost allocation - %s:%i - ptr: %p, size: %zu", +		      a->callerfn, a->line, a->ptr, a->size); +	} + +	dir = opendir("/proc/self/fd"); +	if (!dir) { +		error("failed to open fd dir"); +		return; +	} + +	debug(DBG_MALLOC, "Open files:"); +	while ((dent = readdir(dir)) != NULL) { +		if (streq(dent->d_name, ".") || streq(dent->d_name, "..")) +			continue; + +		r = readlinkat(dirfd(dir), dent->d_name, buf, sizeof(buf)); +		if (r < 0) { +			debug(DBG_MALLOC, "Failed to readlink %s", dent->d_name); +			continue; +		} +		buf[r] = '\0'; +		debug(DBG_MALLOC, " * %s -> %s", dent->d_name, buf); +		file_count++; +	} +	closedir(dir); + +	if (file_count > 4) +		debug(DBG_MALLOC, "Lost file descriptor(s)"); + +	debug(DBG_MALLOC, "CQEs used: %" PRIu64 ", SQEs used: %" PRIu64, +	      cqe_count, sqe_count); +} + +void +socket_set_low_latency(int sfd) +{ +	int option; + +	assert_return(sfd >= 0); + +	/* Probably not necessary, but can't hurt */ +	if (cfg->socket_defer) { +		option = true; +		if (setsockopt(sfd, SOL_SOCKET, SO_KEEPALIVE, &option, sizeof(option)) < 0) +			error("setsockopt: %m"); +	} + +	/* Doubtful if it has much effect, but can't hurt */ +	if (cfg->socket_iptos) { +		option = IPTOS_LOWDELAY; +		if (setsockopt(sfd, IPPROTO_IP, IP_TOS, &option, sizeof(option)) < 0) +			error("setsockopt: %m"); +	} + +	/* Nagle's algorithm is a poor fit for gaming */ +	if (cfg->socket_nodelay) { +		option = true; +		if (setsockopt(sfd, IPPROTO_TCP, TCP_NODELAY, &option, sizeof(option)) < 0) +			error("setsockopt: %m"); +	} +} + +void +connection_set_local(struct connection *conn, int fd) +{ +	assert_return(conn && fd >= 0); + +	conn->local.addrlen = sizeof(conn->local.storage); +	if (getsockname(fd, (struct sockaddr *)&conn->local.storage, +			&conn->local.addrlen) < 0) +		sprintf(conn->local.addrstr, "<unknown>"); +	else +		saddr_set_addrstr(&conn->local); +} + +void +connection_set_remote(struct connection *conn, struct saddr *remote) +{ +	assert_return(conn && remote); + +	conn->remote = *remote; +	saddr_set_addrstr(&conn->remote); +} + +static void connect_next(struct uring_task *task, struct connection *conn); + +static void +connect_cb(struct uring_task *task, int res) +{ +	struct connection *conn; + +	assert_return(task && task->priv); + +	conn = task->priv; +	if (res < 0) { +		debug(DBG_SRV, "%s: connection to %s failed", +		      task->name, conn->remote.addrstr); +		uring_task_close_fd(task); +		connect_next(task, conn); +		return; +	} + +	connection_set_local(conn, task->fd); + +	debug(DBG_SRV, "%s: connection established %s -> %s", +	      task->name, conn->local.addrstr, conn->remote.addrstr); + +	conn->cb(conn, true); +} + +static void +connect_next(struct uring_task *task, struct connection *conn) +{ +        struct saddr *remote, *tmp; +        int sfd; +        unsigned i; + +	assert_return(task && conn && conn->cb); +again: +	assert_task_alive_or(DBG_UR, task, goto out); + +	i = 0; +        remote = NULL; +        list_for_each_entry(tmp, conn->addrs, list) { +                if (i == conn->next_addr) { +                        remote = tmp; +                        break; +                } +                i++; +        } + +        if (!remote) { +		debug(DBG_SRV, "%s: no more remote addresses to attempt", +		      task->name); +		goto out; +        } + +	conn->next_addr++; +	connection_set_remote(conn, remote); +        debug(DBG_SRV, "%s: attempting to connect to %s", +	      task->name, conn->remote.addrstr); + +        sfd = socket(conn->remote.storage.ss_family, SOCK_STREAM | SOCK_CLOEXEC, 0); +        if (sfd < 0) { +                error("socket: %m"); +                goto again; +        } + +	socket_set_low_latency(sfd); + +	task->priv = conn; +        uring_task_set_fd(task, sfd); +        uring_connect(task, &conn->remote, connect_cb); +	return; + +out: +	conn->cb(conn, false); +} + +void +connect_any(struct uring_task *task, +	    struct list_head *addrs, struct connection *conn, +	    connection_cb_t cb) +{ +	assert_return(task && addrs && conn && cb); + +	conn->next_addr = 0; +	conn->addrs = addrs; +	conn->cb = cb; +	connect_next(task, conn); +} + +uint16_t +saddr_port(struct saddr *saddr) +{ +	assert_return(saddr, 0); + +	switch (saddr->storage.ss_family) { +	case AF_INET: +		return ntohs(saddr->in4.sin_port); +	case AF_INET6: +		return ntohs(saddr->in6.sin6_port); +	default: +		return 0; +	} +} + +char * +saddr_addr(struct saddr *saddr, char *buf, size_t len) +{ +	assert_return(saddr && buf && len > 0, NULL); + +	switch (saddr->storage.ss_family) { +	case AF_INET: +		if (inet_ntop(saddr->in4.sin_family, &saddr->in4.sin_addr, buf, len)) +			return buf; +		break; +	case AF_INET6: +		if (inet_ntop(saddr->in6.sin6_family, &saddr->in6.sin6_addr, buf, len)) +			return buf; +		break; +	default: +		break; +	} + +	snprintf(buf, len, "<unknown>"); +	return buf; +} + +void +saddr_set_ipv4(struct saddr *saddr, in_addr_t ip, in_port_t port) +{ +	assert_return(saddr); + +	memset(&saddr->in4, 0, sizeof(saddr->in4)); +	saddr->in4.sin_family = AF_INET; +	saddr->in4.sin_port = port; +	saddr->in4.sin_addr.s_addr = ip; +	saddr->addrlen = sizeof(saddr->in4); +	saddr_set_addrstr(saddr); +} + +void +saddr_set_ipv6(struct saddr *saddr, const struct in6_addr *ip, in_port_t port) +{ +	assert_return(saddr && ip); + +	memset(&saddr->in6, 0, sizeof(saddr->in6)); +	saddr->in6.sin6_family = AF_INET6; +	saddr->in6.sin6_port = port; +	if (ip) +		saddr->in6.sin6_addr = *ip; +	saddr->addrlen = sizeof(saddr->in6); +	saddr_set_addrstr(saddr); +} + +void +saddr_set_addrstr(struct saddr *saddr) +{ +	assert_return(saddr); + +	char abuf[ADDRSTRLEN]; + +	switch (saddr->storage.ss_family) { +	case AF_INET: +		snprintf(saddr->addrstr, sizeof(saddr->addrstr), +			 "AF_INET4 %s %" PRIu16, +			 saddr_addr(saddr, abuf, sizeof(abuf)), +			 saddr_port(saddr)); +		break; +	case AF_INET6: +		snprintf(saddr->addrstr, sizeof(saddr->addrstr), +			 "AF_INET6 %s %" PRIu16, +			 saddr_addr(saddr, abuf, sizeof(abuf)), +			 saddr_port(saddr)); +		break; +	default: +		snprintf(saddr->addrstr, sizeof(saddr->addrstr), "AF_UNKNOWN"); +		break; +	} +} + +int +strtou16_strict(const char *str, uint16_t *result) +{ +	char *end; +	long val; + +	assert_return(!empty_str(str) && result, -EINVAL); + +	errno = 0; +	val = strtol(str, &end, 10); + +	if (errno == ERANGE && (val == LONG_MAX || val == LONG_MIN)) +		return -EINVAL; +	 +	if (errno != 0 && val == 0) +		return -EINVAL; + +	if (end == str) +		return -EINVAL; + +	if (*end != '\0') +		return -EINVAL; + +	if (val < 1 || val > UINT16_MAX) +		return -EINVAL; + +	if (result) +		*result = val; +	return 0; +} + diff --git a/mcserverproxy/utils.h b/mcserverproxy/utils.h new file mode 100644 index 0000000..c36a36c --- /dev/null +++ b/mcserverproxy/utils.h @@ -0,0 +1,245 @@ +#ifndef fooutilshfoo +#define fooutilshfoo + +#include <stdio.h> +#include <string.h> +#include <stdbool.h> +#include <stdlib.h> +#include <linux/if_packet.h> + +struct list_head { +	struct list_head *next; +	struct list_head *prev; +}; + +#define zmalloc(s) __zmalloc(__func__, __LINE__, s) +void *__zmalloc(const char *fn, int line, size_t s); + +#define xstrdup(s) __xstrdup(__func__, __LINE__, s) +char *__xstrdup(const char *fn, int line, const char *s); + +#define xstrndup(s, n) __xstrndup(__func__, __LINE__, s, n) +char *__xstrndup(const char *fn, int line, const char *s, size_t n); + +#define xfree(s) __xfree(__func__, __LINE__, s) +void __xfree(const char *fn, int line, void *ptr); + +void debug_resource_usage(); + +/* Length of longest DNS name = 253 + trailing dot */ +#define FQDN_STR_LEN 254 + +/* Length of longest port string = strlen("65535") */ +#define PORT_STR_LEN 5 + +/* Length of longest address family string = strlen("AF_INETX") */ +#define AF_STR_LEN 8 + +/* Length of longest addrstr, format = "AF_INETX <IPADDR> <PORT> */ +#define ADDRSTRLEN (AF_STR_LEN + 1 + INET6_ADDRSTRLEN + 1 + PORT_STR_LEN + 1) +struct saddr { +	union { +		struct sockaddr_storage storage; +		struct sockaddr_in in4; +		struct sockaddr_in6 in6; +		struct sockaddr_ll ll; +	}; +	socklen_t addrlen; +	char addrstr[ADDRSTRLEN]; +	struct list_head list; +}; + +struct connection; + +typedef void(*connection_cb_t)(struct connection *, bool); + +struct connection { +	struct saddr remote; +	struct saddr local; + +	struct list_head *addrs; +	unsigned next_addr; + +	connection_cb_t cb; +}; + +struct uring_task; + +void socket_set_low_latency(int sfd); + +void connection_set_local(struct connection *conn, int fd); + +void connection_set_remote(struct connection *conn, struct saddr *remote); + +void connect_any(struct uring_task *task, +		 struct list_head *addrs, struct connection *conn, +		 connection_cb_t cb); + +char *saddr_addr(struct saddr *saddr, char *buf, size_t len); + +uint16_t saddr_port(struct saddr *saddr); + +void saddr_set_ipv4(struct saddr *saddr, in_addr_t ip, in_port_t port); + +void saddr_set_ipv6(struct saddr *saddr, const struct in6_addr *ip, in_port_t port); + +void saddr_set_addrstr(struct saddr *saddr); + +int strtou16_strict(const char *str, uint16_t *result); + +static inline bool empty_str(const char *str) +{ +	if (!str || str[0] == '\0') +		return true; +	else +		return false; +} + +static inline bool streq(const char *a, const char *b) +{ +	return strcmp(a, b) == 0; +} + +static inline bool strcaseeq(const char *a, const char *b) +{ +	return strcasecmp(a, b) == 0; +} + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define chtobe32(x) __bswap_constant_32(x) +#else +#define chtobe32(x) (x) +#endif + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define cinet_addr(a,b,c,d) ((uint32_t)((a)<<0|(b)<<8|(c)<<16|(d)<<24)) +#else +#define cinet_addr(a,b,c,d) ((uint32_t)((a)<<24|(b)<<16|(c)<<8|(d)<<0)) +#endif + +#define LIST_HEAD_INIT(name) { &(name), &(name) } + +#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name) + +static inline void list_init(struct list_head *list) +{ +	list->next = list; +	list->prev = list; +} + +static inline void list_del(struct list_head *list) +{ +	list->next->prev = list->prev; +	list->prev->next = list->next; +} + +static inline void list_add(struct list_head *new, struct list_head *list) +{ +	list->next->prev = new; +	new->next = list->next; +	new->prev = list; +	list->next = new; +} + +static inline void list_replace(struct list_head *old, struct list_head *new) +{ +	old->prev->next = new; +	old->next->prev = new; +	new->next = old->next; +	new->prev = old->prev; +	old->next = old->prev = NULL; +} + +static inline bool list_empty(struct list_head *list) +{ +	return list->next == list; +} + +#define PIPE_RD 0 +#define PIPE_WR 1 + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) + +#define MAX(a, b) ((a) > (b) ? (a) : (b)) + +#define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d)) + +#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) + +#define container_of(ptr, type, member) ({				\ +	const typeof( ((type *)0)->member ) *__mptr = (ptr);		\ +	(type *)( (char *)__mptr - offsetof(type,member) );}) + +#define list_entry(ptr, type, member)					\ +	container_of(ptr, type, member) + +#define list_first_entry(ptr, type, member)				\ +	list_entry((ptr)->next, type, member) + +#define list_next_entry(pos, member)					\ +	list_entry((pos)->member.next, typeof(*(pos)), member) + +#define list_for_each(pos, head)					\ +	for (pos = (head)->next; pos != (head); pos = pos->next) + +#define list_for_each_entry(pos, head, member)				\ +	for (pos = list_first_entry(head, typeof(*pos), member);	\ +	     &pos->member != (head);					\ +	     pos = list_next_entry(pos, member)) + +#define list_for_each_entry_safe(pos, n, head, member)			\ +	for (pos = list_entry((head)->next, typeof(*pos), member),	\ +	     n = list_entry(pos->member.next, typeof(*pos), member);	\ +	     &pos->member != (head);					\ +	     pos = n, n = list_entry(n->member.next, typeof(*n), member)) + +/* +#define _cleanup_(x) __attribute__((cleanup(x))) + +#define DEFINE_TRIVIAL_CLEANUP_FUNC(type, func)				\ +	static inline void func##p(type *p) {				\ +		if (*p)							\ +			func(*p);					\ +	}								\ +	struct __useless_struct_to_allow_trailing_semicolon__ + +static inline unsigned +strv_length(char **strv) +{ +	unsigned len; + +	for (len = 0; strv && *strv; strv++) +		len++; + +	return len; +} + +static inline void strv_free(char **l) { +	char **k; +	if (l) { +		for (k = l; *k; k++) +			free(k); +		free(l); +	} +} +DEFINE_TRIVIAL_CLEANUP_FUNC(char **, strv_free); +#define _cleanup_strv_free_ _cleanup_(strv_freep) + +static inline void freep(void *p) { +	free(*(void**) p); +} +#define _cleanup_free_ _cleanup_(freep) + +DEFINE_TRIVIAL_CLEANUP_FUNC(int, close); +#define _cleanup_close_ _cleanup_(closep) + +DEFINE_TRIVIAL_CLEANUP_FUNC(FILE *, fclose); +#define _cleanup_fclose_ _cleanup_(fclosep) + +DEFINE_TRIVIAL_CLEANUP_FUNC(DIR *, closedir); +#define _cleanup_closedir_ _cleanup_(closedirp) + +*/ + +#endif + | 
