/*
Copyright (C) 2024 'Spoike'.

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; either version 2
of the License, or (at your option) any later version.

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 included (GNU.txt) 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, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/

/*
we have two types of relay here.
1) TURN - blind(ish) forwarding.
	standard protocol for use as ICE/WebRTC relays.
	see TURN_IS_NOT_BLIND
2) qwfwd - compatible with qqshka's fork of the original qwfwd.
	relay incercepts `prx` userinfo in the connect requests to forward to the real peer.
	unlike qqshka's version we relay the next hop's extension handshakes instead of doing them on the proxy.
	we can also relay 'dtlsconnect' requests.
	for use with `connectbr` and compat with things that expect it.
*/

#include "qtv.h"
#include <string.h>
#include <time.h>
#include <zlib.h>

#include "bsd_string.h"

#define QWFWDTIMEOUT	(30*1000)	//how long the relay will stay open for when doing qw-specific forwarding.
#define TURN_IS_NOT_BLIND			//filters what's allowed through - we must have seen a stun binding request/response from the peer before we allow non-binding traffic through. This should help protect the owner's lan a bit.
#if defined(_DEBUG) && !defined(LIBQTV)	//unsafe stuff I'm leaving enabled in debug builds cos debugging is painful when they're protected.
	#define ALLOWPRIVATE			//allow relaying to private lan addresses, so I can test with hosting on localhost
#endif

#define QRY_SERVERLISTINTERVAL	60*1000		//interval between asking for server listings.
#define QRY_REPINGINTERVAL		60*1000		//don't ever spam any server faster than this.
#define QRY_PINGINTERVAL		100			//time between outgoing pings
#define QRY_TIMEOUT				20*60*1000	//servers will be removed if not seen in any server listings for this long.


static void Fwd_DoPing(cluster_t *cluster);
#define countof(x) (sizeof(x)/sizeof((x)[0]))

struct turnclient_s
{	//this is the end that opened the connection
	struct turnclient_s *next;

	SOCKET remotesock;	//the udp socket we're sending to
	struct {
		unsigned int timeout;	//reset to 5 mins on refresh.
		netadr_t remoteaddr;
#ifdef TURN_IS_NOT_BLIND
		qboolean seenstun;		//this is for ICE support. so we can impose a rule that the peer MUST have sent a stun packet before we allow the client to send any non-stun packets. this reduces the chance of being abused for attacks. can potentially still ddos with stun, but shouldn't be an amplification at least.
#endif
	} remotes[8];
	int udpsockid;		//-1 for private tcp
	SOCKET clientsock;	//stoopid tcp clients...
	netadr_t relayaddr;	//yay for udp...
	netadr_t clientaddr;	//yay for udp...
	unsigned int timeout;
	char *username;		//username they authed with. may not switch users once established.
	char *auth;			//auth token generated by the master (recomputed).

	unsigned char key[64];
	unsigned int keysize;

	qboolean isfwd;
};

typedef struct
{
	unsigned short msgtype;
	unsigned short msglen;
	unsigned int magiccookie;
	unsigned int transactid[3];
} stunhdr_t;
//class
#define STUN_REQUEST	0x0000
#define STUN_REPLY		0x0100
#define STUN_ERROR		0x0110
#define STUN_INDICATION	0x0010
//request
#define STUN_BINDING	0x0001
#define STUN_ALLOCATE	0x0003 //TURN
#define STUN_REFRESH	0x0004 //TURN
#define STUN_SEND		0x0006 //TURN	send indications contain data to forward
#define STUN_DATA		0x0007 //TURN	data indications contain reply data.
#define STUN_CREATEPERM	0x0008 //TURN
#define STUN_CHANBIND	0x0009 //TURN

//misc stuff...
#define STUN_MAGIC_COOKIE	0x2112a442

//attributes
#define STUNATTR_MAPPED_ADDRESS			0x0001
//#define STUNATTR_RESPONSE_ADDRESS		0x0002
//#define STUNATTR_CHANGE_REQUEST		0x0003
//#define STUNATTR_SOURCE_ADDRESS		0x0004
//#define STUNATTR_CHANGED_ADDRESS		0x0005
#define STUNATTR_USERNAME				0x0006
//#define STUNATTR_PASSWORD				0x0007
#define STUNATTR_MSGINTEGRITIY_SHA1		0x0008
#define STUNATTR_ERROR_CODE				0x0009
//#define STUNATTR_UNKNOWN_ATTRIBUTES	0x000a
//#define STUNATTR_REFLECTED_FROM		0x000b
//#define STUNATTR_CHANNELNUMBER		0x000c	//TURN
#define STUNATTR_LIFETIME				0x000d	//TURN
//#define STUNATTR_						0x000e
//#define STUNATTR_						0x000f
//#define STUNATTR_						0x0010
//#define STUNATTR_						0x0011
#define STUNATTR_XOR_PEER_ADDRESS		0x0012	//TURN
#define STUNATTR_DATA					0x0013	//TURN
#define STUNATTR_REALM					0x0014	//TURN
#define STUNATTR_NONCE					0x0015	//TURN
#define STUNATTR_XOR_RELAYED_ADDRESS	0x0016	//TURN
#define STUNATTR_REQUESTED_ADDRFAM		0x0017	//TURN
//#define STUNATTR_EVEN_PORT			0x0018	//TURN
#define STUNATTR_REQUESTED_TRANSPORT	0x0019	//TURN
#define STUNATTR_DONT_FRAGMENT			0x001a	//TURN
#define STUNATTR_MSGINTEGRITIY_SHA2_256	0x001c
#define STUNATTR_XOR_MAPPED_ADDRESS		0x0020
//#define STUNATTR_ICE_PRIORITY			0x0024	//ICE
//#define STUNATTR_ICE_USE_CANDIDATE	0x0025	//ICE

//0x8000 attributes are optional, and may be silently ignored without issue.
#define STUNATTR_ADDITIONAL_ADDRFAM		0x8000	//TURN -- listen for ipv6 in addition to ipv4
#define STUNATTR_SOFTWARE				0x8022	//TURN
#define STUNATTR_FINGERPRINT			0x8028
//#define STUNATTR_ICE_CONTROLLED		0x8029	//ICE
//#define STUNATTR_ICE_CONTROLLING		0x802A	//ICE

