diff --git a/CMakeLists.txt b/CMakeLists.txt
index 02576286b..77eaeb3b2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1727,6 +1727,7 @@ set_src(ENGINE_SHARED GLOB_RECURSE src/engine/shared
network_console.cpp
network_console_conn.cpp
network_server.cpp
+ network_stun.cpp
packer.cpp
packer.h
protocol.h
@@ -1740,6 +1741,8 @@ set_src(ENGINE_SHARED GLOB_RECURSE src/engine/shared
snapshot.cpp
snapshot.h
storage.cpp
+ stun.cpp
+ stun.h
teehistorian_ex.cpp
teehistorian_ex.h
teehistorian_ex_chunks.h
@@ -2362,6 +2365,7 @@ if(TOOLS)
map_replace_image.cpp
map_resave.cpp
packetgen.cpp
+ stun.cpp
unicode_confusables.cpp
uuid.cpp
)
diff --git a/src/base/system.h b/src/base/system.h
index 61e89c92c..dbe5a5ef4 100644
--- a/src/base/system.h
+++ b/src/base/system.h
@@ -806,7 +806,7 @@ enum
/**
* @ingroup Network-General
*/
-typedef struct
+typedef struct NETADDR
{
unsigned int type;
unsigned char ip[16];
diff --git a/src/engine/client.h b/src/engine/client.h
index 111e3535b..2db6c5f9d 100644
--- a/src/engine/client.h
+++ b/src/engine/client.h
@@ -31,6 +31,7 @@ class IClient : public IInterface
protected:
// quick access to state of the client
int m_State;
+ int64_t m_StateStartTime;
// quick access to time variables
int m_PrevGameTick[NUM_DUMMIES];
@@ -91,8 +92,20 @@ public:
STATE_RESTARTING,
};
+ enum
+ {
+ CONNECTIVITY_UNKNOWN,
+ CONNECTIVITY_CHECKING,
+ CONNECTIVITY_UNREACHABLE,
+ CONNECTIVITY_REACHABLE,
+ // Different global IP address has been detected for UDP and
+ // TCP connections.
+ CONNECTIVITY_DIFFERING_UDP_TCP_IP_ADDRESSES,
+ };
+
//
inline int State() const { return m_State; }
+ inline int64_t StateStartTime() const { return m_StateStartTime; }
// tick time access
inline int PrevGameTick(int Conn) const { return m_PrevGameTick[Conn]; }
@@ -148,7 +161,8 @@ public:
virtual void EnterGame(int Conn) = 0;
//
- virtual const char *ServerAddress() const = 0;
+ virtual NETADDR ServerAddress() const = 0;
+ virtual const char *ConnectAddressString() const = 0;
virtual const char *MapDownloadName() const = 0;
virtual int MapDownloadAmount() const = 0;
virtual int MapDownloadTotalsize() const = 0;
@@ -237,6 +251,7 @@ public:
virtual SWarning *GetCurWarning() = 0;
virtual CChecksumData *ChecksumData() = 0;
virtual bool InfoTaskRunning() = 0;
+ virtual int UdpConnectivity(int NetType) = 0;
};
class IGameClient : public IInterface
diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp
index df427af59..a9815bd7f 100644
--- a/src/engine/client/client.cpp
+++ b/src/engine/client/client.cpp
@@ -396,7 +396,8 @@ CClient::CClient() :
mem_zero(&m_aInputs, sizeof(m_aInputs));
m_State = IClient::STATE_OFFLINE;
- m_aServerAddressStr[0] = 0;
+ m_StateStartTime = time_get();
+ m_aConnectAddressStr[0] = 0;
mem_zero(m_aSnapshots, sizeof(m_aSnapshots));
m_SnapshotStorage[0].Init();
@@ -655,6 +656,7 @@ void CClient::SetState(int s)
m_State = s;
if(Old != s)
{
+ m_StateStartTime = time_get();
GameClient()->OnStateChange(m_State, Old);
if(s == IClient::STATE_OFFLINE && m_ReconnectTime == 0)
@@ -771,20 +773,20 @@ void CClient::Connect(const char *pAddress, const char *pPassword)
Disconnect();
m_ConnectionID = RandomUuid();
- if(pAddress != m_aServerAddressStr)
- str_copy(m_aServerAddressStr, pAddress, sizeof(m_aServerAddressStr));
+ if(pAddress != m_aConnectAddressStr)
+ str_copy(m_aConnectAddressStr, pAddress, sizeof(m_aConnectAddressStr));
- str_format(aBuf, sizeof(aBuf), "connecting to '%s'", m_aServerAddressStr);
+ str_format(aBuf, sizeof(aBuf), "connecting to '%s'", m_aConnectAddressStr);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf, ClientNetworkPrintColor);
bool is_websocket = false;
- if(strncmp(m_aServerAddressStr, "ws://", 5) == 0)
+ if(strncmp(m_aConnectAddressStr, "ws://", 5) == 0)
{
is_websocket = true;
- str_copy(m_aServerAddressStr, pAddress + 5, sizeof(m_aServerAddressStr));
+ str_copy(m_aConnectAddressStr, pAddress + 5, sizeof(m_aConnectAddressStr));
}
ServerInfoRequest();
- if(net_host_lookup(m_aServerAddressStr, &m_ServerAddress, m_NetClient[CONN_MAIN].NetType()) != 0)
+ if(net_host_lookup(m_aConnectAddressStr, &m_ServerAddress, m_NetClient[CONN_MAIN].NetType()) != 0)
{
char aBufMsg[256];
str_format(aBufMsg, sizeof(aBufMsg), "could not find the address of %s, connecting to localhost", aBuf);
@@ -815,6 +817,7 @@ void CClient::Connect(const char *pAddress, const char *pPassword)
m_ServerAddress.type = NETTYPE_WEBSOCKET_IPV4;
}
m_NetClient[CONN_MAIN].Connect(&m_ServerAddress);
+ m_NetClient[CONN_MAIN].RefreshStun();
SetState(IClient::STATE_CONNECTING);
for(int i = 0; i < RECORDER_MAX; i++)
@@ -2340,16 +2343,17 @@ void CClient::LoadDDNetInfo()
if(!pDDNetInfo)
return;
- const json_value *pVersion = json_object_get(pDDNetInfo, "version");
- if(pVersion->type == json_string)
+ const json_value &DDNetInfo = *pDDNetInfo;
+ const json_value &CurrentVersion = DDNetInfo["version"];
+ if(CurrentVersion.type == json_string)
{
char aNewVersionStr[64];
- str_copy(aNewVersionStr, json_string_get(pVersion), sizeof(aNewVersionStr));
+ str_copy(aNewVersionStr, CurrentVersion, sizeof(aNewVersionStr));
char aCurVersionStr[64];
str_copy(aCurVersionStr, GAME_RELEASE_VERSION, sizeof(aCurVersionStr));
if(ToVersion(aNewVersionStr) > ToVersion(aCurVersionStr))
{
- str_copy(m_aVersionStr, json_string_get(pVersion), sizeof(m_aVersionStr));
+ str_copy(m_aVersionStr, CurrentVersion, sizeof(m_aVersionStr));
}
else
{
@@ -2358,28 +2362,57 @@ void CClient::LoadDDNetInfo()
}
}
- const json_value *pNews = json_object_get(pDDNetInfo, "news");
- if(pNews->type == json_string)
+ const json_value &News = DDNetInfo["news"];
+ if(News.type == json_string)
{
- const char *pNewsString = json_string_get(pNews);
-
// Only mark news button if something new was added to the news
- if(m_aNews[0] && str_find(m_aNews, pNewsString) == nullptr)
+ if(m_aNews[0] && str_find(m_aNews, News) == nullptr)
g_Config.m_UiUnreadNews = true;
- str_copy(m_aNews, pNewsString, sizeof(m_aNews));
+ str_copy(m_aNews, News, sizeof(m_aNews));
}
- const json_value *pMapDownloadUrl = json_object_get(pDDNetInfo, "map-download-url");
- if(pMapDownloadUrl->type == json_string)
+ const json_value &MapDownloadUrl = DDNetInfo["map-download-url"];
+ if(MapDownloadUrl.type == json_string)
{
- const char *pMapDownloadUrlString = json_string_get(pMapDownloadUrl);
- str_copy(m_aMapDownloadUrl, pMapDownloadUrlString, sizeof(m_aMapDownloadUrl));
+ str_copy(m_aMapDownloadUrl, MapDownloadUrl, sizeof(m_aMapDownloadUrl));
}
- const json_value *pPoints = json_object_get(pDDNetInfo, "points");
- if(pPoints->type == json_integer)
- m_Points = pPoints->u.integer;
+ const json_value &Points = DDNetInfo["points"];
+ if(Points.type == json_integer)
+ {
+ m_Points = Points.u.integer;
+ }
+
+ const json_value &StunServersIpv6 = DDNetInfo["stun-servers-ipv6"];
+ if(StunServersIpv6.type == json_array && StunServersIpv6[0].type == json_string)
+ {
+ NETADDR Addr;
+ if(!net_addr_from_str(&Addr, StunServersIpv6[0]))
+ {
+ m_NetClient->FeedStunServer(Addr);
+ }
+ }
+ const json_value &StunServersIpv4 = DDNetInfo["stun-servers-ipv4"];
+ if(StunServersIpv4.type == json_array && StunServersIpv4[0].type == json_string)
+ {
+ NETADDR Addr;
+ if(!net_addr_from_str(&Addr, StunServersIpv4[0]))
+ {
+ m_NetClient->FeedStunServer(Addr);
+ }
+ }
+ const json_value &ConnectingIp = DDNetInfo["connecting-ip"];
+ if(ConnectingIp.type == json_string)
+ {
+ NETADDR Addr;
+ if(!net_addr_from_str(&Addr, ConnectingIp))
+ {
+ m_HaveGlobalTcpAddr = true;
+ m_GlobalTcpAddr = Addr;
+ log_debug("info", "got global tcp ip address: %s", (const char *)ConnectingIp);
+ }
+ }
}
void CClient::PumpNetwork()
@@ -2812,7 +2845,7 @@ void CClient::Update()
if(m_ReconnectTime > 0 && time_get() > m_ReconnectTime)
{
if(State() != STATE_ONLINE)
- Connect(m_aServerAddressStr);
+ Connect(m_aConnectAddressStr);
m_ReconnectTime = 0;
}
@@ -4671,3 +4704,30 @@ int CClient::PredictionMargin() const
{
return m_ServerCapabilities.m_SyncWeaponInput ? g_Config.m_ClPredictionMargin : 10;
}
+
+int CClient::UdpConnectivity(int NetType)
+{
+ NETADDR GlobalUdpAddr;
+ CONNECTIVITY Connectivity = m_NetClient->GetConnectivity(NetType, &GlobalUdpAddr);
+ GlobalUdpAddr.port = 0;
+ switch(Connectivity)
+ {
+ case CONNECTIVITY::UNKNOWN:
+ return CONNECTIVITY_UNKNOWN;
+ case CONNECTIVITY::CHECKING:
+ return CONNECTIVITY_CHECKING;
+ case CONNECTIVITY::UNREACHABLE:
+ return CONNECTIVITY_UNREACHABLE;
+ case CONNECTIVITY::REACHABLE:
+ return CONNECTIVITY_REACHABLE;
+ case CONNECTIVITY::ADDRESS_KNOWN:
+ if(m_HaveGlobalTcpAddr && NetType == (int)m_GlobalTcpAddr.type && net_addr_comp(&m_GlobalTcpAddr, &GlobalUdpAddr) != 0)
+ {
+ return CONNECTIVITY_DIFFERING_UDP_TCP_IP_ADDRESSES;
+ }
+ return CONNECTIVITY_REACHABLE;
+ default:
+ dbg_assert(0, "invalid connectivity value");
+ return CONNECTIVITY_UNKNOWN;
+ }
+}
diff --git a/src/engine/client/client.h b/src/engine/client/client.h
index 4f1b68b4a..a6e84b505 100644
--- a/src/engine/client/client.h
+++ b/src/engine/client/client.h
@@ -129,10 +129,13 @@ class CClient : public IClient, public CDemoPlayer::IListener
class CFriends m_Friends;
class CFriends m_Foes;
- char m_aServerAddressStr[256];
+ char m_aConnectAddressStr[256];
CUuid m_ConnectionID;
+ bool m_HaveGlobalTcpAddr = false;
+ NETADDR m_GlobalTcpAddr;
+
unsigned m_SnapshotParts[NUM_DUMMIES];
int64_t m_LocalStartTime;
@@ -394,7 +397,8 @@ public:
void FinishDDNetInfo();
void LoadDDNetInfo();
- const char *ServerAddress() const override { return m_aServerAddressStr; }
+ NETADDR ServerAddress() const override { return m_ServerAddress; }
+ const char *ConnectAddressString() const override { return m_aConnectAddressStr; }
const char *MapDownloadName() const override { return m_aMapdownloadName; }
int MapDownloadAmount() const override { return !m_pMapdownloadTask ? m_MapdownloadAmount : (int)m_pMapdownloadTask->Current(); }
int MapDownloadTotalsize() const override { return !m_pMapdownloadTask ? m_MapdownloadTotalsize : (int)m_pMapdownloadTask->Size(); }
@@ -527,6 +531,7 @@ public:
SWarning *GetCurWarning() override;
CChecksumData *ChecksumData() override { return &m_Checksum.m_Data; }
bool InfoTaskRunning() override { return m_pDDNetInfoTask != nullptr; }
+ int UdpConnectivity(int NetType) override;
};
#endif
diff --git a/src/engine/shared/network.h b/src/engine/shared/network.h
index 01ee71237..cfd043cb3 100644
--- a/src/engine/shared/network.h
+++ b/src/engine/shared/network.h
@@ -5,6 +5,7 @@
#include "huffman.h"
#include "ringbuffer.h"
+#include "stun.h"
#include
@@ -157,6 +158,49 @@ public:
unsigned char m_aExtraData[4];
};
+enum class CONNECTIVITY
+{
+ UNKNOWN,
+ CHECKING,
+ UNREACHABLE,
+ REACHABLE,
+ ADDRESS_KNOWN,
+};
+
+class CStun
+{
+ class CProtocol
+ {
+ int m_Index;
+ NETSOCKET m_Socket;
+ CStunData m_Stun;
+ bool m_HaveStunServer = false;
+ NETADDR m_StunServer;
+ bool m_HaveAddr = false;
+ NETADDR m_Addr;
+ int64_t m_LastResponse = -1;
+ int64_t m_NextTry = -1;
+ int m_NumUnsuccessfulTries = -1;
+
+ public:
+ CProtocol(int Index, NETSOCKET Socket);
+ void FeedStunServer(NETADDR StunServer);
+ void Refresh();
+ void Update();
+ bool OnPacket(NETADDR Addr, unsigned char *pData, int DataSize);
+ CONNECTIVITY GetConnectivity(NETADDR *pGlobalAddr);
+ };
+ CProtocol m_aProtocols[2];
+
+public:
+ CStun(NETSOCKET Socket);
+ void FeedStunServer(NETADDR StunServer);
+ void Refresh();
+ void Update();
+ bool OnPacket(NETADDR Addr, unsigned char *pData, int DataSize);
+ CONNECTIVITY GetConnectivity(int NetType, NETADDR *pGlobalAddr);
+};
+
class CNetConnection
{
// TODO: is this needed because this needs to be aware of
@@ -430,6 +474,8 @@ class CNetClient
CNetConnection m_Connection;
CNetRecvUnpacker m_RecvUnpacker;
+ CStun *m_pStun = nullptr;
+
public:
NETSOCKET m_Socket;
// openness
@@ -457,6 +503,11 @@ public:
const char *ErrorString() const;
bool SecurityTokenUnknown() { return m_Connection.SecurityToken() == NET_SECURITY_TOKEN_UNKNOWN; }
+
+ // stun
+ void FeedStunServer(NETADDR StunServer);
+ void RefreshStun();
+ CONNECTIVITY GetConnectivity(int NetType, NETADDR *pGlobalAddr);
};
// TODO: both, fix these. This feels like a junk class for stuff that doesn't fit anywere
diff --git a/src/engine/shared/network_client.cpp b/src/engine/shared/network_client.cpp
index b338a5db6..8722ee346 100644
--- a/src/engine/shared/network_client.cpp
+++ b/src/engine/shared/network_client.cpp
@@ -16,6 +16,7 @@ bool CNetClient::Open(NETADDR BindAddr)
// init
m_Socket = Socket;
+ m_pStun = new CStun(m_Socket);
m_Connection.Init(m_Socket, false);
return true;
@@ -25,6 +26,11 @@ int CNetClient::Close()
{
if(!m_Socket)
return 0;
+ if(m_pStun)
+ {
+ delete m_pStun;
+ m_pStun = nullptr;
+ }
return net_udp_close(m_Socket);
}
@@ -40,6 +46,7 @@ int CNetClient::Update()
m_Connection.Update();
if(m_Connection.State() == NET_CONNSTATE_ERROR)
Disconnect(m_Connection.ErrorString());
+ m_pStun->Update();
return 0;
}
@@ -72,6 +79,11 @@ int CNetClient::Recv(CNetChunk *pChunk)
if(Bytes <= 0)
break;
+ if(m_pStun->OnPacket(Addr, pData, Bytes))
+ {
+ continue;
+ }
+
bool Sixup = false;
if(CNetBase::UnpackPacket(pData, Bytes, &m_RecvUnpacker.m_Data, Sixup) == 0)
{
@@ -154,3 +166,18 @@ const char *CNetClient::ErrorString() const
{
return m_Connection.ErrorString();
}
+
+void CNetClient::FeedStunServer(NETADDR StunServer)
+{
+ m_pStun->FeedStunServer(StunServer);
+}
+
+void CNetClient::RefreshStun()
+{
+ m_pStun->Refresh();
+}
+
+CONNECTIVITY CNetClient::GetConnectivity(int NetType, NETADDR *pGlobalAddr)
+{
+ return m_pStun->GetConnectivity(NetType, pGlobalAddr);
+}
diff --git a/src/engine/shared/network_stun.cpp b/src/engine/shared/network_stun.cpp
new file mode 100644
index 000000000..7f9db550c
--- /dev/null
+++ b/src/engine/shared/network_stun.cpp
@@ -0,0 +1,186 @@
+#include "network.h"
+
+#include
+#include
+
+static int IndexFromNetType(int NetType)
+{
+ switch(NetType)
+ {
+ case NETTYPE_IPV6:
+ return 0;
+ case NETTYPE_IPV4:
+ return 1;
+ }
+ return -1;
+}
+
+static const char *IndexToSystem(int Index)
+{
+ switch(Index)
+ {
+ case 0:
+ return "stun/6";
+ case 1:
+ return "stun/4";
+ }
+ dbg_break();
+ return nullptr;
+}
+
+static int RetryWaitSeconds(int NumUnsuccessfulTries)
+{
+ return (1 << clamp(0, NumUnsuccessfulTries, 9));
+}
+
+CStun::CProtocol::CProtocol(int Index, NETSOCKET Socket) :
+ m_Index(Index),
+ m_Socket(Socket)
+{
+ // Initialize `m_Stun` with random data.
+ unsigned char aBuf[32];
+ StunMessagePrepare(aBuf, sizeof(aBuf), &m_Stun);
+}
+
+void CStun::CProtocol::FeedStunServer(NETADDR StunServer)
+{
+ if(net_addr_comp(&m_StunServer, &StunServer) == 0)
+ {
+ return;
+ }
+ m_HaveStunServer = true;
+ m_StunServer = StunServer;
+ m_NumUnsuccessfulTries = 0;
+ Refresh();
+}
+
+void CStun::CProtocol::Refresh()
+{
+ m_NextTry = time_get();
+}
+
+void CStun::CProtocol::Update()
+{
+ int64_t Now = time_get();
+ if(m_NextTry == -1 || Now < m_NextTry || !m_HaveStunServer)
+ {
+ return;
+ }
+ m_NextTry = Now + RetryWaitSeconds(m_NumUnsuccessfulTries) * time_freq();
+ m_NumUnsuccessfulTries += 1;
+ unsigned char aBuf[32];
+ int Size = StunMessagePrepare(aBuf, sizeof(aBuf), &m_Stun);
+ if(net_udp_send(m_Socket, &m_StunServer, aBuf, Size) == -1)
+ {
+ log_error(IndexToSystem(m_Index), "couldn't send stun request");
+ return;
+ }
+}
+
+bool CStun::CProtocol::OnPacket(NETADDR Addr, unsigned char *pData, int DataSize)
+{
+ if(m_NextTry < 0 || !m_HaveStunServer)
+ {
+ return false;
+ }
+ bool Success;
+ NETADDR StunAddr;
+ if(StunMessageParse(pData, DataSize, &m_Stun, &Success, &StunAddr))
+ {
+ return false;
+ }
+ int64_t Now = time_get();
+ int64_t Freq = time_freq();
+ m_LastResponse = Now;
+ if(!Success)
+ {
+ m_HaveAddr = false;
+ log_debug(IndexToSystem(m_Index), "got error response");
+ return true;
+ }
+ m_NextTry = Now + 600 * Freq;
+ m_NumUnsuccessfulTries = -1;
+ m_HaveAddr = true;
+ m_Addr = StunAddr;
+
+ char aStunAddr[NETADDR_MAXSTRSIZE];
+ net_addr_str(&StunAddr, aStunAddr, sizeof(aStunAddr), true);
+ log_debug(IndexToSystem(m_Index), "got address: %s", aStunAddr);
+ return true;
+}
+
+CONNECTIVITY CStun::CProtocol::GetConnectivity(NETADDR *pGlobalAddr)
+{
+ if(!m_HaveStunServer)
+ {
+ return CONNECTIVITY::UNKNOWN;
+ }
+ int64_t Now = time_get();
+ int64_t Freq = time_freq();
+ bool HaveTriedALittle = m_NumUnsuccessfulTries >= 5 && (m_LastResponse == -1 || Now - m_LastResponse >= 30 * Freq);
+ if(m_LastResponse == -1 && !HaveTriedALittle)
+ {
+ return CONNECTIVITY::CHECKING;
+ }
+ else if(HaveTriedALittle)
+ {
+ return CONNECTIVITY::UNREACHABLE;
+ }
+ else if(!m_HaveAddr)
+ {
+ return CONNECTIVITY::REACHABLE;
+ }
+ else
+ {
+ *pGlobalAddr = m_Addr;
+ return CONNECTIVITY::ADDRESS_KNOWN;
+ }
+}
+
+CStun::CStun(NETSOCKET Socket) :
+ m_aProtocols{CProtocol(0, Socket), CProtocol(1, Socket)}
+{
+}
+
+void CStun::FeedStunServer(NETADDR StunServer)
+{
+ int Index = IndexFromNetType(StunServer.type);
+ if(Index < 0)
+ {
+ return;
+ }
+ m_aProtocols[Index].FeedStunServer(StunServer);
+}
+
+void CStun::Refresh()
+{
+ for(auto &Protocol : m_aProtocols)
+ {
+ Protocol.Refresh();
+ }
+}
+
+void CStun::Update()
+{
+ for(auto &Protocol : m_aProtocols)
+ {
+ Protocol.Update();
+ }
+}
+
+bool CStun::OnPacket(NETADDR Addr, unsigned char *pData, int DataSize)
+{
+ int Index = IndexFromNetType(Addr.type);
+ if(Index < 0)
+ {
+ return false;
+ }
+ return m_aProtocols[Index].OnPacket(Addr, pData, DataSize);
+}
+
+CONNECTIVITY CStun::GetConnectivity(int NetType, NETADDR *pGlobalAddr)
+{
+ int Index = IndexFromNetType(NetType);
+ dbg_assert(Index != -1, "invalid nettype");
+ return m_aProtocols[Index].GetConnectivity(pGlobalAddr);
+}
diff --git a/src/engine/shared/stun.cpp b/src/engine/shared/stun.cpp
new file mode 100644
index 000000000..937007c3f
--- /dev/null
+++ b/src/engine/shared/stun.cpp
@@ -0,0 +1,158 @@
+#include "stun.h"
+
+#include
+
+// STUN header (from RFC 5389, section 6, figure 2):
+//
+// 0 1 2 3
+// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// |0 0| STUN Message Type | Message Length |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// | Magic Cookie |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// | |
+// | Transaction ID (96 bits) |
+// | |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+size_t StunMessagePrepare(unsigned char *pBuffer, size_t BufferSize, CStunData *pData)
+{
+ dbg_assert(BufferSize >= 20, "stun message buffer too small");
+ secure_random_fill(pData->m_aSecret, sizeof(pData->m_aSecret));
+ pBuffer[0] = 0x00; // STUN Request Message Type 1: Binding
+ pBuffer[1] = 0x01;
+ pBuffer[2] = 0x00; // Message Length: 0 (extra bytes after the header)
+ pBuffer[3] = 0x00;
+ pBuffer[4] = 0x21; // Magic Cookie: 0x2112A442
+ pBuffer[5] = 0x12;
+ pBuffer[6] = 0xA4;
+ pBuffer[7] = 0x42;
+ mem_copy(pBuffer + 8, pData->m_aSecret, sizeof(pData->m_aSecret)); // Transaction ID
+ return 20;
+}
+
+bool StunMessageParse(const unsigned char *pMessage, size_t MessageSize, const CStunData *pData, bool *pSuccess, NETADDR *pAddr)
+{
+ *pSuccess = false;
+ mem_zero(pAddr, sizeof(*pAddr));
+ if(MessageSize < 20)
+ {
+ return true;
+ }
+ bool Parsed = true;
+ // STUN Success/Error Response Type 1: Binding
+ Parsed = Parsed && pMessage[0] == 0x01 && (pMessage[1] == 0x01 || pMessage[1] == 0x11);
+ uint16_t MessageLength = (pMessage[2] << 8) | pMessage[3];
+ Parsed = Parsed && MessageSize >= 20 + (size_t)MessageLength && MessageLength % 4 == 0;
+ // Magic Cookie: 0x2112A442
+ Parsed = Parsed && pMessage[4] == 0x21 && pMessage[5] == 0x12;
+ Parsed = Parsed && pMessage[6] == 0xA4 && pMessage[7] == 0x42;
+ // Transaction ID
+ Parsed = Parsed && mem_comp(pMessage + 8, pData->m_aSecret, sizeof(pData->m_aSecret)) == 0;
+ if(!Parsed)
+ {
+ return true;
+ }
+
+ *pSuccess = pMessage[1] == 0x01;
+
+ MessageSize = 20 + MessageLength;
+ size_t Offset = 20;
+ bool FoundAddr = false;
+ while(true)
+ {
+ // STUN attribute format (from RFC 5389, section 15, figure 4):
+ //
+ // 0 1 2 3
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | Type | Length |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | Value (variable) ....
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ if(MessageSize == Offset)
+ {
+ break;
+ }
+ else if(MessageSize < Offset + 4)
+ {
+ return true;
+ }
+ uint16_t Type = (pMessage[Offset] << 8) | pMessage[Offset + 1];
+ uint16_t Length = (pMessage[Offset + 2] << 8) | pMessage[Offset + 3];
+ if(MessageSize < Offset + 4 + Length)
+ {
+ return true;
+ }
+ if(*pSuccess && Type == 0x0020) // XOR-MAPPED-ADDRESS
+ {
+ // STUN XOR-MAPPED-ADDRESS attribute format (from RFC 5389, section 15,
+ // figure 6):
+ //
+ // 0 1 2 3
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // |x x x x x x x x| Family | X-Port |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | X-Address (Variable)
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+ if(Length < 4)
+ {
+ return true;
+ }
+ // Only use the first found address.
+ uint8_t Family = pMessage[Offset + 4 + 1];
+ uint16_t Port = (pMessage[Offset + 4 + 2] << 8) | pMessage[Offset + 4 + 3];
+ Port ^= 0x2112;
+ if(Family == 0x01) // IPv4
+ {
+ if(Length != 8)
+ {
+ return true;
+ }
+ if(!FoundAddr)
+ {
+ pAddr->type = NETTYPE_IPV4;
+ mem_copy(pAddr->ip, pMessage + Offset + 4 + 4, 4);
+ pAddr->ip[0] ^= 0x21;
+ pAddr->ip[1] ^= 0x12;
+ pAddr->ip[2] ^= 0xA4;
+ pAddr->ip[3] ^= 0x42;
+ pAddr->port = Port;
+ FoundAddr = true;
+ }
+ }
+ else if(Family == 0x02) // IPv6
+ {
+ if(Length != 20)
+ {
+ return true;
+ }
+ if(!FoundAddr)
+ {
+ pAddr->type = NETTYPE_IPV6;
+ mem_copy(pAddr->ip, pMessage + Offset + 4 + 4, 16);
+ pAddr->ip[0] ^= 0x21;
+ pAddr->ip[1] ^= 0x12;
+ pAddr->ip[2] ^= 0xA4;
+ pAddr->ip[3] ^= 0x42;
+ for(size_t i = 0; i < sizeof(pData->m_aSecret); i++)
+ {
+ pAddr->ip[4 + i] ^= pData->m_aSecret[i];
+ }
+ pAddr->port = Port;
+ FoundAddr = true;
+ }
+ }
+ }
+ // comprehension-required
+ else if(Type <= 0x7fff)
+ {
+ return true;
+ }
+ Offset += 4 + Length;
+ }
+ return *pSuccess && !FoundAddr;
+}
diff --git a/src/engine/shared/stun.h b/src/engine/shared/stun.h
new file mode 100644
index 000000000..a27f9824d
--- /dev/null
+++ b/src/engine/shared/stun.h
@@ -0,0 +1,15 @@
+#ifndef ENGINE_SHARED_STUN_H
+#define ENGINE_SHARED_STUN_H
+#include
+
+struct NETADDR;
+
+class CStunData
+{
+public:
+ unsigned char m_aSecret[12];
+};
+
+size_t StunMessagePrepare(unsigned char *pBuffer, size_t BufferSize, CStunData *pData);
+bool StunMessageParse(const unsigned char *pMessage, size_t MessageSize, const CStunData *pData, bool *pSuccess, NETADDR *pAddr);
+#endif // ENGINE_SHARED_STUN_H
diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp
index 5221a0fb1..3944a32cf 100644
--- a/src/game/client/components/menus.cpp
+++ b/src/game/client/components/menus.cpp
@@ -1462,9 +1462,36 @@ int CMenus::Render()
else if(m_Popup == POPUP_CONNECTING)
{
pTitle = Localize("Connecting to");
- pExtraText = Client()->ServerAddress();
+ pExtraText = Client()->ConnectAddressString();
pButtonText = Localize("Abort");
- if(Client()->MapDownloadTotalsize() > 0)
+ if(Client()->State() == IClient::STATE_CONNECTING && time_get() - Client()->StateStartTime() > time_freq())
+ {
+ int Connectivity = Client()->UdpConnectivity(Client()->ServerAddress().type);
+ const char *pMessage = nullptr;
+ switch(Connectivity)
+ {
+ case IClient::CONNECTIVITY_UNKNOWN:
+ break;
+ case IClient::CONNECTIVITY_CHECKING:
+ pMessage = Localize("Trying to determine UDP connectivity...");
+ break;
+ case IClient::CONNECTIVITY_UNREACHABLE:
+ pMessage = Localize("UDP seems to be filtered.");
+ break;
+ case IClient::CONNECTIVITY_DIFFERING_UDP_TCP_IP_ADDRESSES:
+ pMessage = Localize("UDP and TCP IP addresses seem to be different. Try disabling VPN, proxy or network accelerators.");
+ break;
+ case IClient::CONNECTIVITY_REACHABLE:
+ pMessage = Localize("No answer from server yet.");
+ break;
+ }
+ if(pMessage)
+ {
+ str_format(aBuf, sizeof(aBuf), "%s\n\n%s", Client()->ConnectAddressString(), pMessage);
+ pExtraText = aBuf;
+ }
+ }
+ else if(Client()->MapDownloadTotalsize() > 0)
{
str_format(aBuf, sizeof(aBuf), "%s: %s", Localize("Downloading map"), Client()->MapDownloadName());
pTitle = aBuf;
@@ -2475,7 +2502,9 @@ void CMenus::OnStateChange(int NewState, int OldState)
m_DownloadSpeed = 0.0f;
}
else if(NewState == IClient::STATE_CONNECTING)
+ {
m_Popup = POPUP_CONNECTING;
+ }
else if(NewState == IClient::STATE_ONLINE || NewState == IClient::STATE_DEMOPLAYBACK)
{
if(m_Popup != POPUP_WARNING)
diff --git a/src/tools/stun.cpp b/src/tools/stun.cpp
new file mode 100644
index 000000000..cab2a5941
--- /dev/null
+++ b/src/tools/stun.cpp
@@ -0,0 +1,94 @@
+#include
+#include
+#include
+
+int main(int argc, const char **argv)
+{
+ cmdline_fix(&argc, &argv);
+
+ secure_random_init();
+ log_set_global_logger_default();
+
+ if(argc < 2)
+ {
+ log_info("stun", "usage: %s ", argc > 0 ? argv[0] : "stun");
+ return 1;
+ }
+ NETADDR Addr;
+ if(net_addr_from_str(&Addr, argv[1]))
+ {
+ log_error("stun", "couldn't parse address");
+ return 2;
+ }
+
+ net_init();
+ NETADDR BindAddr;
+ mem_zero(&BindAddr, sizeof(BindAddr));
+ BindAddr.type = NETTYPE_ALL;
+ NETSOCKET Socket = net_udp_create(BindAddr);
+ if(net_socket_type(Socket) == NETTYPE_INVALID)
+ {
+ log_error("stun", "couldn't open udp socket");
+ return 2;
+ }
+
+ CStunData Stun;
+ unsigned char aRequest[32];
+ int RequestSize = StunMessagePrepare(aRequest, sizeof(aRequest), &Stun);
+ if(net_udp_send(Socket, &Addr, aRequest, RequestSize) == -1)
+ {
+ log_error("stun", "failed sending stun request");
+ return 2;
+ }
+
+ NETADDR ResponseAddr;
+ unsigned char *pResponse;
+ while(true)
+ {
+ if(!net_socket_read_wait(Socket, 1000000))
+ {
+ log_error("stun", "no udp message received from server until timeout");
+ return 3;
+ }
+ int ResponseSize = net_udp_recv(Socket, &ResponseAddr, &pResponse);
+ if(ResponseSize == -1)
+ {
+ log_error("stun", "failed receiving udp message");
+ return 2;
+ }
+ else if(ResponseSize == 0)
+ {
+ continue;
+ }
+ if(net_addr_comp(&Addr, &ResponseAddr) != 0)
+ {
+ char aResponseAddr[NETADDR_MAXSTRSIZE];
+ char aAddr[NETADDR_MAXSTRSIZE];
+ net_addr_str(&ResponseAddr, aResponseAddr, sizeof(aResponseAddr), true);
+ net_addr_str(&Addr, aAddr, sizeof(aAddr), true);
+ log_debug("stun", "got message from %s while expecting one from %s", aResponseAddr, aAddr);
+ continue;
+ }
+ bool Success;
+ NETADDR StunAddr;
+ if(StunMessageParse(pResponse, ResponseSize, &Stun, &Success, &StunAddr))
+ {
+ log_debug("stun", "received message from stun server that was not understood");
+ continue;
+ }
+ if(Success)
+ {
+ char aStunAddr[NETADDR_MAXSTRSIZE];
+ net_addr_str(&StunAddr, aStunAddr, sizeof(aStunAddr), 1);
+ log_info("stun", "public ip address: %s", aStunAddr);
+ break;
+ }
+ else
+ {
+ log_info("stun", "error response from stun server");
+ break;
+ }
+ }
+
+ cmdline_free(argc, argv);
+}