Added multi server support

This commit is contained in:
2026-06-23 18:15:37 -04:00
parent 640d4f45f2
commit 4414dce4e4
32 changed files with 543 additions and 207 deletions
@@ -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<FrontendEndpointConfig> frontends,
List<RouteConfig> 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<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.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<String> routeNames = new HashSet<>();
Set<String> 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();
}
}
}
@@ -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<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) {
@@ -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)
));
@@ -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
Binary file not shown.