next up previous contents
Next: Performance Issues Up: Using rq2proxy Previous: An example configuration file

How it works

The server, rq2ps, contains most of the intelligence: the client is more or less a dumb proxy. The server keeps track of temporary associations between Quake II client machine,port pairs, file descriptors, rq2ps port numbers and Quake II server machine, port pairs via. a pair of routing hash-tables.

On startup, the server reads its configuration file, including the options set on the command line. If not running from inetd, it then opens a server socket and listens for connections. If running from inetd, this chapter is skipped, and a synthetic accepted connection is synthesised from fd 0 (which is how inetd passes us the socket we're connected to) (server.c).

The server is responsible for resolving names into IP addresses.

When a client connects, the first thing it does is to send the cookie (in clear). It then reads the global access control list and routing tables from the server (ft.c).

The client then discards any routing entries which don't pertain to it (ie. which don't have one of the IP numbers associated with its primary name or the IP number the packet came in on as their on parameter (or redirect-from IP if they don't have an on parameter)), and builds a file descriptor for each port it's required to proxy, putting them in a hash table keyed by (client name, port) and back-annotates the routing table to contain fds ( client.c).

Both sides now go into proxying mode. Each does a select() on all relevant fds, then loops over them, proxying for each that is ready for reading, exchanging encapsulated UDP packets.

When the client recieves a packet from a client, it does a linear search of the global ACL, then hashes the incoming file descriptor to find the routing table entry it needs to get [clnt ip] and [clnt port] (yes, this is necessary - the target address of the packet might not be the same IP address as specified in the routing entry). The client sends encapsulations of the form:

[length] [orig ip] [clnt ip] [clnt port] [orig port] [data]

All the above (except for [data]) are 4-byte words, and [length] is in bytes, counting everything except itself (so the actual length of the data packet is [length]-16).

The server looks up a route in its routing table, using the quadruple (orig ip, orig port, clnt ip, clnt port) as a key. If it doesn't find an existing route, it creates one, opening a file descriptor to send UDP packets to the Quake II server (it uses linear search through the routing table to find out who to proxy to) (server.c, route.c). It then attaches the packet to the incoming packet queue and gives it a time to send.

When the server finds a packet to proxy back to the client, it looks up a routing entry using the triple (QII server ip, QII server port, file descriptor) as a hash key into a second table. It then sends an encapsulation of the same type (including [orig ip] and [orig port], just as the client sent them: it knows them because it squirreled them away in its routing table) (server.c, route.h, route.c). It then stores this packet (ready-encapsulated) in the outgoing packet queue, with the appropriate time-to-send.

Each server route has one incoming and one outgoing packet buffer. If these buffers are already full when a packet comes in (or out), the convention is to throw away the old packet (you can change this by defining
INDUCED_LATENCY_DROP_POLICY to 0 in config.h.in).

At various times in the loop, if a packet on the input or output queue is ready to send, it is sent. This is a cheap test because the packet queues are held as pointer arrays sorted by increasing time to send. At the end of the loop over select()d fds, any remaining due packets are cleared to prevent them building up in the packet queue and causing confusion when they are eventually cleared.

When the client recieves a packet from rq2ps, it does a linear search of the relevant routing table entries to find who it should send the packet to (the routing table having been back-annotated with the appropriate fds earlier).

If the server detects that a connection hasn't been used for 10s ( the value of PRUNE_AFTER in config.h.in), either with no packet sent from either direction if DROP_CONN_ASYMMETRIC is 0, or with one side having not sent a packet in PRUNE_AFTERs (though the other side may have) if DROP_CONN_ASYMMETRIC is 1, the server shuts down the connection, returns the packet buffers and per-route data to a free pool for re-use and closes the file descriptor.

DROP_CONN_ASYMMETRIC should always be set to 1. This is because some servers refuse to acknowledge the destruction of a client, and carry on sending data until the port they're talking to closes. If DROP_CONN_ASYMMETRIC is 0, the port will never be closed and this situation will continue indefinitely.


next up previous contents
Next: Performance Issues Up: Using rq2proxy Previous: An example configuration file
Richard Watts
1998-12-22