static void TURN_Send(cluster_t *cluster, netmsg_t *m, netadr_t *clientadr)
{
	netadr_t sockaddr;
	SOCKET sock = NET_ChooseSocket(cluster->qwdsocket, &sockaddr, *clientadr);
	unsigned char *bytes = m->data;
	//update the size.
	bytes[2] = (m->cursize-20)>>8;
	bytes[3] = (m->cursize-20)&0xff;
	//send it.
	NET_SendPacket(cluster, sock, m->cursize, bytes, sockaddr);
}
static int TURN_FindPermission(cluster_t *cluster, struct turnclient_s *t, netadr_t *peeraddr)
{	//we only allow packets to/from authorised peers.
	int i;
	for (i = 0; i < countof(t->remotes); i++)
	{
		if ((signed int)(t->remotes[i].timeout-cluster->curtime) < 0)
			continue;	//look for a live one...
		if (Net_CompareAddress(&t->remotes[i].remoteaddr,peeraddr, 0,1))
			return i;
	}
	return -1;
}
static qboolean TURN_Permissable(netadr_t *adr)
{	//we only allow packets to/from authorised peers. we don't worry about inbound ports to give symetric-nat peers a chance.
	if (((struct sockaddr *)adr->sockaddr)->sa_family == AF_INET)
	{
		struct sockaddr_in *a = (struct sockaddr_in *)&adr->sockaddr;
		unsigned int ip = a->sin_addr.s_addr;
		if ((ip&BigLong(0xffff0000)) == BigLong(0xA9FE0000))	//169.254.x.x/16
			return false;//link-local
		else if ((ip&BigLong(0xff000000)) == BigLong(0x0a000000))	//10.x.x.x/8
			return false;//private
		else if ((ip&BigLong(0xff000000)) == BigLong(0x7f000000))	//127.x.x.x/8
			return false;//localhost
		else if ((ip&BigLong(0xfff00000)) == BigLong(0xac100000))	//172.16.x.x/12
			return false;//private
		else if ((ip&BigLong(0xffff0000)) == BigLong(0xc0a80000))	//192.168.x.x/16
			return false;//private
//		else if ((ip&BigLong(0xffc00000)) == BigLong(0x64400000))	//100.64.x.x/10
//			return false;//CGNAT
		else if (ip == BigLong(0x00000000))	//0.0.0.0/32
			return false;//inaddr_any
	}
	if (((struct sockaddr *)adr->sockaddr)->sa_family == AF_INET6)
	{
		struct sockaddr_in6 *a = (struct sockaddr_in6 *)&adr->sockaddr;
		unsigned char *ip6 = a->sin6_addr.s6_addr;
		if ((*(int*)ip6&BigLong(0xffc00000)) == BigLong(0xfe800000))	//fe80::/10
			return false;//link-local
		else if ((*(int*)ip6&BigLong(0xfe000000)) == BigLong(0xfc00000))	//fc::/7
			return false;//ULA/private
		else if (*(int*)ip6 == BigLong(0x20010000)) //2001::/32
			return false;//toredo
		else if ((*(int*)ip6&BigLong(0xffff0000)) == BigLong(0x20020000)) //2002::/16
			return false;//6to4
		else if (memcmp(ip6, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1", 16) == 0)	//::1
			return false;//localhost
		else if (memcmp(ip6, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 16) == 0)	//::
			return false;//inaddr_any
		else if (memcmp(ip6, "\0\0\0\0\0\0\0\0\0\0\xff\xff", 12) == 0)	//::ffff:x.y.z.w
			return false;	//no bypassing ipv4 checks
	}
	return true;
}
#ifdef ALLOWPRIVATE
static qboolean TURN_PermissableIgnore(netadr_t *adr)
{
	if (TURN_Permissable(adr))
		return true;

	//its private... but we're allowing it anyway - with warning
	fprintf(stderr, "WARNING: Allowing relay to access private address\n");
	return true;
}
#define TURN_Permissable TURN_PermissableIgnore
#endif

void TURN_AddFDs(cluster_t *cluster, fd_set *set, int *m)
{
	struct turnclient_s *t;
	for (t = cluster->turns; t; t = t->next)
	{
		if (t->remotesock < FD_SETSIZE)
		{
			FD_SET(t->remotesock, set);
			if (t->remotesock >= *m)
				*m = t->remotesock+1;
		}
	}
}
static void TURN_AddXorAddress(netmsg_t *m, int atrtype, netadr_t *adr)
{
	unsigned short xoredport;
	unsigned int xoredaddr;
	unsigned char *xor = ((unsigned char*)m->data+4);
	if (((struct sockaddr *)adr->sockaddr)->sa_family == AF_INET)
	{
		struct sockaddr_in *out = (struct sockaddr_in *)&adr->sockaddr;
		WriteBigShort(m, atrtype);
		WriteBigShort(m, 4+4);
		WriteBigShort(m, 1);	//ipv4
		xoredport = out->sin_port ^ *(unsigned short*)xor; WriteData(m, &xoredport, sizeof(xoredport));
		xoredaddr = out->sin_addr.s_addr ^ *(unsigned int*)xor; WriteData(m, &xoredaddr, sizeof(xoredaddr));
	}
	if (((struct sockaddr *)adr->sockaddr)->sa_family == AF_INET6)
	{
		struct sockaddr_in6 *out = (struct sockaddr_in6 *)&adr->sockaddr;
		if (((unsigned short*)&out->sin6_addr)[0] == 0 &&
			((unsigned short*)&out->sin6_addr)[1] == 0 &&
			((unsigned short*)&out->sin6_addr)[2] == 0 &&
			((unsigned short*)&out->sin6_addr)[3] == 0 &&
			((unsigned short*)&out->sin6_addr)[4] == 0 &&
			((unsigned short*)&out->sin6_addr)[5] == 0xffff)
		{	//we're listening on a hybrid socket, so if we get an ipv4 packet reply with a proper ipv4 address.
			WriteBigShort(m, atrtype);
			WriteBigShort(m, 4+4);
			WriteBigShort(m, 1);	//ipv4
			xoredport = out->sin6_port ^ *(unsigned short*)xor; WriteData(m, &xoredport, sizeof(xoredport));
			xoredaddr = ((unsigned int*)&out->sin6_addr)[3] ^ *(unsigned int*)xor; WriteData(m, &xoredaddr, sizeof(xoredaddr));
		}
		else
		{
			WriteBigShort(m, atrtype);
			WriteBigShort(m, 4+16);
			WriteBigShort(m, 2);	//ipv6
			xoredport = out->sin6_port ^ *(unsigned short*)xor; WriteData(m, &xoredport, sizeof(xoredport));
			xoredaddr = ((unsigned int*)&out->sin6_addr)[0] ^ ((unsigned int*)xor)[0]; WriteData(m, &xoredaddr, sizeof(xoredaddr));
			xoredaddr = ((unsigned int*)&out->sin6_addr)[1] ^ ((unsigned int*)xor)[1]; WriteData(m, &xoredaddr, sizeof(xoredaddr));
			xoredaddr = ((unsigned int*)&out->sin6_addr)[2] ^ ((unsigned int*)xor)[2]; WriteData(m, &xoredaddr, sizeof(xoredaddr));
			xoredaddr = ((unsigned int*)&out->sin6_addr)[3] ^ ((unsigned int*)xor)[3]; WriteData(m, &xoredaddr, sizeof(xoredaddr));
		}
	}
}
static void TURN_ReadXorAddress(netmsg_t *m, netadr_t *adr)
{
	int type = ReadBigShort(m)&0xff;
	unsigned char *xor = ((unsigned char*)m->data+4);
	memset(adr, 0, sizeof(*adr));
	if (type == 1)
	{
		struct sockaddr_in *out = (struct sockaddr_in *)&adr->sockaddr;
		memset(out, 0, sizeof(*out));
		out->sin_family = AF_INET;
		out->sin_port = *(unsigned short*)((unsigned char*)m->data+m->readpos) ^ *(unsigned short*)xor;
		m->readpos += 2;
		out->sin_addr.s_addr = *(unsigned int*)((unsigned char*)m->data+m->readpos) ^ *(unsigned int*)xor;
		m->readpos += 4;
	}
	else if (type == 2)
	{
		struct sockaddr_in6 *out = (struct sockaddr_in6 *)&adr->sockaddr;
		memset(out, 0, sizeof(*out));
		out->sin6_family = AF_INET6;
		out->sin6_port = *(unsigned short*)((unsigned char*)m->data+m->cursize) ^ *(unsigned short*)xor;
		m->readpos += 2;
		((unsigned int*)&out->sin6_addr)[0] = ((unsigned int*)((unsigned char*)m->data+m->readpos))[0] ^ ((unsigned int*)xor)[0];
		((unsigned int*)&out->sin6_addr)[1] = ((unsigned int*)((unsigned char*)m->data+m->readpos))[1] ^ ((unsigned int*)xor)[1];
		((unsigned int*)&out->sin6_addr)[2] = ((unsigned int*)((unsigned char*)m->data+m->readpos))[2] ^ ((unsigned int*)xor)[2];
		((unsigned int*)&out->sin6_addr)[3] = ((unsigned int*)((unsigned char*)m->data+m->readpos))[3] ^ ((unsigned int*)xor)[3];
		m->readpos += 16;
	}
}
#ifdef TURN_IS_NOT_BLIND
static qboolean TURN_PacketIsStun(unsigned char *msg, size_t sz)
{
	//used to check if a packet going through the relay is a stun packet.
	//we require the peer to 'reply' with a stun packet before we allow non-stun packets through.
	//this prevents us from being used to attack other services, for the most part.
	if (sz < 20)
		return false;	//too small for even just the header
	if ((((msg[0]<<8)|msg[1])&~STUN_ERROR) != STUN_BINDING)
		return false;	//not valid for opening the connection... don't let turn allocation requests through...
	if (((msg[2]<<8)|(msg[3]<<0)) != sz-20)
		return false;	//bad size
	if (((msg[4]<<24)|(msg[5]<<16)|(msg[6]<<8)|(msg[7]<<0)) != STUN_MAGIC_COOKIE)
		return false;	//could actually be stun, but w/e

	//that's probably enough checks.
	return true;
}
static qboolean TURN_PacketIsBanned(unsigned char *msg, size_t sz)
{
	//if it looks like a stun packet, and isn't a binding, assume its a TURN packet and block it. no nesting turn packets over turn.
	if (sz >= 20)
		if (((msg[4]<<24)|(msg[5]<<16)|(msg[6]<<8)|(msg[7]<<0)) == STUN_MAGIC_COOKIE)
			if (((msg[0]<<8)|msg[1]) != STUN_BINDING)
				if (((msg[2]<<8)|(msg[3]<<0)) == sz-20)
					return true;

	if (TURN_PacketIsStun(msg,sz))
		return false;	//allow the ICE probes.
	if (sz && (msg[0] >= 20 && msg[0] <= 63))
		return false;	//always allow dtls
	return true;		//block everything else. we're expecting these to be run by arbitrary third parties, so unencrypted stuff is --ed.
}
#endif

void TURN_CheckFDs(cluster_t *cluster)
{
	int ofs = 20 + 4+20 + 4;
	char buf[8192];
	int len;
	int addrlen;
	struct turnclient_s *t, **link;
	netadr_t from = {NULL};

//FIXME: use epoll or something. this loop is stupid.
	for (link = &cluster->turns; (t = *link);)
	{
		if ((int)(t->timeout-cluster->curtime) < 0)
		{	//check timeouts, kill if expired.
			cluster->numrelays--;
//printf("relay %s timeout\n", t->username);
			*link = t->next; //remove it
			closesocket(t->remotesock);
			free(t);
			continue;
		}

		link = &t->next;
		addrlen = sizeof(from.sockaddr);
		len = recvfrom(t->remotesock, buf+ofs, sizeof(buf)-ofs, 0, (struct sockaddr*)&from.sockaddr, &addrlen);
		if (len > 0)
		{
			int perm = TURN_FindPermission(cluster, t, &from);
			if (perm < 0)
			{
//Sys_Printf(cluster, "TURN: (inbound) peer not authorised\n");
				continue;
			}

			if (t->isfwd)
			{	//just directly forward it to the client.
				netadr_t fup;
				if (len > 4 && *(unsigned int*)(buf+ofs) == ~0)
					t->remotes[perm].seenstun = true;	//proper out of band packets, woo... assume its okay.
				else if (!t->remotes[perm].seenstun)
					continue;	//err... its giving weird responses...
				NET_SendPacket(cluster, NET_ChooseSocket(cluster->qwdsocket, &fup, t->clientaddr), len, buf+ofs, fup);
				continue;
			}

#ifdef TURN_IS_NOT_BLIND
			if (!t->remotes[perm].seenstun)
			{
				if (TURN_PacketIsStun(buf+ofs, len))
					t->remotes[perm].seenstun = true;	//its an ICE/stun binding packet. peer is a genuine ice peer, open it up.
				else
					continue;	//don't let anything through at all until we've seen a stun-binding packet from the peer (required as part of ICE).
			}
			else if (TURN_PacketIsBanned(buf+ofs, len))
				continue;
#endif

			{
				netmsg_t o = {0};
				o.maxsize = ofs;
				ofs -= 4;
				if (((struct sockaddr*)from.sockaddr)->sa_family == AF_INET)
					ofs -= 4+8;
				else if (((struct sockaddr*)from.sockaddr)->sa_family == AF_INET6)
					ofs -= 4+20;
				ofs -= 20;
				o.maxsize -= ofs;
				o.data = buf+ofs;
				o.cursize=0;
				WriteBigShort(&o, STUN_DATA|STUN_INDICATION);
				WriteBigShort(&o, 0); //size (filled in later)
				WriteBigLong(&o, STUN_MAGIC_COOKIE);
				WriteBigLong(&o, 0);
				WriteBigLong(&o, 0);
				WriteBigLong(&o, 0);
				TURN_AddXorAddress(&o, STUNATTR_XOR_PEER_ADDRESS, &from);
				WriteBigShort(&o, STUNATTR_DATA);
				WriteBigShort(&o, len);
				//should be at our original write pos now...
				o.cursize += len;
				o.maxsize = sizeof(buf)-ofs;
				while(o.cursize&3)
					WriteByte(&o, 0);	//pad it to 4 bytes, to make chrome happy.

				TURN_Send(cluster, &o, &t->clientaddr);
			}
		}
	}

	Fwd_DoPing(cluster);
}

static struct turnclient_s *TURN_Allocate(cluster_t *cluster, netadr_t *clientadr, int fam, const char *username, const char *realm)
{
	unsigned char dig[64];
	struct turnclient_s *t;
	SOCKET sock;

	size_t i;
	int pf;
	struct sockaddr *address;
	struct sockaddr_in	address4;
	struct sockaddr_in6	address6;
	socklen_t addrlen;

	unsigned long nonblocking = true;
	unsigned long v6only = false;
	unsigned short *port;
	unsigned int tries = 5;

	switch(fam)
	{
	case 2:
		pf = PF_INET6;
		memset(&address6, 0, sizeof(address6));
		address6.sin6_family = AF_INET6;
		port = &address6.sin6_port;
		address = (struct sockaddr*)&address6;
		addrlen = sizeof(address6);
		break;
	case 1:
		pf = PF_INET;
		address4.sin_family = AF_INET;
		address4.sin_addr.s_addr = INADDR_ANY;
		port = &address4.sin_port;
		address = (struct sockaddr*)&address4;
		addrlen = sizeof(address4);
		break;
	default:
		return NULL;	//erk
	}
	if ((sock = socket (pf, SOCK_DGRAM, IPPROTO_UDP)) == INVALID_SOCKET)
		return NULL;

#if defined(FD_SETSIZE) && !defined(HAVE_EPOLL)
	if (sock >= FD_SETSIZE)
	{	//'select' cannot cope with this fd. dom't bug out.
		closesocket(sock);
		return NULL;
	}
#endif

	if (ioctlsocket (sock, FIONBIO, &nonblocking) == -1)
	{
		closesocket(sock);
		return NULL;
	}

	if (pf == AF_INET6)
		setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&v6only, sizeof(v6only));

#if defined(_WIN32) && defined(SO_EXCLUSIVEADDRUSE)
	//win32 is so fucked up
	setsockopt(newsocket, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char *)&_true, sizeof(_true));
#endif

	for (tries = 5; ; tries--)
	{
		if (!realm)	//qwfwd always uses ephemerial ports. they're outgoing and thus don't need special attention to work around the relay's firewall.
			*port = 0;
		else if (cluster->turn_maxport <= cluster->turn_minport)	//something screwy
			*port = htons(cluster->turn_minport);
		else
			*port = htons(cluster->turn_minport + rand()%(cluster->turn_maxport-cluster->turn_minport));	//pick a random port from the allowed range (constrained by firewall/router settings).
		if (bind (sock, (void *)address, addrlen) != -1)
			break;	//success.

		if (tries <= 0 || *port==0)
		{	//ran out of attempts to pick a random usable port.
			closesocket(sock);
			return NULL;
		}
	}

	t = calloc(1, sizeof(*t) + 64 + strlen(username)+1);
	t->remotesock = sock;

	memcpy(t->relayaddr.sockaddr, address, addrlen);
	getsockname(sock, (struct sockaddr*)t->relayaddr.sockaddr, &addrlen);	//find how it was actually bound

	//report the public address reported by the master.
	if (((struct sockaddr *)t->relayaddr.sockaddr)->sa_family == AF_INET)
		memcpy(&((struct sockaddr_in *)t->relayaddr.sockaddr)->sin_addr, cluster->turn_ipv4, sizeof(cluster->turn_ipv4));
	else if (((struct sockaddr *)t->relayaddr.sockaddr)->sa_family == AF_INET6)
		memcpy(&((struct sockaddr_in6 *)t->relayaddr.sockaddr)->sin6_addr, cluster->turn_ipv6, sizeof(cluster->turn_ipv6));

	t->clientaddr = *clientadr;

	t->timeout = cluster->curtime + 5*60*1000;
	for (i = 0; i < countof(t->remotes); i++)
		t->remotes[i].timeout = cluster->curtime - 1; //some time in the past.

	t->auth = (char*)(t+1);
	t->username = t->auth+64;
	strcpy(t->username, username);
	tobase64(t->auth,64, dig, CalcHMAC(&hash_sha1, dig,sizeof(dig), t->username,strlen(t->username), cluster->turnkey,sizeof(cluster->turnkey)));

	//compute our 'long-term' key. stoopid md5. so we can send the right responses.
	if (realm)
	{
		hashfunc_t *pwdhash = &hash_md5;
		size_t usersz = strlen(t->username);
		size_t realmsz = strlen(realm);
		size_t authsz = strlen(t->auth);
		char *tmpkey = alloca(usersz + realmsz + authsz + 3);
		memcpy(tmpkey+0,                  t->username, usersz);
			   tmpkey[usersz] = ':';
		memcpy(tmpkey+usersz+1,           realm, realmsz);
			   tmpkey[usersz+1+realmsz] = ':';
		memcpy(tmpkey+usersz+1+realmsz+1, t->auth, authsz);
			   tmpkey[usersz+1+realmsz+1+authsz] = '\0';
		t->keysize = CalcHash(pwdhash, t->key,sizeof(t->key), tmpkey,strlen(tmpkey));
	}
	else
		t->isfwd = true;

	cluster->numrelays++;
	t->next = cluster->turns;
	cluster->turns = t;

#ifdef HAVE_EPOLL
	{
		struct epoll_event ev;
		ev.data.ptr = t;
		ev.events = EPOLLIN;
		epoll_ctl(cluster->epfd, EPOLL_CTL_ADD, t->remotesock, &ev);
	}
#endif
	return t;
}

static size_t TURN_GenerateNonce(netadr_t *adr, char *buf, size_t bufsize)
{	//needs to be reproducible.
	//also pretty much needs to be base64. stoopid stun rules.
	char key[64];
	size_t keysize;
	hashfunc_t *func = &hash_sha1;	//any.
	void *ctx = alloca(func->contextsize);
	if (sizeof(key) < func->digestsize)
		return 0;	//panic
	func->init(ctx);
	func->process(ctx, "weewoo", 6);
	if (((struct sockaddr *)adr->sockaddr)->sa_family == AF_INET)
	{
		struct sockaddr_in *out = (struct sockaddr_in *)&adr->sockaddr;
		func->process(ctx, (void*)out,sizeof(*out));
	}
	else if (((struct sockaddr *)adr->sockaddr)->sa_family == AF_INET6)
	{
		struct sockaddr_in6 *out = (struct sockaddr_in6 *)&adr->sockaddr;
		func->process(ctx, (void*)out,sizeof(*out));
	}
	else
		return 0;	//shouldn't be possible... should probably put an assert here.

	func->terminate(key, ctx);
	keysize = func->digestsize;

	tobase64(buf,bufsize, key,keysize);
	return strlen(buf);
}
static qboolean TURN_ValidateNonce(unsigned char *nonce, netadr_t *adr)
{
	unsigned char buf[20];
	size_t insz = (nonce?nonce[-1]:0); //read the attribute's length. lazily.
	size_t needsz = TURN_GenerateNonce(adr, buf, sizeof(buf));
	if (insz != needsz)
		return false;	//screwy size... reject it.
	else
		return !memcmp(nonce, buf, needsz);
}
static qboolean TURN_ValidateIntegrity(cluster_t *cluster, netmsg_t *m, const unsigned char *user, const unsigned char *realm, const unsigned char *integritycheck, const struct turnclient_s *t)
{
	int attroffset = (integritycheck-(unsigned char*)m->data);
	unsigned char needintegrity[32];

	size_t keysize;
	unsigned char key[32];
	char authbuf[64];
	char *auth;

	hashfunc_t *pwdhash = &hash_md5;
	hashfunc_t *hash;
	if (((integritycheck[-4]<<8)|integritycheck[-3]) == STUNATTR_MSGINTEGRITIY_SHA1)
		hash = &hash_sha1;
//	else if (((integritycheck[-4]<<8)|integritycheck[-3]) == STUNATTR_MSGINTEGRITIY_SHA2_256)
//		hash = &hash_sha2_256;
	else
		return false;	//can't validate this

	if (((integritycheck[-2]<<8)|integritycheck[-1]) != hash->digestsize)
		return false;	//nope... screwy.

	if (t)
	{	//we already validated the username... make sure the connection stays using the same one.
		if (strcmp(user, t->username))
			return false;	//err... it changed? no, get lost.
		auth = t->auth;		//still using the same password, too
	}
	else
	{	//validate the username... we just check the timestamp bit.
		time_t timestamp = strtoull(user, NULL, 0);
		time_t now = time(NULL);
		unsigned char dig[32];

		if (timestamp > now+10)
			return false;	//clockskew? reject timestamps in the future...
		if (timestamp+60*60 < now)
			return false; //more than an hour old, you'll need to get a new authorisation.

		//figure out what the correct auth string should be.
		keysize = CalcHMAC(&hash_sha1, dig,sizeof(dig), user,strlen(user), cluster->turnkey,sizeof(cluster->turnkey));
		tobase64(authbuf,sizeof(authbuf), dig, keysize);
		auth = authbuf;	//this should match what the client was told by the broker.
	}

	//compute our 'long-term' key. stoopid md5.
	{
		size_t usersz = strlen(user);
		size_t realmsz = strlen(realm);
		size_t authsz = strlen(auth);
		char *tmpkey = alloca(usersz + realmsz + authsz + 3);
		memcpy(tmpkey+0,                  user, usersz);
			   tmpkey[usersz] = ':';
		memcpy(tmpkey+usersz+1,           realm, realmsz);
			   tmpkey[usersz+1+realmsz] = ':';
		memcpy(tmpkey+usersz+1+realmsz+1, auth, authsz);
			   tmpkey[usersz+1+realmsz+1+authsz] = '\0';
		keysize = CalcHash(pwdhash, key,sizeof(key), tmpkey,strlen(tmpkey));
	}

	//message integrity is a bit annoying
	((unsigned char*)m->data)[2] = ((attroffset+hash->digestsize-20)>>8)&0xff;	//hashed header length is up to the end of the hmac attribute
	((unsigned char*)m->data)[3] = ((attroffset+hash->digestsize-20)>>0)&0xff;
	attroffset-=4;	//but the hash is to the start of the attribute's header

	//compute the hmac that we're actually checking, success if it matches.
	keysize = CalcHMAC(hash, needintegrity, sizeof(needintegrity), m->data, attroffset, key,keysize);
	return !memcmp(needintegrity, integritycheck, keysize);
}
static void TURN_AddIntegrity(cluster_t *cluster, netmsg_t *m, const struct turnclient_s *t)
{
	size_t keysize;
	hashfunc_t *hash = &hash_sha1;
	unsigned char integrity[64];
	unsigned int crc;

	if (t)
	{
		//message integrity is a bit annoying
		((unsigned char*)m->data)[2] = ((m->cursize+4+hash->digestsize-20)>>8)&0xff;	//hashed header length is up to the end of the hmac attribute
		((unsigned char*)m->data)[3] = ((m->cursize+4+hash->digestsize-20)>>0)&0xff;
		keysize = CalcHMAC(hash, integrity, sizeof(integrity), m->data, m->cursize, t->key,t->keysize);
		WriteBigShort(m, STUNATTR_MSGINTEGRITIY_SHA1);
		WriteBigShort(m, keysize);
		WriteData(m, integrity, keysize);
	}

#ifndef NO_ZLIB
	((unsigned char*)m->data)[2] = ((m->cursize+8-20)>>8)&0xff;	//dummy length
	((unsigned char*)m->data)[3] = ((m->cursize+8-20)>>0)&0xff;
	crc = crc32(0, m->data, m->cursize)^0x5354554e;
	WriteBigShort(m, STUNATTR_FINGERPRINT);
	WriteBigShort(m, sizeof(crc));
	WriteBigLong(m, crc);
#endif
}
static qboolean TURN_ValidateFingerprint(cluster_t *cluster, netmsg_t *m, unsigned int needcrc)
{	//this just verifies that its actually a correct non-corrupted STUN packet. otherwise assume some other multiplexed protocol.
	unsigned int crc;

	if (m->cursize != m->readpos)
		return false;	//must be the last attribute. so don't actually need to fiddle the size.

	((unsigned char*)m->data)[2] = (((m->readpos-20)>>8)&0xff);	//restore the size (integrity might have fucked with it)
	((unsigned char*)m->data)[3] = (((m->readpos-20)>>0)&0xff);
	crc = crc32(0, m->data, m->readpos-8)^0x5354554e;
	if (needcrc == crc)
		return true;	//needs to match.
	return false;		//oops.
}
qboolean TURN_IsRequest(cluster_t *cluster, netmsg_t *m, netadr_t *from)
{
	stunhdr_t hdr;
	unsigned short atr, len, prot;
	unsigned int nextread;
	unsigned int error = 0;
	void *data;
	int fam = 1;
	size_t datasize;
	size_t lifetime;
	netadr_t peeraddr[8];
	int inpeers;
	struct turnclient_s *t;
	unsigned char *innonce, *inrealm, *inuser, *inintegrity;
	unsigned char noncebuf[64];

	//try and identify them first.
	for (t = cluster->turns; t; t = t->next)
	{
		if (Net_CompareAddress(&t->clientaddr, from, 0, 1))
		{	//forward it as-is... mostly.
			if (!t->isfwd)	//turn... check the packet for stun/turn stuff. we now know their connection. woo.
				break;
			if (m->cursize>=11 && !memcmp("\xff\xff\xff\xff""connect ", m->data, 11))
			{	//connect ver qport challenge info
				//we need to pop one proxy from the 'prx' list so the next proxy doesn't try to connect to itself.
				char userinfo[1024];
				char prx[256];
				char *s = (char*)m->data+11;
				char *at;
				char *infostart;
				size_t pre,infolen,trail;

				//parse it a bit
				s = COM_ParseToken(s, noncebuf,sizeof(noncebuf), "");	//read the ver
				if (atoi(noncebuf)!=28)
					return true;	//that protocol ain't supported... nor allowed. get lost. we won't be able to block it over dtls but we also shouldn't need to care (yay for using dtlsconnect instead, which leaks no userinfo beyond the target chain)
				s = COM_ParseToken(s, noncebuf,sizeof(noncebuf), "");	//read the qport
				infostart = s= COM_ParseToken(s, noncebuf,sizeof(noncebuf), "");	//read the challenge (probably not ours)
				s = COM_ParseToken(s, userinfo,sizeof(userinfo), "");	//read the challenge (probably not ours)

				//change the userinfo
				Info_ValueForKey(userinfo, "prx", prx,sizeof(prx));
				at = strrchr(prx, '@');
				if (!*prx)
					return true;	//err... no next hop? we're meant to be handling it? wtf?
				else if (at)
					*at = 0;	//strip off the last proxy (the one we're meant to be proxying to)
				else
					*prx = 0;	//clear it entirely. next hop is the final server. lucky client.
				Info_SetValueForStarKey(userinfo, "prx", prx, sizeof(userinfo));
				//we could set some "*clientip" key, appending 'from', so the server can use that instead of realip mess, but that'd require the server to trust us when banning.

				//hack up the packet to include the new info (with any trailing data still in place, ready for the next hop to see.
				pre = (infostart-(char*)m->data);
				infolen = strlen(userinfo);
				trail = m->cursize-(s-(char*)m->data);
				m->cursize = pre+2+infolen+1+trail;
				if (m->cursize > m->maxsize)
					return true;	//don't crash.
				memmove(&((char*)m->data)[pre+infolen+3], s, trail);
				((char*)m->data)[pre] = ' ';
				((char*)m->data)[pre+1] = '\"';
				memcpy(&((char*)m->data)[pre+2], userinfo, infolen);
				((char*)m->data)[pre+2+infolen] = '\"';
				//should have wiped the userinfo part...
				//((char*)m->data)[m->cursize] = 0;	//for debugging.
			}
			else if (m->cursize>=15 && !memcmp("\xff\xff\xff\xff""dtlsconnect ", m->data, 15))
			{	//dtlsconnect challenge target
				//we need to strip the target. the connect will then be hidden from us.
				char *peer = COM_ParseToken((char*)m->data+15, noncebuf,sizeof(noncebuf), "");	//read the peer's challenge.
				char *at = strrchr(peer, '@');
				if (!*peer)
					return true;	//no next hop? wtf?
				peer = (at?at:peer);
				m->cursize = peer-(char*)m->data;	//just truncate it.

				//while (*peer == '@' || *peer == ' ')
				//	peer++;
				//NET_StringToAddr(peer, &peeraddr[0], 27500);
				//if (!Net_CompareAddress(&peeraddr[0], t->remotes[0].remoteaddr))
				//	return true;	//tried connecting to somewhere else... doesn't make sense.
			}
			t->timeout = t->remotes[0].timeout = cluster->curtime + QWFWDTIMEOUT;	//renew it from c2s packets.
			if (!t->remotes[0].seenstun && m->cursize < 4 && *(unsigned int*)m->data!=~0)
				return true;	//don't allow it through until we get a proper response from the target to know its actually valid and not a ddos.
			NET_SendPacket(cluster, t->remotesock, m->cursize, m->data, t->remotes[0].remoteaddr);
			return true;
		}
	}

	if (m->cursize < 20 || !cluster->turnenabled)
		return false;
	hdr.msgtype = ReadBigShort(m);
//	if (hdr.msgtype != STUN_ALLOCATE)
//		return false;
	hdr.msglen = ReadBigShort(m);
	if (20+hdr.msglen != m->cursize)
		return false;	//nope... sized wrong. must be something else.
	hdr.magiccookie = ReadBigLong(m);
	if (hdr.magiccookie != STUN_MAGIC_COOKIE)
		return false;	//might be an older version, but we don't care. don't let it multiplex badly.
	hdr.transactid[0] = ReadBigLong(m);
	hdr.transactid[1] = ReadBigLong(m);
	hdr.transactid[2] = ReadBigLong(m);

	t = NULL;
	prot = 0;
	lifetime = 0;
	data = NULL;
	datasize = 0;
	inuser = inrealm = innonce = inintegrity = NULL;
	inpeers = 0;

//Sys_Printf(cluster, "TURN: msgtype %x\n", hdr.msgtype);
	for(; m->readpos < m->cursize; m->readpos = nextread)
	{
		atr = ReadBigShort(m);
		len = ReadBigShort(m);
		nextread = m->readpos + ((len+3)&~3);
		if (nextread > m->cursize)
			return false;	//nope, corrupt
//Sys_Printf(cluster, " TURN: attribute %04x\n", atr);
		if (atr == STUNATTR_FINGERPRINT)
		{
#ifdef NO_ZLIB
			continue;
#else
			if (!TURN_ValidateFingerprint(cluster, m, ReadBigLong(m)))
			{
//Sys_Printf(cluster, " TURN: Invalid fingerprint\n");
				return false;
			}
#endif
		}
		/*else if (atr == STUNATTR_MSGINTEGRITIY_SHA2_256 && (!inintegrity || (inintegrity[-4]<<8)|inintegrity[-3]==STUNATTR_MSGINTEGRITIY_SHA1))
		{
			inintegrity = (char*)m->data + m->readpos;
		}*/
		else if (inintegrity)
		{
//Sys_Printf(cluster, " TURN: Ignoring post-integrity %04x\n", atr);
			continue;	//anything after the integrity must be ignored (except those things above).
		}
		else if (atr == STUNATTR_MSGINTEGRITIY_SHA1)
			inintegrity = (char*)m->data + m->readpos;
		else if (atr == STUNATTR_REQUESTED_TRANSPORT)
			prot = ReadBigLong(m)>>24;
		else if (atr == STUNATTR_LIFETIME)
			lifetime = ReadBigLong(m)*1000;
		else if (atr == STUNATTR_REQUESTED_ADDRFAM)
			fam = ReadBigLong(m);
		else if (atr == STUNATTR_ADDITIONAL_ADDRFAM)
		{	//optional
			//fam = ReadBigLong(m);
		}
		else if (atr == STUNATTR_DONT_FRAGMENT)
			;
		else if (atr == STUNATTR_SOFTWARE)
			;
		else if (atr == STUNATTR_USERNAME)
			inuser = (char*)m->data + m->readpos;
		else if (atr == STUNATTR_REALM)
			inrealm = (char*)m->data + m->readpos;
		else if (atr == STUNATTR_NONCE)
			innonce = (char*)m->data + m->readpos;
		else if (atr == STUNATTR_DATA && hdr.msgtype == (STUN_INDICATION|STUN_SEND))
		{
			data = (char*)m->data + m->readpos;
			datasize = len;
		}
		else if (atr == STUNATTR_XOR_PEER_ADDRESS && (
					hdr.msgtype == (STUN_INDICATION|STUN_SEND) ||
					hdr.msgtype == STUN_CREATEPERM))
		{
			if (inpeers < countof(peeraddr))
				TURN_ReadXorAddress(m, &peeraddr[inpeers++]);	//FIXME: we should support multiple of these.
		}
		else if (atr & 0x8000)
		{
			continue;		//unknown optional attributes
		}
		else
			error = 420;
	}
	if (!error && prot != 17 && hdr.msgtype == STUN_ALLOCATE)	//only udp supported between relay and remote peer.
		error = 442;

	if (!error)
	{
		if (hdr.msgtype == STUN_CREATEPERM || hdr.msgtype == STUN_REFRESH || hdr.msgtype == STUN_ALLOCATE)
		{
			if (!inuser || !inrealm || !innonce || !inintegrity)
				error = 401;	//something is null...
			else if (!TURN_ValidateNonce(innonce, from))
				error = 438;	//'Stale Nonce' shouldn't really be happening... client somehow changed address?
			else if (t && strcmp(inuser, t->username))
				error = 441;	//'Wrong Credentials' - no changing usernames!
			else if (!TURN_ValidateIntegrity(cluster, m, inuser, inrealm, inintegrity, t))
			{
//Sys_Printf(cluster, " TURN: Validation failed\n");
				error = 401;	//'Unauthorised'
			}
			else if (!t && hdr.msgtype == STUN_ALLOCATE)
			{
				t = TURN_Allocate(cluster, from, fam, inuser, inrealm);
				if (!t)
					error = 508; //'Insufficient Capacity'
			}
		}
	}
	if (!t && !error)
		error = 437; //'Allocation Mismatch'

	if (hdr.msgtype == STUN_ALLOCATE || hdr.msgtype == STUN_REFRESH || hdr.msgtype == STUN_CREATEPERM)
	{
		struct{
			stunhdr_t hdr;
			unsigned int data[64];
		} pkt;
		netmsg_t o = {0, 0, sizeof(pkt), &pkt};

		if (t && !error && hdr.msgtype == STUN_CREATEPERM)
		{	//FIXME: request could include multiple peers
			int i, p;
			for (p = 0; p < inpeers; p++)
				if (!TURN_Permissable(&peeraddr[p]))
					error = 403;	//no relaying to localhost/etc. no bypassing firewalls.
			if (!error)
			{
				for (p = 0; p < inpeers; p++)
				{
					for (i = 0; i < countof(t->remotes); i++)
					{
						if ((signed int)(t->remotes[i].timeout-cluster->curtime) < 0)
							continue;	//look for a live one...
						if (Net_CompareAddress(&t->remotes[i].remoteaddr, &peeraddr[p], 0,1))
						{
							t->remotes[i].timeout = cluster->curtime + 5*60*1000;	//bump it.
							break;
						}
					}
					if (i == countof(t->remotes))
					{
						for (i = 0; i < countof(t->remotes); i++)
						{
							if ((signed int)(t->remotes[i].timeout-cluster->curtime) < 0)
							{	//this one's dead...
								t->remotes[i].remoteaddr = peeraddr[p];
								t->remotes[i].timeout = cluster->curtime + 5*60*1000;
								break;
							}
						}
						if (i == countof(t->remotes))
							error = 508;	//'Insufficient Capacity'
					}
				}
			}
		}

		if (error)
			WriteBigShort(&o, STUN_ERROR|hdr.msgtype);
		else
			WriteBigShort(&o, STUN_REPLY|hdr.msgtype);
		WriteBigShort(&o, 0);
		WriteBigLong(&o, hdr.magiccookie);
		WriteBigLong(&o, hdr.transactid[0]);
		WriteBigLong(&o, hdr.transactid[1]);
		WriteBigLong(&o, hdr.transactid[2]);
		if (error)
		{
			//WriteBigShort(STUNATTR_SOFTWARE);
			WriteBigShort(&o, STUNATTR_ERROR_CODE);
			WriteBigShort(&o, 4);
			WriteBigLong(&o, ((error/100)<<8) | (error%100));	//this is stupid.
			if (error == 420)
				;//WriteBigShort(STUNATTR_UNKNOWN_ATTRIBUTES);

			if ((error == 401 && !innonce) || error == 438)
			{
				char *realm = "fteqtv";
				size_t sz = TURN_GenerateNonce(from, noncebuf, sizeof(noncebuf));
				WriteBigShort(&o, STUNATTR_NONCE);
				WriteBigShort(&o, sz);
				WriteData(&o, noncebuf, sz);
				while (o.cursize&3)
					WriteByte(&o, 0);

				sz = strlen(realm);
				WriteBigShort(&o, STUNATTR_REALM);
				WriteBigShort(&o, sz);
				WriteData(&o, realm, sz);
				while (o.cursize&3)
					WriteByte(&o, 0);
			}
		}
		else if (t)
		{
			if (hdr.msgtype == STUN_ALLOCATE || hdr.msgtype == STUN_REFRESH)
			{
				if (lifetime > 5*60*1000)
					lifetime = 5*60*1000;
				if (lifetime && t->timeout < lifetime)
					t->timeout = lifetime;
			}
			lifetime = t->timeout;
			//WriteBigShort(STUNATTR_SOFTWARE);
			if (hdr.msgtype == STUN_ALLOCATE || hdr.msgtype == STUN_REFRESH)
			{
				WriteBigShort(&o, STUNATTR_LIFETIME);
				WriteBigShort(&o, 4);
				WriteBigLong(&o, lifetime/1000);
			}
			if (hdr.msgtype == STUN_ALLOCATE)
			{
				TURN_AddXorAddress(&o, STUNATTR_XOR_RELAYED_ADDRESS, &t->relayaddr);
				TURN_AddXorAddress(&o, STUNATTR_XOR_MAPPED_ADDRESS, from);
			}
			if (hdr.msgtype == STUN_ALLOCATE || hdr.msgtype == STUN_REFRESH || hdr.msgtype == STUN_CREATEPERM)
				TURN_AddIntegrity(cluster, &o, t);
		}
		else
			return true;	//wut?

		TURN_Send(cluster, &o, from);
		return true;
	}
	else if (hdr.msgtype == (STUN_INDICATION|STUN_SEND))
	{
		//just sending to the other end
		if (t && inpeers == 1)
		{
			int perm = TURN_FindPermission(cluster, t, &peeraddr[0]); //sends do NOT refresh.
			if (perm >= 0)
			{
#ifdef TURN_IS_NOT_BLIND
				if (!t->remotes[perm].seenstun && !TURN_PacketIsStun(data, datasize))
					;	//don't relay non-stun packets until we know the peer is running a stun server too.
				else if (TURN_PacketIsBanned(data, datasize))
					;
				else
#endif
					NET_SendPacket(cluster, t->remotesock, datasize, data, peeraddr[0]);
			}
		}
		return true;
	}
	else
		return false;	//dunno what that rubbish is.
}

