Changeset View
Standalone View
source/network/StunClient.cpp
/* Copyright (C) 2017 Wildfire Games. | /* Copyright (C) 2019 Wildfire Games. | ||||
* Copyright (C) 2013-2016 SuperTuxKart-Team. | * Copyright (C) 2013-2016 SuperTuxKart-Team. | ||||
* This file is part of 0 A.D. | * This file is part of 0 A.D. | ||||
* | * | ||||
* 0 A.D. is free software: you can redistribute it and/or modify | * 0 A.D. is free software: you can redistribute it and/or modify | ||||
* it under the terms of the GNU General Public License as published by | * it under the terms of the GNU General Public License as published by | ||||
* the Free Software Foundation, either version 2 of the License, or | * the Free Software Foundation, either version 2 of the License, or | ||||
* (at your option) any later version. | * (at your option) any later version. | ||||
* | * | ||||
▲ Show 20 Lines • Show All 109 Lines • ▼ Show 20 Lines | while (a--) | ||||
if (n > 1) | if (n > 1) | ||||
result <<= 8; | result <<= 8; | ||||
result += buffer[offset - 1 - a]; | result += buffer[offset - 1 - a]; | ||||
} | } | ||||
return true; | return true; | ||||
} | } | ||||
static void IPv4ToString(u32 ip, char* ipStr, size_t ipStrlen) | |||||
{ | |||||
sockaddr_in addr = {}; | |||||
addr.sin_family = AF_INET; | |||||
addr.sin_addr.s_addr = htonl(ip); | |||||
getnameinfo(reinterpret_cast<sockaddr *>(&addr), sizeof(addr), ipStr, ipStrlen, nullptr, 0, NI_NUMERICHOST); | |||||
} | |||||
static int HostnameToIPv4(const char* hostname, int ai_flags, u32* ip) | |||||
{ | |||||
addrinfo hints = {}; | |||||
hints.ai_flags = ai_flags; | |||||
hints.ai_family = AF_INET; | |||||
hints.ai_socktype = SOCK_DGRAM; | |||||
addrinfo* res; | |||||
if (int status = getaddrinfo(hostname, nullptr, &hints, &res)) | |||||
{ | |||||
#ifdef UNICODE | |||||
LOGERROR("HostnameToIPv4: Error in getaddrinfo: %s", utf8_from_wstring(gai_strerror(status))); | |||||
#else | |||||
LOGERROR("HostnameToIPv4: Error in getaddrinfo: %s", gai_strerror(status)); | |||||
#endif | |||||
return false; | |||||
} | |||||
ENSURE(res); | |||||
*ip = ntohl(((sockaddr_in *)res->ai_addr)->sin_addr.s_addr); | |||||
elexis: c-style casts -> static_cast,reinterpret_cast etc.
`sockaddr_in *` -> `sockaddr_in*` | |||||
freeaddrinfo(res); | |||||
return true; | |||||
} | |||||
elexisUnsubmitted Done Inline ActionsI'd suggest moving these helpers to a new file source/network/IPTools.cpp or similar, as done in D1746, so that it can be used by all networking code without being located in an unrelated file. (Also notice in the new enet version ENet 1.3.14 (January 27, 2019):, enet does use getaddrinfo and getnameinfo where available https://github.com/lsalzman/enet/blob/master/ChangeLog but that doesn't relate to the StunClient, so we still need these functions I guess) elexis: I'd suggest moving these helpers to a new file `source/network/IPTools.cpp` or similar, as done… | |||||
LefoAuthorUnsubmitted Done Inline ActionsThat would make collision with D1746, wouldn't it? I'd wait after D1746 is done, then we can change this. We're gonna need to change some things anyway when we start implementing lobby support. Lefo: That would make collision with D1746, wouldn't it? I'd wait after D1746 is done, then we can… | |||||
elexisUnsubmitted Done Inline Actions(Either after that is committed or just before this is committed in case D1746 is let rot.)
Would be very good to fix this upstream. Technically we didn't try to communicate yet. elexis: (Either after that is committed or just before this is committed in case D1746 is let rot.)… | |||||
LefoAuthorUnsubmitted Done Inline ActionsThe problem also is that with zpl-c's version of enet these conversion functions are no longer available for IPv4. That's the main reason I removed use of enet to convert ip addresses.
I'm sure it would but it seems to be hard: https://github.com/lsalzman/enet/issues/78 From what I can see, people already tried with no luck. Lefo: The problem also is that with zpl-c's version of enet these conversion functions are no longer… | |||||
elexisUnsubmitted Done Inline ActionsJust because something has failed by others doesn't mean that one can't try to look at the underyling problem and try to solve it. (Like, in case we wanted to). From what I read of lsalzman, the problem is that it's a lot of work to determine what the best ipv6 enet implementaiton would be to satisfy every stakeholder and in how far the existing patches qualify as such; including discussion in the mailing list. So maybe fixing the issue would be to oneself review all the 4 pull requests, figure out where they fail and where they succeed, coming up with the ideal implementation that satisfies everyone, minimizes backwards compatibility problems, is approved by the mailinglist, writing a summary why the patches 1-4 are wrong and the new one the right way to go, laying out all the evidence, and only then passing it on as a pull request. Certainly seems cleaner and more rewarrding to fix the upstream review / communication knot than addressing a library problem in the application layer. (Which doesn't mean one must not start with Plan B) elexis: Just because something has failed by others doesn't mean that one can't try to look at the… | |||||
LefoAuthorUnsubmitted Done Inline ActionsWell I'm a bit sceptical in this. From what I see it seems that lsalzman just has no time to deal with IPv6: https://github.com/lsalzman/enet/pull/21#issuecomment-236402694 It might be possible to implement it better so it does not break backward compatibility. But that would be a lot of work and I'm not really much motivated to do it when I don't even know whether it would ever got upstream. It's also a lot complicated by the fact that enet relies on passing huge structures to user such as ENetPeer https://github.com/lsalzman/enet/blob/master/include/enet/enet.h#L267 Lefo: Well I'm a bit sceptical in this. From what I see it seems that lsalzman just has no time to… | |||||
/** | /** | ||||
* Creates a STUN request and sends it to a STUN server. | * Creates a STUN request and sends it to a STUN server. | ||||
* The request is sent through transactionHost, from which the answer | * The request is sent through transactionHost, from which the answer | ||||
* will be retrieved by ReceiveStunResponse and interpreted by ParseStunResponse. | * will be retrieved by ReceiveStunResponse and interpreted by ParseStunResponse. | ||||
*/ | */ | ||||
bool CreateStunRequest(ENetHost* transactionHost) | bool CreateStunRequest(ENetHost* transactionHost) | ||||
{ | { | ||||
ENSURE(transactionHost); | ENSURE(transactionHost); | ||||
CStr server_name; | CStr server_name; | ||||
CFG_GET_VAL("lobby.stun.server", server_name); | CFG_GET_VAL("lobby.stun.server", server_name); | ||||
CFG_GET_VAL("lobby.stun.port", m_StunServerPort); | CFG_GET_VAL("lobby.stun.port", m_StunServerPort); | ||||
debug_printf("GetPublicAddress: Using STUN server %s:%d\n", server_name.c_str(), m_StunServerPort); | debug_printf("GetPublicAddress: Using STUN server %s:%d\n", server_name.c_str(), m_StunServerPort); | ||||
addrinfo hints; | if (!HostnameToIPv4(server_name.c_str(), 0, &m_StunServerIP)) { | ||||
Done Inline ActionsNot sure what the coding conventions say about this. https://trac.wildfiregames.com/wiki/Coding_Conventions Stan: Not sure what the coding conventions say about this. https://trac.wildfiregames. | |||||
Done Inline ActionsI also preferred the old version. But I changed it to this form on vladislavbelov's request. Lefo: I also preferred the old version. But I changed it to this form on vladislavbelov's request. | |||||
Done Inline Actions@vladislavbelov saying so is enough for me :) Was just wondering because I didn't even know that could be done. Wonder what the scope of that status is in this case. Stan: @vladislavbelov saying so is enough for me :) Was just wondering because I didn't even know… | |||||
Done Inline ActionsWhy I vote this. Because you don't need to share the variable with all other code, only with the space where it's required. If there would be another similar block, you would need to call all these variables by some meaning names. vladislavbelov: Why I vote this. Because you don't need to share the variable with all other code, only with… | |||||
Done Inline Actions@Stan the scope is the consequent of if statement (code inside {}). It is similar to for cycle in which you can also define variable in initialization part. @vladislavbelov I prefer creating another {} block for limiting scope of variables. This seems ugly because it is not C compatible and it does not work in any other comparison case (such as equal instead of not equal or comparing to any other number different from zero). if (int status = getaddrinfo(...) == 0) which doesn't work because status variable gets assigned 0 or 1 depending on result of comparison and the original result of getaddrinfo gets dropped. Lefo: @Stan the scope is the consequent of if statement (code inside {}). It is similar to for cycle… | |||||
Done Inline Actions
I can say that it's ugly about useless {}. But it's just tastes I think. I just prefer the if variable while it's compilable.
Then you shouldn't use reinterpret_cast, STL and other C++ stuff here.
You're right until C++17 :)
Yes, I agree. But it's a more complex problem, that people don't know the language. It should be solved on the review stage. vladislavbelov: > I prefer creating another {} block for limiting scope of variables.
I can say that it's ugly… | |||||
Done Inline Actionsif (foo)\n { Why does this hardcode IPv4 and not have some condition for IPv6? elexis: ```
if (foo)\n
{
```
Why does this hardcode IPv4 and not have some condition for IPv6? | |||||
Done Inline ActionsBecase this code is related to STUN. There is no STUN for IPv6. One of the reasons we want IPv6 is that there is no need for ugly stuff such as STUN. Lefo: Becase this code is related to STUN. There is no STUN for IPv6. One of the reasons we want IPv6… | |||||
Done Inline Actions
Is that absolutely true or is there only one of the main historic use cases for NAT dropped? Also since the StunClient was imported from supertuxkart, we might want to inform them in of the update once we merged it and if we find an active maintainer of this file. elexis: > There is no STUN for IPv6
Is that absolutely true or is there only one of the main historic… | |||||
Done Inline ActionsI did not ever see any NAT on IPv6. And I hope I ever won't. Let's hope that people realize that it breaks way too many things and it is bad by design soon enough to not to implement it anywhere. If you send packet from IP address X and recipient receives this packet from IP address Y then there is something fundamentally wrong with the brain of the one who is responsible. Lefo: I did not ever see any NAT on IPv6. And I hope I ever won't. Let's hope that people realize… | |||||
Not Done Inline ActionsMostly wondering whether this could use a protocol version agnostic function. Otherwise a code comment explaining the implementation decision would be useful. elexis: Mostly wondering whether this could use a protocol version agnostic function. Otherwise a code… | |||||
Done Inline ActionsI think that IPv6 will kill NAT. I don't know of any ISP or router that would use NAT for any reason. When it comes to privacy random temporary addresses are usually used. When it comes to security stateful firewalls are usually used. Lefo: I think that IPv6 will kill NAT. I don't know of any ISP or router that would use NAT for any… | |||||
addrinfo* res; | LOGERROR("CreateStunRequest: HostnameToIPv4 failed"); | ||||
memset(&hints, 0, sizeof(hints)); | |||||
hints.ai_family = AF_UNSPEC; // AF_INET or AF_INET6 to force version | |||||
hints.ai_socktype = SOCK_STREAM; | |||||
// Resolve the stun server name so we can send it a STUN request | |||||
int status = getaddrinfo(server_name.c_str(), nullptr, &hints, &res); | |||||
if (status != 0) | |||||
{ | |||||
#ifdef UNICODE | |||||
LOGERROR("GetPublicAddress: Error in getaddrinfo: %s", utf8_from_wstring(gai_strerror(status))); | |||||
#else | |||||
LOGERROR("GetPublicAddress: Error in getaddrinfo: %s", gai_strerror(status)); | |||||
#endif | |||||
return false; | return false; | ||||
} | } | ||||
ENSURE(res); | |||||
// Documentation says it points to "one or more addrinfo structures" | |||||
sockaddr_in* current_interface = (sockaddr_in*)(res->ai_addr); | |||||
m_StunServerIP = ntohl(current_interface->sin_addr.s_addr); | |||||
StunClient::SendStunRequest(transactionHost, m_StunServerIP, m_StunServerPort); | StunClient::SendStunRequest(transactionHost, m_StunServerIP, m_StunServerPort); | ||||
freeaddrinfo(res); | |||||
return true; | return true; | ||||
} | } | ||||
void StunClient::SendStunRequest(ENetHost* transactionHost, u32 targetIp, u16 targetPort) | void StunClient::SendStunRequest(ENetHost* transactionHost, u32 targetIp, u16 targetPort) | ||||
{ | { | ||||
std::vector<u8> buffer; | std::vector<u8> buffer; | ||||
AddUInt16(buffer, m_MethodTypeBinding); | AddUInt16(buffer, m_MethodTypeBinding); | ||||
AddUInt16(buffer, 0); // length | AddUInt16(buffer, 0); // length | ||||
Show All 25 Lines | bool ReceiveStunResponse(ENetHost* transactionHost, std::vector<u8>& buffer) | ||||
ENSURE(transactionHost); | ENSURE(transactionHost); | ||||
// TransportAddress sender; | // TransportAddress sender; | ||||
const int LEN = 2048; | const int LEN = 2048; | ||||
char input_buffer[LEN]; | char input_buffer[LEN]; | ||||
memset(input_buffer, 0, LEN); | memset(input_buffer, 0, LEN); | ||||
#ifdef ENET_IPV6 | |||||
sockaddr_in6 addr; | |||||
#else | |||||
sockaddr_in addr; | sockaddr_in addr; | ||||
#endif | |||||
socklen_t from_len = sizeof(addr); | socklen_t from_len = sizeof(addr); | ||||
int len = recvfrom(transactionHost->socket, input_buffer, LEN, 0, (sockaddr*)(&addr), &from_len); | int len = recvfrom(transactionHost->socket, input_buffer, LEN, 0, (sockaddr*)(&addr), &from_len); | ||||
int delay = 200; | int delay = 200; | ||||
CFG_GET_VAL("lobby.stun.delay", delay); | CFG_GET_VAL("lobby.stun.delay", delay); | ||||
// Wait to receive the message because enet sockets are non-blocking | // Wait to receive the message because enet sockets are non-blocking | ||||
const int max_tries = 5; | const int max_tries = 5; | ||||
for (int count = 0; len < 0 && (count < max_tries || max_tries == -1); ++count) | for (int count = 0; len < 0 && (count < max_tries || max_tries == -1); ++count) | ||||
{ | { | ||||
usleep(delay * 1000); | usleep(delay * 1000); | ||||
len = recvfrom(transactionHost->socket, input_buffer, LEN, 0, (sockaddr*)(&addr), &from_len); | len = recvfrom(transactionHost->socket, input_buffer, LEN, 0, (sockaddr*)(&addr), &from_len); | ||||
} | } | ||||
if (len < 0) | if (len < 0) | ||||
{ | { | ||||
LOGERROR("GetPublicAddress: recvfrom error (%d): %s", errno, strerror(errno)); | LOGERROR("GetPublicAddress: recvfrom error (%d): %s", errno, strerror(errno)); | ||||
return false; | return false; | ||||
} | } | ||||
u32 sender_ip = ntohl((u32)(addr.sin_addr.s_addr)); | #ifdef ENET_IPV6 | ||||
u32 sender_ip = ntohl(reinterpret_cast<u32*>(addr.sin6_addr.s6_addr)[3]); | |||||
Done Inline Actionsstatic_cast Stan: static_cast | |||||
Done Inline Actionswould make it inconsistent with current IPv4 code, wanna change that too? Lefo: would make it inconsistent with current IPv4 code, wanna change that too? | |||||
Done Inline ActionsYeah in that case. It's better to know which type of cast we use :) Stan: Yeah in that case. It's better to know which type of cast we use :) | |||||
Done Inline Actionsreinterpret_cast Lefo: reinterpret_cast | |||||
u16 sender_port = ntohs(addr.sin6_port); | |||||
const u16 v4_mapped_prefix[] = {0, 0, 0, 0, 0, 0xFFFF}; | |||||
bool sender_is_ipv4 = !memcmp(addr.sin6_addr.s6_addr, v4_mapped_prefix, sizeof(v4_mapped_prefix)); | |||||
#else | |||||
u32 sender_ip = ntohl(static_cast<u32>(addr.sin_addr.s_addr)); | |||||
u16 sender_port = ntohs(addr.sin_port); | u16 sender_port = ntohs(addr.sin_port); | ||||
bool sender_is_ipv4 = true; | |||||
#endif | |||||
if (sender_ip != m_StunServerIP) | if (!sender_is_ipv4 || sender_ip != m_StunServerIP || sender_port != m_StunServerPort) | ||||
LOGERROR("GetPublicAddress: Received stun response from different address: %d:%d (%d.%d.%d.%d:%d) %s", | { | ||||
addr.sin_addr.s_addr, | char ipStr[256] = "(error)"; | ||||
addr.sin_port, | getnameinfo(reinterpret_cast<sockaddr *>(&addr), sizeof(addr), ipStr, sizeof(ipStr), nullptr, 0, NI_NUMERICHOST); | ||||
(sender_ip >> 24) & 0xff, | LOGERROR("GetPublicAddress: Received stun response from different address: [%s]:%d", ipStr, (int)sender_port); | ||||
(sender_ip >> 16) & 0xff, | } | ||||
(sender_ip >> 8) & 0xff, | |||||
(sender_ip >> 0) & 0xff, | |||||
sender_port, | |||||
input_buffer); | |||||
// Convert to network string. | // Convert to network string. | ||||
buffer.resize(len); | buffer.resize(len); | ||||
memcpy(buffer.data(), (u8*)input_buffer, len); | memcpy(buffer.data(), (u8*)input_buffer, len); | ||||
return true; | return true; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 123 Lines • ▼ Show 20 Lines | if (!transactionHost) | ||||
return JS::UndefinedValue(); | return JS::UndefinedValue(); | ||||
} | } | ||||
bool success = STUNRequestAndResponse(transactionHost); | bool success = STUNRequestAndResponse(transactionHost); | ||||
enet_host_destroy(transactionHost); | enet_host_destroy(transactionHost); | ||||
if (!success) | if (!success) | ||||
return JS::UndefinedValue(); | return JS::UndefinedValue(); | ||||
// Convert m_IP to string | |||||
char ipStr[256] = "(error)"; | char ipStr[256] = "(error)"; | ||||
ENetAddress addr; | IPv4ToString(m_IP, ipStr, sizeof(ipStr)); | ||||
Done Inline ActionsCould be a function maybe to avoid duplication. Stan: Could be a function maybe to avoid duplication. | |||||
Done Inline ActionsOk, use of getaddrinfo could be converted to function as well. For me: fix ntohl -> htonl. Lefo: Ok, use of getaddrinfo could be converted to function as well.
For me: fix ntohl -> htonl. | |||||
Done Inline ActionsNot sure what ntohl is so can't really tell :) Stan: Not sure what ntohl is so can't really tell :) | |||||
addr.host = ntohl(m_IP); | |||||
enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr)); | |||||
JSContext* cx = scriptInterface.GetContext(); | JSContext* cx = scriptInterface.GetContext(); | ||||
JSAutoRequest rq(cx); | JSAutoRequest rq(cx); | ||||
JS::RootedValue stunEndpoint(cx); | JS::RootedValue stunEndpoint(cx); | ||||
scriptInterface.Eval("({})", &stunEndpoint); | scriptInterface.Eval("({})", &stunEndpoint); | ||||
scriptInterface.SetProperty(stunEndpoint, "ip", CStr(ipStr)); | scriptInterface.SetProperty(stunEndpoint, "ip", CStr(ipStr)); | ||||
scriptInterface.SetProperty(stunEndpoint, "port", m_Port); | scriptInterface.SetProperty(stunEndpoint, "port", m_Port); | ||||
return stunEndpoint; | return stunEndpoint; | ||||
} | } | ||||
StunClient::StunEndpoint* StunClient::FindStunEndpointJoin(ENetHost* transactionHost) | StunClient::StunEndpoint* StunClient::FindStunEndpointJoin(ENetHost* transactionHost) | ||||
{ | { | ||||
ENSURE(transactionHost); | ENSURE(transactionHost); | ||||
if (!STUNRequestAndResponse(transactionHost)) | if (!STUNRequestAndResponse(transactionHost)) | ||||
return nullptr; | return nullptr; | ||||
// Convert m_IP to string | |||||
char ipStr[256] = "(error)"; | char ipStr[256] = "(error)"; | ||||
ENetAddress addr; | IPv4ToString(m_IP, ipStr, sizeof(ipStr)); | ||||
addr.host = ntohl(m_IP); | |||||
enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr)); | |||||
return new StunEndpoint({ m_IP, m_Port }); | return new StunEndpoint({ m_IP, m_Port }); | ||||
} | } | ||||
void StunClient::SendHolePunchingMessages(ENetHost* enetClient, const char* serverAddress, u16 serverPort) | void StunClient::SendHolePunchingMessages(ENetHost* enetClient, const char* serverAddress, u16 serverPort) | ||||
{ | { | ||||
// Convert ip string to int64 | // Convert ip string to in_addr | ||||
ENetAddress addr; | u32 addr; | ||||
addr.port = serverPort; | |||||
enet_address_set_host(&addr, serverAddress); | if (!HostnameToIPv4(serverAddress, AI_NUMERICHOST, &addr)) { | ||||
elexisUnsubmitted Not Done Inline ActionsserverAddress.c_str() elexis: serverAddress.c_str() | |||||
LOGERROR("SendHolePunchingMessages: HostnameToIPv4 failed"); | |||||
return; | |||||
} | |||||
int delay = 200; | int delay = 200; | ||||
CFG_GET_VAL("lobby.stun.delay", delay); | CFG_GET_VAL("lobby.stun.delay", delay); | ||||
// Send an UDP message from enet host to ip:port | // Send an UDP message from enet host to ip:port | ||||
for (int i = 0; i < 3; ++i) | for (int i = 0; i < 3; ++i) | ||||
{ | { | ||||
StunClient::SendStunRequest(enetClient, htonl(addr.host), serverPort); | StunClient::SendStunRequest(enetClient, addr, serverPort); | ||||
usleep(delay * 1000); | usleep(delay * 1000); | ||||
elexisUnsubmitted Done Inline Actions(This freezes the mainthread for seconds. Should become async) elexis: (This freezes the mainthread for seconds. Should become async) | |||||
LefoAuthorUnsubmitted Done Inline ActionsYes, I agree. But this is way out of scope of this patch. Lefo: Yes, I agree. But this is way out of scope of this patch. | |||||
elexisUnsubmitted Done Inline Actions(Just trying to get skilled people interested in related bugs in order to optimize the engine and my laziness simultaneously) elexis: (Just trying to get skilled people interested in related bugs in order to optimize the engine… | |||||
} | } | ||||
} | } |
c-style casts -> static_cast,reinterpret_cast etc.
sockaddr_in * -> sockaddr_in*