Valid XHTML 1.0!

Rambetter's Ban System for UrT 4.1




Section 1: Deploying the Player Database and Ban System


Step A: Install Java and Other Dependencies

You have to install a JDK (Java Development Kit) in order to compile and run the player database code. Installing Java works differently for every operating system. You need Java 5 or newer. I would recommend either OpenJDK or Sun's JDK. You need to have the commands java, javac, and jar available in your command shell.

You also need to have Subversion installed; you need the svn command at your disposal.


Step B: Download Player Database Source

To get the source code and documentation for the player database:

rambetter@porky% svn checkout svn://svn.nerius.com/repos/urt-playerdb ./urt-playerdb

The most recent version of this HTML document that you are now reading is available as ./urt-playerdb/README.html .

Note that all of the standalone player database source code is distributed under a BSD-style license, which means that you can do pretty much anything you want with the source code, even sell it.


Step C: Tweak Settings Before Compiling

You should have a look at the file ./urt-playerdb/src/net/clanwtf/server/PlayerDBBanServer.java . This is the main class that launches the player database. Read the code comments near the top. They explain some important things regarding running the player database. You probably don't need to modify anything in the source code, but you may want to consider modifying any of the following constants in the source code:

DEBUG
LOG_AUTHORIZE_PLAYER_DENIED
USE_INTERNED_NAME_STRINGS
OUTGOING_BUFF_LEN
ACCEPT_DATABASE_WRITES
ACCEPT_DATABASE_QUERIES
FILL_GAPS_IN_C_BANS
COMPILE_ADDITIONAL_BANS_USING_DATABASE

The comment above each constant describes what it does. You most likely want to leave the constants unmodified.


Step D: Compiling

rambetter@porky% cd ./urt-playerdb/src/
rambetter@porky% javac `find . -name '*.java'`
rambetter@porky% jar cvf urt-playerdb.jar `find . -name '*.class'`

The file urt-playerdb.jar now contains the compiled form of the player database server. You will need to have this file handy whenever you want to run the player database or any of its helper programs.


Step E: Running

You'll want to create a directory specifically for running the player database. Move the file urt-playerdb.jar into this directory. For example:

rambetter@porky% mkdir ~/urt-playerdb-run
rambetter@porky% mv urt-playerdb.jar ~/urt-playerdb-run/
rambetter@porky% cd ~/urt-playerdb-run/

Now you need to set a password of your choice for the player database:

rambetter@porky% echo "pa55w0rd" > ./.password

Before you run the player database you need to set the CLASSPATH environment variable. For example:

rambetter@porky% CLASSPATH=`pwd`/urt-playerdb.jar
rambetter@porky% export CLASSPATH

You are now ready to start the player database:

rambetter@porky% java -Xmx256m -XX:MaxPermSize=64m net.clanwtf.server.PlayerDBBanServer ./ 10030 127.0.0.1 >> playerdb.out 2>> playerdb.err

You can change the IP address (127.0.0.1 in above example) to be any single network interface you want to listen on, or you can omit that third argument to listen on all available network interfaces. I would suggest only listening on the loopback interface for security reasons if your game servers are running on the same box.

You can omit the options -Xmx256m and -XX:MaxPermSize=64m . I've included them for reference. You only need to set them if your database is very large.

You most likely will want to start the player database as a background process; you won't want to stay logged in for the database to run in your command shell. To do this, you can either use something like the screen utility, or you can run the process as a daemon using a utility that is specific to your operating system (for example, /usr/sbin/daemon on FreeBSD).


Step F: Creating Ban Lists

The creation of ban lists is already discussed in the comments near the top of PlayerDBBanServer.java, but I will go over that prodedure again here.

Creating, deleting, or modifying a ban list is really easy and can be done anytime (even while the player database server is running). You simply have to create a file with the .banlist suffix and place that into your urt-playerdb-run/ directory.

Let's create a file cheaters.banlist with the following contents:

// This is a comment and will be ignored.
// Blank lines such as the following are ignored too.

