Compare commits

..

2 Commits

Author SHA1 Message Date
5e66dccc29 Try to open tunnel only if its configured on server side
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-10-07 15:20:30 +03:00
53a88b6d99 Implemented tunnels for FlexConnect
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-07 00:42:55 +03:00
7 changed files with 242 additions and 24 deletions

22
pom.xml
View File

@ -33,6 +33,19 @@
<target>16</target> <target>16</target>
</configuration> </configuration>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins> </plugins>
</build> </build>
@ -50,6 +63,15 @@
<version>1.16.4-R0.1-SNAPSHOT</version> <version>1.16.4-R0.1-SNAPSHOT</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<!--<groupId>com.sshtools</groupId>
<artifactId>maverick-synergy-client</artifactId>
<version>3.1.1</version> -->
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-core</artifactId>
<version>2.14.0</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -3,6 +3,7 @@ package fi.flexplex.connect;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException;
import java.util.HashSet; import java.util.HashSet;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
@ -11,10 +12,13 @@ import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import fi.flexplex.connect.util.FileChangeListener; import fi.flexplex.connect.util.FileChangeListener;
import fi.flexplex.connect.util.Proxy;
import fi.flexplex.connect.util.Tunnel;
public final class FlexConnect extends JavaPlugin { public final class FlexConnect extends JavaPlugin {
private final Set<String> proxyAddresses = new HashSet<>(); private final Set<Tunnel> tunnels = new HashSet<>();
private final Set<Proxy> proxies = new HashSet<>();
private String token = ""; private String token = "";
private FlexPlexGraphQLApi flexPlexGraphQLApi; private FlexPlexGraphQLApi flexPlexGraphQLApi;
@ -27,8 +31,13 @@ public final class FlexConnect extends JavaPlugin {
return this.flexPlexGraphQLApi; return this.flexPlexGraphQLApi;
} }
public Set<String> getProxyAddresses() { public boolean isIpAllowedToJoin(final String ip) {
return this.proxyAddresses; for (final Proxy proxy : this.proxies) {
if (proxy.ipAllowedToJoin(ip)) {
return true;
}
}
return false;
} }
@Override @Override
@ -42,7 +51,11 @@ public final class FlexConnect extends JavaPlugin {
} }
// GraphQL Api // GraphQL Api
try {
this.flexPlexGraphQLApi = new FlexPlexGraphQLApi(this, "https://api.flexplex.fi/graphql"); this.flexPlexGraphQLApi = new FlexPlexGraphQLApi(this, "https://api.flexplex.fi/graphql");
} catch (final MalformedURLException e) {
e.printStackTrace();
}
boolean configsModified = false; boolean configsModified = false;
@ -95,12 +108,30 @@ public final class FlexConnect extends JavaPlugin {
// Update whitelists for flexplex api // Update whitelists for flexplex api
this.flexPlexGraphQLApi.updateWhitelist(); this.flexPlexGraphQLApi.updateWhitelist();
// Load real FlexPlex proxy addresses from FlexPlex API
this.proxyAddresses.addAll(this.flexPlexGraphQLApi.getProxyAddresses());
// Load allowed proxy addresses from config // Load allowed proxy addresses from config
this.proxyAddresses.addAll(this.getConfig().getStringList("allowedProxies")); for (final String host : this.getConfig().getStringList("allowedProxies")) {
this.proxies.add(new Proxy(host));
}
// Load tunnels and proxy addresses from FlexPlex API and start tunnels
// Load proxies from FlexPlex API
this.proxies.addAll(this.flexPlexGraphQLApi.getProxies());
// Open tunnels if there is any available
for (final Proxy proxy : this.proxies) {
if (proxy.hasTunnel()) {
final Tunnel tunnel = new Tunnel(this, proxy);
this.tunnels.add(tunnel);
tunnel.start();
}
}
}
@Override
public void onDisable() {
for (final Tunnel tunnel : this.tunnels) {
tunnel.stop();
}
} }
} }

View File

