Skip to content

Commit f8a8798

Browse files
committed
Add UPnP support via miniupnpc
1 parent 5895898 commit f8a8798

File tree

7 files changed

+305
-0
lines changed

7 files changed

+305
-0
lines changed

CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ CMAKE_DEPENDENT_OPTION(WITH_DEVELOPER_MODE "Use GTK for the developer mode" OFF
9595
"NOT USE_CONSOLE" OFF)
9696
# USE_LIBNOTIFY
9797
CMAKE_DEPENDENT_OPTION(USE_LIBNOTIFY "Use libnotify for desktop notifications" ON "WITH_DEVELOPER_MODE AND NOT USE_CONSOLE AND NOT WIN32" OFF)
98+
# USE_MINIUPNPC
99+
option(USE_MINIUPNPC "Use miniupnpc for UPnP support" ON)
98100
# USE_WINDOWS_RUNTIME
99101
CMAKE_DEPENDENT_OPTION(USE_WINDOWS_RUNTIME "Use Windows Runtime features" ON "WIN32" OFF)
100102

@@ -242,6 +244,10 @@ if (USE_LIBNOTIFY)
242244
src/C4ToastLibNotify.cpp src/C4ToastLibNotify.h)
243245
endif ()
244246

247+
if (USE_MINIUPNPC)
248+
list(APPEND CLONK_SOURCES src/C4Network2UPnPMiniUPnPc.cpp)
249+
endif ()
250+
245251
if (USE_WINDOWS_RUNTIME)
246252
list(APPEND CLONK_SOURCES
247253
src/C4ToastWinRT.cpp src/C4ToastWinRT.h)
@@ -426,6 +432,12 @@ if (USE_LIBNOTIFY)
426432
target_link_libraries(clonk PkgConfig::libnotify)
427433
endif ()
428434

435+
# Link miniupnpc
436+
if (USE_MINIUPNPC)
437+
find_package(miniupnpc CONFIG REQUIRED)
438+
target_link_libraries(clonk miniupnpc::miniupnpc)
439+
endif ()
440+
429441
# Link Windows libraries
430442
if (WIN32)
431443
target_link_libraries(clonk dbghelp dwmapi iphlpapi winmm ws2_32)

cmake/filelists/Engine.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ src/C4Network2Res.h
196196
src/C4Network2ResDlg.cpp
197197
src/C4Network2Stats.cpp
198198
src/C4Network2Stats.h
199+
src/C4Network2UPnP.h
199200
src/C4NumberParsing.h
200201
src/C4Object.cpp
201202
src/C4Object.h

src/C4Network2IO.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <C4Network2Reference.h>
2121