// When you place a full IP address in the ban list, it will
// be treated as a class C ban.  For example, the following:

190.229.148.198:-1 // jorge, wallhack

// is exactly the same as:

190.229.148.*:-1 // alternate form of ban

// The two ban lines above will be treated identically.  You can
// use the star notation if you like, but it's not necessary.  The two
// examples above will ban IP addresses between 190.229.148.0 and
// 190.229.148.255, inclusive.  If COMPILE_ADDITIONAL_BANS_USING_DATABASE
// is set to true, for every player touching this IP address range,
// all of their known IP addresses will be placed in the internal
// ban list.

// To do a class B ban:

207.6.*.*:-1 // aLeK, aimbot

// The above will ban the range of IP addresses from
// 207.6.0.0 to 207.6.255.255.  Internal bans for players hitting
// these IP addresses are not computed in the case of a class B.

// The following is NOT a class B ban:

207.6.0.0:-1

// The above line is equivalent to:

207.6.0.*:-1

// and it will only ban the IPs from 207.6.0.0 to 207.6.0.255.

// I will now describe interval banning.  It only works if
// FILL_GAPS_IN_C_BANS is set to true.  Let's say we have the
// following 2 bans:

71.98.66.*:-1
71.98.71.*:-1

// These 2 Ip addresses are fairly close together.  Then,
// the IP address range 71.98.66.0 to 71.98.71.255 will be added
// to the internal ban list.

Modifications to ban list files will be automatically detected by the player database within a minute. New files and removed files will likewise be detected.


Step G: Shutting Down the Player Database

It's important to shut down the database cleanly in order to have it save player data to disc on shutdown. Do NOT use CTRL+C or the kill command to shut the database down. To shut the server down cleanly, you need to send a well-formed UDP packet to the database server that means "quit", and you need to do it from the same host that the database is running on.

An example command that will shut down the server on a FreeBSD operating system:

rambetter@porky% PLAYERDB_HOST="localhost"
rambetter@porky% PLAYERDB_PORT="10030"
rambetter@porky% PLAYERDB_PASSWORD="pa55w0rd"

rambetter@porky% REQUEST_BODY=`printf "playerDBRequest\n${PLAYERDB_PASSWORD}\nquit\n"`
rambetter@porky% echo "$REQUEST_BODY" | nc -w 1 -u "$PLAYERDB_HOST" "$PLAYERDB_PORT"

For a complete reference on all commands available to the database, see Section 3.

Note that netcat (the nc command) typically has an input buffer of size 1024. This means that incoming UDP packets larger than this size would be truncated to 1024 bytes. Therefore, I would not recommend using netcat for operations which return large packets, such as the database query operations. You should write your own custom programs (using C, PHP, Python, or Java for example) that send UDP packets.



Section 2: Modding the UrT Game Server

The source code for the UrT game server side of things is here:

rambetter@porky% svn checkout svn://svn.nerius.com/repos/ioUrT-server-4.1

You'll want to read the README-extra-patches.txt file in ./ioUrT-server-4.1/extra-patches/ . The patch you're looking for in particular is playerdb.patch .

More information about this SVN repository for the game server code can be found on this page.

The README-extra-patches.txt file already explains the new server cvars that are defined with this patch, but I will go over that in more detail here. Here is a sample server.cfg file with the new server cvars:

set sv_requireValidGuid "1" // Require that a cl_guid be present for every player, and
                            // that it be a string of length 32 consisting of the characters
                            // '0' through '9' and 'A' through 'F'.  The default for this cvar
                            // is 0, which means don't require the valid cl_guid.
                            // The database won't store player information for invalid guids.

set sv_playerDBHost "localhost:10030" // Host and port of the player database and ban system.

set sv_playerDBPassword "pa55w0rd" // Password for the player database and ban system.

set sv_playerDBUserInfo "1" // Send ClientUserinfo strings to player database every time a player changes it.
                            // By receiving this information the database builds player data.  The default value
                            // for this cvar is 0, which means don't send.