@ -20,13 +20,15 @@ public final class FlexConnectListener implements Listener {
@EventHandler @EventHandler
public void onPlayerLogin(final PlayerLoginEvent event) { public void onPlayerLogin(final PlayerLoginEvent event) {
if (flexConnect.getProxyAddresses().contains(event.getRealAddress().getHostAddress())) { final String ip = event.getRealAddress().getHostAddress();
if (this.flexConnect.isIpAllowedToJoin(ip)) {
// Connection is coming from allowed proxy // Connection is coming from allowed proxy
return; return;
} }
// Do not allow connections from other proxies // Do not allow connections from other proxies
event.disallow(PlayerLoginEvent.Result.KICK_OTHER, "Access denied"); event.disallow(PlayerLoginEvent.Result.KICK_OTHER, "Access denied");
flexConnect.getLogger().warning("Connecting blocked from " + ip);
} }
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)

View File

@ -4,6 +4,8 @@ import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashSet; import java.util.HashSet;
@ -14,23 +16,24 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import fi.flexplex.connect.util.Proxy;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
public final class FlexPlexGraphQLApi { public final class FlexPlexGraphQLApi {
private final String graphqlEndpoint; private final URL graphqlEndpoint;
private final FlexConnect flexConnect; private final FlexConnect flexConnect;
public FlexPlexGraphQLApi(final FlexConnect flexConnect, final String graphQLEndpoint) { public FlexPlexGraphQLApi(final FlexConnect flexConnect, final String graphQLEndpoint) throws MalformedURLException {
this.flexConnect = flexConnect; this.flexConnect = flexConnect;
this.graphqlEndpoint = graphQLEndpoint; this.graphqlEndpoint = URI.create(graphQLEndpoint).toURL();
} }
public Set<String> getProxyAddresses() { public Set<Proxy> getProxies() {
final Set<String> output = new HashSet<>(); final Set<Proxy> output = new HashSet<>();
try { try {
final URL url = new URL(graphqlEndpoint); final HttpURLConnection conn = (HttpURLConnection) graphqlEndpoint.openConnection();
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true); conn.setDoOutput(true);
conn.setDoInput(true); conn.setDoInput(true);
conn.setConnectTimeout(10000); conn.setConnectTimeout(10000);
@ -40,22 +43,26 @@ public final class FlexPlexGraphQLApi {
final DataOutputStream wr = new DataOutputStream(conn.getOutputStream()); final DataOutputStream wr = new DataOutputStream(conn.getOutputStream());
wr.writeBytes("{\"query\":\"query { proxyAddresses }\"}"); wr.writeBytes("{\"query\":\"query{proxies(token:\\\"" + this.flexConnect.getToken() + "\\\"){host,tunnelPort,forwardingPort}}\"}");
wr.flush(); wr.flush();
wr.close(); wr.close();
final JsonObject response = (JsonObject) new JsonParser().parse(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); final JsonObject response = (JsonObject) new JsonParser().parse(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
if (response.get("data").getAsJsonObject().has("proxyAddresses")) { if (response.get("data").getAsJsonObject().has("proxies")) {
final JsonArray array = response.get("data").getAsJsonObject().get("proxyAddresses").getAsJsonArray(); final JsonArray array = response.get("data").getAsJsonObject().get("proxies").getAsJsonArray();
for (int i = 0; i < array.size(); i++) { for (int i = 0; i < array.size(); i++) {
output.add(array.get(i).getAsString()); final JsonObject proxy = array.get(i).getAsJsonObject();
final Proxy p = new Proxy(proxy.get("host").getAsString());
p.setForwardingPort(proxy.get("forwardingPort").getAsInt());
p.setTunnelPort(proxy.get("tunnelPort").getAsInt());
output.add(p);
} }
} }
conn.disconnect(); conn.disconnect();
} catch (final IOException e) { } catch (final IOException e) {
this.flexConnect.getLogger().warning("Updating whitelist data failed, propably connection to FlexPlex API server is down."); this.flexConnect.getLogger().warning("Getting proxies failed, propably connection to FlexPlex API server is down.");
} }
return output; return output;
} }
@ -82,8 +89,7 @@ public final class FlexPlexGraphQLApi {
} }
try { try {
final URL url = new URL(graphqlEndpoint); final HttpURLConnection conn = (HttpURLConnection) graphqlEndpoint.openConnection();
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true); conn.setDoOutput(true);
conn.setDoInput(true); conn.setDoInput(true);
conn.setConnectTimeout(10000); conn.setConnectTimeout(10000);

View File

@ -0,0 +1,45 @@
package fi.flexplex.connect.util;
public final class Proxy {
private final String host;
private int tunnelPort = -1;
private int forwardingPort = -1;
public Proxy(final String host) {
this.host = host;
}
public String getHost() {
return this.host;
}
public int getTunnelPort() {
return this.tunnelPort;
}
public int getForwardingPort() {
return this.forwardingPort;
}
public void setTunnelPort(final int port) {
this.tunnelPort = port;
}
public void setForwardingPort(final int port) {
this.forwardingPort = port;
}
public boolean hasTunnel() {
return this.tunnelPort != -1 && this.forwardingPort != -1;
}
public boolean ipAllowedToJoin(final String ip) {
if (this.hasTunnel()) {
return "0:0:0:0:0:0:0:1%0".equals(ip);
} else {
return ip.equals(this.host);
}
}
}

View File

@ -0,0 +1,83 @@
package fi.flexplex.connect.util;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.future.ConnectFuture;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.client.session.forward.PortForwardingTracker;
import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.bukkit.Bukkit;
import fi.flexplex.connect.FlexConnect;
public final class Tunnel {
private final FlexConnect flexConnect;
private final Proxy proxy;
private final Thread thread;
private final SshClient client = SshClient.setUpDefaultClient();
private boolean isRunning = false;
public Tunnel(final FlexConnect flexConnect, final Proxy proxy) {
this.flexConnect = flexConnect;
this.proxy = proxy;
this.client.setForwardingFilter(new TunnelForwardingFilter());
this.thread = new Thread() {
@Override
public void run() {
while (isRunning) {
session();
flexConnect.getLogger().warning("Tunnel session ended. Trying to reconnect in 10 seconds");
try {
Thread.sleep(10_000);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
};
}
public void start() {
this.isRunning = true;
this.thread.start();
}
public void stop() {
this.isRunning = false;
this.client.stop();
}
private void session() {
this.flexConnect.getLogger().info("Tunnel connecting to " + this.proxy.getHost() + ":" + this.proxy.getTunnelPort());
try {
client.start();
final ConnectFuture cf = client.connect("FlexConnect", this.proxy.getHost(), this.proxy.getTunnelPort());
final ClientSession session = cf.verify().getSession();
session.addPasswordIdentity(flexConnect.getToken());
session.auth().verify(TimeUnit.SECONDS.toMillis(5));
final SshdSocketAddress remote = new SshdSocketAddress("127.0.0.1", this.proxy.getForwardingPort());
final SshdSocketAddress local = new SshdSocketAddress(Bukkit.getPort());
try (PortForwardingTracker tracker = session.createRemotePortForwardingTracker(remote, local)) {
this.flexConnect.getLogger().info("Tunnel successfully connected " + this.proxy.getHost() + ":" + this.proxy.getTunnelPort());
while (isRunning) {
Thread.sleep(5000);
if (session.isClosed()) {
break;
}
}
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
} catch (final IOException e) {
this.flexConnect.getLogger().warning(e.getMessage());
}
}
}

View File

@ -0,0 +1,29 @@
package fi.flexplex.connect.util;
import org.apache.sshd.common.session.Session;
import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.apache.sshd.server.forward.ForwardingFilter;
public final class TunnelForwardingFilter implements ForwardingFilter {
@Override
public boolean canForwardAgent(final Session session, final String requestType) {
return false;
}
@Override
public boolean canForwardX11(final Session session, final String requestType) {
return false;
}
@Override
public boolean canListen(final SshdSocketAddress address, final Session session) {
return false;
}
@Override
public boolean canConnect(final Type type, final SshdSocketAddress address, final Session session) {
return true;
}
}