Building a Bidirectional Bedrock Version Proxy with Cloudburst Protocol

Answered using AI by Vibe Coding

Code Question:

https://vibecoding-ai.net/ 1000 символов за раз Я хочу написать свой собственный AnyProxy проект на подобии ViaProxy (ViaVersion) для поддержки захода со старых версий клиента на сервера с последними версиями, и наоборот с новых версий клиента на сервера со старыми версиями, AnyProxy\src\main\java\ru\vbproduction667\anyproxy\AnyProxy.java путь к основному файлу в моём проекте, хочу использовать https://github.com/CloudburstMC/Protocol где есть уже поддержка протоколов от 291 версия 1.7 до последней версии 859 1.21.124, пример импорта кодеков import org.cloudburstmc.protocol.bedrock.codec.v859.Bedrock_v859;, https://github.com/CloudburstMC/Network RakNet составляющая используемая в https://github.com/CloudburstMC/ProxyPass, маппинги возьму из репозиториев Nukkit, GeyserMC и PocketMine-MP, важно не используй устаревшие пути com.nukkitx. они уже давно заменены на org.cloudburstmc., AnyProxy\src\main\java\ru\vbproduction667\anyproxy\network\protocol мой кастомный путь, напиши все нужные и

Code Solution:

Отличная идея для проекта! Создание такого AnyProxy — амбициозная, но очень полезная задача. Ниже я постараюсь максимально полно ответить на ваш запрос, учитывая все указанные вами детали.

Структура проекта и основные классы:

Ваш файл AnyProxy.java будет точкой входа. В нем необходимо инициализировать прокси-сервер, который будет слушать входящие соединения от клиентов и устанавливать исходящие соединения с целевыми серверами.

// AnyProxy/src/main/java/ru/vbproduction667/anyproxy/AnyProxy.java
package ru.vbproduction667.anyproxy;

import lombok.extern.slf4j.Slf4j;
import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec;
import org.cloudburstmc.protocol.bedrock.codec.v859.Bedrock_v859; // Пример импорта кодека
import org.cloudburstmc.protocol.bedrock.netty.BedrockCipher;
import org.cloudburstmc.protocol.bedrock.netty.BedrockRakNetServerInitializer;
import org.cloudburstmc.protocol.bedrock.netty.BedrockServerChannel;
import org.cloudburstmc.protocol.bedrock.netty.codec.frame.BedrockFrameDecoder;
import org.cloudburstmc.protocol.bedrock.netty.codec.frame.BedrockFrameEncoder;
import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketDecoder;
import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketEncoder;
import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
import org.cloudburstmc.protocol.bedrock.util.BedrockVersion;
import org.cloudburstmc.raknet.RakNetServer;
import org.cloudburstmc.raknet.pipeline.RakNetDatagramCodec;
import org.cloudburstmc.raknet.pipeline.RakNetPacketCodec;
import org.cloudburstmc.raknet.protocol.RakNetPacket;

import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.nio.NioDatagramChannel;

import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j // Использование Lombok для логирования
public class AnyProxy {

    private final String bindAddress;
    private final int bindPort;
    private final String remoteAddress;
    private final int remotePort;

    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;

    // Маппинг для хранения активных прокси-сессий (клиент-сервер)
    private final Map<InetSocketAddress, ProxySession> activeSessions = new ConcurrentHashMap<>();

    public AnyProxy(String bindAddress, int bindPort, String remoteAddress, int remotePort) {
        this.bindAddress = bindAddress;
        this.bindPort = bindPort;
        this.remoteAddress = remoteAddress;
        this.remotePort = remotePort;
    }