void Fwd_NewQWFwd(cluster_t *cluster, netadr_t *from, char *targ)
{
	char *rechallenge = "\xff\xff\xff\xffgetchallenge\n";
	struct turnclient_s *t;
	netadr_t adr;
	char *at = strrchr(targ, '@');
	if (at)
		targ = at+1;	//we only care about the next hop here, not the full route.

	if (NET_StringToAddr(targ, &adr, 27500))
	{
		if (!cluster->relayenabled)
		{
			Netchan_OutOfBandPrint(cluster, *from, "n" "Relay not enabled.\n");
			return;	//don't allow it.
		}
		else if (!TURN_Permissable(&adr))
		{	//don't route to 127.* or 192.168.* etc.
			Netchan_OutOfBandPrint(cluster, *from, "n" "Target address is private\n");
			return;
		}
		else
			t = TURN_Allocate(cluster, from, (((struct sockaddr*)adr.sockaddr)->sa_family==AF_INET6)?2:1, "fwd", NULL);
		if (t)
		{
			t->remotes[0].remoteaddr = adr;
			t->remotes[0].timeout = cluster->curtime + QWFWDTIMEOUT;

//Netchan_OutOfBandPrint(cluster, *from, "n" "relay established\n");

			//send a quick challenge to the remote as if it came from the client. the client can then deal with it when it comes back, instead of waiting for the client's next getchallenge timeout.
			NET_SendPacket(cluster, t->remotesock, strlen(rechallenge), rechallenge, t->remotes[0].remoteaddr);

			return;
		}
		else
			Netchan_OutOfBandPrint(cluster, *from, "n" "Unable to set up relay\n");	//ran out of FDs?
	}
	else
		Netchan_OutOfBandPrint(cluster, *from, "n" "Unanle to resolve target address: %s\n", targ);
}

