DEADSOFTWARE

master: add unban command
[d2df-sdl.git] / src / mastersrv / master.c
index 9b48afc9a4f091803ac449ab87f0dbd6fca59adf..c68d59c1042f9b090632971b5e696648df28ec3e 100644 (file)
@@ -1,3 +1,18 @@
+/* Copyright (C)  Doom 2D: Forever Developers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License ONLY.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
 #include <stdlib.h>
 #include <stdio.h>
 #include <stdint.h>
@@ -7,6 +22,10 @@
 #include <string.h>
 #include <time.h>
 #include <signal.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
 
 #include <enet/enet.h>
 #include <enet/types.h>
 #define MS_URGENT_FILE "urgent.txt"
 #define MS_MOTD_FILE "motd.txt"
 #define MS_BAN_FILE "master_bans.txt"
+#define MS_PIPE_FILE "d2df_master.pipe"
 
 #define DEFAULT_SPAM_CAP 10
 #define DEFAULT_MAX_SERVERS MS_MAX_SERVERS
 #define DEFAULT_MAX_PER_HOST 4
-#define DEFAULT_TIMEOUT 100
-#define DEFAULT_BAN_TIME (3 * 86400)
+#define DEFAULT_SERVER_TIMEOUT 100
+#define DEFAULT_CLIENT_TIMEOUT 3
+#define DEFAULT_SPAM_TIMEOUT 1
 #define DEFAULT_PORT 25665
 
 #define NET_BUFSIZE 65536
@@ -90,6 +111,7 @@ typedef struct server_s {
   char        map[MAX_STRLEN + 2];
   time_t      death_time;
   time_t      timestamp;
+  ENetPeer   *peer; // who sent this server in
 } server_t;
 
 // real servers
@@ -123,7 +145,9 @@ static ban_record_t *banlist;
 
 // settings
 static int ms_port = DEFAULT_PORT;
-static int ms_timeout = DEFAULT_TIMEOUT;
+static int ms_sv_timeout = DEFAULT_SERVER_TIMEOUT;
+static int ms_cl_timeout = DEFAULT_CLIENT_TIMEOUT;
+static int ms_spam_timeout = DEFAULT_SPAM_TIMEOUT;
 static int ms_spam_cap = DEFAULT_SPAM_CAP;
 static char ms_motd[MAX_STRLEN + 1] = "";
 static char ms_urgent[MAX_STRLEN + 1] = "";
@@ -141,6 +165,17 @@ static int cl_spam_cnt;
 
 /* common utility functions */
 