2222
#include <C4Network2Discover.h>
23+
#include "C4Network2UPnP.h"
2324
#include <C4Application.h>
2425
#include <C4UserMessages.h>
2526
#include <C4Log.h>
@@ -106,18 +107,23 @@ bool C4Network2IO::Init(std::shared_ptr<spdlog::logger> logger, const std::uint1
106107
Thread.SetCallback(Ev_Net_Disconn, this);
107108
Thread.SetCallback(Ev_Net_Packet, this);
108109

110+
// initialize UPnP
111+
UPnP = std::make_unique<C4Network2UPnP>();
112+
109113
// initialize net i/o classes: TCP first
110114
pNetIO_TCP = CreateNetIO(this->logger, "TCP I/O", new C4NetIOTCP{}, iPortTCP, Thread);
111115
if (pNetIO_TCP)
112116
{
113117
pNetIO_TCP->SetCallback(this);
118+
UPnP->AddMapping(P_TCP, iPortTCP, 0);
114119
}
115120

116121
// then UDP
117122
pNetIO_UDP = CreateNetIO(this->logger, "UDP I/O", new C4NetIOUDP{}, iPortUDP, Thread);
118123
if (pNetIO_UDP)
119124
{
120125
pNetIO_UDP->SetCallback(this);
126+
UPnP->AddMapping(P_UDP, iPortUDP, 0);
121127
}
122128

123129
// no protocols?
@@ -177,6 +183,8 @@ void C4Network2IO::Clear() // by main thread
177183
if (pNetIO_TCP) { Thread.RemoveProc(pNetIO_TCP); delete pNetIO_TCP; pNetIO_TCP = nullptr; }
178184
if (pNetIO_UDP) { Thread.RemoveProc(pNetIO_UDP); delete pNetIO_UDP; pNetIO_UDP = nullptr; }
179185
if (pRefServer) { Thread.RemoveProc(pRefServer); delete pRefServer; pRefServer = nullptr; }
186+
// clear UPnP
187+
UPnP.reset();
180188
// remove auto-accepts
181189
ClearAutoAccept();
182190
// reset flags

src/C4Network2IO.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ class C4Network2IO
5656
// discovery net i/o
5757
class C4Network2IODiscover *pNetIODiscover;
5858

59+
// UPnP
60+
std::unique_ptr<class C4Network2UPnP> UPnP;
61+
5962
// reference server
6063
class C4Network2RefServer *pRefServer;
6164

src/C4Network2UPnP.h

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* LegacyClonk
3+
*
4+
* Copyright (c) 2022, The LegacyClonk Team and contributors
5+
*
6+
* Distributed under the terms of the ISC license; see accompanying file
7+
* "COPYING" for details.
8+
*
9+
* "Clonk" is a registered trademark of Matthes Bender, used with permission.
10+
* See accompanying file "TRADEMARK" for details.
11+
*
12+
* To redistribute this file separately, substitute the full license texts
13+
* for the above references.
14+
*/
15+
16+
#pragma once
17+
18+
#include "C4Network2IO.h"
19+
20+
#include <memory>
21+
22+
class C4Network2UPnP
23+
{
24+
private:
25+
struct Impl;
26+
27+
public:
28+
C4Network2UPnP();
29+
~C4Network2UPnP() noexcept;
30+
31+
public:
32+
void AddMapping(C4Network2IOProtocol protocol, std::uint16_t internalPort, std::uint16_t externalPort);
33+
void ClearMappings();
34+
35+
private:
36+
const std::unique_ptr<Impl> impl;
37+
};

src/C4Network2UPnPMiniUPnPc.cpp

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/*
2+
* LegacyClonk
3+
*
4+
* Copyright (c) 2022, The LegacyClonk Team and contributors
5+
*
6+
* Distributed under the terms of the ISC license; see accompanying file
7+
* "COPYING" for details.
8+
*
9+
* "Clonk" is a registered trademark of Matthes Bender, used with permission.
10+
* See accompanying file "TRADEMARK" for details.
11+
*
12+
* To redistribute this file separately, substitute the full license texts
13+
* for the above references.
14+
*/
15+
16+
#include "C4Application.h"
17+
#include "C4Network2UPnP.h"
18+
#include "C4Strings.h"
19+
20+
#include <array>
21+
#include <future>
22+
#include <limits>
23+
24+
#include "miniupnpc/miniupnpc.h"
25+
#include "miniupnpc/upnpcommands.h"
26+
#include "miniupnpc/upnperrors.h"
27+
28+
struct C4Network2UPnP::Impl
29+
{
30+
private:
31+
struct PortMapping
32+
{
33+
std::array<char, 4> Protocol;
34+
std::array<char, C4Strings::NumberOfCharactersForDigits<std::uint16_t> + 1> InternalPort;
35+
std::array<char, C4Strings::NumberOfCharactersForDigits<std::uint16_t> + 1> ExternalPort;
36+
37+
PortMapping(const C4Network2IOProtocol protocol, const std::uint16_t internalPort, const std::uint16_t externalPort)
38+
{
39+
if (protocol == P_TCP)
40+
{
41+
Protocol = {"TCP"};
42+
}
43+
else
44+
{
45+
Protocol = {"UDP"};
46+
}
47+
48+
*std::to_chars(InternalPort.data(), InternalPort.data() + InternalPort.size() - 1, internalPort).ptr = '\0';
49+
*std::to_chars(ExternalPort.data(), ExternalPort.data() + ExternalPort.size() - 1, externalPort).ptr = '\0';
50+
}
51+
};
52+
53+
public:
54+
Impl() : logger{CreateLogger("C4Network2UPnP", {.ShowLoggerNameInGui = true})}
55+
{
56+
action = std::async([this]
57+
{
58+
logger->debug("Discovering devices");
59+
int error;
60+
const C4DeleterFunctionUniquePtr<&freeUPNPDevlist> deviceList{upnpDiscover(DiscoveryDelay, nullptr, nullptr, UPNP_LOCAL_PORT_ANY, 0, 2, &error)};
61+
62+
if (!deviceList)
63+
{
64+
logger->error("Failed to get device list: {}", ([error]
65+
{
66+
switch (error)
67+
{
68+
case UPNPDISCOVER_SUCCESS:
69+
return "Success";
70+
71+
case UPNPDISCOVER_UNKNOWN_ERROR:
72+
return "Unknown error";
73+
74+
case UPNPDISCOVER_SOCKET_ERROR:
75+
return "Socket error";
76+
77+
case UPNPDISCOVER_MEMORY_ERROR:
78+
return "Memory error";
79+
80+
default:
81+
return "Invalid error";
82+
}
83+
})());
84+
85+
return;
86+
}
87+
88+
if (const int status{UPNP_GetValidIGD(deviceList.get(), &urls, &igdData, lanAddress.data(), lanAddress.size())}; !status)
89+
{
90+
logger->error("Could not find valid IGD");
91+
}
92+
});
93+
}
94+
95+
~Impl()
96+
{
97+
if (urls.controlURL)
98+
{
99+
FreeUPNPUrls(&urls);
100+
}
101+
}
102+
103+
void AddMapping(const C4Network2IOProtocol protocol, std::uint16_t internalPort, std::uint16_t externalPort)
104+
{
105+
action = std::async([=,this, action = std::move(action)]
106+
{
107+
action.wait();
108+
109+
if (!IsInitialized())
110+
{
111+
return;
112+
}
113+
114+
PortMapping mapping{protocol, internalPort, externalPort > 0 ? externalPort : internalPort};
115+
116+
decltype(mapping.ExternalPort) externalPortBuffer{mapping.ExternalPort};
117+
118+
const std::tuple arguments{
119+
urls.controlURL,
120+
igdData.first.servicetype,
121+
externalPortBuffer.data(),
122+
mapping.InternalPort.data(),
123+
lanAddress.data(),
124+
STD_PRODUCT,
125+
mapping.Protocol.data(),
126+
nullptr,
127+
nullptr
128+
};
129+
130+
const int result{externalPort == 0
131+
? std::apply(&UPNP_AddAnyPortMapping, std::tuple_cat(arguments, std::make_tuple(mapping.ExternalPort.data())))
132+
: std::apply(&UPNP_AddPortMapping, arguments)};
133+
134+
if (result == UPNPCOMMAND_SUCCESS)
135+
{
136+
logger->info(
137+
"Added port mapping {} {} -> {}:{}",
138+
mapping.Protocol.data(),
139+
mapping.ExternalPort.data(),
140+
lanAddress.data(),
141+
mapping.InternalPort.data()
142+
);
143+
144+
mapping.ExternalPort = std::move(externalPortBuffer);
145+
mappings.emplace_back(std::move(mapping));
146+
}
147+
else
148+
{
149+
logger->error(
150+
"Failed to add port mapping {} {} -> {}:{}: {}",
151+
mapping.Protocol.data(),
152+
mapping.ExternalPort.data(),
153+
lanAddress.data(),
154+
mapping.InternalPort.data(),
155+
strupnperror(result)
156+
);
157+
}
158+
});
159+
}
160+
161+
void ClearMappings()
162+
{
163+
action = std::async([this, action = std::move(action)]
164+
{
165+
action.wait();
166+
167+
if (!IsInitialized())
168+
{
169+
return;
170+
}
171+
172+
for (const auto &mapping : mappings)
173+
{
174+
const int result{UPNP_DeletePortMapping(
175+
urls.controlURL,
176+
igdData.first.servicetype,
177+
mapping.ExternalPort.data(),
178+
mapping.Protocol.data(),
179+
nullptr
180+
)};
181+
if (result == UPNPCOMMAND_SUCCESS)
182+
{
183+
logger->info("Removed port mapping {} {} -> {}:{}",
184+
mapping.Protocol.data(),
185+
mapping.ExternalPort.data(),
186+
lanAddress.data(),
187+
mapping.InternalPort.data()
188+
);
189+
190+
mappings.emplace_back(std::move(mapping));
191+
}
192+
else
193+
{
194+
logger->error("Failed to remove port mapping {} {} -> {}:{}: {}",
195+
mapping.Protocol.data(),
196+
mapping.ExternalPort.data(),
197+
lanAddress.data(),
198+
mapping.InternalPort.data(),
199+
strupnperror(result)
200+
);
201+
}
202+
}
203+
204+
mappings.clear();
205+
});
206+
}
207+
208+
bool IsInitialized() const
209+
{
210+
return urls.controlURL;
211+
}
212+
213+
private:
214+
std::shared_ptr<spdlog::logger> logger;
215+
std::future<void> action;
216+
UPNPUrls urls{};
217+
IGDdatas igdData;
218+
std::array<char, 64> lanAddress;
219+
220+
std::vector<PortMapping> mappings;
221+
222+
static constexpr auto DiscoveryDelay = 2000;
223+
};
224+
225+
C4Network2UPnP::C4Network2UPnP()
226+
: impl{std::make_unique<Impl>()}
227+
{
228+
}
229+
230+
C4Network2UPnP::~C4Network2UPnP() noexcept
231+
{
232+
ClearMappings();
233+
}
234+
235+
void C4Network2UPnP::AddMapping(const C4Network2IOProtocol protocol, const std::uint16_t internalPort, const std::uint16_t externalPort)
236+
{
237+
impl->AddMapping(protocol, internalPort, externalPort);
238+
}
239+
240+
void C4Network2UPnP::ClearMappings()
241+
{
242+
impl->ClearMappings();
243+
}

src/C4ResStrTable.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,7 @@ IDS_MSG_TOPICIN=2
757757
IDS_MSG_TRYLEAGUESIGNUP=2
758758
IDS_MSG_UPDATEFAILED=0
759759
IDS_MSG_UPDATENOTAVAILABLE=0
760+
IDS_MSG_UPNPHINT=0
760761
IDS_MSG_USINGPLR=1
761762
IDS_MSG_USINGPLR_DESC=0
762763
IDS_MSG_WASKICKEDFROMTHECHANNEL=2

0 commit comments

Comments
 (0)