diff --git a/README.md b/README.md index b1a12e4..7bc2660 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,95 @@ # DirtSimpleP2P -DirtSimpleP2P lets a public Bungee/Waterfall proxy reach a Paper/Spigot server that is running at home behind NAT. +DirtSimpleP2P is a Minecraft networking plugin that lets a public BungeeCord or Waterfall proxy reach backend Paper/Spigot servers that are behind NAT. -You do not need to open the Minecraft port on your home router. The backend server connects outbound to your public proxy/VPS, then Minecraft traffic is carried through that tunnel. +It is designed for server owners who want to host a backend Minecraft server at home or on a private network without opening the backend Minecraft port on their router. -## What You Get +The backend server connects outbound to the public proxy. Bungee/Waterfall then connects to a local port on the proxy machine, and DirtSimpleP2P carries the Minecraft TCP connection through the tunnel. -- One jar file for both servers -- No home router port forwarding -- Works with a public Bungee/Waterfall proxy -- Works with a backend Paper/Spigot server +## Features + +- One jar for both the proxy and backend server +- No home router port forwarding for backend Minecraft servers +- NAT-safe outbound tunnel from backend to proxy +- Multiple simultaneous Minecraft player connections +- Multiplexed framed TCP protocol - Secret-token authentication -- Automatic secret-token generation -- Optional TLS encryption with generated self-signed certificates -- Multiple Minecraft player connections over one tunnel -- Fast reconnect with heartbeat-based latency tolerance +- Automatic token generation +- Optional TLS encryption +- Self-signed TLS certificate generation +- TLS certificate pinning on backend servers +- Fast reconnect with heartbeat checks +- Multiple public proxies can connect to one backend +- One proxy can expose multiple named backend servers - `/dsp2p status` and `/dsp2p doctor` commands -- Simple properties config files +- Simple `config.properties` setup -## Current Status +## Requirements -This is still a development build, not a paid release build yet. +- Java 21 +- Maven 3.8+ +- BungeeCord or Waterfall for the public proxy side +- Paper or Spigot for the backend server side -The tunnel core exists and the plugin jar builds. TLS can now generate a self-signed certificate when enabled, but before paid release it should get more real-world testing on actual Bungee and Paper servers. +## How It Works -## Build The Jar +Normal Minecraft proxy setup expects Bungee/Waterfall to connect directly to a backend server. -From this project folder, run: +That does not work well when the backend server is at home behind NAT, unless you forward ports on the home router. + +DirtSimpleP2P avoids that: + +1. The proxy server runs DirtSimpleP2P in `frontend` mode. +2. The backend server runs DirtSimpleP2P in `backend` mode. +3. The backend opens an outbound tunnel to the public proxy. +4. The proxy exposes a local port such as `127.0.0.1:25566`. +5. Bungee/Waterfall sends players to that local port. +6. DirtSimpleP2P carries the TCP traffic through the tunnel to the backend server. + +The public proxy needs an open tunnel port. The home/backend router does not need a Minecraft port forward. + +## Build + +From the project folder: ```bash mvn clean package ``` -The plugin jar will be here: +The plugin jar is created at: ```bash plugin/target/DirtSimpleP2P-0.1.0-SNAPSHOT.jar ``` -Use this same jar on both servers. +Use this same jar on both the proxy and backend server. ## Basic Setup -You need two Minecraft-side servers: +You need: -- Public proxy server: BungeeCord, Waterfall, or another Bungee-compatible proxy -- Backend server: Paper or Spigot +- A public BungeeCord/Waterfall proxy, usually on a VPS +- A backend Paper/Spigot server, which can be at home or on a private network -The public proxy is usually on a VPS. The backend server can be at home. +### 1. Install On The Proxy -## Step 1: Install On Bungee +Put the jar in your BungeeCord/Waterfall `plugins` folder: -Put this jar in your Bungee/Waterfall `plugins` folder: - -```bash +```text DirtSimpleP2P-0.1.0-SNAPSHOT.jar ``` Start the proxy once, then stop it. -DirtSimpleP2P will create: +DirtSimpleP2P creates: -```bash +```text plugins/DirtSimpleP2P/config.properties ``` -Open that file. +### 2. Configure The Proxy -## Step 2: Configure Bungee - -On the Bungee/proxy server, use: +Use this on the BungeeCord/Waterfall side: ```properties role=frontend @@ -96,70 +116,57 @@ tunnel.tls.requireClientAuth=false routes=minecraft route.minecraft.frontendBindHost=127.0.0.1 route.minecraft.frontendBindPort=25566 +route.minecraft.backendNode=paper-backend ``` -Change: +Notes: -- `tunnel.listenPort` only if port `24445` is already used +- `tunnel.listenPort` is the public tunnel port that the backend connects to. +- `tunnel.authToken` is generated automatically on first start. +- Copy the generated token to the backend config. +- `route.minecraft.frontendBindPort` is the local port Bungee/Waterfall should use for this backend. +- `route.minecraft.backendNode` must match the backend server's `node.name`. -The plugin automatically generates `tunnel.authToken` on first start. +### 3. Point Bungee/Waterfall At The Local Tunnel -Copy the generated Bungee token into the backend server config. The token must match on both sides. - -To enable TLS on Bungee, change: - -```properties -tunnel.tls.enabled=true -tunnel.tls.allowInsecure=false -``` - -When Bungee starts, DirtSimpleP2P will generate: - -```text -plugins/DirtSimpleP2P/certs/frontend.p12 -``` - -It will also fill in `tunnel.tls.keyStorePassword` if that value is blank. - -## Step 3: Point Bungee At The Local Tunnel - -In your Bungee server config, set the backend server address to: +In your BungeeCord/Waterfall server config, set the backend address to: ```text 127.0.0.1:25566 ``` -That is not your home server IP. DirtSimpleP2P listens there locally on the proxy server. +Do not put your home server IP in the Bungee/Waterfall config. Bungee connects locally, and DirtSimpleP2P carries the traffic through the tunnel. -## Step 4: Install On Paper Or Spigot +### 4. Install On The Backend -Put the same jar in your backend Minecraft server `plugins` folder: +Put the same jar in your Paper/Spigot `plugins` folder: -```bash +```text DirtSimpleP2P-0.1.0-SNAPSHOT.jar ``` Start the backend server once, then stop it. -DirtSimpleP2P will create: +DirtSimpleP2P creates: -```bash +```text plugins/DirtSimpleP2P/config.properties ``` -Open that file. +### 5. Configure The Backend -## Step 5: Configure Paper Or Spigot - -On the backend server, use: +Use this on the Paper/Spigot side: ```properties role=backend node.name=paper-backend -tunnel.connectHost=YOUR_BUNGEE_OR_VPS_IP -tunnel.connectPort=24445 -tunnel.authToken=PASTE_THE_BUNGEE_TOKEN_HERE +frontends=proxy1 +frontend.proxy1.connectHost=YOUR_PROXY_IP_OR_DOMAIN +frontend.proxy1.connectPort=24445 +frontend.proxy1.tls.pinnedCertificateSha256= + +tunnel.authToken=PASTE_THE_PROXY_TOKEN_HERE tunnel.connectTimeoutMillis=5000 tunnel.heartbeatIntervalMillis=2000 @@ -171,7 +178,6 @@ tunnel.reconnectMaxMillis=10000 tunnel.tls.enabled=false tunnel.tls.allowInsecure=true tunnel.tls.trustOnFirstUse=true -tunnel.tls.pinnedCertificateSha256= routes=minecraft route.minecraft.backendTargetHost=127.0.0.1 @@ -180,58 +186,167 @@ route.minecraft.backendTargetPort=25565 Change: -- `tunnel.connectHost` to your public proxy/VPS IP or domain name -- `tunnel.authToken` to the same secret used on Bungee -- `route.minecraft.backendTargetPort` if your backend Minecraft server is not on `25565` +- `frontend.proxy1.connectHost` to your public proxy IP or domain +- `frontend.proxy1.connectPort` to the proxy `tunnel.listenPort` +- `tunnel.authToken` to the token from the proxy config +- `route.minecraft.backendTargetPort` if your Paper/Spigot server is not listening on `25565` -Do not put your home IP in the Bungee config. The backend connects out to the proxy. +### 6. Start Everything -To enable TLS on Paper/Spigot, change: +Start the public proxy first. + +Then start the backend Paper/Spigot server. + +If the tunnel connects, the proxy log should show that a backend authenticated. Players connect to your normal public proxy address. + +## TLS + +TLS is optional, but recommended for real deployments. + +On the proxy: ```properties tunnel.tls.enabled=true tunnel.tls.allowInsecure=false +tunnel.tls.autoGenerate=true ``` -Leave this enabled for the easiest setup: +When TLS is enabled, the proxy can generate: + +```text +plugins/DirtSimpleP2P/certs/frontend.p12 +``` + +On the backend: ```properties +tunnel.tls.enabled=true +tunnel.tls.allowInsecure=false tunnel.tls.trustOnFirstUse=true ``` -On the first successful TLS connection, the backend saves the Bungee certificate fingerprint here: +On the first successful TLS connection, the backend saves the proxy certificate fingerprint: ```properties -tunnel.tls.pinnedCertificateSha256= +frontend.proxy1.tls.pinnedCertificateSha256= ``` -After that, the backend will only trust that same Bungee certificate. +After that, the backend only trusts that same certificate. -## Step 6: Start Everything +For stricter security, copy the certificate fingerprint from the proxy log and paste it into the backend config before the first connection. -Start the public Bungee/Waterfall proxy first. +## Multi-Server Setups -Then start the backend Paper/Spigot server. +### Two Public Proxies, One Backend -If it works, the Bungee logs should show that a backend tunnel authenticated. +Use this when two public BungeeCord/Waterfall proxies should both reach the same backend server. -Players should connect to your normal public proxy address. Bungee sends them to `127.0.0.1:25566`, and DirtSimpleP2P carries the connection to the backend server. +Backend config: + +```properties +role=backend +node.name=survival-backend + +frontends=proxy1,proxy2 +frontend.proxy1.connectHost=proxy1.example.com +frontend.proxy1.connectPort=24445 +frontend.proxy1.tls.pinnedCertificateSha256= +frontend.proxy2.connectHost=proxy2.example.com +frontend.proxy2.connectPort=24445 +frontend.proxy2.tls.pinnedCertificateSha256= + +routes=minecraft +route.minecraft.backendTargetHost=127.0.0.1 +route.minecraft.backendTargetPort=25565 +``` + +Each proxy config: + +```properties +role=frontend +node.name=proxy1 + +routes=minecraft +route.minecraft.frontendBindHost=127.0.0.1 +route.minecraft.frontendBindPort=25566 +route.minecraft.backendNode=survival-backend +``` + +Each proxy listens for tunnel connections. The backend connects outbound to both proxies. + +### One Proxy, Multiple Backends + +Use this when one proxy should expose more than one backend server. + +Proxy config: + +```properties +role=frontend +node.name=main-proxy + +routes=survival,lobby +route.survival.frontendBindHost=127.0.0.1 +route.survival.frontendBindPort=25566 +route.survival.backendNode=survival-backend +route.lobby.frontendBindHost=127.0.0.1 +route.lobby.frontendBindPort=25567 +route.lobby.backendNode=lobby-backend +``` + +Bungee/Waterfall server entries: + +```text +survival -> 127.0.0.1:25566 +lobby -> 127.0.0.1:25567 +``` + +Survival backend config: + +```properties +role=backend +node.name=survival-backend + +frontends=main +frontend.main.connectHost=YOUR_PROXY_IP +frontend.main.connectPort=24445 + +routes=survival +route.survival.backendTargetHost=127.0.0.1 +route.survival.backendTargetPort=25565 +``` + +Lobby backend config: + +```properties +role=backend +node.name=lobby-backend + +frontends=main +frontend.main.connectHost=YOUR_PROXY_IP +frontend.main.connectPort=24445 + +routes=lobby +route.lobby.backendTargetHost=127.0.0.1 +route.lobby.backendTargetPort=25565 +``` + +All nodes in the same private tunnel group must use the same `tunnel.authToken`. ## Commands -Run these commands in game or from the server console: +Run from the server console or in game: ```text /dsp2p status ``` -Shows whether the agent is running, whether the tunnel is connected, active streams, TLS state, and the last important event. +Shows role, node name, whether the tunnel is connected, connected tunnel count, active streams, TLS state, and the latest event. ```text /dsp2p doctor ``` -Checks the config and prints useful setup/security hints. +Checks the config and prints setup/security hints. Permission: @@ -239,35 +354,35 @@ Permission: dirtsimplep2p.command ``` -On Paper/Spigot it defaults to server operators. On Bungee, give that permission to staff who should see tunnel diagnostics. +On Paper/Spigot, the permission defaults to server operators. On BungeeCord/Waterfall, give this permission to staff who should see tunnel diagnostics. ## Firewall Notes -On the public proxy/VPS, allow: +On the public proxy/VPS, allow the tunnel port: ```text 24445/tcp ``` -On the home router, you do not need to forward: +On the home router, you do not need to forward the backend Minecraft port: ```text 25565/tcp ``` -Your backend server must be allowed to make outbound connections to the proxy/VPS. +The backend server must be allowed to make outbound TCP connections to the proxy. ## Reconnect Behavior -DirtSimpleP2P is tuned to notice real drops quickly while still tolerating short latency spikes. +DirtSimpleP2P is tuned to detect real drops quickly while still tolerating short latency spikes. Default behavior: - Heartbeat every `2` seconds -- Warns in status if heartbeats are delayed -- Disconnects after about `8` seconds of no tunnel traffic +- Status warning if heartbeats are delayed +- Disconnect after about `8` seconds without tunnel traffic - First reconnect retry after about `250ms` -- Backoff grows up to `10` seconds if the proxy/VPS is still unreachable +- Exponential backoff up to `10` seconds while the proxy is unreachable Useful config values: @@ -279,39 +394,43 @@ tunnel.reconnectInitialMillis=250 tunnel.reconnectMaxMillis=10000 ``` -If your users are very far from the VPS or your home internet has frequent latency spikes, increase `tunnel.heartbeatMissesBeforeDisconnect` to `5` or `6`. +If your network has frequent latency spikes, increase: -## Common Problems +```properties +tunnel.heartbeatMissesBeforeDisconnect=5 +``` -### Plugin says the token is still the default +## Troubleshooting -Restart once on each side so DirtSimpleP2P can generate a token. +### Token Problems -Then copy the generated Bungee token into the backend config. The token must be the same on both sides. +The token must match on both sides. -### Backend says connection refused +If the plugin generated a token on the proxy, copy that generated value into the backend config. + +### Backend Says Connection Refused Check that: -- Bungee/proxy server is running -- DirtSimpleP2P started on Bungee -- `tunnel.connectHost` points to the proxy/VPS -- `tunnel.connectPort` matches the Bungee `tunnel.listenPort` -- The VPS firewall allows that TCP port +- The proxy server is running +- DirtSimpleP2P started on the proxy +- `frontend.proxy1.connectHost` points to the proxy IP or domain +- `frontend.proxy1.connectPort` matches the proxy `tunnel.listenPort` +- The proxy firewall allows the tunnel port -### Bungee cannot connect to backend server +### Bungee Cannot Reach The Backend -Make sure your Bungee backend server entry points to: +Make sure the Bungee/Waterfall backend server entry points to the local tunnel port: ```text 127.0.0.1:25566 ``` -Also make sure the backend Paper/Spigot server is running and connected to the tunnel. +Also check that the backend Paper/Spigot server is running and the tunnel is connected. -### Players disconnect or freeze +### Players Disconnect Or Freeze -Check both server logs. Look for: +Check both server logs for: - reconnect messages - heartbeat timeout messages @@ -320,23 +439,20 @@ Check both server logs. Look for: ## Security Notes -The secret token is important. Do not share it. +Keep `tunnel.authToken` private. Anyone with the token can attempt to authenticate to the tunnel. -Local-test configs use: +For local testing, insecure mode is available: ```properties tunnel.tls.enabled=false tunnel.tls.allowInsecure=true ``` -For a better setup, enable TLS on both sides: +For real deployments, enable TLS: ```properties tunnel.tls.enabled=true tunnel.tls.allowInsecure=false ``` -The Bungee side generates a self-signed certificate. The Paper/Spigot side pins that certificate after the first successful connection. - -Trust-on-first-use is convenient, but the very first TLS connection is still the sensitive moment. For a stricter setup, copy the fingerprint from the Bungee log and paste it into the backend config before the first connection. - +Trust-on-first-use is convenient, but the first TLS connection is the sensitive moment. For the strictest setup, manually copy the proxy certificate fingerprint into the backend config before the first connection. diff --git a/common/src/main/java/me/proxylink/common/AgentConfig.java b/common/src/main/java/me/proxylink/common/AgentConfig.java index fec41e8..761a3d9 100644 --- a/common/src/main/java/me/proxylink/common/AgentConfig.java +++ b/common/src/main/java/me/proxylink/common/AgentConfig.java @@ -2,9 +2,11 @@ package me.proxylink.common; import java.nio.file.Path; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.Set; import java.util.regex.Pattern; public record AgentConfig( @@ -12,6 +14,7 @@ public record AgentConfig( String nodeName, TunnelConfig tunnel, TlsConfig tls, + List frontends, List routes ) { private static final Pattern ROUTE_NAME = Pattern.compile("[A-Za-z0-9._-]+"); @@ -21,6 +24,7 @@ public record AgentConfig( nodeName = requireNonBlank(nodeName, "nodeName"); Objects.requireNonNull(tunnel, "tunnel"); Objects.requireNonNull(tls, "tls"); + frontends = List.copyOf(Objects.requireNonNull(frontends, "frontends")); routes = List.copyOf(Objects.requireNonNull(routes, "routes")); } @@ -51,10 +55,27 @@ public record AgentConfig( } if (role == Role.BACKEND) { - validateHostPort(errors, "tunnel.connect", tunnel.connectHost(), tunnel.connectPort()); + if (frontends.isEmpty()) { + errors.add("At least one frontend endpoint is required for backend mode"); + } + Set frontendNames = new HashSet<>(); + for (FrontendEndpointConfig frontend : frontends) { + if (!ROUTE_NAME.matcher(frontend.name()).matches()) { + errors.add("Frontend endpoint name contains invalid characters: " + frontend.name()); + } + if (!frontendNames.add(frontend.name())) { + errors.add("Duplicate frontend endpoint name: " + frontend.name()); + } + validateHostPort(errors, "frontend." + frontend.name() + ".connect", frontend.connectHost(), frontend.connectPort()); + } if (tls.enabled()) { - if (tls.trustStorePath() == null && tls.pinnedCertificateSha256().isBlank() && !tls.trustOnFirstUse()) { - errors.add("TLS is enabled on backend, so set tunnel.tls.pinnedCertificateSha256, set tunnel.tls.trustOnFirstUse=true, or configure a truststore"); + if (tls.trustStorePath() == null && !tls.trustOnFirstUse()) { + for (FrontendEndpointConfig frontend : frontends) { + if (frontend.pinnedCertificateSha256().isBlank() && tls.pinnedCertificateSha256().isBlank()) { + errors.add("TLS is enabled on backend, so set frontend." + frontend.name() + + ".tls.pinnedCertificateSha256, set tunnel.tls.trustOnFirstUse=true, or configure a truststore"); + } + } } if (tls.trustStorePath() != null) { requireSecret(errors, "tunnel.tls.trustStorePassword", tls.trustStorePassword()); @@ -85,12 +106,24 @@ public record AgentConfig( errors.add("tunnel.reconnectMaxMillis must be greater than or equal to tunnel.reconnectInitialMillis"); } + Set routeNames = new HashSet<>(); + Set frontendBinds = new HashSet<>(); for (RouteConfig route : routes) { if (!ROUTE_NAME.matcher(route.name()).matches()) { errors.add("Route name contains invalid characters: " + route.name()); } + if (!routeNames.add(route.name())) { + errors.add("Duplicate route name: " + route.name()); + } if (role == Role.FRONTEND) { validateHostPort(errors, "route." + route.name() + ".frontendBind", route.frontendBindHost(), route.frontendBindPort()); + String bindKey = route.frontendBindHost() + ":" + route.frontendBindPort(); + if (!frontendBinds.add(bindKey)) { + errors.add("Duplicate frontend bind address: " + bindKey); + } + if (!route.backendNode().isBlank() && !ROUTE_NAME.matcher(route.backendNode()).matches()) { + errors.add("Route " + route.name() + " has an invalid backendNode: " + route.backendNode()); + } } if (role == Role.BACKEND) { validateHostPort(errors, "route." + route.name() + ".backendTarget", route.backendTargetHost(), route.backendTargetPort()); @@ -192,15 +225,30 @@ public record AgentConfig( } } + public record FrontendEndpointConfig( + String name, + String connectHost, + int connectPort, + String pinnedCertificateSha256 + ) { + public FrontendEndpointConfig { + name = requireNonBlank(name, "name"); + connectHost = requireNonBlank(connectHost, "connectHost"); + pinnedCertificateSha256 = pinnedCertificateSha256 == null ? "" : pinnedCertificateSha256.trim(); + } + } + public record RouteConfig( String name, String frontendBindHost, int frontendBindPort, + String backendNode, String backendTargetHost, int backendTargetPort ) { public RouteConfig { name = requireNonBlank(name, "name"); + backendNode = backendNode == null ? "" : backendNode.trim(); } } } diff --git a/common/src/main/java/me/proxylink/common/AgentConfigIO.java b/common/src/main/java/me/proxylink/common/AgentConfigIO.java index e035d27..7eea133 100644 --- a/common/src/main/java/me/proxylink/common/AgentConfigIO.java +++ b/common/src/main/java/me/proxylink/common/AgentConfigIO.java @@ -56,7 +56,43 @@ public final class AgentConfigIO { value(properties, "tunnel.tls.pinnedCertificateSha256", "") ); - return new AgentConfig(role, nodeName, tunnel, tls, routes(properties)); + return new AgentConfig(role, nodeName, tunnel, tls, frontends(properties, tunnel, tls), routes(properties)); + } + + private static List frontends( + Properties properties, + AgentConfig.TunnelConfig tunnel, + AgentConfig.TlsConfig tls + ) { + String frontendsValue = value(properties, "frontends", ""); + List frontends = new ArrayList<>(); + + if (frontendsValue.isBlank()) { + if (!tunnel.connectHost().isBlank() && tunnel.connectPort() > 0) { + frontends.add(new AgentConfig.FrontendEndpointConfig( + "default", + tunnel.connectHost(), + tunnel.connectPort(), + tls.pinnedCertificateSha256() + )); + } + return frontends; + } + + for (String rawName : frontendsValue.split(",")) { + String name = rawName.trim(); + if (name.isEmpty()) { + continue; + } + String prefix = "frontend." + name + "."; + frontends.add(new AgentConfig.FrontendEndpointConfig( + name, + required(properties, prefix + "connectHost"), + intValue(properties, prefix + "connectPort", -1), + value(properties, prefix + "tls.pinnedCertificateSha256", "") + )); + } + return frontends; } private static List routes(Properties properties) { @@ -72,6 +108,7 @@ public final class AgentConfigIO { name, value(properties, prefix + "frontendBindHost", "127.0.0.1"), intValue(properties, prefix + "frontendBindPort", -1), + value(properties, prefix + "backendNode", ""), value(properties, prefix + "backendTargetHost", "127.0.0.1"), intValue(properties, prefix + "backendTargetPort", -1) )); diff --git a/common/target/classes/me/proxylink/common/AgentConfig$FrontendEndpointConfig.class b/common/target/classes/me/proxylink/common/AgentConfig$FrontendEndpointConfig.class new file mode 100644 index 0000000..fcde23d Binary files /dev/null and b/common/target/classes/me/proxylink/common/AgentConfig$FrontendEndpointConfig.class differ diff --git a/common/target/classes/me/proxylink/common/AgentConfig$Role.class b/common/target/classes/me/proxylink/common/AgentConfig$Role.class index fb2c49b..4d57f7a 100644 Binary files a/common/target/classes/me/proxylink/common/AgentConfig$Role.class and b/common/target/classes/me/proxylink/common/AgentConfig$Role.class differ diff --git a/common/target/classes/me/proxylink/common/AgentConfig$RouteConfig.class b/common/target/classes/me/proxylink/common/AgentConfig$RouteConfig.class index 2227cd4..14701e9 100644 Binary files a/common/target/classes/me/proxylink/common/AgentConfig$RouteConfig.class and b/common/target/classes/me/proxylink/common/AgentConfig$RouteConfig.class differ diff --git a/common/target/classes/me/proxylink/common/AgentConfig$TlsConfig.class b/common/target/classes/me/proxylink/common/AgentConfig$TlsConfig.class index 328828c..a11d15e 100644 Binary files a/common/target/classes/me/proxylink/common/AgentConfig$TlsConfig.class and b/common/target/classes/me/proxylink/common/AgentConfig$TlsConfig.class differ diff --git a/common/target/classes/me/proxylink/common/AgentConfig$TunnelConfig.class b/common/target/classes/me/proxylink/common/AgentConfig$TunnelConfig.class index 1b876f6..a7d3af7 100644 Binary files a/common/target/classes/me/proxylink/common/AgentConfig$TunnelConfig.class and b/common/target/classes/me/proxylink/common/AgentConfig$TunnelConfig.class differ diff --git a/common/target/classes/me/proxylink/common/AgentConfig.class b/common/target/classes/me/proxylink/common/AgentConfig.class index 7119968..f5a77e3 100644 Binary files a/common/target/classes/me/proxylink/common/AgentConfig.class and b/common/target/classes/me/proxylink/common/AgentConfig.class differ diff --git a/common/target/classes/me/proxylink/common/AgentConfigIO.class b/common/target/classes/me/proxylink/common/AgentConfigIO.class index 7f8ee76..34c1f18 100644 Binary files a/common/target/classes/me/proxylink/common/AgentConfigIO.class and b/common/target/classes/me/proxylink/common/AgentConfigIO.class differ diff --git a/common/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/common/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst index 30f6586..5d712fa 100644 --- a/common/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ b/common/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -3,6 +3,7 @@ me/proxylink/common/AgentConfig.class me/proxylink/common/AgentConfigIO.class me/proxylink/common/AuthPayloads$Response.class me/proxylink/common/FrameType.class +me/proxylink/common/AgentConfig$FrontendEndpointConfig.class me/proxylink/common/AuthPayloads$Challenge.class me/proxylink/common/ProtocolConstants.class me/proxylink/common/FrameCodec.class diff --git a/common/target/proxylink-common-0.1.0-SNAPSHOT.jar b/common/target/proxylink-common-0.1.0-SNAPSHOT.jar index 037db0d..3ec3fa9 100644 Binary files a/common/target/proxylink-common-0.1.0-SNAPSHOT.jar and b/common/target/proxylink-common-0.1.0-SNAPSHOT.jar differ diff --git a/plugin/src/main/java/me/proxylink/plugin/PluginRuntime.java b/plugin/src/main/java/me/proxylink/plugin/PluginRuntime.java index 94f8cf5..c7148c4 100644 --- a/plugin/src/main/java/me/proxylink/plugin/PluginRuntime.java +++ b/plugin/src/main/java/me/proxylink/plugin/PluginRuntime.java @@ -74,6 +74,7 @@ public final class PluginRuntime { lines.add("Node: " + status.nodeName()); lines.add("Agent: " + (status.running() ? "running" : "stopped")); lines.add("Tunnel: " + (status.tunnelConnected() ? "connected" : "not connected")); + lines.add("Connected tunnels: " + status.connectedTunnels()); lines.add("Active streams: " + status.activeStreams()); lines.add("TLS: " + (status.tlsEnabled() ? "enabled" : "disabled")); lines.add("Last event: " + status.lastEvent()); @@ -99,6 +100,10 @@ public final class PluginRuntime { lines.add("Config: valid"); lines.add("Role: " + lower(loaded.role())); lines.add("TLS: " + (loaded.tls().enabled() ? "enabled" : "disabled")); + lines.add("Routes: " + loaded.routes().size()); + if (loaded.role() == AgentConfig.Role.BACKEND) { + lines.add("Frontend endpoints: " + loaded.frontends().size()); + } if (!loaded.tls().enabled()) { lines.add("Security: TLS is disabled. This is okay for local testing, not ideal for paid production use."); @@ -136,14 +141,19 @@ public final class PluginRuntime { return new BackendAgent(config, this::saveLearnedTlsFingerprint); } - private void saveLearnedTlsFingerprint(String fingerprint) { + private void saveLearnedTlsFingerprint(String endpointName, String fingerprint) { if (configPath == null || fingerprint == null || fingerprint.isBlank()) { return; } try { - ConfigFileEditor.setProperty(configPath, "tunnel.tls.pinnedCertificateSha256", fingerprint); + ConfigFileEditor.setProperty(configPath, + "frontend." + endpointName + ".tls.pinnedCertificateSha256", + fingerprint); + if (config != null && config.frontends().size() == 1) { + ConfigFileEditor.setProperty(configPath, "tunnel.tls.pinnedCertificateSha256", fingerprint); + } config = AgentConfigIO.load(configPath); - logger.info("Saved frontend TLS certificate pin to " + configPath); + logger.info("Saved frontend TLS certificate pin for " + endpointName + " to " + configPath); } catch (Exception e) { logger.log(Level.WARNING, "Unable to save frontend TLS certificate pin", e); } diff --git a/plugin/src/main/java/me/proxylink/plugin/tunnel/BackendAgent.java b/plugin/src/main/java/me/proxylink/plugin/tunnel/BackendAgent.java index d18b8c2..2e323b3 100644 --- a/plugin/src/main/java/me/proxylink/plugin/tunnel/BackendAgent.java +++ b/plugin/src/main/java/me/proxylink/plugin/tunnel/BackendAgent.java @@ -26,7 +26,7 @@ import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; +import java.util.function.BiConsumer; import java.util.logging.Level; import java.util.logging.Logger; @@ -34,21 +34,21 @@ public final class BackendAgent implements ManagedAgent { private static final Logger LOGGER = Logger.getLogger(BackendAgent.class.getName()); private final AgentConfig config; - private final Consumer learnedTlsFingerprintConsumer; + private final BiConsumer learnedTlsFingerprintConsumer; private final ExecutorService executor = Executors.newThreadPerTaskExecutor( new NamedThreadFactory("proxylink-backend", true) ); private final AtomicBoolean running = new AtomicBoolean(false); - private final AtomicReference learnedTlsFingerprint = new AtomicReference<>(""); + private final Map learnedTlsFingerprints = new ConcurrentHashMap<>(); + private final Map activeTunnels = new ConcurrentHashMap<>(); private final AtomicReference lastEvent = new AtomicReference<>("not started"); - private volatile BackendTunnel activeTunnel; public BackendAgent(AgentConfig config) { - this(config, ignored -> { + this(config, (ignoredEndpoint, ignoredFingerprint) -> { }); } - public BackendAgent(AgentConfig config, Consumer learnedTlsFingerprintConsumer) { + public BackendAgent(AgentConfig config, BiConsumer learnedTlsFingerprintConsumer) { this.config = config; this.learnedTlsFingerprintConsumer = learnedTlsFingerprintConsumer; } @@ -58,22 +58,30 @@ public final class BackendAgent implements ManagedAgent { if (!running.compareAndSet(false, true)) { return; } - executor.execute(this::runReconnectLoop); - lastEvent.set("connecting to frontend " + config.tunnel().connectHost() + ":" + config.tunnel().connectPort()); - LOGGER.info(() -> "Backend agent connecting to frontend " - + config.tunnel().connectHost() + ":" + config.tunnel().connectPort()); + for (AgentConfig.FrontendEndpointConfig frontend : config.frontends()) { + executor.execute(() -> runReconnectLoop(frontend)); + } + lastEvent.set("connecting to " + config.frontends().size() + " frontend endpoint(s)"); + LOGGER.info(() -> "Backend agent connecting to " + config.frontends().size() + " frontend endpoint(s)"); } @Override public TunnelStatus status() { - BackendTunnel tunnel = activeTunnel; - boolean connected = running.get() && tunnel != null && tunnel.isOpen(); - int activeStreams = connected ? tunnel.streamCount() : 0; + int connectedTunnels = 0; + int activeStreams = 0; + for (BackendTunnel tunnel : activeTunnels.values()) { + if (tunnel.isOpen()) { + connectedTunnels++; + activeStreams += tunnel.streamCount(); + } + } + boolean connected = running.get() && connectedTunnels > 0; return new TunnelStatus( config.role(), config.nodeName(), running.get(), connected, + connectedTunnels, activeStreams, config.tls().enabled(), lastEvent.get() @@ -85,76 +93,94 @@ public final class BackendAgent implements ManagedAgent { if (!running.compareAndSet(true, false)) { return; } - BackendTunnel tunnel = activeTunnel; - if (tunnel != null) { + for (BackendTunnel tunnel : activeTunnels.values()) { tunnel.close("backend shutting down"); } + activeTunnels.clear(); executor.shutdownNow(); lastEvent.set("backend stopped"); LOGGER.info("Backend agent stopped"); } - private void runReconnectLoop() { + private void runReconnectLoop(AgentConfig.FrontendEndpointConfig frontend) { long delayMillis = config.tunnel().reconnectInitialMillis(); while (running.get()) { try { - runSingleConnection(); + runSingleConnection(frontend); delayMillis = config.tunnel().reconnectInitialMillis(); } catch (Exception e) { if (running.get()) { - lastEvent.set("connection failed: " + e.getMessage()); - LOGGER.log(Level.WARNING, "Backend tunnel connection failed", e); + lastEvent.set("connection to " + frontend.name() + " failed: " + e.getMessage()); + LOGGER.log(Level.WARNING, "Backend tunnel connection to " + frontend.name() + " failed", e); } } if (running.get()) { - sleepWithBackoff(delayMillis); + sleepWithBackoff(frontend, delayMillis); delayMillis = Math.min(delayMillis * 2, config.tunnel().reconnectMaxMillis()); } } } - private void runSingleConnection() throws IOException, GeneralSecurityException { - Socket socket = TlsSocketFactory.createClientSocket(connectionConfig(), this::learnTlsFingerprint); - FrameCodec codec = new FrameCodec(socket.getInputStream(), socket.getOutputStream()); - authenticateToFrontend(codec); + private void runSingleConnection(AgentConfig.FrontendEndpointConfig frontend) + throws IOException, GeneralSecurityException { + Socket socket = null; + boolean handedToTunnel = false; + try { + AgentConfig.FrontendEndpointConfig pinnedFrontend = frontendWithLearnedPin(frontend); + socket = TlsSocketFactory.createClientSocket( + config, + pinnedFrontend, + fingerprint -> learnTlsFingerprint(frontend, fingerprint) + ); + FrameCodec codec = new FrameCodec(socket.getInputStream(), socket.getOutputStream()); + authenticateToFrontend(codec); - BackendTunnel tunnel = new BackendTunnel(socket, codec); - activeTunnel = tunnel; - lastEvent.set("authenticated to frontend"); - LOGGER.info(() -> "Backend tunnel authenticated to " - + config.tunnel().connectHost() + ":" + config.tunnel().connectPort()); - tunnel.runReadLoop(); + BackendTunnel tunnel = new BackendTunnel(frontend, socket, codec); + BackendTunnel previous = activeTunnels.put(frontend.name(), tunnel); + if (previous != null) { + previous.close("replaced by a new authenticated tunnel to " + frontend.name()); + } + + handedToTunnel = true; + lastEvent.set("authenticated to frontend " + frontend.name()); + LOGGER.info(() -> "Backend tunnel authenticated to " + frontend.name() + + " at " + frontend.connectHost() + ":" + frontend.connectPort()); + tunnel.runReadLoop(); + } finally { + if (!handedToTunnel && socket != null) { + try { + socket.close(); + } catch (IOException ignored) { + } + } + } } - private AgentConfig connectionConfig() { - String pin = learnedTlsFingerprint.get(); - if (pin.isBlank()) { - return config; + private AgentConfig.FrontendEndpointConfig frontendWithLearnedPin(AgentConfig.FrontendEndpointConfig frontend) { + if (!frontend.pinnedCertificateSha256().isBlank()) { + return frontend; } - AgentConfig.TlsConfig tls = config.tls(); - AgentConfig.TlsConfig pinnedTls = new AgentConfig.TlsConfig( - tls.enabled(), - tls.keyStorePath(), - tls.keyStorePassword(), - tls.trustStorePath(), - tls.trustStorePassword(), - tls.requireClientAuth(), - tls.autoGenerate(), - tls.trustOnFirstUse(), + String pin = learnedTlsFingerprints.getOrDefault(frontend.name(), ""); + if (pin.isBlank()) { + return frontend; + } + return new AgentConfig.FrontendEndpointConfig( + frontend.name(), + frontend.connectHost(), + frontend.connectPort(), pin ); - return new AgentConfig(config.role(), config.nodeName(), config.tunnel(), pinnedTls, config.routes()); } - private void learnTlsFingerprint(String fingerprint) { + private void learnTlsFingerprint(AgentConfig.FrontendEndpointConfig frontend, String fingerprint) { if (fingerprint == null || fingerprint.isBlank()) { return; } - if (learnedTlsFingerprint.compareAndSet("", fingerprint)) { - learnedTlsFingerprintConsumer.accept(fingerprint); - lastEvent.set("pinned frontend TLS certificate"); - LOGGER.info("Pinned frontend TLS certificate fingerprint: " + fingerprint); + if (learnedTlsFingerprints.putIfAbsent(frontend.name(), fingerprint) == null) { + learnedTlsFingerprintConsumer.accept(frontend.name(), fingerprint); + lastEvent.set("pinned frontend TLS certificate for " + frontend.name()); + LOGGER.info("Pinned frontend TLS certificate fingerprint for " + frontend.name() + ": " + fingerprint); } } @@ -189,10 +215,10 @@ public final class BackendAgent implements ManagedAgent { } } - private void sleepWithBackoff(long delayMillis) { + private void sleepWithBackoff(AgentConfig.FrontendEndpointConfig frontend, long delayMillis) { long jitter = ThreadLocalRandom.current().nextLong(0, Math.max(1, delayMillis / 4)); long sleepMillis = delayMillis + jitter; - LOGGER.info(() -> "Reconnecting backend tunnel in " + sleepMillis + "ms"); + LOGGER.info(() -> "Reconnecting backend tunnel to " + frontend.name() + " in " + sleepMillis + "ms"); try { Thread.sleep(sleepMillis); } catch (InterruptedException e) { @@ -201,6 +227,7 @@ public final class BackendAgent implements ManagedAgent { } private final class BackendTunnel { + private final AgentConfig.FrontendEndpointConfig frontend; private final Socket socket; private final FrameCodec codec; private final AtomicBoolean open = new AtomicBoolean(true); @@ -210,7 +237,8 @@ public final class BackendAgent implements ManagedAgent { ); private volatile long lastReadMillis = System.currentTimeMillis(); - private BackendTunnel(Socket socket, FrameCodec codec) { + private BackendTunnel(AgentConfig.FrontendEndpointConfig frontend, Socket socket, FrameCodec codec) { + this.frontend = frontend; this.socket = socket; this.codec = codec; } @@ -236,13 +264,11 @@ public final class BackendAgent implements ManagedAgent { } } catch (Exception e) { if (open.get()) { - LOGGER.log(Level.WARNING, "Backend tunnel read loop stopped", e); + LOGGER.log(Level.WARNING, "Backend tunnel read loop stopped for frontend=" + frontend.name(), e); } } finally { close("frontend tunnel disconnected"); - if (activeTunnel == this) { - activeTunnel = null; - } + activeTunnels.remove(frontend.name(), this); } } @@ -266,8 +292,8 @@ public final class BackendAgent implements ManagedAgent { stream.closeSocket(); } streams.clear(); - lastEvent.set("closed backend tunnel: " + reason); - LOGGER.info(() -> "Closed backend tunnel reason=" + reason); + lastEvent.set("closed backend tunnel to " + frontend.name() + ": " + reason); + LOGGER.info(() -> "Closed backend tunnel to " + frontend.name() + " reason=" + reason); } private void scheduleHeartbeat() { @@ -283,7 +309,7 @@ public final class BackendAgent implements ManagedAgent { return; } if (idleMillis > interval * 2) { - lastEvent.set("heartbeat delayed " + idleMillis + "ms; tunnel still open"); + lastEvent.set("heartbeat delayed " + idleMillis + "ms for " + frontend.name() + "; tunnel still open"); } try { codec.writeFrame(Frame.empty(FrameType.PING, 0)); diff --git a/plugin/src/main/java/me/proxylink/plugin/tunnel/FrontendAgent.java b/plugin/src/main/java/me/proxylink/plugin/tunnel/FrontendAgent.java index 1f914d4..2daa0dd 100644 --- a/plugin/src/main/java/me/proxylink/plugin/tunnel/FrontendAgent.java +++ b/plugin/src/main/java/me/proxylink/plugin/tunnel/FrontendAgent.java @@ -40,7 +40,7 @@ public final class FrontendAgent implements ManagedAgent { new NamedThreadFactory("proxylink-frontend", true) ); private final AtomicBoolean running = new AtomicBoolean(false); - private final AtomicReference activeTunnel = new AtomicReference<>(); + private final Map activeTunnels = new ConcurrentHashMap<>(); private final AtomicReference lastEvent = new AtomicReference<>("not started"); private final List routeServerSockets = new ArrayList<>(); private volatile ServerSocket tunnelServerSocket; @@ -75,14 +75,21 @@ public final class FrontendAgent implements ManagedAgent { @Override public TunnelStatus status() { - FrontendTunnel tunnel = activeTunnel.get(); - boolean connected = running.get() && tunnel != null && tunnel.isOpen(); - int activeStreams = connected ? tunnel.streamCount() : 0; + int connectedTunnels = 0; + int activeStreams = 0; + for (FrontendTunnel tunnel : activeTunnels.values()) { + if (tunnel.isOpen()) { + connectedTunnels++; + activeStreams += tunnel.streamCount(); + } + } + boolean connected = running.get() && connectedTunnels > 0; return new TunnelStatus( config.role(), config.nodeName(), running.get(), connected, + connectedTunnels, activeStreams, config.tls().enabled(), lastEvent.get() @@ -100,10 +107,10 @@ public final class FrontendAgent implements ManagedAgent { closeQuietly(serverSocket); } - FrontendTunnel tunnel = activeTunnel.getAndSet(null); - if (tunnel != null) { + for (FrontendTunnel tunnel : activeTunnels.values()) { tunnel.close("frontend shutting down"); } + activeTunnels.clear(); executor.shutdownNow(); lastEvent.set("frontend stopped"); @@ -130,7 +137,7 @@ public final class FrontendAgent implements ManagedAgent { FrameCodec codec = new FrameCodec(socket.getInputStream(), socket.getOutputStream()); AuthPayloads.Hello hello = authenticateBackend(codec); FrontendTunnel tunnel = new FrontendTunnel(socket, codec, hello.nodeName()); - FrontendTunnel previous = activeTunnel.getAndSet(tunnel); + FrontendTunnel previous = activeTunnels.put(hello.nodeName(), tunnel); if (previous != null) { previous.close("replaced by a new authenticated backend tunnel"); } @@ -186,10 +193,10 @@ public final class FrontendAgent implements ManagedAgent { playerSocket.setTcpNoDelay(true); playerSocket.setKeepAlive(true); - FrontendTunnel tunnel = activeTunnel.get(); + FrontendTunnel tunnel = selectTunnelForRoute(route); if (tunnel == null || !tunnel.isOpen()) { LOGGER.warning(() -> "Rejecting player connection for route " + route.name() - + " because no backend tunnel is authenticated"); + + " because no matching backend tunnel is authenticated"); closeQuietly(playerSocket); continue; } @@ -203,8 +210,34 @@ public final class FrontendAgent implements ManagedAgent { } } + private FrontendTunnel selectTunnelForRoute(AgentConfig.RouteConfig route) { + if (!route.backendNode().isBlank()) { + return activeTunnels.get(route.backendNode()); + } + + FrontendTunnel selected = null; + int openTunnels = 0; + for (FrontendTunnel tunnel : activeTunnels.values()) { + if (!tunnel.isOpen()) { + continue; + } + selected = tunnel; + openTunnels++; + } + + if (openTunnels == 1) { + return selected; + } + if (openTunnels > 1) { + lastEvent.set("route " + route.name() + " is ambiguous; set route." + route.name() + ".backendNode"); + LOGGER.warning(() -> "Route " + route.name() + + " has no backendNode but multiple backend tunnels are connected"); + } + return null; + } + private void clearActiveTunnel(FrontendTunnel tunnel) { - activeTunnel.compareAndSet(tunnel, null); + activeTunnels.remove(tunnel.backendNodeName, tunnel); } private static void closeQuietly(ServerSocket serverSocket) { diff --git a/plugin/src/main/java/me/proxylink/plugin/tunnel/TlsSocketFactory.java b/plugin/src/main/java/me/proxylink/plugin/tunnel/TlsSocketFactory.java index c0e98ad..d461171 100644 --- a/plugin/src/main/java/me/proxylink/plugin/tunnel/TlsSocketFactory.java +++ b/plugin/src/main/java/me/proxylink/plugin/tunnel/TlsSocketFactory.java @@ -62,13 +62,27 @@ final class TlsSocketFactory { static Socket createClientSocket(AgentConfig config, Consumer learnedFingerprintConsumer) throws IOException, GeneralSecurityException { + AgentConfig.FrontendEndpointConfig endpoint = new AgentConfig.FrontendEndpointConfig( + "default", + config.tunnel().connectHost(), + config.tunnel().connectPort(), + config.tls().pinnedCertificateSha256() + ); + return createClientSocket(config, endpoint, learnedFingerprintConsumer); + } + + static Socket createClientSocket( + AgentConfig config, + AgentConfig.FrontendEndpointConfig endpoint, + Consumer learnedFingerprintConsumer + ) throws IOException, GeneralSecurityException { Socket socket; PinnedCertificateTrustManager pinningTrustManager = null; if (!config.tls().enabled()) { socket = SocketFactory.getDefault().createSocket(); } else { - pinningTrustManager = pinningTrustManager(config); + pinningTrustManager = pinningTrustManager(config, endpoint); SSLContext context = sslContext( config.tls().keyStorePath(), config.tls().keyStorePassword(), @@ -82,7 +96,7 @@ final class TlsSocketFactory { socket.setTcpNoDelay(true); socket.setKeepAlive(true); socket.connect( - new InetSocketAddress(config.tunnel().connectHost(), config.tunnel().connectPort()), + new InetSocketAddress(endpoint.connectHost(), endpoint.connectPort()), config.tunnel().connectTimeoutMillis() ); @@ -117,10 +131,16 @@ final class TlsSocketFactory { return context; } - private static PinnedCertificateTrustManager pinningTrustManager(AgentConfig config) { - if (!config.tls().pinnedCertificateSha256().isBlank() || config.tls().trustOnFirstUse()) { + private static PinnedCertificateTrustManager pinningTrustManager( + AgentConfig config, + AgentConfig.FrontendEndpointConfig endpoint + ) { + String pinnedCertificate = endpoint.pinnedCertificateSha256().isBlank() + ? config.tls().pinnedCertificateSha256() + : endpoint.pinnedCertificateSha256(); + if (!pinnedCertificate.isBlank() || config.tls().trustOnFirstUse()) { return new PinnedCertificateTrustManager( - config.tls().pinnedCertificateSha256(), + pinnedCertificate, config.tls().trustOnFirstUse() ); } diff --git a/plugin/src/main/java/me/proxylink/plugin/tunnel/TunnelStatus.java b/plugin/src/main/java/me/proxylink/plugin/tunnel/TunnelStatus.java index 2f3a268..e8df5b7 100644 --- a/plugin/src/main/java/me/proxylink/plugin/tunnel/TunnelStatus.java +++ b/plugin/src/main/java/me/proxylink/plugin/tunnel/TunnelStatus.java @@ -7,11 +7,12 @@ public record TunnelStatus( String nodeName, boolean running, boolean tunnelConnected, + int connectedTunnels, int activeStreams, boolean tlsEnabled, String lastEvent ) { public static TunnelStatus stopped(AgentConfig.Role role, String nodeName, boolean tlsEnabled, String lastEvent) { - return new TunnelStatus(role, nodeName, false, false, 0, tlsEnabled, lastEvent); + return new TunnelStatus(role, nodeName, false, false, 0, 0, tlsEnabled, lastEvent); } } diff --git a/plugin/src/main/resources/bungee-default.properties b/plugin/src/main/resources/bungee-default.properties index 3674d4e..d04aa15 100644 --- a/plugin/src/main/resources/bungee-default.properties +++ b/plugin/src/main/resources/bungee-default.properties @@ -1,6 +1,7 @@ # DirtSimpleP2P Bungee/Waterfall frontend config. # Put this same jar in the Bungee plugins folder. # Bungee should point its backend server entry at route.minecraft.frontendBindHost:route.minecraft.frontendBindPort. +# Add more routes when this proxy should reach multiple backend servers. role=frontend node.name=bungee-frontend @@ -31,3 +32,13 @@ tunnel.tls.requireClientAuth=false routes=minecraft route.minecraft.frontendBindHost=127.0.0.1 route.minecraft.frontendBindPort=25566 +route.minecraft.backendNode=paper-backend + +# Multi-backend example: +# routes=minecraft,lobby +# route.minecraft.frontendBindHost=127.0.0.1 +# route.minecraft.frontendBindPort=25566 +# route.minecraft.backendNode=survival-backend +# route.lobby.frontendBindHost=127.0.0.1 +# route.lobby.frontendBindPort=25567 +# route.lobby.backendNode=lobby-backend diff --git a/plugin/src/main/resources/paper-default.properties b/plugin/src/main/resources/paper-default.properties index c49e70f..0ab9d60 100644 --- a/plugin/src/main/resources/paper-default.properties +++ b/plugin/src/main/resources/paper-default.properties @@ -5,8 +5,20 @@ role=backend node.name=paper-backend -tunnel.connectHost=YOUR_BUNGEE_OR_VPS_IP -tunnel.connectPort=24445 +frontends=proxy1 +frontend.proxy1.connectHost=YOUR_BUNGEE_OR_VPS_IP +frontend.proxy1.connectPort=24445 +frontend.proxy1.tls.pinnedCertificateSha256= + +# Multi-proxy example: +# frontends=proxy1,proxy2 +# frontend.proxy1.connectHost=proxy1.example.com +# frontend.proxy1.connectPort=24445 +# frontend.proxy1.tls.pinnedCertificateSha256= +# frontend.proxy2.connectHost=proxy2.example.com +# frontend.proxy2.connectPort=24445 +# frontend.proxy2.tls.pinnedCertificateSha256= + # Replaced automatically on first start. Paste the same value from the Bungee config here. tunnel.authToken=AUTO_GENERATED_ON_FIRST_START @@ -24,7 +36,6 @@ tunnel.reconnectMaxMillis=10000 tunnel.tls.enabled=false tunnel.tls.allowInsecure=true tunnel.tls.trustOnFirstUse=true -tunnel.tls.pinnedCertificateSha256= routes=minecraft route.minecraft.backendTargetHost=127.0.0.1 diff --git a/plugin/target/DirtSimpleP2P-0.1.0-SNAPSHOT.jar b/plugin/target/DirtSimpleP2P-0.1.0-SNAPSHOT.jar index c8437ce..c1817cb 100644 Binary files a/plugin/target/DirtSimpleP2P-0.1.0-SNAPSHOT.jar and b/plugin/target/DirtSimpleP2P-0.1.0-SNAPSHOT.jar differ diff --git a/plugin/target/classes/bungee-default.properties b/plugin/target/classes/bungee-default.properties index 3674d4e..d04aa15 100644 --- a/plugin/target/classes/bungee-default.properties +++ b/plugin/target/classes/bungee-default.properties @@ -1,6 +1,7 @@ # DirtSimpleP2P Bungee/Waterfall frontend config. # Put this same jar in the Bungee plugins folder. # Bungee should point its backend server entry at route.minecraft.frontendBindHost:route.minecraft.frontendBindPort. +# Add more routes when this proxy should reach multiple backend servers. role=frontend node.name=bungee-frontend @@ -31,3 +32,13 @@ tunnel.tls.requireClientAuth=false routes=minecraft route.minecraft.frontendBindHost=127.0.0.1 route.minecraft.frontendBindPort=25566 +route.minecraft.backendNode=paper-backend + +# Multi-backend example: +# routes=minecraft,lobby +# route.minecraft.frontendBindHost=127.0.0.1 +# route.minecraft.frontendBindPort=25566 +# route.minecraft.backendNode=survival-backend +# route.lobby.frontendBindHost=127.0.0.1 +# route.lobby.frontendBindPort=25567 +# route.lobby.backendNode=lobby-backend diff --git a/plugin/target/classes/me/proxylink/plugin/PluginRuntime.class b/plugin/target/classes/me/proxylink/plugin/PluginRuntime.class index 62b4da3..def4c30 100644 Binary files a/plugin/target/classes/me/proxylink/plugin/PluginRuntime.class and b/plugin/target/classes/me/proxylink/plugin/PluginRuntime.class differ diff --git a/plugin/target/classes/me/proxylink/plugin/tunnel/BackendAgent$1.class b/plugin/target/classes/me/proxylink/plugin/tunnel/BackendAgent$1.class index b6459b3..63d9410 100644 Binary files a/plugin/target/classes/me/proxylink/plugin/tunnel/BackendAgent$1.class and b/plugin/target/classes/me/proxylink/plugin/tunnel/BackendAgent$1.class differ diff --git a/plugin/target/classes/me/proxylink/plugin/tunnel/BackendAgent$BackendTunnel.class b/plugin/target/classes/me/proxylink/plugin/tunnel/BackendAgent$BackendTunnel.class index 780d289..1e22b5f 100644 Binary files a/plugin/target/classes/me/proxylink/plugin/tunnel/BackendAgent$BackendTunnel.class and b/plugin/target/classes/me/proxylink/plugin/tunnel/BackendAgent$BackendTunnel.class differ diff --git a/plugin/target/classes/me/proxylink/plugin/tunnel/BackendAgent.class b/plugin/target/classes/me/proxylink/plugin/tunnel/BackendAgent.class index 208e111..9269125 100644 Binary files a/plugin/target/classes/me/proxylink/plugin/tunnel/BackendAgent.class and b/plugin/target/classes/me/proxylink/plugin/tunnel/BackendAgent.class differ diff --git a/plugin/target/classes/me/proxylink/plugin/tunnel/FrontendAgent$1.class b/plugin/target/classes/me/proxylink/plugin/tunnel/FrontendAgent$1.class index d630224..13094df 100644 Binary files a/plugin/target/classes/me/proxylink/plugin/tunnel/FrontendAgent$1.class and b/plugin/target/classes/me/proxylink/plugin/tunnel/FrontendAgent$1.class differ diff --git a/plugin/target/classes/me/proxylink/plugin/tunnel/FrontendAgent$FrontendTunnel.class b/plugin/target/classes/me/proxylink/plugin/tunnel/FrontendAgent$FrontendTunnel.class index 6607a65..d835cb9 100644 Binary files a/plugin/target/classes/me/proxylink/plugin/tunnel/FrontendAgent$FrontendTunnel.class and b/plugin/target/classes/me/proxylink/plugin/tunnel/FrontendAgent$FrontendTunnel.class differ diff --git a/plugin/target/classes/me/proxylink/plugin/tunnel/FrontendAgent.class b/plugin/target/classes/me/proxylink/plugin/tunnel/FrontendAgent.class index fe63388..feea5d7 100644 Binary files a/plugin/target/classes/me/proxylink/plugin/tunnel/FrontendAgent.class and b/plugin/target/classes/me/proxylink/plugin/tunnel/FrontendAgent.class differ diff --git a/plugin/target/classes/me/proxylink/plugin/tunnel/TlsSocketFactory.class b/plugin/target/classes/me/proxylink/plugin/tunnel/TlsSocketFactory.class index dd60367..efd1816 100644 Binary files a/plugin/target/classes/me/proxylink/plugin/tunnel/TlsSocketFactory.class and b/plugin/target/classes/me/proxylink/plugin/tunnel/TlsSocketFactory.class differ diff --git a/plugin/target/classes/me/proxylink/plugin/tunnel/TunnelStatus.class b/plugin/target/classes/me/proxylink/plugin/tunnel/TunnelStatus.class index fe3ec2b..aa986b2 100644 Binary files a/plugin/target/classes/me/proxylink/plugin/tunnel/TunnelStatus.class and b/plugin/target/classes/me/proxylink/plugin/tunnel/TunnelStatus.class differ diff --git a/plugin/target/classes/paper-default.properties b/plugin/target/classes/paper-default.properties index c49e70f..0ab9d60 100644 --- a/plugin/target/classes/paper-default.properties +++ b/plugin/target/classes/paper-default.properties @@ -5,8 +5,20 @@ role=backend node.name=paper-backend -tunnel.connectHost=YOUR_BUNGEE_OR_VPS_IP -tunnel.connectPort=24445 +frontends=proxy1 +frontend.proxy1.connectHost=YOUR_BUNGEE_OR_VPS_IP +frontend.proxy1.connectPort=24445 +frontend.proxy1.tls.pinnedCertificateSha256= + +# Multi-proxy example: +# frontends=proxy1,proxy2 +# frontend.proxy1.connectHost=proxy1.example.com +# frontend.proxy1.connectPort=24445 +# frontend.proxy1.tls.pinnedCertificateSha256= +# frontend.proxy2.connectHost=proxy2.example.com +# frontend.proxy2.connectPort=24445 +# frontend.proxy2.tls.pinnedCertificateSha256= + # Replaced automatically on first start. Paste the same value from the Bungee config here. tunnel.authToken=AUTO_GENERATED_ON_FIRST_START @@ -24,7 +36,6 @@ tunnel.reconnectMaxMillis=10000 tunnel.tls.enabled=false tunnel.tls.allowInsecure=true tunnel.tls.trustOnFirstUse=true -tunnel.tls.pinnedCertificateSha256= routes=minecraft route.minecraft.backendTargetHost=127.0.0.1 diff --git a/plugin/target/original-DirtSimpleP2P-0.1.0-SNAPSHOT.jar b/plugin/target/original-DirtSimpleP2P-0.1.0-SNAPSHOT.jar index 73f5623..27486b5 100644 Binary files a/plugin/target/original-DirtSimpleP2P-0.1.0-SNAPSHOT.jar and b/plugin/target/original-DirtSimpleP2P-0.1.0-SNAPSHOT.jar differ