Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a212b61
feat: add tunnel transport metadata
robinbraemer Jun 14, 2026
8709759
refactor: isolate websocket tunnel transport
robinbraemer Jun 14, 2026
fdbbbc2
feat: add p2p tunnel header encoder
robinbraemer Jun 14, 2026
324ceca
feat: add optional libp2p tunnel transport
robinbraemer Jun 14, 2026
bc9063e
build: ship libp2p tunnel on java 11
robinbraemer Jun 14, 2026
a4c62e5
fix: bind libp2p tunnel in common module
robinbraemer Jun 15, 2026
8d0a8e5
fix: package netty for embedded libp2p
robinbraemer Jun 15, 2026
999549d
fix: start libp2p host lazily
robinbraemer Jun 15, 2026
7a704ce
Add native libp2p endpoint registration
robinbraemer Jun 15, 2026
505ce6e
Log native libp2p offline mode
robinbraemer Jun 15, 2026
4905296
feat: report native libp2p endpoint status
robinbraemer Jun 15, 2026
d09e07b
fix(connect): reconnect native libp2p registration
robinbraemer Jun 16, 2026
c02d1c9
feat(connect): support native libp2p relay reservations
robinbraemer Jun 16, 2026
af618ca
fix(connect): prefer relay candidates for native endpoints
robinbraemer Jun 16, 2026
844ecb2
fix(connect): authorize native moxy peers
robinbraemer Jun 16, 2026
4771056
fix(connect): sign native records as protobuf
robinbraemer Jun 16, 2026
25e505e
fix(connect): harden native libp2p endpoint lifecycle
robinbraemer Jun 16, 2026
7fc9a4f
fix(connect): half-close native status streams
robinbraemer Jun 16, 2026
85fd69d
docs(connect): note Paper connection throttle
robinbraemer Jun 18, 2026
5ec108e
refactor: finalize connect libp2p endpoint naming
robinbraemer Jun 19, 2026
1e1f816
fix(connect): sign challenged libp2p relay addrs
robinbraemer Jun 19, 2026
c50f191
obs(connect): log libp2p bootstrap peer mismatches
robinbraemer Jun 19, 2026
2edd7aa
fix(connect): tolerate relay bootstrap mismatches
robinbraemer Jun 19, 2026
992f5be
fix(connect): harden libp2p endpoint bootstrap
robinbraemer Jun 19, 2026
ea37e11
fix(connect): avoid stale libp2p relay records
robinbraemer Jun 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,35 @@ low latency edge proxies network nearest to you.

Please refer to https://connect.minekube.com for more documentation.

## Connect libp2p endpoint mode

The Connect libp2p endpoint path is enabled by configuring the Connect edge peer
address. It keeps the normal WatchService path available as fallback while the
endpoint also registers a stable libp2p peer for proxy-initiated session streams.

Environment variables:

- `CONNECT_LIBP2P_EDGE_ADDR`: comma-separated Connect edge libp2p multiaddrs,
each including `/p2p/<connect-edge-peer-id>`. Setting this enables libp2p mode.
- `CONNECT_LIBP2P_LISTEN_ADDR`: optional endpoint listen multiaddrs. The
default is `/ip4/127.0.0.1/tcp/0`.
- `CONNECT_LIBP2P_ADVERTISE_ADDRS`: optional explicit endpoint addresses
to publish instead of the local listen addresses.
- `CONNECT_LIBP2P_RELAY_ADDRS`: optional relay bootstrap multiaddrs. The
endpoint reserves each relay through these addresses. During registration,
the Connect edge can challenge the endpoint to sign equivalent
`/p2p-circuit/p2p/<endpoint-peer-id>` addresses that are better for other
edge proxies to dial, such as private per-machine relay addresses.

## Working setups

When installing the Connect plugin the following platform settings are supported.