set sv_playerDBBanIDs "cheaters, asshats" // Require that all connecting IP addresses are not on these ban lists.
                                          // If this property is the empty string, all players will be allowed to
                                          // connect, and no authorizePlayer packets will be sent to the
                                          // player database server.  However, the native UrT banlist.txt banning
                                          // system will still be in place whether or not we use ban lists here.

set sv_permaBanBypass "pa55" // Bypass the ban system by typing this in your client:
                             //   \setu permabanbypass "pa55"


Section 3: Protocol Reference

This section describes the communication over network UDP that happens with the player database server.

For all of the commands below (except for largePacketTest) that respond with a single packet, if the information to be returned cannot fit in one packet (length is greater than OUTGOING_BUFF_LEN), then the string "\n<< snipped >>\n" will be tacked on to the end of the response packet.

The first command defined below describes something called a challenge. A challenge is optional on all of the commands below.


authorizePlayer

A game server will send a UDP packet like this to the player database:

@@@@playerDBRequest
pa55w0rd
authorizePlayer:0afc5e92
cheaters, asshats
99.50.206.241

@@@@ is the 4 byte quantity consisting of all 1 bits (0xffffffff).

The string "0afc5e92" in the request can actually be any 32 bit hexadecimal quantity. It must consist of exactly 8 characters, where each character is a digit or a lowercase letter between 'a' and 'f'. This field is called the challenge, and it can be present on all request types made to the server, not just authorizePlayer. It gets echoed back in the response (see example response below). Its purpose is to prevent spoofed packets from being sent to the person making the request. The challenge is optional on all of the commands (you must leave out the colon before the challenge as well if the challenge is to be omitted).

The lines of text in the request are separated by a single newline character. There must be a trailing newline after the IP address (the last line).

The playerDB responds with a UDP packet like so:

@@@@playerDBResponse "authorizePlayer:0afc5e92" "99.50.206.241" "denied"

or

@@@@playerDBResponse "authorizePlayer:0afc5e92" "99.50.206.241" "allowed"

Note how the challenge is echoed back in the response. The response is not followed by a newline character.

If one of the supplied ban lists does not exist, the request still gets processed, but that missing banlist does not prevent the player from being approved.


clientUserInfo

On ClientUserinfo change a packet is sent to the player database:

@@@@playerDBRequest
pa55w0rd
clientUserInfo
\ip\87.208.10.234:27960\name\Kagh_NL_\... << snipped >> ...\cl_guid\D8FDC4796B55536CFF0EA6EB4328A341

There is no response to this packet type. A challenge can be included in the request, but it serves no purpose since there is no response.


banCausedBy

To figure out why an IP address is on an internal ban list:

playerDBRequest
pa55w0rd
banCausedBy
cheaters
99.50.206.241

This will first determine if the supplied IP address (99.50.206.241) is on the specified ban list (cheaters). Only a single ban list can be specified. If the IP is indeed on the internal ban list, a packet such as the following will be returned:

playerDBResponse
banCausedBy
cheaters
99.50.206.241

banned
99.50.*.*:-1 // MaestroHalo
160.33.43.*:-1 // huh

If FILL_GAPS_IN_C_BANS is set to true in the server, it's possible that a player will be banned but there may not be a direct reason for that ban. The reason for the ban may be that the player falls in the interval between two close internal bans. If this is the case, a packet such as the following will be returned:

playerDBResponse
banCausedBy
cheaters
99.50.206.241

banned

If a player is not affacted by this ban list, the following packet structure is returned:

playerDBResponse
banCausedBy
cheaters
99.50.206.241

clean

If there is no ban list by the specified name, the request is silently ignored.

Note that a challenge may accompany this request type, but has been omitted in the example.


dumpBanlists

A useful way to debug this server's ban list compilation algorithms is by sending packets such as this one:

playerDBRequest
pa55w0rd
dumpBanLists