    public void start() throws InterruptedException {
        bossGroup = new NioEventLoopGroup();
        workerGroup = new NioEventLoopGroup();

        try {
            // Создаем RakNetServer для прослушивания входящих соединений от клиентов
            RakNetServer rakNetServer = RakNetServer.builder()
                    .address(new InetSocketAddress(bindAddress, bindPort))
                    .eventLoopGroup(bossGroup) // Используем bossGroup для RakNet
                    .maximumConnections(Integer.MAX_VALUE)
                    .build();

            // Создаем Netty ServerBootstrap для обработки Bedrock протокола
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup) // bossGroup для принятия соединений, workerGroup для обработки
                    .channel(NioDatagramChannel.class) // UDP канал для Bedrock
                    .option(ChannelOption.SO_BROADCAST, true)
                    .handler(rakNetServer.getInitializer()) // Инициализируем RakNetServer
                    .childHandler(new ChannelInitializer<DatagramChannel>() {
                        @Override
                        protected void initChannel(DatagramChannel ch) throws Exception {
                            // Здесь будет основной Bedrock протокол
                            ch.pipeline().addLast(new BedrockRakNetServerInitializer() {
                                @Override
                                protected void initSession(Channel sessionChannel) {
                                    // Когда RakNet устанавливает сессию, мы добавляем Bedrock обработчики
                                    sessionChannel.pipeline()
                                            .addLast(new BedrockFrameDecoder()) // Декодирует фреймы Bedrock
                                            .addLast(new BedrockFrameEncoder()) // Кодирует фреймы Bedrock
                                            .addLast(new BedrockPacketDecoder(Bedrock_v859.CODEC)) // Декодирует пакеты Bedrock
                                            .addLast(new BedrockPacketEncoder(Bedrock_v859.CODEC)) // Кодирует пакеты Bedrock
                                            .addLast(new ClientConnectionHandler(AnyProxy.this, sessionChannel)); // Ваш обработчик клиентских соединений
                                }
                            });
                        }
                    });

            ChannelFuture future = b.bind(bindAddress, bindPort).sync();
            log.info("AnyProxy запущен на {}:{}, перенаправляет на {}:{}", bindAddress, bindPort, remoteAddress, remotePort);

            future.channel().closeFuture().sync();
        } finally {
            shutdown();
        }
    }

    public void shutdown() {
        if (bossGroup != null) {
            bossGroup.shutdownGracefully();
        }
        if (workerGroup != null) {
            workerGroup.shutdownGracefully();
        }
        log.info("AnyProxy остановлен.");
    }

    public static void main(String[] args) throws InterruptedException {
        // Пример использования
        String bindAddress = "0.0.0.0"; // Адрес, на котором прокси будет слушать клиентов
        int bindPort = 19132; // Порт, на котором прокси будет слушать клиентов
        String remoteAddress = "127.0.0.1"; // Адрес целевого сервера
        int remotePort = 19133; // Порт целевого сервера

        AnyProxy proxy = new AnyProxy(bindAddress, bindPort, remoteAddress, remotePort);
        proxy.start();
    }

    /**
     * Обработчик входящих соединений от клиентов.
     */
    private static class ClientConnectionHandler extends ChannelInboundHandlerAdapter {

        private final AnyProxy parent;
        private final Channel clientChannel;
        private ProxySession session;

        public ClientConnectionHandler(AnyProxy parent, Channel clientChannel) {
            this.parent = parent;
            this.clientChannel = clientChannel;
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            InetSocketAddress clientAddress = (InetSocketAddress) ctx.channel().remoteAddress();
            log.info("Новое входящее соединение от клиента: {}", clientAddress);

            // Создаем новую прокси-сессию для этого клиента
            session = new ProxySession(parent, clientChannel, parent.remoteAddress, parent.remotePort);
            parent.activeSessions.put(clientAddress, session);

            // Запускаем подключение к удаленному серверу
            session.connectToServer().addListener(future -> {
                if (future.isSuccess()) {
                    log.info("Успешно подключено к удаленному серверу для клиента {}", clientAddress);
                } else {
                    log.error("Не удалось подключиться к удаленному серверу для клиента {}: {}", clientAddress, future.cause().getMessage());
                    ctx.close(); // Закрываем клиентское соединение, если не удалось подключиться к серверу
                }
            });
            super.channelActive(ctx);
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            if (msg instanceof BedrockPacket) {
                BedrockPacket packet = (BedrockPacket) msg;
                // Здесь будет логика обработки пакетов от клиента, их модификация и пересылка на сервер
                // В зависимости от версии клиента и версии сервера, пакеты будут транслироваться
                // Для начала просто перешлем пакет, а потом добавим логику трансляции
                if (session != null && session.getServerChannel() != null) {
                    session.getServerChannel().writeAndFlush(packet);
                }
            } else {
                log.warn("Получен неизвестный объект от клиента: {}", msg.getClass().getName());
            }
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            InetSocketAddress clientAddress = (InetSocketAddress) ctx.channel().remoteAddress();
            log.info("Соединение с клиентом {} разорвано.", clientAddress);
            if (session != null) {
                session.disconnect();
                parent.activeSessions.remove(clientAddress);
            }
            super.channelInactive(ctx);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            log.error("Ошибка в обработчике клиента {}: {}", ctx.channel().remoteAddress(), cause.getMessage());
            ctx.close();
        }
    }
}

