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); +}