First, this will force a reload and/or pruning of ban lists if they have been modified on disk (just like the auto-load feature). Then, the internal ban list for each .banlist file is saved to the data directory; the internal ban list file is named with the original ban list name plus "-internal". So for example if you have a ban list called cheaters.banlist in your data directory, this will generate a file cheaters.banlist-internal containing all of the ban lines that were internally generated by the ban list compiler.

This request will be ignored if the request packet does not originate from the host on which the server is running. If the server is listening on the wildcard interface (all interfaces), then the packet must originate from the loopback address.

There is no response packet for this request type. Note that a challenge may accompany the request, but it serves no purpose.


queryByIP

To query all players in the database by IP address, send this:

playerDBRequest
pa55w0rd
queryByIP:64ea25a6
99.50.206.241

This would return a packet that looks like this:

playerDBResponse
queryByIP:64ea25a6
99.50.206.241

cl_guid:
    5212B71033CDDCE449A4DDD99649647E
IPs:
    99.50.206.241
    99.50.206.243
names:
    Rambetter@sam
    booby
    n00bsy
    wTf|Rambetter

cl_guid:
    0E60A7B8C6039878AA480A9E7F596A42
IPs:
    99.50.206.241
names:
    Rambetter@hugo

The lines that are indented above have the tab character at their beginning. If there are no matching players, there will be only one newline after the header in the response packet, not two.

Note that a challenge has been included in the example above, but is not required.


queryByIPShort

To get just a list of guids on a specific IP address, send this:

playerDBRequest
pa55w0rd
queryByIPShort
99.50.206.241

This would return a packet that looks like this:

playerDBResponse
queryByIPShort
99.50.206.241

5212B71033CDDCE449A4DDD99649647E
0E60A7B8C6039878AA480A9E7F596A42

The lines following the header are a list of guids. If there are no matches, there will only be one newline after the header, not two.


queryByIPRange

To query all players in the database by a range of IP addresss, send this:

playerDBRequest
pa55w0rd
queryByIPRange
99.50.206.128-99.50.208.255

This would return a packet that looks like this:

playerDBResponse
queryByIPRange
99.50.206.128-99.50.208.255

cl_guid:
    5212B71033CDDCE449A4DDD99649647E
IPs:
    99.50.206.241
    99.50.206.243
names:
    Rambetter@sam
    booby
    n00bsy
    wTf|Rambetter

cl_guid:
    C0E6F20ACE21F3AFF73B7E417D1A8560
IPs:
    99.50.207.13
names:
    Rambetter@porky

The begin and end IP address is in the range are inclusive boundaries.

Note that a challenge may accompany this request type, but has been omitted in the example.


queryByIPRangeShort

To get just a list of guids on a range of IP address, send this:

playerDBRequest
pa55w0rd
queryByIPRangeShort
99.50.206.128-99.50.208.255

This would return a packet that looks like this:

playerDBResponse
queryByIPRangeShort
99.50.206.128-99.50.208.255

5212B71033CDDCE449A4DDD99649647E
C0E6F20ACE21F3AFF73B7E417D1A8560

queryByGuid

To query a player by cl_guid, send this:

playerDBRequest
pa55w0rd
queryByGuid
5212B71033CDDCE449A4DDD99649647E

This would return a packet that looks like this:

playerDBResponse
queryByGuid
5212B71033CDDCE449A4DDD99649647E

cl_guid:
    5212B71033CDDCE449A4DDD99649647E
IPs:
    99.50.206.241
names:
    Rambetter@sam
    booby
    n00bsy
    wTf|Rambetter

Packets with invalid guids will be silently ignored. A valid cl_guid must contain 32 characters consisting of '0' through '9' and 'A' through 'F'. If no player with the given guid exists, just the headers are returned.

Note that a challenge may accompany this request type, but has been omitted in the example.


queryByNameRegexp

To query a player by name using a regular expression:

playerDBRequest
pa55w0rd
queryByNameRegexp
Rambetter

This might return the following:

playerDBResponse
queryByNameRegexp
Rambetter

cl_guid:
    5212B71033CDDCE449A4DDD99649647E
IPs:
    99.50.206.241
    99.50.206.243
names:
    Rambetter@sam
    booby
    n00bsy
    wTf|Rambetter

cl_guid:
    C0E6F20ACE21F3AFF73B7E417D1A8560
IPs:
    99.50.207.13
names:
    Rambetter@porky

As you can see, the names "Rambetter@sam", "wTf|Rambetter", and "Rambetter@porky" match the regular expression "Rambetter". To perform a case-insensitive search, precede your regular expression with "(?i)", for example "(?i)rAmBeTtEr" would match "Rambetter@sam" and the other two names listed above. To match an exact name, the expression is for example "^Rambetter\@sam$"; The '^' matches the beginning of a string, the '\' is an escape character, and the '$' matches the end of the string. This expression would match the name "Rambetter@sam" and nothing else.

Note that a challenge may accompany this request type, but has been omitted in the example.


queryByNameRegexpShort

Another way to query players by name:

playerDBRequest
pa55w0rd
queryByNameRegexpShort
Rambetter

This would return something like the following:

playerDBResponse
queryByNameRegexpShort
Rambetter

5212B71033CDDCE449A4DDD99649647E
C0E6F20ACE21F3AFF73B7E417D1A8560

queryByNameExact

In case you're too afraid to understand regular expressions, you can query the player database by exact player name:

playerDBRequest
pa55w0rd
queryByNameExact
Rambetter

This works just like the regular expression name query except that the name must match exactly.


queryByNameExactShort

Returns just the client guids that have a name matching exactly:

playerDBRequest
pa55w0rd
queryByNameExactShort
Rambetter

truncateDatabase

Your database of players may grow to be very large. To truncate the database, send a packet similar to the following:

playerDBRequest
pa55w0rd
truncateDatabase
2880

Because this is an important operation, there will be a return packet even though that packet will not contain any important information. The return packet will look like this:

playerDBResponse
truncateDatabase
2880

success

The numeric value ("2880" in this example) must be a positive integer no greater than 305760 (approximately 35 years); it specifies the number of hours of data to keep. All data older than this many hours will be discarded. All ban lists will be reloaded and recompiled.

If the database was compiled with ACCEPT_DATABASE_WRITES as false, the database truncation request will be silently ignored. Otherwise, if you do not get a return packet for this operation, it probably means that the truncate operation was not successful, and that the database server probably shut down as a result (if it was running). The only other reason why this operation would fail would be due to dropped network packets.

Before the server even attempts this operation, it does an autosave. You will have the autosave backup until the next autosave takes place (about one hour after the trucate operation). The truncated database will only reside in memory at first. It will not be persisted until either the next autosave or server shutdown.

This request will be ignored if the request packet does not originate from the host on which the server is running. If the server is listening on the wildcard interface (all interfaces), then the packet must originate from the loopback address.

Note that a challenge may accompany this request type, but has been omitted in the example.


quit

To shut the server down, send this kind of packet from the loopback or local address:

playerDBRequest
pa55w0rd
quit

This request will be ignored if the request packet does not originate from the host on which the server is running. If the server is listening on the wildcard interface (all interfaces), then the packet must originate from the loopback address. There is no response to this packet type.

Note that a challenge may accompany this request type, but it serves no purpose because there is no response.


largePacketTest

Use this to test out how large outgoing packets can be.

playerDBRequest
pa55w0rd
largePacketTest
4096

The above request will trigger a response packet to be sent that looks similar to the following:

playerDBResponse
largePacketTest
4096/9216

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa << snipped >>

The third line of output consists of the number that was input (4096 in this case), followed by the forward slash, followed by the value of OUTGOING_BUFF_LEN. The total size of the returned packet (the headers plus the 'a' characters that follow) will be the minimum of these two numbers.

Note that a challenge may accompany this request type, but has been omitted in the example.


IPMap

