diff --git a/GSv6Fwd.sln b/GSv6Fwd.sln new file mode 100644 index 0000000..dbf5cfa --- /dev/null +++ b/GSv6Fwd.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.10 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GSv6Fwd", "GSv6Fwd\GSv6Fwd.vcxproj", "{87DEAE49-7638-4CDB-88EB-054B1F3CB0D2}" +EndProject +Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "GSv6FwdSetup", "GSv6FwdSetup\GSv6FwdSetup.wixproj", "{F8171B99-F5F9-4ABF-9FE5-6753539611AF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {87DEAE49-7638-4CDB-88EB-054B1F3CB0D2}.Debug|x64.ActiveCfg = Debug|x64 + {87DEAE49-7638-4CDB-88EB-054B1F3CB0D2}.Debug|x64.Build.0 = Debug|x64 + {87DEAE49-7638-4CDB-88EB-054B1F3CB0D2}.Debug|x86.ActiveCfg = Debug|Win32 + {87DEAE49-7638-4CDB-88EB-054B1F3CB0D2}.Debug|x86.Build.0 = Debug|Win32 + {87DEAE49-7638-4CDB-88EB-054B1F3CB0D2}.Release|x64.ActiveCfg = Release|x64 + {87DEAE49-7638-4CDB-88EB-054B1F3CB0D2}.Release|x64.Build.0 = Release|x64 + {87DEAE49-7638-4CDB-88EB-054B1F3CB0D2}.Release|x86.ActiveCfg = Release|Win32 + {87DEAE49-7638-4CDB-88EB-054B1F3CB0D2}.Release|x86.Build.0 = Release|Win32 + {F8171B99-F5F9-4ABF-9FE5-6753539611AF}.Debug|x64.ActiveCfg = Debug|x86 + {F8171B99-F5F9-4ABF-9FE5-6753539611AF}.Debug|x86.ActiveCfg = Debug|x86 + {F8171B99-F5F9-4ABF-9FE5-6753539611AF}.Debug|x86.Build.0 = Debug|x86 + {F8171B99-F5F9-4ABF-9FE5-6753539611AF}.Release|x64.ActiveCfg = Release|x86 + {F8171B99-F5F9-4ABF-9FE5-6753539611AF}.Release|x86.ActiveCfg = Release|x86 + {F8171B99-F5F9-4ABF-9FE5-6753539611AF}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9C375CA2-3B67-4C23-B946-B3A5C8A79190} + EndGlobalSection +EndGlobal diff --git a/GSv6Fwd/GSv6Fwd.cpp b/GSv6Fwd/GSv6Fwd.cpp new file mode 100644 index 0000000..70a0ee4 --- /dev/null +++ b/GSv6Fwd/GSv6Fwd.cpp @@ -0,0 +1,557 @@ +#define WIN32_LEAN_AND_MEAN +#include + +#include +#include + +#pragma comment(lib, "ws2_32") +#include +#include + +#pragma comment(lib, "iphlpapi") +#include + +#define SERVICE_NAME L"GSv6FwdSvc" + +static const unsigned short UDP_PORTS[] = { + 47998, 47999, 48000, 48010 +}; + +static const unsigned short TCP_PORTS[] = { + 47984, 47989 +}; + +typedef struct _SOCKET_TUPLE { + SOCKET s1; + SOCKET s2; +} SOCKET_TUPLE, *PSOCKET_TUPLE; + +typedef struct _LISTENER_TUPLE { + SOCKET listener; + unsigned short port; +} LISTENER_TUPLE, *PLISTENER_TUPLE; + +typedef struct _UDP_TUPLE { + SOCKET ipv6Socket; + SOCKET ipv4Socket; + unsigned short port; +} UDP_TUPLE, *PUDP_TUPLE; + +int +ForwardSocketData(SOCKET from, SOCKET to) +{ + char buffer[4096]; + int len; + + len = recv(from, buffer, sizeof(buffer), 0); + if (len <= 0) { + return len; + } + + if (send(to, buffer, len, 0) != len) { + return SOCKET_ERROR; + } + + return len; +} + +DWORD +WINAPI +TcpRelayThreadProc(LPVOID Context) +{ + PSOCKET_TUPLE tuple = (PSOCKET_TUPLE)Context; + fd_set fds; + int err; + bool s1ReadShutdown = false; + bool s2ReadShutdown = false; + + for (;;) { + FD_ZERO(&fds); + + if (!s1ReadShutdown) { + FD_SET(tuple->s1, &fds); + } + if (!s2ReadShutdown) { + FD_SET(tuple->s2, &fds); + } + if (s1ReadShutdown && s2ReadShutdown) { + // Both sides gracefully closed + break; + } + + err = select(0, &fds, NULL, NULL, NULL); + if (err <= 0) { + break; + } + else if (FD_ISSET(tuple->s1, &fds)) { + err = ForwardSocketData(tuple->s1, tuple->s2); + if (err == 0) { + // Graceful closure from s1. Propagate to s2. + shutdown(tuple->s2, SD_SEND); + s1ReadShutdown = true; + } + else if (err < 0) { + // Forceful closure. Tear down the whole connection. + break; + } + } + else if (FD_ISSET(tuple->s2, &fds)) { + err = ForwardSocketData(tuple->s2, tuple->s1); + if (err == 0) { + // Graceful closure from s2. Propagate to s1. + shutdown(tuple->s1, SD_SEND); + s2ReadShutdown = true; + } + else if (err < 0) { + // Forceful closure. Tear down the whole connection. + break; + } + } + } + + closesocket(tuple->s1); + closesocket(tuple->s2); + free(tuple); + return 0; +} + +int +FindLocalAddressBySocket(SOCKET s, PIN_ADDR targetAddress) +{ + union { + IP_ADAPTER_ADDRESSES addresses; + char buffer[8192]; + }; + ULONG error; + ULONG length; + PIP_ADAPTER_ADDRESSES currentAdapter; + PIP_ADAPTER_UNICAST_ADDRESS currentAddress; + SOCKADDR_IN6 localSockAddr; + int localSockAddrLen; + + // Get local address of the accepted socket so we can find the interface + localSockAddrLen = sizeof(localSockAddr); + if (getsockname(s, (PSOCKADDR)&localSockAddr, &localSockAddrLen) == SOCKET_ERROR) { + fprintf(stderr, "getsockname() failed: %d\n", WSAGetLastError()); + return WSAGetLastError(); + } + + // Get a list of all interfaces and addresses on the system + length = sizeof(buffer); + error = GetAdaptersAddresses(AF_UNSPEC, + GAA_FLAG_SKIP_ANYCAST | + GAA_FLAG_SKIP_MULTICAST | + GAA_FLAG_SKIP_DNS_SERVER | + GAA_FLAG_SKIP_FRIENDLY_NAME, + NULL, + &addresses, + &length); + if (error != ERROR_SUCCESS) { + fprintf(stderr, "GetAdaptersAddresses() failed: %d\n", error); + return error; + } + + // First, find the interface that owns the incoming address + currentAdapter = &addresses; + while (currentAdapter != NULL) { + // Check if this interface has the IP address we want + currentAddress = currentAdapter->FirstUnicastAddress; + while (currentAddress != NULL) { + if (currentAddress->Address.lpSockaddr->sa_family == AF_INET6) { + PSOCKADDR_IN6 ifaceAddrV6 = (PSOCKADDR_IN6)currentAddress->Address.lpSockaddr; + if (RtlEqualMemory(&localSockAddr.sin6_addr, &ifaceAddrV6->sin6_addr, sizeof(IN6_ADDR))) { + break; + } + } + + currentAddress = currentAddress->Next; + } + + if (currentAddress != NULL) { + // It does, bail out + break; + } + + currentAdapter = currentAdapter->Next; + } + + // Check if we found the incoming interface + if (currentAdapter == NULL) { + // Hopefully the error is caused by transient interface reconfiguration + fprintf(stderr, "Unable to find incoming interface\n"); + return WSAENETDOWN; + } + + // Now find an IPv4 address on this interface + currentAddress = currentAdapter->FirstUnicastAddress; + while (currentAddress != NULL) { + if (currentAddress->Address.lpSockaddr->sa_family == AF_INET) { + PSOCKADDR_IN ifaceAddrV4 = (PSOCKADDR_IN)currentAddress->Address.lpSockaddr; + *targetAddress = ifaceAddrV4->sin_addr; + return 0; + } + + currentAddress = currentAddress->Next; + } + + // If we get here, there was no IPv4 address on this interface. + // This is a valid situation, for example if the IPv6 interface + // has no IPv4 connectivity. In this case, we can preserve most + // functionality by forwarding via localhost. WoL won't work but + // the basic stuff will. + fprintf(stderr, "WARNING: No IPv4 connectivity on incoming interface\n"); + targetAddress->S_un.S_addr = htonl(INADDR_LOOPBACK); + return 0; +} + +DWORD +WINAPI +TcpListenerThreadProc(LPVOID Context) +{ + PLISTENER_TUPLE tuple = (PLISTENER_TUPLE)Context; + SOCKET acceptedSocket, targetSocket; + SOCKADDR_IN targetAddress; + PSOCKET_TUPLE relayTuple; + HANDLE thread; + + printf("TCP relay running for port %d\n", tuple->port); + + for (;;) { + acceptedSocket = accept(tuple->listener, NULL, 0); + if (acceptedSocket == INVALID_SOCKET) { + fprintf(stderr, "accept() failed: %d\n", WSAGetLastError()); + break; + } + + targetSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (targetSocket == INVALID_SOCKET) { + fprintf(stderr, "socket() failed: %d\n", WSAGetLastError()); + closesocket(acceptedSocket); + continue; + } + + RtlZeroMemory(&targetAddress, sizeof(targetAddress)); + targetAddress.sin_family = AF_INET; + targetAddress.sin_port = htons(tuple->port); + if (FindLocalAddressBySocket(acceptedSocket, &targetAddress.sin_addr) != 0) { + continue; + } + + if (connect(targetSocket, (PSOCKADDR)&targetAddress, sizeof(targetAddress)) == SOCKET_ERROR) { + fprintf(stderr, "connect() failed: %d\n", WSAGetLastError()); + closesocket(acceptedSocket); + closesocket(targetSocket); + continue; + } + + relayTuple = (PSOCKET_TUPLE)malloc(sizeof(*relayTuple)); + if (relayTuple == NULL) { + closesocket(acceptedSocket); + closesocket(targetSocket); + break; + } + + relayTuple->s1 = acceptedSocket; + relayTuple->s2 = targetSocket; + + thread = CreateThread(NULL, 0, TcpRelayThreadProc, relayTuple, 0, NULL); + if (thread == INVALID_HANDLE_VALUE) { + fprintf(stderr, "CreateThread() failed: %d\n", GetLastError()); + closesocket(acceptedSocket); + closesocket(targetSocket); + free(relayTuple); + break; + } + + CloseHandle(thread); + } + + closesocket(tuple->listener); + free(tuple); + return 0; +} + +int StartTcpRelay(unsigned short Port) +{ + SOCKET listeningSocket; + SOCKADDR_IN6 addr6; + HANDLE thread; + PLISTENER_TUPLE tuple; + + listeningSocket = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); + if (listeningSocket == INVALID_SOCKET) { + fprintf(stderr, "socket() failed: %d\n", WSAGetLastError()); + return WSAGetLastError(); + } + + RtlZeroMemory(&addr6, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons(Port); + if (bind(listeningSocket, (PSOCKADDR)&addr6, sizeof(addr6)) == SOCKET_ERROR) { + fprintf(stderr, "bind() failed: %d\n", WSAGetLastError()); + return WSAGetLastError(); + } + + if (listen(listeningSocket, SOMAXCONN) == SOCKET_ERROR) { + fprintf(stderr, "listen() failed: %d\n", WSAGetLastError()); + return WSAGetLastError(); + } + + tuple = (PLISTENER_TUPLE)malloc(sizeof(*tuple)); + if (tuple == NULL) { + return ERROR_OUTOFMEMORY; + } + + tuple->listener = listeningSocket; + tuple->port = Port; + + thread = CreateThread(NULL, 0, TcpListenerThreadProc, tuple, 0, NULL); + if (thread == INVALID_HANDLE_VALUE) { + fprintf(stderr, "CreateThread() failed: %d\n", GetLastError()); + return GetLastError(); + } + + CloseHandle(thread); + return 0; +} + +int +ForwardUdpPacket(SOCKET from, SOCKET to, + PSOCKADDR target, int targetLen, + PSOCKADDR source, int sourceLen) +{ + int len; + char buffer[4096]; + + len = recvfrom(from, buffer, sizeof(buffer), 0, source, &sourceLen); + if (len < 0) { + fprintf(stderr, "recvfrom() failed: %d\n", WSAGetLastError()); + return WSAGetLastError(); + } + + if (sendto(to, buffer, len, 0, target, targetLen) != len) { + fprintf(stderr, "sendto() failed: %d\n", WSAGetLastError()); + // Fake success, since we may just be waiting for a target address + } + + return 0; +} + +DWORD +WINAPI +UdpRelayThreadProc(LPVOID Context) +{ + PUDP_TUPLE tuple = (PUDP_TUPLE)Context; + fd_set fds; + int err; + SOCKADDR_IN6 lastRemote; + SOCKADDR_IN localTarget; + + printf("UDP relay running for port %d\n", tuple->port); + + RtlZeroMemory(&localTarget, sizeof(localTarget)); + localTarget.sin_family = AF_INET; + localTarget.sin_port = htons(tuple->port); + localTarget.sin_addr.S_un.S_addr = htonl(INADDR_LOOPBACK); + + RtlZeroMemory(&lastRemote, sizeof(lastRemote)); + + for (;;) { + FD_ZERO(&fds); + + FD_SET(tuple->ipv6Socket, &fds); + FD_SET(tuple->ipv4Socket, &fds); + + err = select(0, &fds, NULL, NULL, NULL); + if (err <= 0) { + break; + } + else if (FD_ISSET(tuple->ipv6Socket, &fds)) { + // Forwarding incoming IPv6 packets to the IPv4 port + // and storing the source address as our current remote + // target for sending IPv4 data back. + err = ForwardUdpPacket(tuple->ipv6Socket, tuple->ipv4Socket, + (PSOCKADDR)&localTarget, sizeof(localTarget), + (PSOCKADDR)&lastRemote, sizeof(lastRemote)); + if (err < 0) { + break; + } + } + else if (FD_ISSET(tuple->ipv4Socket, &fds)) { + // Forwarding incoming IPv4 packets to the last known + // address IPv6 address we've heard from. Discard the source. + SOCKADDR_STORAGE unused; + err = ForwardUdpPacket(tuple->ipv4Socket, tuple->ipv6Socket, + (PSOCKADDR)&lastRemote, sizeof(lastRemote), + (PSOCKADDR)&unused, sizeof(unused)); + if (err < 0) { + break; + } + } + } + + closesocket(tuple->ipv6Socket); + closesocket(tuple->ipv4Socket); + free(tuple); + return 0; +} + +int StartUdpRelay(unsigned short Port) +{ + SOCKET ipv6Socket; + SOCKET ipv4Socket; + SOCKADDR_IN6 addr6; + SOCKADDR_IN addr; + PUDP_TUPLE tuple; + HANDLE thread; + + ipv6Socket = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); + if (ipv6Socket == INVALID_SOCKET) { + fprintf(stderr, "socket() failed: %d\n", WSAGetLastError()); + return WSAGetLastError(); + } + + RtlZeroMemory(&addr6, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons(Port); + if (bind(ipv6Socket, (PSOCKADDR)&addr6, sizeof(addr6)) == SOCKET_ERROR) { + fprintf(stderr, "bind() failed: %d\n", WSAGetLastError()); + return WSAGetLastError(); + } + + ipv4Socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (ipv4Socket == INVALID_SOCKET) { + fprintf(stderr, "socket() failed: %d\n", WSAGetLastError()); + return WSAGetLastError(); + } + + RtlZeroMemory(&addr, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.S_un.S_addr = htonl(INADDR_LOOPBACK); + if (bind(ipv4Socket, (PSOCKADDR)&addr, sizeof(addr)) == SOCKET_ERROR) { + fprintf(stderr, "bind() failed: %d\n", WSAGetLastError()); + return WSAGetLastError(); + } + + tuple = (PUDP_TUPLE)malloc(sizeof(*tuple)); + if (tuple == NULL) { + return ERROR_OUTOFMEMORY; + } + + tuple->ipv4Socket = ipv4Socket; + tuple->ipv6Socket = ipv6Socket; + tuple->port = Port; + + thread = CreateThread(NULL, 0, UdpRelayThreadProc, tuple, 0, NULL); + if (thread == INVALID_HANDLE_VALUE) { + fprintf(stderr, "CreateThread() failed: %d\n", GetLastError()); + return GetLastError(); + } + + CloseHandle(thread); + + return 0; +} + +int Run(void) +{ + int err; + WSADATA data; + + err = WSAStartup(MAKEWORD(2, 0), &data); + if (err == SOCKET_ERROR) { + fprintf(stderr, "WSAStartup() failed: %d\n", err); + return err; + } + + for (int i = 0; i < ARRAYSIZE(TCP_PORTS); i++) { + err = StartTcpRelay(TCP_PORTS[i]); + if (err != 0) { + fprintf(stderr, "Failed to start relay on TCP %d: %d\n", TCP_PORTS[i], err); + return err; + } + } + + for (int i = 0; i < ARRAYSIZE(UDP_PORTS); i++) { + err = StartUdpRelay(UDP_PORTS[i]); + if (err != 0) { + fprintf(stderr, "Failed to start relay on UDP %d: %d\n", UDP_PORTS[i], err); + return err; + } + } + + return 0; +} + +static SERVICE_STATUS_HANDLE ServiceStatusHandle; +static SERVICE_STATUS ServiceStatus; + +DWORD +WINAPI +HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext) +{ + switch (dwControl) + { + case SERVICE_CONTROL_INTERROGATE: + return NO_ERROR; + + case SERVICE_CONTROL_STOP: + ServiceStatus.dwCurrentState = SERVICE_STOPPED; + SetServiceStatus(ServiceStatusHandle, &ServiceStatus); + return NO_ERROR; + + default: + return NO_ERROR; + } +} + +VOID +WINAPI +ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) +{ + int err; + + ServiceStatusHandle = RegisterServiceCtrlHandlerEx(SERVICE_NAME, HandlerEx, NULL); + if (ServiceStatusHandle == NULL) { + fprintf(stderr, "RegisterServiceCtrlHandlerEx() failed: %d\n", GetLastError()); + return; + } + + ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + ServiceStatus.dwServiceSpecificExitCode = 0; + ServiceStatus.dwWin32ExitCode = NO_ERROR; + ServiceStatus.dwWaitHint = 0; + ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP; + ServiceStatus.dwCheckPoint = 0; + + // Start the relay + err = Run(); + if (err != 0) { + ServiceStatus.dwCurrentState = SERVICE_STOPPED; + ServiceStatus.dwWin32ExitCode = err; + SetServiceStatus(ServiceStatusHandle, &ServiceStatus); + return; + } + + // Tell SCM we're running + ServiceStatus.dwCurrentState = SERVICE_RUNNING; + SetServiceStatus(ServiceStatusHandle, &ServiceStatus); +} + + +static const SERVICE_TABLE_ENTRY ServiceTable[] = { + { SERVICE_NAME, ServiceMain }, + { NULL, NULL } +}; + +int main(int argc, char* argv[]) +{ + if (argc == 2 && !strcmp(argv[1], "exe")) { + Run(); + SuspendThread(GetCurrentThread()); + return 0; + } + + return StartServiceCtrlDispatcher(ServiceTable); +} + diff --git a/GSv6Fwd/GSv6Fwd.vcxproj b/GSv6Fwd/GSv6Fwd.vcxproj new file mode 100644 index 0000000..4946851 --- /dev/null +++ b/GSv6Fwd/GSv6Fwd.vcxproj @@ -0,0 +1,155 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {87DEAE49-7638-4CDB-88EB-054B1F3CB0D2} + Win32Proj + GSv6Fwd + 10.0.15063.0 + + + + Application + true + v141 + Unicode + + + Application + false + v141 + true + Unicode + + + Application + true + v141 + Unicode + + + Application + false + v141 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + + + true + + + false + + + false + + + + NotUsing + Level3 + Disabled + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + MultiThreaded + + + Console + true + + + + + NotUsing + Level3 + Disabled + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + MultiThreaded + + + Console + true + + + + + NotUsing + Level3 + MaxSpeed + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + MultiThreaded + + + Console + true + true + true + + + + + NotUsing + Level3 + MaxSpeed + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + MultiThreaded + + + Console + true + true + true + + + + + + + + + \ No newline at end of file diff --git a/GSv6Fwd/GSv6Fwd.vcxproj.filters b/GSv6Fwd/GSv6Fwd.vcxproj.filters new file mode 100644 index 0000000..5b082c6 --- /dev/null +++ b/GSv6Fwd/GSv6Fwd.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + \ No newline at end of file diff --git a/GSv6FwdSetup/GSv6FwdSetup.wixproj b/GSv6FwdSetup/GSv6FwdSetup.wixproj new file mode 100644 index 0000000..8bd1f1d --- /dev/null +++ b/GSv6FwdSetup/GSv6FwdSetup.wixproj @@ -0,0 +1,47 @@ + + + + Debug + x86 + 3.10 + f8171b99-f5f9-4abf-9fe5-6753539611af + 2.0 + GSv6FwdSetup + Package + + + bin\$(Configuration)\ + obj\$(Configuration)\ + Debug + + + bin\$(Configuration)\ + obj\$(Configuration)\ + + + + + + + GSv6Fwd + {87deae49-7638-4cdb-88eb-054b1f3cb0d2} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + + + + + + + \ No newline at end of file diff --git a/GSv6FwdSetup/Product.wxs b/GSv6FwdSetup/Product.wxs new file mode 100644 index 0000000..94197ed --- /dev/null +++ b/GSv6FwdSetup/Product.wxs @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +