From: Stefan Sperling Subject: gotd per-uid connection limit To: gameoftrees@openbsd.org Date: Fri, 30 Dec 2022 18:47:50 +0100 gotd should enforce a limit on the number of connections per user. This patch adds such a limit. The limit is hard-coded for now. Developers will only run fetch/send commands occasionally, which means a fairly low default limit can be used. I have tested this patch manually and it seems to work as expected. Connections beyond the limit are dropped, and new connections become possible once active connections have terminated. The limit will need to be made configurable via gotd.conf later. Ideally, it should be possible to configure connection limits per user/UID. Servers offering anonymous fetches we will want to use a relatively high limit for the anonymous user, because most clients will then appear with the same anonymous UID. Something else we could build on top of this is rate-limiting of new connections on a per-UID basis. With low limits this doesn't matter very much, but it could matter for the anonymous case with a higher limit. ok? diff c602198afc6ce7d8c96397f6482e7aff4e02db41 4ed29391aa7da22b2818462c73a60f3890206cca commit - c602198afc6ce7d8c96397f6482e7aff4e02db41 commit + 4ed29391aa7da22b2818462c73a60f3890206cca blob - fe0c0107c18a0d38f7565091ea1d3badb537969e blob + d17cc8a6c43063e47af1fb6b1ce517a12eb909c5 --- gotd/gotd.h +++ gotd/gotd.h @@ -23,6 +23,7 @@ #define GOTD_EMPTY_PATH "/var/empty" #define GOTD_MAXCLIENTS 1024 +#define GOTD_MAX_CONN_PER_UID 4 #define GOTD_FD_RESERVE 5 #define GOTD_FD_NEEDED 6 #define GOTD_FILENO_MSG_PIPE 3 blob - 075110e34e7f07475aa0f1df07f74fac98521044 blob + bcc891f386e858648339da2b211be2df91530d4b --- gotd/listen.c +++ gotd/listen.c @@ -46,6 +46,7 @@ struct gotd_listen_client { STAILQ_ENTRY(gotd_listen_client) entry; uint32_t id; int fd; + uid_t euid; }; STAILQ_HEAD(gotd_listen_clients, gotd_listen_client); @@ -54,6 +55,15 @@ static struct { static volatile int listen_client_cnt; static int inflight; +struct gotd_uid_connection_counter { + STAILQ_ENTRY(gotd_uid_connection_counter) entry; + uid_t euid; + int nconnections; +}; +STAILQ_HEAD(gotd_client_uids, gotd_uid_connection_counter); +static struct gotd_client_uids gotd_client_uids[GOTD_CLIENT_TABLE_SIZE]; +static SIPHASH_KEY uid_hash_key; + static struct { pid_t pid; const char *title; @@ -132,9 +142,46 @@ static const struct got_error * return id; } +static uint64_t +uid_hash(uid_t euid) +{ + return SipHash24(&uid_hash_key, &euid, sizeof(euid)); +} + +static void +add_uid_connection_counter(struct gotd_uid_connection_counter *counter) +{ + uint64_t slot = uid_hash(counter->euid) % nitems(gotd_client_uids); + STAILQ_INSERT_HEAD(&gotd_client_uids[slot], counter, entry); +} + +static void +remove_uid_connection_counter(struct gotd_uid_connection_counter *counter) +{ + uint64_t slot = uid_hash(counter->euid) % nitems(gotd_client_uids); + STAILQ_REMOVE(&gotd_client_uids[slot], counter, + gotd_uid_connection_counter, entry); +} + +static struct gotd_uid_connection_counter * +find_uid_connection_counter(uid_t euid) +{ + uint64_t slot; + struct gotd_uid_connection_counter *c; + + slot = uid_hash(euid) % nitems(gotd_client_uids); + STAILQ_FOREACH(c, &gotd_client_uids[slot], entry) { + if (c->euid == euid) + return c; + } + + return NULL; +} + static const struct got_error * disconnect(struct gotd_listen_client *client) { + struct gotd_uid_connection_counter *counter; uint64_t slot; int client_fd; @@ -143,6 +190,17 @@ disconnect(struct gotd_listen_client *client) slot = client_hash(client->id) % nitems(gotd_listen_clients); STAILQ_REMOVE(&gotd_listen_clients[slot], client, gotd_listen_client, entry); + + counter = find_uid_connection_counter(client->euid); + if (counter) { + if (counter->nconnections > 0) + counter->nconnections--; + if (counter->nconnections == 0) { + remove_uid_connection_counter(counter); + free(counter); + } + } + client_fd = client->fd; free(client); inflight--; @@ -189,6 +247,7 @@ gotd_accept(int fd, short event, void *arg) socklen_t len; int s = -1; struct gotd_listen_client *client = NULL; + struct gotd_uid_connection_counter *counter = NULL; struct gotd_imsg_connect iconn; uid_t euid; gid_t egid; @@ -233,6 +292,25 @@ gotd_accept(int fd, short event, void *arg) goto err; } + counter = find_uid_connection_counter(euid); + if (counter == NULL) { + counter = calloc(1, sizeof(*counter)); + if (counter == NULL) { + log_warn("%s: calloc", __func__); + goto err; + } + counter->euid = euid; + counter->nconnections = 1; + add_uid_connection_counter(counter); + } else { + if (counter->nconnections >= GOTD_MAX_CONN_PER_UID) { + log_warnx("maximum connections exceeded for uid %d", + euid); + goto err; + } + counter->nconnections++; + } + client = calloc(1, sizeof(*client)); if (client == NULL) { log_warn("%s: calloc", __func__); @@ -240,6 +318,7 @@ gotd_accept(int fd, short event, void *arg) } client->id = get_client_id(); client->fd = s; + client->euid = euid; s = -1; add_client(client); log_debug("%s: new client connected on fd %d uid %d gid %d", __func__, @@ -353,6 +432,7 @@ listen_main(const char *title, int gotd_socket) struct event evsigint, evsigterm, evsighup, evsigusr1; arc4random_buf(&clients_hash_key, sizeof(clients_hash_key)); + arc4random_buf(&uid_hash_key, sizeof(uid_hash_key)); gotd_listen.title = title; gotd_listen.pid = getpid();