Added multi server support
This commit is contained in:
@@ -1,75 +1,95 @@
|
|||||||
# DirtSimpleP2P
|
# 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
|
## Features
|
||||||
- No home router port forwarding
|
|
||||||
- Works with a public Bungee/Waterfall proxy
|
- One jar for both the proxy and backend server
|
||||||
- Works with a backend Paper/Spigot 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
|
- Secret-token authentication
|
||||||
- Automatic secret-token generation
|
- Automatic token generation
|
||||||
- Optional TLS encryption with generated self-signed certificates
|
- Optional TLS encryption
|
||||||
- Multiple Minecraft player connections over one tunnel
|
- Self-signed TLS certificate generation
|
||||||
- Fast reconnect with heartbeat-based latency tolerance
|
- 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
|
- `/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
|
```bash
|
||||||
mvn clean package
|
mvn clean package
|
||||||
```
|
```
|
||||||
|
|
||||||
The plugin jar will be here:
|
The plugin jar is created at:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
plugin/target/DirtSimpleP2P-0.1.0-SNAPSHOT.jar
|
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
|
## Basic Setup
|
||||||
|
|
||||||
You need two Minecraft-side servers:
|
You need:
|
||||||
|
|
||||||
- Public proxy server: BungeeCord, Waterfall, or another Bungee-compatible proxy
|
- A public BungeeCord/Waterfall proxy, usually on a VPS
|
||||||
- Backend server: Paper or Spigot
|
- 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:
|
```text
|
||||||
|
|
||||||
```bash
|
|
||||||
DirtSimpleP2P-0.1.0-SNAPSHOT.jar
|
DirtSimpleP2P-0.1.0-SNAPSHOT.jar
|
||||||
```
|
```
|
||||||
|
|
||||||
Start the proxy once, then stop it.
|
Start the proxy once, then stop it.
|
||||||
|
|
||||||
DirtSimpleP2P will create:
|
DirtSimpleP2P creates:
|
||||||
|
|
||||||
```bash
|
```text
|
||||||
plugins/DirtSimpleP2P/config.properties
|
plugins/DirtSimpleP2P/config.properties
|
||||||
```
|
```
|
||||||
|
|
||||||
Open that file.
|
### 2. Configure The Proxy
|
||||||
|
|
||||||
## Step 2: Configure Bungee
|
Use this on the BungeeCord/Waterfall side:
|
||||||
|
|
||||||
On the Bungee/proxy server, use:
|
|
||||||
|
|
||||||
```properties
|
```properties
|
||||||
role=frontend
|
role=frontend
|
||||||
@@ -96,70 +116,57 @@ tunnel.tls.requireClientAuth=false
|
|||||||
routes=minecraft
|
routes=minecraft
|
||||||
route.minecraft.frontendBindHost=127.0.0.1
|
route.minecraft.frontendBindHost=127.0.0.1
|
||||||
route.minecraft.frontendBindPort=25566
|
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.
|
In your BungeeCord/Waterfall server config, set the backend address to:
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
127.0.0.1:25566
|
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
|
DirtSimpleP2P-0.1.0-SNAPSHOT.jar
|
||||||
```
|
```
|
||||||
|
|
||||||
Start the backend server once, then stop it.
|
Start the backend server once, then stop it.
|
||||||
|
|
||||||
DirtSimpleP2P will create:
|
DirtSimpleP2P creates:
|
||||||
|
|
||||||
```bash
|
```text
|
||||||
plugins/DirtSimpleP2P/config.properties
|
plugins/DirtSimpleP2P/config.properties
|
||||||
```
|
```
|
||||||
|
|
||||||
Open that file.
|
### 5. Configure The Backend
|
||||||
|
|
||||||
## Step 5: Configure Paper Or Spigot
|
Use this on the Paper/Spigot side:
|
||||||
|
|
||||||
On the backend server, use:
|
|
||||||
|
|
||||||
```properties
|
```properties
|
||||||
role=backend
|
role=backend
|
||||||
node.name=paper-backend
|
node.name=paper-backend
|
||||||
|
|
||||||
tunnel.connectHost=YOUR_BUNGEE_OR_VPS_IP
|
frontends=proxy1
|
||||||
tunnel.connectPort=24445
|
frontend.proxy1.connectHost=YOUR_PROXY_IP_OR_DOMAIN
|
||||||
tunnel.authToken=PASTE_THE_BUNGEE_TOKEN_HERE
|
frontend.proxy1.connectPort=24445
|
||||||
|
frontend.proxy1.tls.pinnedCertificateSha256=
|
||||||
|
|
||||||
|
tunnel.authToken=PASTE_THE_PROXY_TOKEN_HERE
|
||||||
|
|
||||||
tunnel.connectTimeoutMillis=5000
|
tunnel.connectTimeoutMillis=5000
|
||||||
tunnel.heartbeatIntervalMillis=2000
|
tunnel.heartbeatIntervalMillis=2000
|
||||||
@@ -171,7 +178,6 @@ tunnel.reconnectMaxMillis=10000
|
|||||||
tunnel.tls.enabled=false
|
tunnel.tls.enabled=false
|
||||||
tunnel.tls.allowInsecure=true
|
tunnel.tls.allowInsecure=true
|
||||||
tunnel.tls.trustOnFirstUse=true
|
tunnel.tls.trustOnFirstUse=true
|
||||||
tunnel.tls.pinnedCertificateSha256=
|
|
||||||
|
|
||||||
routes=minecraft
|
routes=minecraft
|
||||||
route.minecraft.backendTargetHost=127.0.0.1
|
route.minecraft.backendTargetHost=127.0.0.1
|
||||||
@@ -180,58 +186,167 @@ route.minecraft.backendTargetPort=25565
|
|||||||
|
|
||||||
Change:
|
Change:
|
||||||
|
|
||||||
- `tunnel.connectHost` to your public proxy/VPS IP or domain name
|
- `frontend.proxy1.connectHost` to your public proxy IP or domain
|
||||||
- `tunnel.authToken` to the same secret used on Bungee
|
- `frontend.proxy1.connectPort` to the proxy `tunnel.listenPort`
|
||||||
- `route.minecraft.backendTargetPort` if your backend Minecraft server is not on `25565`
|
- `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
|
```properties
|
||||||
tunnel.tls.enabled=true
|
tunnel.tls.enabled=true
|
||||||
tunnel.tls.allowInsecure=false
|
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
|
```properties
|
||||||
|
tunnel.tls.enabled=true
|
||||||
|
tunnel.tls.allowInsecure=false
|
||||||
tunnel.tls.trustOnFirstUse=true
|
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
|
```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
|
## Commands
|
||||||
|
|
||||||
Run these commands in game or from the server console:
|
Run from the server console or in game:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/dsp2p status
|
/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
|
```text
|
||||||
/dsp2p doctor
|
/dsp2p doctor
|
||||||
```
|
```
|
||||||
|
|
||||||
Checks the config and prints useful setup/security hints.
|
Checks the config and prints setup/security hints.
|
||||||
|
|
||||||
Permission:
|
Permission:
|
||||||
|
|
||||||
@@ -239,35 +354,35 @@ Permission:
|
|||||||
dirtsimplep2p.command
|
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
|
## Firewall Notes
|
||||||
|
|
||||||
On the public proxy/VPS, allow:
|
On the public proxy/VPS, allow the tunnel port:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
24445/tcp
|
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
|
```text
|
||||||
25565/tcp
|
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
|
## 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:
|
Default behavior:
|
||||||
|
|
||||||
- Heartbeat every `2` seconds
|
- Heartbeat every `2` seconds
|
||||||
- Warns in status if heartbeats are delayed
|
- Status warning if heartbeats are delayed
|
||||||
- Disconnects after about `8` seconds of no tunnel traffic
|
- Disconnect after about `8` seconds without tunnel traffic
|
||||||
- First reconnect retry after about `250ms`
|
- 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:
|
Useful config values:
|
||||||
|
|
||||||
@@ -279,39 +394,43 @@ tunnel.reconnectInitialMillis=250
|
|||||||
tunnel.reconnectMaxMillis=10000
|
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:
|
Check that:
|
||||||
|
|
||||||
- Bungee/proxy server is running
|
- The proxy server is running
|
||||||
- DirtSimpleP2P started on Bungee
|
- DirtSimpleP2P started on the proxy
|
||||||
- `tunnel.connectHost` points to the proxy/VPS
|
- `frontend.proxy1.connectHost` points to the proxy IP or domain
|
||||||
- `tunnel.connectPort` matches the Bungee `tunnel.listenPort`
|
- `frontend.proxy1.connectPort` matches the proxy `tunnel.listenPort`
|
||||||
- The VPS firewall allows that TCP port
|
- 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
|
```text
|
||||||
127.0.0.1:25566
|
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
|
- reconnect messages
|
||||||
- heartbeat timeout messages
|
- heartbeat timeout messages
|
||||||
@@ -320,23 +439,20 @@ Check both server logs. Look for:
|
|||||||
|
|
||||||
## Security Notes
|
## 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
|
```properties
|
||||||
tunnel.tls.enabled=false
|
tunnel.tls.enabled=false
|
||||||
tunnel.tls.allowInsecure=true
|
tunnel.tls.allowInsecure=true
|
||||||
```
|
```
|
||||||
|
|
||||||
For a better setup, enable TLS on both sides:
|
For real deployments, enable TLS:
|
||||||
|
|
||||||
```properties
|
```properties
|
||||||
tunnel.tls.enabled=true
|
tunnel.tls.enabled=true
|
||||||
tunnel.tls.allowInsecure=false
|
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 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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package me.proxylink.common;
|
|||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public record AgentConfig(
|
public record AgentConfig(
|
||||||
@@ -12,6 +14,7 @@ public record AgentConfig(
|
|||||||
String nodeName,
|
String nodeName,
|
||||||
TunnelConfig tunnel,
|
TunnelConfig tunnel,
|
||||||
TlsConfig tls,
|
TlsConfig tls,
|
||||||
|
List<FrontendEndpointConfig> frontends,
|
||||||
List<RouteConfig> routes
|
List<RouteConfig> routes
|
||||||
) {
|
) {
|
||||||
private static final Pattern ROUTE_NAME = Pattern.compile("[A-Za-z0-9._-]+");
|
private static final Pattern ROUTE_NAME = Pattern.compile("[A-Za-z0-9._-]+");
|
||||||
@@ -21,6 +24,7 @@ public record AgentConfig(
|
|||||||
nodeName = requireNonBlank(nodeName, "nodeName");
|
nodeName = requireNonBlank(nodeName, "nodeName");
|
||||||
Objects.requireNonNull(tunnel, "tunnel");
|
Objects.requireNonNull(tunnel, "tunnel");
|
||||||
Objects.requireNonNull(tls, "tls");
|
Objects.requireNonNull(tls, "tls");
|
||||||
|
frontends = List.copyOf(Objects.requireNonNull(frontends, "frontends"));
|
||||||
routes = List.copyOf(Objects.requireNonNull(routes, "routes"));
|
routes = List.copyOf(Objects.requireNonNull(routes, "routes"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,10 +55,27 @@ public record AgentConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (role == Role.BACKEND) {
|
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<String> 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.enabled()) {
|
||||||
if (tls.trustStorePath() == null && tls.pinnedCertificateSha256().isBlank() && !tls.trustOnFirstUse()) {
|
if (tls.trustStorePath() == null && !tls.trustOnFirstUse()) {
|
||||||
errors.add("TLS is enabled on backend, so set tunnel.tls.pinnedCertificateSha256, set tunnel.tls.trustOnFirstUse=true, or configure a truststore");
|
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) {
|
if (tls.trustStorePath() != null) {
|
||||||
requireSecret(errors, "tunnel.tls.trustStorePassword", tls.trustStorePassword());
|
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");
|
errors.add("tunnel.reconnectMaxMillis must be greater than or equal to tunnel.reconnectInitialMillis");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Set<String> routeNames = new HashSet<>();
|
||||||
|
Set<String> frontendBinds = new HashSet<>();
|
||||||
for (RouteConfig route : routes) {
|
for (RouteConfig route : routes) {
|
||||||
if (!ROUTE_NAME.matcher(route.name()).matches()) {
|
if (!ROUTE_NAME.matcher(route.name()).matches()) {
|
||||||
errors.add("Route name contains invalid characters: " + route.name());
|
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) {
|
if (role == Role.FRONTEND) {
|
||||||
validateHostPort(errors, "route." + route.name() + ".frontendBind", route.frontendBindHost(), route.frontendBindPort());
|
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) {
|
if (role == Role.BACKEND) {
|
||||||
validateHostPort(errors, "route." + route.name() + ".backendTarget", route.backendTargetHost(), route.backendTargetPort());
|
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(
|
public record RouteConfig(
|
||||||
String name,
|
String name,
|
||||||
String frontendBindHost,
|
String frontendBindHost,
|
||||||
int frontendBindPort,
|
int frontendBindPort,
|
||||||
|
String backendNode,
|
||||||
String backendTargetHost,
|
String backendTargetHost,
|
||||||
int backendTargetPort
|
int backendTargetPort
|
||||||
) {
|
) {
|
||||||
public RouteConfig {
|
public RouteConfig {
|
||||||
name = requireNonBlank(name, "name");
|
name = requireNonBlank(name, "name");
|
||||||
|
backendNode = backendNode == null ? "" : backendNode.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,43 @@ public final class AgentConfigIO {
|
|||||||
value(properties, "tunnel.tls.pinnedCertificateSha256", "")
|
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<AgentConfig.FrontendEndpointConfig> frontends(
|
||||||
|
Properties properties,
|
||||||
|
AgentConfig.TunnelConfig tunnel,
|
||||||
|
AgentConfig.TlsConfig tls
|
||||||
|
) {
|
||||||
|
String frontendsValue = value(properties, "frontends", "");
|
||||||
|
List<AgentConfig.FrontendEndpointConfig> 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<AgentConfig.RouteConfig> routes(Properties properties) {
|
private static List<AgentConfig.RouteConfig> routes(Properties properties) {
|
||||||
@@ -72,6 +108,7 @@ public final class AgentConfigIO {
|
|||||||
name,
|
name,
|
||||||
value(properties, prefix + "frontendBindHost", "127.0.0.1"),
|
value(properties, prefix + "frontendBindHost", "127.0.0.1"),
|
||||||
intValue(properties, prefix + "frontendBindPort", -1),
|
intValue(properties, prefix + "frontendBindPort", -1),
|
||||||
|
value(properties, prefix + "backendNode", ""),
|
||||||
value(properties, prefix + "backendTargetHost", "127.0.0.1"),
|
value(properties, prefix + "backendTargetHost", "127.0.0.1"),
|
||||||
intValue(properties, prefix + "backendTargetPort", -1)
|
intValue(properties, prefix + "backendTargetPort", -1)
|
||||||
));
|
));
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
@@ -3,6 +3,7 @@ me/proxylink/common/AgentConfig.class
|
|||||||
me/proxylink/common/AgentConfigIO.class
|
me/proxylink/common/AgentConfigIO.class
|
||||||
me/proxylink/common/AuthPayloads$Response.class
|
me/proxylink/common/AuthPayloads$Response.class
|
||||||
me/proxylink/common/FrameType.class
|
me/proxylink/common/FrameType.class
|
||||||
|
me/proxylink/common/AgentConfig$FrontendEndpointConfig.class
|
||||||
me/proxylink/common/AuthPayloads$Challenge.class
|
me/proxylink/common/AuthPayloads$Challenge.class
|
||||||
me/proxylink/common/ProtocolConstants.class
|
me/proxylink/common/ProtocolConstants.class
|
||||||
me/proxylink/common/FrameCodec.class
|
me/proxylink/common/FrameCodec.class
|
||||||
|
|||||||
Binary file not shown.
@@ -74,6 +74,7 @@ public final class PluginRuntime {
|
|||||||
lines.add("Node: " + status.nodeName());
|
lines.add("Node: " + status.nodeName());
|
||||||
lines.add("Agent: " + (status.running() ? "running" : "stopped"));
|
lines.add("Agent: " + (status.running() ? "running" : "stopped"));
|
||||||
lines.add("Tunnel: " + (status.tunnelConnected() ? "connected" : "not connected"));
|
lines.add("Tunnel: " + (status.tunnelConnected() ? "connected" : "not connected"));
|
||||||
|
lines.add("Connected tunnels: " + status.connectedTunnels());
|
||||||
lines.add("Active streams: " + status.activeStreams());
|
lines.add("Active streams: " + status.activeStreams());
|
||||||
lines.add("TLS: " + (status.tlsEnabled() ? "enabled" : "disabled"));
|
lines.add("TLS: " + (status.tlsEnabled() ? "enabled" : "disabled"));
|
||||||
lines.add("Last event: " + status.lastEvent());
|
lines.add("Last event: " + status.lastEvent());
|
||||||
@@ -99,6 +100,10 @@ public final class PluginRuntime {
|
|||||||
lines.add("Config: valid");
|
lines.add("Config: valid");
|
||||||
lines.add("Role: " + lower(loaded.role()));
|
lines.add("Role: " + lower(loaded.role()));
|
||||||
lines.add("TLS: " + (loaded.tls().enabled() ? "enabled" : "disabled"));
|
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()) {
|
if (!loaded.tls().enabled()) {
|
||||||
lines.add("Security: TLS is disabled. This is okay for local testing, not ideal for paid production use.");
|
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);
|
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()) {
|
if (configPath == null || fingerprint == null || fingerprint.isBlank()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
ConfigFileEditor.setProperty(configPath,
|
||||||
|
"frontend." + endpointName + ".tls.pinnedCertificateSha256",
|
||||||
|
fingerprint);
|
||||||
|
if (config != null && config.frontends().size() == 1) {
|
||||||
ConfigFileEditor.setProperty(configPath, "tunnel.tls.pinnedCertificateSha256", fingerprint);
|
ConfigFileEditor.setProperty(configPath, "tunnel.tls.pinnedCertificateSha256", fingerprint);
|
||||||
|
}
|
||||||
config = AgentConfigIO.load(configPath);
|
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) {
|
} catch (Exception e) {
|
||||||
logger.log(Level.WARNING, "Unable to save frontend TLS certificate pin", e);
|
logger.log(Level.WARNING, "Unable to save frontend TLS certificate pin", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import java.util.concurrent.ThreadLocalRandom;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
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.Level;
|
||||||
import java.util.logging.Logger;
|
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 static final Logger LOGGER = Logger.getLogger(BackendAgent.class.getName());
|
||||||
|
|
||||||
private final AgentConfig config;
|
private final AgentConfig config;
|
||||||
private final Consumer<String> learnedTlsFingerprintConsumer;
|
private final BiConsumer<String, String> learnedTlsFingerprintConsumer;
|
||||||
private final ExecutorService executor = Executors.newThreadPerTaskExecutor(
|
private final ExecutorService executor = Executors.newThreadPerTaskExecutor(
|
||||||
new NamedThreadFactory("proxylink-backend", true)
|
new NamedThreadFactory("proxylink-backend", true)
|
||||||
);
|
);
|
||||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||||
private final AtomicReference<String> learnedTlsFingerprint = new AtomicReference<>("");
|
private final Map<String, String> learnedTlsFingerprints = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, BackendTunnel> activeTunnels = new ConcurrentHashMap<>();
|
||||||
private final AtomicReference<String> lastEvent = new AtomicReference<>("not started");
|
private final AtomicReference<String> lastEvent = new AtomicReference<>("not started");
|
||||||
private volatile BackendTunnel activeTunnel;
|
|
||||||
|
|
||||||
public BackendAgent(AgentConfig config) {
|
public BackendAgent(AgentConfig config) {
|
||||||
this(config, ignored -> {
|
this(config, (ignoredEndpoint, ignoredFingerprint) -> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public BackendAgent(AgentConfig config, Consumer<String> learnedTlsFingerprintConsumer) {
|
public BackendAgent(AgentConfig config, BiConsumer<String, String> learnedTlsFingerprintConsumer) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.learnedTlsFingerprintConsumer = learnedTlsFingerprintConsumer;
|
this.learnedTlsFingerprintConsumer = learnedTlsFingerprintConsumer;
|
||||||
}
|
}
|
||||||
@@ -58,22 +58,30 @@ public final class BackendAgent implements ManagedAgent {
|
|||||||
if (!running.compareAndSet(false, true)) {
|
if (!running.compareAndSet(false, true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
executor.execute(this::runReconnectLoop);
|
for (AgentConfig.FrontendEndpointConfig frontend : config.frontends()) {
|
||||||
lastEvent.set("connecting to frontend " + config.tunnel().connectHost() + ":" + config.tunnel().connectPort());
|
executor.execute(() -> runReconnectLoop(frontend));
|
||||||
LOGGER.info(() -> "Backend agent connecting to frontend "
|
}
|
||||||
+ config.tunnel().connectHost() + ":" + config.tunnel().connectPort());
|
lastEvent.set("connecting to " + config.frontends().size() + " frontend endpoint(s)");
|
||||||
|
LOGGER.info(() -> "Backend agent connecting to " + config.frontends().size() + " frontend endpoint(s)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TunnelStatus status() {
|
public TunnelStatus status() {
|
||||||
BackendTunnel tunnel = activeTunnel;
|
int connectedTunnels = 0;
|
||||||
boolean connected = running.get() && tunnel != null && tunnel.isOpen();
|
int activeStreams = 0;
|
||||||
int activeStreams = connected ? tunnel.streamCount() : 0;
|
for (BackendTunnel tunnel : activeTunnels.values()) {
|
||||||
|
if (tunnel.isOpen()) {
|
||||||
|
connectedTunnels++;
|
||||||
|
activeStreams += tunnel.streamCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean connected = running.get() && connectedTunnels > 0;
|
||||||
return new TunnelStatus(
|
return new TunnelStatus(
|
||||||
config.role(),
|
config.role(),
|
||||||
config.nodeName(),
|
config.nodeName(),
|
||||||
running.get(),
|
running.get(),
|
||||||
connected,
|
connected,
|
||||||
|
connectedTunnels,
|
||||||
activeStreams,
|
activeStreams,
|
||||||
config.tls().enabled(),
|
config.tls().enabled(),
|
||||||
lastEvent.get()
|
lastEvent.get()
|
||||||
@@ -85,76 +93,94 @@ public final class BackendAgent implements ManagedAgent {
|
|||||||
if (!running.compareAndSet(true, false)) {
|
if (!running.compareAndSet(true, false)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BackendTunnel tunnel = activeTunnel;
|
for (BackendTunnel tunnel : activeTunnels.values()) {
|
||||||
if (tunnel != null) {
|
|
||||||
tunnel.close("backend shutting down");
|
tunnel.close("backend shutting down");
|
||||||
}
|
}
|
||||||
|
activeTunnels.clear();
|
||||||
executor.shutdownNow();
|
executor.shutdownNow();
|
||||||
lastEvent.set("backend stopped");
|
lastEvent.set("backend stopped");
|
||||||
LOGGER.info("Backend agent stopped");
|
LOGGER.info("Backend agent stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runReconnectLoop() {
|
private void runReconnectLoop(AgentConfig.FrontendEndpointConfig frontend) {
|
||||||
long delayMillis = config.tunnel().reconnectInitialMillis();
|
long delayMillis = config.tunnel().reconnectInitialMillis();
|
||||||
while (running.get()) {
|
while (running.get()) {
|
||||||
try {
|
try {
|
||||||
runSingleConnection();
|
runSingleConnection(frontend);
|
||||||
delayMillis = config.tunnel().reconnectInitialMillis();
|
delayMillis = config.tunnel().reconnectInitialMillis();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (running.get()) {
|
if (running.get()) {
|
||||||
lastEvent.set("connection failed: " + e.getMessage());
|
lastEvent.set("connection to " + frontend.name() + " failed: " + e.getMessage());
|
||||||
LOGGER.log(Level.WARNING, "Backend tunnel connection failed", e);
|
LOGGER.log(Level.WARNING, "Backend tunnel connection to " + frontend.name() + " failed", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (running.get()) {
|
if (running.get()) {
|
||||||
sleepWithBackoff(delayMillis);
|
sleepWithBackoff(frontend, delayMillis);
|
||||||
delayMillis = Math.min(delayMillis * 2, config.tunnel().reconnectMaxMillis());
|
delayMillis = Math.min(delayMillis * 2, config.tunnel().reconnectMaxMillis());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runSingleConnection() throws IOException, GeneralSecurityException {
|
private void runSingleConnection(AgentConfig.FrontendEndpointConfig frontend)
|
||||||
Socket socket = TlsSocketFactory.createClientSocket(connectionConfig(), this::learnTlsFingerprint);
|
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());
|
FrameCodec codec = new FrameCodec(socket.getInputStream(), socket.getOutputStream());
|
||||||
authenticateToFrontend(codec);
|
authenticateToFrontend(codec);
|
||||||
|
|
||||||
BackendTunnel tunnel = new BackendTunnel(socket, codec);
|
BackendTunnel tunnel = new BackendTunnel(frontend, socket, codec);
|
||||||
activeTunnel = tunnel;
|
BackendTunnel previous = activeTunnels.put(frontend.name(), tunnel);
|
||||||
lastEvent.set("authenticated to frontend");
|
if (previous != null) {
|
||||||
LOGGER.info(() -> "Backend tunnel authenticated to "
|
previous.close("replaced by a new authenticated tunnel to " + frontend.name());
|
||||||
+ config.tunnel().connectHost() + ":" + config.tunnel().connectPort());
|
|
||||||
tunnel.runReadLoop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private AgentConfig connectionConfig() {
|
handedToTunnel = true;
|
||||||
String pin = learnedTlsFingerprint.get();
|
lastEvent.set("authenticated to frontend " + frontend.name());
|
||||||
if (pin.isBlank()) {
|
LOGGER.info(() -> "Backend tunnel authenticated to " + frontend.name()
|
||||||
return config;
|
+ " at " + frontend.connectHost() + ":" + frontend.connectPort());
|
||||||
|
tunnel.runReadLoop();
|
||||||
|
} finally {
|
||||||
|
if (!handedToTunnel && socket != null) {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
}
|
}
|
||||||
AgentConfig.TlsConfig tls = config.tls();
|
}
|
||||||
AgentConfig.TlsConfig pinnedTls = new AgentConfig.TlsConfig(
|
}
|
||||||
tls.enabled(),
|
}
|
||||||
tls.keyStorePath(),
|
|
||||||
tls.keyStorePassword(),
|
private AgentConfig.FrontendEndpointConfig frontendWithLearnedPin(AgentConfig.FrontendEndpointConfig frontend) {
|
||||||
tls.trustStorePath(),
|
if (!frontend.pinnedCertificateSha256().isBlank()) {
|
||||||
tls.trustStorePassword(),
|
return frontend;
|
||||||
tls.requireClientAuth(),
|
}
|
||||||
tls.autoGenerate(),
|
String pin = learnedTlsFingerprints.getOrDefault(frontend.name(), "");
|
||||||
tls.trustOnFirstUse(),
|
if (pin.isBlank()) {
|
||||||
|
return frontend;
|
||||||
|
}
|
||||||
|
return new AgentConfig.FrontendEndpointConfig(
|
||||||
|
frontend.name(),
|
||||||
|
frontend.connectHost(),
|
||||||
|
frontend.connectPort(),
|
||||||
pin
|
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()) {
|
if (fingerprint == null || fingerprint.isBlank()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (learnedTlsFingerprint.compareAndSet("", fingerprint)) {
|
if (learnedTlsFingerprints.putIfAbsent(frontend.name(), fingerprint) == null) {
|
||||||
learnedTlsFingerprintConsumer.accept(fingerprint);
|
learnedTlsFingerprintConsumer.accept(frontend.name(), fingerprint);
|
||||||
lastEvent.set("pinned frontend TLS certificate");
|
lastEvent.set("pinned frontend TLS certificate for " + frontend.name());
|
||||||
LOGGER.info("Pinned frontend TLS certificate fingerprint: " + fingerprint);
|
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 jitter = ThreadLocalRandom.current().nextLong(0, Math.max(1, delayMillis / 4));
|
||||||
long sleepMillis = delayMillis + jitter;
|
long sleepMillis = delayMillis + jitter;
|
||||||
LOGGER.info(() -> "Reconnecting backend tunnel in " + sleepMillis + "ms");
|
LOGGER.info(() -> "Reconnecting backend tunnel to " + frontend.name() + " in " + sleepMillis + "ms");
|
||||||
try {
|
try {
|
||||||
Thread.sleep(sleepMillis);
|
Thread.sleep(sleepMillis);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
@@ -201,6 +227,7 @@ public final class BackendAgent implements ManagedAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final class BackendTunnel {
|
private final class BackendTunnel {
|
||||||
|
private final AgentConfig.FrontendEndpointConfig frontend;
|
||||||
private final Socket socket;
|
private final Socket socket;
|
||||||
private final FrameCodec codec;
|
private final FrameCodec codec;
|
||||||
private final AtomicBoolean open = new AtomicBoolean(true);
|
private final AtomicBoolean open = new AtomicBoolean(true);
|
||||||
@@ -210,7 +237,8 @@ public final class BackendAgent implements ManagedAgent {
|
|||||||
);
|
);
|
||||||
private volatile long lastReadMillis = System.currentTimeMillis();
|
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.socket = socket;
|
||||||
this.codec = codec;
|
this.codec = codec;
|
||||||
}
|
}
|
||||||
@@ -236,13 +264,11 @@ public final class BackendAgent implements ManagedAgent {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (open.get()) {
|
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 {
|
} finally {
|
||||||
close("frontend tunnel disconnected");
|
close("frontend tunnel disconnected");
|
||||||
if (activeTunnel == this) {
|
activeTunnels.remove(frontend.name(), this);
|
||||||
activeTunnel = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,8 +292,8 @@ public final class BackendAgent implements ManagedAgent {
|
|||||||
stream.closeSocket();
|
stream.closeSocket();
|
||||||
}
|
}
|
||||||
streams.clear();
|
streams.clear();
|
||||||
lastEvent.set("closed backend tunnel: " + reason);
|
lastEvent.set("closed backend tunnel to " + frontend.name() + ": " + reason);
|
||||||
LOGGER.info(() -> "Closed backend tunnel reason=" + reason);
|
LOGGER.info(() -> "Closed backend tunnel to " + frontend.name() + " reason=" + reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleHeartbeat() {
|
private void scheduleHeartbeat() {
|
||||||
@@ -283,7 +309,7 @@ public final class BackendAgent implements ManagedAgent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (idleMillis > interval * 2) {
|
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 {
|
try {
|
||||||
codec.writeFrame(Frame.empty(FrameType.PING, 0));
|
codec.writeFrame(Frame.empty(FrameType.PING, 0));
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public final class FrontendAgent implements ManagedAgent {
|
|||||||
new NamedThreadFactory("proxylink-frontend", true)
|
new NamedThreadFactory("proxylink-frontend", true)
|
||||||
);
|
);
|
||||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||||
private final AtomicReference<FrontendTunnel> activeTunnel = new AtomicReference<>();
|
private final Map<String, FrontendTunnel> activeTunnels = new ConcurrentHashMap<>();
|
||||||
private final AtomicReference<String> lastEvent = new AtomicReference<>("not started");
|
private final AtomicReference<String> lastEvent = new AtomicReference<>("not started");
|
||||||
private final List<ServerSocket> routeServerSockets = new ArrayList<>();
|
private final List<ServerSocket> routeServerSockets = new ArrayList<>();
|
||||||
private volatile ServerSocket tunnelServerSocket;
|
private volatile ServerSocket tunnelServerSocket;
|
||||||
@@ -75,14 +75,21 @@ public final class FrontendAgent implements ManagedAgent {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TunnelStatus status() {
|
public TunnelStatus status() {
|
||||||
FrontendTunnel tunnel = activeTunnel.get();
|
int connectedTunnels = 0;
|
||||||
boolean connected = running.get() && tunnel != null && tunnel.isOpen();
|
int activeStreams = 0;
|
||||||
int activeStreams = connected ? tunnel.streamCount() : 0;
|
for (FrontendTunnel tunnel : activeTunnels.values()) {
|
||||||
|
if (tunnel.isOpen()) {
|
||||||
|
connectedTunnels++;
|
||||||
|
activeStreams += tunnel.streamCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean connected = running.get() && connectedTunnels > 0;
|
||||||
return new TunnelStatus(
|
return new TunnelStatus(
|
||||||
config.role(),
|
config.role(),
|
||||||
config.nodeName(),
|
config.nodeName(),
|
||||||
running.get(),
|
running.get(),
|
||||||
connected,
|
connected,
|
||||||
|
connectedTunnels,
|
||||||
activeStreams,
|
activeStreams,
|
||||||
config.tls().enabled(),
|
config.tls().enabled(),
|
||||||
lastEvent.get()
|
lastEvent.get()
|
||||||
@@ -100,10 +107,10 @@ public final class FrontendAgent implements ManagedAgent {
|
|||||||
closeQuietly(serverSocket);
|
closeQuietly(serverSocket);
|
||||||
}
|
}
|
||||||
|
|
||||||
FrontendTunnel tunnel = activeTunnel.getAndSet(null);
|
for (FrontendTunnel tunnel : activeTunnels.values()) {
|
||||||
if (tunnel != null) {
|
|
||||||
tunnel.close("frontend shutting down");
|
tunnel.close("frontend shutting down");
|
||||||
}
|
}
|
||||||
|
activeTunnels.clear();
|
||||||
|
|
||||||
executor.shutdownNow();
|
executor.shutdownNow();
|
||||||
lastEvent.set("frontend stopped");
|
lastEvent.set("frontend stopped");
|
||||||
@@ -130,7 +137,7 @@ public final class FrontendAgent implements ManagedAgent {
|
|||||||
FrameCodec codec = new FrameCodec(socket.getInputStream(), socket.getOutputStream());
|
FrameCodec codec = new FrameCodec(socket.getInputStream(), socket.getOutputStream());
|
||||||
AuthPayloads.Hello hello = authenticateBackend(codec);
|
AuthPayloads.Hello hello = authenticateBackend(codec);
|
||||||
FrontendTunnel tunnel = new FrontendTunnel(socket, codec, hello.nodeName());
|
FrontendTunnel tunnel = new FrontendTunnel(socket, codec, hello.nodeName());
|
||||||
FrontendTunnel previous = activeTunnel.getAndSet(tunnel);
|
FrontendTunnel previous = activeTunnels.put(hello.nodeName(), tunnel);
|
||||||
if (previous != null) {
|
if (previous != null) {
|
||||||
previous.close("replaced by a new authenticated backend tunnel");
|
previous.close("replaced by a new authenticated backend tunnel");
|
||||||
}
|
}
|
||||||
@@ -186,10 +193,10 @@ public final class FrontendAgent implements ManagedAgent {
|
|||||||
playerSocket.setTcpNoDelay(true);
|
playerSocket.setTcpNoDelay(true);
|
||||||
playerSocket.setKeepAlive(true);
|
playerSocket.setKeepAlive(true);
|
||||||
|
|
||||||
FrontendTunnel tunnel = activeTunnel.get();
|
FrontendTunnel tunnel = selectTunnelForRoute(route);
|
||||||
if (tunnel == null || !tunnel.isOpen()) {
|
if (tunnel == null || !tunnel.isOpen()) {
|
||||||
LOGGER.warning(() -> "Rejecting player connection for route " + route.name()
|
LOGGER.warning(() -> "Rejecting player connection for route " + route.name()
|
||||||
+ " because no backend tunnel is authenticated");
|
+ " because no matching backend tunnel is authenticated");
|
||||||
closeQuietly(playerSocket);
|
closeQuietly(playerSocket);
|
||||||
continue;
|
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) {
|
private void clearActiveTunnel(FrontendTunnel tunnel) {
|
||||||
activeTunnel.compareAndSet(tunnel, null);
|
activeTunnels.remove(tunnel.backendNodeName, tunnel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void closeQuietly(ServerSocket serverSocket) {
|
private static void closeQuietly(ServerSocket serverSocket) {
|
||||||
|
|||||||
@@ -62,13 +62,27 @@ final class TlsSocketFactory {
|
|||||||
|
|
||||||
static Socket createClientSocket(AgentConfig config, Consumer<String> learnedFingerprintConsumer)
|
static Socket createClientSocket(AgentConfig config, Consumer<String> learnedFingerprintConsumer)
|
||||||
throws IOException, GeneralSecurityException {
|
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<String> learnedFingerprintConsumer
|
||||||
|
) throws IOException, GeneralSecurityException {
|
||||||
Socket socket;
|
Socket socket;
|
||||||
PinnedCertificateTrustManager pinningTrustManager = null;
|
PinnedCertificateTrustManager pinningTrustManager = null;
|
||||||
|
|
||||||
if (!config.tls().enabled()) {
|
if (!config.tls().enabled()) {
|
||||||
socket = SocketFactory.getDefault().createSocket();
|
socket = SocketFactory.getDefault().createSocket();
|
||||||
} else {
|
} else {
|
||||||
pinningTrustManager = pinningTrustManager(config);
|
pinningTrustManager = pinningTrustManager(config, endpoint);
|
||||||
SSLContext context = sslContext(
|
SSLContext context = sslContext(
|
||||||
config.tls().keyStorePath(),
|
config.tls().keyStorePath(),
|
||||||
config.tls().keyStorePassword(),
|
config.tls().keyStorePassword(),
|
||||||
@@ -82,7 +96,7 @@ final class TlsSocketFactory {
|
|||||||
socket.setTcpNoDelay(true);
|
socket.setTcpNoDelay(true);
|
||||||
socket.setKeepAlive(true);
|
socket.setKeepAlive(true);
|
||||||
socket.connect(
|
socket.connect(
|
||||||
new InetSocketAddress(config.tunnel().connectHost(), config.tunnel().connectPort()),
|
new InetSocketAddress(endpoint.connectHost(), endpoint.connectPort()),
|
||||||
config.tunnel().connectTimeoutMillis()
|
config.tunnel().connectTimeoutMillis()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -117,10 +131,16 @@ final class TlsSocketFactory {
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PinnedCertificateTrustManager pinningTrustManager(AgentConfig config) {
|
private static PinnedCertificateTrustManager pinningTrustManager(
|
||||||
if (!config.tls().pinnedCertificateSha256().isBlank() || config.tls().trustOnFirstUse()) {
|
AgentConfig config,
|
||||||
|
AgentConfig.FrontendEndpointConfig endpoint
|
||||||
|
) {
|
||||||
|
String pinnedCertificate = endpoint.pinnedCertificateSha256().isBlank()
|
||||||
|
? config.tls().pinnedCertificateSha256()
|
||||||
|
: endpoint.pinnedCertificateSha256();
|
||||||
|
if (!pinnedCertificate.isBlank() || config.tls().trustOnFirstUse()) {
|
||||||
return new PinnedCertificateTrustManager(
|
return new PinnedCertificateTrustManager(
|
||||||
config.tls().pinnedCertificateSha256(),
|
pinnedCertificate,
|
||||||
config.tls().trustOnFirstUse()
|
config.tls().trustOnFirstUse()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ public record TunnelStatus(
|
|||||||
String nodeName,
|
String nodeName,
|
||||||
boolean running,
|
boolean running,
|
||||||
boolean tunnelConnected,
|
boolean tunnelConnected,
|
||||||
|
int connectedTunnels,
|
||||||
int activeStreams,
|
int activeStreams,
|
||||||
boolean tlsEnabled,
|
boolean tlsEnabled,
|
||||||
String lastEvent
|
String lastEvent
|
||||||
) {
|
) {
|
||||||
public static TunnelStatus stopped(AgentConfig.Role role, String nodeName, 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# DirtSimpleP2P Bungee/Waterfall frontend config.
|
# DirtSimpleP2P Bungee/Waterfall frontend config.
|
||||||
# Put this same jar in the Bungee plugins folder.
|
# Put this same jar in the Bungee plugins folder.
|
||||||
# Bungee should point its backend server entry at route.minecraft.frontendBindHost:route.minecraft.frontendBindPort.
|
# 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
|
role=frontend
|
||||||
node.name=bungee-frontend
|
node.name=bungee-frontend
|
||||||
@@ -31,3 +32,13 @@ tunnel.tls.requireClientAuth=false
|
|||||||
routes=minecraft
|
routes=minecraft
|
||||||
route.minecraft.frontendBindHost=127.0.0.1
|
route.minecraft.frontendBindHost=127.0.0.1
|
||||||
route.minecraft.frontendBindPort=25566
|
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
|
||||||
|
|||||||
@@ -5,8 +5,20 @@
|
|||||||
role=backend
|
role=backend
|
||||||
node.name=paper-backend
|
node.name=paper-backend
|
||||||
|
|
||||||
tunnel.connectHost=YOUR_BUNGEE_OR_VPS_IP
|
frontends=proxy1
|
||||||
tunnel.connectPort=24445
|
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.
|
# Replaced automatically on first start. Paste the same value from the Bungee config here.
|
||||||
tunnel.authToken=AUTO_GENERATED_ON_FIRST_START
|
tunnel.authToken=AUTO_GENERATED_ON_FIRST_START
|
||||||
|
|
||||||
@@ -24,7 +36,6 @@ tunnel.reconnectMaxMillis=10000
|
|||||||
tunnel.tls.enabled=false
|
tunnel.tls.enabled=false
|
||||||
tunnel.tls.allowInsecure=true
|
tunnel.tls.allowInsecure=true
|
||||||
tunnel.tls.trustOnFirstUse=true
|
tunnel.tls.trustOnFirstUse=true
|
||||||
tunnel.tls.pinnedCertificateSha256=
|
|
||||||
|
|
||||||
routes=minecraft
|
routes=minecraft
|
||||||
route.minecraft.backendTargetHost=127.0.0.1
|
route.minecraft.backendTargetHost=127.0.0.1
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,7 @@
|
|||||||
# DirtSimpleP2P Bungee/Waterfall frontend config.
|
# DirtSimpleP2P Bungee/Waterfall frontend config.
|
||||||
# Put this same jar in the Bungee plugins folder.
|
# Put this same jar in the Bungee plugins folder.
|
||||||
# Bungee should point its backend server entry at route.minecraft.frontendBindHost:route.minecraft.frontendBindPort.
|
# 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
|
role=frontend
|
||||||
node.name=bungee-frontend
|
node.name=bungee-frontend
|
||||||
@@ -31,3 +32,13 @@ tunnel.tls.requireClientAuth=false
|
|||||||
routes=minecraft
|
routes=minecraft
|
||||||
route.minecraft.frontendBindHost=127.0.0.1
|
route.minecraft.frontendBindHost=127.0.0.1
|
||||||
route.minecraft.frontendBindPort=25566
|
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
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -5,8 +5,20 @@
|
|||||||
role=backend
|
role=backend
|
||||||
node.name=paper-backend
|
node.name=paper-backend
|
||||||
|
|
||||||
tunnel.connectHost=YOUR_BUNGEE_OR_VPS_IP
|
frontends=proxy1
|
||||||
tunnel.connectPort=24445
|
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.
|
# Replaced automatically on first start. Paste the same value from the Bungee config here.
|
||||||
tunnel.authToken=AUTO_GENERATED_ON_FIRST_START
|
tunnel.authToken=AUTO_GENERATED_ON_FIRST_START
|
||||||
|
|
||||||
@@ -24,7 +36,6 @@ tunnel.reconnectMaxMillis=10000
|
|||||||
tunnel.tls.enabled=false
|
tunnel.tls.enabled=false
|
||||||
tunnel.tls.allowInsecure=true
|
tunnel.tls.allowInsecure=true
|
||||||
tunnel.tls.trustOnFirstUse=true
|
tunnel.tls.trustOnFirstUse=true
|
||||||
tunnel.tls.pinnedCertificateSha256=
|
|
||||||
|
|
||||||
routes=minecraft
|
routes=minecraft
|
||||||
route.minecraft.backendTargetHost=127.0.0.1
|
route.minecraft.backendTargetHost=127.0.0.1
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user