+static char *u_strstrip(char *p) {
+  if (!p) return p;
+  while (isspace(*p)) ++p;
+  const size_t len = strlen(p);
+  if (len) {
+    for (size_t i = len - 1; i && isspace(p[i]); --i)
+      p[i] = '\0';
+  }
+  return p;
+}
+
 static char *u_vabuf(void) {
   static char vabuf[4][MAX_STRLEN];
   static int idx = 0;
@@ -158,14 +193,14 @@ static const char *u_strtime(const time_t t) {
 
 static inline const char *u_logprefix(const enum log_severity_e s) {
   switch (s) {
-    case LOG_WARN: return "WARNING:";
-    case LOG_ERROR: return "ERROR:";
+    case LOG_WARN: return "WARNING: ";
+    case LOG_ERROR: return "ERROR: ";
     default: return "";
   }
 }
 
 static void u_log(const enum log_severity_e severity, const char *fmt, ...) {
-  printf("[%s] %s ", u_strtime(time(NULL)), u_logprefix(severity));
+  printf("[%s] %s", u_strtime(time(NULL)), u_logprefix(severity));
   va_list args;
   va_start(args, fmt);
   vprintf(fmt, args);
@@ -195,6 +230,17 @@ static bool u_strisprint(const char *str) {
   return true;
 }
 
+static bool u_strisver(const char *str) {
+  if (!str || !*str)
+    return false;
+  for (const char *p = str; *p; ++p) {
+    // version strings consist of 0-9 . and space
+    if (!isdigit(*p) && *p != '.' && *p != ' ')
+      return false;
+  }
+  return true;
+}
+
 static const char *u_iptostr(const enet_uint32 host) {
   ENetAddress addr = { .host = host, .port = 0 };
   char *buf = u_vabuf();
@@ -227,7 +273,7 @@ static inline enet_uint32 u_prefixtomask(const enet_uint32 prefix) {
 }
 
 static inline enet_uint32 u_masktoprefix(const enet_uint32 mask) {
-  return (32 - __builtin_ctz(mask));
+  return (32 - __builtin_ctz(ENET_NET_TO_HOST_32(mask)));
 }
 
 static inline void u_printsv(const server_t *sv) {
@@ -328,24 +374,32 @@ void b_write_server(enet_buf_t *buf, const server_t *s) {
 
 /* server functions */
 
-static void sv_remove(const enet_uint32 host, const enet_uint16 port) {
-  for (int i = 0; i < max_servers; ++i) {
-    if (servers[i].host == host && servers[i].port == port) {
-      servers[i].host = 0;
-      servers[i].port = 0;
-      --num_servers;
+static inline void sv_remove(server_t *sv) {
+  if (sv->host) {
+    // drop the associated peer, if any
+    if (sv->peer && sv->peer->state == ENET_PEER_STATE_CONNECTED && sv->peer->data == sv) {
+      sv->peer->data = NULL;
+      enet_peer_reset(sv->peer);
     }
+    sv->host = 0;
+    sv->port = 0;
+    sv->peer = NULL;
+    --num_servers;
+  }
+}
+
+static void sv_remove_by_addr(const enet_uint32 host, const enet_uint16 port) {
+  for (int i = 0; i < max_servers; ++i) {
+    if (servers[i].host == host && servers[i].port == port)
+      sv_remove(servers + i);
   }
 }
 
 static void sv_remove_by_host(enet_uint32 host, enet_uint32 mask) {
   host &= mask;
   for (int i = 0; i < max_servers; ++i) {
-    if (servers[i].host && (servers[i].host & mask) == host) {
-      servers[i].host = 0;
-      servers[i].port = 0;
-      --num_servers;
-    }
+    if (servers[i].host && (servers[i].host & mask) == host)
+      sv_remove(servers + i);
   }
 }
 
@@ -383,6 +437,14 @@ static inline server_t *sv_find_or_add(const enet_uint32 host, const enet_uint32
   return empty;
 }
 
+static inline void sv_clear_peer(ENetPeer *peer) {
+  server_t *sv = peer->data;
+  if (sv) {
+    sv->peer = NULL;
+    peer->data = NULL;
+  }
+}
+
 /* ban list functions */
 
 static inline time_t ban_get_time(const int cnt) {
@@ -445,7 +507,7 @@ static ban_record_t *ban_record_add_addr(const enet_uint32 host, const enet_uint
   return rec;
 }
 
-static ban_record_t *ban_record_add_ip(const char *ip, const int cnt, const time_t cur) {
+static enet_uint32 ban_parse_ip_mask(const char *ip, enet_uint32 *out_mask) {
   enet_uint32 prefix = 32;
 
   // find and get the prefix length, if any
@@ -458,15 +520,23 @@ static ban_record_t *ban_record_add_ip(const char *ip, const int cnt, const time
   }
 
   ENetAddress addr = { 0 };
-  if (enet_address_set_host_ip(&addr, ip_copy) != 0) {
-    u_log(LOG_ERROR, "banlist: `%s` is not a valid IP address", ip_copy);
-    return NULL;
-  }
+  if (enet_address_set_host_ip(&addr, ip_copy) != 0)
+    return 0;
 
   // transform prefix length into mask
-  const enet_uint32 mask = u_prefixtomask(prefix);
+  *out_mask = u_prefixtomask(prefix);
 
-  return ban_record_add_addr(addr.host, mask, cnt, cur);
+  return addr.host;
+}
+
+static ban_record_t *ban_record_add_ip(const char *ip, const int cnt, const time_t cur) {
+  enet_uint32 mask = 0;
+  const enet_uint32 host = ban_parse_ip_mask(ip, &mask);
+  if (!host) {
+    u_log(LOG_ERROR, "banlist: `%s` is not a valid address", ip);
+    return NULL;
+  }
+  return ban_record_add_addr(host, mask, cnt, cur);
 }
 
 static void ban_free_list(void) {
@@ -479,6 +549,30 @@ static void ban_free_list(void) {
   banlist = NULL;
 }
 
+static bool ban_record_remove_addr(const enet_uint32 host, const enet_uint32 mask) {
+  for (ban_record_t *b = banlist; b; b = b->next) {
+    if ((b->host == host) && (b->mask == mask)) {
+      if (b == banlist)
+        banlist = b->next;
+      if (b->next) b->next->prev = b->prev;
+      if (b->prev) b->prev->next = b->next;
+      free(b);
+      return true;
+    }
+  }
+  return false;
+}
+
+static bool ban_record_remove_ip(const char *ip) {
+  enet_uint32 mask = 0;
+  const enet_uint32 host = ban_parse_ip_mask(ip, &mask);
+  if (!host) {
+    u_log(LOG_ERROR, "unban: `%s` is not a valid address", ip);
+    return NULL;
+  }
+  return ban_record_remove_addr(host, mask);
+}
+
 static void ban_load_list(const char *fname) {
   FILE *f = fopen(fname, "r");
   if (!f) {
@@ -497,13 +591,14 @@ static void ban_load_list(const char *fname) {
       continue;
 
     char ip[21] = { 0 }; // optionally includes the "/nn" prefix length at the end
-    time_t exp = 0;
+    int expd = 0;
     int count = 0;
-    if (sscanf(ln, "%20s %ld %d", ip, &exp, &count) < 3) {
+    if (sscanf(ln, "%20s %d %d", ip, &expd, &count) < 3) {
       u_log(LOG_ERROR, "banlist: malformed line: `%s`", ln);
       continue;
     }
 
+    const time_t exp = (time_t)expd; // shut up gcc
     if (ban_record_add_ip(ip, count, exp))
       u_log(LOG_NOTE, "banlist: banned %s until %s (ban level %d)", ip, u_strtime(exp), count);
   }
@@ -520,7 +615,7 @@ static void ban_save_list(const char *fname) {
 
   for (ban_record_t *rec = banlist; rec; rec = rec->next) {
     if (rec->ban_count)
-      fprintf(f, "%s/%u %ld %d\n", u_iptostr(rec->host), u_masktoprefix(rec->mask), rec->cur_ban, rec->ban_count);
+      fprintf(f, "%s/%u %d %d\n", u_iptostr(rec->host), u_masktoprefix(rec->mask), (int)rec->cur_ban, rec->ban_count);
   }
 
   fclose(f);
@@ -548,6 +643,25 @@ static bool ban_sanity_check(const server_t *srv) {
   return true;
 }
 
+static void ban_add_mask(const enet_uint32 host, const enet_uint32 mask, const char *reason) {
+  const time_t now = time(NULL);
+
+  ban_record_t *rec = ban_record_add_addr(host, mask, 0, 0);
+  if (!rec) u_fatal("OOM trying to ban %s", u_iptostr(host));
+
+  rec->cur_ban = now + ban_get_time(rec->ban_count);
+  rec->ban_count++;
+
+  u_log(LOG_NOTE, "banned %s until %s, reason: %s, ban level: %d", u_iptostr(rec->host), u_strtime(rec->cur_ban), reason, rec->ban_count);
+
+  ban_save_list(MS_BAN_FILE);
+
+  sv_remove_by_host(host, mask);
+
+  if (host == cl_last_addr)
+    cl_last_addr = 0;
+}
+
 static void ban_add(const enet_uint32 host, const char *reason) {
   const time_t now = time(NULL);
 
@@ -567,15 +681,94 @@ static void ban_add(const enet_uint32 host, const char *reason) {
     cl_last_addr = 0;
 }
 
+static void ban_remove_mask(const enet_uint32 host, const enet_uint32 mask) {
+  if (!ban_record_remove_addr(host, mask)) {
+    u_log(LOG_ERROR, "could not find %s in ban list", u_iptostr(host));
+    return;
+  }
+  u_log(LOG_NOTE, "unbanned %s", u_iptostr(host));
+  ban_save_list(MS_BAN_FILE);
+}
+
 static inline void ban_peer(ENetPeer *peer, const char *reason) {
   if (peer) {
     ban_add(peer->address.host, reason);
+    sv_clear_peer(peer);
     enet_peer_reset(peer);
   }
 }
 
 /* main */
 
+#if ENABLE_PIPE
+
+static int io_fd = -1;
+
+static bool io_install_pipe(void) {
+  const int rc = mkfifo(MS_PIPE_FILE, 0664);
+  if (rc < 0 && errno != EEXIST) {
+    u_log(LOG_ERROR, "io_install_pipe(): mkfifo(): %s", strerror(errno));
+    return false;
+  }
+
+  io_fd = open(MS_PIPE_FILE, O_RDONLY | O_NONBLOCK);
+  if (io_fd < 0) {
+    u_log(LOG_ERROR, "io_install_pipe(): open(): %s", strerror(errno));
+    remove(MS_PIPE_FILE);
+    return false;
+  }
+
+  return true;
+}
+
+static void io_uninstall_pipe(void) {
+  if (io_fd >= 0) {
+    close(io_fd);
+    io_fd = -1;
+  }
+  remove(MS_PIPE_FILE);
+}
+
+static void io_read_commands(void) {
+  if (io_fd < 0)
+    return;
+
+  char cmd[128];
+  const int cmd_len = read(io_fd, cmd, sizeof(cmd) - 1);
+  if (cmd_len < 1)
+    return;
+  cmd[cmd_len] = '\0';
+
+  if (!strncmp(cmd, "ban ", 4)) {
+    const char *ip = u_strstrip(cmd + 4); // skip "ban "
+    enet_uint32 mask = 0;
+    enet_uint32 host = ban_parse_ip_mask(ip, &mask);
+    if (!host) {
+      u_log(LOG_ERROR, "ban: `%s` is not a valid address", ip);
+      return;
+    }
+    ban_add_mask(host, mask, "banned by console");
+  } else if (!strncmp(cmd, "unban ", 6)) {
+    const char *ip = u_strstrip(cmd + 6); // skip "unban "
+    enet_uint32 mask = 0;
+    enet_uint32 host = ban_parse_ip_mask(ip, &mask);
+    if (!host) {
+      u_log(LOG_ERROR, "ban: `%s` is not a valid address", ip);
+      return;
+    }
+    ban_remove_mask(host, mask);
+  } else if (!strncmp(cmd, "reload", 6)) {
+    u_log(LOG_WARN, "reloading banlist");
+    ban_free_list();
+    ban_load_list(MS_BAN_FILE);
+  } else if (!strncmp(cmd, "die", 3)) {
+    u_log(LOG_WARN, "shutting down");
+    exit(0);
+  }
+}
+
+#endif
+
 static void deinit(void) {
   // ban_save_list(MS_BAN_FILE);
   ban_free_list();
@@ -584,17 +777,10 @@ static void deinit(void) {
     ms_host = NULL;
   }
   enet_deinitialize();
-}
-
-#ifdef SIGUSR1
-static void sigusr_handler(int signum) {
-  if (signum == SIGUSR1) {
-    u_log(LOG_WARN, "received SIGUSR1, reloading banlist");
-    ban_free_list();
-    ban_load_list(MS_BAN_FILE);
-  }
-}
+#ifdef ENABLE_PIPE
+  io_uninstall_pipe();
 #endif
+}
 
 static bool handle_msg(const enet_uint8 msgid, ENetPeer *peer) {
   server_t *sv = NULL;
@@ -638,8 +824,18 @@ static bool handle_msg(const enet_uint8 msgid, ENetPeer *peer) {
           return true;
         }
         // only then update the times
-        sv->death_time = now + ms_timeout;
+        sv->death_time = now + ms_sv_timeout;
         sv->timestamp = now;
+        // check if we're updating from a new peer
+        if (sv->peer != peer) {
+          // if there was an old one, kill it
+          if (sv->peer) {
+            sv->peer->data = NULL;
+            enet_peer_reset(sv->peer);
+          }
+          sv->peer = peer;
+          peer->data = sv;
+        }
         u_log(LOG_NOTE, "updated server #%d:", sv - servers);
         u_printsv(sv);
       } else {
@@ -665,7 +861,7 @@ static bool handle_msg(const enet_uint8 msgid, ENetPeer *peer) {
         // then add that shit
         *sv = tmpsv;
         sv->host = peer->address.host;
-        sv->death_time = now + ms_timeout;
+        sv->death_time = now + ms_sv_timeout;
         sv->timestamp = now;
         if (!ban_sanity_check(sv)) {
           sv->host = 0;
@@ -673,6 +869,8 @@ static bool handle_msg(const enet_uint8 msgid, ENetPeer *peer) {
           ban_peer(peer, "tripped sanity check");
           return true;
         }
+        sv->peer = peer;
+        peer->data = sv;
         ++num_servers;
         u_log(LOG_NOTE, "added new server #%d:", sv - servers);
         u_printsv(sv);
@@ -685,11 +883,15 @@ static bool handle_msg(const enet_uint8 msgid, ENetPeer *peer) {
         ban_peer(peer, "malformed MSG_RM");
         return true;
       }
-      sv_remove(peer->address.host, tmpsv.port);
+      sv_remove_by_addr(peer->address.host, tmpsv.port);
+      // this peer can be disconnected pretty much immediately since he has no servers left, tell him to fuck off
+      sv_clear_peer(peer);
+      enet_peer_disconnect_later(peer, 0);
       return true;
 
     case NET_MSG_LIST:
       buf_send.pos = 0;
+      buf_send.overflow = 0;
       b_write_uint8(&buf_send, NET_MSG_LIST);
 
       clientver[0] = 0;
@@ -709,6 +911,11 @@ static bool handle_msg(const enet_uint8 msgid, ENetPeer *peer) {
         return true;
       }
 
+      if (clientver[0] && !u_strisver(clientver)) {
+        ban_peer(peer, "malformed MSG_LIST clientver");
+        return true;
+      }
+
       for (int i = 0; i < max_servers; ++i) {
         if (servers[i].host)
           b_write_server(&buf_send, servers + i);
@@ -725,7 +932,11 @@ static bool handle_msg(const enet_uint8 msgid, ENetPeer *peer) {
 
       ENetPacket *p = enet_packet_create(buf_send.data, buf_send.pos, ENET_PACKET_FLAG_RELIABLE);
       enet_peer_send(peer, NET_CH_MAIN, p);
-      enet_host_flush(ms_host);
+      // enet_host_flush(ms_host);
+
+      // this peer can be disconnected pretty much immediately after receiving the server list, tell him to fuck off
+      sv_clear_peer(peer);
+      enet_peer_disconnect_later(peer, 0);
 
       u_log(LOG_NOTE, "sent server list to %s:%d (ver %s)", u_iptostr(peer->address.host), peer->address.port, clientver[0] ? clientver : "<old>");
       return true;
@@ -742,10 +953,12 @@ static void print_usage(void) {
   printf("Available options:\n");
   printf("-h     show this message and exit\n");
   printf("-p N   listen on port N (default: %d)\n", DEFAULT_PORT);
-  printf("-t N   seconds before server is removed from list (default: %d)\n", DEFAULT_TIMEOUT);
+  printf("-t N   seconds before server is removed from list (default: %d)\n", DEFAULT_SERVER_TIMEOUT);
+  printf("-c N   how long a client is allowed to hold the connection active (default: %d)\n", DEFAULT_CLIENT_TIMEOUT);
   printf("-s N   max number of servers in server list, 1-%d (default: %d)\n", MS_MAX_SERVERS, DEFAULT_MAX_SERVERS);
   printf("-d N   if N > 0, disallow more than N servers on the same IP (default: %d)\n", DEFAULT_MAX_PER_HOST);
-  printf("-f N   crappy spam filter: ban people after they send N requests in a row too fast (default: %d)\n", DEFAULT_SPAM_CAP);
+  printf("-f N   crappy spam filter: ban clients after they send N requests in a row too fast (default: %d)\n", DEFAULT_SPAM_CAP);
+  printf("-w N   how often does a client have to send packets for the filter to kick in, i.e. once every N sec (default: %d)\n", DEFAULT_SPAM_TIMEOUT);
   fflush(stdout);
 }
 
@@ -772,7 +985,7 @@ static bool parse_args(int argc, char **argv) {
   if (argc < 2)
     return true;
 
-  if (!strcmp(argv[0], "-h")) {
+  if (!strcmp(argv[1], "-h")) {
     print_usage();
     return false;
   }
@@ -780,10 +993,12 @@ static bool parse_args(int argc, char **argv) {
   for (int i = 1; i < argc; ++i) {
     const bool success =
          parse_int_arg(argc, argv, i, "-p", 1, 0xFFFF, &ms_port)
-      || parse_int_arg(argc, argv, i, "-t", 1, 0x7FFFFFFF, &ms_timeout)
+      || parse_int_arg(argc, argv, i, "-t", 1, 0x7FFFFFFF, &ms_sv_timeout)
+      || parse_int_arg(argc, argv, i, "-c", 1, 0x7FFFFFFF, &ms_cl_timeout)
       || parse_int_arg(argc, argv, i, "-s", 1, MS_MAX_SERVERS, &max_servers)
       || parse_int_arg(argc, argv, i, "-d", 0, MS_MAX_SERVERS, &max_servers_per_host)
-      || parse_int_arg(argc, argv, i, "-f", 0, 0xFFFF, &ms_spam_cap);
+      || parse_int_arg(argc, argv, i, "-f", 0, 0xFFFF, &ms_spam_cap)
+      || parse_int_arg(argc, argv, i, "-w", 1, 0x7FFFFFFF, &ms_spam_timeout);
     if (success) {
       ++i;
     } else {
@@ -796,15 +1011,15 @@ static bool parse_args(int argc, char **argv) {
 }
 
 // a stupid thing to filter sustained spam from a single IP
-static bool spam_filter(ENetPeer *peer) {
-  const time_t now = time(NULL);
+static bool spam_filter(ENetPeer *peer, const time_t now) {
   if (peer->address.host == cl_last_addr) {
     // spam === sending shit faster than once a second
-    if (now - cl_last_time < 1) {
+    if (now - cl_last_time < ms_spam_timeout) {
       if (cl_spam_cnt > 1)
         u_log(LOG_WARN, "address %s is sending packets too fast", u_iptostr(peer->address.host));
       if (++cl_spam_cnt >= ms_spam_cap) {
         ban_peer(peer, "spam");
+        cl_last_addr = 0;
         return true;
       }
     } else {
@@ -818,6 +1033,11 @@ static bool spam_filter(ENetPeer *peer) {
   return false;
 }
 
+// filter incoming UDP packets before the protocol kicks in
+static int packet_filter(ENetHost *host, ENetEvent *event) {
+  return !!ban_check(host->receivedAddress.host);
+}
+
 int main(int argc, char **argv) {
   if (enet_initialize() != 0)
     u_fatal("could not init enet");
@@ -841,71 +1061,88 @@ int main(int argc, char **argv) {
 
   atexit(deinit);
 
-#ifdef SIGUSR1
-  signal(SIGUSR1, sigusr_handler);
+#ifdef ENABLE_PIPE
+  io_install_pipe();
 #endif
 
   ENetAddress addr;
   addr.host = 0;
   addr.port = ms_port;
-  ms_host = enet_host_create(&addr, MS_MAX_CLIENTS, NET_CH_COUNT, 0, 0);
+  ms_host = enet_host_create(&addr, MS_MAX_CLIENTS, NET_CH_COUNT + 1, 0, 0);
   if (!ms_host)
     u_fatal("could not create enet host on port %d", ms_port);
 
+  ms_host->intercept = packet_filter;
+
   bool running = true;
   enet_uint8 msgid = 0;
   ENetEvent event;
   while (running) {
-    while (enet_host_service(ms_host, &event, 5000) > 0) {
-      if (!event.peer) {
-        continue; // can this even happen?
-      } else if (ban_check(event.peer->address.host)) {
-        enet_peer_reset(event.peer);
-        continue;
-      }
+    while (enet_host_service(ms_host, &event, 10) > 0) {
+      const time_t now = time(NULL);
+      const bool filtered = !event.peer || (ms_spam_cap && spam_filter(event.peer, now));
+
+      if (!filtered) {
+        switch (event.type) {
+          case ENET_EVENT_TYPE_CONNECT:
+            u_log(LOG_NOTE, "%s:%d connected", u_iptostr(event.peer->address.host), event.peer->address.port);
+            if (event.peer->channelCount != NET_CH_COUNT)
+              ban_peer(event.peer, "what is this");
+            else
+              enet_peer_timeout(event.peer, 0, 0, ms_cl_timeout * 1000);
+            break;
 
-      if (event.type != ENET_EVENT_TYPE_DISCONNECT)
-        if (spam_filter(event.peer))
-          continue;
+          case ENET_EVENT_TYPE_RECEIVE:
+            if (!event.packet || event.packet->dataLength == 0) {
+              ban_peer(event.peer, "empty packet");
+              break;
+            }
+            // set up receive buffer
+            buf_recv.pos = 0;
+            buf_recv.overflow = 0;
+            buf_recv.data = event.packet->data;
+            buf_recv.size = event.packet->dataLength;
+            // read message id and handle the message
+            msgid = b_read_uint8(&buf_recv);
+            if (!handle_msg(msgid, event.peer)) {
+              // cheeky cunt sending invalid messages
+              ban_peer(event.peer, "unknown message");
+            }
+            break;
 
-      switch (event.type) {
-        case ENET_EVENT_TYPE_CONNECT:
-          u_log(LOG_NOTE, "%s:%d connected", u_iptostr(event.peer->address.host), event.peer->address.port);
-          break;
+          case ENET_EVENT_TYPE_DISCONNECT:
 
-        case ENET_EVENT_TYPE_RECEIVE:
-          if (!event.packet || event.packet->dataLength == 0) {
-            ban_peer(event.peer, "empty packet");
+            // u_log(LOG_NOTE, "%s:%d disconnected", u_iptostr(event.peer->address.host), event.peer->address.port);
             break;
-          }
-          // set up receive buffer
-          buf_recv.pos = 0;
-          buf_recv.overflow = 0;
-          buf_recv.data = event.packet->data;
-          buf_recv.size = event.packet->dataLength;
-          // read message id and handle the message
-          msgid = b_read_uint8(&buf_recv);
-          if (!handle_msg(msgid, event.peer)) {
-            // cheeky cunt sending invalid messages
-            ban_peer(event.peer, "unknown message");
-          }
-          break;
 
-        default:
-          break;
+          default:
+            break;
+        }
+      } else if (event.peer) {
+        // u_log(LOG_WARN, "filtered event %d from %s", event.type, u_iptostr(event.peer->address.host));
+        sv_clear_peer(event.peer);
+        enet_peer_reset(event.peer);
+      }
+
+      if (event.packet) {
+        buf_recv.data = NULL;
+        enet_packet_destroy(event.packet);
       }
     }
 
     const time_t now = time(NULL);
+
+    // time out servers
     for (int i = 0; i < max_servers; ++i) {
-      if (servers[i].host) {
-        if (servers[i].death_time <= now) {
-          u_log(LOG_NOTE, "server #%d %s:%d timed out", i, u_iptostr(servers[i].host), servers[i].port);
-          servers[i].host = 0;
-          servers[i].port = 0;
-          --num_servers;
-        }
+      if (servers[i].host && servers[i].death_time <= now) {
+        u_log(LOG_NOTE, "server #%d %s:%d timed out", i, u_iptostr(servers[i].host), servers[i].port);
+        sv_remove(servers + i);
       }
     }
+
+#ifdef ENABLE_PIPE
+    // read commands from pipe
+    io_read_commands();
+#endif
   }
 }