2019-07-29 21:06:18 -07:00

952 lines
34 KiB
C++

#include "mist.h"
#include <Ws2tcpip.h>
#include <stdio.h>
#include <assert.h>
#include <shellapi.h>
#include <objbase.h>
#include <WinInet.h>
#pragma comment(lib, "miniupnpc.lib")
#pragma comment(lib, "libnatpmp.lib")
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "wininet.lib")
#define MINIUPNP_STATICLIB
#include <miniupnpc/miniupnpc.h>
#include <miniupnpc/upnpcommands.h>
#include <miniupnpc/upnperrors.h>
#define NATPMP_STATICLIB
#include <natpmp.h>
static struct port_entry {
int proto;
int port;
bool withServer;
} k_Ports[] = {
{IPPROTO_TCP, 47984, false},
{IPPROTO_TCP, 47989, false},
{IPPROTO_TCP, 48010, true},
{IPPROTO_UDP, 47998, true},
{IPPROTO_UDP, 47999, true},
{IPPROTO_UDP, 48000, true},
#if 0
// These are not currently used, so let's
// avoid testing them for now.
{IPPROTO_UDP, 48002, true},
{IPPROTO_UDP, 48010, true}
#endif
};
char logFilePath[MAX_PATH + 1];
enum MessagePriority {
MpInfo,
MpWarn,
MpError
};
VOID CALLBACK MsgBoxHelpCallback(LPHELPINFO lpHelpInfo)
{
const char* helpUrl = (const char*)lpHelpInfo->dwContextId;
if (!helpUrl) {
return;
}
// It's recommended to initialize COM before calling ShellExecute()
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
ShellExecuteA(nullptr, "open", helpUrl, nullptr, nullptr, SW_SHOWNORMAL);
}
void DisplayMessage(const char* message, const char* helpUrl = nullptr, MessagePriority priority = MpError, bool terminal = true)
{
fprintf(CONSOLE_OUT, "%s\n", message);
fprintf(LOG_OUT, "%s\n", message);
if (terminal) {
char logPath[MAX_PATH + 1];
FILE* f;
fprintf(LOG_OUT, "--------------- CURRENT MISS LOG -------------------\n");
ExpandEnvironmentStringsA("%ProgramData%\\MISS\\miss-current.log", logPath, sizeof(logPath));
f = fopen(logPath, "r");
if (f != nullptr) {
char buffer[1024];
while (!feof(f)) {
int bytesRead = fread(buffer, 1, ARRAYSIZE(buffer), f);
fwrite(buffer, 1, bytesRead, LOG_OUT);
}
fclose(f);
}
else {
fprintf(LOG_OUT, "Failed to find current MISS log\n");
}
fprintf(LOG_OUT, "\n----------------- OLD MISS LOG ---------------------\n");
ExpandEnvironmentStringsA("%ProgramData%\\MISS\\miss-old.log", logPath, sizeof(logPath));
f = fopen(logPath, "r");
if (f != nullptr) {
char buffer[1024];
while (!feof(f)) {
int bytesRead = fread(buffer, 1, ARRAYSIZE(buffer), f);
fwrite(buffer, 1, bytesRead, LOG_OUT);
}
fclose(f);
}
else {
fprintf(LOG_OUT, "Failed to find old MISS log\n");
}
fprintf(LOG_OUT, "--------------- CURRENT GSV6FWD LOG -------------------\n");
ExpandEnvironmentStringsA("%ProgramData%\\MISS\\GSv6Fwd-current.log", logPath, sizeof(logPath));
f = fopen(logPath, "r");
if (f != nullptr) {
char buffer[1024];
while (!feof(f)) {
int bytesRead = fread(buffer, 1, ARRAYSIZE(buffer), f);
fwrite(buffer, 1, bytesRead, LOG_OUT);
}
fclose(f);
}
else {
fprintf(LOG_OUT, "Failed to find current GSv6Fwd log\n");
}
fprintf(LOG_OUT, "\n----------------- OLD GSV6FWD LOG ---------------------\n");
ExpandEnvironmentStringsA("%ProgramData%\\MISS\\GSv6Fwd-old.log", logPath, sizeof(logPath));
f = fopen(logPath, "r");
if (f != nullptr) {
char buffer[1024];
while (!feof(f)) {
int bytesRead = fread(buffer, 1, ARRAYSIZE(buffer), f);
fwrite(buffer, 1, bytesRead, LOG_OUT);
}
fclose(f);
}
else {
fprintf(LOG_OUT, "Failed to find old GSv6Fwd log\n");
}
fflush(LOG_OUT);
}
MSGBOXPARAMSA msgParams;
msgParams.cbSize = sizeof(msgParams);
msgParams.hwndOwner = nullptr;
msgParams.hInstance = nullptr;
msgParams.lpszText = message;
msgParams.lpszCaption = "Moonlight Internet Streaming Tester";
msgParams.dwStyle = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
if (helpUrl) {
msgParams.dwStyle |= MB_HELP;
}
switch (priority) {
case MpInfo:
msgParams.dwStyle |= MB_ICONINFORMATION;
break;
case MpWarn:
msgParams.dwStyle |= MB_ICONWARNING;
break;
case MpError:
msgParams.dwStyle |= MB_ICONERROR;
break;
}
msgParams.lpfnMsgBoxCallback = MsgBoxHelpCallback;
msgParams.dwContextHelpId = (DWORD_PTR)helpUrl;
msgParams.dwLanguageId = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT);
MessageBoxIndirectA(&msgParams);
if (priority != MpInfo && terminal) {
UINT flags = MB_YESNO | MB_TOPMOST | MB_SETFOREGROUND | MB_ICONINFORMATION;
switch (MessageBoxA(nullptr, "Would you like to view the troubleshooting log?",
"Moonlight Internet Streaming Tester", flags))
{
case IDYES:
// It's recommended to initialize COM before calling ShellExecute()
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
ShellExecuteA(nullptr, "open", logFilePath, nullptr, nullptr, SW_SHOWNORMAL);
break;
}
}
}
bool IsGameStreamEnabled()
{
DWORD error;
DWORD enabled;
DWORD len;
HKEY key;
error = RegOpenKeyExA(HKEY_LOCAL_MACHINE, "Software\\NVIDIA Corporation\\NvStream", 0, KEY_READ | KEY_WOW64_64KEY, &key);
if (error != ERROR_SUCCESS) {
fprintf(LOG_OUT, "RegOpenKeyEx() failed: %d\n", error);
DisplayMessage("GeForce Experience was not detected on this PC. Make sure you're installing this utility on your GeForce GameStream-compatible PC, not the device running Moonlight.",
"https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
return false;
}
len = sizeof(enabled);
error = RegQueryValueExA(key, "EnableStreaming", nullptr, nullptr, (LPBYTE)&enabled, &len);
RegCloseKey(key);
if (error != ERROR_SUCCESS || !enabled) {
// GFE may not even write EnableStreaming until the user enables GameStream for the first time
if (error != ERROR_SUCCESS) {
fprintf(LOG_OUT, "RegQueryValueExA() failed: %d\n", error);
}
DisplayMessage("GameStream is not enabled in GeForce Experience. Please open GeForce Experience settings, navigate to the Shield tab, and turn GameStream on.",
"https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
return false;
}
else {
fprintf(LOG_OUT, "GeForce Experience installed and GameStream is enabled\n");
return true;
}
}
enum PortTestStatus {
PortTestOk,
PortTestError,
PortTestUnknown
};
PortTestStatus TestPort(PSOCKADDR_STORAGE addr, int proto, int port, bool withServer)
{
SOCKET clientSock = INVALID_SOCKET, serverSock = INVALID_SOCKET;
int err;
clientSock = socket(addr->ss_family, proto == IPPROTO_TCP ? SOCK_STREAM : SOCK_DGRAM, proto);
if (clientSock == INVALID_SOCKET) {
fprintf(LOG_OUT, "socket() failed: %d\n", WSAGetLastError());
return PortTestError;
}
if (withServer) {
// Even if we are testing IPv6, our server socket should still be on IPv4 to allow the
// IPv6 relay to do its job (since it's already bound to all GFE ports on v6)
serverSock = socket(AF_INET, proto == IPPROTO_TCP ? SOCK_STREAM : SOCK_DGRAM, proto);
if (serverSock == INVALID_SOCKET) {
fprintf(LOG_OUT, "socket() failed: %d\n", WSAGetLastError());
closesocket(clientSock);
return PortTestError;
}
SOCKADDR_IN sin = {};
sin.sin_family = AF_INET;
sin.sin_port = htons(port);
err = bind(serverSock, (struct sockaddr*)&sin, sizeof(sin));
if (err == SOCKET_ERROR) {
if (WSAGetLastError() == WSAEADDRINUSE) {
// If someone is already listening (perhaps GFE is currently streaming),
// we can proceed if it's a TCP connection.
if (proto == IPPROTO_TCP) {
closesocket(serverSock);
serverSock = INVALID_SOCKET;
}
else {
// We can't continue to test for UDP ports.
fprintf(LOG_OUT, "Unknown (in use)\n");
closesocket(clientSock);
closesocket(serverSock);
return PortTestUnknown;
}
}
else {
fprintf(LOG_OUT, "bind() failed: %d\n", WSAGetLastError());
closesocket(clientSock);
closesocket(serverSock);
return PortTestError;
}
}
if (proto == IPPROTO_TCP && serverSock != INVALID_SOCKET) {
err = listen(serverSock, 1);
if (err == SOCKET_ERROR) {
fprintf(LOG_OUT, "listen() failed: %d\n", WSAGetLastError());
closesocket(clientSock);
closesocket(serverSock);
return PortTestError;
}
}
}
ULONG nbIo = 1;
err = ioctlsocket(clientSock, FIONBIO, &nbIo);
if (err == SOCKET_ERROR) {
fprintf(LOG_OUT, "ioctlsocket() failed: %d\n", WSAGetLastError());
closesocket(clientSock);
if (serverSock != INVALID_SOCKET) {
closesocket(serverSock);
}
return PortTestError;
}
SOCKADDR_IN6 sin6;
int addrLen = addr->ss_family == AF_INET ?
sizeof(SOCKADDR_IN) : sizeof(SOCKADDR_IN6);
RtlCopyMemory(&sin6, addr, addrLen);
sin6.sin6_port = htons(port);
if (proto == IPPROTO_TCP) {
err = connect(clientSock, (struct sockaddr*)&sin6, addrLen);
if (err == SOCKET_ERROR && WSAGetLastError() != WSAEWOULDBLOCK) {
fprintf(LOG_OUT, "connect() failed: %d\n", WSAGetLastError());
}
else {
struct timeval timeout = {};
fd_set fds;
FD_ZERO(&fds);
if (serverSock != INVALID_SOCKET) {
FD_SET(serverSock, &fds);
}
else {
FD_SET(clientSock, &fds);
}
// If we have a server socket, listen for the accept() instead of the
// connect() so we can be compatible with the loopback relay.
timeout.tv_sec = 3;
err = select(0,
serverSock != INVALID_SOCKET ? &fds : nullptr,
serverSock == INVALID_SOCKET ? &fds : nullptr,
nullptr, &timeout);
if (err == 1) {
// Our FD was signalled for connect() or accept() completion
fprintf(LOG_OUT, "Success\n");
}
else if (err == 0) {
// Timed out
fprintf(LOG_OUT, "Timeout\n");
}
else {
fprintf(LOG_OUT, "select() failed: %d\n", WSAGetLastError());
}
}
closesocket(clientSock);
if (serverSock != INVALID_SOCKET) {
closesocket(serverSock);
}
return err == 1 ? PortTestOk : PortTestError;
}
else {
const char testMsg[] = "moonlight-test";
// Send several test packets to ensure a random lost packet doesn't make the test fail
for (int i = 0; i < 5; i++) {
err = sendto(clientSock, testMsg, sizeof(testMsg), 0, (struct sockaddr*)&sin6, addrLen);
if (err == SOCKET_ERROR) {
fprintf(LOG_OUT, "sendto() failed: %d\n", WSAGetLastError());
closesocket(clientSock);
closesocket(serverSock);
return PortTestError;
}
Sleep(200);
}
struct timeval timeout = {};
fd_set fds;
FD_ZERO(&fds);
FD_SET(serverSock, &fds);
timeout.tv_sec = 3;
err = select(0, &fds, nullptr, nullptr, &timeout);
if (err == 1) {
// Our FD was signalled for data available
fprintf(LOG_OUT, "Success\n");
}
else if (err == 0) {
// Timed out
fprintf(LOG_OUT, "Timeout\n");
}
else {
fprintf(LOG_OUT, "select() failed: %d\n", WSAGetLastError());
}
closesocket(clientSock);
closesocket(serverSock);
return err == 1 ? PortTestOk : PortTestError;
}
}
PortTestStatus TestHttpPort(PSOCKADDR_STORAGE addr, int port)
{
HINTERNET hInternet;
HINTERNET hRequest;
char url[1024];
hInternet = InternetOpenA("MIST", 0, nullptr, nullptr, 0);
if (hInternet == nullptr) {
fprintf(LOG_OUT, "InternetOpen() failed: %d\n", GetLastError());
return PortTestError;
}
char addrStr[INET6_ADDRSTRLEN + 2];
if (addr->ss_family == AF_INET) {
inet_ntop(AF_INET, &((struct sockaddr_in*)addr)->sin_addr, addrStr, sizeof(addrStr));
}
else {
// The address string must be escaped for usage in URLs
addrStr[0] = '[';
inet_ntop(AF_INET6, &((struct sockaddr_in6*)addr)->sin6_addr, &addrStr[1], INET6_ADDRSTRLEN);
strcat_s(addrStr, "]");
}
sprintf(url, "%s://%s:%d/",
port == 47989 ? "http" : "https",
addrStr,
port);
hRequest = InternetOpenUrlA(hInternet, url, nullptr, 0,
INTERNET_FLAG_IGNORE_CERT_DATE_INVALID | INTERNET_FLAG_NO_UI | INTERNET_FLAG_NO_AUTH | INTERNET_FLAG_NO_COOKIES | INTERNET_FLAG_RELOAD,
NULL);
if (hRequest == nullptr) {
if (GetLastError() == ERROR_INTERNET_CLIENT_AUTH_CERT_NEEDED) {
// This is expected for our HTTPS connection
fprintf(LOG_OUT, "Success\n");
}
else {
// CANNOT_CONNECT is the "expected" error
if (GetLastError() == ERROR_INTERNET_CANNOT_CONNECT) {
fprintf(LOG_OUT, "Failed\n");
}
else {
fprintf(LOG_OUT, "Failed: %d\n", GetLastError());
}
InternetCloseHandle(hInternet);
return PortTestError;
}
}
else {
fprintf(LOG_OUT, "Success\n");
InternetCloseHandle(hRequest);
}
InternetCloseHandle(hInternet);
return PortTestOk;
}
bool TestAllPorts(PSOCKADDR_STORAGE addr, char* portMsg, int portMsgLen, bool consolePrint)
{
bool ret = true;
for (int i = 0; i < ARRAYSIZE(k_Ports); i++) {
fprintf(LOG_OUT, "Testing %s %d...",
k_Ports[i].proto == IPPROTO_TCP ? "TCP" : "UDP",
k_Ports[i].port);
if (consolePrint) {
fprintf(CONSOLE_OUT, "\tTesting %s %d...\n",
k_Ports[i].proto == IPPROTO_TCP ? "TCP" : "UDP",
k_Ports[i].port);
}
PortTestStatus status = TestPort(addr, k_Ports[i].proto, k_Ports[i].port, k_Ports[i].withServer);
if (status != PortTestError && !k_Ports[i].withServer) {
// Test using a real HTTP client if the port wasn't totally dead.
// This is required to confirm functionality with the loopback relay.
// TestHttpPort() can take significantly longer to timeout than TestPort(),
// so we only do this test if we believe we're likely to get a response.
fprintf(LOG_OUT, "Testing TCP %d with HTTP traffic...", k_Ports[i].port);
status = TestHttpPort(addr, k_Ports[i].port);
}
if (status != PortTestOk) {
// If we got an unknown result, assume it matches with whatever
// we've gotten so far.
if (status == PortTestError || !ret) {
if (portMsg != NULL && portMsgLen > 0) {
int msgLen = snprintf(portMsg, portMsgLen, "%s %d\n",
k_Ports[i].proto == IPPROTO_TCP ? "TCP" : "UDP",
k_Ports[i].port);
portMsg += msgLen;
portMsgLen -= msgLen;
}
// Keep going to check all ports and report the failing ones
ret = false;
}
}
}
return ret;
}
bool FindLocalInterfaceIPAddress(int family, PSOCKADDR_STORAGE addr)
{
SOCKET s;
struct addrinfo hint = {};
struct addrinfo* result;
int err;
fprintf(LOG_OUT, "Finding local %s address...", family == AF_INET ? "IPv4" : "IPv6");
hint.ai_family = family;
hint.ai_socktype = SOCK_STREAM;
hint.ai_protocol = IPPROTO_TCP;
hint.ai_flags = AI_ADDRCONFIG;
err = getaddrinfo("moonlight-stream.org", "https", &hint, &result);
if (err != 0 || result == NULL) {
fprintf(LOG_OUT, "getaddrinfo() failed: %d\n", err);
return false;
}
s = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (s == INVALID_SOCKET) {
fprintf(LOG_OUT, "socket() failed: %d\n", WSAGetLastError());
freeaddrinfo(result);
return false;
}
err = connect(s, (struct sockaddr*)result->ai_addr, result->ai_addrlen);
if (err == SOCKET_ERROR) {
fprintf(LOG_OUT, "connect() failed: %d\n", WSAGetLastError());
closesocket(s);
freeaddrinfo(result);
return false;
}
freeaddrinfo(result);
// Determine which local interface we bound to
int nameLen = sizeof(*addr);
err = getsockname(s, (struct sockaddr*)addr, &nameLen);
if (err == SOCKET_ERROR) {
fprintf(LOG_OUT, "getsockname() failed: %d\n", WSAGetLastError());
closesocket(s);
return false;
}
char addrStr[INET6_ADDRSTRLEN];
if (addr->ss_family == AF_INET) {
inet_ntop(AF_INET, &((struct sockaddr_in*)addr)->sin_addr, addrStr, sizeof(addrStr));
}
else {
inet_ntop(AF_INET6, &((struct sockaddr_in6*)addr)->sin6_addr, addrStr, sizeof(addrStr));
}
fprintf(LOG_OUT, "%s\n", addrStr);
return true;
}
enum UPnPPortStatus {
NOT_FOUND,
OK,
CONFLICTED,
ERRORED
};
UPnPPortStatus UPnPCheckPort(struct UPNPUrls* urls, struct IGDdatas* data, int proto, const char* myAddr, int port, char* conflictMessage)
{
char intClient[16];
char intPort[6];
char desc[80];
char enabled[4];
char leaseDuration[16];
const char* protoStr;
char portStr[6];
snprintf(portStr, sizeof(portStr), "%d", port);
switch (proto)
{
case IPPROTO_TCP:
protoStr = "TCP";
break;
case IPPROTO_UDP:
protoStr = "UDP";
break;
default:
assert(false);
return ERRORED;
}
fprintf(LOG_OUT, "Checking for UPnP port mapping for %s %s -> %s...", protoStr, portStr, myAddr);
int err = UPNP_GetSpecificPortMappingEntry(
urls->controlURL, data->first.servicetype, portStr, protoStr, nullptr,
intClient, intPort, desc, enabled, leaseDuration);
if (err == 714) {
// NoSuchEntryInArray
fprintf(LOG_OUT, "NOT FOUND\n");
return NOT_FOUND;
}
else if (err == UPNPCOMMAND_SUCCESS) {
if (!strcmp(myAddr, intClient)) {
fprintf(LOG_OUT, "OK\n");
return OK;
}
else {
fprintf(LOG_OUT, "CONFLICT - %s %s\n", desc, intClient);
snprintf(conflictMessage, 128, "%s (%s)", desc, intClient);
return CONFLICTED;
}
}
else {
fprintf(LOG_OUT, "ERROR %d\n", err);
return ERRORED;
}
}
bool CheckWANAccess(PSOCKADDR_IN wanAddr, PSOCKADDR_IN reportedWanAddr, bool* foundPortForwardingRules, bool* igdDisconnected)
{
natpmp_t natpmp;
bool foundUpnpIgd = false;
*foundPortForwardingRules = false;
*igdDisconnected = false;
bool gotReportedWanAddress = false;
int natPmpErr = initnatpmp(&natpmp, 0, 0);
if (natPmpErr != 0) {
fprintf(LOG_OUT, "initnatpmp() failed: %d\n", natPmpErr);
}
else {
natPmpErr = sendpublicaddressrequest(&natpmp);
if (natPmpErr < 0) {
fprintf(LOG_OUT, "sendpublicaddressrequest() failed: %d\n", natPmpErr);
closenatpmp(&natpmp);
}
}
{
fprintf(CONSOLE_OUT, "\tTesting UPnP...\n");
int upnpErr;
struct UPNPDev* ipv4Devs = upnpDiscoverAll(5000, nullptr, nullptr, UPNP_LOCAL_PORT_ANY, 0, 2, &upnpErr);
struct UPNPUrls urls;
struct IGDdatas data;
char myAddr[128];
char wanAddrStr[128];
int ret = UPNP_GetValidIGD(ipv4Devs, &urls, &data, myAddr, sizeof(myAddr));
if (ret != 0) {
// Connected or disconnected IGD
if (ret == 1 || ret == 2) {
foundUpnpIgd = true;
if (ret == 2) {
*igdDisconnected = true;
}
fprintf(LOG_OUT, "Discovered UPnP IGD at: %s\n", urls.controlURL);
fprintf(LOG_OUT, "Detecting WAN IP address via UPnP...");
ret = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, wanAddrStr);
if (ret == UPNPCOMMAND_SUCCESS && strlen(wanAddrStr) > 0) {
reportedWanAddr->sin_addr.S_un.S_addr = inet_addr(wanAddrStr);
fprintf(LOG_OUT, "%s\n", wanAddrStr);
if (reportedWanAddr->sin_addr.S_un.S_addr != 0) {
gotReportedWanAddress = true;
}
}
else {
fprintf(LOG_OUT, "FAILED %d\n", ret);
}
char conflictMessage[512];
*foundPortForwardingRules = true;
for (int i = 0; i < ARRAYSIZE(k_Ports); i++) {
char conflictEntry[128];
UPnPPortStatus status = UPnPCheckPort(&urls, &data, k_Ports[i].proto, myAddr, k_Ports[i].port, conflictEntry);
if (status != OK) {
*foundPortForwardingRules = false;
}
switch (status)
{
case CONFLICTED:
snprintf(conflictMessage, sizeof(conflictMessage),
"Detected a port forwarding conflict with another PC on your network: %s\n\n"
"Remove that PC from your network or uninstall the Moonlight Internet Streaming Helper from it, then restart your router.",
conflictEntry);
DisplayMessage(conflictMessage);
return false;
default:
continue;
}
}
}
else {
fprintf(LOG_OUT, "No UPnP IGD detected\n");
}
FreeUPNPUrls(&urls);
}
else {
fprintf(LOG_OUT, "No UPnP devices detected\n");
}
}
// Use the delay of upnpDiscoverAll() to also allow the NAT-PMP endpoint time to respond
if (natPmpErr >= 0) {
fprintf(CONSOLE_OUT, "\tTesting NAT-PMP...\n");
fprintf(LOG_OUT, "Detecting WAN IP address via NAT-PMP...");
natpmpresp_t response;
natPmpErr = readnatpmpresponseorretry(&natpmp, &response);
closenatpmp(&natpmp);
if (natPmpErr == 0) {
char addrStr[64];
reportedWanAddr->sin_addr = response.pnu.publicaddress.addr;
inet_ntop(AF_INET, &response.pnu.publicaddress.addr, addrStr, sizeof(addrStr));
fprintf(LOG_OUT, "%s\n", addrStr);
if (reportedWanAddr->sin_addr.S_un.S_addr != 0) {
gotReportedWanAddress = true;
if (!foundUpnpIgd) {
// Just in case we have a NAT-PMP gateway that doesn't do NAT reflection
// let's assume it's all okay if we got any response at all
*foundPortForwardingRules = true;
}
}
}
else {
fprintf(LOG_OUT, "FAILED %d\n", natPmpErr);
}
}
fprintf(LOG_OUT, "Detecting WAN IP address via STUN...");
fprintf(CONSOLE_OUT, "\tTesting STUN...\n");
if (!getExternalAddressPortIP4(IPPROTO_UDP, 0, wanAddr) && !getExternalAddressPortIP4(IPPROTO_TCP, 0, wanAddr)) {
DisplayMessage("Unable to determine your public IP address. Please check your Internet connection or try again in a few minutes.");
return false;
}
else {
char addrStr[64];
inet_ntop(AF_INET, &wanAddr->sin_addr, addrStr, sizeof(addrStr));
fprintf(LOG_OUT, "%s\n", addrStr);
if (!gotReportedWanAddress) {
// If we didn't get anything from UPnP or NAT-PMP, just populate the reported
// address with what we got from STUN
*reportedWanAddr = *wanAddr;
}
}
return true;
}
bool IsPossibleCGN(PSOCKADDR_IN wanAddr)
{
DWORD addr = htonl(wanAddr->sin_addr.S_un.S_addr);
// 10.0.0.0/8 - ISPs used to use this
if ((addr & 0xFF000000) == 0x0A000000) {
return true;
}
// 100.64.0.0/10 - RFC6598 official CGN address
else if ((addr & 0xFFC00000) == 0x64400000) {
return true;
}
return false;
}
bool IsDoubleNAT(PSOCKADDR_IN wanAddr)
{
DWORD addr = htonl(wanAddr->sin_addr.S_un.S_addr);
// 10.0.0.0/8
if ((addr & 0xFF000000) == 0x0A000000) {
return true;
}
// 172.16.0.0/12
else if ((addr & 0xFFF00000) == 0xAC100000) {
return true;
}
// 192.168.0.0/16
else if ((addr & 0xFFFF0000) == 0xC0A80000) {
return true;
}
return false;
}
int main(int argc, char* argv[])
{
WSADATA wsaData;
SYSTEMTIME time;
char timeString[MAX_PATH + 1] = {};
char tempPath[MAX_PATH + 1];
GetTempPathA(sizeof(tempPath), tempPath);
snprintf(logFilePath, sizeof(logFilePath), "%s\\%s", tempPath, "mis-test.log");
freopen(logFilePath, "w", LOG_OUT);
// Print a log header
fprintf(LOG_OUT, "Moonlight Internet Streaming Tester v" VER_VERSION_STR "\n");
// Print the current time
GetSystemTime(&time);
GetTimeFormatA(LOCALE_SYSTEM_DEFAULT, 0, &time, "hh':'mm':'ss tt", timeString, ARRAYSIZE(timeString));
fprintf(LOG_OUT, "The current UTC time is: %s\n", timeString);
// Print a console header
fprintf(CONSOLE_OUT, "Moonlight Internet Streaming Tester v" VER_VERSION_STR "\n\n");
int err = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (err != NO_ERROR) {
DisplayMessage("Unable to initialize WinSock");
return err;
}
fprintf(CONSOLE_OUT, "Checking if GameStream is enabled...\n");
// First check if GameStream is enabled
if (!IsGameStreamEnabled()) {
return -1;
}
union {
SOCKADDR_STORAGE ss;
SOCKADDR_IN sin;
SOCKADDR_IN6 sin6;
};
char msgBuf[2048];
char portMsgBuf[512];
fprintf(CONSOLE_OUT, "Testing GameStream connectivity on this PC...\n");
// Try to connect via IPv4 loopback
ss = {};
sin.sin_family = AF_INET;
sin.sin_addr = in4addr_loopback;
fprintf(LOG_OUT, "Testing GameStream ports via loopback\n");
if (!TestAllPorts(&ss, portMsgBuf, sizeof(portMsgBuf), true)) {
snprintf(msgBuf, sizeof(msgBuf),
"Local GameStream connectivity check failed.\n\nFirst, try reinstalling GeForce Experience. If that doesn't resolve the problem, try temporarily disabling your antivirus and firewall.");
DisplayMessage(msgBuf, "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
return -1;
}
if (!FindLocalInterfaceIPAddress(AF_INET, &ss)) {
DisplayMessage("Unable to perform GameStream connectivity check. Please check your Internet connection and try again.");
return -1;
}
fprintf(CONSOLE_OUT, "Testing GameStream connectivity on your local network...\n");
// Try to connect via LAN IPv4 address
fprintf(LOG_OUT, "Testing GameStream ports via local network\n");
if (!TestAllPorts(&ss, portMsgBuf, sizeof(portMsgBuf), true)) {
snprintf(msgBuf, sizeof(msgBuf),
"Local network GameStream connectivity check failed. This is almost always caused by a firewall on your computer blocking the connection.\n\nTry temporarily disabling your antivirus and firewall.");
DisplayMessage(msgBuf, "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
return -1;
}
fprintf(CONSOLE_OUT, "Detecting public IP address...\n");
bool rulesFound, igdDisconnected;
SOCKADDR_IN locallyReportedWanAddr;
if (!CheckWANAccess(&sin, &locallyReportedWanAddr, &rulesFound, &igdDisconnected)) {
return -1;
}
fprintf(CONSOLE_OUT, "Testing GameStream connectivity over the Internet...\n");
// Detect a double NAT by detecting STUN and and UPnP mismatches
if (sin.sin_addr.S_un.S_addr != locallyReportedWanAddr.sin_addr.S_un.S_addr) {
fprintf(LOG_OUT, "Testing GameStream ports via UPnP/NAT-PMP reported WAN address\n");
// We don't actually care about the outcome here but it's nice to have in logs
// to determine whether solving the double NAT will actually make Moonlight work.
TestAllPorts((PSOCKADDR_STORAGE)&locallyReportedWanAddr, portMsgBuf, sizeof(portMsgBuf), false);
fprintf(LOG_OUT, "Detected inconsistency between UPnP/NAT-PMP and STUN reported WAN addresses!\n");
}
char wanAddrStr[64];
inet_ntop(AF_INET, &sin.sin_addr, wanAddrStr, sizeof(wanAddrStr));
// Try to connect via WAN IPv4 address
fprintf(LOG_OUT, "Testing GameStream ports via STUN-reported WAN address\n");
if (!TestAllPorts(&ss, portMsgBuf, sizeof(portMsgBuf), true)) {
// Many UPnP devices report IGD disconnected when double-NATed. If it was really offline,
// we probably would not have even gotten past STUN.
if (IsDoubleNAT(&locallyReportedWanAddr) || igdDisconnected) {
snprintf(msgBuf, sizeof(msgBuf), "Your router appears be connected to the Internet through another router. Click the Help button for guidance on fixing this issue.");
DisplayMessage(msgBuf, "https://github.com/moonlight-stream/moonlight-docs/wiki/Internet-Streaming-Errors#connected-through-another-router-error");
}
else {
struct addrinfo hint = {};
struct addrinfo* result;
// We can get here if the router doesn't support NAT reflection.
// We'll need to call out to our loopback server to get a second opinion.
hint.ai_family = AF_UNSPEC;
hint.ai_flags = AI_ADDRCONFIG;
err = getaddrinfo("loopback.moonlight-stream.org", NULL, &hint, &result);
if (err != 0 || result == NULL) {
fprintf(LOG_OUT, "getaddrinfo() failed: %d\n", err);
}
else {
// First try the relay server over IPv4. If this passes, it's considered a full success
fprintf(LOG_OUT, "Testing GameStream ports via IPv4 loopback server\n");
for (struct addrinfo* current = result; current != NULL; current = current->ai_next) {
if (current->ai_family == AF_INET) {
fprintf(CONSOLE_OUT, "Testing GameStream connectivity over the Internet using a relay server...\n");
if (TestAllPorts((PSOCKADDR_STORAGE)current->ai_addr, portMsgBuf, sizeof(portMsgBuf), true)) {
freeaddrinfo(result);
goto AllTestsPassed;
}
}
}
// If that fails, try the relay server over IPv6. If this passes, it will be a partial success
fprintf(LOG_OUT, "Testing GameStream ports via IPv6 loopback server\n");
for (struct addrinfo* current = result; current != NULL; current = current->ai_next) {
if (current->ai_family == AF_INET6) {
fprintf(CONSOLE_OUT, "Testing GameStream connectivity over the Internet using an IPv6 relay server...\n");
if (TestAllPorts((PSOCKADDR_STORAGE)current->ai_addr, NULL, 0, true)) {
snprintf(msgBuf, sizeof(msgBuf), "This PC has limited connectivity for Internet hosting. It will work only for clients on certain networks.\n\n"
"If you want to try streaming with this configuration, you must pair Moonlight to your gaming PC from your home network before trying to stream over the Internet.\n\n"
"To get full connectivity, please contact your ISP and ask for a \"public IP address\" which they may offer for free upon request. For more information and workarounds, click the Help button.");
DisplayMessage(msgBuf, "https://github.com/moonlight-stream/moonlight-docs/wiki/Internet-Streaming-Errors#limited-connectivity-for-hosting-error", MpWarn);
freeaddrinfo(result);
return 0;
}
}
}
}
freeaddrinfo(result);
if (IsPossibleCGN(&locallyReportedWanAddr)) {
snprintf(msgBuf, sizeof(msgBuf), "Your ISP is running a Carrier-Grade NAT that is preventing you from hosting services like Moonlight on the Internet. Click the Help button for guidance on fixing this issue.");
DisplayMessage(msgBuf, "https://github.com/moonlight-stream/moonlight-docs/wiki/Internet-Streaming-Errors#carrier-grade-nat-error");
}
else {
snprintf(msgBuf, sizeof(msgBuf), "Internet GameStream connectivity check failed. Click the Help button for guidance on fixing this issue.\n\nThe following ports were not forwarded properly:\n%s", portMsgBuf);
DisplayMessage(msgBuf, "https://github.com/moonlight-stream/moonlight-docs/wiki/Internet-Streaming-Errors#internet-gamestream-connectivity-check-error");
}
}
return -1;
}
AllTestsPassed:
snprintf(msgBuf, sizeof(msgBuf), "This PC is ready to host over the Internet!\n\n"
"For the easiest setup, you should pair Moonlight to your gaming PC from your home network before trying to stream over the Internet.\n\n"
"If you can't, you can type the following address into Moonlight's Add PC dialog: %s", wanAddrStr);
DisplayMessage(msgBuf, nullptr, MpInfo);
return 0;
}