Now you may want to generate a Google Maps interface showing where players are located. To do this, you can push the following request to the player database server:

playerDBRequest
pa55w0rd
IPMap:7fc8b9b0
24

This would return information for players who have been active in the past 24 hours (you can change "24" to be any positive value less than or equal to 305760 specifying a number of hours, or zero to mean an infinite number of hours). The information returned looks like this:

playerDBResponse
IPMap:7fc8b9b0
24
1/2

<< binary data >>
playerDBResponse
IPMap:7fc8b9b0
24
2/2

<< binary data >>

In the example above, 2 UDP packets would be returned, not just one. The line in the response headers "1/2" means the first packet out of 2. Likewise, "2/2" means the second packet out of 2. Multiple packets would be generated when not all data fits in one packet. The binary data after the headers contains IP addresses. The binary data has a size which is a multiple of 4, and every 4 bytes makes up an IP address; the first byte of a 4-byte quantity is the most significant octet of the IP address and so on. The algorithm for determining which IP addresses make the response is as follows. First, the database finds all players (all unique guids) that have been seen in the past 24 hours. Then, for each player, one of their IP addresses is chosen, but that IP address must be one that has been seen in the past 24 hours. That IP address is put in the response. So, the response contains exactly one IP address for each player seen in the past 24 hours.

If there are no IP addresses to be returned, the last line of the headers will read "1/1" and there will only be one newline after the headers, not two.

Please note that this operation should be used sparingly. It will likely take many CPU cycles to finish.



Appendix A: Building an Initial Database from UrT Log Files

You may want to parse all of your UrT server logs to build an initial database of players. There is a tool to do this. The Java program to do this is net.clanwtf.playerdb.LogParser . This program takes an arbitrary number of arguments, where each argument is a directory in which to search for server log files. A server log file is defined as any file with the .log suffix, except for qconsole.log or botlib.log . The program reads from standard in, which should be an existing player database file. If no standard in is supplied, the program creates a new player database. The program writes to standard out a player database which is the result of combining the input with the parsed log files. Last-modified timestamps of the individual log files are used to determine internal timestamps for the player data.

I've written a shell script that makes use of the LogParser program. You can save this script as urt-playerdb-run/generate-fresh-database . Here is the shell script:

#!/bin/sh

# Arguments are directories which should be recursively searched for logs.
# For example, let's say you log files *games.log in the following directories:
#
#   ~urt1/.q3a/q3ut4/
#   ~urt1/.q3a/q3ut4/bak/2010-02-15_01:11:29_logs/
#   ~urt2/.q3a/q3ut4/
#   ~urt2/.q3a/q3ut4/bak/2009-09-25_20:07:18_logs/
#
# Then you could simply run this script like so:
#
#   generate-fresh-database ~urt1/.q3a/q3ut4/ ~urt2/.q3a/q3ut4/
#
# which would create a file playerdb.bin-fresh.  You would want to
# rename this to playerdb.bin to start using it before you start the
# player database server.

set -e

THIS_DIR=`pwd`
DIRNAME=`dirname "$0"`
cd "$DIRNAME"
SCRIPTDIR=`pwd`
CLASSPATH="${SCRIPTDIR}/urt-playerdb.jar"
export CLASSPATH
cd "$THIS_DIR"
java net.clanwtf.playerdb.LogParser > "${SCRIPTDIR}/tempdb.bin"
for LOGSDIR in "$@"; do
  echo "Recursively finding log files in ${LOGSDIR}."
  cat "${SCRIPTDIR}/tempdb.bin" | java -Xmx256m net.clanwtf.playerdb.LogParser \
    `find "${LOGSDIR}" -type d` > "${SCRIPTDIR}/tempdb.bin-new"
  mv "${SCRIPTDIR}/tempdb.bin-new" "${SCRIPTDIR}/tempdb.bin"
done
mv "${SCRIPTDIR}/tempdb.bin" "${SCRIPTDIR}/playerdb.bin-fresh"