Skip to content

Commit 6655c32

Browse files
committed
[INTERNAL][CORE] moves Socks5Logic out of XMPPConnection
The whole logic regarding the Socks5Proxy is now handled in its own class. In addition only the STUN discovery is performed asynchronous. Assuming the UPNP router works as expected mapping and unmapping ports just takes a few milliseconds.
1 parent 7d6531c commit 6655c32

3 files changed

Lines changed: 344 additions & 230 deletions

File tree

core/src/saros/net/util/NetworkingUtils.java

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import java.net.InetAddress;
55
import java.net.NetworkInterface;
66
import java.net.SocketException;
7-
import java.util.ArrayList;
87
import java.util.Enumeration;
98
import java.util.LinkedList;
109
import java.util.List;
@@ -78,25 +77,4 @@ public static Socks5Proxy getSocks5ProxySafe() {
7877

7978
return proxy;
8079
}
81-
82-
/**
83-
* Adds a specified IP (String) to the list of addresses of the Socks5Proxy. (the target attempts
84-
* the stream host addresses one by one in the order of the list)
85-
*
86-
* @param ip String of the address of the Socks5Proxy (stream host)
87-
* @param inFront boolean flag, if the address is to be inserted in front of the list. If <code>
88-
* false</code>, address is added at the end of the list.
89-
*/
90-
public static void addProxyAddress(String ip, boolean inFront) {
91-
Socks5Proxy proxy = getSocks5ProxySafe();
92-
93-
if (!inFront) {
94-
proxy.addLocalAddress(ip);
95-
return;
96-
}
97-
ArrayList<String> list = new ArrayList<String>(proxy.getLocalAddresses());
98-
list.remove(ip);
99-
list.add(0, ip);
100-
proxy.replaceLocalAddresses(list);
101-
}
10280
}
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
package saros.net.xmpp;
2+
3+
import java.net.InetAddress;
4+
import java.net.InetSocketAddress;
5+
import java.util.ArrayList;
6+
import java.util.Collection;
7+
import java.util.Collections;
8+
import java.util.List;
9+
import java.util.stream.Collectors;
10+
import org.apache.log4j.Logger;
11+
import org.bitlet.weupnp.GatewayDevice;
12+
import org.jivesoftware.smack.SmackConfiguration;
13+
import org.jivesoftware.smackx.bytestreams.socks5.Socks5Proxy;
14+
import saros.net.stun.IStunService;
15+
import saros.net.upnp.IUPnPService;
16+
import saros.net.util.NetworkingUtils;
17+
import saros.util.ThreadUtils;
18+
19+
/**
20+
* Access class for accessing the Smack Socks5 proxy. It supports UPNP and STUN to retrieve possible
21+
* Socks5 candidates and allows access to the local Smack Socks5 proxy behind gateways that supports
22+
* UPNP.
23+
*/
24+
class Socks5ProxySupport {
25+
26+
private static final int STUN_DISCOVERY_TIMEOUT = 10000;
27+
28+
/* DO NOT CHANGE THE CONTENT OF THIS STRING, NEVER NEVER NEVER !!!
29+
* The UNPN implementation will currently not overwrite present PORT mappings if they share not
30+
* the same description. */
31+
private static final String UPNP_PORT_MAPPING_DESCRIPTION = "Saros Socks5 TCP";
32+
33+
private static final Logger log = Logger.getLogger(Socks5ProxySupport.class);
34+
35+
private static final Object socks5AddressReplacementLock = new Object();
36+
37+
private final IUPnPService upnpService;
38+
private final IStunService stunService;
39+
40+
/** The current gateway device to use for port mapping or <code>null</code>. */
41+
private GatewayDevice device;
42+
43+
/** The current used Socks5 proxy or <code>null</code>. */
44+
private Socks5Proxy socks5Proxy;
45+
46+
/**
47+
* There is so much magic involved in Smack. Performing a disconnect shuts the proxy down so we
48+
* need to remember the port.
49+
*/
50+
private int socks5ProxyPort;
51+
52+
public Socks5ProxySupport(final IUPnPService upnpService, final IStunService stunService) {
53+
this.upnpService = upnpService;
54+
this.stunService = stunService;
55+
}
56+
57+
/**
58+
* Enables the Socks5 proxy. If the port number is negative the logic will try to find an unused
59+
* port starting with the positive value of the port number up to 65535.
60+
*
61+
* @param port the port to use
62+
* @param proxyAddresses collection containing addresses that should be published as public
63+
* addresses for connection purpose
64+
* @param gatewayDeviceId the ID an UPNP device for port mapping or <code>null</code>
65+
* @param useExternalGatewayDeviceAddress if <code>true</code> the public address of the device
66+
* will be published
67+
* @param stunServerAddress address of a stun server to retrieve and publish public addresses or
68+
* <code>null</code>
69+
* @param stunServerPort port of the stun server, not used if <code>stunServerAddress</code> is
70+
* <code>null</code>
71+
* @return <code>true</code> if the proxy was successfully started, <code>false</code> if it could
72+
* not be started or is already running
73+
*/
74+
public synchronized boolean enableProxy(
75+
final int port,
76+
final Collection<String> proxyAddresses,
77+
final String gatewayDeviceId,
78+
final boolean useExternalGatewayDeviceAddress,
79+
final String stunServerAddress,
80+
final int stunServerPort) {
81+
82+
if (socks5Proxy != null) return false;
83+
84+
SmackConfiguration.setLocalSocks5ProxyEnabled(true);
85+
SmackConfiguration.setLocalSocks5ProxyPort(port);
86+
87+
socks5Proxy = Socks5Proxy.getSocks5Proxy();
88+
89+
if (socks5Proxy == null) {
90+
log.warn("failed to start Socks5 proxy on port: " + port);
91+
SmackConfiguration.setLocalSocks5ProxyEnabled(false);
92+
return false;
93+
}
94+
95+
socks5ProxyPort = socks5Proxy.getPort();
96+
97+
// Unlikely but the connection was already lost, but signal that the proxy as running.
98+
if (socks5ProxyPort <= 0) return true;
99+
100+
// Remove any addresses that Smack already discovered because we use our own logic.
101+
socks5Proxy.replaceLocalAddresses(Collections.emptyList());
102+
103+
log.info(
104+
"started Socks5 proxy on port: "
105+
+ socks5Proxy.getPort()
106+
+ " [listening on all interfaces]");
107+
108+
final List<String> proxyAddressesToPublish = new ArrayList<>();
109+
110+
if (proxyAddresses != null && proxyAddresses.isEmpty())
111+
log.warn("Socks5 preconfigured addresses list is empty, using autodetect mode");
112+
113+
if (proxyAddresses == null || proxyAddresses.isEmpty()) {
114+
NetworkingUtils.getAllNonLoopbackLocalIPAddresses(true)
115+
.stream()
116+
.map(InetAddress::getHostAddress)
117+
.forEach(proxyAddressesToPublish::add);
118+
} else {
119+
proxyAddressesToPublish.addAll(proxyAddresses);
120+
}
121+
122+
addSocks5ProxyAddresses(socks5Proxy, proxyAddressesToPublish, true);
123+
124+
// as STUN discovery can fail, take ages etc. do not block here
125+
if (stunService != null && stunServerAddress != null && !stunServerAddress.isEmpty()) {
126+
127+
ThreadUtils.runSafeAsync(
128+
"saros-stun-discovery",
129+
log,
130+
() -> {
131+
discoverAndPublishStunAddresses(socks5Proxy, stunServerAddress, stunServerPort);
132+
});
133+
}
134+
135+
if (upnpService != null && gatewayDeviceId != null && !gatewayDeviceId.isEmpty()) {
136+
device = getGatewayDevice(gatewayDeviceId);
137+
138+
if (device == null) {
139+
log.warn(
140+
"could not find a gateway device with id: + "
141+
+ gatewayDeviceId
142+
+ " in the current network environment");
143+
} else {
144+
mapPort(socks5Proxy, upnpService, device);
145+
146+
if (useExternalGatewayDeviceAddress) {
147+
final InetAddress externalAddress = upnpService.getExternalAddress(device);
148+
149+
if (externalAddress != null) {
150+
log.debug(
151+
"obtained public IP address "
152+
+ externalAddress
153+
+ " from device: "
154+
+ device.getFriendlyName());
155+
156+
addSocks5ProxyAddresses(
157+
socks5Proxy, Collections.singletonList(externalAddress.getHostAddress()), true);
158+
}
159+
}
160+
}
161+
}
162+
163+
return true;
164+
}
165+
166+
/**
167+
* Stops the current Socks5 proxy if enabled and disables further usage of the Socks5 proxy. This
168+
* method can be safely called to just prevent the global usage of the Socks5 proxy.
169+
*/
170+
public synchronized void disableProxy() {
171+
172+
if (socks5Proxy == null) {
173+
SmackConfiguration.setLocalSocks5ProxyEnabled(false);
174+
return;
175+
}
176+
177+
socks5Proxy.stop();
178+
179+
SmackConfiguration.setLocalSocks5ProxyEnabled(false);
180+
181+
socks5Proxy = null;
182+
183+
log.info("stopped Socks5 proxy on port: " + socks5ProxyPort);
184+
185+
if (socks5ProxyPort > 0 && device != null) {
186+
assert upnpService != null;
187+
unmapPort(upnpService, device, socks5ProxyPort);
188+
}
189+
190+
socks5ProxyPort = 0;
191+
device = null;
192+
}
193+
194+
private GatewayDevice getGatewayDevice(final String gatewayDeviceId) {
195+
assert (upnpService != null);
196+
197+
final List<GatewayDevice> devices = upnpService.getGateways(false);
198+
199+
if (devices == null) {
200+
log.warn("unable to retrieve gateway device(s) due to network failure");
201+
return null;
202+
}
203+
204+
for (GatewayDevice currentDevice : devices) {
205+
if (gatewayDeviceId.equals(currentDevice.getUSN())) {
206+
return currentDevice;
207+
}
208+
}
209+
210+
return null;
211+
}
212+
213+
private static void mapPort(
214+
final Socks5Proxy proxy, final IUPnPService upnpService, final GatewayDevice device) {
215+
216+
final int socks5ProxyPort = proxy.getPort();
217+
218+
if (socks5ProxyPort <= 0) return;
219+
220+
upnpService.deletePortMapping(device, socks5ProxyPort, IUPnPService.TCP);
221+
222+
if (!upnpService.createPortMapping(
223+
device, socks5ProxyPort, IUPnPService.TCP, UPNP_PORT_MAPPING_DESCRIPTION)) {
224+
225+
log.warn(
226+
"failed to create port mapping on device: "
227+
+ device.getFriendlyName()
228+
+ " ["
229+
+ socks5ProxyPort
230+
+ "|"
231+
+ IUPnPService.TCP
232+
+ "]");
233+
234+
return;
235+
}
236+
237+
log.info(
238+
"added port mapping on device: "
239+
+ device.getFriendlyName()
240+
+ " ["
241+
+ socks5ProxyPort
242+
+ "|"
243+
+ IUPnPService.TCP
244+
+ "]");
245+
}
246+
247+
private static void unmapPort(
248+
final IUPnPService upnpService, final GatewayDevice device, final int port) {
249+
250+
if (!upnpService.isMapped(device, port, IUPnPService.TCP)) return;
251+
252+
if (!upnpService.deletePortMapping(device, port, IUPnPService.TCP)) {
253+
log.warn(
254+
"failed to delete port mapping on device: "
255+
+ device.getFriendlyName()
256+
+ " ["
257+
+ port
258+
+ "|"
259+
+ IUPnPService.TCP
260+
+ "]");
261+
}
262+
263+
log.info(
264+
"removed port mapping on device: "
265+
+ device.getFriendlyName()
266+
+ " ["
267+
+ port
268+
+ "|"
269+
+ IUPnPService.TCP
270+
+ "]");
271+
}
272+
273+
private void discoverAndPublishStunAddresses(
274+
final Socks5Proxy proxy, final String stunServerAddress, final int stunServerPort) {
275+
276+
assert (stunService != null);
277+
278+
final Collection<InetSocketAddress> addresses =
279+
stunService.discover(stunServerAddress, stunServerPort, STUN_DISCOVERY_TIMEOUT);
280+
281+
if (addresses.isEmpty()) {
282+
log.warn(
283+
"could not discover any public address using STUN server "
284+
+ stunServerAddress
285+
+ " (port="
286+
+ stunServerPort
287+
+ ")");
288+
289+
return;
290+
}
291+
292+
// stun returns always literal IP addresses
293+
final List<String> discoveredAddresses =
294+
addresses.stream().map(InetSocketAddress::getHostString).collect(Collectors.toList());
295+
log.debug(
296+
"STUN discovery result for STUN server "
297+
+ stunServerAddress
298+
+ " (port="
299+
+ stunServerPort
300+
+ ") : "
301+
+ discoveredAddresses);
302+
303+
addSocks5ProxyAddresses(proxy, discoveredAddresses, true);
304+
}
305+
306+
private static void addSocks5ProxyAddresses(
307+
final Socks5Proxy proxy, final Collection<String> addresses, boolean inFront) {
308+
309+
synchronized (socks5AddressReplacementLock) {
310+
final List<String> newAddresses = new ArrayList<>();
311+
312+
if (inFront) newAddresses.addAll(addresses);
313+
314+
newAddresses.addAll(proxy.getLocalAddresses());
315+
316+
if (!inFront) newAddresses.addAll(addresses);
317+
318+
final List<String> distinctAddresses =
319+
newAddresses.stream().sequential().distinct().collect(Collectors.toList());
320+
321+
log.info("Socks5 proxy - public published IP addresses : " + distinctAddresses);
322+
323+
proxy.replaceLocalAddresses(distinctAddresses);
324+
}
325+
}
326+
}

0 commit comments

Comments
 (0)