struct relaypeer_s
{
	netadr_t adr;
	unsigned int lastalive;	//last time it was reported by a master (removed after timeout)
	unsigned int lastpong;	//when we last saw a response from them
	unsigned int lastping;	//timestamp we last sent a ping
	unsigned int curping;	//ping time from last pong (or PING_UNRESPONSIVE)
#define PING_UNRESPONSIVE (~0u)
	struct relaypeer_s *next;
};
static void Fwd_AddPeer(cluster_t *cluster, netadr_t *a)
{
	struct relaypeer_s *rp;
	//this loop could be improved with a hash table.
	for (rp = cluster->relaypeer; rp; rp = rp->next)
	{
		if (Net_CompareAddress(&rp->adr, a, 0, 1))
			break;
	}

	if (!rp)
	{	//add it...
		if (!TURN_Permissable(a))
			return;	//unless its a private address.
		rp = malloc(sizeof(*rp));
		rp->adr = *a;
		rp->lastpong = cluster->curtime;
		rp->lastping = cluster->curtime+0x80000000;	//something that's expired.
		rp->curping = PING_UNRESPONSIVE;	//don't know yet.
		rp->next = cluster->relaypeer;
		cluster->relaypeer = rp;

		cluster->numpeers += 1;
	}

	rp->lastalive = cluster->curtime;	//mark as still alive.
}
void Fwd_ParseServerList(cluster_t *cluster, netmsg_t *m, int af)
{	//response from master
	netadr_t a;
	int j;
	char t;
	while(m->readpos < m->cursize)
	{
		if (af == AF_INET)
			t = '\\';
		else if (af == AF_INET6)
			t = '/';
		else
			t = ReadByte(m);	//fancy protocol that has leading ids

		if (t == '\\')
		{	//ipv4
			struct sockaddr_in *o = (struct sockaddr_in *)a.sockaddr;
			if (m->readpos+6 > m->cursize)
				break;	//eof?
			memset(&a, 0, sizeof(a));
			o->sin_family = AF_INET;
			for (j = 0; j < 4; j++)
				((char*)&o->sin_addr)[j] = ReadByte(m);
			((char*)&o->sin_port)[0] = ReadByte(m);
			((char*)&o->sin_port)[1] = ReadByte(m);
		}
		else if (t == '/')
		{	//ipv6
			struct sockaddr_in6 *o = (struct sockaddr_in6 *)a.sockaddr;
			if (m->readpos+18 > m->cursize)
				break;	//eof?
			memset(&a, 0, sizeof(a));
			o->sin6_family = AF_INET6;
			for (j = 0; j < 16; j++)
				((char*)&o->sin6_addr)[j] = ReadByte(m);
			((char*)&o->sin6_port)[0] = ReadByte(m);
			((char*)&o->sin6_port)[1] = ReadByte(m);

			continue;	//pingstatus does not support ipv6, so don't bother pinging any.
		}
		else
			break;	//erk?
		Fwd_AddPeer(cluster, &a);
	}
}
void Fwd_PingResponse(cluster_t *cluster, netadr_t *from)
{
	struct relaypeer_s *rp;
	//this loop could be improved with a hash table.
	for (rp = cluster->relaypeer; rp; rp = rp->next)
	{
		if (Net_CompareAddress(&rp->adr, from, 0, 1))
		{
			//we have no way to verify the peer.
			//its probably better to allow an attacker to force a large ping rather than a low one. its more obvious.
			//the real target should respond eventually giving an upper bound for the ping value.
			//don't extend aliveness though, if a peer stops heartbeating then we just let it hide. only the master responses may extend its lifetime.
			rp->curping = cluster->curtime - rp->lastping;
			rp->lastpong = cluster->curtime;
			return;
		}
	}
}
static void Fwd_DoPing(cluster_t *cluster)
{
	struct relaypeer_s *rp, **l;
	if (!cluster->pingtreeenabled || !cluster->relayenabled)
		return;	//don't spam at all.
	if (cluster->curtime-cluster->relay_lastping < QRY_PINGINTERVAL)
		return; //don't burst
	cluster->relay_lastping = cluster->curtime;

	if (cluster->curtime-cluster->relay_lastquery >= QRY_SERVERLISTINTERVAL)
	{
		netadr_t adr;
		cluster->relay_lastquery = cluster->curtime;
		if (NET_StringToAddr(cluster->master, &adr, 27950))
		{
			if (((struct sockaddr_in *)adr.sockaddr)->sin_family == AF_INET &&
				((struct sockaddr_in *)adr.sockaddr)->sin_port == htons(27000))
			{
				netadr_t realadr;
				NET_SendPacket (cluster, NET_ChooseSocket(cluster->qwdsocket, &realadr, adr), 2, "c\n", realadr); //legacy. screwy. no 0xff prefix.
			}
			else
				Netchan_OutOfBandPrint(cluster, adr, "getserversExt %s %u empty full"/*" ipv6"*/, cluster->protocolname, cluster->protocolver);
		}
		return;	//space it out with pings.
	}

	for (l = &cluster->relaypeer; (rp=*l);)
	{
		if (cluster->curtime-rp->lastping >= QRY_REPINGINTERVAL)	//its been long enough since this one was pinged...
		{
			if (cluster->curtime - rp->lastalive > QRY_TIMEOUT)
			{	//its been too long. cut our losses. its dead jim.
				*l = rp->next;
				free(rp);
				cluster->numpeers -= 1;
				continue;
			}

			//okay, we're pinging this one.
			rp->lastping = cluster->curtime;
			Netchan_OutOfBandPrint(cluster, rp->adr, "k");	//tiny unrealistic ping...

			//unlink it... walk em till the end, and then insert there. if there's 10000 servers then it'll just ping slower... much slower... and fail to report them all in a single udp packet, but hey.
			for (*l = rp->next; *l; l = &(*l)->next)
				;
			*l = rp;
			rp->next = NULL;

			return;	//only ping one.
		}
		l = &rp->next;
	}
}
void Fwd_PingStatus(cluster_t *cluster, netadr_t *from, qboolean ext)
{
	struct relaypeer_s *rp;
	char buffer[8192];
	netmsg_t send;
	netadr_t fixup;
	int j;
	int resetsize;

	if (!cluster->pingtreeenabled || !cluster->relayenabled)
		return;	//don't bother responding.

	InitNetMsg (&send, buffer, sizeof(buffer));

	//small prefix...
	WriteLong (&send, -1);	// -1 sequence means out of band
	if (ext)
		WriteString2 (&send, "pinglist");
	else
		WriteByte (&send, 'n');	// 'a2c_print'... yeah, this is fucked.

	resetsize = send.cursize;

	for (rp = cluster->relaypeer; rp; rp = rp->next)
	{
		if (rp->lastping > 0x7fff)
			continue;	//don't report dead ones. don't do negatives either!
		if (((struct sockaddr*)rp->adr.sockaddr)->sa_family == AF_INET)
		{
			struct sockaddr_in *a = ((struct sockaddr_in*)rp->adr.sockaddr);
			if (send.cursize > 1400)
			{
				NET_SendPacket (cluster, NET_ChooseSocket(cluster->qwdsocket, &fixup, *from), send.cursize, send.data, fixup);
				send.cursize = resetsize;
			}

			if (ext)
				WriteByte (&send, '\\');
			for (j = 0; j < 4; j++)
				WriteByte (&send, ((char*)&a->sin_addr)[j]);
			WriteShort(&send, ntohs(a->sin_port));
			WriteShort(&send, rp->curping);
		}
		else if (((struct sockaddr*)rp->adr.sockaddr)->sa_family == AF_INET6)
		{
			struct sockaddr_in6 *a = ((struct sockaddr_in6*)rp->adr.sockaddr);
			if (send.cursize > 1400)
			{
				NET_SendPacket (cluster, NET_ChooseSocket(cluster->qwdsocket, &fixup, *from), send.cursize, send.data, fixup);
				send.cursize = resetsize;
			}

			if (ext)
				WriteByte (&send, '/');
			else
				continue;
			for (j = 0; j < 16; j++)
				WriteByte (&send, ((char*)&a->sin6_addr)[j]);
			WriteShort(&send, ntohs(a->sin6_port));
			WriteShort(&send, rp->curping);
		}
	}

// send the datagram
	NET_SendPacket (cluster, NET_ChooseSocket(cluster->qwdsocket, &fixup, *from), send.cursize, send.data, fixup);
}

