fteqw/engine/common/net_ice.c
Shpoike ff1a2299f4 megacommit.
adds qtv relay support.
lots of other misc tweaks.
2024-07-14 19:58:26 +01:00

6095 lines
184 KiB
C

/*
Interactive Connectivity Establishment (rfc 5245)
find out your peer's potential ports.
spam your peer with stun packets.
see what sticks.
the 'controller' assigns some final candidate pair to ensure that both peers send+receive from a single connection.
if no candidates are available, try using stun to find public nat addresses.
in fte, a 'pair' is actually in terms of each local socket and remote address. hopefully that won't cause too much weirdness.
this does limit which interfaces we can send packets from, which may cause issues with local TURN relays(does not result in extra prflx candidates) and VPNs(may need to be set up as the default route), and prevents us from being able to report reladdr in candidate offers (again mostly only of use with TURN)
lan connections should resolve to a host interface anyway
stun test packets must contain all sorts of info. username+integrity+fingerprint for validation. priority+usecandidate+icecontrol(ing) to decree the priority of any new remote candidates, whether its finished, and just who decides whether its finished.
peers don't like it when those are missing.
host candidates - addresses that are directly known (but are probably unroutable private things)
server reflexive candidates - addresses that we found from some public stun server (useful for NATs that use a single public port for each unique private port)
peer reflexive candidates - addresses that our peer finds out about as we spam them
relayed candidates - some sort of socks5 or something proxy.
Note: Even after the ICE connection becomes active, you should continue to collect local candidates and transmit them to the peer out of band.
this allows the connection to pick a new route if some router dies (like a relay kicking us).
FIXME: the client currently disconnects from the broker. the server tracks players via ip rather than ICE.
tcp rtp framing should generally be done with a 16-bit network-endian length prefix followed by the data.
NOTE: we do NOT distinguish between media-level and session-level attributes, as such we can only handle ONE media stream per session. we also don't support rtcp.
*/
/*
webrtc
basically just sctp-over-dtls-over-ice or srtp(negotiated via dtls)-over-ice.
the sctp part is pure bloat+pain for us, but as its required for browser compat we have to support it anyway - but we only use it where we must.
we don't do any srtp stuff at all.
*/
/*
broker
ftemaster provides a broker service
*/
#include "quakedef.h"
#include "netinc.h"
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
#define STUN_DATA 0x0007 //TURN
#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_USERNAME 0x0006
#define STUNATTR_MSGINTEGRITIY_SHA1 0x0008
#define STUNATTR_ERROR_CODE 0x0009
//#define STUNATTR_CHANNELNUMBER 0x000c //TURN
#define STUNATTR_LIFETIME 0x000d //TURN
#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_PASSWORD_ALGORITHM 0x001D //yay, screw md5
#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
typedef struct
{
unsigned short attrtype;
unsigned short attrlen;
} stunattr_t;
#if defined(SUPPORT_ICE) || (defined(MASTERONLY) && defined(AVAIL_ZLIB))
#include "zlib.h"
#endif
#ifdef SUPPORT_ICE
static cvar_t net_ice_exchangeprivateips = CVARFD("net_ice_exchangeprivateips", "0", CVAR_NOTFROMSERVER, "Boolean. When set to 0, hides private IP addresses from your peers - only addresses determined from the other side of your router will be shared. You should only need to set this to 1 if mdns is unavailable.");
static cvar_t net_ice_allowstun = CVARFD("net_ice_allowstun", "1", CVAR_NOTFROMSERVER, "Boolean. When set to 0, prevents the use of stun to determine our public address (does not prevent connecting to our peer's server-reflexive candidates).");
static cvar_t net_ice_allowturn = CVARFD("net_ice_allowturn", "1", CVAR_NOTFROMSERVER, "Boolean. When set to 0, prevents registration of turn connections (does not prevent connecting to our peer's relay candidates).");
static cvar_t net_ice_allowmdns = CVARFD("net_ice_allowmdns", "1", CVAR_NOTFROMSERVER, "Boolean. When set to 0, prevents the use of multicast-dns to obtain candidates using random numbers instead of revealing private network info.");
static cvar_t net_ice_relayonly = CVARFD("net_ice_relayonly", "0", CVAR_NOTFROMSERVER, "Boolean. When set to 1, blocks reporting non-relay local candidates, does not attempt to connect to remote candidates other than via a relay.");
static cvar_t net_ice_usewebrtc = CVARFD("net_ice_usewebrtc", "", CVAR_NOTFROMSERVER, "Use webrtc's extra overheads rather than simple ICE. This makes packets larger and is slower to connect, but is compatible with the web port.");
static cvar_t net_ice_servers = CVARFD("net_ice_servers", "", CVAR_NOTFROMSERVER, "A space-separated list of ICE servers, eg stun:host.example:3478 or turn:host.example:3478?user=foo?auth=blah");
static cvar_t net_ice_debug = CVARFD("net_ice_debug", "0", CVAR_NOTFROMSERVER, "0: Hide messy details.\n1: Show new candidates.\n2: Show each connectivity test.");
#if 0 //enable this for testing only. will generate excessive error messages with non-hacked turn servers...
#define ASCOPE_TURN_REQUIRESCOPE ASCOPE_HOST //try sending it any address.
#else
#define ASCOPE_TURN_REQUIRESCOPE ASCOPE_LAN //don't report loopback/link-local addresses to turn relays.
#endif
struct icecandidate_s
{
struct icecandinfo_s info;
struct icecandidate_s *next;
netadr_t peer;
//peer needs telling or something.
qboolean dirty;
qboolean ismdns; //indicates that the candidate is a .local domain and thus can be shared without leaking private info.
//these are bitmasks. one bit for each local socket.
unsigned int reachable; //looked like it was up a while ago...
unsigned int reached; //timestamp of when it was last reachable. pick a new one if too old.
unsigned int tried; //don't try again this round
};
struct icestate_s
{
struct icestate_s *next;
void *module;
netadr_t qadr; //address reported to the rest of the engine (packets from our peer get remapped to this)
netadr_t chosenpeer; //address we're sending our data to.
struct iceserver_s
{ //stun/turn servers
netadr_t addr;
qboolean isstun;
unsigned int stunretry; //once a second, extended to once a minite on reply
unsigned int stunrnd[3];
//turn stuff.
ftenet_generic_connection_t *con; //TURN needs unique client addresses otherwise it gets confused and starts reporting errors about client ids. make sure these connections are unique.
//FIXME: should be able to get away with only one if the addr field is unique... move to icestate_s
netadrtype_t family; //ipv4 or ipv6. can't send to other peers.
char *user, *auth;
enum {
TURN_UNINITED, //never tried to poke server, need to send a quicky allocate request.
TURN_HAVE_NONCE, //nonce should be valid, need to send a full allocate request.
TURN_ALLOCATED, //we have an allocation that we need to refresh every now and then.
TURN_TERMINATING, //we have an allocation that we need to refresh every now and then.
} state;
unsigned int expires; //allocation expiry time.
char *nonce, *realm; //gathered from the server
// netadr_t relay, srflx; //our relayed port, and our local address for the sake of it.
unsigned int peers;
struct
{
unsigned int expires; //once a second, extended to once a minite on reply
unsigned int retry; //gotta keep retrying
unsigned int stunrnd[3]; //so we know when to stop retrying.
struct icecandidate_s *rc; //if its not rc then we need to reauth now.
} peer[32];
} server[8];
unsigned int servers;
qboolean brokerless; //we lost connection to our broker... clean up on failure status.
unsigned int icetimeout; //time when we consider the connection dead
unsigned int keepalive; //sent periodically...
unsigned int retries; //bumped after each round of connectivity checks. affects future intervals.
enum iceproto_e proto;
enum icemode_e mode;
qboolean initiator; //sends the initial sdpoffer.
qboolean controlled; //controller chooses final ports.
enum icestate_e state;
char *conname; //internal id.
char *friendlyname; //who you're talking to.
unsigned int originid; //should be randomish
unsigned int originversion;//bumped each time something in the sdp changes.
char originaddress[16];
struct icecandidate_s *lc;
char *lpwd;
char *lufrag;
struct icecandidate_s *rc;
char *rpwd;
char *rufrag;
unsigned int tiehigh;
unsigned int tielow;
int foundation;
qboolean blockcandidates; //don't send candidates yet. FIXME: replace with gathering.
#ifdef HAVE_DTLS
void *dtlsstate;
struct sctp_s *sctp; //ffs! extra processing needed.
const dtlsfuncs_t *dtlsfuncs;
qboolean dtlspassive; //true=server, false=client (separate from ice controller and whether we're hosting. yay...)
dtlscred_t cred; //credentials info for dtls (both peer and local info)
quint16_t mysctpport;
quint16_t peersctpport;
qboolean sctpoptional;
qboolean peersctpoptional; //still uses dtls at least.
#endif
ftenet_connections_t *connections; //used only for PRIVATE sockets.
struct icecodecslot_s
{
//FIXME: we should probably include decode state in here somehow so multiple connections don't clobber each other.
int id;
char *name;
} codecslot[34]; //96-127. don't really need to care about other ones.
struct
{ //this block is for our inbound udp broker reliability, ensuring we get candidate info to where its needed...
char *text;
unsigned int inseq;
unsigned int outseq;
} u;
};
typedef struct sctp_s
{
char *friendlyname; //for printing/debugging.
struct icestate_s *icestate; //for forwarding over ice connections...
const struct dtlsfuncs_s *dtlsfuncs; //for forwarding over dtls connects (ice-lite)
void *dtlsstate;
quint16_t myport, peerport;
qboolean peerhasfwdtsn;
double nextreinit;
void *cookie; //sent part of the handshake (might need resending).
size_t cookiesize;
struct
{
quint32_t verifycode;
qboolean writable;
quint32_t tsn; //Transmit Sequence Number
quint32_t ctsn; //acked tsn
quint32_t losttsn; //total gap size...
} o;
struct
{
quint32_t verifycode;
int ackneeded;
quint32_t ctsn;
quint32_t htsn; //so we don't have to walk so many packets to count gaps.
#define SCTP_RCVSIZE 2048 //cannot be bigger than 65536
qbyte received[SCTP_RCVSIZE/8];
struct
{
quint32_t firsttns; //so we only ack fragments that were complete.
quint32_t tsn; //if a continuation doesn't match, we drop it.
quint32_t ppid;
quint16_t sid;
quint16_t seq;
size_t size;
qboolean toobig;
qbyte buf[65536];
} r;
} i;
unsigned short qstreamid; //in network endian.
} sctp_t;
#ifdef HAVE_DTLS
static const struct
{
const char *name;
hashfunc_t *hash;
} webrtc_hashes[] =
{ //RFC8211 specifies this list of hashes
// {"md2", &hash_md2}, //deprecated, hopefully we won't see it
// {"md5", &hash_md5}, //deprecated, hopefully we won't see it
{"sha-1", &hash_sha1},
{"sha-224", &hash_sha2_224},
{"sha-256", &hash_sha2_256},
{"sha-384", &hash_sha2_384},
{"sha-512", &hash_sha2_512},
};
#endif
static neterr_t ICE_Transmit(void *cbctx, const qbyte *data, size_t datasize);
static neterr_t TURN_Encapsulate(struct icestate_s *ice, netadr_t *to, const qbyte *data, size_t datasize);
static void TURN_AuthorisePeer(struct icestate_s *con, struct iceserver_s *srv, int peer);
#ifdef HAVE_DTLS
static neterr_t SCTP_Transmit(sctp_t *sctp, const void *data, size_t length);
#endif
static qboolean ICE_SetFailed(struct icestate_s *con, const char *reasonfmt, ...) LIKEPRINTF(2);
static struct icestate_s *icelist;
const char *ICE_GetCandidateType(struct icecandinfo_s *info)
{
switch(info->type)
{
case ICE_HOST: return "host";
case ICE_SRFLX: return "srflx";
case ICE_PRFLX: return "prflx";
case ICE_RELAY: return "relay";
}
return "?";
}
static struct icecodecslot_s *ICE_GetCodecSlot(struct icestate_s *ice, int slot)
{
if (slot >= 96 && slot < 96+32)
return &ice->codecslot[slot-96];
else if (slot == 0)
return &ice->codecslot[32];
else if (slot == 8)
return &ice->codecslot[33];
return NULL;
}
#if !defined(SERVERONLY) && defined(VOICECHAT)
extern cvar_t snd_voip_send;
struct rtpheader_s
{
unsigned char v2_p1_x1_cc4;
unsigned char m1_pt7;
unsigned short seq;
unsigned int timestamp;
unsigned int ssrc;
unsigned int csrc[1]; //sized according to cc
};
void S_Voip_RTP_Parse(unsigned short sequence, const char *codec, const unsigned char *data, unsigned int datalen);
qboolean S_Voip_RTP_CodecOkay(const char *codec);
static qboolean NET_RTP_Parse(void)
{
struct rtpheader_s *rtpheader = (void*)net_message.data;
if (net_message.cursize >= sizeof(*rtpheader) && (rtpheader->v2_p1_x1_cc4 & 0xc0) == 0x80)
{
int hlen;
int padding = 0;
struct icestate_s *con;
//make sure this really came from an accepted rtp stream
//note that an rtp connection equal to the game connection will likely mess up when sequences start to get big
//(especially problematic in sane clients that start with a random sequence)
for (con = icelist; con; con = con->next)
{
if (con->state != ICE_INACTIVE && (con->proto == ICEP_VIDEO || con->proto == ICEP_VOICE) && NET_CompareAdr(&net_from, &con->chosenpeer))
{
struct icecodecslot_s *codec = ICE_GetCodecSlot(con, rtpheader->m1_pt7 & 0x7f);
if (codec) //untracked slot
{
char *codecname = codec->name;
if (!codecname) //inactive slot
continue;
if (rtpheader->v2_p1_x1_cc4 & 0x20)
padding = net_message.data[net_message.cursize-1];
hlen = sizeof(*rtpheader);
hlen += ((rtpheader->v2_p1_x1_cc4 & 0xf)-1) * sizeof(int);
if (con->proto == ICEP_VOICE)
S_Voip_RTP_Parse((unsigned short)BigShort(rtpheader->seq), codecname, hlen+(char*)(rtpheader), net_message.cursize - padding - hlen);
// if (con->proto == ICEP_VIDEO)
// S_Video_RTP_Parse((unsigned short)BigShort(rtpheader->seq), codecname, hlen+(char*)(rtpheader), net_message.cursize - padding - hlen);
return true;
}
}
}
}
return false;
}
qboolean NET_RTP_Active(void)
{
struct icestate_s *con;
for (con = icelist; con; con = con->next)
{
if (con->state == ICE_CONNECTED && con->proto == ICEP_VOICE)
return true;
}
return false;
}
qboolean NET_RTP_Transmit(unsigned int sequence, unsigned int timestamp, const char *codec, char *cdata, int clength)
{
sizebuf_t buf;
char pdata[512];
int i;
struct icestate_s *con;
qboolean built = false;
memset(&buf, 0, sizeof(buf));
buf.maxsize = sizeof(pdata);
buf.cursize = 0;
buf.allowoverflow = true;
buf.data = pdata;
for (con = icelist; con; con = con->next)
{
if (con->state == ICE_CONNECTED && con->proto == ICEP_VOICE)
{
for (i = 0; i < countof(con->codecslot); i++)
{
if (con->codecslot[i].name && !strcmp(con->codecslot[i].name, codec))
{
if (!built)
{
built = true;
MSG_WriteByte(&buf, (2u<<6) | (0u<<5) | (0u<<4) | (0<<0)); //v2_p1_x1_cc4
MSG_WriteByte(&buf, (0u<<7) | (con->codecslot[i].id<<0)); //m1_pt7
MSG_WriteShort(&buf, BigShort(sequence&0xffff)); //seq
MSG_WriteLong(&buf, BigLong(timestamp)); //timestamp
MSG_WriteLong(&buf, BigLong(0)); //ssrc
SZ_Write(&buf, cdata, clength);
if (buf.overflowed)
return built;
}
ICE_Transmit(con, buf.data, buf.cursize);
break;
}
}
}
}
return built;
}
#endif
static struct icestate_s *QDECL ICE_Find(void *module, const char *conname)
{
struct icestate_s *con;
for (con = icelist; con; con = con->next)
{
if (con->module == module && !strcmp(con->conname, conname))
return con;
}
return NULL;
}
static ftenet_connections_t *ICE_PickConnection(struct icestate_s *con)
{
if (con->connections)
return con->connections;
switch(con->proto)
{
default:
break;
#ifndef SERVERONLY
case ICEP_VOICE:
case ICEP_QWCLIENT:
return cls.sockets;
#endif
#ifndef CLIENTONLY
case ICEP_QWSERVER:
return svs.sockets;
#endif
}
return NULL;
}
static qboolean TURN_AddXorAddressAttrib(sizebuf_t *buf, unsigned int attr, netadr_t *to)
{ //12 or 24 bytes.
int alen, atype, aofs, i;
if (to->type == NA_IP)
{
alen = 4;
atype = 1;
aofs = 0;
}
else if (to->type == NA_IPV6 &&
!*(int*)&to->address.ip6[0] &&
!*(int*)&to->address.ip6[4] &&
!*(short*)&to->address.ip6[8] &&
*(short*)&to->address.ip6[10] == (short)0xffff)
{ //just because we use an ipv6 address for ipv4 internally doesn't mean we should tell the peer that they're on ipv6...
alen = 4;
atype = 1;
aofs = sizeof(to->address.ip6) - sizeof(to->address.ip);
}
else if (to->type == NA_IPV6)
{
alen = 16;
atype = 2;
aofs = 0;
}
else
return false;
MSG_WriteShort(buf, BigShort(attr));
MSG_WriteShort(buf, BigShort(4+alen));
MSG_WriteShort(buf, BigShort(atype));
MSG_WriteShort(buf, to->port ^ *(short*)(buf->data+4));
for (i = 0; i < alen; i++)
MSG_WriteByte(buf, ((qbyte*)&to->address)[aofs+i] ^ (buf->data+4)[i]);
return true;
}
void Con_HexDump(qbyte *packet, size_t len, size_t badoffset);
static qboolean TURN_AddAuth(sizebuf_t *buf, struct iceserver_s *srv)
{ //adds auth info to a stun packet
unsigned short len;
char integrity[DIGEST_MAXSIZE];
hashfunc_t *hash = &hash_sha1;
hashfunc_t *pwdhash = &hash_md5;
if (!srv->user || !srv->nonce || !srv->realm)
return false;
MSG_WriteShort(buf, BigShort(STUNATTR_USERNAME));
len = strlen(srv->user);
MSG_WriteShort(buf, BigShort(len));
SZ_Write (buf, srv->user, len);
if (len&3)
SZ_Write (buf, "\0\0\0\0", 4-(len&3));
MSG_WriteShort(buf, BigShort(STUNATTR_REALM));
len = strlen(srv->realm);
MSG_WriteShort(buf, BigShort(len));
SZ_Write (buf, srv->realm, len);
if (len&3)
SZ_Write (buf, "\0\0\0\0", 4-(len&3));
MSG_WriteShort(buf, BigShort(STUNATTR_NONCE));
len = strlen(srv->nonce);
MSG_WriteShort(buf, BigShort(len));
SZ_Write (buf, srv->nonce, len);
if (len&3)
SZ_Write (buf, "\0\0\0\0", 4-(len&3));
if (pwdhash != &hash_md5)
{
MSG_WriteShort(buf, BigShort(STUNATTR_PASSWORD_ALGORITHM));
len = strlen(srv->nonce);
MSG_WriteShort(buf, 4);
if (pwdhash == &hash_md5)
MSG_WriteShort(buf, 1);
else if (pwdhash == &hash_sha2_256)
MSG_WriteShort(buf, 2);
else
return false; //not defined... panic.
MSG_WriteShort(buf, 0); //paramlength
//no params.
}
//message integrity is a bit annoying
buf->data[2] = ((buf->cursize+4+hash->digestsize-20)>>8)&0xff; //hashed header length is up to the end of the hmac attribute
buf->data[3] = ((buf->cursize+4+hash->digestsize-20)>>0)&0xff;
//but the hash is to the start of the attribute's header
{ //long-term credentials do stuff weird.
char *tmpkey = va("%s:%s:%s", srv->user, srv->realm, srv->auth);
len = CalcHash(pwdhash, integrity,sizeof(integrity), tmpkey, strlen(tmpkey));
}
len = CalcHMAC(hash, integrity, sizeof(integrity), buf->data, buf->cursize, integrity,len);
if (hash == &hash_sha2_256)
MSG_WriteShort(buf, BigShort(STUNATTR_MSGINTEGRITIY_SHA2_256));
else if (hash == &hash_sha1)
MSG_WriteShort(buf, BigShort(STUNATTR_MSGINTEGRITIY_SHA1));
else
return false; //not defined!
MSG_WriteShort(buf, BigShort(len)); //integrity length
SZ_Write(buf, integrity, len); //integrity data
return true;
}
static const char *ICE_NetworkToName(struct icestate_s *ice, int network)
{
network -= 1;
if (network >= 0 && network < MAX_CONNECTIONS)
{ //return the cvar name
ftenet_connections_t *col = ICE_PickConnection(ice);
if (col && col->conn[network])
return col->conn[network]->name;
}
else if (network >= MAX_CONNECTIONS)
{
network -= MAX_CONNECTIONS;
if (network >= countof(ice->server))
{ //a peer-reflexive address from poking a TURN server...
network -= countof(ice->server);
if (network < ice->servers)
return "turn-reflexive";
}
else
return va("turn:%s", ice->server[network].realm);
}
return "<UNKNOWN>";
}
static neterr_t TURN_Encapsulate(struct icestate_s *ice, netadr_t *to, const qbyte *data, size_t datasize)
{
sizebuf_t buf;
unsigned int network = to->connum-1;
struct iceserver_s *srv;
if (to->type == NA_INVALID)
return NETERR_NOROUTE;
if (to->connum && network >= MAX_CONNECTIONS)
{ //fancy turn-related gubbins
network -= MAX_CONNECTIONS;
if (network >= countof(ice->server))
{ //really high, its from the raw socket, unstunned.
network -= countof(ice->server);
if (network >= countof(ice->server))
return NETERR_NOROUTE;
srv = &ice->server[network];
if (!srv->con || net_ice_relayonly.ival)
return NETERR_CLOGGED;
return srv->con->SendPacket(srv->con, datasize, data, &srv->addr);
}
srv = &ice->server[network];
memset(&buf, 0, sizeof(buf));
buf.maxsize = 20+//stun header
8+16+//(max)peeraddr
4+((datasize+3)&~3);//data
buf.cursize = 0;
buf.data = alloca(buf.maxsize);
MSG_WriteShort(&buf, BigShort(STUN_INDICATION|STUN_SEND));
MSG_WriteShort(&buf, 0); //fill in later
MSG_WriteLong(&buf, BigLong(STUN_MAGIC_COOKIE));
MSG_WriteLong(&buf, 0); //randomid
MSG_WriteLong(&buf, 0); //randomid
MSG_WriteLong(&buf, 0); //randomid
if (!TURN_AddXorAddressAttrib(&buf, STUNATTR_XOR_PEER_ADDRESS, to))
return NETERR_NOROUTE;
MSG_WriteShort(&buf, BigShort(STUNATTR_DATA));
MSG_WriteShort(&buf, BigShort(datasize));
SZ_Write(&buf, data, datasize);
if (datasize&3)
SZ_Write(&buf, "\0\0\0\0", 4-(datasize&3));
//fill in the length (for the final time, after filling in the integrity and fingerprint)
buf.data[2] = ((buf.cursize-20)>>8)&0xff;
buf.data[3] = ((buf.cursize-20)>>0)&0xff;
if (!srv->con)
return NETERR_CLOGGED;
return srv->con->SendPacket(srv->con, buf.cursize, buf.data, &srv->addr);
}
if (net_ice_relayonly.ival)
return NETERR_CLOGGED;
return NET_SendPacket(ICE_PickConnection(ice), datasize, data, to);
}
static neterr_t ICE_Transmit(void *cbctx, const qbyte *data, size_t datasize)
{
struct icestate_s *ice = cbctx;
if (ice->chosenpeer.type == NA_INVALID)
{
if (ice->state == ICE_FAILED)
return NETERR_DISCONNECTED;
else
return NETERR_CLOGGED;
}
return TURN_Encapsulate(ice, &ice->chosenpeer, data, datasize);
}
static struct icestate_s *QDECL ICE_Create(void *module, const char *conname, const char *peername, enum icemode_e mode, enum iceproto_e proto, qboolean initiator)
{
ftenet_connections_t *collection;
struct icestate_s *con;
static unsigned int icenum;
//only allow modes that we actually support.
if (mode != ICEM_RAW && mode != ICEM_ICE && mode != ICEM_WEBRTC)
return NULL;
#ifndef HAVE_DTLS
if (mode == ICEM_WEBRTC)
return NULL;
#endif
//only allow protocols that we actually support.
switch(proto)
{
default:
return NULL;
// case ICEP_VIDEO:
// collection = NULL;
// break;
#if !defined(SERVERONLY) && defined(VOICECHAT)
case ICEP_VOICE:
case ICEP_VIDEO:
collection = cls.sockets;
if (!collection)
{
NET_InitClient(false);
collection = cls.sockets;
}
break;
#endif
#ifndef SERVERONLY
case ICEP_QWCLIENT:
collection = cls.sockets;
if (!collection)
{
NET_InitClient(false);
collection = cls.sockets;
}
break;
#endif
#ifndef CLIENTONLY
case ICEP_QWSERVER:
collection = svs.sockets;
break;
#endif
}
if (!collection)
return NULL; //not initable or something
if (!conname)
{
int rnd[2];
Sys_RandomBytes((void*)rnd, sizeof(rnd));
conname = va("fte%08x%08x", rnd[0], rnd[1]);
}
if (ICE_Find(module, conname))
return NULL; //don't allow dupes.
con = Z_Malloc(sizeof(*con));
con->conname = Z_StrDup(conname);
icenum++;
con->friendlyname = peername?Z_StrDup(peername):Z_StrDupf("%i", icenum);
con->proto = proto;
con->rpwd = Z_StrDup("");
con->rufrag = Z_StrDup("");
Sys_RandomBytes((void*)&con->originid, sizeof(con->originid));
con->originversion = 1;
Q_strncpyz(con->originaddress, "127.0.0.1", sizeof(con->originaddress));
con->initiator = initiator;
con->controlled = !initiator;
con->blockcandidates = true; //until offers/answers are sent.
#ifdef HAVE_DTLS
con->dtlspassive = (proto == ICEP_QWSERVER); //note: may change later.
if (mode == ICEM_WEBRTC)
{ //dtls+sctp is a mandatory part of our connection, sadly.
if (!con->dtlsfuncs)
{
if (con->dtlspassive)
con->dtlsfuncs = DTLS_InitServer();
else
con->dtlsfuncs = DTLS_InitClient(); //credentials are a bit different, though fingerprints make it somewhat irrelevant.
}
if (con->dtlsfuncs && con->dtlsfuncs->GenTempCertificate && !con->cred.local.certsize)
{
Con_DPrintf("Generating dtls certificate...\n");
if (!con->dtlsfuncs->GenTempCertificate(NULL, &con->cred.local))
{
con->dtlsfuncs = NULL;
mode = ICEM_ICE;
Con_DPrintf("Failed\n");
}
else
Con_DPrintf("Done\n");
}
else
{ //failure if we can't do the whole dtls thing.
con->dtlsfuncs = NULL;
Con_Printf(CON_WARNING"DTLS %s support unavailable, disabling encryption (and webrtc compat).\n", con->dtlspassive?"server":"client");
mode = ICEM_ICE; //fall back on unencrypted (this doesn't depend on the peer, so while shitty it hopefully shouldn't be exploitable with a downgrade-attack)
}
if (mode == ICEM_WEBRTC && !net_ice_usewebrtc.ival)
con->sctpoptional = true;
if (mode == ICEM_WEBRTC)
con->mysctpport = 27500;
}
con->qadr.port = htons(con->mysctpport);
#endif
con->qadr.type = NA_ICE;
con->qadr.prot = NP_DGRAM;
Q_strncpyz(con->qadr.address.icename, con->friendlyname, sizeof(con->qadr.address.icename));
con->qadr.port = icenum;
con->mode = mode;
con->next = icelist;
icelist = con;
{
int rnd[1]; //'must have at least 24 bits randomness'
Sys_RandomBytes((void*)rnd, sizeof(rnd));
con->lufrag = Z_StrDupf("%08x", rnd[0]);
}
{
int rnd[4]; //'must have at least 128 bits randomness'
Sys_RandomBytes((void*)rnd, sizeof(rnd));
con->lpwd = Z_StrDupf("%08x%08x%08x%08x", rnd[0], rnd[1], rnd[2], rnd[3]);
}
Sys_RandomBytes((void*)&con->tiehigh, sizeof(con->tiehigh));
Sys_RandomBytes((void*)&con->tielow, sizeof(con->tielow));
if (collection)
{
int i, m;
netadr_t addr[64];
struct ftenet_generic_connection_s *gcon[sizeof(addr)/sizeof(addr[0])];
unsigned int flags[sizeof(addr)/sizeof(addr[0])];
const char *params[sizeof(addr)/sizeof(addr[0])];
m = NET_EnumerateAddresses(collection, gcon, flags, addr, params, sizeof(addr)/sizeof(addr[0]));
for (i = 0; i < m; i++)
{
if (addr[i].type == NA_IP || addr[i].type == NA_IPV6)
{
if (flags[i] & ADDR_REFLEX)
ICE_AddLCandidateInfo(con, &addr[i], i, ICE_SRFLX); //FIXME: needs reladdr relport info
else
ICE_AddLCandidateInfo(con, &addr[i], i, ICE_HOST);
}
}
}
return con;
}
//if either remotecand is null, new packets will be sent to all.
static qboolean ICE_SendSpam(struct icestate_s *con)
{
struct icecandidate_s *rc;
int i;
int bestlocal = -1;
struct icecandidate_s *bestpeer = NULL;
ftenet_connections_t *collection = ICE_PickConnection(con);
if (!collection)
return false;
//only send one ping to each.
for(rc = con->rc; rc; rc = rc->next)
{
for (i = 0; i < MAX_CONNECTIONS; i++)
{
if (collection->conn[i] && collection->conn[i]->prot == NP_DGRAM && (collection->conn[i]->addrtype[0]==NA_IP||collection->conn[i]->addrtype[0]==NA_IPV6))
{
if (!(rc->tried & (1u<<i)))
{
if (!bestpeer || bestpeer->info.priority < rc->info.priority)
{
if (NET_ClassifyAddress(&rc->peer, NULL) < ASCOPE_TURN_REQUIRESCOPE)
{ //don't waste time asking the relay to poke its loopback. if only because it'll report lots of errors.
rc->tried |= (1u<<i);
continue;
}
bestpeer = rc;
bestlocal = i;
}
}
}
}
//send via appropriate turn servers
if (rc->info.type == ICE_RELAY || rc->info.type == ICE_SRFLX)
{
for (i = 0; i < con->servers; i++)
{
if (con->server[i].state!=TURN_ALLOCATED)
continue; //not ready yet...
if (!con->server[i].con)
continue; //can't...
if (!(rc->tried & (1u<<(MAX_CONNECTIONS+i))))
{
//fixme: no local priority. a multihomed machine will try the same ip from different ports.
if (!bestpeer || bestpeer->info.priority < rc->info.priority)
{
if (con->server[i].family && rc->peer.type != con->server[i].family)
continue; //if its ipv4-only then don't send it ipv6 packets or whatever.
bestpeer = rc;
bestlocal = MAX_CONNECTIONS+i;
}
}
}
}
}
if (bestpeer && bestlocal >= 0)
{
neterr_t err;
netadr_t to;
sizebuf_t buf;
char data[512];
char integ[20];
int crc;
qboolean usecandidate = false;
const char *candtype;
unsigned int priority;
memset(&buf, 0, sizeof(buf));
buf.maxsize = sizeof(data);
buf.cursize = 0;
buf.data = data;
bestpeer->tried |= (1u<<bestlocal);
to = bestpeer->peer;
to.connum = 1+bestlocal;
if (to.type == NA_INVALID)
return true; //erk?
safeswitch(bestpeer->info.type)
{
case ICE_HOST: candtype="host"; break;
case ICE_SRFLX: candtype="srflx"; break;
case ICE_PRFLX: candtype="prflx"; break;
case ICE_RELAY: candtype="relay"; break;
safedefault: candtype="?";
}
if (bestlocal >= MAX_CONNECTIONS)
{
struct iceserver_s *srv = &con->server[bestlocal-MAX_CONNECTIONS];
unsigned int i;
unsigned int now = Sys_Milliseconds();
for (i = 0; ; i++)
{
if (i == srv->peers)
{
if (i == countof(srv->peer))
return true;
srv->peer[i].expires = now;
srv->peer[i].rc = bestpeer;
Sys_RandomBytes((char*)srv->peer[i].stunrnd, sizeof(srv->peer[i].stunrnd));
srv->peer[i].retry = now;
srv->peers++;
}
if (srv->peer[i].rc == bestpeer)
{
break;
}
}
if ((int)(srv->peer[i].retry-now) <= 0)
{
srv->peer[i].retry = now + (con->state==ICE_CONNECTED?2000:50); //keep retrying till we get an ack. be less agressive once it no longer matters so much
TURN_AuthorisePeer(con, srv, i);
}
}
if (!con->controlled && NET_CompareAdr(&to, &con->chosenpeer) && to.connum == con->chosenpeer.connum)
usecandidate = true;
MSG_WriteShort(&buf, BigShort(STUN_BINDING));
MSG_WriteShort(&buf, 0); //fill in later
MSG_WriteLong(&buf, BigLong(STUN_MAGIC_COOKIE)); //magic
MSG_WriteLong(&buf, BigLong(0)); //randomid
MSG_WriteLong(&buf, BigLong(0)); //randomid
MSG_WriteLong(&buf, BigLong(0x80000000|bestlocal)); //randomid
if (usecandidate)
{
MSG_WriteShort(&buf, BigShort(STUNATTR_ICE_USE_CANDIDATE));//ICE-USE-CANDIDATE
MSG_WriteShort(&buf, BigShort(0)); //just a flag, so no payload to this attribute
}
//username
MSG_WriteShort(&buf, BigShort(STUNATTR_USERNAME)); //USERNAME
MSG_WriteShort(&buf, BigShort(strlen(con->rufrag) + 1 + strlen(con->lufrag)));
SZ_Write(&buf, con->rufrag, strlen(con->rufrag));
MSG_WriteChar(&buf, ':');
SZ_Write(&buf, con->lufrag, strlen(con->lufrag));
while(buf.cursize&3)
MSG_WriteChar(&buf, 0); //pad
//priority
priority =
//FIXME. should be set to:
// priority = (2^24)*(type preference) +
// (2^8)*(local preference) +
// (2^0)*(256 - component ID)
//type preference should be 126 and is a function of the candidate type (direct sending should be highest priority at 126)
//local preference should reflect multi-homed preferences. ipv4+ipv6 count as multihomed.
//component ID should be 1 (rtcp would be 2 if we supported it)
(1<<24)*(bestlocal>=MAX_CONNECTIONS?0:126) +
(1<<8)*((bestpeer->peer.type == NA_IP?32768:0)+bestlocal*256+(255/*-adrno*/)) +
(1<<0)*(256 - bestpeer->info.component);
MSG_WriteShort(&buf, BigShort(STUNATTR_ICE_PRIORITY));//ICE-PRIORITY
MSG_WriteShort(&buf, BigShort(4));
MSG_WriteLong(&buf, BigLong(priority));
//these two attributes carry a random 64bit tie-breaker.
//the controller is the one with the highest number.
if (con->controlled)
{
MSG_WriteShort(&buf, BigShort(STUNATTR_ICE_CONTROLLED));//ICE-CONTROLLED
MSG_WriteShort(&buf, BigShort(8));
MSG_WriteLong(&buf, BigLong(con->tiehigh));
MSG_WriteLong(&buf, BigLong(con->tielow));
}
else
{
MSG_WriteShort(&buf, BigShort(STUNATTR_ICE_CONTROLLING));//ICE-CONTROLLING
MSG_WriteShort(&buf, BigShort(8));
MSG_WriteLong(&buf, BigLong(con->tiehigh));
MSG_WriteLong(&buf, BigLong(con->tielow));
}
//message integrity is a bit annoying
data[2] = ((buf.cursize+4+sizeof(integ)-20)>>8)&0xff; //hashed header length is up to the end of the hmac attribute
data[3] = ((buf.cursize+4+sizeof(integ)-20)>>0)&0xff;
//but the hash is to the start of the attribute's header
CalcHMAC(&hash_sha1, integ, sizeof(integ), data, buf.cursize, con->rpwd, strlen(con->rpwd));
MSG_WriteShort(&buf, BigShort(STUNATTR_MSGINTEGRITIY_SHA1)); //MESSAGE-INTEGRITY
MSG_WriteShort(&buf, BigShort(20)); //sha1 key length
SZ_Write(&buf, integ, sizeof(integ)); //integrity data
data[2] = ((buf.cursize+8-20)>>8)&0xff; //dummy length
data[3] = ((buf.cursize+8-20)>>0)&0xff;
crc = crc32(0, data, buf.cursize)^0x5354554e;
MSG_WriteShort(&buf, BigShort(STUNATTR_FINGERPRINT)); //FINGERPRINT
MSG_WriteShort(&buf, BigShort(sizeof(crc)));
MSG_WriteLong(&buf, BigLong(crc));
//fill in the length (for the fourth time, after filling in the integrity and fingerprint)
data[2] = ((buf.cursize-20)>>8)&0xff;
data[3] = ((buf.cursize-20)>>0)&0xff;
err = TURN_Encapsulate(con, &to, buf.data, buf.cursize);
switch(err)
{
case NETERR_SENT:
if (net_ice_debug.ival >= 2)
Con_Printf(S_COLOR_GRAY"[%s]: checking %s -> %s:%i (%s)\n", con->friendlyname, ICE_NetworkToName(con, to.connum), bestpeer->info.addr, bestpeer->info.port, candtype);
break;
case NETERR_CLOGGED: //oh well... we retry anyway.
break;
case NETERR_NOROUTE:
if (net_ice_debug.ival >= 2)
Con_Printf(S_COLOR_GRAY"ICE NETERR_NOROUTE to %s:%i(%s)\n", bestpeer->info.addr, bestpeer->info.port, candtype);
break;
case NETERR_DISCONNECTED:
if (net_ice_debug.ival >= 2)
Con_Printf(S_COLOR_GRAY"ICE NETERR_DISCONNECTED to %s:%i(%s)\n", bestpeer->info.addr, bestpeer->info.port, candtype);
break;
case NETERR_MTU:
if (net_ice_debug.ival >= 2)
Con_Printf(S_COLOR_GRAY"ICE NETERR_MTU to %s:%i(%s) (%i bytes)\n", bestpeer->info.addr, bestpeer->info.port, candtype, buf.cursize);
break;
default:
if (net_ice_debug.ival >= 2)
Con_Printf(S_COLOR_GRAY"ICE send error(%i) to %s:%i(%s)\n", err, bestpeer->info.addr, bestpeer->info.port, candtype);
break;
}
return true;
}
return false;
}
#ifdef HAVE_TCP
struct turntcp_connection_s
{ //this sends packets only to the relay, and accepts them only from there too. all packets must be stun packets (for byte-count framing)
ftenet_generic_connection_t pub;
vfsfile_t *f;
struct
{
qbyte buf[20+65536];
unsigned int offset;
unsigned int avail;
} recv, send;
netadr_t adr;
};
static qboolean TURN_TCP_GetPacket(struct ftenet_generic_connection_s *con)
{
struct turntcp_connection_s *n = (struct turntcp_connection_s*)con;
qboolean tried = false;
int err;
if (n->send.avail)
{
err = VFS_WRITE(n->f, n->send.buf+n->send.offset, n->send.avail);
if (err >= 0)
{
n->send.avail -= err;
n->send.offset += err;
}
}
for (;;)
{
if (n->recv.avail >= 20) //must be a stun packet...
{
unsigned int psize = 20 + (n->recv.buf[n->recv.offset+2]<<8)+n->recv.buf[n->recv.offset+3];
if (psize <= n->recv.avail)
{
memcpy(net_message.data, n->recv.buf+n->recv.offset, psize);
n->recv.avail -= psize;
n->recv.offset += psize;
net_message.cursize = psize;
net_from = n->adr;
return true;
}
}
if (n->recv.offset && n->recv.offset+n->recv.avail == sizeof(n->recv.buf))
{
memmove(n->recv.buf, n->recv.buf+n->recv.offset, n->recv.avail);
n->recv.offset = 0;
}
else if (tried)
return false; //don't infinitely loop.
err = VFS_READ(n->f, n->recv.buf+n->recv.offset+n->recv.avail, sizeof(n->recv.buf) - (n->recv.offset+n->recv.avail));
if (!err) //no data...
return false;
else if (err < 0)
return false;
else
{
tried = true;
n->recv.avail += err;
}
}
}
static neterr_t TURN_TCP_SendPacket(struct ftenet_generic_connection_s *con, int length, const void *data, netadr_t *to)
{
int err;
struct turntcp_connection_s *n = (struct turntcp_connection_s*)con;
if (!NET_CompareAdr(to, &n->adr))
return NETERR_NOROUTE;
//validate the packet - make sure its a TURN one. only checking size here cos we're lazy and that's enough.
if (length < 20 || length != 20 + (((const qbyte*)data)[2]<<8)+((const qbyte*)data)[3])
return NETERR_NOROUTE;
if (!n->send.avail && length < sizeof(n->send.buf))
{ //avoid copying if we have to
err = VFS_WRITE(n->f, data, length);
if (err >= 0 && err < length)
{ //but sometimes its partial.
data = (const char*)data + err;
length -= err;
n->send.offset = 0;
memcpy(n->send.buf, data, length);
n->send.avail = length;
}
if (!err)
return NETERR_CLOGGED; //mostly so we don't spam while still doing tcp/tls handshakes.
}
else
{
if ( n->send.avail+length > sizeof(n->send.buf))
return NETERR_CLOGGED; //can't possibly fit.
if (n->send.offset+n->send.avail+length > sizeof(n->send.buf))
{ //move it down if we need.
memmove(n->send.buf, n->send.buf+n->send.offset, n->send.avail);
n->send.offset = 0;
}
memcpy(n->send.buf+n->send.offset, data, length);
n->send.avail += length;
err = VFS_WRITE(n->f, n->send.buf+n->send.offset, n->send.avail);
if (err >= 0)
{
n->send.offset += err;
n->send.avail -= err;
}
}
if (err >= 0)
{
return NETERR_SENT; //sent something at least.
}
else
{
//one of the VFS_ERROR_* codes
return NETERR_DISCONNECTED;
}
}
static void TURN_TCP_Close(struct ftenet_generic_connection_s *con)
{
struct turntcp_connection_s *n = (struct turntcp_connection_s*)con;
if (n->f)
VFS_CLOSE(n->f);
}
static ftenet_generic_connection_t *TURN_TCP_EstablishConnection(ftenet_connections_t *col, const char *address, netadr_t adr)
{
struct turntcp_connection_s *n = Z_Malloc(sizeof(*n));
n->pub.thesocket = TCP_OpenStream(&adr, address);
n->f = FS_WrapTCPSocket(n->pub.thesocket, true, address);
#ifdef HAVE_SSL
//convert to tls...
if (adr.prot == NP_TLS || adr.prot == NP_RTC_TLS)
n->f = FS_OpenSSL(address, n->f, false);
#endif
if (!n->f)
{
Z_Free(n);
return NULL;
}
n->pub.owner = col;
n->pub.SendPacket = TURN_TCP_SendPacket;
n->pub.GetPacket = TURN_TCP_GetPacket;
n->pub.Close = TURN_TCP_Close;
n->adr = adr;
return &n->pub;
}
#endif
static void ICE_ToStunServer(struct icestate_s *con, struct iceserver_s *srv)
{
sizebuf_t buf;
char data[512];
int crc;
ftenet_connections_t *collection = ICE_PickConnection(con);
neterr_t err;
if (!collection)
return;
memset(&buf, 0, sizeof(buf));
buf.maxsize = sizeof(data);
buf.cursize = 0;
buf.data = data;
if (srv->isstun)
{
if (!net_ice_allowstun.ival || net_ice_relayonly.ival)
return;
if (net_ice_debug.ival >= 2)
Con_Printf(S_COLOR_GRAY"[%s]: STUN: Checking public IP via %s\n", con->friendlyname, NET_AdrToString(data, sizeof(data), &srv->addr));
MSG_WriteShort(&buf, BigShort(STUN_BINDING));
}
else
{
if (!net_ice_allowturn.ival)
{
if (net_ice_relayonly.ival)
{
Con_Printf("%s: forcing %s on\n", net_ice_relayonly.name, net_ice_allowturn.name);
Cvar_Set(&net_ice_allowturn, "1");
}
return;
}
if (!srv->con)
{
if (srv->addr.type == NA_INVALID)
return; //nope...
if (srv->addr.prot != NP_DGRAM)
{
#ifdef HAVE_TCP
srv->con = TURN_TCP_EstablishConnection(collection, srv->realm, srv->addr);
#else
srv->con = NULL;
#endif
}
else
{
netadr_t localadr;
memset(&localadr, 0, sizeof(localadr));
localadr.type = srv->addr.type;
localadr.prot = srv->addr.prot;
srv->con = FTENET_Datagram_EstablishConnection(collection, srv->realm, localadr, NULL);
}
if (!srv->con)
{
srv->addr.type = NA_INVALID; //fail it.
return;
}
srv->con->connum = 1+MAX_CONNECTIONS+countof(con->server)+(srv-con->server); //*sigh*
}
if (srv->state==TURN_TERMINATING)
{
if (net_ice_debug.ival >= 2)
Con_Printf(S_COLOR_GRAY"[%s]: TURN: Terminating %s\n", con->friendlyname, NET_AdrToString(data, sizeof(data), &srv->addr));
MSG_WriteShort(&buf, BigShort(STUN_REFRESH));
}
else if (srv->state==TURN_ALLOCATED)
{
if (net_ice_debug.ival >= 2)
Con_Printf(S_COLOR_GRAY"[%s]: TURN: Refreshing %s\n", con->friendlyname, NET_AdrToString(data, sizeof(data), &srv->addr));
MSG_WriteShort(&buf, BigShort(STUN_REFRESH));
}
else
{
if (net_ice_debug.ival >= 2)
Con_Printf(S_COLOR_GRAY "[%s]: TURN: Allocating %s\n", con->friendlyname, NET_AdrToString(data, sizeof(data), &srv->addr));
MSG_WriteShort(&buf, BigShort(STUN_ALLOCATE));
}
}
Sys_RandomBytes((char*)srv->stunrnd, sizeof(srv->stunrnd));
MSG_WriteShort(&buf, 0); //fill in later
MSG_WriteLong(&buf, BigLong(STUN_MAGIC_COOKIE));
MSG_WriteLong(&buf, srv->stunrnd[0]); //randomid
MSG_WriteLong(&buf, srv->stunrnd[1]); //randomid
MSG_WriteLong(&buf, srv->stunrnd[2]); //randomid
if (!srv->isstun)
{
if (srv->state<TURN_ALLOCATED)
{
MSG_WriteShort(&buf, BigShort(STUNATTR_REQUESTED_TRANSPORT));
MSG_WriteShort(&buf, BigShort(4));
MSG_WriteLong(&buf, 17/*udp*/);
switch (srv->family)
{
case NA_IP:
//MSG_WriteShort(&buf, BigShort(STUNATTR_REQUESTED_ADDRFAM));
//MSG_WriteShort(&buf, BigShort(4));
//MSG_WriteLong(&buf, 1/*ipv4*/);
break;
case NA_IPV6:
MSG_WriteShort(&buf, BigShort(STUNATTR_REQUESTED_ADDRFAM));
MSG_WriteShort(&buf, BigShort(4));
MSG_WriteLong(&buf, 2/*ipv6*/);
break;
case NA_INVALID: //ask for both ipv4+ipv6.
MSG_WriteShort(&buf, BigShort(STUNATTR_ADDITIONAL_ADDRFAM));
MSG_WriteShort(&buf, BigShort(4));
MSG_WriteLong(&buf, 2/*ipv6*/);
break;
default:
return; //nope... not valid.
}
}
// MSG_WriteShort(&buf, BigShort(STUNATTR_DONT_FRAGMENT));
// MSG_WriteShort(&buf, BigShort(0));
/* MSG_WriteShort(&buf, BigShort(STUNATTR_SOFTWARE));
crc = strlen(FULLENGINENAME);
MSG_WriteShort(&buf, BigShort(crc));
SZ_Write (&buf, FULLENGINENAME, crc);
if (crc&3)
SZ_Write (&buf, "\0\0\0\0", 4-(crc&3));
*/
if (srv->state==TURN_TERMINATING)
{
MSG_WriteShort(&buf, BigShort(STUNATTR_LIFETIME));
MSG_WriteShort(&buf, BigShort(4));
MSG_WriteLong(&buf, 0);
}
else
{
// MSG_WriteShort(&buf, BigShort(STUNATTR_LIFETIME));
// MSG_WriteShort(&buf, BigShort(4));
// MSG_WriteLong(&buf, BigLong(300));
if (srv->state != TURN_UNINITED)
{
if (!TURN_AddAuth(&buf, srv))
return;
}
}
}
else
{
data[2] = ((buf.cursize+8-20)>>8)&0xff; //dummy length
data[3] = ((buf.cursize+8-20)>>0)&0xff;
crc = crc32(0, data, buf.cursize)^0x5354554e;
MSG_WriteShort(&buf, BigShort(STUNATTR_FINGERPRINT));
MSG_WriteShort(&buf, BigShort(sizeof(crc)));
MSG_WriteLong(&buf, BigLong(crc));
}
//fill in the length (for the final time, after filling in the integrity and fingerprint)
data[2] = ((buf.cursize-20)>>8)&0xff;
data[3] = ((buf.cursize-20)>>0)&0xff;
if (srv->isstun)
err = NET_SendPacket(collection, buf.cursize, data, &srv->addr);
else if (srv->con)
err = srv->con->SendPacket(srv->con, buf.cursize, data, &srv->addr);
else
return;
if (err == NETERR_CLOGGED)
srv->stunretry = Sys_Milliseconds(); //just keep retrying until it actually goes through.
}
static void TURN_AuthorisePeer(struct icestate_s *con, struct iceserver_s *srv, int peer)
{
struct icecandidate_s *rc = srv->peer[peer].rc;
sizebuf_t buf;
netadr_t to2;
char data[512];
if (srv->state != TURN_ALLOCATED)
return;
memset(&buf, 0, sizeof(buf));
buf.maxsize = sizeof(data);
buf.cursize = 0;
buf.data = data;
MSG_WriteShort(&buf, BigShort(STUN_CREATEPERM));
MSG_WriteShort(&buf, 0); //fill in later
MSG_WriteLong(&buf, BigLong(STUN_MAGIC_COOKIE)); //magic
MSG_WriteLong(&buf, srv->peer[peer].stunrnd[0]); //randomid
MSG_WriteLong(&buf, srv->peer[peer].stunrnd[1]); //randomid
MSG_WriteLong(&buf, srv->peer[peer].stunrnd[2]); //randomid
if (!TURN_AddXorAddressAttrib(&buf, STUNATTR_XOR_PEER_ADDRESS, &rc->peer))
return;
if (*rc->info.reladdr && strcmp(rc->info.addr, rc->info.reladdr)) //allow the relay to bypass the peer's relay if its different (TURN doesn't care about port permissions).
if (NET_StringToAdr(rc->info.reladdr, rc->info.relport, &to2))
TURN_AddXorAddressAttrib(&buf, STUNATTR_XOR_PEER_ADDRESS, &to2);
if (!TURN_AddAuth(&buf, srv))
return;
buf.data[2] = ((buf.cursize-20)>>8)&0xff;
buf.data[3] = ((buf.cursize-20)>>0)&0xff;
srv->con->SendPacket(srv->con, buf.cursize, buf.data, &srv->addr);
if (net_ice_debug.ival >= 1)
Con_Printf(S_COLOR_GRAY"[%s]: (re)registering %s -> %s:%i (%s)\n", con->friendlyname, srv->realm, rc->info.addr, rc->info.port, ICE_GetCandidateType(&rc->info));
}
static void QDECL ICE_AddRCandidateInfo(struct icestate_s *con, struct icecandinfo_s *n);
static struct mdns_peer_s
{
double nextretry;
int tries; //stop sending after 4, forget a couple seconds after that.
struct icestate_s *con;
struct icecandinfo_s can;
struct mdns_peer_s *next;
} *mdns_peers;
static SOCKET mdns_socket = INVALID_SOCKET;
static char mdns_name[2][43]; //client, server (so we reply with the right set of sockets... make per-ice?)
struct dnshdr_s
{
unsigned short tid, flags, questions, answerrr, authrr, addrr;
};
static qbyte *MDNS_ReadCName(qbyte *in, qbyte *end, char *out, char *outend)
{
char *cname = out;
while (*in && in < end)
{
if (cname != out)
*cname++ = '.';
if (cname+1+*in > outend)
return end; //if it overflows then its an error.
memcpy(cname, in+1, *in);
cname += *in;
in += 1+*in;
}
*cname = 0;
return ++in;
}
static void MDNS_ProcessPacket(qbyte *inmsg, size_t inmsgsize, netadr_t *source)
{
struct dnshdr_s *rh = (struct dnshdr_s *)inmsg;
unsigned short flags;
qbyte *in, *end;
struct {
unsigned short idx, count;
char name[256];
unsigned short type;
unsigned short class;
unsigned int ttl;
qbyte *data;
unsigned short datasize;
} a;
struct mdns_peer_s *peer;
end = inmsg + inmsgsize;
//ignore packets from outside the lan...
if (NET_ClassifyAddress(source, NULL) > ASCOPE_LAN)
return;
if (source->port != htons(5353))
return; //don't answer/read anything unless its actually mdns. there's supposed to be legacy stuff, but browsers don't seem to respond to that either so lets just play safe.
if (inmsgsize < sizeof(*rh))
return; //some kind of truncation...
flags = ntohs(rh->flags);
if (flags & 0x780f)
return; //opcode must be 0, response must be 0
in = (qbyte*)(rh+1);
if (rh->questions)
{
a.count = ntohs(rh->questions);
for (a.idx = 0; a.idx < a.count; a.idx++)
{
ftenet_connections_t *collection = NULL;
qbyte *questionstart = in;
in = MDNS_ReadCName(in, end, a.name, a.name+sizeof(a.name));
if (in+4 > end)
return; //error...
a.type = *in++<<8;
a.type |= *in++<<0;
a.class = *in++<<8;
a.class |= *in++<<0;
#ifdef HAVE_CLIENT
if (*mdns_name[0] && !strcmp(a.name, mdns_name[0]))
collection = cls.sockets;
#endif
#ifdef HAVE_SERVER
if (*mdns_name[1] && !strcmp(a.name, mdns_name[1]))
collection = svs.sockets;
#endif
if (collection && (a.type == 1/*A*/ || a.type == 28/*AAAA*/) && a.class == 1/*IN*/)
{
qbyte resp[512], *o = resp;
int n,m, found=0, sz,ty;
netadr_t addr[16];
struct ftenet_generic_connection_s *gcon[sizeof(addr)/sizeof(addr[0])];
unsigned int flags[sizeof(addr)/sizeof(addr[0])];
const char *params[sizeof(addr)/sizeof(addr[0])];
struct sockaddr_qstorage dest;
const unsigned int ttl = 120;
m = NET_EnumerateAddresses(collection, gcon, flags, addr, params, sizeof(addr)/sizeof(addr[0]));
*o++ = 0;*o++ = 0; //tid - must be 0 for mdns responses.
*o++=0x84;*o++= 0; //flags
*o++ = 0;*o++ = 0; //questions
*o++ = 0;*o++ = 0; //answers
*o++ = 0;*o++ = 0; //auths
*o++ = 0;*o++ = 0; //additionals
for (n = 0; n < m; n++)
{
if (NET_ClassifyAddress(&addr[n], NULL) == ASCOPE_LAN)
{ //just copy a load of crap over
if (addr[n].type == NA_IP)
sz = 4, ty=1;/*A*/
else if (addr[n].type == NA_IPV6)
sz = 16, ty=28;/*AAAA*/
else
continue; //nope.
if (ty != a.type)
continue;
a.class |= 0x8000;
if (o+(in-questionstart)+6+sz > resp+sizeof(resp))
break; //won't fit.
memcpy(o, questionstart, in-questionstart-4);
o += in-questionstart-4;
*o++ = ty>>8; *o++ = ty;
*o++ = a.class>>8; *o++ = a.class;
*o++ = ttl>>24; *o++ = ttl>>16; *o++ = ttl>>8; *o++ = ttl>>0;
*o++ = sz>>8; *o++ = sz;
memcpy(o, &addr[n].address, sz);
o+=sz;
found++;
}
}
resp[6] = found>>8; resp[7] = found&0xff; //replace the answer count now that we actually know
if (!found) //don't bother if we can't actually answer it.
continue;
//send a multicast response... (browsers don't seem to respond to unicasts).
if (a.type & 0x8000)
{ //they asked for a unicast response.
resp[0] = inmsg[0]; resp[1] = inmsg[1]; //redo the tid.
sz = NetadrToSockadr(source, &dest);
}
else
{
sz = sizeof(struct sockaddr_in);
memset(&dest, 0, sz);
((struct sockaddr_in*)&dest)->sin_family = AF_INET;
((struct sockaddr_in*)&dest)->sin_port = htons(5353);
((struct sockaddr_in*)&dest)->sin_addr.s_addr = inet_addr("224.0.0.251");
}
sendto(mdns_socket, resp, o-resp, 0, (struct sockaddr*)&dest, sz);
if (net_ice_debug.ival)
Con_Printf(S_COLOR_GRAY"%s: Answering mdns (%s)\n", NET_AdrToString(resp, sizeof(resp), source), a.name);
}
}
}
a.count = ntohs(rh->answerrr);
for (a.idx = 0; a.idx < a.count; a.idx++)
{
in = MDNS_ReadCName(in, end, a.name, a.name+sizeof(a.name));
if (in+10 > end)
return; //error...
a.type = *in++<<8;
a.type |= *in++<<0;
a.class = *in++<<8;
a.class |= *in++<<0;
a.ttl = *in++<<24;
a.ttl |= *in++<<16;
a.ttl |= *in++<<8;
a.ttl |= *in++<<0;
a.datasize = *in++<<8;
a.datasize |= *in++<<0;
a.data = in;
in += a.datasize;
if (in > end)
return;
for (peer = mdns_peers; peer; peer = peer->next)
{
if (!strcmp(a.name, peer->can.addr))
{ //this is the record we were looking for. yay.
struct icestate_s *ice;
for (ice = icelist; ice; ice = ice->next)
if (ice == peer->con)
break;
if (!ice)
break; //no longer valid...
if ((a.type&0x7fff) == 1/*A*/ && (a.class&0x7fff) == 1/*IN*/ && a.datasize == 4)
{ //we got a proper ipv4 address. yay.
Q_snprintfz(peer->can.addr, sizeof(peer->can.addr), "%i.%i.%i.%i", a.data[0], a.data[1], a.data[2], a.data[3]);
}
else if ((a.type&0x7fff) == 28/*AAAA*/ && (a.class&0x7fff) == 1/*IN*/ && a.datasize == 16)
{ //we got a proper ipv4 address. yay.
Q_snprintfz(peer->can.addr, sizeof(peer->can.addr), "%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x",
(a.data[ 0]<<8)|a.data[ 1],
(a.data[ 2]<<8)|a.data[ 3],
(a.data[ 4]<<8)|a.data[ 5],
(a.data[ 6]<<8)|a.data[ 7],
(a.data[ 8]<<8)|a.data[ 9],
(a.data[10]<<8)|a.data[11],
(a.data[12]<<8)|a.data[13],
(a.data[14]<<8)|a.data[15]);
}
else
{
Con_Printf("Useless answer\n");
break;
}
if (net_ice_debug.ival)
Con_Printf(S_COLOR_GRAY"[%s]: Resolved %s to %s\n", peer->con->friendlyname, a.name, peer->can.addr);
if (peer->tries != 4)
{ //first response?...
peer->tries = 4;
peer->nextretry = Sys_DoubleTime()+0.5;
}
ICE_AddRCandidateInfo(peer->con, &peer->can); //restore it, so we can handle alternatives properly.
Q_strncpyz(peer->can.addr, a.name, sizeof(peer->can.addr));
break;
}
}
}
}
static void MDNS_ReadPackets(void)
{
qbyte inmsg[9000];
int inmsgsize;
netadr_t adr;
struct sockaddr_qstorage source;
for(;;)
{
int slen = sizeof(source);
inmsgsize = recvfrom(mdns_socket, inmsg, sizeof(inmsg), 0, (struct sockaddr*)&source, &slen);
if (inmsgsize <= 0)
break;
SockadrToNetadr(&source, slen, &adr);
MDNS_ProcessPacket(inmsg, inmsgsize, &adr);
}
}
static void MDNS_Shutdown(void)
{
if (mdns_socket == INVALID_SOCKET)
return;
closesocket(mdns_socket);
mdns_socket = INVALID_SOCKET;
}
static void MDNS_GenChars(char *d, size_t len, qbyte *s)
{ //big endian hex, big endian data, can just do it by bytes.
static char tohex[16] = "0123456789abcdef";
for (; len--; s++)
{
*d++ = tohex[*s>>4];
*d++ = tohex[*s&15];
}
}
static void MDNS_Generate(char name[43])
{ //generate a suitable mdns name.
unsigned char uuid[16];
Sys_RandomBytes((char*)uuid, sizeof(uuid));
uuid[8]&=~(1<<6); //clear clk_seq_hi_res bit 6
uuid[8]|=(1<<7); //set clk_seq_hi_res bit 7
uuid[6] &= ~0xf0; //clear time_hi_and_version's high 4 bits
uuid[6] |= 0x40; //replace with version
MDNS_GenChars(name+0, 4, uuid+0);
name[8] = '-';
MDNS_GenChars(name+9, 2, uuid+4);
name[13] = '-';
MDNS_GenChars(name+14, 2, uuid+6);
name[18] = '-';
MDNS_GenChars(name+19, 2, uuid+8);
name[23] = '-';
MDNS_GenChars(name+24, 6, uuid+10);
strcpy(name+36, ".local");
}
static qboolean MDNS_Setup(void)
{
struct sockaddr_in adr;
int _true = true;
// int _false = false;
struct ip_mreq mbrship;
qboolean success = true;
if (mdns_socket != INVALID_SOCKET)
return true; //already got one
memset(&adr, 0, sizeof(adr));
adr.sin_family = AF_INET;
adr.sin_port = htons(5353);
adr.sin_addr.s_addr = INADDR_ANY;
memset(&mbrship, 0, sizeof(mbrship));
mbrship.imr_multiaddr.s_addr = inet_addr("224.0.0.251");
//browsers don't seem to let us take the easy route, so we can't just use our existing helpers.
mdns_socket = socket (PF_INET, SOCK_CLOEXEC|SOCK_DGRAM, IPPROTO_UDP);
if (mdns_socket == INVALID_SOCKET) success = false;
if (!success || 0 > ioctlsocket(mdns_socket, FIONBIO, (void*)&_true)) success = false;
//other processes on the same host may be listening for mdns packets to insert their own responses (eg two browsers), so we need to ensure that other processes can use the same port. there's no real security here for us (that comes from stun's user/pass stuff).
if (!success || 0 > setsockopt (mdns_socket, SOL_SOCKET, SO_REUSEADDR, (const void*)&_true, sizeof(_true))) success = false;
#ifdef SO_REUSEPORT //not on windows.
if (!success || 0 > setsockopt (mdns_socket, SOL_SOCKET, SO_REUSEPORT, (const void*)&_true, sizeof(_true))) success = false;
#endif
#if IP_MULTICAST_LOOP //ideally we'd prefer to not receive our own requests, but this is host-level, not socket-level, so unusable for communication with browsers on the same machine
// if (success) setsockopt (mdns_socket, IPPROTO_IP, IP_MULTICAST_LOOP, &_false, sizeof(_false));
#endif
if (!success || 0 > setsockopt (mdns_socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, (const void*)&mbrship, sizeof(mbrship))) success = false;
adr.sin_addr.s_addr = INADDR_ANY;
if (!success || bind (mdns_socket, (void *)&adr, sizeof(adr)) < 0)
success = false;
if (!success)
{
MDNS_Shutdown();
Con_Printf("mdns setup failed\n");
}
else
MDNS_Generate(mdns_name[0]), MDNS_Generate(mdns_name[1]);
return success;
}
static void MDNS_SendQuery(struct mdns_peer_s *peer)
{
char *n = peer->can.addr, *dot;
struct sockaddr_in dest;
qbyte outmsg[1024], *o = outmsg;
memset(&dest, 0, sizeof(dest));
dest.sin_family = AF_INET;
dest.sin_port = htons(5353);
dest.sin_addr.s_addr = inet_addr("224.0.0.251");
*o++ = 0;*o++ = 0; //tid
*o++ = 0;*o++ = 0; //flags
*o++ = 0;*o++ = 1; //questions
*o++ = 0;*o++ = 0; //answers
*o++ = 0;*o++ = 0; //auths
*o++ = 0;*o++ = 0; //additionals
//mdns is strictly utf-8. no punycode needed.
for(;;)
{
dot = strchr(n, '.');
if (!dot)
dot = n + strlen(n);
if (dot == n)
return; //err... can't write a 0-length label.
*o++ = dot-n;
memcpy(o, n, dot-n); o += dot-n; n += dot-n;
if (!*n)
break;
n++;
}
*o++ = 0;
*o++ = 0; *o++ = 1; //type: 'A' record
*o++ = 0; *o++ = 1; //class: 'IN'
sendto(mdns_socket, outmsg, o-outmsg, 0, (struct sockaddr*)&dest, sizeof(dest));
peer->tries++;
peer->nextretry = Sys_DoubleTime() + (50/1000.0);
}
static void MDNS_SendQueries(void)
{
double time;
struct mdns_peer_s *peer, **link;
if (mdns_socket == INVALID_SOCKET)
return;
MDNS_ReadPackets();
if (!mdns_peers)
return;
time = Sys_DoubleTime();
for (link = &mdns_peers; (peer=*link); )
{
if (peer->nextretry < time)
{
if (peer->tries == 4)
{ //bye bye.
*link = peer->next;
BZ_Free(peer);
continue;
}
MDNS_SendQuery(peer);
if (peer->tries == 4)
peer->nextretry = Sys_DoubleTime() + 2.0;
break; //don't spam multiple each frame.
}
link = &peer->next;
}
}
static void MDNS_AddQuery(struct icestate_s *con, struct icecandinfo_s *can)
{
struct mdns_peer_s *peer;
if (!MDNS_Setup())
return;
peer = Z_Malloc(sizeof(*peer));
peer->con = con;
peer->can = *can;
peer->next = mdns_peers;
peer->tries = 0;
peer->nextretry = Sys_DoubleTime();
mdns_peers = peer;
MDNS_SendQuery(peer);
}
static qboolean MDNS_CharsAreHex(char *s, size_t len)
{
for (; len--; s++)
{
if (*s >= '0' && *s <= '9')
;
else if (*s >= 'a' && *s <= 'f')
;
else
return false;
}
return true;
}
int ParsePartialIP(const char *s, netadr_t *a);
static void QDECL ICE_AddRCandidateInfo(struct icestate_s *con, struct icecandinfo_s *n)
{
struct icecandidate_s *o;
qboolean isnew;
netadr_t peer;
int peerbits;
//I don't give a damn about rtcp.
if (n->component != 1)
return;
if (n->transport != 0)
return; //only UDP is supported.
//check if its an mDNS name - must be a UUID, with a .local on the end.
if (net_ice_allowmdns.ival &&
MDNS_CharsAreHex(n->addr, 8) && n->addr[8]=='-' &&
MDNS_CharsAreHex(n->addr+9, 4) && n->addr[13]=='-' &&
MDNS_CharsAreHex(n->addr+14, 4) && n->addr[18]=='-' &&
MDNS_CharsAreHex(n->addr+19, 4) && n->addr[23]=='-' &&
MDNS_CharsAreHex(n->addr+24, 12) && !strcmp(&n->addr[36], ".local"))
{
MDNS_AddQuery(con, n);
return;
}
//don't use the regular string->addr, they can fail and stall and make us unresponsive etc. hostnames don't really make sense here anyway.
peerbits = ParsePartialIP(n->addr, &peer);
peer.prot = NP_DGRAM;
peer.port = htons(n->port);
if (peer.type == NA_IP && peerbits == 32)
{
//ignore invalid addresses
if (!peer.address.ip[0] && !peer.address.ip[1] && !peer.address.ip[2] && !peer.address.ip[3])
return;
}
else if (peer.type == NA_IPV6 && peerbits == 128)
{
//ignore invalid addresses
int i;
for (i = 0; i < countof(peer.address.ip6); i++)
if (peer.address.ip6[i])
break;
if (i == countof(peer.address.ip6))
return; //all clear. in6_addr_any
}
else
return; //bad address type, or partial.
if (*n->candidateid)
{
for (o = con->rc; o; o = o->next)
{
//not sure that updating candidates is particuarly useful tbh, but hey.
if (!strcmp(o->info.candidateid, n->candidateid))
break;
}
}
else
{
for (o = con->rc; o; o = o->next)
{
//avoid dupes.
if (!strcmp(o->info.addr, n->addr) && o->info.port == n->port)
break;
}
}
if (!o)
{
o = Z_Malloc(sizeof(*o));
o->next = con->rc;
con->rc = o;
Q_strncpyz(o->info.candidateid, n->candidateid, sizeof(o->info.candidateid));
isnew = true;
}
else
{
isnew = false;
}
Q_strncpyz(o->info.addr, n->addr, sizeof(o->info.addr));
o->info.port = n->port;
o->info.type = n->type;
o->info.priority = n->priority;
o->info.network = n->network;
o->info.generation = n->generation;
o->info.foundation = n->foundation;
o->info.component = n->component;
o->info.transport = n->transport;
o->dirty = true;
o->peer = peer;
o->tried = 0;
o->reachable = 0;
if (net_ice_debug.ival >= 1)
Con_Printf(S_COLOR_GRAY"[%s]: %s remote candidate %s: [%s]:%i\n", con->friendlyname, isnew?"Added":"Updated", o->info.candidateid, o->info.addr, o->info.port);
if (n->type == ICE_RELAY && *n->reladdr && (strcmp(n->addr, n->reladdr) || n->port != n->relport))
{ //for relay candidates, add an srflx candidate too.
struct icecandinfo_s t = o->info;
t.type = ICE_SRFLX;
strcpy(t.addr, n->reladdr);
t.port = n->relport;
*t.reladdr = 0;
t.relport = 0;
t.priority |= 1<<24; //nudge its priority up slightly to favour more direct links when we can.
*t.candidateid = 0; //anonymous...
ICE_AddRCandidateInfo(con, &t);
}
}
static qboolean QDECL ICE_Set(struct icestate_s *con, const char *prop, const char *value);
static void ICE_ParseSDPLine(struct icestate_s *con, const char *value)
{
if (!strncmp(value, "a=ice-pwd:", 10))
ICE_Set(con, "rpwd", value+10);
else if (!strncmp(value, "a=ice-ufrag:", 12))
ICE_Set(con, "rufrag", value+12);
#ifdef HAVE_DTLS
else if (!strncmp(value, "a=setup:", 8))
{ //this is their state, so we want the opposite.
if (!strncmp(value+8, "passive", 7))
con->dtlspassive = false;
else if (!strncmp(value+8, "active", 6))
con->dtlspassive = true;
}
else if (!strncmp(value, "a=fingerprint:", 14))
{
hashfunc_t *hash = NULL;
int i;
char name[64];
value = COM_ParseOut(value+14, name, sizeof(name));
for (i = 0; i < countof(webrtc_hashes); i++)
{
if (!strcasecmp(name, webrtc_hashes[i].name))
{
hash = webrtc_hashes[i].hash;
break;
}
}
if (hash && (!con->cred.peer.hash || hash->digestsize>con->cred.peer.hash->digestsize)) //FIXME: digest size is not a good indicator of whether its exploitable or not, but should work for sha1/sha2 options. the sender here is expected to be trustworthy anyway.
{
int b, o, v;
while (*value == ' ')
value++;
for (b = 0; b < hash->digestsize; )
{
v = *value;
if (v >= '0' && v <= '9')
o = (v-'0');
else if (v >= 'A' && v <= 'F')
o = (v-'A'+10);
else if (v >= 'a' && v <= 'f')
o = (v-'a'+10);
else
break;
o <<= 4;
v = *++value;
if (v >= '0' && v <= '9')
o |= (v-'0');
else if (v >= 'A' && v <= 'F')
o |= (v-'A'+10);
else if (v >= 'a' && v <= 'f')
o |= (v-'a'+10);
else
break;
con->cred.peer.digest[b++] = o;
v = *++value;
if (v != ':')
break;
value++;
}
if (b == hash->digestsize)
con->cred.peer.hash = hash; //it was the right size, woo.
else
con->cred.peer.hash = NULL; //bad! (should we 0-pad?)
}
}
else if (!strncmp(value, "a=sctp-port:", 12))
con->peersctpport = atoi(value+12);
else if (!strncmp(value, "a=sctp-optional:", 16))
con->peersctpoptional = atoi(value+16);
#endif
else if (!strncmp(value, "a=rtpmap:", 9))
{
char name[64];
int codec;
char *sl;
value += 9;
codec = strtoul(value, (char**)&value, 0);
if (*value == ' ') value++;
COM_ParseOut(value, name, sizeof(name));
sl = strchr(name, '/');
if (sl)
*sl = '@';
ICE_Set(con, va("codec%i", codec), name);
}
else if (!strncmp(value, "a=candidate:", 12))
{
struct icecandinfo_s n;
memset(&n, 0, sizeof(n));
value += 12;
n.foundation = strtoul(value, (char**)&value, 0);
if(*value == ' ')value++;
n.component = strtoul(value, (char**)&value, 0);
if(*value == ' ')value++;
if (!strncasecmp(value, "UDP ", 4))
{
n.transport = 0;
value += 3;
}
else
return;
if(*value == ' ')value++;
n.priority = strtoul(value, (char**)&value, 0);
if(*value == ' ')value++;
value = COM_ParseOut(value, n.addr, sizeof(n.addr));
if (!value) return;
if(*value == ' ')value++;
n.port = strtoul(value, (char**)&value, 0);
if(*value == ' ')value++;
if (strncmp(value, "typ ", 4)) return;
value += 3;
if(*value == ' ')value++;
if (!strncmp(value, "host", 4))
n.type = ICE_HOST;
else if (!strncmp(value, "srflx", 4))
n.type = ICE_SRFLX;
else if (!strncmp(value, "prflx", 4))
n.type = ICE_PRFLX;
else if (!strncmp(value, "relay", 4))
n.type = ICE_RELAY;
else
return;
while (*value)
{
if(*value == ' ')value++;
if (!strncmp(value, "raddr ", 6))
{
value += 6;
value = COM_ParseOut(value, n.reladdr, sizeof(n.reladdr));
if (!value)
break;
}
else if (!strncmp(value, "rport ", 6))
{
value += 6;
n.relport = strtoul(value, (char**)&value, 0);
}
/*else if (!strncmp(value, "network-cost ", 13))
{
value += 13;
n.netcost = strtoul(value, (char**)&value, 0);
}*/
/*else if (!strncmp(value, "ufrag ", 6))
{
value += 6;
while (*value && *value != ' ')
value++;
}*/
else
{
//this is meant to be extensible.
while (*value && *value != ' ')
value++;
if(*value == ' ')value++;
while (*value && *value != ' ')
value++;
}
}
ICE_AddRCandidateInfo(con, &n);
}
}
static qboolean QDECL ICE_Set(struct icestate_s *con, const char *prop, const char *value)
{
if (!strcmp(prop, "state"))
{
int oldstate = con->state;
if (!strcmp(value, STRINGIFY(ICE_CONNECTING)))
{
con->state = ICE_CONNECTING;
if (net_ice_debug.ival >= 1)
Con_Printf(S_COLOR_GRAY"[%s]: ice state connecting\n", con->friendlyname);
}
else if (!strcmp(value, STRINGIFY(ICE_INACTIVE)))
{
con->state = ICE_INACTIVE;
if (net_ice_debug.ival >= 1)
Con_Printf(S_COLOR_GRAY"[%s]: ice state inactive\n", con->friendlyname);
}
else if (!strcmp(value, STRINGIFY(ICE_FAILED)))
{
if (con->state != ICE_FAILED)
{
con->state = ICE_FAILED;
if (net_ice_debug.ival >= 1)
Con_Printf(S_COLOR_GRAY"[%s]: ice state failed\n", con->friendlyname);
}
}
else if (!strcmp(value, STRINGIFY(ICE_CONNECTED)))
{
con->state = ICE_CONNECTED;
if (net_ice_debug.ival >= 1)
Con_Printf(S_COLOR_GRAY"[%s]: ice state connected\n", con->friendlyname);
}
else
{
Con_Printf("ICE_Set invalid state %s\n", value);
con->state = ICE_INACTIVE;
}
con->icetimeout = Sys_Milliseconds() + 30*1000;
con->retries = 0;
#ifndef SERVERONLY
if (con->state == ICE_CONNECTING && (con->proto == ICEP_QWCLIENT || con->proto == ICEP_VOICE))
if (!cls.sockets)
NET_InitClient(false);
#endif
if (con->state >= ICE_CONNECTING)
{
#ifndef SERVERONLY
if (con->proto == ICEP_QWCLIENT)
CL_Transfer(&con->qadr); //okay, the client should be using this ice connection now. FIXME: this should only switch them over if they're still trying to use the aerlier broker.
#endif
#ifdef HAVE_DTLS
if (con->mode == ICEM_WEBRTC)
{
if (!con->dtlsstate && con->dtlsfuncs)
{
if (con->cred.peer.hash)
con->dtlsstate = con->dtlsfuncs->CreateContext(&con->cred, con, ICE_Transmit, con->dtlspassive);
else if (net_enable_dtls.ival >= 3)
{ //peer doesn't seem to support dtls.
ICE_SetFailed(con, "peer does not support dtls. Set net_enable_dtls to 1 to make optional.\n");
}
else if (con->state == ICE_CONNECTING && net_enable_dtls.ival>=2)
Con_Printf(CON_WARNING"WARNING: [%s]: peer does not support dtls.\n", con->friendlyname);
}
if (!con->sctp && (!con->sctpoptional || !con->peersctpoptional) && con->mysctpport && con->peersctpport)
{
con->sctp = Z_Malloc(sizeof(*con->sctp));
con->sctp->icestate = con;
con->sctp->friendlyname = con->friendlyname;
con->sctp->myport = htons(con->mysctpport);
con->sctp->peerport = htons(con->peersctpport);
con->sctp->o.tsn = rand() ^ (rand()<<16);
Sys_RandomBytes((void*)&con->sctp->o.verifycode, sizeof(con->sctp->o.verifycode));
Sys_RandomBytes((void*)&con->sctp->i.verifycode, sizeof(con->sctp->i.verifycode));
}
}
else if (!con->dtlsstate && con->cred.peer.hash)
{
if (!con->peersctpoptional)
ICE_SetFailed(con, "peer is trying to use dtls.%s\n", net_enable_dtls.ival?"":" Set ^[/net_enable_dtls 1^].");
}
#endif
}
if (oldstate != con->state && con->state == ICE_INACTIVE)
{ //forget our peer
struct icecandidate_s *c;
int i;
memset(&con->chosenpeer, 0, sizeof(con->chosenpeer));
#ifdef HAVE_DTLS
if (con->sctp)
{
Z_Free(con->sctp->cookie);
Z_Free(con->sctp);
con->sctp = NULL;
}
if (con->dtlsstate)
{
con->dtlsfuncs->DestroyContext(con->dtlsstate);
con->dtlsstate = NULL;
}
#endif
while(con->rc)
{
c = con->rc;
con->rc = c->next;
Z_Free(c);
}
while(con->lc)
{
c = con->lc;
con->lc = c->next;
Z_Free(c);
}
for (i = 0; i < con->servers; i++)
{
struct iceserver_s *s = &con->server[i];
if (s->con)
{ //make sure we tell the TURN server to release our allocation.
s->state = TURN_TERMINATING;
ICE_ToStunServer(con, s);
s->con->Close(s->con);
s->con = NULL;
}
Z_Free(s->nonce);
s->nonce = NULL;
s->peers = 0;
}
}
if (oldstate != con->state && con->state == ICE_CONNECTED)
{
if (con->chosenpeer.type == NA_INVALID)
ICE_SetFailed(con, "ICE failed. peer not valid.\n");
#ifndef CLIENTONLY
else if (con->proto == ICEP_QWSERVER && con->mode != ICEM_WEBRTC)
{
net_from = con->qadr;
SVC_GetChallenge(false);
}
#endif
if (con->state == ICE_CONNECTED)
{
if (con->proto >= ICEP_VOICE || net_ice_debug.ival)
{
char msg[256];
Con_Printf(S_COLOR_GRAY "[%s]: %s connection established (peer %s, via %s).\n", con->friendlyname, con->proto == ICEP_VOICE?"voice":"data", NET_AdrToString(msg, sizeof(msg), &con->chosenpeer), ICE_NetworkToName(con, con->chosenpeer.connum));
}
}
}
#if !defined(SERVERONLY) && defined(VOICECHAT)
snd_voip_send.ival = (snd_voip_send.ival & ~4) | (NET_RTP_Active()?4:0);
#endif
}
else if (!strcmp(prop, "controlled"))
con->controlled = !!atoi(value);
else if (!strcmp(prop, "controller"))
con->controlled = !atoi(value);
else if (!strncmp(prop, "codec", 5))
{
struct icecodecslot_s *codec = ICE_GetCodecSlot(con, atoi(prop+5));
if (!codec)
return false;
codec->id = atoi(prop+5);
#if !defined(SERVERONLY) && defined(VOICECHAT)
if (!S_Voip_RTP_CodecOkay(value))
#endif
{
Z_Free(codec->name);
codec->name = NULL;
return false;
}
Z_Free(codec->name);
codec->name = Z_StrDup(value);
}
else if (!strcmp(prop, "rufrag"))
{
Z_Free(con->rufrag);
con->rufrag = Z_StrDup(value);
}
else if (!strcmp(prop, "rpwd"))
{
Z_Free(con->rpwd);
con->rpwd = Z_StrDup(value);
}
else if (!strcmp(prop, "server"))
{
netadr_t hostadr[1];
qboolean okay;
qboolean tcp = false;
qboolean tls;
qboolean stun;
char *s, *next;
const char *user=NULL, *auth=NULL;
const char *host;
netadrtype_t family = NA_INVALID;
if (!strncmp(value, "stun:", 5))
stun=true, tls=false, value+=5;
else if (!strncmp(value, "turn:", 5))
stun=false, tls=false, value+=5;
else if (!strncmp(value, "turns:", 6))
stun=false, tls=true, value+=6;
else
return false; //nope, uri not known.
host = Z_StrDup(value);
s = strchr(host, '?');
for (;s;s=next)
{
*s++ = 0;
next = strchr(s, '?');
if (next)
*next = 0;
if (!strncmp(s, "transport=", 10))
{
if (!strcmp(s+10, "udp"))
tcp=false;
else if (!strcmp(s+10, "tcp"))
tcp=true;
}
else if (!strncmp(s, "user=", 5))
user = s+5;
else if (!strncmp(s, "auth=", 5))
auth = s+5;
else if (!strncmp(s, "fam=", 4))
{
if (!strcmp(s+4, "ipv4") || !strcmp(s+4, "ip4") || !strcmp(s+4, "ip") || !strcmp(s+4, "4"))
family=NA_IP;
else if (!strcmp(s+4, "ipv6") || !strcmp(s+4, "ip6") || !strcmp(s+4, "6"))
family=NA_IPV6;
}
}
okay = !strchr(host, '/');
if (con->servers == countof(con->server))
okay = false;
else if (okay)
{
struct iceserver_s *srv = &con->server[con->servers];
//handily both stun and turn default to the same port numbers.
//FIXME: worker thread...
okay = NET_StringToAdr2(host, tls?5349:3478, hostadr, countof(hostadr), NULL);
if (okay)
{
if (tls)
hostadr->prot = tcp?NP_TLS:NP_DTLS;
else
hostadr->prot = tcp?NP_STREAM:NP_DGRAM;
con->servers++;
srv->isstun = stun;
srv->family = family;
srv->realm = Z_StrDup(host);
Sys_RandomBytes((char*)srv->stunrnd, sizeof(srv->stunrnd));
srv->stunretry = Sys_Milliseconds(); //'now'...
srv->addr = *hostadr;
srv->user = user?Z_StrDup(user):NULL;
srv->auth = auth?Z_StrDup(auth):NULL;
}
}
Z_Free(s);
return !!okay;
}
else if (!strcmp(prop, "sdp") || !strcmp(prop, "sdpoffer") || !strcmp(prop, "sdpanswer"))
{
char line[8192];
const char *eol;
for (; *value; value = eol)
{
eol = strchr(value, '\n');
if (!eol)
eol = value+strlen(value);
if (eol-value < sizeof(line))
{
memcpy(line, value, eol-value);
line[eol-value] = 0;
if (eol>value && line[eol-value-1] == '\r')
line[eol-value-1] = 0;
ICE_ParseSDPLine(con, line);
}
if (eol && *eol)
eol++;
}
}
else
return false;
return true;
}
static qboolean ICE_SetFailed(struct icestate_s *con, const char *reasonfmt, ...)
{
va_list argptr;
char string[256];
va_start (argptr, reasonfmt);
Q_vsnprintfz (string,sizeof(string)-1, reasonfmt,argptr);
va_end (argptr);
if (con->state == ICE_FAILED)
Con_Printf(S_COLOR_GRAY"[%s]: %s\n", con->friendlyname, string); //we were probably already expecting this. don't show it as a warning.
else
Con_Printf(CON_WARNING"[%s]: %s\n", con->friendlyname, string);
return ICE_Set(con, "state", STRINGIFY(ICE_FAILED)); //does the disconnection stuff.
}
static char *ICE_CandidateToSDP(struct icecandidate_s *can, char *value, size_t valuelen)
{
Q_snprintfz(value, valuelen, "a=candidate:%i %i %s %i %s %i typ %s",
can->info.foundation,
can->info.component,
can->info.transport==0?"UDP":"ERROR",
can->info.priority,
can->info.addr,
can->info.port,
ICE_GetCandidateType(&can->info)
);
if (can->info.generation)
Q_strncatz(value, va(" generation %i", can->info.generation), valuelen); //firefox doesn't like this.
if (can->info.type != ICE_HOST)
{
if (net_ice_relayonly.ival)
{ //don't leak srflx info (technically this info is mandatory)
Q_strncatz(value, va(" raddr %s", can->info.addr), valuelen);
Q_strncatz(value, va(" rport %i", can->info.port), valuelen);
}
else
{
if (*can->info.reladdr)
Q_strncatz(value, va(" raddr %s", can->info.reladdr), valuelen);
else
Q_strncatz(value, " raddr 0.0.0.0", valuelen);
Q_strncatz(value, va(" rport %i", can->info.relport), valuelen);
}
}
return value;
}
static qboolean ICE_LCandidateIsPrivate(struct icecandidate_s *caninfo)
{ //return true for the local candidates that we're actually allowed to report. they'll stay flagged as 'dirty' otherwise.
if (!net_ice_exchangeprivateips.ival && caninfo->info.type == ICE_HOST && !caninfo->ismdns)
return true;
if (net_ice_relayonly.ival && caninfo->info.type != ICE_RELAY)
return true;
return false;
}
static qboolean QDECL ICE_Get(struct icestate_s *con, const char *prop, char *value, size_t valuelen)
{
if (!strcmp(prop, "sid"))
Q_strncpyz(value, con->conname, valuelen);
else if (!strcmp(prop, "state"))
{
switch(con->state)
{
case ICE_INACTIVE:
Q_strncpyz(value, STRINGIFY(ICE_INACTIVE), valuelen);
break;
case ICE_FAILED:
Q_strncpyz(value, STRINGIFY(ICE_FAILED), valuelen);
break;
case ICE_GATHERING:
Q_strncpyz(value, STRINGIFY(ICE_GATHERING), valuelen);
break;
case ICE_CONNECTING:
Q_strncpyz(value, STRINGIFY(ICE_CONNECTING), valuelen);
break;
case ICE_CONNECTED:
Q_strncpyz(value, STRINGIFY(ICE_CONNECTED), valuelen);
break;
}
}
else if (!strcmp(prop, "lufrag"))
Q_strncpyz(value, con->lufrag, valuelen);
else if (!strcmp(prop, "lpwd"))
Q_strncpyz(value, con->lpwd, valuelen);
else if (!strncmp(prop, "codec", 5))
{
int codecid = atoi(prop+5);
struct icecodecslot_s *codec = ICE_GetCodecSlot(con, atoi(prop+5));
if (!codec || codec->id != codecid)
return false;
if (codec->name)
Q_strncpyz(value, codec->name, valuelen);
else
Q_strncpyz(value, "", valuelen);
}
else if (!strcmp(prop, "newlc"))
{
struct icecandidate_s *can;
Q_strncpyz(value, "0", valuelen);
for (can = con->lc; can; can = can->next)
{
if (can->dirty && !ICE_LCandidateIsPrivate(can))
{
Q_strncpyz(value, "1", valuelen);
break;
}
}
}
else if (!strcmp(prop, "peersdp"))
{ //for debugging.
Q_strncpyz(value, "", valuelen);
if ((con->proto == ICEP_QWSERVER || con->proto == ICEP_QWCLIENT) && con->mode == ICEM_WEBRTC)
{
#ifdef HAVE_DTLS
if (con->cred.peer.hash)
{
int b;
Q_strncatz(value, "a=fingerprint:", valuelen);
for (b = 0; b < countof(webrtc_hashes); b++)
{
if (con->cred.peer.hash == webrtc_hashes[b].hash)
break;
}
Q_strncatz(value, (b==countof(webrtc_hashes))?"UNKNOWN":webrtc_hashes[b].name, valuelen);
for (b = 0; b < con->cred.peer.hash->digestsize; b++)
Q_strncatz(value, va(b?":%02X":" %02X", con->cred.peer.digest[b]), valuelen);
Q_strncatz(value, "\n", valuelen);
}
#endif
}
Q_strncatz(value, va("a=ice-pwd:%s\n", con->rpwd), valuelen);
Q_strncatz(value, va("a=ice-ufrag:%s\n", con->rufrag), valuelen);
#ifdef HAVE_DTLS
if (con->peersctpport)
Q_strncatz(value, va("a=sctp-port:%i\n", con->peersctpport), valuelen); //stupid hardcoded thing.
if (con->peersctpoptional)
Q_strncatz(value, "a=sctp-optional:1\n", valuelen);
#endif
}
else if (!strcmp(prop, "sdp") || !strcmp(prop, "sdpoffer") || !strcmp(prop, "sdpanswer"))
{
struct icecandidate_s *can;
netadr_t sender;
char tmpstr[MAX_QPATH], *at;
int i;
{
netadr_t addr[1];
struct ftenet_generic_connection_s *gcon[countof(addr)];
int flags[countof(addr)];
const char *params[countof(addr)];
if (!NET_EnumerateAddresses(ICE_PickConnection(con), gcon, flags, addr, params, countof(addr)))
{
sender.type = NA_INVALID;
sender.port = 0;
}
else
sender = *addr;
}
Q_strncpyz(value, "v=0\n", valuelen); //version...
Q_strncatz(value, va("o=%s %u %u IN IP4 %s\n", "-", con->originid, con->originversion, con->originaddress), valuelen); //originator. usually just dummy info.
Q_strncatz(value, va("s=%s\n", con->conname), valuelen); //session name.
Q_strncatz(value, "t=0 0\n", valuelen); //start+end times...
Q_strncatz(value, va("a=ice-options:trickle\n"), valuelen);
if (con->proto == ICEP_QWSERVER || con->proto == ICEP_QWCLIENT)
{
#ifdef HAVE_DTLS
if (net_enable_dtls.ival >= 3)
{ //this is a preliminary check to avoid wasting time
if (!con->cred.local.certsize)
return false; //fail if we cannot do dtls when its required.
if (!strcmp(prop, "sdpanswer") && !con->cred.peer.hash)
return false; //don't answer if they failed to provide a cert
}
if (con->cred.local.certsize)
{
qbyte fingerprint[DIGEST_MAXSIZE];
int b;
hashfunc_t *hash = &hash_sha2_256; //browsers use sha-256, lets match them.
CalcHash(hash, fingerprint, sizeof(fingerprint), con->cred.local.cert, con->cred.local.certsize);
Q_strncatz(value, "a=fingerprint:sha-256", valuelen);
for (b = 0; b < hash->digestsize; b++)
Q_strncatz(value, va(b?":%02X":" %02X", fingerprint[b]), valuelen);
Q_strncatz(value, "\n", valuelen);
if (con->mode == ICEM_WEBRTC)
{
Q_strncatz(value, "m=application 9 UDP/DTLS/SCTP webrtc-datachannel\n", valuelen);
if (con->mysctpport)
Q_strncatz(value, va("a=sctp-port:%i\n", con->mysctpport), valuelen); //stupid hardcoded thing.
if (con->sctpoptional)
Q_strncatz(value, "a=sctp-optional:1\n", valuelen);
}
else
Q_strncatz(value, "m=application 9 UDP/DTLS\n", valuelen);
}
else
Q_strncatz(value, "m=application 9 UDP\n", valuelen);
#endif
}
// Q_strncatz(value, va("c=IN %s %s\n", sender.type==NA_IPV6?"IP6":"IP4", NET_BaseAdrToString(tmpstr, sizeof(tmpstr), &sender)), valuelen);
Q_strncatz(value, "c=IN IP4 0.0.0.0\n", valuelen);
for (can = con->lc; can; can = can->next)
{
char canline[256];
if (ICE_LCandidateIsPrivate(can))
continue; //ignore it.
can->dirty = false; //doesn't matter now.
ICE_CandidateToSDP(can, canline, sizeof(canline));
Q_strncatz(value, canline, valuelen);
Q_strncatz(value, "\n", valuelen);
}
Q_strncatz(value, va("a=ice-pwd:%s\n", con->lpwd), valuelen);
Q_strncatz(value, va("a=ice-ufrag:%s\n", con->lufrag), valuelen);
#ifdef HAVE_DTLS
if (con->dtlsfuncs)
{
if (!strcmp(prop, "sdpanswer"))
{ //answerer decides.
if (con->dtlspassive)
Q_strncatz(value, va("a=setup:passive\n"), valuelen);
else
Q_strncatz(value, va("a=setup:active\n"), valuelen);
}
else if (!strcmp(prop, "sdpoffer"))
Q_strncatz(value, va("a=setup:actpass\n"), valuelen); //don't care if we're active or passive
}
#endif
/*fixme: merge the codecs into a single media line*/
for (i = 0; i < countof(con->codecslot); i++)
{
int id = con->codecslot[i].id;
if (!con->codecslot[i].name)
continue;
Q_strncatz(value, va("m=audio %i RTP/AVP %i\n", sender.port, id), valuelen);
Q_strncatz(value, va("b=RS:0\n"), valuelen);
Q_strncatz(value, va("b=RR:0\n"), valuelen);
Q_strncpyz(tmpstr, con->codecslot[i].name, sizeof(tmpstr));
at = strchr(tmpstr, '@');
if (at)
{
*at = '/';
Q_strncatz(value, va("a=rtpmap:%i %s\n", id, tmpstr), valuelen);
}
else
Q_strncatz(value, va("a=rtpmap:%i %s/%i\n", id, tmpstr, 8000), valuelen);
for (can = con->lc; can; can = can->next)
{
char canline[256];
can->dirty = false; //doesn't matter now.
ICE_CandidateToSDP(can, canline, sizeof(canline));
Q_strncatz(value, canline, valuelen);
Q_strncatz(value, "\n", valuelen);
}
}
}
else
return false;
return true;
}
static void ICE_PrintSummary(struct icestate_s *con, qboolean islisten)
{
char msg[64];
Con_Printf(S_COLOR_GRAY" ^[[%s]\\ice\\%s^]: ", con->friendlyname, con->friendlyname);
switch(con->proto)
{
case ICEP_VOICE: Con_Printf(S_COLOR_GRAY"(voice) "); break;
case ICEP_VIDEO: Con_Printf(S_COLOR_GRAY"(video) "); break;
default: break;
}
switch(con->state)
{
case ICE_INACTIVE: Con_Printf(S_COLOR_RED "inactive"); break;
case ICE_FAILED: Con_Printf(S_COLOR_RED "failed"); break;
case ICE_GATHERING: Con_Printf(S_COLOR_YELLOW "gathering"); break;
case ICE_CONNECTING: Con_Printf(S_COLOR_YELLOW "connecting"); break;
case ICE_CONNECTED: Con_Printf(S_COLOR_GRAY"%s via %s", NET_AdrToString(msg,sizeof(msg), &con->chosenpeer), ICE_NetworkToName(con, con->chosenpeer.connum)); break;
}
#ifdef HAVE_DTLS
if (con->dtlsstate)
Con_Printf(S_COLOR_GREEN " (encrypted%s)", con->sctp?", sctp":"");
else if (con->sctp)
Con_Printf(S_COLOR_RED " (plain-text, sctp)"); //weeeeeeird and pointless...
else
#endif
Con_Printf(S_COLOR_RED " (plain-text)");
Con_Printf("\n");
}
static void ICE_Debug(struct icestate_s *con)
{
const char *addrclass;
struct icecandidate_s *can;
char buf[65536];
int i;
ICE_Get(con, "state", buf, sizeof(buf));
Con_Printf("ICE [%s] (%s):\n", con->friendlyname, buf);
if (con->brokerless)
Con_Printf(" timeout: %g\n", (int)(con->icetimeout-Sys_Milliseconds())/1000.0);
else
{
unsigned int idle = (Sys_Milliseconds()+30*1000 - con->icetimeout);
if (idle > 500)
Con_Printf(" idle: %g\n", idle/1000.0);
}
if (net_ice_debug.ival >= 2)
{ //rather uninteresting really...
if (con->initiator)
ICE_Get(con, "sdpoffer", buf, sizeof(buf));
else
ICE_Get(con, "sdpanswer", buf, sizeof(buf));
Con_Printf("sdp:\n"S_COLOR_YELLOW "%s\n", buf);
//incomplete anyway
ICE_Get(con, "peersdp", buf, sizeof(buf));
Con_Printf("peer:\n"S_COLOR_YELLOW"%s\n", buf);
}
Con_Printf(" servers:\n");
for (i = 0; i < con->servers; i++)
{
const char *status = "?";
switch(con->server[i].state)
{
case TURN_UNINITED: status = "uninited"; break;
case TURN_HAVE_NONCE: status = "registering"; break;
case TURN_ALLOCATED: status = "allocated"; break;
case TURN_TERMINATING: status = "terminating"; break;
}
NET_AdrToString(buf,sizeof(buf), &con->server[i].addr);
Con_Printf(" %s:%s %s realm=%s user=%s auth=%s\n", con->server[i].isstun?"stun":"turn", buf, status, con->server[i].realm, con->server[i].user?con->server[i].user:"<unspecified>", con->server[i].auth?"<hidden>":"<none>");
}
Con_Printf(" local:\n");
for (can = con->lc; can; can = can->next)
{
ICE_CandidateToSDP(can, buf, sizeof(buf));
if (con->chosenpeer.type!=NA_INVALID && con->chosenpeer.connum == can->info.network)
Con_Printf(S_COLOR_GREEN " %s"S_COLOR_GRAY" <chosen>\n", buf);
else if (can->dirty)
Con_Printf(S_COLOR_RED " %s"S_COLOR_GRAY" <not sent>\n", buf);
else
Con_Printf(S_COLOR_YELLOW" %s\n", buf);
}
Con_Printf(" remote:\n");
for (can = con->rc; can; can = can->next)
{
ICE_CandidateToSDP(can, buf, sizeof(buf));
if (can->reachable)
{
if (con->chosenpeer.type!=NA_INVALID && NET_CompareAdr(&can->peer,&con->chosenpeer))
Con_Printf(S_COLOR_GREEN " %s"S_COLOR_GRAY" <chosen>\n", buf);
else
Con_Printf(S_COLOR_YELLOW" %s"S_COLOR_GRAY" <reachable>\n", buf);
}
else if (NET_ClassifyAddress(&can->peer, &addrclass) < ASCOPE_TURN_REQUIRESCOPE)
Con_Printf(S_COLOR_RED" %s"S_COLOR_GRAY" <ignored: %s>\n", buf, addrclass);
else
Con_Printf(S_COLOR_RED" %s"S_COLOR_GRAY" <unreachable>\n", buf);
}
}
static void ICE_Show_f(void)
{
const char *findname = Cmd_Argv(1);
struct icestate_s *ice;
for (ice = icelist; ice; ice = ice->next)
{
if (!*findname || !strcmp(findname, ice->friendlyname))
ICE_Debug(ice);
}
}
static struct icecandinfo_s *QDECL ICE_GetLCandidateInfo(struct icestate_s *con)
{
struct icecandidate_s *can;
for (can = con->lc; can; can = can->next)
{
if (can->dirty)
{
if (ICE_LCandidateIsPrivate(can))
continue;
can->dirty = false;
return &can->info;
}
}
return NULL;
}
static qboolean QDECL ICE_GetLCandidateSDP(struct icestate_s *con, char *out, size_t outsize)
{
struct icecandinfo_s *info = ICE_GetLCandidateInfo(con);
if (info)
{
struct icecandidate_s *can = (struct icecandidate_s*)info;
ICE_CandidateToSDP(can, out, outsize);
return true;
}
return false;
}
static unsigned int ICE_ComputePriority(netadr_t *adr, struct icecandinfo_s *info)
{
int tpref, lpref;
switch(info->type)
{
case ICE_HOST: tpref = 126; break; //ideal
case ICE_PRFLX: tpref = 110; break;
case ICE_SRFLX: tpref = 100; break;
default:
case ICE_RELAY: tpref = 0; break; //relays suck
}
lpref = 0;
if (info->transport == 0)
lpref += 0x8000; //favour udp the most (tcp sucks for stalls)
lpref += (255-info->network)<<16; //favour the first network/socket specified
lpref += (255-NET_ClassifyAddress(adr, NULL))<<8;
if (adr->type == NA_IP)
lpref += 0x0001; //favour ipv4 over ipv6 (less header/mtu overhead...). this is only slight,
return (tpref<<24) + (lpref<<lpref) + ((256 - info->component)<<0);
}
//adrno is 0 if the type is anything but host.
void QDECL ICE_AddLCandidateInfo(struct icestate_s *con, netadr_t *adr, int adrno, int type)
{
int rnd[2];
struct icecandidate_s *cand;
if (!con)
return;
switch(adr->type)
{
case NA_IP:
case NA_IPV6:
switch(NET_ClassifyAddress(adr, NULL))
{
case ASCOPE_PROCESS://doesn't make sense.
case ASCOPE_HOST: //don't waste time asking the relay to poke its loopback. if only because it'll report lots of errors.
return;
case ASCOPE_NET: //public addresses, just add local candidates as normal
break;
case ASCOPE_LINK: //random screwy addresses... hopefully don't need em if we're talking to a broker... no dhcp server is weird.
return;
case ASCOPE_LAN: //private addresses. give them random info instead...
if (net_ice_allowmdns.ival && MDNS_Setup())
{
for (cand = con->lc; cand; cand = cand->next)
{
if (cand->ismdns)
return; //DUPE
}
cand = Z_Malloc(sizeof(*cand));
cand->next = con->lc;
con->lc = cand;
Q_strncpyz(cand->info.addr, mdns_name[con->proto == ICEP_QWSERVER], sizeof(cand->info.addr));
cand->info.port = ntohs(adr->port);
cand->info.type = type;
cand->info.generation = 0;
cand->info.foundation = 1;
cand->info.component = 1;
cand->info.network = adr->connum;
cand->dirty = true;
cand->ismdns = true;
Sys_RandomBytes((void*)rnd, sizeof(rnd));
Q_strncpyz(cand->info.candidateid, va("x%08x%08x", rnd[0], rnd[1]), sizeof(cand->info.candidateid));
cand->info.priority = ICE_ComputePriority(adr, &cand->info);
return;
}
break;
}
break;
default: //bad protocols
return; //no, just no.
}
switch(adr->prot)
{
case NP_DTLS:
case NP_DGRAM:
break;
default:
return; //don't report any tcp/etc connections...
}
for (cand = con->lc; cand; cand = cand->next)
{
if (NET_CompareAdr(adr, &cand->peer))
return; //DUPE
}
cand = Z_Malloc(sizeof(*cand));
cand->next = con->lc;
con->lc = cand;
NET_BaseAdrToString(cand->info.addr, sizeof(cand->info.addr), adr);
cand->info.port = ntohs(adr->port);
cand->info.type = type;
cand->info.generation = 0;
cand->info.foundation = 1;
cand->info.component = 1;
cand->info.network = adr->connum;
cand->dirty = true;
Sys_RandomBytes((void*)rnd, sizeof(rnd));
Q_strncpyz(cand->info.candidateid, va("x%08x%08x", rnd[0], rnd[1]), sizeof(cand->info.candidateid));
cand->info.priority = ICE_ComputePriority(adr, &cand->info);
}
void QDECL ICE_AddLCandidateConn(ftenet_connections_t *col, netadr_t *addr, int type)
{
struct icestate_s *ice;
for (ice = icelist; ice; ice = ice->next)
{
if (ICE_PickConnection(ice) == col)
ICE_AddLCandidateInfo(ice, addr, 0, type);
}
}
static void ICE_Destroy(struct icestate_s *con)
{
struct icecandidate_s *c;
ICE_Set(con, "state", STRINGIFY(ICE_INACTIVE));
#ifdef HAVE_DTLS
if (con->sctp)
{
Z_Free(con->sctp->cookie);
Z_Free(con->sctp);
}
if (con->dtlsstate)
con->dtlsfuncs->DestroyContext(con->dtlsstate);
if (con->cred.local.cert)
Z_Free(con->cred.local.cert);
if (con->cred.local.key)
Z_Free(con->cred.local.key);
#endif
while(con->rc)
{
c = con->rc;
con->rc = c->next;
Z_Free(c);
}
while(con->lc)
{
c = con->lc;
con->lc = c->next;
Z_Free(c);
}
while (con->servers)
{
struct iceserver_s *s = &con->server[--con->servers];
if (s->con)
{ //make sure we tell the TURN server to release our allocation.
s->state = TURN_TERMINATING;
ICE_ToStunServer(con, s);
s->con->Close(s->con);
}
Z_Free(s->user);
Z_Free(s->auth);
Z_Free(s->realm);
Z_Free(s->nonce);
}
if (con->connections)
FTENET_CloseCollection(con->connections);
Z_Free(con->lufrag);
Z_Free(con->lpwd);
Z_Free(con->rufrag);
Z_Free(con->rpwd);
Z_Free(con->friendlyname);
Z_Free(con->conname);
//has already been unlinked
Z_Free(con);
}
//send pings to establish/keep the connection alive
void ICE_Tick(void)
{
struct icestate_s **link, *con;
unsigned int curtime;
if (!icelist)
return;
curtime = Sys_Milliseconds();
MDNS_SendQueries();
for (link = &icelist; (con=*link);)
{
if (con->brokerless)
{
if (con->state <= ICE_GATHERING)
{
*link = con->next;
ICE_Destroy(con);
continue;
}
else if ((signed int)(curtime-con->icetimeout) > 0)
ICE_SetFailed(con, S_COLOR_GRAY"[%s]: ice timeout\n", con->friendlyname);
}
switch(con->mode)
{
case ICEM_RAW:
//raw doesn't do handshakes or keepalives. it should just directly connect.
//raw just uses the first (assumed only) option
if (con->state == ICE_CONNECTING)
{
struct icecandidate_s *rc;
rc = con->rc;
if (!rc || !NET_StringToAdr(rc->info.addr, rc->info.port, &con->chosenpeer))
con->chosenpeer.type = NA_INVALID;
ICE_Set(con, "state", STRINGIFY(ICE_CONNECTED));
}
break;
case ICEM_WEBRTC:
case ICEM_ICE:
if (con->state == ICE_CONNECTING || con->state == ICE_CONNECTED)
{
size_t i, j;
struct iceserver_s *srv;
for (i = 0; i < con->servers; i++)
{
srv = &con->server[i];
if ((signed int)(srv->stunretry-curtime) < 0)
{
srv->stunretry = curtime + 2*1000;
ICE_ToStunServer(con, srv);
}
for (j = 0; j < srv->peers; j++)
{
if ((signed int)(srv->peer[j].retry-curtime) < 0)
{
TURN_AuthorisePeer(con, srv, j);
srv->peer[j].retry = curtime + 2*1000;
}
}
if (srv->con)
{
while (srv->con->GetPacket(srv->con))
{
net_from.connum = srv->con->connum;
net_from_connection = srv->con;
srv->con->owner->ReadGamePacket();
}
}
}
if (con->keepalive < curtime)
{
if (!ICE_SendSpam(con))
{
struct icecandidate_s *rc;
struct icecandidate_s *best = NULL;
for (rc = con->rc; rc; rc = rc->next)
{ //FIXME:
if (rc->reachable && (!best || rc->info.priority > best->info.priority))
best = rc;
}
if (best)
{
netadr_t nb = best->peer;
for (i = 0; ; i++)
{
if (best->reachable&(1<<i))
{
best->tried &= ~(1<<i); //keep poking it...
nb.connum = i+1;
break;
}
}
if (memcmp(&con->chosenpeer, &nb, sizeof(nb)) && (con->chosenpeer.type==NA_INVALID || !con->controlled))
{ //it actually changed... let them know NOW.
best->tried &= ~(1<<(con->chosenpeer.connum-1)); //make sure we resend this one.
con->chosenpeer = nb;
ICE_SendSpam(con);
if (net_ice_debug.ival >= 1)
{
char msg[64];
Con_Printf(S_COLOR_GRAY"[%s]: New peer chosen %s (%s), via %s.\n", con->friendlyname, NET_AdrToString(msg, sizeof(msg), &con->chosenpeer), ICE_GetCandidateType(&best->info), ICE_NetworkToName(con, con->chosenpeer.connum));
}
}
}
/*if (con->state == ICE_CONNECTED && best)
{ //once established, back off somewhat
for (rc = con->rc; rc; rc = rc->next)
rc->tried &= ~rc->reachable;
}
else*/
{
for (rc = con->rc; rc; rc = rc->next)
rc->tried = 0;
}
con->retries++;
if (con->retries > 32)
con->retries = 32;
con->keepalive = curtime + 200*(con->retries); //RTO... ish.
}
else
con->keepalive = curtime + 50*(con->retries+1); //Ta... absmin of 5ms
}
}
if (con->state == ICE_CONNECTED)
{
#ifdef HAVE_DTLS
if (con->sctp)
SCTP_Transmit(con->sctp, NULL,0); //try to keep it ticking...
if (con->dtlsstate)
con->dtlsfuncs->Timeouts(con->dtlsstate);
#endif
//FIXME: We should be sending a stun binding indication every 15 secs with a fingerprint attribute
}
break;
}
link = &con->next;
}
}
static void QDECL ICE_Close(struct icestate_s *con, qboolean force)
{
struct icestate_s **link;
for (link = &icelist; *link; )
{
if (con == *link)
{
if (!force)
con->brokerless = true;
else
{
*link = con->next;
ICE_Destroy(con);
}
return;
}
else
link = &(*link)->next;
}
}
static void QDECL ICE_CloseModule(void *module)
{
struct icestate_s **link, *con;
for (link = &icelist; *link; )
{
con = *link;
if (con->module == module)
{
*link = con->next;
ICE_Destroy(con);
}
else
link = &(*link)->next;
}
}
icefuncs_t iceapi =
{
ICE_Create,
ICE_Set,
ICE_Get,
ICE_GetLCandidateInfo,
ICE_AddRCandidateInfo,
ICE_Close,
ICE_CloseModule,
ICE_GetLCandidateSDP,
ICE_Find
};
#endif
#if defined(SUPPORT_ICE) && defined(HAVE_DTLS)
//========================================
//WebRTC's interpretation of SCTP. its annoying, but hey its only 28 wasted bytes... along with the dtls overhead too. most of this is redundant.
//we only send unreliably.
//there's no point in this code without full webrtc code.
struct sctp_header_s
{
quint16_t srcport;
quint16_t dstport;
quint32_t verifycode;
quint32_t crc;
};
struct sctp_chunk_s
{
qbyte type;
#define SCTP_TYPE_DATA 0
#define SCTP_TYPE_INIT 1
#define SCTP_TYPE_INITACK 2
#define SCTP_TYPE_SACK 3
#define SCTP_TYPE_PING 4
#define SCTP_TYPE_PONG 5
#define SCTP_TYPE_ABORT 6
#define SCTP_TYPE_SHUTDOWN 7
#define SCTP_TYPE_SHUTDOWNACK 8
#define SCTP_TYPE_ERROR 9
#define SCTP_TYPE_COOKIEECHO 10
#define SCTP_TYPE_COOKIEACK 11
#define SCTP_TYPE_SHUTDOWNDONE 14
#define SCTP_TYPE_PAD 132
#define SCTP_TYPE_FORWARDTSN 192
qbyte flags;
quint16_t length;
//value...
};
struct sctp_chunk_data_s
{
struct sctp_chunk_s chunk;
quint32_t tsn;
quint16_t sid;
quint16_t seq;
quint32_t ppid;
#define SCTP_PPID_DCEP 50 //datachannel establishment protocol
#define SCTP_PPID_DATA 53 //our binary quake data.
};
struct sctp_chunk_init_s
{
struct sctp_chunk_s chunk;
quint32_t verifycode;
quint32_t arwc;
quint16_t numoutstreams;
quint16_t numinstreams;
quint32_t tsn;
};
struct sctp_chunk_sack_s
{
struct sctp_chunk_s chunk;
quint32_t tsn;
quint32_t arwc;
quint16_t gaps;
quint16_t dupes;
/*struct {
quint16_t first;
quint16_t last;
} gapblocks[]; //actually received rather than gaps, but same meaning.
quint32_t dupe_tsns[];*/
};
struct sctp_chunk_fwdtsn_s
{
struct sctp_chunk_s chunk;
quint32_t tsn;
/*struct
{
quint16_t sid;
quint16_t seq;
} streams[];*/
};
static neterr_t SCTP_PeerSendPacket(sctp_t *sctp, int length, const void *data)
{ //sends to the dtls layer (which will send to the generic ice dispatcher that'll send to the dgram stuff... layers on layers.
struct icestate_s *peer = sctp->icestate;
if (sctp->dtlsstate)
return sctp->dtlsfuncs->Transmit(sctp->dtlsstate, data, length);
else if (peer)
{
if (peer->dtlsstate)
return peer->dtlsfuncs->Transmit(peer->dtlsstate, data, length);
else if (peer->chosenpeer.type != NA_INVALID)
return ICE_Transmit(peer, data, length);
else if (peer->state < ICE_CONNECTING)
return NETERR_DISCONNECTED;
else
return NETERR_CLOGGED;
}
else
return NETERR_NOROUTE;
}
static quint32_t SCTP_Checksum(const struct sctp_header_s *h, size_t size)
{
int k;
const qbyte *buf = (const qbyte*)h;
size_t ofs;
uint32_t crc = 0xFFFFFFFF;
for (ofs = 0; ofs < size; ofs++)
{
if (ofs >= 8 && ofs < 8+4)
; //the header's crc should be read as 0.
else
crc ^= buf[ofs];
for (k = 0; k < 8; k++) //CRC-32C polynomial 0x1EDC6F41 in reversed bit order.
crc = crc & 1 ? (crc >> 1) ^ 0x82f63b78 : crc >> 1;
}
return ~crc;
}
neterr_t SCTP_Transmit(sctp_t *sctp, const void *data, size_t length)
{
qbyte pkt[65536];
size_t pktlen = 0;
struct sctp_header_s *h = (void*)pkt;
struct sctp_chunk_data_s *d;
struct sctp_chunk_fwdtsn_s *fwd;
if (length > sizeof(pkt))
return NETERR_MTU;
h->dstport = sctp->peerport;
h->srcport = sctp->myport;
h->verifycode = sctp->o.verifycode;
pktlen += sizeof(*h);
//advance our ctsn if we're received the relevant packets
while(sctp->i.htsn)
{
quint32_t tsn = sctp->i.ctsn+1;
if (!(sctp->i.received[(tsn>>3)%sizeof(sctp->i.received)] & 1<<(tsn&7)))
break;
//advance our cumulative ack.
sctp->i.received[(tsn>>3)%sizeof(sctp->i.received)] &= ~(1<<(tsn&7));
sctp->i.ctsn = tsn;
sctp->i.htsn--;
}
if (!sctp->o.writable)
{
double time = Sys_DoubleTime();
if (time > sctp->nextreinit)
{
sctp->nextreinit = time + 0.5;
if (!sctp->cookie)
{
struct sctp_chunk_init_s *init = (struct sctp_chunk_init_s*)&pkt[pktlen];
struct {
quint16_t ptype;
quint16_t plen;
} *ftsn = (void*)(init+1);
h->verifycode = 0;
init->chunk.type = SCTP_TYPE_INIT;
init->chunk.flags = 0;
init->chunk.length = BigShort(sizeof(*init)+sizeof(*ftsn));
init->verifycode = sctp->i.verifycode;
init->arwc = BigLong(65535);
init->numoutstreams = BigShort(2);
init->numinstreams = BigShort(2);
init->tsn = BigLong(sctp->o.tsn);
ftsn->ptype = BigShort(49152);
ftsn->plen = BigShort(sizeof(*ftsn));
pktlen += sizeof(*init) + sizeof(*ftsn);
h->crc = SCTP_Checksum(h, pktlen);
return SCTP_PeerSendPacket(sctp, pktlen, h);
}
else
{
struct sctp_chunk_s *cookie = (struct sctp_chunk_s*)&pkt[pktlen];
if (pktlen + sizeof(*cookie) + sctp->cookiesize > sizeof(pkt))
return NETERR_DISCONNECTED;
cookie->type = SCTP_TYPE_COOKIEECHO;
cookie->flags = 0;
cookie->length = BigShort(sizeof(*cookie)+sctp->cookiesize);
memcpy(cookie+1, sctp->cookie, sctp->cookiesize);
pktlen += sizeof(*cookie) + sctp->cookiesize;
h->crc = SCTP_Checksum(h, pktlen);
return SCTP_PeerSendPacket(sctp, pktlen, h);
}
}
return NETERR_CLOGGED; //nope, not ready yet
}
if (sctp->peerhasfwdtsn && (int)(sctp->o.ctsn-sctp->o.tsn) < -5 && sctp->o.losttsn)
{
fwd = (struct sctp_chunk_fwdtsn_s*)&pkt[pktlen];
fwd->chunk.type = SCTP_TYPE_FORWARDTSN;
fwd->chunk.flags = 0;
fwd->chunk.length = BigShort(sizeof(*fwd));
fwd->tsn = BigLong(sctp->o.tsn-1);
//we only send unordered unreliables, so this stream stuff here is irrelevant.
// fwd->streams[0].sid = sctp->qstreamid;
// fwd->streams[0].seq = BigShort(0);
pktlen += sizeof(*fwd);
}
if (sctp->i.ackneeded >= 2)
{
struct sctp_chunk_sack_s *rsack;
struct sctp_chunk_sack_gap_s {
uint16_t first;
uint16_t last;
} *rgap;
quint32_t otsn;
rsack = (struct sctp_chunk_sack_s*)&pkt[pktlen];
rsack->chunk.type = SCTP_TYPE_SACK;
rsack->chunk.flags = 0;
rsack->chunk.length = BigShort(sizeof(*rsack));
rsack->tsn = BigLong(sctp->i.ctsn);
rsack->arwc = BigLong(65535);
rsack->gaps = 0;
rsack->dupes = BigShort(0);
pktlen += sizeof(*rsack);
rgap = (struct sctp_chunk_sack_gap_s*)&pkt[pktlen];
for (otsn = 0; otsn < sctp->i.htsn; otsn++)
{
quint32_t tsn = sctp->i.ctsn+otsn;
if (!(sctp->i.received[(tsn>>3)%sizeof(sctp->i.received)] & 1<<(tsn&7)))
continue; //missing, don't report it in the 'gaps'... yeah, backwards naming.
if (rsack->gaps && rgap[-1].last == otsn-1)
rgap[-1].last = otsn; //merge into the last one.
else
{
rgap->first = otsn; //these values are Offset from the Cumulative TSN, to save storage.
rgap->last = otsn;
rgap++;
rsack->gaps++;
pktlen += sizeof(*rgap);
if (pktlen >= 500)
break; //might need fragmentation... just stop here.
}
}
for (otsn = 0, rgap = (struct sctp_chunk_sack_gap_s*)&pkt[pktlen]; otsn < rsack->gaps; otsn++)
{
rgap[otsn].first = BigShort(rgap[otsn].first);
rgap[otsn].last = BigShort(rgap[otsn].last);
}
rsack->gaps = BigShort(rsack->gaps);
sctp->i.ackneeded = 0;
}
if (pktlen + sizeof(*d) + length >= 500 && length && pktlen != sizeof(*h))
{ //probably going to result in fragmentation issues. send separate packets.
h->crc = SCTP_Checksum(h, pktlen);
SCTP_PeerSendPacket(sctp, pktlen, h);
//reset to the header
pktlen = sizeof(*h);
}
if (length)
{
d = (void*)&pkt[pktlen];
d->chunk.type = SCTP_TYPE_DATA;
d->chunk.flags = 3|4;
d->chunk.length = BigShort(sizeof(*d) + length);
d->tsn = BigLong(sctp->o.tsn++);
d->sid = sctp->qstreamid;
d->seq = BigShort(0); //not needed for unordered
d->ppid = BigLong(SCTP_PPID_DATA);
memcpy(d+1, data, length);
pktlen += sizeof(*d) + length;
//chrome insists on pointless padding at the end. its annoying.
while(pktlen&3)
pkt[pktlen++]=0;
}
if (pktlen == sizeof(*h))
return NETERR_SENT; //nothing to send...
h->crc = SCTP_Checksum(h, pktlen);
return SCTP_PeerSendPacket(sctp, pktlen, h);
}
static void SCTP_DecodeDCEP(sctp_t *sctp, qbyte *resp)
{ //send an ack...
size_t pktlen = 0;
struct sctp_header_s *h = (void*)resp;
struct sctp_chunk_data_s *d;
char *data = "\02";
size_t length = 1; //*sigh*...
struct
{
qbyte type;
qbyte chantype;
quint16_t priority;
quint32_t relparam;
quint16_t labellen;
quint16_t protocollen;
} *dcep = (void*)sctp->i.r.buf;
if (dcep->type == 3)
{
char *label = (qbyte*)(dcep+1);
char *prot = label + strlen(label)+1;
sctp->qstreamid = sctp->i.r.sid;
if (net_ice_debug.ival >= 1)
Con_Printf(S_COLOR_GRAY"[%s]: New SCTP Channel: \"%s\" (%s)\n", sctp->friendlyname, label, prot);
h->dstport = sctp->peerport;
h->srcport = sctp->myport;
h->verifycode = sctp->o.verifycode;
pktlen += sizeof(*h);
pktlen = (pktlen+3)&~3; //pad.
d = (void*)&resp[pktlen];
d->chunk.type = SCTP_TYPE_DATA;
d->chunk.flags = 3;
d->chunk.length = BigShort(sizeof(*d) + length);
d->tsn = BigLong(sctp->o.tsn++);
d->sid = sctp->qstreamid;
d->seq = BigShort(0); //not needed for unordered
d->ppid = BigLong(SCTP_PPID_DCEP);
memcpy(d+1, data, length);
pktlen += sizeof(*d) + length;
h->crc = SCTP_Checksum(h, pktlen);
SCTP_PeerSendPacket(sctp, pktlen, h);
}
}
struct sctp_errorcause_s
{
quint16_t cause;
quint16_t length;
};
static void SCTP_ErrorChunk(sctp_t *sctp, const char *errortype, struct sctp_errorcause_s *s, size_t totallen)
{
quint16_t cc, cl;
while(totallen > 0)
{
if (totallen < sizeof(*s))
return; //that's an error in its own right
cc = BigShort(s->cause);
cl = BigShort(s->length);
if (totallen < cl)
return; //err..
if (net_ice_debug.ival >= 1) switch(cc)
{
case 1: Con_Printf(S_COLOR_GRAY"[%s]: SCTP %s: Invalid Stream Identifier\n", sctp->friendlyname, errortype); break;
case 2: Con_Printf(S_COLOR_GRAY"[%s]: SCTP %s: Missing Mandatory Parameter\n", sctp->friendlyname, errortype); break;
case 3: Con_Printf(S_COLOR_GRAY"[%s]: SCTP %s: Stale Cookie Error\n", sctp->friendlyname, errortype); break;
case 4: Con_Printf(S_COLOR_GRAY"[%s]: SCTP %s: Out of Resource\n", sctp->friendlyname, errortype); break;
case 5: Con_Printf(S_COLOR_GRAY"[%s]: SCTP %s: Unresolvable Address\n", sctp->friendlyname, errortype); break;
case 6: Con_Printf(S_COLOR_GRAY"[%s]: SCTP %s: Unrecognized Chunk Type\n", sctp->friendlyname, errortype); break;
case 7: Con_Printf(S_COLOR_GRAY"[%s]: SCTP %s: Invalid Mandatory Parameter\n", sctp->friendlyname, errortype); break;
case 8: Con_Printf(S_COLOR_GRAY"[%s]: SCTP %s: Unrecognized Parameters\n", sctp->friendlyname, errortype); break;
case 9: Con_Printf(S_COLOR_GRAY"[%s]: SCTP %s: No User Data\n", sctp->friendlyname, errortype); break;
case 10: Con_Printf(S_COLOR_GRAY"[%s]: SCTP %s: Cookie Received While Shutting Down\n", sctp->friendlyname, errortype); break;
case 11: Con_Printf(S_COLOR_GRAY"[%s]: SCTP %s: Restart of an Association with New Addresses\n", sctp->friendlyname, errortype); break;
case 12: Con_Printf(S_COLOR_GRAY"[%s]: SCTP %s: User Initiated Abort\n", sctp->friendlyname, errortype); break;
case 13: Con_Printf(S_COLOR_GRAY"[%s]: SCTP %s: Protocol Violation [%s]\n", sctp->friendlyname, errortype, (char*)(s+1)); break;
default: Con_Printf(S_COLOR_GRAY"[%s]: SCTP %s: Unknown Reason\n", sctp->friendlyname, errortype); break;
}
totallen -= cl;
totallen &= ~3;
s = (struct sctp_errorcause_s*)((qbyte*)s + ((cl+3)&~3));
}
}
void SCTP_Decode(sctp_t *sctp, ftenet_connections_t *col)
{
qbyte resp[4096];
qbyte *msg = net_message.data;
qbyte *msgend = msg+net_message.cursize;
struct sctp_header_s *h = (struct sctp_header_s*)msg;
struct sctp_chunk_s *c = (struct sctp_chunk_s*)(h+1);
quint16_t clen;
if ((qbyte*)c+1 > msgend)
return; //runt
if (h->dstport != sctp->myport)
return; //not for us...
if (h->srcport != sctp->peerport)
return; //not from them... we could check for a INIT but its over dtls anyway so why give a damn.
if (h->verifycode != ((c->type == SCTP_TYPE_INIT)?0:sctp->i.verifycode))
return; //wrong cookie... (be prepared to parse dupe inits if our ack got lost...
if (h->crc != SCTP_Checksum(h, net_message.cursize))
return; //crc wrong. assume corruption.
if (net_message.cursize&3)
{
if (net_ice_debug.ival >= 2)
Con_Printf(S_COLOR_GRAY"[%s]: SCTP: packet not padded\n", sctp->friendlyname);
return; //mimic chrome, despite it being pointless.
}
//passed the simple header checks, spend a memcpy...
msg = alloca(net_message.cursize);
memcpy(msg, net_message.data, net_message.cursize);
msgend = msg+net_message.cursize;
h = (struct sctp_header_s*)msg;
c = (struct sctp_chunk_s*)(h+1);
while ((qbyte*)(c+1) <= msgend)
{
clen = BigShort(c->length);
if ((qbyte*)c + clen > msgend || clen < sizeof(*c))
{
Con_Printf(CON_ERROR"Corrupt SCTP message\n");
break;
}
safeswitch(c->type)
{
case SCTP_TYPE_DATA:
if (clen >= sizeof(struct sctp_chunk_data_s))
{
struct sctp_chunk_data_s *dc = (void*)c;
quint32_t tsn = BigLong(dc->tsn), u;
qint32_t adv = tsn - sctp->i.ctsn;
sctp->i.ackneeded++;
if (adv >= SCTP_RCVSIZE)
{
if (net_ice_debug.ival >= 1)
Con_Printf(S_COLOR_GRAY"[%s]: SCTP: Future Packet\n", sctp->friendlyname);/*too far in the future. we can't track such things*/
}
else if (adv <= 0)
{
if (net_ice_debug.ival >= 2)
Con_Printf(S_COLOR_GRAY"[%s]: SCTP: PreCumulative\n", sctp->friendlyname);/*already acked this*/
}
else if (sctp->i.received[(tsn>>3)%sizeof(sctp->i.received)] & 1<<(tsn&7))
{
if (net_ice_debug.ival >= 2)
Con_DPrintf(S_COLOR_GRAY"[%s]: SCTP: Dupe\n", sctp->friendlyname);/*already processed it. FIXME: Make a list for the next SACK*/
}
else
{
qboolean err = false;
if (c->flags & 2)
{ //beginning...
sctp->i.r.firsttns = tsn;
sctp->i.r.tsn = tsn;
sctp->i.r.size = 0;
sctp->i.r.ppid = dc->ppid;
sctp->i.r.sid = dc->sid;
sctp->i.r.seq = dc->seq;
sctp->i.r.toobig = false;
}
else
{
if (sctp->i.r.tsn != tsn || sctp->i.r.ppid != dc->ppid)
err = true;
}
if (err)
; //don't corrupt anything in case we get a quick resend that fixes it.
else
{
size_t dlen = clen-sizeof(*dc);
if (adv > sctp->i.htsn) //weird maths in case it wraps.
sctp->i.htsn = adv;
sctp->i.r.tsn++;
if (sctp->i.r.size + dlen > sizeof(sctp->i.r.buf))
{
if (net_ice_debug.ival >= 2)
Con_Printf(S_COLOR_GRAY"[%s]: SCTP: Oversized\n", sctp->friendlyname);
sctp->i.r.toobig = true; //reassembled packet was too large, just corrupt it.
}
else
{
memcpy(sctp->i.r.buf+sctp->i.r.size, dc+1, dlen); //include the dc header
sctp->i.r.size += dlen;
}
if (c->flags & 1) //an ending. we have the complete packet now.
{
for (u = sctp->i.r.tsn - sctp->i.r.firsttns; u --> 0; )
{
tsn = sctp->i.r.firsttns + u;
sctp->i.received[(tsn>>3)%sizeof(sctp->i.received)] |= 1<<(tsn&7);
}
if (sctp->i.r.toobig)
;/*ignore it when it cannot be handled*/
else if (sctp->i.r.ppid == BigLong(SCTP_PPID_DATA))
{
memmove(net_message.data, sctp->i.r.buf, sctp->i.r.size);
net_message.cursize = sctp->i.r.size;
col->ReadGamePacket();
if (net_message.cursize != sctp->i.r.size)
{
net_message.cursize = 0;
return; //something weird happened...
}
}
else if (sctp->i.r.ppid == BigLong(SCTP_PPID_DCEP))
SCTP_DecodeDCEP(sctp, resp);
}
}
//FIXME: we don't handle reordering properly at all.
// if (c->flags & 4)
// Con_Printf("\tUnordered\n");
// Con_Printf("\tStream Id %i\n", BigShort(dc->sid));
// Con_Printf("\tStream Seq %i\n", BigShort(dc->seq));
// Con_Printf("\tPPID %i\n", BigLong(dc->ppid));
}
}
break;
case SCTP_TYPE_INIT:
case SCTP_TYPE_INITACK:
if (clen >= sizeof(struct sctp_chunk_init_s))
{
qboolean isack = c->type==SCTP_TYPE_INITACK;
struct sctp_chunk_init_s *init = (void*)c;
struct {
quint16_t ptype;
quint16_t plen;
} *p = (void*)(init+1);
sctp->i.ctsn = BigLong(init->tsn)-1;
sctp->i.htsn = 0;
sctp->o.verifycode = init->verifycode;
(void)BigLong(init->arwc);
(void)BigShort(init->numoutstreams);
(void)BigShort(init->numinstreams);
while ((qbyte*)p+sizeof(*p) <= (qbyte*)c+clen)
{
unsigned short ptype = BigShort(p->ptype);
unsigned short plen = BigShort(p->plen);
switch(ptype)
{
case 7: //init cookie
if (sctp->cookie)
Z_Free(sctp->cookie);
sctp->cookiesize = plen - sizeof(*p);
sctp->cookie = Z_Malloc(sctp->cookiesize);
memcpy(sctp->cookie, p+1, sctp->cookiesize);
break;
case 32773: //Padding
case 32776: //ASCONF
break;
case 49152:
sctp->peerhasfwdtsn = true;
break;
default:
if (net_ice_debug.ival >= 2)
Con_Printf(S_COLOR_GRAY"[%s]: SCTP: Found unknown init parameter %i||%#x\n", sctp->friendlyname, ptype, ptype);
if (ptype&0x4000)
; //FIXME: SCTP_TYPE_ERROR(6,"Unrecognized Chunk Type")
if (!(ptype&0x8000))
return; //'do not process nay further chunks'
//otherwise parse the next as normal.
break;
}
p = (void*)((qbyte*)p + ((plen+3)&~3));
}
if (isack)
{
sctp->nextreinit = 0;
if (sctp->cookie)
SCTP_Transmit(sctp, NULL, 0); //make sure we send acks occasionally even if we have nothing else to say.
}
else
{
struct sctp_header_s *rh = (void*)resp;
struct sctp_chunk_init_s *rinit = (void*)(rh+1);
struct {
quint16_t ptype;
quint16_t plen;
struct {
qbyte data[16];
} cookie;
} *rinitcookie = (void*)(rinit+1);
struct {
quint16_t ptype;
quint16_t plen;
} *rftsn = (void*)(rinitcookie+1);
qbyte *end = sctp->peerhasfwdtsn?(void*)(rftsn+1):(void*)(rinitcookie+1);
rh->srcport = sctp->myport;
rh->dstport = sctp->peerport;
rh->verifycode = sctp->o.verifycode;
rh->crc = 0;
*rinit = *init;
rinit->chunk.type = SCTP_TYPE_INITACK;
rinit->chunk.flags = 0;
rinit->chunk.length = BigShort(end-(qbyte*)rinit);
rinit->verifycode = sctp->i.verifycode;
rinit->arwc = BigLong(65536);
rinit->numoutstreams = init->numoutstreams;
rinit->numinstreams = init->numinstreams;
rinit->tsn = BigLong(sctp->o.tsn);
rinitcookie->ptype = BigShort(7);
rinitcookie->plen = BigShort(sizeof(*rinitcookie));
memcpy(&rinitcookie->cookie, "deadbeefdeadbeef", sizeof(rinitcookie->cookie)); //frankly the contents of the cookie are irrelevant to anything. we've already verified the peer's ice pwd/ufrag stuff as well as their dtls certs etc.
rftsn->ptype = BigShort(49152);
rftsn->plen = BigShort(sizeof(*rftsn));
//complete. calc the proper crc and send it off.
rh->crc = SCTP_Checksum(rh, end-resp);
SCTP_PeerSendPacket(sctp, end-resp, rh);
}
}
break;
case SCTP_TYPE_SACK:
if (clen >= sizeof(struct sctp_chunk_sack_s))
{
struct sctp_chunk_sack_s *sack = (void*)c;
quint32_t tsn = BigLong(sack->tsn);
sctp->o.ctsn = tsn;
sctp->o.losttsn = BigShort(sack->gaps); //if there's a gap then they're telling us they got a later one.
//Con_Printf(CON_ERROR"Sack %#x (%i in flight)\n"
// "\tgaps: %i, dupes %i\n",
// tsn, sctp->o.tsn-tsn,
// BigShort(sack->gaps), BigShort(sack->dupes));
}
break;
case SCTP_TYPE_PING:
if (clen >= sizeof(struct sctp_chunk_s))
{
struct sctp_chunk_s *ping = (void*)c;
struct sctp_header_s *pongh = Z_Malloc(sizeof(*pongh) + clen);
pongh->srcport = sctp->myport;
pongh->dstport = sctp->peerport;
pongh->verifycode = sctp->o.verifycode;
pongh->crc = 0;
memcpy(pongh+1, ping, clen);
((struct sctp_chunk_s*)(pongh+1))->type = SCTP_TYPE_PONG;
//complete. calc the proper crc and send it off.
pongh->crc = SCTP_Checksum(pongh, sizeof(*pongh) + clen);
SCTP_PeerSendPacket(sctp, sizeof(*pongh) + clen, pongh);
Z_Free(pongh);
}
break;
// case SCTP_TYPE_PONG: //we don't send pings
case SCTP_TYPE_ABORT:
SCTP_ErrorChunk(sctp, "Abort", (struct sctp_errorcause_s*)(c+1), clen-sizeof(*c));
if (sctp->icestate)
ICE_SetFailed(sctp->icestate, "SCTP Abort");
break;
case SCTP_TYPE_SHUTDOWN: //FIXME. we should send an ack...
if (sctp->icestate)
ICE_SetFailed(sctp->icestate, "SCTP Shutdown");
if (net_ice_debug.ival >= 1)
Con_Printf(S_COLOR_GRAY"[%s]: SCTP: Shutdown\n", sctp->friendlyname);
break;
// case SCTP_TYPE_SHUTDOWNACK: //we don't send shutdowns, cos we're lame like that...
case SCTP_TYPE_ERROR:
//not fatal...
SCTP_ErrorChunk(sctp, "Error", (struct sctp_errorcause_s*)(c+1), clen-sizeof(*c));
break;
case SCTP_TYPE_COOKIEECHO:
if (clen >= sizeof(struct sctp_chunk_s))
{
struct sctp_header_s *rh = (void*)resp;
struct sctp_chunk_s *rack = (void*)(rh+1);
qbyte *end = (void*)(rack+1);
rh->srcport = sctp->myport;
rh->dstport = sctp->peerport;
rh->verifycode = sctp->o.verifycode;
rh->crc = 0;
rack->type = SCTP_TYPE_COOKIEACK;
rack->flags = 0;
rack->length = BigShort(sizeof(*rack));
//complete. calc the proper crc and send it off.
rh->crc = SCTP_Checksum(rh, end-resp);
SCTP_PeerSendPacket(sctp, end-resp, rh);
sctp->o.writable = true; //channel SHOULD now be open for data.
}
break;
case SCTP_TYPE_COOKIEACK:
sctp->o.writable = true; //we know the other end is now open.
break;
case SCTP_TYPE_PAD:
//don't care.
break;
case SCTP_TYPE_FORWARDTSN:
if (clen >= sizeof(struct sctp_chunk_fwdtsn_s))
{
struct sctp_chunk_fwdtsn_s *fwd = (void*)c;
quint32_t tsn = BigLong(fwd->tsn), count;
count = tsn - sctp->i.ctsn;
if ((int)count < 0)
break; //overflow? don't go backwards.
if (count > 1024)
count = 1024; //don't advance too much in one go. we'd block and its probably an error anyway.
while(count --> 0)
{
tsn = ++sctp->i.ctsn;
sctp->i.received[(tsn>>3)%sizeof(sctp->i.received)] &= ~(1<<(tsn&7));
if (sctp->i.htsn)
sctp->i.htsn--;
sctp->i.ackneeded++; //flag for a sack if we actually completed something here.
}
}
break;
// case SCTP_TYPE_SHUTDOWNDONE:
safedefault:
//no idea what this chunk is, just ignore it...
if (net_ice_debug.ival >= 1)
Con_Printf(S_COLOR_GRAY"[%s]: SCTP: Unsupported chunk %i\n", sctp->friendlyname, c->type);
switch (c->type>>6)
{
case 0:
clen = (qbyte*)msgend - (qbyte*)c; //'do not process any further chunks'
break;
case 1:
clen = (qbyte*)msgend - (qbyte*)c; //'do not process any further chunks'
/*FIXME: SCTP_TYPE_ERROR(6,"Unrecognized Chunk Type")*/
break;
case 2:
//silently ignore it
break;
case 3:
//ignore-with-error
/*FIXME: SCTP_TYPE_ERROR(6,"Unrecognized Chunk Type")*/
break;
}
break;
}
c = (struct sctp_chunk_s*)((qbyte*)c + ((clen+3)&~3)); //next chunk is 4-byte aligned.
}
if (sctp->i.ackneeded >= 5)
SCTP_Transmit(sctp, NULL, 0); //make sure we send acks occasionally even if we have nothing else to say.
//we already made sense of it all.
net_message.cursize = 0;
}
#if 0
qboolean SCTP_Handshake(const dtlsfuncs_t *dtlsfuncs, void *dtlsstate, sctp_t **out)
{
const int myport = ICELITE_SCTP_PORT;
struct cookiedata_s
{
qbyte peerhasfwdtsn;
int overifycode;
int iverifycode;
int ictsn;
int otsn;
// int checkcode;
};
//if this is an sctp init packet, send a cookie.
//if its an initack then create the new state.
qbyte resp[4096];
qbyte *msg = net_message.data;
qbyte *msgend = msg+net_message.cursize;
struct sctp_header_s *h = (struct sctp_header_s*)msg;
struct sctp_chunk_s *c = (struct sctp_chunk_s*)(h+1);
quint16_t clen;
if (net_message.cursize&3)
return false;
if ((qbyte*)c+1 > msgend)
return false; //runt
if (h->dstport != htons(myport))
return false; //not for us...
clen = BigShort(c->length);
if ((qbyte*)c + ((clen+3)&~3) == msgend) //don't allow multiple chucks
switch(c->type)
{
default:
return false; //not the right kind of packet.
case SCTP_TYPE_COOKIEECHO:
if (clen == sizeof(struct sctp_chunk_s)+sizeof(struct cookiedata_s))
{
struct cookiedata_s *cookie = (struct cookiedata_s *)(c+1);
sctp_t *sctp;
struct sctp_header_s *rh = (void*)resp;
struct sctp_chunk_s *rack = (void*)(rh+1);
qbyte *end = (void*)(rack+1);
if (h->verifycode == cookie->iverifycode)
if (h->crc == SCTP_Checksum(h, net_message.cursize)) //make sure the crc matches.
{ //looks okay.
NET_AdrToString(resp, sizeof(resp), &net_from);
*out = sctp = Z_Malloc(sizeof(*sctp) + strlen(resp));
sctp->friendlyname = strcpy((char*)(sctp+1), resp);
sctp->icestate = NULL;
sctp->dtlsfuncs = dtlsfuncs;
sctp->dtlsstate = dtlsstate;
sctp->myport = h->dstport;
sctp->peerport = h->srcport;
sctp->o.tsn = cookie->otsn;
sctp->i.ctsn = cookie->ictsn;
sctp->o.verifycode = cookie->overifycode;
sctp->i.verifycode = cookie->iverifycode;
sctp->peerhasfwdtsn = cookie->peerhasfwdtsn;
sctp->o.writable = true; //channel SHOULD now be open for data (once it gets the ack anyway).
//let our peer know too.
rh->srcport = sctp->myport;
rh->dstport = sctp->peerport;
rh->verifycode = sctp->o.verifycode;
rh->crc = 0;
rack->type = SCTP_TYPE_COOKIEACK;
rack->flags = 0;
rack->length = BigShort(sizeof(*rack));
//complete. calc the proper crc and send it off.
rh->crc = SCTP_Checksum(rh, end-resp);
SCTP_PeerSendPacket(sctp, end-resp, rh);
return true;
}
}
break;
case SCTP_TYPE_INIT:
if (h->verifycode == 0) //this must be 0 for inits.
if (clen >= sizeof(struct sctp_chunk_init_s))
if (h->crc == SCTP_Checksum(h, net_message.cursize)) //make sure the crc matches.
{
struct sctp_chunk_init_s *init = (void*)c;
struct {
quint16_t ptype;
quint16_t plen;
} *p = (void*)(init+1);
struct cookiedata_s cookie = {0};
cookie.otsn = rand() ^ (rand()<<16);
Sys_RandomBytes((void*)&cookie.iverifycode, sizeof(cookie.iverifycode));
cookie.ictsn = BigLong(init->tsn)-1;
cookie.overifycode = init->verifycode;
(void)BigLong(init->arwc);
(void)BigShort(init->numoutstreams);
(void)BigShort(init->numinstreams);
while ((qbyte*)p+sizeof(*p) <= (qbyte*)c+clen)
{
unsigned short ptype = BigShort(p->ptype);
unsigned short plen = BigShort(p->plen);
switch(ptype)
{
case 32776: //ASCONF
break;
case 49152:
cookie.peerhasfwdtsn = true;
break;
default:
if (net_ice_debug.ival >= 2)
Con_Printf(S_COLOR_GRAY"[%s]: SCTP: Found unknown init parameter %i||%#x\n", NET_AdrToString(resp,sizeof(resp), &net_from), ptype, ptype);
break;
}
p = (void*)((qbyte*)p + ((plen+3)&~3));
}
{
struct sctp_header_s *rh = (void*)resp;
struct sctp_chunk_init_s *rinit = (void*)(rh+1);
struct {
quint16_t ptype;
quint16_t plen;
struct cookiedata_s cookie;
} *rinitcookie = (void*)(rinit+1);
struct {
quint16_t ptype;
quint16_t plen;
} *rftsn = (void*)(rinitcookie+1);
qbyte *end = cookie.peerhasfwdtsn?(void*)(rftsn+1):(void*)(rinitcookie+1);
rh->srcport = h->dstport;
rh->dstport = h->srcport;
rh->verifycode = init->verifycode;
rh->crc = 0;
*rinit = *init;
rinit->chunk.type = SCTP_TYPE_INITACK;
rinit->chunk.flags = 0;
rinit->chunk.length = BigShort(end-(qbyte*)rinit);
rinit->verifycode = cookie.iverifycode;
rinit->arwc = BigLong(65536);
rinit->numoutstreams = init->numoutstreams;
rinit->numinstreams = init->numinstreams;
rinit->tsn = BigLong(cookie.otsn);
rinitcookie->ptype = BigShort(7);
rinitcookie->plen = BigShort(sizeof(*rinitcookie));
memcpy(&rinitcookie->cookie, &cookie, sizeof(rinitcookie->cookie)); //frankly the contents of the cookie are irrelevant to anything. we've already verified the peer's ice pwd/ufrag stuff as well as their dtls certs etc.
rftsn->ptype = BigShort(49152);
rftsn->plen = BigShort(sizeof(*rftsn));
//complete. calc the proper crc and send it off.
rh->crc = SCTP_Checksum(rh, end-resp);
dtlsfuncs->Transmit(dtlsstate, resp, end-resp);
}
return true;
}
break;
}
return false;
}
#endif
//========================================
#endif
#if defined(SUPPORT_ICE) || (defined(MASTERONLY) && defined(AVAIL_ZLIB))
qboolean ICE_WasStun(ftenet_connections_t *col)
{
#ifdef SUPPORT_ICE
if (net_from.type == NA_ICE)
return false; //this stuff over an ICE connection doesn't make sense.
#endif
// if (net_from.prot != NP_DGRAM)
// return false;
#if defined(HAVE_CLIENT) && defined(VOICECHAT)
if (col == cls.sockets)
{
if (NET_RTP_Parse())
return true;
}
#endif
if ((net_from.type == NA_IP || net_from.type == NA_IPV6) && net_message.cursize >= 20 && *net_message.data<2)
{
stunhdr_t *stun = (stunhdr_t*)net_message.data;
int stunlen = BigShort(stun->msglen);
#ifdef SUPPORT_ICE
if ((stun->msgtype == BigShort(STUN_BINDING|STUN_REPLY) || stun->msgtype == BigShort(STUN_BINDING|STUN_ERROR)) && net_message.cursize == stunlen + sizeof(*stun))
{
//binding reply (or error)
netadr_t adr = net_from;
char xor[16];
short portxor;
stunattr_t *attr = (stunattr_t*)(stun+1);
int alen;
unsigned short attrval;
int err = 0;
char errmsg[64];
*errmsg = 0;
adr.type = NA_INVALID;
while(stunlen)
{
stunlen -= sizeof(*attr);
alen = (unsigned short)BigShort(attr->attrlen);
if (alen > stunlen)
return false;
stunlen -= (alen+3)&~3;
attrval = BigShort(attr->attrtype);
switch(attrval)
{
case STUNATTR_USERNAME:
case STUNATTR_MSGINTEGRITIY_SHA1:
break;
default:
if (attrval & 0x8000)
break; //okay to ignore
return true;
case STUNATTR_MAPPED_ADDRESS:
if (adr.type != NA_INVALID)
break; //ignore it if we already got an address...
//fallthrough
case STUNATTR_XOR_MAPPED_ADDRESS:
if (attrval == STUNATTR_XOR_MAPPED_ADDRESS)
{
portxor = *(short*)&stun->magiccookie;
memcpy(xor, &stun->magiccookie, sizeof(xor));
}
else
{
portxor = 0;
memset(xor, 0, sizeof(xor));
}
if (alen == 8 && ((qbyte*)attr)[5] == 1) //ipv4
{
adr.type = NA_IP;
adr.port = (((short*)attr)[3]) ^ portxor;
*(int*)adr.address.ip = *(int*)(&((qbyte*)attr)[8]) ^ *(int*)xor;
}
else if (alen == 20 && ((qbyte*)attr)[5] == 2) //ipv6
{
adr.type = NA_IPV6;
adr.port = (((short*)attr)[3]) ^ portxor;
((int*)adr.address.ip6)[0] = ((int*)&((qbyte*)attr)[8])[0] ^ ((int*)xor)[0];
((int*)adr.address.ip6)[1] = ((int*)&((qbyte*)attr)[8])[1] ^ ((int*)xor)[1];
((int*)adr.address.ip6)[2] = ((int*)&((qbyte*)attr)[8])[2] ^ ((int*)xor)[2];
((int*)adr.address.ip6)[3] = ((int*)&((qbyte*)attr)[8])[3] ^ ((int*)xor)[3];
}
break;
case STUNATTR_ERROR_CODE:
{
unsigned short len = BigShort(attr->attrlen)-4;
if (len > sizeof(errmsg)-1)
len = sizeof(errmsg)-1;
memcpy(errmsg, &((qbyte*)attr)[8], len);
errmsg[len] = 0;
if (err==0)
err = (((qbyte*)attr)[6]*100) + (((qbyte*)attr)[7]%100);
}
break;
}
alen = (alen+3)&~3;
attr = (stunattr_t*)((char*)(attr+1) + alen);
}
if (err)
{
char sender[256];
if (net_ice_debug.ival >= 1)
Con_Printf("%s: Stun error code %u : %s\n", NET_AdrToString(sender, sizeof(sender), &net_from), err, errmsg);
}
else if (adr.type!=NA_INVALID && !err)
{
struct icestate_s *con;
for (con = icelist; con; con = con->next)
{
struct icecandidate_s *rc;
size_t i;
struct iceserver_s *s;
if (con->mode == ICEM_RAW)
continue;
for (i = 0; i < con->servers; i++)
{
s = &con->server[i];
if (NET_CompareAdr(&net_from, &s->addr) && s->stunrnd[0] == stun->transactid[0] &&
s->stunrnd[1] == stun->transactid[1] &&
s->stunrnd[2] == stun->transactid[2])
{ //check to see if this is a new server-reflexive address, which happens when the peer is behind a nat.
for (rc = con->lc; rc; rc = rc->next)
{
if (NET_CompareAdr(&adr, &rc->peer))
break;
}
if (!rc)
{
//netadr_t reladdr;
//int relflags;
//const char *relpath;
int rnd[2];
struct icecandidate_s *src; //server Reflexive Candidate
char str[256];
src = Z_Malloc(sizeof(*src));
src->next = con->lc;
con->lc = src;
src->peer = adr;
NET_BaseAdrToString(src->info.addr, sizeof(src->info.addr), &adr);
src->info.port = ntohs(adr.port);
//if (net_from.connum >= 1 && net_from.connum < 1+MAX_CONNECTIONS && col->conn[net_from.connum-1])
// col->conn[net_from.connum-1]->GetLocalAddresses(col->conn[net_from.connum-1], &relflags, &reladdr, &relpath, 1);
//FIXME: we don't really know which one... NET_BaseAdrToString(src->info.reladdr, sizeof(src->info.reladdr), &reladdr);
//src->info.relport = ntohs(reladdr.port);
src->info.type = ICE_SRFLX;
src->info.component = 1;
src->info.network = net_from.connum;
src->dirty = true;
src->info.priority = ICE_ComputePriority(&src->peer, &src->info); //FIXME
Sys_RandomBytes((void*)rnd, sizeof(rnd));
Q_strncpyz(src->info.candidateid, va("x%08x%08x", rnd[0], rnd[1]), sizeof(src->info.candidateid));
if (net_ice_debug.ival >= 1)
Con_Printf(S_COLOR_GRAY"[%s]: Public address: %s\n", con->friendlyname, NET_AdrToString(str, sizeof(str), &adr));
}
s->stunretry = Sys_Milliseconds() + 60*1000;
return true;
}
}
//check to see if this is a new peer-reflexive address, which happens when the peer is behind a nat.
for (rc = con->rc; rc; rc = rc->next)
{
if (NET_CompareAdr(&net_from, &rc->peer))
{
if (!(rc->reachable & (1u<<(net_from.connum-1))))
{
char str[256];
if (net_ice_debug.ival >= 1)
Con_Printf(S_COLOR_GRAY"[%s]: We can reach %s (%s) via %s\n", con->friendlyname, NET_AdrToString(str, sizeof(str), &net_from), ICE_GetCandidateType(&rc->info), ICE_NetworkToName(con, net_from.connum));
}
rc->reachable |= 1u<<(net_from.connum-1);
rc->reached = Sys_Milliseconds();
if (NET_CompareAdr(&net_from, &con->chosenpeer) && (stun->transactid[2] & BigLong(0x80000000)))
{
if (con->state == ICE_CONNECTING)
ICE_Set(con, "state", STRINGIFY(ICE_CONNECTED));
}
return true;
}
}
}
//only accept actual responses, not spoofed stuff.
if (stun->magiccookie == BigLong(0x2112a442)
&& stun->transactid[0]==col->srflx_tid[0]
&& stun->transactid[1]==col->srflx_tid[1]
&& stun->transactid[2]==col->srflx_tid[2]
&& !NET_CompareAdr(&col->srflx[adr.type!=NA_IP], &adr))
{
if (col->srflx[adr.type!=NA_IP].type==NA_INVALID)
Con_Printf(S_COLOR_GRAY"Public address reported as %s\n", NET_AdrToString(errmsg, sizeof(errmsg), &adr));
else
Con_Printf(CON_ERROR"Server reflexive address changed to %s\n", NET_AdrToString(errmsg, sizeof(errmsg), &adr));
col->srflx[adr.type!=NA_IP] = adr;
}
}
return true;
}
else if (stun->msgtype == BigShort(STUN_BINDING|STUN_INDICATION))// && net_message.cursize == stunlen + sizeof(*stun) && stun->magiccookie == BigLong(0x2112a442))
{
//binding indication. used as an rtp keepalive. should have a fingerprint
return true;
}
else if (stun->msgtype == BigShort(STUN_DATA|STUN_INDICATION)
&& net_message.cursize == stunlen + sizeof(*stun) && stun->magiccookie == BigLong(STUN_MAGIC_COOKIE))
{
//TURN relayed data
//these MUST come from a _known_ turn server.
netadr_t adr;
char xor[16];
short portxor;
void *data = NULL;
unsigned short datasize = 0;
unsigned short attrval;
stunattr_t *attr = (stunattr_t*)(stun+1);
int alen;
unsigned int network = net_from.connum-1; //also net_from_connection->connum
struct icestate_s *con;
if (network < MAX_CONNECTIONS)
return true; //don't handle this if its on the non-turn sockets.
network -= MAX_CONNECTIONS;
if (network < countof(con->server))
return true; //don't double-decapsulate...
network -= countof(con->server);
for (con = icelist; con; con = con->next)
{
if (network < con->servers && net_from_connection == con->server[network].con)
break;
}
if (!con)
return true; //don't know what it was. just ignore it.
if (network >= con->servers || !NET_CompareAdr(&net_from, &con->server[network].addr))
return true; //right socket, but not from the server that we expected...
adr.type = NA_INVALID;
while(stunlen>0)
{
stunlen -= sizeof(*attr);
alen = (unsigned short)BigShort(attr->attrlen);
if (alen > stunlen)
return false;
stunlen -= (alen+3)&~3;
attrval = BigShort(attr->attrtype);
switch(attrval)
{
default:
if (attrval & 0x8000)
break; //okay to ignore
return true;
case STUNATTR_DATA:
data = attr+1;
datasize = alen;
break;
case STUNATTR_XOR_PEER_ADDRESS:
//always xor
portxor = *(short*)&stun->magiccookie;
memcpy(xor, &stun->magiccookie, sizeof(xor));
adr.prot = NP_DGRAM;
adr.connum = net_from.connum;
adr.scopeid = net_from.scopeid;
if (alen == 8 && ((qbyte*)attr)[5] == 1) //ipv4
{
adr.type = NA_IP;
adr.port = (((short*)attr)[3]) ^ portxor;
*(int*)adr.address.ip = *(int*)(&((qbyte*)attr)[8]) ^ *(int*)xor;
}
else if (alen == 20 && ((qbyte*)attr)[5] == 2) //ipv6
{
adr.type = NA_IPV6;
adr.port = (((short*)attr)[3]) ^ portxor;
((int*)adr.address.ip6)[0] = ((int*)&((qbyte*)attr)[8])[0] ^ ((int*)xor)[0];
((int*)adr.address.ip6)[1] = ((int*)&((qbyte*)attr)[8])[1] ^ ((int*)xor)[1];
((int*)adr.address.ip6)[2] = ((int*)&((qbyte*)attr)[8])[2] ^ ((int*)xor)[2];
((int*)adr.address.ip6)[3] = ((int*)&((qbyte*)attr)[8])[3] ^ ((int*)xor)[3];
}
break;
}
alen = (alen+3)&~3;
attr = (stunattr_t*)((char*)(attr+1) + alen);
}
if (data)
{
memmove(net_message.data, data, net_message.cursize = datasize);
adr.connum = net_from.connum-countof(con->server); //came via the relay.
net_from = adr;
col->ReadGamePacket();
return true;
}
}
else if ((stun->msgtype == BigShort(STUN_CREATEPERM|STUN_REPLY) || stun->msgtype == BigShort(STUN_CREATEPERM|STUN_ERROR))
&& net_message.cursize == stunlen + sizeof(*stun) && stun->magiccookie == BigLong(STUN_MAGIC_COOKIE))
{
//TURN CreatePermissions reply (or error)
unsigned short attrval;
stunattr_t *attr = (stunattr_t*)(stun+1), *nonce=NULL, *realm=NULL;
int alen;
struct iceserver_s *s = NULL;
int i, j;
struct icestate_s *con;
char errmsg[128];
int err = 0;
*errmsg = 0;
//make sure it makes sense.
while(stunlen>0)
{
stunlen -= sizeof(*attr);
alen = (unsigned short)BigShort(attr->attrlen);
if (alen > stunlen)
return false;
stunlen -= (alen+3)&~3;
attrval = BigShort(attr->attrtype);
switch(attrval)
{
default:
if (attrval & 0x8000)
break; //okay to ignore
return true;
case STUNATTR_NONCE:
nonce = attr;
break;
case STUNATTR_REALM:
realm = attr;
break;
case STUNATTR_ERROR_CODE:
{
unsigned short len = BigShort(attr->attrlen)-4;
if (len > sizeof(errmsg)-1)
len = sizeof(errmsg)-1;
memcpy(errmsg, &((qbyte*)attr)[8], len);
errmsg[len] = 0;
if (err==0)
err = (((qbyte*)attr)[6]*100) + (((qbyte*)attr)[7]%100);
}
break;
case STUNATTR_MSGINTEGRITIY_SHA1:
break;
}
alen = (alen+3)&~3;
attr = (stunattr_t*)((char*)(attr+1) + alen);
}
//now figure out what it acked.
for (con = icelist; con; con = con->next)
{
for (i = 0; i < con->servers; i++)
{
s = &con->server[i];
if (NET_CompareAdr(&net_from, &s->addr))
for (j = 0; j < s->peers; j++)
{
if (s->peer[j].stunrnd[0] == stun->transactid[0] && s->peer[j].stunrnd[1] == stun->transactid[1] && s->peer[j].stunrnd[2] == stun->transactid[2])
{ //the lifetime of a permission is a fixed 5 mins (this is separately from the port allocation)
unsigned int now = Sys_Milliseconds();
if (err)
{
if (err == 438 && realm && nonce)
{
alen = BigShort(nonce->attrlen);
Z_Free(s->nonce);
s->nonce = Z_Malloc(alen+1);
memcpy(s->nonce, nonce+1, alen);
s->nonce[alen] = 0;
alen = BigShort(realm->attrlen);
Z_Free(s->realm);
s->realm = Z_Malloc(alen+1);
memcpy(s->realm, realm+1, alen);
s->realm[alen] = 0;
s->peer[j].retry = now; //retry fast.
}
}
else
{
now -= 25; //we don't know when it acked, so lets pretend we're a few MS ago.
s->peer[j].expires = now + 5*60*1000;
s->peer[j].retry = now + 4*60*1000; //start trying to refresh it a min early (which will do resends).
}
//next attempt will use a different id.
Sys_RandomBytes((char*)s->peer[i].stunrnd, sizeof(s->peer[i].stunrnd));
return true;
}
}
}
if (i < con->servers)
break;
}
return true;
}
else if ((stun->msgtype == BigShort(STUN_ALLOCATE|STUN_REPLY) || stun->msgtype == BigShort(STUN_ALLOCATE|STUN_ERROR)||
(stun->msgtype == BigShort(STUN_REFRESH|STUN_REPLY) || stun->msgtype == BigShort(STUN_REFRESH|STUN_ERROR)))
&& net_message.cursize == stunlen + sizeof(*stun) && stun->magiccookie == BigLong(STUN_MAGIC_COOKIE))
{
//TURN allocate reply (or error)
netadr_t adrs[2], ladr, *adr; //the last should be our own ip.
char xor[16];
short portxor;
unsigned short attrval;
stunattr_t *attr = (stunattr_t*)(stun+1);
int alen;
int err = 0;
char errmsg[64];
struct iceserver_s *s = NULL;
int i;
struct icestate_s *con;
qboolean noncechanged = false;
unsigned int lifetime = 0;
//gotta have come from our private socket.
unsigned int network = net_from.connum-1; //also net_from_connection->connum
if (network < MAX_CONNECTIONS)
return true; //don't handle this if its on the non-turn sockets.
network -= MAX_CONNECTIONS;
if (network < countof(con->server))
return true; //TURN-over-TURN is bad...
network -= countof(con->server);
for (con = icelist; con; con = con->next)
{
if (network < con->servers && net_from_connection == con->server[network].con)
{
s = &con->server[network];
if (s->stunrnd[0] == stun->transactid[0] && s->stunrnd[1] == stun->transactid[1] && s->stunrnd[2] == stun->transactid[2] && NET_CompareAdr(&net_from, &s->addr))
break;
if (net_ice_debug.ival)
Con_Printf(S_COLOR_GRAY"Stale transaction id (got %x, expected %x)\n", stun->transactid[0], s->stunrnd[0]);
}
}
if (!con)
return true; //don't know what it was. just ignore it.
network += 1 + MAX_CONNECTIONS; //fix it up to refer to the relay rather than the private socket.
adrs[0].type = NA_INVALID;
adrs[1].type = NA_INVALID;
ladr.type = NA_INVALID;
while(stunlen>0)
{
stunlen -= sizeof(*attr);
alen = (unsigned short)BigShort(attr->attrlen);
if (alen > stunlen)
return false;
stunlen -= (alen+3)&~3;
attrval = BigShort(attr->attrtype);
switch(attrval)
{
case STUNATTR_LIFETIME:
if (alen >= 4)
lifetime = BigLong(*(int*)(attr+1));
break;
// case STUNATTR_SOFTWARE:
case STUNATTR_MSGINTEGRITIY_SHA1:
// case STUNATTR_FINGERPRINT:
break;
default:
if (attrval & 0x8000)
break; //okay to ignore
err = -1; //got an attribute we 'must' handle...
return true;
case STUNATTR_NONCE:
Z_Free(s->nonce);
s->nonce = Z_Malloc(alen+1);
memcpy(s->nonce, attr+1, alen);
s->nonce[alen] = 0;
noncechanged = true;
break;
case STUNATTR_REALM:
Z_Free(s->realm);
s->realm = Z_Malloc(alen+1);
memcpy(s->realm, attr+1, alen);
s->realm[alen] = 0;
break;
case STUNATTR_XOR_RELAYED_ADDRESS:
case STUNATTR_XOR_MAPPED_ADDRESS:
if (BigShort(attr->attrtype) == STUNATTR_XOR_MAPPED_ADDRESS)
adr = &ladr;
else
adr = adrs[0].type?&adrs[1]:&adrs[0];
//always xor
portxor = *(short*)&stun->magiccookie;
memcpy(xor, &stun->magiccookie, sizeof(xor));
adr->prot = NP_DGRAM;
adr->connum = net_from.connum;
adr->scopeid = net_from.scopeid;
if (alen == 8 && ((qbyte*)attr)[5] == 1) //ipv4
{
adr->type = NA_IP;
adr->port = (((short*)attr)[3]) ^ portxor;
*(int*)adr->address.ip = *(int*)(&((qbyte*)attr)[8]) ^ *(int*)xor;
}
else if (alen == 20 && ((qbyte*)attr)[5] == 2) //ipv6
{
adr->type = NA_IPV6;
adr->port = (((short*)attr)[3]) ^ portxor;
((int*)adr->address.ip6)[0] = ((int*)&((qbyte*)attr)[8])[0] ^ ((int*)xor)[0];
((int*)adr->address.ip6)[1] = ((int*)&((qbyte*)attr)[8])[1] ^ ((int*)xor)[1];
((int*)adr->address.ip6)[2] = ((int*)&((qbyte*)attr)[8])[2] ^ ((int*)xor)[2];
((int*)adr->address.ip6)[3] = ((int*)&((qbyte*)attr)[8])[3] ^ ((int*)xor)[3];
}
break;
case STUNATTR_ERROR_CODE:
{
unsigned short len = BigShort(attr->attrlen)-4;
if (len > sizeof(errmsg)-1)
len = sizeof(errmsg)-1;
memcpy(errmsg, &((qbyte*)attr)[8], len);
errmsg[len] = 0;
if (!len)
Q_strncpyz(errmsg, "<no description>", len);
if (err==0)
err = (((qbyte*)attr)[6]*100) + (((qbyte*)attr)[7]%100);
}
break;
}
alen = (alen+3)&~3;
attr = (stunattr_t*)((char*)(attr+1) + alen);
}
if (err)
{
char sender[256];
if (err == 438/*stale nonce*/)
{ //reset everything.
s->state = noncechanged?TURN_HAVE_NONCE:TURN_UNINITED;
s->stunretry = Sys_Milliseconds();
if (net_ice_debug.ival >= 1)
Con_Printf(S_COLOR_GRAY"[%s]: %s: TURN error code %u : %s\n", con->friendlyname, NET_AdrToString(sender, sizeof(sender), &net_from), err, errmsg);
}
else if (err == 403/*forbidden*/) //something bad...
{
s->state = TURN_UNINITED, s->stunretry = Sys_Milliseconds() + 60*1000;
if (net_ice_debug.ival >= 1)
Con_Printf(CON_ERROR"[%s]: %s: TURN error code %u : %s\n", con->friendlyname, NET_AdrToString(sender, sizeof(sender), &net_from), err, errmsg);
}
else if (err == 401 && s->state == TURN_UNINITED && s->nonce) //failure when sending auth... give up for a min
{ //this happens from initial auth. we need to reply with the real auth request now.
s->state = TURN_HAVE_NONCE, s->stunretry = Sys_Milliseconds();
}
else
{
s->stunretry = Sys_Milliseconds() + 60*1000;
// if (net_ice_debug.ival >= 1)
Con_Printf(CON_ERROR"[%s]: %s: TURN error code %u : %s\n", con->friendlyname, NET_AdrToString(sender, sizeof(sender), &net_from), err, errmsg);
}
}
else
{
struct icecandidate_s *lc;
for (i = 0; i < countof(adrs); i++)
{
if (adrs[i].type != NA_INVALID && stun->msgtype == BigShort(STUN_ALLOCATE|STUN_REPLY))
{
s->state = TURN_ALLOCATED;
if (!i)
s->family = adrs[i].type;
if (s->family != adrs[i].type)
s->family = NA_INVALID; //send it both types.
if (ladr.type != NA_INVALID)
adr = &ladr; //can give a proper reflexive address
else
adr = &adrs[i]; //no info... give something.
//check to see if this is a new server-reflexive address, which happens when the peer is behind a nat.
for (lc = con->lc; lc; lc = lc->next)
{
if (NET_CompareAdr(&adrs[i], &lc->peer))
break;
}
if (!lc)
{
int rnd[2];
struct icecandidate_s *src; //server Reflexive Candidate
char str[256];
src = Z_Malloc(sizeof(*src));
src->next = con->lc;
con->lc = src;
src->peer = adrs[i];
NET_BaseAdrToString(src->info.addr, sizeof(src->info.addr), &adrs[i]);
src->info.port = ntohs(adrs[i].port);
NET_BaseAdrToString(src->info.reladdr, sizeof(src->info.reladdr), adr);
src->info.relport = ntohs(adr->port);
src->info.type = ICE_RELAY;
src->info.component = 1;
src->info.network = network;
src->dirty = true;
src->info.priority = ICE_ComputePriority(&adrs[i], &src->info);
Sys_RandomBytes((void*)rnd, sizeof(rnd));
Q_strncpyz(src->info.candidateid, va("x%08x%08x", rnd[0], rnd[1]), sizeof(src->info.candidateid));
if (net_ice_debug.ival >= 1)
Con_Printf(S_COLOR_GRAY"[%s]: Relayed local candidate: %s\n", con->friendlyname, NET_AdrToString(str, sizeof(str), &adrs[i]));
}
}
}
if (lifetime < 60) //don't spam reauth requests too often...
lifetime = 60;
s->stunretry = Sys_Milliseconds() + (lifetime-50)*1000;
return true;
}
return true;
}
#endif
if (stun->msgtype == BigShort(STUN_BINDING|STUN_REQUEST) && net_message.cursize == stunlen + sizeof(*stun) && stun->magiccookie == BigLong(STUN_MAGIC_COOKIE))
{
char username[256];
char integrity[20];
#ifdef SUPPORT_ICE
struct icestate_s *con;
int role = 0;
unsigned int tiehigh = 0;
unsigned int tielow = 0;
qboolean usecandidate = false;
unsigned int priority = 0;
char *lpwd = NULL;
#endif
char *integritypos = NULL;
int error = 0;
sizebuf_t buf;
char data[512];
int alen = 0, atype = 0, aofs = 0;
int crc;
//binding request
stunattr_t *attr = (stunattr_t*)(stun+1);
*username = 0;
while(stunlen)
{
alen = (unsigned short)BigShort(attr->attrlen);
if (alen+sizeof(*attr) > stunlen)
return false;
switch((unsigned short)BigShort(attr->attrtype))
{ case 0xc057: /*'network cost'*/ break;
default:
//unknown attributes < 0x8000 are 'mandatory to parse', and such packets must be dropped in their entirety.
//other ones are okay.
if (!((unsigned short)BigShort(attr->attrtype) & 0x8000))
return false;
break;
case STUNATTR_USERNAME:
if (alen < sizeof(username))
{
memcpy(username, attr+1, alen);
username[alen] = 0;
// Con_Printf("Stun username = \"%s\"\n", username);
}
break;
case STUNATTR_MSGINTEGRITIY_SHA1:
memcpy(integrity, attr+1, sizeof(integrity));
integritypos = (char*)(attr+1);
break;
#ifdef SUPPORT_ICE
case STUNATTR_ICE_PRIORITY:
// Con_Printf("priority = \"%i\"\n", priority);
priority = BigLong(*(int*)(attr+1));
break;
case STUNATTR_ICE_USE_CANDIDATE:
usecandidate = true;
break;
#endif
case STUNATTR_FINGERPRINT:
// Con_Printf("fingerprint = \"%08x\"\n", BigLong(*(int*)(attr+1)));
break;
#ifdef SUPPORT_ICE
case STUNATTR_ICE_CONTROLLED:
case STUNATTR_ICE_CONTROLLING:
role = (unsigned short)BigShort(attr->attrtype);
tiehigh = BigLong(((int*)(attr+1))[0]);
tielow = BigLong(((int*)(attr+1))[1]);
break;
#endif
}
alen = (alen+3)&~3;
attr = (stunattr_t*)((char*)(attr+1) + alen);
stunlen -= alen+sizeof(*attr);
}
#ifdef SUPPORT_ICE
if (*username || integritypos)
{
//we need to know which connection its from in order to validate the integrity
for (con = icelist; con; con = con->next)
{
if (!strcmp(va("%s:%s", con->lufrag, con->rufrag), username))
break;
}
if (!con)
{
if (net_ice_debug.ival >= 2)
Con_Printf("Received STUN request from unknown user \"%s\"\n", username);
return true;
}
/*else if (con->chosenpeer.type != NA_INVALID)
{ //got one.
if (!NET_CompareAdr(&net_from, &con->chosenpeer))
return true; //FIXME: we're too stupid to handle switching. pretend to be dead.
}*/
else if (con->state == ICE_INACTIVE)
return true; //bad timing
else
{
struct icecandidate_s *rc;
if (net_ice_debug.ival >= 2)
Con_Printf(S_COLOR_GRAY"[%s]: got binding request on %s from %s\n", con->friendlyname, ICE_NetworkToName(con, net_from.connum), NET_AdrToString(username,sizeof(username), &net_from));
if (integritypos)
{
char key[20];
//the hmac is a bit weird. the header length includes the integrity attribute's length, but the checksum doesn't even consider the attribute header.
stun->msglen = BigShort(integritypos+sizeof(integrity) - (char*)stun - sizeof(*stun));
CalcHMAC(&hash_sha1, key, sizeof(key), (qbyte*)stun, integritypos-4 - (char*)stun, con->lpwd, strlen(con->lpwd));
if (memcmp(key, integrity, sizeof(integrity)))
{
Con_DPrintf(CON_WARNING"Integrity is bad! needed %x got %x\n", *(int*)key, *(int*)integrity);
return true;
}
}
//check to see if this is a new peer-reflexive address, which happens when the peer is behind a nat.
for (rc = con->rc; rc; rc = rc->next)
{
if (NET_CompareAdr(&net_from, &rc->peer))
break;
}
if (!rc)
{
//netadr_t reladdr;
//int relflags;
//const char *relpath;
struct icecandidate_s *rc;
rc = Z_Malloc(sizeof(*rc));
rc->next = con->rc;
con->rc = rc;
rc->peer = net_from;
NET_BaseAdrToString(rc->info.addr, sizeof(rc->info.addr), &net_from);
rc->info.port = ntohs(net_from.port);
//if (net_from.connum >= 1 && net_from.connum < 1+MAX_CONNECTIONS && col->conn[net_from.connum-1])
// col->conn[net_from.connum-1]->GetLocalAddresses(col->conn[net_from.connum-1], &relflags, &reladdr, &relpath, 1);
//FIXME: we don't really know which one... NET_BaseAdrToString(rc->info.reladdr, sizeof(rc->info.reladdr), &reladdr);
//rc->info.relport = ntohs(reladdr.port);
rc->info.type = ICE_PRFLX;
rc->dirty = true;
rc->info.priority = priority;
}
//flip ice control role, if we're wrong.
if (role && role != (con->controlled?STUNATTR_ICE_CONTROLLING:STUNATTR_ICE_CONTROLLED))
{
if (tiehigh == con->tiehigh && tielow == con->tielow)
{
Con_Printf("ICE: Evil loopback hack enabled\n");
if (usecandidate)
{
if ((con->chosenpeer.connum != net_from.connum || !NET_CompareAdr(&con->chosenpeer, &net_from)) && net_ice_debug.ival >= 1)
{
char msg[64];
if (con->chosenpeer.connum-1 >= MAX_CONNECTIONS)
Con_Printf(S_COLOR_GRAY"[%s]: New peer imposed %s, via %s.\n", con->friendlyname, NET_AdrToString(msg, sizeof(msg), &net_from), ICE_NetworkToName(con, con->chosenpeer.connum));
else
Con_Printf(S_COLOR_GRAY"[%s]: New peer imposed %s.\n", con->friendlyname, NET_AdrToString(msg, sizeof(msg), &net_from));
}
con->chosenpeer = net_from;
if (con->state == ICE_CONNECTING)
ICE_Set(con, "state", STRINGIFY(ICE_CONNECTED));
}
}
else
{
con->controlled = (tiehigh > con->tiehigh) || (tiehigh == con->tiehigh && tielow > con->tielow);
if (net_ice_debug.ival >= 1)
Con_Printf(S_COLOR_GRAY"[%s]: role conflict detected. We should be %s\n", con->friendlyname, con->controlled?"controlled":"controlling");
error = 87;
}
}
else if (usecandidate && con->controlled)
{
//in the controlled role, we're connected once we're told the pair to use (by the usecandidate flag).
//note that this 'nominates' candidate pairs, from which the highest priority is chosen.
//so we just pick select the highest.
//this is problematic, however, as we don't actually know the real priority that the peer thinks we'll nominate it with.
if ((con->chosenpeer.connum != net_from.connum || !NET_CompareAdr(&con->chosenpeer, &net_from)) && net_ice_debug.ival >= 1)
{
char msg[64];
Con_Printf(S_COLOR_GRAY"[%s]: New peer imposed %s, via %s.\n", con->friendlyname, NET_AdrToString(msg, sizeof(msg), &net_from), ICE_NetworkToName(con, net_from.connum));
}
con->chosenpeer = net_from;
if (con->state == ICE_CONNECTING)
ICE_Set(con, "state", STRINGIFY(ICE_CONNECTED));
}
lpwd = con->lpwd;
}
}//otherwise its just an ip check
else
con = NULL;
#else
(void)integritypos;
#endif
memset(&buf, 0, sizeof(buf));
buf.maxsize = sizeof(data);
buf.cursize = 0;
buf.data = data;
if (net_from.type == NA_IP)
{
alen = 4;
atype = 1;
aofs = 0;
}
else if (net_from.type == NA_IPV6 &&
!*(int*)&net_from.address.ip6[0] &&
!*(int*)&net_from.address.ip6[4] &&
!*(short*)&net_from.address.ip6[8] &&
*(short*)&net_from.address.ip6[10] == (short)0xffff)
{ //just because we use an ipv6 address for ipv4 internally doesn't mean we should tell the peer that they're on ipv6...
alen = 4;
atype = 1;
aofs = sizeof(net_from.address.ip6) - sizeof(net_from.address.ip);
}
else if (net_from.type == NA_IPV6)
{
alen = 16;
atype = 2;
aofs = 0;
}
else
{
alen = 0;
atype = 0;
}
//Con_DPrintf("STUN from %s\n", NET_AdrToString(data, sizeof(data), &net_from));
MSG_WriteShort(&buf, BigShort(error?(STUN_BINDING|STUN_ERROR):(STUN_BINDING|STUN_REPLY)));
MSG_WriteShort(&buf, BigShort(0)); //fill in later
MSG_WriteLong(&buf, stun->magiccookie);
MSG_WriteLong(&buf, stun->transactid[0]);
MSG_WriteLong(&buf, stun->transactid[1]);
MSG_WriteLong(&buf, stun->transactid[2]);
if (error == 87)
{
char *txt = "Role Conflict";
MSG_WriteShort(&buf, BigShort(STUNATTR_ERROR_CODE));
MSG_WriteShort(&buf, BigShort(4 + strlen(txt)));
MSG_WriteShort(&buf, 0); //reserved
MSG_WriteByte(&buf, 0); //class
MSG_WriteByte(&buf, error); //code
SZ_Write(&buf, txt, strlen(txt)); //readable
while(buf.cursize&3) //padding
MSG_WriteChar(&buf, 0);
}
else if (1)
{ //xor mapped
netadr_t xored = net_from;
int i;
xored.port ^= *(short*)(data+4);
for (i = 0; i < alen; i++)
((qbyte*)&xored.address)[aofs+i] ^= ((qbyte*)data+4)[i];
MSG_WriteShort(&buf, BigShort(STUNATTR_XOR_MAPPED_ADDRESS));
MSG_WriteShort(&buf, BigShort(4+alen));
MSG_WriteShort(&buf, BigShort(atype));
MSG_WriteShort(&buf, xored.port);
SZ_Write(&buf, (char*)&xored.address + aofs, alen);
}
else
{ //non-xor mapped
MSG_WriteShort(&buf, BigShort(STUNATTR_MAPPED_ADDRESS));
MSG_WriteShort(&buf, BigShort(4+alen));
MSG_WriteShort(&buf, BigShort(atype));
MSG_WriteShort(&buf, net_from.port);
SZ_Write(&buf, (char*)&net_from.address + aofs, alen);
}
MSG_WriteShort(&buf, BigShort(STUNATTR_USERNAME)); //USERNAME
MSG_WriteShort(&buf, BigShort(strlen(username)));
SZ_Write(&buf, username, strlen(username));
while(buf.cursize&3)
MSG_WriteChar(&buf, 0);
#ifdef SUPPORT_ICE
if (lpwd)
{
//message integrity is a bit annoying
data[2] = ((buf.cursize+4+hash_sha1.digestsize-20)>>8)&0xff; //hashed header length is up to the end of the hmac attribute
data[3] = ((buf.cursize+4+hash_sha1.digestsize-20)>>0)&0xff;
//but the hash is to the start of the attribute's header
CalcHMAC(&hash_sha1, integrity, sizeof(integrity), data, buf.cursize, lpwd, strlen(lpwd));
MSG_WriteShort(&buf, BigShort(STUNATTR_MSGINTEGRITIY_SHA1));
MSG_WriteShort(&buf, BigShort(hash_sha1.digestsize)); //sha1 key length
SZ_Write(&buf, integrity, hash_sha1.digestsize); //integrity data
}
#endif
data[2] = ((buf.cursize+8-20)>>8)&0xff; //dummy length
data[3] = ((buf.cursize+8-20)>>0)&0xff;
crc = crc32(0, data, buf.cursize)^0x5354554e;
MSG_WriteShort(&buf, BigShort(STUNATTR_FINGERPRINT)); //FINGERPRINT
MSG_WriteShort(&buf, BigShort(sizeof(crc)));
MSG_WriteLong(&buf, BigLong(crc));
data[2] = ((buf.cursize-20)>>8)&0xff;
data[3] = ((buf.cursize-20)>>0)&0xff;
#ifdef SUPPORT_ICE
if (con)
TURN_Encapsulate(con, &net_from, data, buf.cursize);
else
#endif
NET_SendPacket(col, buf.cursize, data, &net_from);
return true;
}
}
#ifdef SUPPORT_ICE
{
struct icestate_s *con;
struct icecandidate_s *rc;
for (con = icelist; con; con = con->next)
{
for (rc = con->rc; rc; rc = rc->next)
{
if (NET_CompareAdr(&net_from, &rc->peer))
{
// if (rc->reachable)
{ //found it. fix up its source address to our ICE connection (so we don't have path-switching issues) and keep chugging along.
con->icetimeout = Sys_Milliseconds() + 1000*30; //not dead yet...
#ifdef HAVE_DTLS
if (con->dtlsstate)
{
switch(con->dtlsfuncs->Received(con->dtlsstate, &net_message))
{
case NETERR_SENT:
break; //
case NETERR_NOROUTE:
return false; //not a dtls packet at all. don't de-ICE it when we're meant to be using ICE.
case NETERR_DISCONNECTED: //dtls failure. ICE failed.
ICE_SetFailed(con, "DTLS Terminated");
return true;
default: //some kind of failure decoding the dtls packet. drop it.
return true;
}
}
#endif
net_from = con->qadr;
#ifdef HAVE_DTLS
if (con->sctp)
SCTP_Decode(con->sctp, col);
else
#endif
if (net_message.cursize)
col->ReadGamePacket();
return true;
}
}
}
}
}
#endif
return false;
}
#ifdef SUPPORT_ICE
int ICE_GetPeerCertificate(netadr_t *to, enum certprops_e prop, char *out, size_t outsize)
{
#ifdef HAVE_DTLS
struct icestate_s *con;
int i, c;
for (con = icelist; con; con = con->next)
{
if (NET_CompareAdr(to, &con->qadr))
{
if (prop==QCERT_LOBBYSTATUS)
{
*out = 0;
switch(con->state)
{
case ICE_INACTIVE:
Q_strncpyz(out, "idle", outsize);
break;
case ICE_FAILED:
Q_strncpyz(out, "Failed", outsize);
break;
case ICE_GATHERING:
Q_strncpyz(out, "Gathering", outsize);
break;
case ICE_CONNECTING:
for (i = 0, c = false; i < con->servers; i++)
if (!con->server[i].isstun)
{
if (con->server[i].state == TURN_ALLOCATED)
break;
c = true;
}
if (i == con->servers)
{
if (net_ice_relayonly.ival)
Q_strncpyz(out, "Probing ("CON_ERROR"NO TURN SERVER"CON_DEFAULT")", outsize); //can't work, might still get an allocation though.
else if (c)
Q_strncpyz(out, "Probing ("CON_WARNING"waiting for TURN allocation"CON_DEFAULT")", outsize); //still good for latency. not for privacy though.
else
Q_strncpyz(out, "Probing ("CON_WARNING"no relay configured"CON_DEFAULT")", outsize); //still good for latency. not for privacy though.
}
else
Q_strncpyz(out, "Probing ("S_COLOR_GREEN"with fallback"CON_DEFAULT")", outsize); //we have a relay for a fallback, all is good, hopefully. except we're still at this stage...
break;
case ICE_CONNECTED: //past the ICE stage (but maybe not the dtls+sctp layers, these should be less likely to fail, but dtls versions may become an issue)
//if (con->dtlsstate && notokay)
if (con->sctp && !con->sctp->o.writable)
Q_strncpyz(out, "Establishing", outsize); //will also block for the dtls channel of course. its not as easy check the dtls layer.
else
Q_strncpyz(out, "Established", outsize);
break;
}
return strlen(out);
}
else if (con->dtlsstate && con->dtlsfuncs->GetPeerCertificate)
return con->dtlsfuncs->GetPeerCertificate(con->dtlsstate, prop, out, outsize);
else if (prop==QCERT_ISENCRYPTED && con->dtlsstate)
return 0;
}
}
#endif
return -1;
}
void ICE_Terminate(netadr_t *to)
{
struct icestate_s *con;
for (con = icelist; con; con = con->next)
{
if (NET_CompareAdr(to, &con->qadr))
{
ICE_Set(con, "state", STRINGIFY(ICE_INACTIVE));
return;
}
}
}
neterr_t ICE_SendPacket(size_t length, const void *data, netadr_t *to)
{
struct icestate_s *con;
for (con = icelist; con; con = con->next)
{
if (NET_CompareAdr(to, &con->qadr))
{
con->icetimeout = Sys_Milliseconds()+30*1000; //keep it alive a little longer.
if (con->state == ICE_CONNECTING)
return NETERR_CLOGGED;
else if (con->state != ICE_CONNECTED)
return NETERR_DISCONNECTED;
#ifdef HAVE_DTLS
if (con->sctp)
return SCTP_Transmit(con->sctp, data, length);
if (con->dtlsstate)
return con->dtlsfuncs->Transmit(con->dtlsstate, data, length);
#endif
if (con->chosenpeer.type != NA_INVALID)
return ICE_Transmit(con, data, length);
return NETERR_CLOGGED; //still pending
}
}
return NETERR_DISCONNECTED;
}
#endif
#endif
#ifdef SUPPORT_ICE
//this is the clientside part of our custom accountless broker protocol
//basically just keeps the broker processing, but doesn't send/receive actual game packets.
//inbound messages can change ice connection states.
//clients only handle one connection. servers need to handle multiple
typedef struct {
ftenet_generic_connection_t generic;
//config state
char brokername[64]; //dns name
netadr_t brokeradr; //actual ip
char gamename[64]; //what we're trying to register as/for with the broker
//broker connection state
vfsfile_t *broker;
qboolean handshaking;
double nextping; //send heartbeats every now and then
double heartbeat;
double timeout; //detect if the broker goes dead, so we can reconnect reliably (instead of living for two hours without anyone able to connect).
qbyte in[8192];
size_t insize;
qbyte out[8192];
size_t outsize;
int error; //outgoing data is corrupt. kill it.
//client state...
struct icestate_s *ice;
int serverid;
//server state...
struct
{
struct icestate_s *ice;
} *clients;
size_t numclients;
} ftenet_ice_connection_t;
static void FTENET_ICE_Close(ftenet_generic_connection_t *gcon)
{
ftenet_ice_connection_t *b = (void*)gcon;
int cl;
if (b->broker)
VFS_CLOSE(b->broker);
for (cl = 0; cl < b->numclients; cl++)
if (b->clients[cl].ice)
iceapi.Close(b->clients[cl].ice, true);
Z_Free(b->clients);
if (b->ice)
iceapi.Close(b->ice, true);
Z_Free(b);
}
static void FTENET_ICE_Flush(ftenet_ice_connection_t *b)
{
int r;
if (!b->outsize || b->error || !b->broker)
return;
r = VFS_WRITE(b->broker, b->out, b->outsize);
if (r > 0)
{
b->outsize -= r;
memmove(b->out, b->out+r, b->outsize);
}
if (r < 0)
b->error = true;
}
static neterr_t FTENET_ICE_SendPacket(ftenet_generic_connection_t *gcon, int length, const void *data, netadr_t *to)
{
ftenet_ice_connection_t *b = (void*)gcon;
if (to->prot != NP_RTC_TCP && to->prot != NP_RTC_TLS)
return NETERR_NOROUTE;
if (!NET_CompareAdr(to, &b->brokeradr))
return NETERR_NOROUTE; //its using some other broker, don't bother trying to handle it here.
if (b->error)
return NETERR_DISCONNECTED;
return NETERR_CLOGGED; //we'll switch to a connect localcmd when the connection completes, so we don't really need to send any packets when they're using ICE. Just make sure the client doesn't give up.
}
static void FTENET_ICE_SplurgeRaw(ftenet_ice_connection_t *b, const qbyte *data, size_t len)
{
//0: dropclient (cl=-1 drops entire connection)
if (b->outsize+len > sizeof(b->out))
b->error = true;
else
{
memcpy(b->out+b->outsize, data, len);
b->outsize += len;
}
}
static void FTENET_ICE_SplurgeWS(ftenet_ice_connection_t *b, enum websocketpackettype_e pkttype, const qbyte *data1, size_t len1, const qbyte *data2, size_t len2)
{
size_t tlen = len1+len2;
qbyte header[8];
header[0] = 0x80|pkttype;
if (tlen >= 126)
{
header[1] = 126;
header[2] = tlen>>8; //bigendian
header[3] = tlen&0xff;
FTENET_ICE_SplurgeRaw(b, header, 4);
}
else
{ //small data
header[1] = tlen;
FTENET_ICE_SplurgeRaw(b, header, 2);
}
FTENET_ICE_SplurgeRaw(b, data1, len1);
FTENET_ICE_SplurgeRaw(b, data2, len2);
}
static void FTENET_ICE_SplurgeCmd(ftenet_ice_connection_t *b, int icemsg, int cl, const char *data)
{
qbyte msg[3] = {icemsg, cl&0xff, (cl>>8)&0xff}; //little endian...
FTENET_ICE_SplurgeWS(b, WS_PACKETTYPE_BINARYFRAME, msg, sizeof(msg), data, strlen(data));
}
static void FTENET_ICE_Heartbeat(ftenet_ice_connection_t *b)
{
b->heartbeat = realtime+30;
#ifdef HAVE_SERVER
if (b->generic.islisten)
{
extern cvar_t maxclients;
char info[2048];
int i;
client_t *cl;
int numclients = 0;
for (i=0 ; i<svs.allocated_client_slots ; i++)
{
cl = &svs.clients[i];
if ((cl->state == cs_connected || cl->state == cs_spawned || cl->name[0]) && !cl->spectator)
numclients++;
}
*info = 0;
//first line contains the serverinfo, or some form of it
{
char *resp = info;
const char *ignorekeys[] = {
"maxclients", "map", "*gamedir", "*z_ext", //this is a DP protocol query, so some QW fields are not needed
"gamename", "modname", "protocol", "clients", "sv_maxclients", "mapname", "qcstatus", "challenge", NULL}; //and we need to add some
const char *prioritykeys[] = {"hostname", NULL}; //make sure we include these before we start overflowing
char protocolname[64];
const char *gamestatus;
COM_ParseOut(com_protocolname.string, protocolname, sizeof(protocolname)); //we can only report one, so report the first.
if (svprogfuncs)
{
eval_t *v = PR_FindGlobal(svprogfuncs, "worldstatus", PR_ANY, NULL);
if (v)
gamestatus = PR_GetString(svprogfuncs, v->string);
else
gamestatus = "";
}
else
gamestatus = "";
Info_SetValueForKey(resp, "gamename", protocolname, sizeof(info) - (resp-info));//distinguishes it from other types of games
Info_SetValueForKey(resp, "protocol", SV_GetProtocolVersionString(), sizeof(info) - (resp-info));
Info_SetValueForKey(resp, "modname", FS_GetGamedir(true), sizeof(info) - (resp-info));
Info_SetValueForKey(resp, "clients", va("%d", numclients), sizeof(info) - (resp-info));
Info_SetValueForKey(resp, "sv_maxclients", maxclients.string, sizeof(info) - (resp-info));
Info_SetValueForKey(resp, "mapname", InfoBuf_ValueForKey(&svs.info, "map"), sizeof(info) - (resp-info));
resp += strlen(resp);
//now include the full/regular serverinfo
resp += InfoBuf_ToString(&svs.info, resp, sizeof(info) - (resp-info), prioritykeys, ignorekeys, NULL, NULL, NULL);
*resp = 0;
//and any possibly-long qc status string
if (*gamestatus)
Info_SetValueForKey(resp, "qcstatus", gamestatus, sizeof(info) - (resp-info));
resp += strlen(resp);
*resp++ = 0;
}
FTENET_ICE_SplurgeCmd(b, ICEMSG_SERVERINFO, -1, info);
}
#endif
}
static void FTENET_ICE_SendOffer(ftenet_ice_connection_t *b, int cl, struct icestate_s *ice, const char *type)
{
char buf[8192];
//okay, now send the sdp to our peer.
if (iceapi.Get(ice, type, buf, sizeof(buf)))
{
char json[8192+256];
if (ice->state == ICE_GATHERING)
ice->state = ICE_CONNECTING;
if (ice->mode == ICEM_WEBRTC)
{
Q_strncpyz(json, va("{\"type\":\"%s\",\"sdp\":\"", type+3), sizeof(json));
COM_QuotedString(buf, json+strlen(json), sizeof(json)-strlen(json)-2, true);
Q_strncatz(json, "\"}", sizeof(json));
FTENET_ICE_SplurgeCmd(b, ICEMSG_OFFER, cl, json);
}
else
FTENET_ICE_SplurgeCmd(b, ICEMSG_OFFER, cl, buf);
ice->blockcandidates = false;
}
}
static void FTENET_ICE_Establish(ftenet_ice_connection_t *b, const char *peeraddr, int cl, struct icestate_s **ret)
{ //sends offer
struct icestate_s *ice;
qboolean usewebrtc;
char *s;
if (*ret)
iceapi.Close(*ret, false);
#ifndef HAVE_DTLS
usewebrtc = false;
#else
if (!*net_ice_usewebrtc.string && net_enable_dtls.ival)
usewebrtc = true; //let the peer decide. this means we can use dtls, but not sctp.
else
usewebrtc = net_ice_usewebrtc.ival;
#endif
ice = *ret = iceapi.Create(b, NULL, b->generic.islisten?((peeraddr&&*peeraddr)?va("%s:%i", peeraddr,cl):NULL):va("/%s", b->gamename), usewebrtc?ICEM_WEBRTC:ICEM_ICE, b->generic.islisten?ICEP_QWSERVER:ICEP_QWCLIENT, !b->generic.islisten);
if (!*ret)
return; //some kind of error?!?
iceapi.Set(ice, "server", va("stun:%s:%i", b->brokername, BigShort(b->brokeradr.port)));
s = net_ice_servers.string;
while((s=COM_Parse(s)))
iceapi.Set(ice, "server", com_token);
if (!b->generic.islisten)
FTENET_ICE_SendOffer(b, cl, ice, "sdpoffer");
}
static void FTENET_ICE_Refresh(ftenet_ice_connection_t *b, int cl, struct icestate_s *ice)
{ //sends offer
char buf[8192];
if (ice->blockcandidates)
return; //don't send candidates before the offers...
while (ice && iceapi.GetLCandidateSDP(ice, buf, sizeof(buf)))
{
char json[8192+256];
if (ice->mode == ICEM_WEBRTC)
{
Q_strncpyz(json, "{\"candidate\":\"", sizeof(json));
COM_QuotedString(buf+2, json+strlen(json), sizeof(json)-strlen(json)-2, true);
Q_strncatz(json, "\",\"sdpMid\":\"0\",\"sdpMLineIndex\":0}", sizeof(json));
FTENET_ICE_SplurgeCmd(b, ICEMSG_CANDIDATE, cl, json);
}
else
FTENET_ICE_SplurgeCmd(b, ICEMSG_CANDIDATE, cl, buf);
}
}
static void Buf_ReadString(const char **data, const char *end, char *out, size_t outsize)
{
const char *in = *data;
char c;
outsize--; //count the null early.
while (in < end)
{
c = *in++;
if (!c)
break;
if (outsize)
{
outsize--;
*out++ = c;
}
}
*out = 0;
*data = in;
}
static qboolean FTENET_ICE_GetPacket(ftenet_generic_connection_t *gcon)
{
json_t *json;
ftenet_ice_connection_t *b = (void*)gcon;
int ctrl, len, cmd, cl, ofs;
const char *data;
char n;
if (!b->broker)
{
const char *s;
if (b->timeout > realtime)
return false;
b->generic.thesocket = TCP_OpenStream(&b->brokeradr, b->brokername); //save this for select.
b->broker = FS_WrapTCPSocket(b->generic.thesocket, true, b->brokername);
#ifdef HAVE_SSL
//convert to tls...
if (b->brokeradr.prot == NP_TLS || b->brokeradr.prot == NP_RTC_TLS)
b->broker = FS_OpenSSL(b->brokername, b->broker, false);
#endif
if (!b->broker)
{
b->timeout = realtime + 30;
Con_Printf("rtc broker connection to %s failed (retry: 30 secs)\n", b->brokername);
return false;
}
b->insize = b->outsize = 0;
COM_Parse(com_protocolname.string);
b->handshaking = true;
s = va("GET /%s/%s HTTP/1.1\r\n"
"Host: %s\r\n"
"Connection: Upgrade\r\n"
"Upgrade: websocket\r\n"
"Sec-WebSocket-Version: 13\r\n"
"Sec-WebSocket-Protocol: %s\r\n"
"\r\n", com_token, b->gamename, b->brokername, b->generic.islisten?"rtc_host":"rtc_client");
FTENET_ICE_SplurgeRaw(b, s, strlen(s));
b->heartbeat = realtime;
b->nextping = realtime + 100;
b->timeout = realtime + 270;
}
if (b->error)
{
handleerror:
b->generic.thesocket = INVALID_SOCKET;
if (b->broker)
VFS_CLOSE(b->broker);
b->broker = NULL;
for (cl = 0; cl < b->numclients; cl++)
{
if (b->clients[cl].ice)
iceapi.Close(b->clients[cl].ice, false);
b->clients[cl].ice = NULL;
}
if (b->ice)
iceapi.Close(b->ice, false);
b->ice = NULL;
if (b->error != 1 || !b->generic.islisten)
return false; //permanant error...
b->error = false;
b->insize = b->outsize = 0;
b->timeout = realtime + 30;
return false;
}
//keep checking for new candidate info.
if (b->ice)
FTENET_ICE_Refresh(b, b->serverid, b->ice);
for (cl = 0; cl < b->numclients; cl++)
if (b->clients[cl].ice)
FTENET_ICE_Refresh(b, cl, b->clients[cl].ice);
if (realtime >= b->heartbeat)
FTENET_ICE_Heartbeat(b);
len = VFS_READ(b->broker, b->in+b->insize, sizeof(b->in)-1-b->insize);
if (!len)
{
FTENET_ICE_Flush(b);
if (realtime > b->nextping)
{ //nothing happening... make sure the connection isn't dead...
FTENET_ICE_SplurgeWS(b, WS_PACKETTYPE_PING, NULL, 0, NULL, 0);
b->nextping = realtime + 100;
}
return false; //nothing new
}
if (len < 0)
{
if (!b->error)
Con_Printf("rtc broker connection to %s failed (retry: 30 secs)\n", b->brokername);
b->error = true;
goto handleerror;
}
b->insize += len;
b->in[b->insize] = 0;
ofs = 0;
b->nextping = realtime + 100;
b->timeout = max(b->timeout, realtime + 270);
if (b->handshaking)
{ //we're still waiting for an http 101 code. websocket data starts straight after.
char *end = strstr(b->in, "\r\n\r\n");
if (!end)
return false; //not available yet...
if (strncmp(b->in, "HTTP/1.1 101 ", 13))
{
b->error = ~0;
return false;
}
end+=4;
b->handshaking = false; //done...
ofs = (qbyte*)end-b->in;
}
while (b->insize >= ofs+2)
{
ctrl = b->in[ofs+0];
len = b->in[ofs+1];
ofs+=2;
if (len > 126)
{//unsupported
b->error = 1;
break;
}
else if (len == 126)
{
if (b->insize <= 4)
break;
len = (b->in[ofs+0]<<8)|(b->in[ofs+1]);
ofs+=2;
}
if (b->insize < ofs+len)
break;
n = b->in[ofs+len];
b->in[ofs+len] = 0;
switch(ctrl & 0xf)
{
case WS_PACKETTYPE_PING:
FTENET_ICE_SplurgeWS(b, WS_PACKETTYPE_PONG, NULL, 0, b->in+ofs, len);
FTENET_ICE_Flush(b);
break;
case WS_PACKETTYPE_CLOSE:
b->error = true;
break;
default:
break;
case WS_PACKETTYPE_BINARYFRAME:
cmd = b->in[ofs];
cl = (short)(b->in[ofs+1] | (b->in[ofs+2]<<8));
data = b->in+ofs+3;
switch(cmd)
{
case ICEMSG_PEERLOST: //the broker lost its connection to our peer...
if (cl == -1)
{
b->error = true;
if (net_ice_debug.ival)
Con_Printf(S_COLOR_GRAY"[%s]: Broker host lost connection: %s\n", b->ice?b->ice->friendlyname:"?", *data?data:"<NO REASON>");
}
else if (cl >= 0 && cl < b->numclients)
{
if (net_ice_debug.ival)
Con_Printf(S_COLOR_GRAY"[%s]: Broker client lost connection: %s\n", b->clients[cl].ice?b->clients[cl].ice->friendlyname:"?", *data?data:"<NO REASON>");
if (b->clients[cl].ice)
iceapi.Close(b->clients[cl].ice, false);
b->clients[cl].ice = NULL;
}
break;
case ICEMSG_NAMEINUSE:
Con_Printf("Unable to listen on /%s - name already taken\n", b->gamename);
b->error = true; //try again later.
break;
case ICEMSG_GREETING: //reports the trailing url we're 'listening' on. anyone else using that url will connect to us.
data = strchr(data, '/');
if (data++)
Q_strncpyz(b->gamename, data, sizeof(b->gamename));
Con_Printf("Publicly listening on /%s\n", b->gamename);
break;
case ICEMSG_NEWPEER: //relay connection established with a new peer
//note that the server ought to wait for an offer from the client before replying with any ice state, but it doesn't really matter for our use-case.
{
char peer[MAX_QPATH];
char relay[MAX_QPATH];
char *s;
Buf_ReadString(&data, b->in+ofs+len, peer, sizeof(peer));
Buf_ReadString(&data, b->in+ofs+len, relay, sizeof(relay));
if (b->generic.islisten)
{
// Con_DPrintf("Client connecting: %s\n", data);
if (cl < 1024 && cl >= b->numclients)
{ //looks like a new one... but don't waste memory
Z_ReallocElements((void**)&b->clients, &b->numclients, cl+1, sizeof(b->clients[0]));
}
if (cl >= 0 && cl < b->numclients)
{
FTENET_ICE_Establish(b, *peer?peer:NULL, cl, &b->clients[cl].ice);
for (s = relay; (s=COM_Parse(s)); )
iceapi.Set(b->clients[cl].ice, "server", com_token);
if (net_ice_debug.ival)
Con_Printf(S_COLOR_GRAY"[%s]: New client spotted...\n", b->clients[cl].ice?b->clients[cl].ice->friendlyname:"?");
}
else if (net_ice_debug.ival)
Con_Printf(S_COLOR_GRAY"[%s]: New client spotted, but index is unusable\n", "?");
}
else
{
//Con_DPrintf("Server found: %s\n", data);
FTENET_ICE_Establish(b, *peer?peer:NULL, cl, &b->ice);
b->serverid = cl;
for (s = relay; (s=COM_Parse(s)); )
iceapi.Set(b->ice, "server", com_token);
if (net_ice_debug.ival)
Con_Printf(S_COLOR_GRAY"[%s]: Meta channel to game server now open\n", b->ice?b->ice->friendlyname:"?");
}
}
break;
case ICEMSG_OFFER: //we received an offer from a client
json = JSON_Parse(data);
if (json)
//should probably also verify the type.
data = JSON_GetString(json, "sdp", com_token,sizeof(com_token), NULL);
if (b->generic.islisten)
{
if (cl >= 0 && cl < b->numclients && b->clients[cl].ice)
{
if (net_ice_debug.ival)
Con_Printf(S_COLOR_GRAY"[%s]: Got offer:\n%s\n", b->clients[cl].ice?b->clients[cl].ice->friendlyname:"?", data);
iceapi.Set(b->clients[cl].ice, "sdpoffer", data);
iceapi.Set(b->clients[cl].ice, "state", STRINGIFY(ICE_CONNECTING));
FTENET_ICE_SendOffer(b, cl, b->clients[cl].ice, "sdpanswer");
}
else if (net_ice_debug.ival)
Con_Printf(S_COLOR_GRAY"[%s]: Got bad offer/answer:\n%s\n", b->clients[cl].ice?b->clients[cl].ice->friendlyname:"?", data);
}
else
{
if (b->ice)
{
if (net_ice_debug.ival)
Con_Printf(S_COLOR_GRAY"[%s]: Got answer:\n%s\n", b->ice?b->ice->friendlyname:"?", data);
iceapi.Set(b->ice, "sdpanswer", data);
iceapi.Set(b->ice, "state", STRINGIFY(ICE_CONNECTING));
}
else if (net_ice_debug.ival)
Con_Printf(S_COLOR_GRAY"[%s]: Got bad offer/answer:\n%s\n", b->ice?b->ice->friendlyname:"?", data);
}
JSON_Destroy(json);
break;
case ICEMSG_CANDIDATE:
json = JSON_Parse(data);
if (json)
{
data = com_token;
com_token[0]='a';
com_token[1]='=';
com_token[2]=0;
JSON_GetString(json, "candidate", com_token+2,sizeof(com_token)-2, NULL);
}
// Con_Printf("Candidate update: %s\n", data);
if (b->generic.islisten)
{
if (cl >= 0 && cl < b->numclients && b->clients[cl].ice)
{
if (net_ice_debug.ival)
Con_Printf(S_COLOR_GRAY"[%s]: Got candidate:\n%s\n", b->clients[cl].ice->friendlyname, data);
iceapi.Set(b->clients[cl].ice, "sdp", data);
}
}
else
{
if (b->ice)
{
if (net_ice_debug.ival)
Con_Printf(S_COLOR_GRAY"[%s]: Got candidate:\n%s\n", b->ice->friendlyname, data);
iceapi.Set(b->ice, "sdp", data);
}
}
JSON_Destroy(json);
break;
default:
if (net_ice_debug.ival)
Con_Printf(S_COLOR_GRAY"[%s]: Broker send unknown packet: %i\n", b->ice?b->ice->friendlyname:"?", cmd);
break;
}
break;
}
ofs+=len;
b->in[ofs] = n;
}
if (ofs)
{ //and eat the newly parsed data...
b->insize -= ofs;
memmove(b->in, b->in+ofs, b->insize);
}
FTENET_ICE_Flush(b);
return false;
}
static void FTENET_ICE_PrintStatus(ftenet_generic_connection_t *gcon)
{
ftenet_ice_connection_t *b = (void*)gcon;
size_t c;
if (b->ice)
ICE_Debug(b->ice);
if (b->numclients)
{
size_t activeice = 0;
for (c = 0; c < b->numclients; c++)
if (b->clients[c].ice)
{
activeice++;
ICE_PrintSummary(b->clients[c].ice, b->generic.islisten);
}
Con_Printf("%u ICE connections\n", (unsigned)activeice);
}
}
static int FTENET_ICE_GetLocalAddresses(struct ftenet_generic_connection_s *gcon, unsigned int *adrflags, netadr_t *addresses, const char **adrparms, int maxaddresses)
{
ftenet_ice_connection_t *b = (void*)gcon;
if (maxaddresses < 1)
return 0;
*addresses = b->brokeradr;
*adrflags = 0;
*adrparms = b->gamename;
return 1;
}
static qboolean FTENET_ICE_ChangeLocalAddress(struct ftenet_generic_connection_s *gcon, const char *address, netadr_t *newadr)
{
ftenet_ice_connection_t *b = (void*)gcon;
netadr_t adr;
const char *path;
if (!NET_StringToAdr2(address, PORT_ICEBROKER, &adr, 1, &path))
return true; //err... something failed? don't break what works!
if (!NET_CompareAdr(&adr, &b->brokeradr))
return false; //the broker changed! zomg! just kill it all!
if (path && *path++=='/')
{
if (*path && strcmp(path, b->gamename))
return false; //it changed! and we care! break everything!
}
return true;
}
ftenet_generic_connection_t *FTENET_ICE_EstablishConnection(ftenet_connections_t *col, const char *address, netadr_t adr, const struct dtlspeercred_s *peerinfo)
{
ftenet_ice_connection_t *newcon;
const char *path;
char *c;
if (!NET_StringToAdr2(address, PORT_ICEBROKER, &adr, 1, &path))
return NULL;
/* if (adr.prot == NP_ICES)
adr.prot = NP_TLS;
else if (adr.prot == NP_ICE)
adr.prot = NP_STREAM;
*/
newcon = Z_Malloc(sizeof(*newcon));
if (!strncmp(address, "ice://", 6)||!strncmp(address, "rtc://", 6))
address+=6;
else if (!strncmp(address, "ices://", 7)||!strncmp(address, "rtcs://", 7))
address+=7;
if (address == path && *path=='/')
{
if (!strncmp(net_ice_broker.string, "tls://", 6) || !strncmp(net_ice_broker.string, "tcp://", 6))
Q_strncpyz(newcon->brokername, net_ice_broker.string+6, sizeof(newcon->brokername)); //name is for prints only.
else
Q_strncpyz(newcon->brokername, net_ice_broker.string, sizeof(newcon->brokername)); //name is for prints only.
Q_strncpyz(newcon->gamename, path+1, sizeof(newcon->gamename)); //so we know what to tell the broker.
}
else
{
Q_strncpyz(newcon->brokername, address, sizeof(newcon->brokername)); //name is for prints only.
if (path && *path == '/' && path-address < sizeof(newcon->brokername))
{
newcon->brokername[path-address] = 0;
Q_strncpyz(newcon->gamename, path+1, sizeof(newcon->gamename)); //so we know what to tell the broker.
}
else
*newcon->gamename = 0;
}
c = strchr(newcon->brokername, ':');
if (c) *c = 0;
newcon->brokeradr = adr;
newcon->broker = NULL;
newcon->timeout = realtime;
newcon->heartbeat = realtime;
newcon->nextping = realtime;
newcon->generic.owner = col;
newcon->generic.thesocket = INVALID_SOCKET;
newcon->generic.addrtype[0] = NA_INVALID;
newcon->generic.addrtype[1] = NA_INVALID;
newcon->generic.GetPacket = FTENET_ICE_GetPacket;
newcon->generic.SendPacket = FTENET_ICE_SendPacket;
newcon->generic.Close = FTENET_ICE_Close;
newcon->generic.PrintStatus = FTENET_ICE_PrintStatus;
newcon->generic.GetLocalAddresses = FTENET_ICE_GetLocalAddresses;
newcon->generic.ChangeLocalAddress = FTENET_ICE_ChangeLocalAddress;
newcon->generic.islisten = col->islisten;
return &newcon->generic;
}
void ICE_Init(void)
{
Cvar_Register(&net_ice_exchangeprivateips, "networking");
Cvar_Register(&net_ice_allowstun, "networking");
Cvar_Register(&net_ice_allowturn, "networking");
Cvar_Register(&net_ice_allowmdns, "networking");
Cvar_Register(&net_ice_relayonly, "networking");
Cvar_Register(&net_ice_usewebrtc, "networking");
Cvar_Register(&net_ice_servers, "networking");
Cvar_Register(&net_ice_debug, "networking");
Cmd_AddCommand("net_ice_show", ICE_Show_f);
}
#ifdef HAVE_SERVER
void SVC_ICE_Offer(void)
{ //handles an 'ice_offer' udp message from a broker
extern cvar_t net_ice_servers;
struct icestate_s *ice;
static float throttletimer;
const char *sdp, *s;
char buf[1400];
int sz;
char *clientaddr = Cmd_Argv(1); //so we can do ip bans on the client's srflx address
char *brokerid = Cmd_Argv(2); //specific id to identify the pairing on the broker.
netadr_t adr;
json_t *json;
if (!sv.state)
return; //err..?
if (net_from.prot != NP_DTLS && net_from.prot != NP_WSS && net_from.prot != NP_TLS)
{ //a) dtls provides a challenge (ensuring we can at least ban them).
//b) this contains the caller's ips. We'll be pinging them anyway, but hey. also it'll be too late at this point but it keeps the other side honest.
Con_ThrottlePrintf(&throttletimer, 0, CON_WARNING"%s: ice handshake from %s was unencrypted\n", NET_AdrToString (buf, sizeof(buf), &net_from), clientaddr);
return;
}
if (!NET_StringToAdr_NoDNS(clientaddr, 0, &adr)) //no dns-resolution denial-of-service attacks please.
{
Con_ThrottlePrintf(&throttletimer, 0, CON_WARNING"%s: ice handshake specifies bad client address: %s\n", NET_AdrToString (buf, sizeof(buf), &net_from), clientaddr);
return;
}
if (SV_BannedReason(&adr)!=NULL)
{
Con_ThrottlePrintf(&throttletimer, 0, CON_WARNING"%s: ice handshake for %s - banned\n", NET_AdrToString (buf, sizeof(buf), &net_from), clientaddr);
return;
}
ice = iceapi.Create(NULL, brokerid, clientaddr, ICEM_WEBRTC, ICEP_QWSERVER, false);
if (!ice)
return; //some kind of error?!?
//use the sender as a stun server. FIXME: put server's address in the request instead.
iceapi.Set(ice, "server", va("stun:%s", NET_AdrToString (buf, sizeof(buf), &net_from))); //the sender should be able to act as a stun server for use. should probably just pass who its sending to and call it a srflx anyway, tbh.
s = net_ice_servers.string;
while((s=COM_Parse(s)))
iceapi.Set(ice, "server", com_token);
sdp = MSG_ReadString();
json = JSON_Parse(sdp); //browsers are poo
if (json)
sdp = JSON_GetString(json, "sdp", buf,sizeof(buf), "");
if (iceapi.Set(ice, "sdpoffer", sdp))
{
iceapi.Set(ice, "state", STRINGIFY(ICE_CONNECTING)); //skip gathering, just trickle.
Q_snprintfz(buf, sizeof(buf), "\xff\xff\xff\xff""ice_answer %s", brokerid);
sz = strlen(buf)+1;
if (iceapi.Get(ice, "sdpanswer", buf+sz, sizeof(buf)-sz))
{
sz += strlen(buf+sz);
NET_SendPacket(svs.sockets, sz, buf, &net_from);
}
}
JSON_Destroy(json);
//and because we won't have access to its broker, disconnect it from any persistent state to let it time out.
iceapi.Close(ice, false);
}
void SVC_ICE_Candidate(void)
{ //handles an 'ice_ccand' udp message from a broker
struct icestate_s *ice;
json_t *json;
const char *sdp;
char buf[1400];
char *brokerid = Cmd_Argv(1); //specific id to identify the pairing on the broker.
unsigned int seq = atoi(Cmd_Argv(2)); //their seq, to ack and prevent dupes
unsigned int ack = atoi(Cmd_Argv(3)); //so we don't resend endlessly... *cough*
if (net_from.prot != NP_DTLS && net_from.prot != NP_WSS && net_from.prot != NP_TLS)
{
return;
}
ice = iceapi.Find(NULL, brokerid);
if (!ice)
return; //bad state. lost packet?
//parse the inbound candidates
for(;;)
{
sdp = MSG_ReadStringLine();
if (msg_badread || !*sdp)
break;
if (seq++ < ice->u.inseq)
continue;
ice->u.inseq++;
json = JSON_Parse(sdp);
if (json)
{
sdp = buf;
buf[0]='a';
buf[1]='=';
buf[2]=0;
JSON_GetString(json, "candidate", buf+2,sizeof(buf)-2, NULL);
}
iceapi.Set(ice, "sdp", sdp);
JSON_Destroy(json);
}
while (ack > ice->u.outseq)
{ //drop an outgoing candidate line
char *nl = strchr(ice->u.text, '\n');
if (nl)
{
nl++;
memmove(ice->u.text, nl, strlen(nl)+1); //chop it away.
ice->u.outseq++;
continue;
}
//wut?
if (ack > ice->u.outseq)
ice->u.outseq = ack; //a gap? oh noes!
break;
}
//check for new candidates to include
while (iceapi.GetLCandidateSDP(ice, buf, sizeof(buf)))
{
Z_StrCat(&ice->u.text, buf);
Z_StrCat(&ice->u.text, "\n");
}
Q_snprintfz(buf, sizeof(buf), "\xff\xff\xff\xff""ice_scand %s %u %u\n%s", brokerid, ice->u.outseq, ice->u.inseq, ice->u.text?ice->u.text:"");
NET_SendPacket(svs.sockets, strlen(buf), buf, &net_from);
}
#endif
#endif