- PaperMC/Spigot
- If running in Online mode you must set to `enforce-secure-profile: false` in [server.properties](https://minecraft.fandom.com/wiki/Server.properties)
- For Paper/Spigot endpoints, set `settings.connection-throttle: -1` in `bukkit.yml`.
Connect may retry the backend handshake while detecting the server's forwarding mode, and
Paper's default connection throttle can reject that retry as `Connection throttled!`.
- ✔️️ No forwarding + Online mode
- ✔️ No forwarding + Offline mode
- ✔️ Velocity forwarding + Online/Offline mode
Expand Down
10 changes: 4 additions & 6 deletions build-logic/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,14 @@ repositories {
dependencies {
implementation("net.kyori", "indra-common", "2.0.6")
implementation("org.jfrog.buildinfo", "build-info-extractor-gradle", "4.26.1")
implementation("gradle.plugin.com.github.johnrengelman", "shadow", "7.1.1")
implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.11")
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
}
kotlinOptions.jvmTarget = "11"
}
4 changes: 3 additions & 1 deletion build-logic/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ object Versions {
const val protocVersion = "3.19.4"
const val bstatsVersion = "3.0.2"
const val gsonVersion = "2.8.6"
const val jvmLibp2pVersion = "1.3.2-RELEASE"
const val kotlinStdlibVersion = "1.9.22"

const val checkerQual = "3.19.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ tasks {
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11

withSourcesJar()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

plugins {
id("connect.base-conventions")
id("com.github.johnrengelman.shadow")
id("com.gradleup.shadow")
}

tasks {
Expand Down Expand Up @@ -51,4 +51,4 @@ fun callAddRelocations(configuration: Configuration, shadowJar: ShadowJar) =
configuration.dependencies.forEach {
if (it is ProjectDependency)
addRelocations(it.dependencyProject, shadowJar)
}
}
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ subprojects {
in deployProjects -> plugins.apply("connect.shadow-conventions")
else -> plugins.apply("connect.base-conventions")
}
}
}
2 changes: 2 additions & 0 deletions bungee/src/main/java/com/minekube/connect/BungeePlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.minekube.connect.module.BungeeListenerModule;
import com.minekube.connect.module.BungeePlatformModule;
import com.minekube.connect.module.CommandModule;
import com.minekube.connect.module.Libp2pEndpointModule;
import com.minekube.connect.module.ProxyCommonModule;
import com.minekube.connect.module.WatcherModule;
import com.minekube.connect.util.ReflectionUtils;
Expand Down Expand Up @@ -64,6 +65,7 @@ public void onEnable() {
new CommandModule(),
new BungeeListenerModule(),
// new BungeeAddonModule(), - don't need proxy-side data injection
new Libp2pEndpointModule(),
new WatcherModule()
);
}
Expand Down
11 changes: 5 additions & 6 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ dependencies {
api("org.bstats", "bstats-base", Versions.bstatsVersion)

implementation("com.squareup.okhttp3:okhttp:4.9.3")
implementation("io.libp2p:jvm-libp2p:${Versions.jvmLibp2pVersion}")
api("io.netty", "netty-transport", Versions.nettyVersion)
api("io.netty", "netty-codec", Versions.nettyVersion)
api("io.netty", "netty-transport-native-unix-common", Versions.nettyVersion)
implementation("org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlinStdlibVersion}")
runtimeOnly("io.grpc", "grpc-netty-shaded", Versions.gRPCVersion)
implementation("io.grpc", "grpc-protobuf", Versions.gRPCVersion)
implementation("io.grpc", "grpc-stub", Versions.gRPCVersion)
implementation("javax.annotation", "javax.annotation-api", "1.3.2")

// Test deps — pinned to versions still compatible with the Java 8 source target.
testImplementation("org.junit.jupiter:junit-jupiter:5.10.5")
testImplementation("org.mockito:mockito-core:4.11.0")
testImplementation("org.awaitility:awaitility:4.2.2")
Expand All @@ -35,11 +39,6 @@ tasks.test {
useJUnitPlatform()
}

// present on all platforms
provided("io.netty", "netty-transport", Versions.nettyVersion)
provided("io.netty", "netty-codec", Versions.nettyVersion)
provided("io.netty", "netty-transport-native-unix-common", Versions.nettyVersion)

relocate("org.bstats")

configure<BlossomExtension> {
Expand Down
6 changes: 6 additions & 0 deletions core/src/main/java/com/minekube/connect/ConnectPlatform.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.ConfigurationException;
import com.google.inject.name.Named;
import com.minekube.connect.api.ConnectApi;
import com.minekube.connect.api.InstanceHolder;
Expand All @@ -42,6 +43,7 @@
import com.minekube.connect.module.PostInitializeModule;
import com.minekube.connect.register.WatcherRegister;
import com.minekube.connect.tunnel.Tunneler;
import com.minekube.connect.tunnel.p2p.Libp2pEndpoint;
import com.minekube.connect.util.Metrics;
import com.minekube.connect.util.UpdateChecker;
import java.io.IOException;
Expand Down Expand Up @@ -135,6 +137,10 @@ public boolean enable(Module... postInitializeModules) {
}

public boolean disable() {
try {
guice.getInstance(Libp2pEndpoint.class).stop();
} catch (ConfigurationException ignored) {
}
guice.getInstance(WatcherRegister.class).stop();
guice.getInstance(Tunneler.class).close();
guice.getInstance(CommonPlatformInjector.class).shutdown();
Expand Down
31 changes: 22 additions & 9 deletions core/src/main/java/com/minekube/connect/module/CommonModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.name.Named;
import com.minekube.connect.api.ConnectApi;
import com.minekube.connect.api.SimpleConnectApi;
Expand All @@ -45,6 +46,9 @@
import com.minekube.connect.inject.CommonPlatformInjector;
import com.minekube.connect.packet.PacketHandlersImpl;
import com.minekube.connect.platform.util.PlatformUtils;
import com.minekube.connect.tunnel.TunnelClientTransport;
import com.minekube.connect.tunnel.WebSocketTunnelTransport;
import com.minekube.connect.tunnel.p2p.Libp2pTunnelTransport;
import com.minekube.connect.util.Constants;
import com.minekube.connect.util.HttpUtils;
import com.minekube.connect.util.LanguageManager;
Expand All @@ -71,6 +75,10 @@ protected void configure() {

bind(PacketHandlers.class).to(PacketHandlersImpl.class);
bind(PacketHandlersImpl.class).asEagerSingleton();
Multibinder<TunnelClientTransport> transports =
Multibinder.newSetBinder(binder(), TunnelClientTransport.class);
transports.addBinding().to(WebSocketTunnelTransport.class);
transports.addBinding().to(Libp2pTunnelTransport.class);
}

@Provides
Expand Down Expand Up @@ -114,24 +122,29 @@ public OkHttpClient defaultOkHttpClient() {

@Provides
@Singleton
@Named("connectHttpClient")
public OkHttpClient connectOkHttpClient(
@Named("defaultHttpClient") OkHttpClient defaultOkHttpClient,
PlatformUtils platformUtils,
@Named("platformName") String implementationName,
ConnectApi api
) throws IOException {
@Named("connectToken")
public String connectToken() throws IOException {
Path tokenFile = dataDirectory.resolve("token.json");

Optional<String> token = Token.load(tokenFile);
if (!token.isPresent()) {
// Generate and save new token
String t = Token.generate();
Token.save(tokenFile, t);
token = Optional.of(t);
}
final String apiToken = token.get();
return token.get();
}

@Provides
@Singleton
@Named("connectHttpClient")
public OkHttpClient connectOkHttpClient(
@Named("defaultHttpClient") OkHttpClient defaultOkHttpClient,
PlatformUtils platformUtils,
@Named("platformName") String implementationName,
ConnectApi api,
@Named("connectToken") String apiToken
) {
return defaultOkHttpClient.newBuilder()
.addInterceptor(chain -> chain.proceed(chain.request().newBuilder()
// Add authorization token to every request
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2021-2022 Minekube. https://minekube.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* @author Minekube
* @link https://github.com/minekube/connect-java
*/

package com.minekube.connect.module;

import com.google.inject.AbstractModule;
import com.minekube.connect.tunnel.p2p.Libp2pEndpoint;

public final class Libp2pEndpointModule extends AbstractModule {

@Override
protected void configure() {
bind(Libp2pEndpoint.class).asEagerSingleton();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,18 @@ private String tunnelSvcAddr() {
@Override
public void channelActive(@NotNull ChannelHandlerContext ctx) throws Exception {
// Start tunnel from downstream server -> upstream TunnelService
tunnelConn = tunneler.tunnel(
tunnelSvcAddr(),
context.getSessionProposal().getSession().getId(),
new TunnelHandler(logger, ctx.channel())
);
if (FORCE_TUNNEL_SERVICE_ADDR != null && !FORCE_TUNNEL_SERVICE_ADDR.isEmpty()) {
tunnelConn = tunneler.tunnel(
tunnelSvcAddr(),
context.getSessionProposal().getSession().getId(),
new TunnelHandler(logger, ctx.channel())
);
} else {
tunnelConn = tunneler.tunnel(
context.getSessionProposal().getSession(),
new TunnelHandler(logger, ctx.channel())
);
}
context.tunnelConn.set(tunnelConn);
super.channelActive(ctx);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ public void onOpen() {

@Override
public void onProposal(SessionProposal proposal) {
if (proposal.getSession().getTunnelServiceAddr().isEmpty()) {
if (proposal.getSession().getTunnelServiceAddr().isEmpty()
&& proposal.getSession().getTunnelTransportsCount() == 0) {
logger.info("Got session proposal with empty tunnel service address " +
"from WatchService, rejecting it");
proposal.reject(Status.newBuilder()
Expand All @@ -188,6 +189,7 @@ public void onProposal(SessionProposal proposal) {
return;
}

tunneler.prepare(proposal.getSession());
new LocalSession(logger, api, tunneler,
platformInjector.getServerSocketAddress(),
proposal
Expand Down
Loading
Loading