diff --git a/doc/Holepunch-Protocol.txt b/doc/Holepunch-Protocol.txt new file mode 100644 index 00000000..5bbad675 --- /dev/null +++ b/doc/Holepunch-Protocol.txt @@ -0,0 +1,34 @@ + Bird's Hole Punching Protocol + + +Hole punch - a mechanism to bypass a firewall +Server - a third party which is not behind a firewall +Client - anything contacting the server +Magic - the four bytes 00 52 EB 11 +Address - an IPv4 address + + + 0 1 2 3 4 5 6 7 8 9 + +-------+-------+----+ + | Magic |Address|Port| + +-------+-------+----+ + Relay Packet + + +A client that expects to be the target of a hole punch +must contact the server frequently, to keep a UDP +"connection" open, so that the server may relay hole +punching requests to them. + +A client makes a hole punching request to another client +by sending a Relay Packet to the server. The server then +sends another Relay Packet to the client described by the +first packet. The second packet is filled with the source +address and port of the first packet. + +Once a client receives a Relay Packet, this protocol's +purpose is fulfilled and the client is aware that another +client requests a hole punch. + + +vim: noai diff --git a/src/d_clisrv.c b/src/d_clisrv.c index eb191757..d3196d07 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -1916,6 +1916,12 @@ static void SendAskInfo(INT32 node) // now allowed traffic from the host to us in, so once the MS relays // our address to the host, it'll be able to speak to us. HSendPacket(node, false, 0, sizeof (askinfo_pak)); + + if (node != 0 && node != BROADCASTADDR && + cv_rendezvousserver.string[0]) + { + I_NetRequestHolePunch(); + } } serverelem_t serverlist[MAXSERVERLIST]; @@ -5754,6 +5760,22 @@ static void UpdatePingTable(void) } } +static void RenewHolePunch(void) +{ + if (cv_rendezvousserver.string[0]) + { + static time_t past; + + const time_t now = time(NULL); + + if ((now - past) > 20) + { + I_NetRegisterHolePunch(); + past = now; + } + } +} + // Handle timeouts to prevent definitive freezes from happenning static void HandleNodeTimeouts(void) { @@ -5788,6 +5810,11 @@ FILESTAMP MasterClient_Ticker(); #endif + if (netgame && serverrunning) + { + RenewHolePunch(); + } + if (client) { // send keep alive @@ -5847,6 +5874,11 @@ FILESTAMP MasterClient_Ticker(); // Acking the Master Server #endif + if (netgame && serverrunning) + { + RenewHolePunch(); + } + if (client) { if (!resynch_local_inprogress) diff --git a/src/d_net.c b/src/d_net.c index 3b1bad87..ce455e8b 100644 --- a/src/d_net.c +++ b/src/d_net.c @@ -49,6 +49,8 @@ tic_t connectiontimeout = (10*TICRATE); doomcom_t *doomcom = NULL; /// \brief network packet data, points inside doomcom doomdata_t *netbuffer = NULL; +/// \brief hole punching packet, also points inside doomcom +holepunch_t *holepunchpacket = NULL; #ifdef DEBUGFILE FILE *debugfile = NULL; // put some net info in a file during the game @@ -72,6 +74,8 @@ boolean (*I_NetCanGet)(void) = NULL; void (*I_NetCloseSocket)(void) = NULL; void (*I_NetFreeNodenum)(INT32 nodenum) = NULL; SINT8 (*I_NetMakeNodewPort)(const char *address, const char* port) = NULL; +void (*I_NetRequestHolePunch)(void) = NULL; +void (*I_NetRegisterHolePunch)(void) = NULL; boolean (*I_NetOpenSocket)(void) = NULL; boolean (*I_Ban) (INT32 node) = NULL; void (*I_ClearBans)(void) = NULL; @@ -1335,6 +1339,7 @@ boolean D_CheckNetGame(void) I_Error("Too many nodes (%d), max:%d", doomcom->numnodes, MAXNETNODES); netbuffer = (doomdata_t *)(void *)&doomcom->data; + holepunchpacket = (holepunch_t *)(void *)&doomcom->data; #ifdef DEBUGFILE #ifdef _arch_dreamcast diff --git a/src/i_net.h b/src/i_net.h index 0e17077b..c3cd218e 100644 --- a/src/i_net.h +++ b/src/i_net.h @@ -77,11 +77,19 @@ typedef struct char data[MAXPACKETLENGTH]; } ATTRPACK doomcom_t; +typedef struct +{ + INT32 magic; + INT32 addr; + INT16 port; +} ATTRPACK holepunch_t; + #if defined(_MSC_VER) #pragma pack() #endif extern doomcom_t *doomcom; +extern holepunch_t *holepunchpacket; /** \brief return packet in doomcom struct */ @@ -140,6 +148,15 @@ extern boolean (*I_NetOpenSocket)(void); extern void (*I_NetCloseSocket)(void); +/** \brief send a hole punching request +*/ +extern void (*I_NetRequestHolePunch)(void); + +/** \brief register this machine on the hole punching server +*/ +extern void (*I_NetRegisterHolePunch)(void); + + extern boolean (*I_Ban) (INT32 node); extern void (*I_ClearBans)(void); extern const char *(*I_GetNodeAddress) (INT32 node); diff --git a/src/i_tcp.c b/src/i_tcp.c index ba973494..f59bd083 100644 --- a/src/i_tcp.c +++ b/src/i_tcp.c @@ -241,6 +241,8 @@ static size_t broadcastaddresses = 0; static boolean nodeconnected[MAXNETNODES+1]; static mysockaddr_t banned[MAXBANS]; static UINT8 bannedmask[MAXBANS]; +/* See ../doc/Holepunch-Protocol.txt */ +static const INT32 hole_punch_magic = MSBF_LONG (0x52eb11); #endif static size_t numbans = 0; @@ -597,6 +599,55 @@ void Command_Numnodes(void) #endif #ifndef NONET +/* not one of the reserved "local" addresses */ +static boolean +is_external_address (UINT32 p) +{ + UINT8 a = (p & 255); + UINT8 b = ((p >> 8) & 255); + + if (p == (UINT32)~0)/* 255.255.255.255 */ + return 0; + + switch (a) + { + case 0: + case 10: + case 127: + return false; + case 172: + return (b & ~15) != 16;/* 16 - 31 */ + case 192: + return b != 168; + default: + return true; + } +} + +static boolean hole_punch(ssize_t c) +{ + /* See ../doc/Holepunch-Protocol.txt */ + if (cv_rendezvousserver.string[0] && + c == 10 && holepunchpacket->magic == hole_punch_magic && + is_external_address(ntohl(holepunchpacket->addr))) + { + mysockaddr_t addr; + addr.ip4.sin_family = AF_INET; + addr.ip4.sin_addr.s_addr = holepunchpacket->addr; + addr.ip4.sin_port = holepunchpacket->port; + sendto(mysockets[0], NULL, 0, 0, &addr.any, sizeof addr.ip4); + + CONS_Debug(DBG_NETPLAY, + "hole punching request from %s\n", SOCK_AddrToStr(&addr)); + + return true; + } + else + { + return false; + } +} + // Returns true if a packet was received from a new node, false in all other cases static boolean SOCK_Get(void) { @@ -611,7 +662,7 @@ static boolean SOCK_Get(void) fromlen = (socklen_t)sizeof(fromaddress); c = recvfrom(mysockets[n], (char *)&doomcom->data, MAXPACKETLENGTH, 0, (void *)&fromaddress, &fromlen); - if (c != ERRSOCKET) + if (c > 0) { #ifdef USE_STUN if (STUN_got_response(doomcom->data, c)) @@ -620,6 +671,11 @@ static boolean SOCK_Get(void) } #endif + if (hole_punch(c)) + { + return false; + } + // find remote node number for (j = 1; j <= MAXNETNODES; j++) //include LAN { @@ -1319,17 +1375,14 @@ void I_ShutdownTcpDriver(void) } #ifndef NONET -static SINT8 SOCK_NetMakeNodewPort(const char *address, const char *port) +static boolean SOCK_GetAddr(struct sockaddr_in *sin, const char *address, const char *port, boolean test) { - SINT8 newnode = -1; struct my_addrinfo *ai = NULL, *runp, hints; int gaie; - if (!port || !port[0]) + if (!port || !port[0]) port = DEFAULTPORT; - DEBFILE(va("Creating new node: %s@%s\n", address, port)); - memset (&hints, 0x00, sizeof (hints)); hints.ai_flags = 0; hints.ai_family = AF_UNSPEC; @@ -1337,30 +1390,93 @@ static SINT8 SOCK_NetMakeNodewPort(const char *address, const char *port) hints.ai_protocol = IPPROTO_UDP; gaie = I_getaddrinfo(address, port, &hints, &ai); - if (gaie == 0) - { - newnode = getfreenode(); - } - if (newnode == -1) + + if (gaie != 0) { I_freeaddrinfo(ai); - return -1; + return false; + } + + runp = ai; + + if (test) + { + while (runp != NULL) + { + if (sendto(mysockets[0], NULL, 0, 0, runp->ai_addr, runp->ai_addrlen) == 0) + break; + + runp = runp->ai_next; + } + } + + if (runp != NULL) + memcpy(sin, runp->ai_addr, runp->ai_addrlen); + + I_freeaddrinfo(ai); + + return (runp != NULL); +} + +static SINT8 SOCK_NetMakeNodewPort(const char *address, const char *port) +{ + SINT8 newnode = getfreenode(); + + DEBFILE(va("Creating new node: %s@%s\n", address, port)); + + if (newnode != -1) + { + if (!SOCK_GetAddr(&clientaddress[newnode].ip4, address, port, true)) + { + nodeconnected[newnode] = false; + return -1; + } + } + + return newnode; +} + +/* See ../doc/Holepunch-Protocol.txt */ + +static void rendezvous(int size) +{ + char *addrs = strdup(cv_rendezvousserver.string); + + char *host = strtok(addrs, ":"); + char *port = strtok(NULL, ":"); + + mysockaddr_t rzv; + + if (SOCK_GetAddr(&rzv.ip4, host, (port ? port : "7777"), false)) + { + holepunchpacket->magic = hole_punch_magic; + sendto(mysockets[0], doomcom->data, size, 0, &rzv.any, sizeof rzv.ip4); } else - runp = ai; - - while (runp != NULL) { - // find ip of the server - if (sendto(mysockets[0], NULL, 0, 0, runp->ai_addr, runp->ai_addrlen) == 0) - { - memcpy(&clientaddress[newnode], runp->ai_addr, runp->ai_addrlen); - break; - } - runp = runp->ai_next; + CONS_Alert(CONS_ERROR, "Failed to contact rendezvous server (%s).\n", + cv_rendezvousserver.string); } - I_freeaddrinfo(ai); - return newnode; + + free(addrs); +} + +static void SOCK_RequestHolePunch(void) +{ + mysockaddr_t * addr = &clientaddress[doomcom->remotenode]; + + holepunchpacket->addr = addr->ip4.sin_addr.s_addr; + holepunchpacket->port = addr->ip4.sin_port; + + CONS_Debug(DBG_NETPLAY, + "requesting hole punch to node %s\n", SOCK_AddrToStr(addr)); + + rendezvous(10); +} + +static void SOCK_RegisterHolePunch(void) +{ + rendezvous(4); } #endif @@ -1387,6 +1503,9 @@ static boolean SOCK_OpenSocket(void) I_NetCanGet = SOCK_CanGet; #endif + I_NetRequestHolePunch = SOCK_RequestHolePunch; + I_NetRegisterHolePunch = SOCK_RegisterHolePunch; + // build the socket but close it first SOCK_CloseSocket(); return UDP_Socket(); diff --git a/src/mserv.c b/src/mserv.c index ab615711..24848f53 100644 --- a/src/mserv.c +++ b/src/mserv.c @@ -68,6 +68,7 @@ static CV_PossibleValue_t masterserver_update_rate_cons_t[] = { }; consvar_t cv_masterserver = {"masterserver", "https://ms.kartkrew.org/ms/api", CV_SAVE|CV_CALL, NULL, MasterServer_OnChange, 0, NULL, NULL, 0, 0, NULL}; +consvar_t cv_rendezvousserver = {"rendezvousserver", "relay.kartkrew.org", CV_SAVE, NULL, NULL, 0, NULL, NULL, 0, 0, NULL}; consvar_t cv_servername = {"servername", "SRB2Kart server", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Update_parameters, 0, NULL, NULL, 0, 0, NULL}; consvar_t cv_server_contact = {"server_contact", "", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Update_parameters, 0, NULL, NULL, 0, 0, NULL}; @@ -99,6 +100,7 @@ void AddMServCommands(void) CV_RegisterVar(&cv_masterserver_debug); CV_RegisterVar(&cv_masterserver_token); CV_RegisterVar(&cv_advertise); + CV_RegisterVar(&cv_rendezvousserver); CV_RegisterVar(&cv_servername); CV_RegisterVar(&cv_server_contact); #ifdef MASTERSERVER diff --git a/src/mserv.h b/src/mserv.h index 02aaf367..30aa73c0 100644 --- a/src/mserv.h +++ b/src/mserv.h @@ -58,6 +58,7 @@ extern consvar_t cv_masterserver_update_rate; extern consvar_t cv_masterserver_timeout; extern consvar_t cv_masterserver_debug; extern consvar_t cv_masterserver_token; +extern consvar_t cv_rendezvousserver; extern consvar_t cv_advertise;