AnyProxy/src/main/java/ru/vbproduction667/anyproxy/network/protocol/ProxySession.java (Новый файл):

// AnyProxy/src/main/java/ru/vbproduction667/anyproxy/network/protocol/ProxySession.java
package ru.vbproduction667.anyproxy.network.protocol;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.nio.NioDatagramChannel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec;
import org.cloudburstmc.protocol.bedrock.codec.v859.Bedrock_v859; // Пример импорта кодека
import org.cloudburstmc.protocol.bedrock.netty.BedrockRakNetClientInitializer;
import org.cloudburstmc.protocol.bedrock.netty.codec.frame.BedrockFrameDecoder;
import org.cloudburstmc.protocol.bedrock.netty.codec.frame.BedrockFrameEncoder;
import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketDecoder;
import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketEncoder;
import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
import org.cloudburstmc.raknet.RakNetClient;
import org.cloudburstmc.raknet.pipeline.RakNetDatagramCodec;
import org.cloudburstmc.raknet.pipeline.RakNetPacketCodec;

import ru.vbproduction667.anyproxy.AnyProxy;

import java.net.InetSocketAddress;

@Slf4j
public class ProxySession {

    private final AnyProxy parent;
    private final Channel clientChannel; // Канал клиента
    private final String remoteAddress;
    private final int remotePort;

    @Getter
    private Channel serverChannel; // Канал к удаленному серверу

    private EventLoopGroup workerGroup; // Отдельный EventLoopGroup для клиентских соединений к серверу

    public ProxySession(AnyProxy parent, Channel clientChannel, String remoteAddress, int remotePort) {
        this.parent = parent;
        this.clientChannel = clientChannel;
        this.remoteAddress = remoteAddress;
        this.remotePort = remotePort;
        this.workerGroup = new NioEventLoopGroup(1); // Используем отдельный workerGroup для каждого сессии
    }