void TURN_RelayStatus(cmdctxt_t *ctx)
{
	cluster_t *cluster = ctx->cluster;
	unsigned int live=0, unreach=0, dead=0;
	struct relaypeer_s *rp;
	unsigned int turns=0, fwds=0;
	turnclient_t *t;
	for (t = cluster->turns; t; t = t->next)
	{
		if (t->isfwd)
			fwds++;
		else
			turns++;
	}
	for (rp = cluster->relaypeer; rp; rp = rp->next)
	{
		if (rp->curping == PING_UNRESPONSIVE)
			unreach++;	//never got a response.
		else if (cluster->curtime-rp->lastpong > 125*1000)
			dead++;	//no response in the last 2 mins
		else
			live++;
	}

	Cmd_Printf(ctx, " %i relays (%i TURN%s, %i fwd%s)\n", cluster->numrelays, turns, cluster->turnenabled?"":"[OFF]", fwds, cluster->relayenabled?"":"[OFF]");
	if (ctx->cluster->turnenabled)
		Cmd_Printf(ctx, " relaying through %i.%i.%i.%i %i-%i\n", ctx->cluster->turn_ipv4[0],ctx->cluster->turn_ipv4[1],ctx->cluster->turn_ipv4[2],ctx->cluster->turn_ipv4[3], ctx->cluster->turn_minport, ctx->cluster->turn_maxport);
	else if (cluster->relayenabled)
		Cmd_Printf(ctx, " relaying through %i.%i.%i.%i\n", ctx->cluster->turn_ipv4[0],ctx->cluster->turn_ipv4[1],ctx->cluster->turn_ipv4[2],ctx->cluster->turn_ipv4[3]);
	if (cluster->relayenabled)
	{
		if (cluster->pingtreeenabled)
			Cmd_Printf(ctx, " %i peers (%i live, %i stale, %i unreach)\n", cluster->numpeers, live, dead, unreach);
		else
			Cmd_Printf(ctx, " pinging disabled\n");
	}
}