ff1a2299f4
adds qtv relay support. lots of other misc tweaks.
1373 lines
No EOL
47 KiB
C
1373 lines
No EOL
47 KiB
C
/*
|
|
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");
|
|
}
|
|
} |