    public ChannelFuture connectToServer() {
        // Создаем RakNetClient для подключения к удаленному серверу
        RakNetClient rakNetClient = RakNetClient.builder()
                .address(new InetSocketAddress(remoteAddress, remotePort))
                .eventLoopGroup(workerGroup) // Используем workerGroup для RakNet клиента
                .build();

        Bootstrap b = new Bootstrap();
        b.group(workerGroup)
                .channel(NioDatagramChannel.class) // UDP канал для Bedrock
                .handler(rakNetClient.getInitializer()) // Инициализируем RakNetClient
                .remoteAddress(remoteAddress, remotePort) // Указываем удаленный адрес
                .handler(new ChannelInitializer<DatagramChannel>() { // Child handler для Bedrock протокола
                    @Override
                    protected void initChannel(DatagramChannel ch) throws Exception {
                        // Здесь будет основной Bedrock протокол для подключения к серверу
                        ch.pipeline().addLast(new BedrockRakNetClientInitializer() {
                            @Override
                            protected void initSession(Channel sessionChannel) {
                                // Когда RakNet устанавливает сессию, мы добавляем Bedrock обработчики
                                sessionChannel.pipeline()
                                        .addLast(new BedrockFrameDecoder())
                                        .addLast(new BedrockFrameEncoder())
                                        .addLast(new BedrockPacketDecoder(Bedrock_v859.CODEC)) // Декодирует пакеты от сервера
                                        .addLast(new BedrockPacketEncoder(Bedrock_v859.CODEC)) // Кодирует пакеты для сервера
                                        .addLast(new ServerConnectionHandler(ProxySession.this, clientChannel)); // Ваш обработчик серверных соединений
                                serverChannel = sessionChannel; // Сохраняем канал к серверу
                            }
                        });
                    }
                });

        return b.connect();
    }

    public void disconnect() {
        if (clientChannel != null && clientChannel.isOpen()) {
            clientChannel.close();
        }
        if (serverChannel != null && serverChannel.isOpen()) {
            serverChannel.close();
        }
        if (workerGroup != null) {
            workerGroup.shutdownGracefully();
        }
        log.info("Сессия с клиентом {} и сервером {} разорвана.", clientChannel.remoteAddress(), (serverChannel != null ? serverChannel.remoteAddress() : "N/A"));
    }

    /**
     * Обработчик входящих пакетов от удаленного сервера.
     */
    private static class ServerConnectionHandler extends ChannelInboundHandlerAdapter {

        private final ProxySession parent;
        private final Channel clientChannel;

        public ServerConnectionHandler(ProxySession parent, Channel clientChannel) {
            this.parent = parent;
            this.clientChannel = clientChannel;
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            if (msg instanceof BedrockPacket) {
                BedrockPacket packet = (BedrockPacket) msg;
                // Здесь будет логика обработки пакетов от сервера, их модификация и пересылка клиенту
                // Для начала просто перешлем пакет
                if (clientChannel != null && clientChannel.isOpen()) {
                    clientChannel.writeAndFlush(packet);
                }
            } else {
                log.warn("Получен неизвестный объект от сервера: {}", msg.getClass().getName());
            }
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            log.info("Соединение с удаленным сервером разорвано.");
            parent.disconnect(); // Отключаем всю сессию при разрыве соединения с сервером
            super.channelInactive(ctx);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            log.error("Ошибка в обработчике сервера {}: {}", ctx.channel().remoteAddress(), cause.getMessage());
            ctx.close();
            parent.disconnect();
        }
    }
}

