X-Git-Url: http://deadsoftware.ru/gitweb?a=blobdiff_plain;f=src%2Fmastersrv%2Fmaster.c;h=c68d59c1042f9b090632971b5e696648df28ec3e;hb=5a9d04dfb16b32c84964c0940031606e7454259d;hp=2ea5016bbf538ba65ee5da1e0f7679c5610894bf;hpb=97fa3221376e9119da5afaf7336ee7fadf91ca0e;p=d2df-sdl.git diff --git a/src/mastersrv/master.c b/src/mastersrv/master.c index 2ea5016..c68d59c 100644 --- a/src/mastersrv/master.c +++ b/src/mastersrv/master.c @@ -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 . + */ + #include #include #include @@ -6,7 +21,11 @@ #include #include #include +#include +#include #include +#include +#include #include #include @@ -17,12 +36,14 @@ #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,57 @@ 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); + if (enet_address_set_host_ip(&addr, ip_copy) != 0) + return 0; + + // transform prefix length into mask + *out_mask = u_prefixtomask(prefix); + + 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); +} - // transform prefix length into mask - const enet_uint32 mask = u_prefixtomask(prefix); +static void ban_free_list(void) { + ban_record_t *rec = banlist; + while (rec) { + ban_record_t *next = rec->next; + free(rec); + rec = next; + } + banlist = NULL; +} - return ban_record_add_addr(addr.host, mask, cnt, cur); +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) { @@ -487,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); } @@ -510,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); @@ -538,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); @@ -557,22 +681,105 @@ 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(); if (ms_host) { enet_host_destroy(ms_host); ms_host = NULL; } enet_deinitialize(); +#ifdef ENABLE_PIPE + io_uninstall_pipe(); +#endif } static bool handle_msg(const enet_uint8 msgid, ENetPeer *peer) { @@ -617,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 { @@ -644,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; @@ -652,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); @@ -664,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; @@ -688,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); @@ -704,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 : ""); return true; @@ -721,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); } @@ -751,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; } @@ -759,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 { @@ -775,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 { @@ -797,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"); @@ -820,67 +1061,88 @@ int main(int argc, char **argv) { atexit(deinit); +#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 } }