first commit

This commit is contained in:
2026-06-21 12:44:51 -04:00
commit 640d4f45f2
86 changed files with 3216 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>me.proxylink</groupId>
<artifactId>dirtsimplep2p</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>proxylink-common</artifactId>
<packaging>jar</packaging>
</project>
@@ -0,0 +1,206 @@
package me.proxylink.common;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Pattern;
public record AgentConfig(
Role role,
String nodeName,
TunnelConfig tunnel,
TlsConfig tls,
List<RouteConfig> routes
) {
private static final Pattern ROUTE_NAME = Pattern.compile("[A-Za-z0-9._-]+");
public AgentConfig {
Objects.requireNonNull(role, "role");
nodeName = requireNonBlank(nodeName, "nodeName");
Objects.requireNonNull(tunnel, "tunnel");
Objects.requireNonNull(tls, "tls");
routes = List.copyOf(Objects.requireNonNull(routes, "routes"));
}
public void validateOrThrow() {
List<String> errors = new ArrayList<>();
if (routes.isEmpty()) {
errors.add("At least one route is required");
}
if (tunnel.authToken().length() < 32) {
errors.add("tunnel.authToken must be at least 32 characters");
}
if (isPlaceholderSecret(tunnel.authToken())) {
errors.add("tunnel.authToken is still the default placeholder and must be changed");
}
if (!tls.enabled() && !tunnel.allowInsecure()) {
errors.add("TLS is disabled, so tunnel.tls.allowInsecure must be true for explicit local testing");
}
if (role == Role.FRONTEND) {
validateHostPort(errors, "tunnel.listen", tunnel.listenHost(), tunnel.listenPort());
if (tls.enabled() && !tls.autoGenerate()) {
requirePath(errors, "tunnel.tls.keyStore", tls.keyStorePath());
requireSecret(errors, "tunnel.tls.keyStorePassword", tls.keyStorePassword());
}
}
if (role == Role.BACKEND) {
validateHostPort(errors, "tunnel.connect", tunnel.connectHost(), tunnel.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) {
requireSecret(errors, "tunnel.tls.trustStorePassword", tls.trustStorePassword());
}
if (tls.requireClientAuth()) {
requirePath(errors, "tunnel.tls.keyStore", tls.keyStorePath());
requireSecret(errors, "tunnel.tls.keyStorePassword", tls.keyStorePassword());
}
}
}
if (tunnel.connectTimeoutMillis() < 1000) {
errors.add("tunnel.connectTimeoutMillis must be at least 1000");
}
if (tunnel.heartbeatIntervalMillis() < 1000) {
errors.add("tunnel.heartbeatIntervalMillis must be at least 1000");
}
if (tunnel.heartbeatTimeoutMillis() <= tunnel.heartbeatIntervalMillis()) {
errors.add("tunnel.heartbeatTimeoutMillis must be greater than tunnel.heartbeatIntervalMillis");
}
if (tunnel.heartbeatMissesBeforeDisconnect() < 2) {
errors.add("tunnel.heartbeatMissesBeforeDisconnect must be at least 2");
}
if (tunnel.reconnectInitialMillis() < 250) {
errors.add("tunnel.reconnectInitialMillis must be at least 250");
}
if (tunnel.reconnectMaxMillis() < tunnel.reconnectInitialMillis()) {
errors.add("tunnel.reconnectMaxMillis must be greater than or equal to tunnel.reconnectInitialMillis");
}
for (RouteConfig route : routes) {
if (!ROUTE_NAME.matcher(route.name()).matches()) {
errors.add("Route name contains invalid characters: " + route.name());
}
if (role == Role.FRONTEND) {
validateHostPort(errors, "route." + route.name() + ".frontendBind", route.frontendBindHost(), route.frontendBindPort());
}
if (role == Role.BACKEND) {
validateHostPort(errors, "route." + route.name() + ".backendTarget", route.backendTargetHost(), route.backendTargetPort());
}
}
if (!errors.isEmpty()) {
throw new IllegalArgumentException("Invalid configuration:\n - " + String.join("\n - ", errors));
}
}
public RouteConfig requireRoute(String routeName) throws ProtocolException {
for (RouteConfig route : routes) {
if (route.name().equals(routeName)) {
return route;
}
}
throw new ProtocolException("Unknown route: " + routeName);
}
private static void validateHostPort(List<String> errors, String keyPrefix, String host, int port) {
if (host == null || host.isBlank()) {
errors.add(keyPrefix + "Host must not be blank");
}
if (port < 1 || port > 65535) {
errors.add(keyPrefix + "Port must be between 1 and 65535");
}
}
private static void requirePath(List<String> errors, String key, Path path) {
if (path == null) {
errors.add(key + " is required when TLS is enabled");
}
}
private static void requireSecret(List<String> errors, String key, String value) {
if (value == null || value.isBlank()) {
errors.add(key + " is required when TLS is enabled");
}
}
private static String requireNonBlank(String value, String name) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(name + " must not be blank");
}
return value;
}
private static boolean isPlaceholderSecret(String value) {
String normalized = value.toLowerCase(Locale.ROOT);
return normalized.contains("change_me")
|| normalized.contains("replace-with")
|| normalized.contains("placeholder");
}
public enum Role {
FRONTEND,
BACKEND;
public static Role parse(String value) {
return Role.valueOf(value.trim().toUpperCase(Locale.ROOT));
}
}
public record TunnelConfig(
String listenHost,
int listenPort,
String connectHost,
int connectPort,
String authToken,
boolean allowInsecure,
int connectTimeoutMillis,
long heartbeatIntervalMillis,
long heartbeatTimeoutMillis,
int heartbeatMissesBeforeDisconnect,
long reconnectInitialMillis,
long reconnectMaxMillis
) {
public TunnelConfig {
authToken = requireNonBlank(authToken, "authToken");
}
}
public record TlsConfig(
boolean enabled,
Path keyStorePath,
String keyStorePassword,
Path trustStorePath,
String trustStorePassword,
boolean requireClientAuth,
boolean autoGenerate,
boolean trustOnFirstUse,
String pinnedCertificateSha256
) {
public TlsConfig {
keyStorePassword = keyStorePassword == null ? "" : keyStorePassword;
trustStorePassword = trustStorePassword == null ? "" : trustStorePassword;
pinnedCertificateSha256 = pinnedCertificateSha256 == null ? "" : pinnedCertificateSha256.trim();
}
}
public record RouteConfig(
String name,
String frontendBindHost,
int frontendBindPort,
String backendTargetHost,
int backendTargetPort
) {
public RouteConfig {
name = requireNonBlank(name, "name");
}
}
}
@@ -0,0 +1,141 @@
package me.proxylink.common;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
public final class AgentConfigIO {
private AgentConfigIO() {
}
public static AgentConfig load(Path path) throws IOException {
Properties properties = new Properties();
try (InputStream input = Files.newInputStream(path)) {
properties.load(input);
}
Path baseDir = path.toAbsolutePath().getParent();
return fromProperties(properties, baseDir);
}
public static AgentConfig fromProperties(Properties properties) {
return fromProperties(properties, null);
}
public static AgentConfig fromProperties(Properties properties, Path baseDir) {
AgentConfig.Role role = AgentConfig.Role.parse(required(properties, "role"));
String nodeName = value(properties, "node.name", role.name().toLowerCase());
AgentConfig.TunnelConfig tunnel = new AgentConfig.TunnelConfig(
value(properties, "tunnel.listenHost", "0.0.0.0"),
intValue(properties, "tunnel.listenPort", -1),
value(properties, "tunnel.connectHost", ""),
intValue(properties, "tunnel.connectPort", -1),
required(properties, "tunnel.authToken"),
boolValue(properties, "tunnel.tls.allowInsecure", false),
intValue(properties, "tunnel.connectTimeoutMillis", 5000),
longValue(properties, "tunnel.heartbeatIntervalMillis", 2000L),
longValue(properties, "tunnel.heartbeatTimeoutMillis", 8000L),
intValue(properties, "tunnel.heartbeatMissesBeforeDisconnect", 4),
longValue(properties, "tunnel.reconnectInitialMillis", 250L),
longValue(properties, "tunnel.reconnectMaxMillis", 10000L)
);
AgentConfig.TlsConfig tls = new AgentConfig.TlsConfig(
boolValue(properties, "tunnel.tls.enabled", true),
pathValue(properties, "tunnel.tls.keyStore", baseDir),
value(properties, "tunnel.tls.keyStorePassword", ""),
pathValue(properties, "tunnel.tls.trustStore", baseDir),
value(properties, "tunnel.tls.trustStorePassword", ""),
boolValue(properties, "tunnel.tls.requireClientAuth", false),
boolValue(properties, "tunnel.tls.autoGenerate", true),
boolValue(properties, "tunnel.tls.trustOnFirstUse", true),
value(properties, "tunnel.tls.pinnedCertificateSha256", "")
);
return new AgentConfig(role, nodeName, tunnel, tls, routes(properties));
}
private static List<AgentConfig.RouteConfig> routes(Properties properties) {
String routesValue = required(properties, "routes");
List<AgentConfig.RouteConfig> routes = new ArrayList<>();
for (String rawName : routesValue.split(",")) {
String name = rawName.trim();
if (name.isEmpty()) {
continue;
}
String prefix = "route." + name + ".";
routes.add(new AgentConfig.RouteConfig(
name,
value(properties, prefix + "frontendBindHost", "127.0.0.1"),
intValue(properties, prefix + "frontendBindPort", -1),
value(properties, prefix + "backendTargetHost", "127.0.0.1"),
intValue(properties, prefix + "backendTargetPort", -1)
));
}
return routes;
}
private static String required(Properties properties, String key) {
String value = properties.getProperty(key);
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Missing required config key: " + key);
}
return value.trim();
}
private static String value(Properties properties, String key, String defaultValue) {
String value = properties.getProperty(key);
if (value == null) {
return defaultValue;
}
return value.trim();
}
private static int intValue(Properties properties, String key, int defaultValue) {
String value = properties.getProperty(key);
if (value == null || value.isBlank()) {
return defaultValue;
}
try {
return Integer.parseInt(value.trim());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Config key must be an integer: " + key, e);
}
}
private static long longValue(Properties properties, String key, long defaultValue) {
String value = properties.getProperty(key);
if (value == null || value.isBlank()) {
return defaultValue;
}
try {
return Long.parseLong(value.trim());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Config key must be a long integer: " + key, e);
}
}
private static boolean boolValue(Properties properties, String key, boolean defaultValue) {
String value = properties.getProperty(key);
if (value == null || value.isBlank()) {
return defaultValue;
}
return Boolean.parseBoolean(value.trim());
}
private static Path pathValue(Properties properties, String key, Path baseDir) {
String value = properties.getProperty(key);
if (value == null || value.isBlank()) {
return null;
}
Path path = Path.of(value.trim());
if (!path.isAbsolute() && baseDir != null) {
return baseDir.resolve(path).normalize();
}
return path;
}
}
@@ -0,0 +1,144 @@
package me.proxylink.common;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Objects;
public final class AuthPayloads {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private AuthPayloads() {
}
public record Hello(int protocolVersion, String role, String nodeName, byte[] clientNonce) {
public Hello {
Objects.requireNonNull(role, "role");
Objects.requireNonNull(nodeName, "nodeName");
clientNonce = copyNonce(clientNonce, "clientNonce");
}
@Override
public byte[] clientNonce() {
return Arrays.copyOf(clientNonce, clientNonce.length);
}
}
public record Challenge(byte[] serverNonce) {
public Challenge {
serverNonce = copyNonce(serverNonce, "serverNonce");
}
@Override
public byte[] serverNonce() {
return Arrays.copyOf(serverNonce, serverNonce.length);
}
}
public record Response(byte[] hmac) {
public Response {
if (hmac == null || hmac.length != ProtocolConstants.AUTH_RESPONSE_BYTES) {
throw new IllegalArgumentException("hmac must be " + ProtocolConstants.AUTH_RESPONSE_BYTES + " bytes");
}
hmac = Arrays.copyOf(hmac, hmac.length);
}
@Override
public byte[] hmac() {
return Arrays.copyOf(hmac, hmac.length);
}
}
public static byte[] randomNonce() {
byte[] nonce = new byte[ProtocolConstants.AUTH_NONCE_BYTES];
SECURE_RANDOM.nextBytes(nonce);
return nonce;
}
public static byte[] encodeHello(Hello hello) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(bytes);
output.writeInt(hello.protocolVersion());
output.writeUTF(hello.role());
output.writeUTF(hello.nodeName());
output.write(hello.clientNonce());
output.flush();
return bytes.toByteArray();
}
public static Hello decodeHello(byte[] payload) throws IOException {
DataInputStream input = new DataInputStream(new ByteArrayInputStream(payload));
int protocolVersion = input.readInt();
String role = input.readUTF();
String nodeName = input.readUTF();
byte[] clientNonce = input.readNBytes(ProtocolConstants.AUTH_NONCE_BYTES);
if (clientNonce.length != ProtocolConstants.AUTH_NONCE_BYTES || input.available() != 0) {
throw new ProtocolException("Invalid HELLO payload");
}
return new Hello(protocolVersion, role, nodeName, clientNonce);
}
public static byte[] encodeChallenge(Challenge challenge) throws IOException {
return challenge.serverNonce();
}
public static Challenge decodeChallenge(byte[] payload) throws IOException {
if (payload.length != ProtocolConstants.AUTH_NONCE_BYTES) {
throw new ProtocolException("Invalid AUTH_CHALLENGE payload");
}
return new Challenge(payload);
}
public static byte[] encodeResponse(Response response) {
return response.hmac();
}
public static Response decodeResponse(byte[] payload) throws IOException {
if (payload.length != ProtocolConstants.AUTH_RESPONSE_BYTES) {
throw new ProtocolException("Invalid AUTH_RESPONSE payload");
}
return new Response(payload);
}
public static byte[] computeResponse(String authToken, Hello hello, Challenge challenge) throws ProtocolException {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(authToken.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
updateUtf8(mac, ProtocolConstants.AUTH_CONTEXT);
updateUtf8(mac, hello.role());
updateUtf8(mac, hello.nodeName());
mac.update(hello.clientNonce());
mac.update(challenge.serverNonce());
return mac.doFinal();
} catch (GeneralSecurityException e) {
throw new ProtocolException("Unable to compute authentication response", e);
}
}
public static boolean constantTimeEquals(byte[] expected, byte[] actual) {
return MessageDigest.isEqual(expected, actual);
}
private static void updateUtf8(Mac mac, String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
mac.update((byte) ((bytes.length >>> 8) & 0xff));
mac.update((byte) (bytes.length & 0xff));
mac.update(bytes);
}
private static byte[] copyNonce(byte[] nonce, String name) {
if (nonce == null || nonce.length != ProtocolConstants.AUTH_NONCE_BYTES) {
throw new IllegalArgumentException(name + " must be " + ProtocolConstants.AUTH_NONCE_BYTES + " bytes");
}
return Arrays.copyOf(nonce, nonce.length);
}
}
@@ -0,0 +1,38 @@
package me.proxylink.common;
import java.util.Arrays;
import java.util.Objects;
public final class Frame {
private static final byte[] EMPTY = new byte[0];
private final FrameType type;
private final long streamId;
private final byte[] payload;
public Frame(FrameType type, long streamId, byte[] payload) {
this.type = Objects.requireNonNull(type, "type");
this.streamId = streamId;
this.payload = payload == null || payload.length == 0 ? EMPTY : Arrays.copyOf(payload, payload.length);
}
public static Frame empty(FrameType type, long streamId) {
return new Frame(type, streamId, EMPTY);
}
public FrameType type() {
return type;
}
public long streamId() {
return streamId;
}
public byte[] payload() {
return payload.length == 0 ? EMPTY : Arrays.copyOf(payload, payload.length);
}
public int payloadLength() {
return payload.length;
}
}
@@ -0,0 +1,70 @@
package me.proxylink.common;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public final class FrameCodec {
private final DataInputStream input;
private final DataOutputStream output;
public FrameCodec(InputStream input, OutputStream output) {
this.input = new DataInputStream(input);
this.output = new DataOutputStream(output);
}
public Frame readFrame() throws IOException {
int magic;
try {
magic = input.readInt();
} catch (EOFException eof) {
return null;
}
if (magic != ProtocolConstants.MAGIC) {
throw new ProtocolException("Invalid frame magic");
}
int version = Short.toUnsignedInt(input.readShort());
if (version != ProtocolConstants.PROTOCOL_VERSION) {
throw new ProtocolException("Unsupported protocol version: " + version);
}
int typeCode = Byte.toUnsignedInt(input.readByte());
input.readByte();
long streamId = input.readLong();
int payloadLength = input.readInt();
if (payloadLength < 0 || payloadLength > ProtocolConstants.MAX_FRAME_PAYLOAD_BYTES) {
throw new ProtocolException("Invalid frame payload length: " + payloadLength);
}
byte[] payload = input.readNBytes(payloadLength);
if (payload.length != payloadLength) {
throw new EOFException("Unexpected end of stream while reading frame payload");
}
return new Frame(FrameType.fromCode(typeCode), streamId, payload);
}
public void writeFrame(Frame frame) throws IOException {
byte[] payload = frame.payload();
if (payload.length > ProtocolConstants.MAX_FRAME_PAYLOAD_BYTES) {
throw new ProtocolException("Frame payload is too large: " + payload.length);
}
synchronized (output) {
output.writeInt(ProtocolConstants.MAGIC);
output.writeShort(ProtocolConstants.PROTOCOL_VERSION);
output.writeByte(frame.type().code());
output.writeByte(0);
output.writeLong(frame.streamId());
output.writeInt(payload.length);
output.write(payload);
output.flush();
}
}
}
@@ -0,0 +1,46 @@
package me.proxylink.common;
import java.util.HashMap;
import java.util.Map;
public enum FrameType {
HELLO(1),
AUTH_CHALLENGE(2),
AUTH_RESPONSE(3),
AUTH_OK(4),
AUTH_FAILED(5),
PING(6),
PONG(7),
STREAM_OPEN(10),
STREAM_DATA(11),
STREAM_CLOSE(12),
STREAM_RESET(13),
ERROR(20),
GOAWAY(21);
private static final Map<Integer, FrameType> BY_CODE = new HashMap<>();
static {
for (FrameType type : values()) {
BY_CODE.put(type.code, type);
}
}
private final int code;
FrameType(int code) {
this.code = code;
}
public int code() {
return code;
}
public static FrameType fromCode(int code) throws ProtocolException {
FrameType type = BY_CODE.get(code);
if (type == null) {
throw new ProtocolException("Unknown frame type: " + code);
}
return type;
}
}
@@ -0,0 +1,14 @@
package me.proxylink.common;
public final class ProtocolConstants {
public static final int MAGIC = 0x44535032;
public static final int PROTOCOL_VERSION = 1;
public static final int MAX_FRAME_PAYLOAD_BYTES = 1024 * 1024;
public static final int STREAM_DATA_CHUNK_BYTES = 16 * 1024;
public static final int AUTH_NONCE_BYTES = 32;
public static final int AUTH_RESPONSE_BYTES = 32;
public static final String AUTH_CONTEXT = "DirtSimpleP2P ProxyLink Agent Auth v1";
private ProtocolConstants() {
}
}
@@ -0,0 +1,13 @@
package me.proxylink.common;
import java.io.IOException;
public class ProtocolException extends IOException {
public ProtocolException(String message) {
super(message);
}
public ProtocolException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,34 @@
package me.proxylink.common;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Objects;
public record StreamOpenPayload(String routeName) {
public StreamOpenPayload {
Objects.requireNonNull(routeName, "routeName");
if (routeName.isBlank()) {
throw new IllegalArgumentException("routeName must not be blank");
}
}
public byte[] encode() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(bytes);
output.writeUTF(routeName);
output.flush();
return bytes.toByteArray();
}
public static StreamOpenPayload decode(byte[] payload) throws IOException {
DataInputStream input = new DataInputStream(new ByteArrayInputStream(payload));
String routeName = input.readUTF();
if (input.available() != 0) {
throw new ProtocolException("Invalid STREAM_OPEN payload");
}
return new StreamOpenPayload(routeName);
}
}
@@ -0,0 +1,3 @@
artifactId=proxylink-common
groupId=me.proxylink
version=0.1.0-SNAPSHOT
@@ -0,0 +1,16 @@
me/proxylink/common/AgentConfig$RouteConfig.class
me/proxylink/common/AgentConfig.class
me/proxylink/common/AgentConfigIO.class
me/proxylink/common/AuthPayloads$Response.class
me/proxylink/common/FrameType.class
me/proxylink/common/AuthPayloads$Challenge.class
me/proxylink/common/ProtocolConstants.class
me/proxylink/common/FrameCodec.class
me/proxylink/common/AuthPayloads$Hello.class
me/proxylink/common/AgentConfig$TunnelConfig.class
me/proxylink/common/AgentConfig$Role.class
me/proxylink/common/StreamOpenPayload.class
me/proxylink/common/AuthPayloads.class
me/proxylink/common/AgentConfig$TlsConfig.class
me/proxylink/common/ProtocolException.class
me/proxylink/common/Frame.class
@@ -0,0 +1,9 @@
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/AgentConfig.java
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/AgentConfigIO.java
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/AuthPayloads.java
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/Frame.java
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/FrameCodec.java
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/FrameType.java
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/ProtocolConstants.java
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/ProtocolException.java
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/StreamOpenPayload.java
Binary file not shown.