/** * @brief Handles a connectionless message from a client * @sa NET_OOB_Printf * @param[out] stream The stream to write to * @param msg The message buffer to read the connectionless data from */ static void SV_ConnectionlessPacket (struct net_stream *stream, dbuffer *msg) { const char *c; char s[512]; char buf[256]; NET_ReadStringLine(msg, s, sizeof(s)); Cmd_TokenizeString(s, false); c = Cmd_Argv(0); Com_DPrintf(DEBUG_SERVER, "Packet : %s\n", c); if (Q_streq(c, "teaminfo")) SVC_TeamInfo(stream); else if (Q_streq(c, "info")) SVC_Info(stream); else if (Q_streq(c, "status")) SVC_Status(stream); else if (Q_streq(c, "connect")) SVC_DirectConnect(stream); else if (Q_streq(c, "rcon")) SVC_RemoteCommand(stream); else Com_Printf("Bad connectionless packet from %s:\n%s\n", NET_StreamPeerToName(stream, buf, sizeof(buf), true), s); }
/** * @brief A client issued an rcon command. Shift down the remaining args. Redirect all printfs */ static void SVC_RemoteCommand (struct net_stream *stream) { char buf[256]; const char *peername = NET_StreamPeerToName(stream, buf, sizeof(buf), false); bool valid = Rcon_Validate(Cmd_Argv(1)); if (!valid) Com_Printf("Bad rcon from %s:\n%s\n", peername, Cmd_Argv(1)); else Com_Printf("Rcon from %s:\n%s\n", peername, Cmd_Argv(1)); Com_BeginRedirect(stream, sv_outputbuf, SV_OUTPUTBUF_LENGTH); if (!valid) /* inform the client */ Com_Printf("Bad rcon_password.\n"); else { char remaining[1024] = ""; int i; /* execute the rcon commands */ for (i = 2; i < Cmd_Argc(); i++) { Q_strcat(remaining, Cmd_Argv(i), sizeof(remaining)); Q_strcat(remaining, " ", sizeof(remaining)); } /* execute the string */ Cmd_ExecuteString(remaining); } Com_EndRedirect(); }
/** * @brief Handles a connectionless message from a client * @sa NET_OOB_Printf * @param[out] stream The stream to write to * @param msg The message buffer to read the connectionless data from */ static void SV_ConnectionlessPacket (struct net_stream* stream, dbuffer* msg) { char s[512]; NET_ReadStringLine(msg, s, sizeof(s)); Cmd_TokenizeString(s, false, false); const char* c = Cmd_Argv(0); Com_DPrintf(DEBUG_SERVER, "Packet : %s\n", c); if (Q_streq(c, SV_CMD_TEAMINFO)) { SVC_TeamInfo(stream); } else if (Q_streq(c, SV_CMD_INFO)) { SVC_Info(stream); } else if (Q_streq(c, SV_CMD_STATUS)) { SVC_Status(stream); } else if (Q_streq(c, SV_CMD_CONNECT)) { SVC_DirectConnect(stream); } else if (Q_streq(c, SV_CMD_RCON)) { SVC_RemoteCommand(stream); } else { char buf[256]; Com_Printf("Bad connectionless packet from %s:\n%s\n", NET_StreamPeerToName(stream, buf, sizeof(buf), true), s); } }
/** * @brief Find or allocate a bucket for an address */ static leakyBucket_t* SVC_BucketForAddress (struct net_stream& address, int burst, int period) { char node[64]; NET_StreamPeerToName(&address, node, sizeof(node), false); const long hash = Com_HashKey(node, MAX_HASHES); const int now = Sys_Milliseconds(); for (leakyBucket_t* bucket = bucketHashes[hash]; bucket; bucket = bucket->next) { if (!Q_streq(bucket->node, node)) continue; return bucket; } for (int i = 0; i < MAX_BUCKETS; i++) { leakyBucket_t* bucket = &buckets[i]; const int interval = now - bucket->lastTime; /* Reclaim expired buckets */ if (bucket->lastTime > 0 && (interval > (burst * period) || interval < 0)) { if (bucket->prev != nullptr) { bucket->prev->next = bucket->next; } else { bucketHashes[bucket->hash] = bucket->next; } if (bucket->next != nullptr) { bucket->next->prev = bucket->prev; } OBJZERO(*bucket); } if (Q_strnull(bucket->node)) { Q_strncpyz(bucket->node, node, sizeof(bucket->node)); bucket->lastTime = now; bucket->burst = 0; bucket->hash = hash; /* Add to the head of the relevant hash chain */ bucket->next = bucketHashes[hash]; if (bucketHashes[hash] != nullptr) { bucketHashes[hash]->prev = bucket; } bucket->prev = nullptr; bucketHashes[hash] = bucket; return bucket; } } /* Couldn't allocate a bucket for this address */ return nullptr; }
static void NET_ShowStreams_f (void) { char buf[256]; int cnt = 0; for (int i = 0; i < MAX_STREAMS; i++) { if (streams[i] == nullptr) continue; Com_Printf("Steam %i is opened: %s on socket %i (closed: %i, finished: %i, outbound: " UFO_SIZE_T ", inbound: " UFO_SIZE_T ")\n", i, NET_StreamPeerToName(streams[i], buf, sizeof(buf), true), streams[i]->socket, streams[i]->closed, streams[i]->finished, dbuffer_len(streams[i]->outbound), dbuffer_len(streams[i]->inbound)); cnt++; } Com_Printf("%i/%i streams opened\n", cnt, MAX_STREAMS); }
/** * @brief A client issued an rcon command. Shift down the remaining args. Redirect all printfs */ static void SVC_RemoteCommand (struct net_stream* stream) { char buf[64]; const char* peername = NET_StreamPeerToName(stream, buf, sizeof(buf), false); /* Prevent using rcon as an amplifier and make dictionary attacks impractical */ if (SVC_RateLimitAddress(*stream)) { Com_DPrintf(DEBUG_SERVER, "SVC_RemoteCommand: rate limit from %s exceeded, dropping request\n", peername); return; } const bool valid = Rcon_Validate(Cmd_Argv(1)); if (!valid) { static leakyBucket_t bucket; /* Make DoS via rcon impractical */ if (SVC_RateLimit(&bucket, 10, 1000)) { Com_DPrintf(DEBUG_SERVER, "SVC_RemoteCommand: rate limit exceeded, dropping request\n"); return; } Com_Printf("Bad rcon from %s with password: '******'\n", peername, Cmd_Argv(1)); } else { Com_Printf("Rcon from %s\n", peername); } static char sv_outputbuf[1024]; Com_BeginRedirect(stream, sv_outputbuf, sizeof(sv_outputbuf)); if (!valid) { /* inform the client */ Com_Printf(BAD_RCON_PASSWORD); } else { char remaining[1024] = ""; int i; /* execute the rcon commands */ for (i = 2; i < Cmd_Argc(); i++) { Q_strcat(remaining, sizeof(remaining), "%s ", Cmd_Argv(i)); } /* execute the string */ Cmd_ExecuteString("%s", remaining); } Com_EndRedirect(); }
/** * @brief Returns the numerical representation of a @c net_stream * @note Not thread safe! */ const char* NET_StreamToString (struct net_stream* s) { static char node[64]; NET_StreamPeerToName(s, node, sizeof(node), false); return node; }
/** * @sa Qcommon_Frame */ void NET_Wait (int timeout) { struct timeval tv; int ready; int i; fd_set read_fds_out; fd_set write_fds_out; memcpy(&read_fds_out, &read_fds, sizeof(read_fds_out)); memcpy(&write_fds_out, &write_fds, sizeof(write_fds_out)); /* select() won't notice that loopback streams are ready, so we'll * eliminate the delay directly */ if (loopback_ready) timeout = 0; tv.tv_sec = timeout / 1000; tv.tv_usec = 1000 * (timeout % 1000); #ifdef _WIN32 if (read_fds_out.fd_count == 0) { Sys_Sleep(timeout); ready = 0; } else #endif ready = select(maxfd, &read_fds_out, &write_fds_out, nullptr, &tv); if (ready == -1) { Com_Printf("select failed: %s\n", netStringError(netError)); return; } if (ready == 0 && !loopback_ready) return; if (server_socket != INVALID_SOCKET && FD_ISSET(server_socket, &read_fds_out)) { const SOCKET client_socket = accept(server_socket, nullptr, 0); if (client_socket == INVALID_SOCKET) { if (errno != EAGAIN) Com_Printf("accept on socket %d failed: %s\n", server_socket, netStringError(netError)); } else do_accept(client_socket); } for (i = 0; i < MAX_STREAMS; i++) { struct net_stream* s = streams[i]; if (!s) continue; if (s->loopback) { /* If the peer is gone and the buffer is empty, close the stream */ if (!s->loopback_peer && NET_StreamGetLength(s) == 0) { NET_StreamClose(s); } /* Note that s is potentially invalid after the callback returns - we'll close dead streams on the next pass */ else if (s->ready && s->func) { s->func(s); } continue; } if (s->socket == INVALID_SOCKET) continue; if (FD_ISSET(s->socket, &write_fds_out)) { char buf[4096]; int len; if (dbuffer_len(s->outbound) == 0) { FD_CLR(s->socket, &write_fds); /* Finished streams are closed when their outbound queues empty */ if (s->finished) NET_StreamClose(s); continue; } { const ScopedMutex scopedMutex(netMutex); len = s->outbound->get(buf, sizeof(buf)); len = send(s->socket, buf, len, 0); s->outbound->remove(len); } if (len < 0) { Com_Printf("write on socket %d failed: %s\n", s->socket, netStringError(netError)); NET_StreamClose(s); continue; } Com_DPrintf(DEBUG_SERVER, "wrote %d bytes to stream %d (%s)\n", len, i, NET_StreamPeerToName(s, buf, sizeof(buf), true)); } if (FD_ISSET(s->socket, &read_fds_out)) { char buf[4096]; const int len = recv(s->socket, buf, sizeof(buf), 0); if (len <= 0) { if (len == -1) Com_Printf("read on socket %d failed: %s\n", s->socket, netStringError(netError)); NET_StreamClose(s); continue; } else { if (s->inbound) { SDL_LockMutex(netMutex); s->inbound->add(buf, len); SDL_UnlockMutex(netMutex); Com_DPrintf(DEBUG_SERVER, "read %d bytes from stream %d (%s)\n", len, i, NET_StreamPeerToName(s, buf, sizeof(buf), true)); /* Note that s is potentially invalid after the callback returns */ if (s->func) s->func(s); continue; } } } } for (i = 0; i < MAX_DATAGRAM_SOCKETS; i++) { struct datagram_socket* s = datagram_sockets[i]; if (!s) continue; if (FD_ISSET(s->socket, &write_fds_out)) { if (s->queue) { struct datagram* dgram = s->queue; const int len = sendto(s->socket, dgram->msg, dgram->len, 0, (struct sockaddr* )dgram->addr, s->addrlen); if (len == -1) Com_Printf("sendto on socket %d failed: %s\n", s->socket, netStringError(netError)); /* Regardless of whether it worked, we don't retry datagrams */ s->queue = dgram->next; Mem_Free(dgram->msg); Mem_Free(dgram->addr); Mem_Free(dgram); if (!s->queue) s->queue_tail = &s->queue; } else { FD_CLR(s->socket, &write_fds); } } if (FD_ISSET(s->socket, &read_fds_out)) { char buf[256]; char addrbuf[256]; socklen_t addrlen = sizeof(addrbuf); const int len = recvfrom(s->socket, buf, sizeof(buf), 0, (struct sockaddr* )addrbuf, &addrlen); if (len == -1) Com_Printf("recvfrom on socket %d failed: %s\n", s->socket, netStringError(netError)); else s->func(s, buf, len, (struct sockaddr* )addrbuf); } } loopback_ready = false; }
/** * @brief A connection request that did not come from the master * @sa CL_ConnectionlessPacket */ static void SVC_DirectConnect (struct net_stream *stream) { char userinfo[MAX_INFO_STRING]; client_t *cl; player_t *player; int playernum; int version; bool connected; char buf[256]; const char *peername = NET_StreamPeerToName(stream, buf, sizeof(buf), false); Com_DPrintf(DEBUG_SERVER, "SVC_DirectConnect()\n"); if (sv->started || sv->spawned) { Com_Printf("rejected connect because match is already running\n"); NET_OOB_Printf(stream, "print\nGame has started already.\n"); return; } version = atoi(Cmd_Argv(1)); if (version != PROTOCOL_VERSION) { Com_Printf("rejected connect from version %i - %s\n", version, peername); NET_OOB_Printf(stream, "print\nServer is version %s.\n", UFO_VERSION); return; } Q_strncpyz(userinfo, Cmd_Argv(2), sizeof(userinfo)); if (userinfo[0] == '\0') { /* catch empty userinfo */ Com_Printf("Empty userinfo from %s\n", peername); NET_OOB_Printf(stream, "print\nConnection refused.\n"); return; } if (strchr(userinfo, '\xFF')) { /* catch end of message in string exploit */ Com_Printf("Illegal userinfo contained xFF from %s\n", peername); NET_OOB_Printf(stream, "print\nConnection refused.\n"); return; } if (strlen(Info_ValueForKey(userinfo, "ip"))) { /* catch spoofed ips */ Com_Printf("Illegal userinfo contained ip from %s\n", peername); NET_OOB_Printf(stream, "print\nConnection refused.\n"); return; } /* force the IP key/value pair so the game can filter based on ip */ Info_SetValueForKey(userinfo, sizeof(userinfo), "ip", peername); /* find a client slot */ cl = NULL; while ((cl = SV_GetNextClient(cl)) != NULL) if (cl->state == cs_free) break; if (cl == NULL) { NET_OOB_Printf(stream, "print\nServer is full.\n"); Com_Printf("Rejected a connection - server is full.\n"); return; } /* build a new connection - accept the new client * this is the only place a client_t is ever initialized */ OBJZERO(*cl); playernum = cl - SV_GetClient(0); player = PLAYER_NUM(playernum); cl->player = player; cl->player->num = playernum; { const ScopedMutex scopedMutex(svs.serverMutex); connected = svs.ge->ClientConnect(player, userinfo, sizeof(userinfo)); } /* get the game a chance to reject this connection or modify the userinfo */ if (!connected) { const char *rejmsg = Info_ValueForKey(userinfo, "rejmsg"); if (rejmsg[0] != '\0') { NET_OOB_Printf(stream, "print\n%s\nConnection refused.\n", rejmsg); Com_Printf("Game rejected a connection from %s. Reason: %s\n", peername, rejmsg); } else { NET_OOB_Printf(stream, "print\nConnection refused.\n"); Com_Printf("Game rejected a connection from %s.\n", peername); } return; } /* new player */ cl->player->inuse = true; cl->lastmessage = svs.realtime; /* parse some info from the info strings */ strncpy(cl->userinfo, userinfo, sizeof(cl->userinfo) - 1); SV_UserinfoChanged(cl); /* send the connect packet to the client */ if (sv_http_downloadserver->string[0]) NET_OOB_Printf(stream, "client_connect dlserver=%s", sv_http_downloadserver->string); else NET_OOB_Printf(stream, "client_connect"); SV_SetClientState(cl, cs_connected); Q_strncpyz(cl->peername, peername, sizeof(cl->peername)); cl->stream = stream; NET_StreamSetData(stream, cl); }