Пояснения и дальнейшие шаги:

  1. Зависимости Maven/Gradle: Вам понадобятся следующие зависимости в вашем pom.xml (Maven) или build.gradle (Gradle):

    • org.cloudburstmc.protocol:bedrock-protocol:<latest-version> (например, 1.2.0-SNAPSHOT или стабильная, если есть)
    • org.cloudburstmc.protocol:bedrock-codec-vXXX:<latest-version> (для каждого кодека, который вы хотите поддерживать, например, bedrock-codec-v859, bedrock-codec-v291, и т.д.)
    • org.cloudburstmc:raknet:<latest-version>
    • io.netty:netty-all:<latest-version> (или отдельные модули Netty)
    • org.projectlombok:lombok:<latest-version> (для @Slf4j, @Getter и т.д.)
    • org.slf4j:slf4j-api:<latest-version>
    • ch.qos.logback:logback-classic:<latest-version> (или другая реализация SLF4J для логирования)

    Пример pom.xml (частично):

    <dependencies>
        <dependency>
            <groupId>org.cloudburstmc.protocol</groupId>
            <artifactId>bedrock-protocol</artifactId>
            <version>1.2.0-SNAPSHOT</version> <!-- Проверьте актуальную версию -->
        </dependency>
        <dependency>
            <groupId>org.cloudburstmc.protocol</groupId>
            <artifactId>bedrock-codec-v859</artifactId>
            <version>1.2.0-SNAPSHOT</version>
        </dependency>
        <!-- Добавьте другие кодеки по мере необходимости -->
        <dependency>
            <groupId>org.cloudburstmc</groupId>
            <artifactId>raknet</artifactId>
            <version>1.2.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.109.Final</version> <!-- Проверьте актуальную версию -->
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.32</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.13</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.5.6</version>
        </dependency>
    </dependencies>
    
  2. Основная логика трансляции пакетов (Packet Translation): Это самая сложная часть. Вам понадобится:

    • Определение версии клиента и сервера: При первом подключении клиента (например, по LoginPacket), вы сможете определить версию клиента. Затем вам нужно будет установить соединение с сервером и определить его версию (возможно, по ответу на пинг или по первому PlayStatusPacket).
    • Динамическая загрузка кодеков: В BedrockPacketDecoder и BedrockPacketEncoder вам нужно будет использовать соответствующий BedrockCodec для версии клиента и сервера. Это потребует создания маппинга версия -> BedrockCodec.
    • Создание маппингов пакетов:
      • Для каждого пакета, который может отличаться между версиями, вам нужно будет создать свои классы-трансляторы.
      • Например, ClientboundPacketTranslator и ServerboundPacketTranslator.
      • Эти трансляторы будут брать пакет одной версии, преобразовывать его в промежуточный формат (или напрямую в пакет другой версии), а затем отправлять его.
    • Маппинги из Nukkit, GeyserMC, PocketMine-MP: Изучите, как они обрабатывают различия в протоколах. Они являются отличными источниками для логики трансляции. Вам нужно будет адаптировать их под структуру Cloudburst Protocol.
  3. Пример динамического выбора кодека (псевдокод):

    В ClientConnectionHandler и ServerConnectionHandler вам нужно будет определить версию и затем использовать соответствующий кодек:

    // ... в ClientConnectionHandler initSession
    BedrockCodec clientCodec = getCodecForVersion(clientVersion); // Нужна функция для получения кодека
    sessionChannel.pipeline()
        .addLast(new BedrockPacketDecoder(clientCodec))
        .addLast(new BedrockPacketEncoder(clientCodec))
        // ...
    
    // ... в ProxySession connectToServer initSession
    BedrockCodec serverCodec = getCodecForVersion(serverVersion); // Нужна функция для получения кодека
    sessionChannel.pipeline()
        .addLast(new BedrockPacketDecoder(serverCodec))
        .addLast(new BedrockPacketEncoder(serverCodec))
        // ...
    

    Вам понадобится статическая карта Map<Integer, BedrockCodec> для хранения всех поддерживаемых кодеков.

  4. Обработка состояний (логин, игра): Протокол Bedrock имеет разные состояния (например, логин, игра). Вам нужно будет отслеживать текущее состояние для каждого клиента, чтобы правильно обрабатывать пакеты.

  5. Использование org.cloudburstmc.protocol:

    • BedrockPacket - базовый класс для всех пакетов Bedrock.
    • BedrockPacketType - enum для типов пакетов.
    • Каждый кодек (например, Bedrock_v859) содержит методы для чтения и записи пакетов.
    • BedrockRakNetServerInitializer и BedrockRakNetClientInitializer упрощают интеграцию RakNet с Netty.
  6. Маппинги сущностей, блоков, предметов: Помимо пакетов, вам также придется маппировать ID сущностей, блоков, предметов, так как они могут меняться между версиями. Это обычно реализуется через JSON-файлы или хардкодированные карты.

  7. Конфигурация: Добавьте возможность конфигурировать адрес прокси, целевой сервер и, возможно, список поддерживаемых версий.

  8. Тестирование: Это будет критически важно. Вам понадобятся клиенты разных версий и сервера разных версий для тща