diff --git a/.idea/icon.png b/.idea/icon.png
deleted file mode 100644
index 4b526ba9..00000000
Binary files a/.idea/icon.png and /dev/null differ
diff --git a/neutrino-proxy-client-sdk/Dockerfile b/neutrino-proxy-client-sdk/Dockerfile
new file mode 100644
index 00000000..e2a41943
--- /dev/null
+++ b/neutrino-proxy-client-sdk/Dockerfile
@@ -0,0 +1,16 @@
+FROM openjdk:21-jdk-oracle
+#同步时间
+#RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
+# apk update && apk add wget unzip vim && apk add -U tzdata && \
+# ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
+# 设置时区为北京时间
+ENV TZ=Asia/Shanghai
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+RUN mkdir -p /root/neutrino-proxy/config
+WORKDIR /root/neutrino-proxy
+COPY ./target/neutrino-proxy-client.jar /root/neutrino-proxy/neutrino-proxy-client.jar
+COPY ./src/main/resources/app-copy.yml /root/neutrino-proxy/config/app.yml
+#VOLUME ["/root/neutrino-proxy"]
+ENTRYPOINT ["java","-jar","neutrino-proxy-client.jar","config=./config/app.yml"]
+
+#docker run -it -d --restart=always --name np_client -e SERVER_IP=127.0.0.1
diff --git a/neutrino-proxy-client-sdk/pom.xml b/neutrino-proxy-client-sdk/pom.xml
new file mode 100644
index 00000000..9db95010
--- /dev/null
+++ b/neutrino-proxy-client-sdk/pom.xml
@@ -0,0 +1,62 @@
+
+
+
+ neutrino-proxy
+ org.dromara.neutrino-proxy
+ ${revision}
+ ../pom.xml
+
+ 4.0.0
+ jar
+
+ neutrino-proxy-client-sdk
+
+
+
+ org.dromara.neutrino-proxy
+ neutrino-proxy-core
+ ${revision}
+
+
+ org.noear
+ solon-lib
+
+
+ com.github.jsqlparser
+ jsqlparser
+ 4.5
+ compile
+
+
+
+
+ ${project.artifactId}
+
+
+ src/main/resources
+ true
+
+ *.yml
+
+
+
+ ${project.basedir}/src/main/resources
+ false
+
+
+ app-dev.yml
+
+
+
+
+
+
+ org.noear
+ solon-maven-plugin
+
+
+
+
+
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/ProxyClientSdk.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/ProxyClientSdk.java
new file mode 100644
index 00000000..c0570f13
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/ProxyClientSdk.java
@@ -0,0 +1,48 @@
+package org.dromara.neutrinoproxy.client.sdk;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.noear.solon.Solon;
+import org.noear.solon.Utils;
+import org.noear.solon.annotation.SolonMain;
+
+/**
+ *
+ * @author: aoshiguchen
+ * @date: 2022/6/16
+ */
+@Slf4j
+@SolonMain
+public class ProxyClientSdk {
+
+ public static void main(String[] args) {
+ Solon.start(ProxyClientSdk.class, args, app -> {
+ String loglevel = System.getenv("LOG_LEVEL");
+ if (Utils.isNotEmpty(loglevel)) {
+ app.cfg().put("solon.logging.logger.root.level", loglevel);
+ }
+
+ setAlias("neutrino.proxy.tunnel.server-ip", "serverIp");
+ setAlias("neutrino.proxy.tunnel.server-port", "serverPort");
+ setAlias("neutrino.proxy.tunnel.ssl-enable", "sslEnable");
+ setAlias("neutrino.proxy.tunnel.jks-path", "jksPath");
+ setAlias("neutrino.proxy.tunnel.key-store-password", "keyStorePassword");
+ setAlias("neutrino.proxy.tunnel.license-key", "licenseKey");
+
+ log.info("NeutrinoProxy Client :{}", app.cfg().get("solon.app.version"));
+ });
+ }
+
+ /**
+ * 别名处理,支持较短的启动参数名
+ * @param key
+ * @param alias
+ */
+ private static void setAlias(String key, String alias) {
+ String val = Solon.cfg().argx().get(alias);
+ if (StrUtil.isNotBlank(val)) {
+ Solon.cfg().put(key, val);
+ }
+ }
+
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/config/IBeanHandler.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/config/IBeanHandler.java
new file mode 100644
index 00000000..266d5b3d
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/config/IBeanHandler.java
@@ -0,0 +1,8 @@
+package org.dromara.neutrinoproxy.client.sdk.config;
+
+import org.dromara.neutrinoproxy.core.dispatcher.Dispatcher;
+
+public interface IBeanHandler {
+ Dispatcher getDispatcher();
+ ProxyConfig getProxyConfig();
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/config/IProxyConfiguration.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/config/IProxyConfiguration.java
new file mode 100644
index 00000000..abe2a641
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/config/IProxyConfiguration.java
@@ -0,0 +1,187 @@
+package org.dromara.neutrinoproxy.client.sdk.config;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioDatagramChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.handler.timeout.IdleStateHandler;
+import org.dromara.neutrinoproxy.client.sdk.core.*;
+import org.dromara.neutrinoproxy.client.sdk.util.ProxyUtil;
+import org.dromara.neutrinoproxy.core.ProxyMessageDecoder;
+import org.dromara.neutrinoproxy.core.ProxyMessageEncoder;
+import org.dromara.neutrinoproxy.core.aot.NeutrinoCoreRuntimeNativeRegistrar;
+
+import java.net.InetSocketAddress;
+
+/**
+ * 代理配置
+ * @author: aoshiguchen
+ * @date: 2022/10/8
+ */
+
+public abstract class IProxyConfiguration {
+
+ public abstract IBeanHandler getBeanHandler();
+
+ public NioEventLoopGroup tunnelWorkGroup(ProxyConfig proxyConfig) {
+ return new NioEventLoopGroup(proxyConfig.getTunnel().getThreadCount());
+ }
+
+ public NioEventLoopGroup tcpRealServerWorkGroup(ProxyConfig proxyConfig) {
+ // 暂时先公用此配置
+ return new NioEventLoopGroup(proxyConfig.getTunnel().getThreadCount());
+ }
+
+ public NioEventLoopGroup udpServerGroup(ProxyConfig proxyConfig) {
+ // 暂时先公用此配置
+ return new NioEventLoopGroup(proxyConfig.getClient().getUdp().getBossThreadCount());
+ }
+
+ public NioEventLoopGroup udpWorkGroup(ProxyConfig proxyConfig) {
+ // 暂时先公用此配置
+ return new NioEventLoopGroup(proxyConfig.getClient().getUdp().getWorkThreadCount());
+ }
+
+ public Bootstrap cmdTunnelBootstrap(ProxyConfig proxyConfig,
+ NioEventLoopGroup tunnelWorkGroup) {
+ Bootstrap bootstrap = new Bootstrap();
+ bootstrap.group(tunnelWorkGroup);
+ bootstrap.channel(NioSocketChannel.class);
+// bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000);
+// bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
+// /**
+// * TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此,TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据)。
+// * Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。
+// */
+// bootstrap.option(ChannelOption.TCP_NODELAY, true);
+ bootstrap.remoteAddress(InetSocketAddress.createUnresolved(proxyConfig.getTunnel().getServerIp(), proxyConfig.getTunnel().getServerPort()));
+
+ bootstrap.handler(new ChannelInitializer() {
+
+ @Override
+ public void initChannel(SocketChannel ch) throws Exception {
+ if (proxyConfig.getTunnel().getSslEnable()) {
+ ch.pipeline().addLast(ProxyUtil.createSslHandler(proxyConfig));
+ }
+ if (null != proxyConfig.getTunnel().getTransferLogEnable() && proxyConfig.getTunnel().getTransferLogEnable()) {
+ ch.pipeline().addFirst(new LoggingHandler(CmdChannelHandler.class));
+ }
+ ch.pipeline().addLast(new ProxyMessageDecoder(proxyConfig.getProtocol().getMaxFrameLength(),
+ proxyConfig.getProtocol().getLengthFieldOffset(), proxyConfig.getProtocol().getLengthFieldLength(),
+ proxyConfig.getProtocol().getLengthAdjustment(), proxyConfig.getProtocol().getInitialBytesToStrip()));
+ ch.pipeline().addLast(new ProxyMessageEncoder());
+ ch.pipeline().addLast(new IdleStateHandler(proxyConfig.getProtocol().getReadIdleTime(), proxyConfig.getProtocol().getWriteIdleTime(), proxyConfig.getProtocol().getAllIdleTimeSeconds()));
+ ch.pipeline().addLast(new CmdChannelHandler(getBeanHandler()));
+ }
+ });
+ return bootstrap;
+ }
+
+ public Bootstrap tcpProxyTunnelBootstrap(ProxyConfig proxyConfig,
+ NioEventLoopGroup tunnelWorkGroup) {
+ Bootstrap bootstrap = new Bootstrap();
+ bootstrap.group(tunnelWorkGroup);
+ bootstrap.channel(NioSocketChannel.class);
+ bootstrap.remoteAddress(InetSocketAddress.createUnresolved(proxyConfig.getTunnel().getServerIp(), proxyConfig.getTunnel().getServerPort()));
+ bootstrap.handler(new ChannelInitializer() {
+
+ @Override
+ public void initChannel(SocketChannel ch) throws Exception {
+ if (proxyConfig.getTunnel().getSslEnable()) {
+ ch.pipeline().addLast(ProxyUtil.createSslHandler(proxyConfig));
+ }
+ if (null != proxyConfig.getTunnel().getTransferLogEnable() && proxyConfig.getTunnel().getTransferLogEnable()) {
+ ch.pipeline().addFirst(new LoggingHandler(TcpProxyChannelHandler.class));
+ }
+ ch.pipeline().addLast(new ProxyMessageDecoder(proxyConfig.getProtocol().getMaxFrameLength(),
+ proxyConfig.getProtocol().getLengthFieldOffset(), proxyConfig.getProtocol().getLengthFieldLength(),
+ proxyConfig.getProtocol().getLengthAdjustment(), proxyConfig.getProtocol().getInitialBytesToStrip()));
+ ch.pipeline().addLast(new ProxyMessageEncoder());
+ ch.pipeline().addLast(new IdleStateHandler(proxyConfig.getProtocol().getReadIdleTime(), proxyConfig.getProtocol().getWriteIdleTime(), proxyConfig.getProtocol().getAllIdleTimeSeconds()));
+ ch.pipeline().addLast(new TcpProxyChannelHandler(getBeanHandler()));
+ }
+ });
+ return bootstrap;
+ }
+ public Bootstrap udpProxyTunnelBootstrap(ProxyConfig proxyConfig,
+ NioEventLoopGroup tunnelWorkGroup) {
+ Bootstrap bootstrap = new Bootstrap();
+ bootstrap.group(tunnelWorkGroup);
+ bootstrap.channel(NioSocketChannel.class);
+ bootstrap.remoteAddress(InetSocketAddress.createUnresolved(proxyConfig.getTunnel().getServerIp(), proxyConfig.getTunnel().getServerPort()));
+ bootstrap.handler(new ChannelInitializer() {
+
+ @Override
+ public void initChannel(SocketChannel ch) throws Exception {
+ if (proxyConfig.getTunnel().getSslEnable()) {
+ ch.pipeline().addLast(ProxyUtil.createSslHandler(proxyConfig));
+ }
+ if (null != proxyConfig.getTunnel().getTransferLogEnable() && proxyConfig.getTunnel().getTransferLogEnable()) {
+ ch.pipeline().addFirst(new LoggingHandler(TcpProxyChannelHandler.class));
+ }
+ ch.pipeline().addLast(new ProxyMessageDecoder(proxyConfig.getProtocol().getMaxFrameLength(),
+ proxyConfig.getProtocol().getLengthFieldOffset(), proxyConfig.getProtocol().getLengthFieldLength(),
+ proxyConfig.getProtocol().getLengthAdjustment(), proxyConfig.getProtocol().getInitialBytesToStrip()));
+ ch.pipeline().addLast(new ProxyMessageEncoder());
+ ch.pipeline().addLast(new IdleStateHandler(proxyConfig.getProtocol().getReadIdleTime(), proxyConfig.getProtocol().getWriteIdleTime(), proxyConfig.getProtocol().getAllIdleTimeSeconds()));
+ ch.pipeline().addLast(new UdpProxyChannelHandler(getBeanHandler()));
+ }
+ });
+ return bootstrap;
+ }
+
+ public Bootstrap realServerBootstrap(ProxyConfig proxyConfig,
+ NioEventLoopGroup tcpRealServerWorkGroup
+ ) {
+ Bootstrap bootstrap = new Bootstrap();
+ bootstrap.group(tcpRealServerWorkGroup);
+ bootstrap.channel(NioSocketChannel.class);
+ bootstrap.handler(new ChannelInitializer() {
+
+ @Override
+ public void initChannel(SocketChannel ch) throws Exception {
+ if (null != proxyConfig.getTunnel().getTransferLogEnable() && proxyConfig.getTunnel().getTransferLogEnable()) {
+ ch.pipeline().addFirst(new LoggingHandler(RealServerChannelHandler.class));
+ }
+ ch.pipeline().addLast(new RealServerChannelHandler());
+ }
+ });
+ return bootstrap;
+ }
+
+ public Bootstrap udpServerBootstrap(ProxyConfig proxyConfig,
+ NioEventLoopGroup udpServerGroup,
+ NioEventLoopGroup udpWorkGroup) {
+ Bootstrap bootstrap = new Bootstrap();
+ bootstrap.group(udpServerGroup)
+ // 主线程处理
+ .channel(NioDatagramChannel.class)
+ // 广播
+ .option(ChannelOption.SO_BROADCAST, true)
+ // 设置读缓冲区为2M
+ .option(ChannelOption.SO_RCVBUF, 2048 * 1024)
+ // 设置写缓冲区为1M
+ .option(ChannelOption.SO_SNDBUF, 1024 * 1024)
+ .handler(new ChannelInitializer() {
+ @Override
+ protected void initChannel(NioDatagramChannel ch) {
+ ChannelPipeline pipeline = ch.pipeline();
+ if (null != proxyConfig.getClient().getUdp().getTransferLogEnable() && proxyConfig.getClient().getUdp().getTransferLogEnable()) {
+ ch.pipeline().addFirst(new LoggingHandler(UdpRealServerHandler.class));
+ }
+ pipeline.addLast(udpWorkGroup, new UdpRealServerHandler());
+ }
+ });
+ return bootstrap;
+ }
+
+ public NeutrinoCoreRuntimeNativeRegistrar neutrinoCoreRuntimeNativeRegistrar() {
+ return new NeutrinoCoreRuntimeNativeRegistrar();
+ }
+
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/config/NeutrinoClientRuntimeNativeRegistrar.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/config/NeutrinoClientRuntimeNativeRegistrar.java
new file mode 100644
index 00000000..5b060964
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/config/NeutrinoClientRuntimeNativeRegistrar.java
@@ -0,0 +1,26 @@
+package org.dromara.neutrinoproxy.client.sdk.config;
+
+import org.noear.solon.annotation.Component;
+import org.noear.solon.aot.RuntimeNativeMetadata;
+import org.noear.solon.aot.RuntimeNativeRegistrar;
+import org.noear.solon.aot.hint.MemberCategory;
+import org.noear.solon.core.AppContext;
+
+/**
+ * @author songyinyin
+ * @since 2023/10/21 22:50
+ */
+@Component
+public class NeutrinoClientRuntimeNativeRegistrar implements RuntimeNativeRegistrar {
+ @Override
+ public void register(AppContext context, RuntimeNativeMetadata metadata) {
+ metadata.registerResourceInclude("test.jks");
+
+ metadata.registerReflection(ProxyConfig.Protocol.class, MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS);
+ metadata.registerReflection(ProxyConfig.Client.class, MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS);
+ metadata.registerReflection(ProxyConfig.Tunnel.class, MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS);
+ metadata.registerReflection(ProxyConfig.Tcp.class, MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS);
+ metadata.registerReflection(ProxyConfig.Udp.class, MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS);
+ metadata.registerReflection(ProxyConfig.Reconnection.class, MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS);
+ }
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/config/ProxyConfig.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/config/ProxyConfig.java
new file mode 100644
index 00000000..ba740bef
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/config/ProxyConfig.java
@@ -0,0 +1,74 @@
+package org.dromara.neutrinoproxy.client.sdk.config;
+
+import lombok.Data;
+import org.noear.solon.annotation.Component;
+import org.noear.solon.annotation.Inject;
+
+/**
+ *
+ * @author: aoshiguchen
+ * @date: 2022/6/16
+ */
+@Data
+@Component
+public class ProxyConfig {
+ @Inject("${neutrino.proxy.protocol}")
+ private Protocol protocol;
+ @Inject("${neutrino.proxy.tunnel}")
+ private Tunnel tunnel;
+ @Inject("${neutrino.proxy.client}")
+ private Client client;
+
+ @Data
+ public static class Protocol {
+ private Integer maxFrameLength;
+ private Integer lengthFieldOffset;
+ private Integer lengthFieldLength;
+ private Integer initialBytesToStrip;
+ private Integer lengthAdjustment;
+ private Integer readIdleTime;
+ private Integer writeIdleTime;
+ private Integer allIdleTimeSeconds;
+ }
+
+ @Data
+ public static class Tunnel {
+ private String keyStorePassword;
+ private String jksPath;
+ private String serverIp;
+ private Integer serverPort;
+ private Boolean sslEnable;
+ private Integer obtainLicenseInterval;
+ private String licenseKey;
+ private Integer threadCount;
+ private String clientId;
+ private Boolean transferLogEnable;
+ private Boolean heartbeatLogEnable;
+ private Reconnection reconnection;
+ }
+
+ @Data
+ public static class Client {
+// private Tcp tcp;
+ private Udp udp;
+ }
+
+ @Data
+ public static class Reconnection {
+ private Integer intervalSeconds;
+ private Boolean unlimited;
+ }
+
+ @Data
+ public static class Tcp {
+
+ }
+
+ @Data
+ public static class Udp {
+ private Integer bossThreadCount;
+ private Integer workThreadCount;
+ private String puppetPortRange;
+ private Boolean transferLogEnable;
+ }
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/constant/Constants.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/constant/Constants.java
new file mode 100644
index 00000000..02dd5fe8
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/constant/Constants.java
@@ -0,0 +1,13 @@
+package org.dromara.neutrinoproxy.client.sdk.constant;
+
+import io.netty.util.AttributeKey;
+import org.dromara.neutrinoproxy.client.sdk.util.UdpChannelBindInfo;
+
+/**
+ * @author: aoshiguchen
+ * @date: 2023/9/21
+ */
+public interface Constants {
+ AttributeKey UDP_CHANNEL_BIND_KEY = AttributeKey.newInstance("udpChannelBindKey");
+
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/CmdChannelHandler.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/CmdChannelHandler.java
new file mode 100644
index 00000000..f4bb1c67
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/CmdChannelHandler.java
@@ -0,0 +1,87 @@
+package org.dromara.neutrinoproxy.client.sdk.core;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.timeout.IdleStateEvent;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.neutrinoproxy.client.sdk.config.IBeanHandler;
+import org.dromara.neutrinoproxy.client.sdk.config.ProxyConfig;
+import org.dromara.neutrinoproxy.client.sdk.util.ProxyUtil;
+import org.dromara.neutrinoproxy.core.Constants;
+import org.dromara.neutrinoproxy.core.ProxyMessage;
+
+/**
+ * 处理与服务端之间的数据传输
+ * @author: aoshiguchen
+ * @date: 2022/6/16
+ */
+@Slf4j
+public class CmdChannelHandler extends SimpleChannelInboundHandler{
+ private static volatile Boolean transferLogEnable = Boolean.FALSE;
+ private IBeanHandler beanHandler;
+
+ public CmdChannelHandler(IBeanHandler beanHandler) {
+ this.beanHandler = beanHandler;
+ ProxyConfig proxyConfig = beanHandler.getProxyConfig();
+ if (null != proxyConfig.getClient() && null != proxyConfig.getTunnel().getHeartbeatLogEnable()) {
+ transferLogEnable = proxyConfig.getTunnel().getHeartbeatLogEnable();
+ }
+ }
+
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, ProxyMessage proxyMessage) throws Exception {
+ if (ProxyMessage.TYPE_HEARTBEAT != proxyMessage.getType() || transferLogEnable) {
+ log.debug("[CMD Channel]Client CmdChannel recieved proxy message, type is {}", proxyMessage.getType());
+ }
+ beanHandler.getDispatcher().dispatch(ctx, proxyMessage);
+ }
+
+ @Override
+ public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
+ Channel realServerChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
+ if (realServerChannel != null) {
+ realServerChannel.config().setOption(ChannelOption.AUTO_READ, ctx.channel().isWritable());
+ }
+
+ super.channelWritabilityChanged(ctx);
+ }
+
+ @Override
+ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+ log.info("[CMD Channel]Client CmdChannel disconnect");
+ ProxyUtil.setCmdChannel(null);
+ ProxyUtil.clearRealServerChannels();
+
+ super.channelInactive(ctx);
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ log.error("[CMD Channel]Client CmdChannel Error channelId:{}", ctx.channel().id().asLongText(), cause);
+ ctx.close();
+ }
+
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+ if(evt instanceof IdleStateEvent) {
+ IdleStateEvent event = (IdleStateEvent)evt;
+ switch (event.state()) {
+ case READER_IDLE:
+ // 读超时,断开连接
+ log.error("[CMD Channel] Read timeout disconnect");
+ ctx.channel().close();
+ break;
+ case WRITER_IDLE:
+ ctx.channel().writeAndFlush(ProxyMessage.buildHeartbeatMessage());
+ break;
+ case ALL_IDLE:
+ log.error("[CMD Channel] ReadWrite timeout disconnect");
+ ctx.close();
+ break;
+ }
+ }
+ }
+
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/CustomThreadFactory.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/CustomThreadFactory.java
new file mode 100644
index 00000000..1bddce35
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/CustomThreadFactory.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2022 aoshiguchen
+ *
+ * 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
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package org.dromara.neutrinoproxy.client.sdk.core;
+
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ *
+ * @author: aoshiguchen
+ * @date: 2022/9/4
+ */
+public class CustomThreadFactory implements ThreadFactory {
+ private final ThreadGroup group;
+ private final AtomicInteger threadNumber = new AtomicInteger(1);
+ private final String namePrefix;
+
+ public CustomThreadFactory(String prefix) {
+ SecurityManager s = System.getSecurityManager();
+ group = (s != null) ? s.getThreadGroup() :
+ Thread.currentThread().getThreadGroup();
+ namePrefix = prefix + "-thread-";
+ }
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
+ if (t.isDaemon()) {
+ t.setDaemon(false);
+ }
+ if (t.getPriority() != Thread.NORM_PRIORITY) {
+ t.setPriority(Thread.NORM_PRIORITY);
+ }
+ return t;
+ }
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/IAbProxyClientService.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/IAbProxyClientService.java
new file mode 100644
index 00000000..0af1ef55
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/IAbProxyClientService.java
@@ -0,0 +1,127 @@
+package org.dromara.neutrinoproxy.client.sdk.core;
+
+import cn.hutool.core.util.StrUtil;
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.neutrinoproxy.client.sdk.config.ProxyConfig;
+import org.dromara.neutrinoproxy.client.sdk.util.ProxyUtil;
+import org.dromara.neutrinoproxy.client.sdk.util.UdpServerUtil;
+import org.dromara.neutrinoproxy.core.ProxyMessage;
+import org.noear.solon.Solon;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * 代理客户端服务
+ * @author: aoshiguchen
+ * @date: 2022/6/16
+ */
+@Slf4j
+public class IAbProxyClientService {
+
+ public ProxyConfig proxyConfig;
+
+ public Bootstrap cmdTunnelBootstrap;
+
+ public Bootstrap udpServerBootstrap;
+
+ private volatile Channel channel;
+ /**
+ * 重连次数
+ */
+ private volatile int reconnectCount = 0;
+
+ /**
+ * 重连服务执行器
+ */
+ public static final ScheduledExecutorService reconnectExecutor = Executors.newSingleThreadScheduledExecutor(new CustomThreadFactory("ClientReconnect"));
+
+ public void init_i(){
+ reconnectExecutor.scheduleWithFixedDelay(this::reconnect, 10, proxyConfig.getTunnel().getReconnection().getIntervalSeconds(), TimeUnit.SECONDS);
+ try {
+ this.start();
+ UdpServerUtil.initCache(proxyConfig, udpServerBootstrap);
+ } catch (Exception e) {
+ // 启动连不上也做一下重连,因此先catch异常
+ log.error("[CmdChannel] start error", e);
+ }
+ }
+
+ public void start() {
+ if (StrUtil.isEmpty(proxyConfig.getTunnel().getServerIp())) {
+ log.error("not found server-ip config.");
+ Solon.stop();
+ return;
+ }
+ if (null == proxyConfig.getTunnel().getServerPort()) {
+ log.error("not found server-port config.");
+ Solon.stop();
+ return;
+ }
+ if (null != proxyConfig.getTunnel().getSslEnable() && proxyConfig.getTunnel().getSslEnable()
+ && StrUtil.isEmpty(proxyConfig.getTunnel().getJksPath())) {
+ log.error("not found jks-path config.");
+ Solon.stop();
+ return;
+ }
+ if (StrUtil.isEmpty(proxyConfig.getTunnel().getLicenseKey())) {
+ log.error("not found license-key config.");
+ Solon.stop();
+ return;
+ }
+ if (null == channel || !channel.isActive()) {
+ try {
+ connectProxyServer();
+ } catch (Exception e) {
+ log.error("client start error", e);
+ }
+ } else {
+ channel.writeAndFlush(ProxyMessage.buildAuthMessage(proxyConfig.getTunnel().getLicenseKey(), ProxyUtil.getClientId()));
+ }
+ }
+
+ /**
+ * 连接代理服务器
+ */
+ private void connectProxyServer() throws InterruptedException {
+ cmdTunnelBootstrap.connect()
+ .addListener(new ChannelFutureListener() {
+
+ @Override
+ public void operationComplete(ChannelFuture future) throws Exception {
+ if (future.isSuccess()) {
+ channel = future.channel();
+ // 连接成功,向服务器发送客户端认证信息(licenseKey)
+ ProxyUtil.setCmdChannel(future.channel());
+ future.channel().writeAndFlush(ProxyMessage.buildAuthMessage(proxyConfig.getTunnel().getLicenseKey(), ProxyUtil.getClientId()));
+ log.info("[CmdChannel] connect proxy server success. channelId:{}", future.channel().id().asLongText());
+
+// reconnectServiceEnable = true;
+ reconnectCount = 0;
+ } else {
+ log.info("[CmdChannel] connect proxy server failed!");
+ }
+ }
+ }).sync();
+ }
+
+ protected synchronized void reconnect() {
+ if (null != channel) {
+ if (channel.isActive()) {
+ return;
+ }
+ channel.close();
+ }
+
+ log.info("[CmdChannel] client reconnect seq:{}", ++reconnectCount);
+ try {
+ connectProxyServer();
+ } catch (Exception e) {
+ log.error("[CmdChannel] reconnect error", e);
+ }
+ }
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/ProxyChannelBorrowListener.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/ProxyChannelBorrowListener.java
new file mode 100644
index 00000000..1dce20d2
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/ProxyChannelBorrowListener.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2022 aoshiguchen
+ *
+ * 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
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.dromara.neutrinoproxy.client.sdk.core;
+
+import io.netty.channel.Channel;
+
+/**
+ *
+ * @author: aoshiguchen
+ * @date: 2022/6/16
+ */
+public interface ProxyChannelBorrowListener {
+
+ void success(Channel channel);
+
+ void error(Throwable cause);
+
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/ProxyClientService.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/ProxyClientService.java
new file mode 100644
index 00000000..8922a8bc
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/ProxyClientService.java
@@ -0,0 +1,31 @@
+package org.dromara.neutrinoproxy.client.sdk.core;
+
+import io.netty.bootstrap.Bootstrap;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.neutrinoproxy.client.sdk.config.ProxyConfig;
+import org.noear.solon.annotation.Component;
+import org.noear.solon.annotation.Init;
+import org.noear.solon.annotation.Inject;
+
+/**
+ * 代理客户端服务
+ * @author: aoshiguchen
+ * @date: 2022/6/16
+ */
+@Slf4j
+@Component
+public class ProxyClientService extends IAbProxyClientService{
+ @Inject
+ private ProxyConfig proxyConfig;
+ @Inject("cmdTunnelBootstrap")
+ private Bootstrap cmdTunnelBootstrap;
+ @Inject("udpServerBootstrap")
+ private Bootstrap udpServerBootstrap;
+ @Init
+ public void init() {
+ super.proxyConfig=proxyConfig;
+ super.cmdTunnelBootstrap=cmdTunnelBootstrap;
+ super.udpServerBootstrap=udpServerBootstrap;
+ init_i();
+ }
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/RealServerChannelHandler.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/RealServerChannelHandler.java
new file mode 100644
index 00000000..ea0cb3d5
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/RealServerChannelHandler.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2022 aoshiguchen
+ *
+ * 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
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.dromara.neutrinoproxy.client.sdk.core;
+
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.neutrinoproxy.client.sdk.util.ProxyUtil;
+import org.dromara.neutrinoproxy.core.Constants;
+import org.dromara.neutrinoproxy.core.ProxyMessage;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.SimpleChannelInboundHandler;
+
+/**
+ * 处理与被代理客户端的数据传输
+ * @author: aoshiguchen
+ * @date: 2022/6/16
+ */
+@Slf4j
+public class RealServerChannelHandler extends SimpleChannelInboundHandler {
+
+
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
+ Channel realServerChannel = ctx.channel();
+ Channel proxyChannel = realServerChannel.attr(Constants.NEXT_CHANNEL).get();
+ if (null == proxyChannel) {
+ // 代理客户端连接断开
+ ctx.channel().close();
+ } else {
+
+ if (proxyChannel.isWritable()) {
+ if (!realServerChannel.config().isAutoRead()) {
+ realServerChannel.config().setAutoRead(true);
+ }
+ } else {
+ if (realServerChannel.config().isAutoRead()) {
+ realServerChannel.config().setAutoRead(false);
+ }
+ }
+
+ byte[] bytes = new byte[buf.readableBytes()];
+ buf.readBytes(bytes);
+ String visitorId = ProxyUtil.getVisitorIdByRealServerChannel(realServerChannel);
+ proxyChannel.writeAndFlush(ProxyMessage.buildTransferMessage(visitorId, bytes));
+ }
+ }
+
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) throws Exception {
+ super.channelActive(ctx);
+ }
+
+ @Override
+ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+ Channel realServerChannel = ctx.channel();
+ String visitorId = ProxyUtil.getVisitorIdByRealServerChannel(realServerChannel);
+ ProxyUtil.removeRealServerChannel(visitorId);
+ Channel channel = realServerChannel.attr(Constants.NEXT_CHANNEL).get();
+ if (channel != null) {
+ channel.writeAndFlush(ProxyMessage.buildDisconnectMessage(visitorId));
+ }
+
+ super.channelInactive(ctx);
+ }
+
+ @Override
+ public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
+ Channel realServerChannel = ctx.channel();
+ Channel proxyChannel = realServerChannel.attr(Constants.NEXT_CHANNEL).get();
+ if (proxyChannel != null) {
+ proxyChannel.config().setOption(ChannelOption.AUTO_READ, realServerChannel.isWritable());
+ }
+
+ super.channelWritabilityChanged(ctx);
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ log.error("Client ProxyChannel Error", cause);
+ }
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/TcpProxyChannelHandler.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/TcpProxyChannelHandler.java
new file mode 100644
index 00000000..59161321
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/TcpProxyChannelHandler.java
@@ -0,0 +1,87 @@
+package org.dromara.neutrinoproxy.client.sdk.core;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.timeout.IdleStateEvent;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.neutrinoproxy.client.sdk.config.IBeanHandler;
+import org.dromara.neutrinoproxy.client.sdk.config.ProxyConfig;
+import org.dromara.neutrinoproxy.client.sdk.util.ProxyUtil;
+import org.dromara.neutrinoproxy.core.Constants;
+import org.dromara.neutrinoproxy.core.ProxyMessage;
+import org.dromara.neutrinoproxy.core.dispatcher.Dispatcher;
+import org.noear.solon.Solon;
+
+/**
+ * 处理与服务端之间的数据传输
+ * @author: aoshiguchen
+ * @date: 2022/6/16
+ */
+@Slf4j
+public class TcpProxyChannelHandler extends SimpleChannelInboundHandler {
+
+ private IBeanHandler beanHandler;
+ public TcpProxyChannelHandler(IBeanHandler beanHandler) {
+ this.beanHandler = beanHandler;
+ }
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, ProxyMessage proxyMessage) throws Exception {
+ if (ProxyMessage.TYPE_HEARTBEAT != proxyMessage.getType()) {
+ log.debug("[TCP Proxy Channel]Client ProxyChannel recieved proxy message, type is {}", proxyMessage.getType());
+ }
+ beanHandler.getDispatcher().dispatch(ctx, proxyMessage);
+ }
+
+ @Override
+ public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
+ Channel realServerChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
+ if (realServerChannel != null) {
+ realServerChannel.config().setOption(ChannelOption.AUTO_READ, ctx.channel().isWritable());
+ }
+
+ super.channelWritabilityChanged(ctx);
+ }
+
+ @Override
+ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+ // 数据传输连接
+ Channel realServerChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
+ if (realServerChannel != null && realServerChannel.isActive()) {
+ realServerChannel.close();
+ }
+
+ ProxyUtil.removeTcpProxyChanel(ctx.channel());
+ super.channelInactive(ctx);
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ log.error("[TCP Proxy Channel]Client ProxyChannel Error channelId:{}", ctx.channel().id().asLongText(), cause);
+ ctx.close();
+ }
+
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+ if(evt instanceof IdleStateEvent) {
+ IdleStateEvent event = (IdleStateEvent)evt;
+ switch (event.state()) {
+ case READER_IDLE:
+ if (ctx.channel().isWritable()) {
+ // 读超时,断开连接
+ log.info("[TCP Proxy Channel]Read timeout");
+ ctx.channel().close();
+ }
+ break;
+ case WRITER_IDLE:
+ ctx.channel().writeAndFlush(ProxyMessage.buildHeartbeatMessage());
+ break;
+ case ALL_IDLE:
+// log.debug("[TCP Proxy Channel]ReadWrite timeout");
+// ctx.close();
+ break;
+ }
+ }
+ }
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/UdpProxyChannelHandler.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/UdpProxyChannelHandler.java
new file mode 100644
index 00000000..40dec1b6
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/UdpProxyChannelHandler.java
@@ -0,0 +1,84 @@
+package org.dromara.neutrinoproxy.client.sdk.core;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.timeout.IdleStateEvent;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.neutrinoproxy.client.sdk.config.IBeanHandler;
+import org.dromara.neutrinoproxy.client.sdk.util.ProxyUtil;
+import org.dromara.neutrinoproxy.core.Constants;
+import org.dromara.neutrinoproxy.core.ProxyMessage;
+import org.dromara.neutrinoproxy.core.dispatcher.Dispatcher;
+import org.noear.solon.Solon;
+
+/**
+ * 处理与服务端之间的数据传输
+ * @author: aoshiguchen
+ * @date: 2022/6/16
+ */
+@Slf4j
+public class UdpProxyChannelHandler extends SimpleChannelInboundHandler {
+
+ private IBeanHandler beanHandler;
+ public UdpProxyChannelHandler(IBeanHandler beanHandler) {
+ this.beanHandler = beanHandler;
+ }
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, ProxyMessage proxyMessage) throws Exception {
+ if (ProxyMessage.TYPE_HEARTBEAT != proxyMessage.getType()) {
+ log.debug("[UDP Proxy Channel]Client ProxyChannel recieved proxy message, type is {}", proxyMessage.getType());
+ }
+ beanHandler.getDispatcher().dispatch(ctx, proxyMessage);
+ }
+
+ @Override
+ public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
+ Channel realServerChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
+ if (realServerChannel != null) {
+ realServerChannel.config().setOption(ChannelOption.AUTO_READ, ctx.channel().isWritable());
+ }
+
+ super.channelWritabilityChanged(ctx);
+ }
+
+ @Override
+ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+ // 数据传输连接
+ Channel realServerChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
+ if (realServerChannel != null && realServerChannel.isActive()) {
+ realServerChannel.close();
+ }
+
+ ProxyUtil.removeTcpProxyChanel(ctx.channel());
+ super.channelInactive(ctx);
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ log.error("[UDP Proxy Channel]Client ProxyChannel Error channelId:{}", ctx.channel().id().asLongText(), cause);
+ ctx.close();
+ }
+
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+ if(evt instanceof IdleStateEvent) {
+ IdleStateEvent event = (IdleStateEvent)evt;
+ switch (event.state()) {
+ case READER_IDLE:
+ // 读超时,断开连接
+ log.info("[UDP Proxy Channel]Read timeout");
+ ctx.channel().close();
+ break;
+ case WRITER_IDLE:
+ ctx.channel().writeAndFlush(ProxyMessage.buildHeartbeatMessage());
+ break;
+ case ALL_IDLE:
+ log.debug("[UDP Proxy Channel]ReadWrite timeout");
+ ctx.close();
+ break;
+ }
+ }
+ }
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/UdpRealServerHandler.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/UdpRealServerHandler.java
new file mode 100644
index 00000000..758a5618
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/core/UdpRealServerHandler.java
@@ -0,0 +1,41 @@
+package org.dromara.neutrinoproxy.client.sdk.core;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.socket.DatagramPacket;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.neutrinoproxy.client.sdk.constant.Constants;
+import org.dromara.neutrinoproxy.client.sdk.util.UdpChannelBindInfo;
+import org.dromara.neutrinoproxy.core.ProxyMessage;
+
+import java.net.InetSocketAddress;
+
+/**
+ * @author: aoshiguchen
+ * @date: 2023/9/21
+ */
+@Slf4j
+public class UdpRealServerHandler extends SimpleChannelInboundHandler {
+
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket datagramPacket) throws Exception {
+ log.debug("chid---<:{} port:{}", ctx.channel().id().asLongText(), ((InetSocketAddress)ctx.channel().localAddress()).getPort());
+ UdpChannelBindInfo udpChannelBindInfo = ctx.channel().attr(Constants.UDP_CHANNEL_BIND_KEY).get();
+ if (null != udpChannelBindInfo) {
+ byte[] bytes = new byte[datagramPacket.content().readableBytes()];
+ datagramPacket.content().readBytes(bytes);
+
+ udpChannelBindInfo.getTunnelChannel().writeAndFlush(ProxyMessage.buildUdpTransferMessage(new ProxyMessage.UdpBaseInfo()
+ .setVisitorId(udpChannelBindInfo.getVisitorId())
+ .setVisitorIp(udpChannelBindInfo.getVisitorIp())
+ .setVisitorPort(udpChannelBindInfo.getVisitorPort())
+ .setServerPort(udpChannelBindInfo.getServerPort())
+ .setTargetIp(udpChannelBindInfo.getTargetIp())
+ .setTargetPort(udpChannelBindInfo.getTargetPort()))
+ .setData(bytes)
+ );
+
+ udpChannelBindInfo.getLockChannel().setResponseCount(udpChannelBindInfo.getLockChannel().getResponseCount() + 1);
+ }
+ }
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/ProxyMessageAuthHandler.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/ProxyMessageAuthHandler.java
new file mode 100644
index 00000000..5ae9219e
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/ProxyMessageAuthHandler.java
@@ -0,0 +1,46 @@
+package org.dromara.neutrinoproxy.client.sdk.handler;
+
+import io.netty.channel.ChannelHandlerContext;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.neutrinoproxy.client.sdk.config.ProxyConfig;
+import org.dromara.neutrinoproxy.core.Constants;
+import org.dromara.neutrinoproxy.core.ExceptionEnum;
+import org.dromara.neutrinoproxy.core.ProxyMessage;
+import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
+import org.dromara.neutrinoproxy.core.dispatcher.Match;
+import org.noear.snack.ONode;
+import org.noear.solon.Solon;
+
+/**
+ * 认证信息处理器
+ * @author: aoshiguchen
+ * @date: 2022/9/4
+ */
+@Slf4j
+@Match(type = Constants.ProxyDataTypeName.AUTH)
+public class ProxyMessageAuthHandler implements ProxyMessageHandler {
+ private ProxyConfig proxyConfig;
+
+ public ProxyMessageAuthHandler(ProxyConfig proxyConfig){
+ this.proxyConfig=proxyConfig;
+ }
+ @Override
+ public void handle(ChannelHandlerContext context, ProxyMessage proxyMessage) {
+ String info = proxyMessage.getInfo();
+ ONode load = ONode.load(info);
+ Integer code = load.get("code").getInt();
+ log.info("Auth result:{}", info);
+ if (ExceptionEnum.AUTH_FAILED.getCode().equals(code)) {
+ // 客户端认证失败,直接停止服务
+ log.info("client auth failed , client stop.");
+ context.channel().close();
+ if (!proxyConfig.getTunnel().getReconnection().getUnlimited()) {
+ Solon.stop();
+ }
+ } else if (ExceptionEnum.CONNECT_FAILED.getCode().equals(code) ||
+ ExceptionEnum.LICENSE_CANNOT_REPEAT_CONNECT.getCode().equals(code)
+ ){
+ context.channel().close();
+ }
+ }
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/ProxyMessageConnectHandler.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/ProxyMessageConnectHandler.java
new file mode 100644
index 00000000..804c2cc2
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/ProxyMessageConnectHandler.java
@@ -0,0 +1,91 @@
+package org.dromara.neutrinoproxy.client.sdk.handler;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.*;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.neutrinoproxy.client.sdk.config.ProxyConfig;
+import org.dromara.neutrinoproxy.client.sdk.core.ProxyChannelBorrowListener;
+import org.dromara.neutrinoproxy.client.sdk.util.ProxyUtil;
+import org.dromara.neutrinoproxy.core.Constants;
+import org.dromara.neutrinoproxy.core.ProxyDataTypeEnum;
+import org.dromara.neutrinoproxy.core.ProxyMessage;
+import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
+import org.dromara.neutrinoproxy.core.dispatcher.Match;
+
+/**
+ * 连接信息处理器
+ * @author: aoshiguchen
+ * @date: 2022/6/16
+ */
+@Slf4j
+@Match(type = Constants.ProxyDataTypeName.CONNECT)
+public class ProxyMessageConnectHandler implements ProxyMessageHandler {
+
+ private Bootstrap tcpProxyTunnelBootstrap;
+ private Bootstrap realServerBootstrap;
+ private ProxyConfig proxyConfig;
+
+ public ProxyMessageConnectHandler(Bootstrap tcpProxyTunnelBootstrap,Bootstrap realServerBootstrap,ProxyConfig proxyConfig){
+ this.proxyConfig=proxyConfig;
+ this.realServerBootstrap=realServerBootstrap;
+ this.tcpProxyTunnelBootstrap=tcpProxyTunnelBootstrap;
+ }
+
+ @Override
+ public void handle(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
+ final Channel cmdChannel = ctx.channel();
+ final String visitorId = proxyMessage.getInfo();
+ String[] serverInfo = new String(proxyMessage.getData()).split(":");
+ String ip = serverInfo[0];
+ int port = Integer.parseInt(serverInfo[1]);
+ // 连接真实的、被代理的服务
+ realServerBootstrap.connect(ip, port).addListener(new ChannelFutureListener() {
+
+ @Override
+ public void operationComplete(ChannelFuture future) throws Exception {
+
+ // 连接后端服务器成功
+ if (future.isSuccess()) {
+ final Channel realServerChannel = future.channel();
+
+ realServerChannel.config().setOption(ChannelOption.AUTO_READ, false);
+
+ // 获取连接
+ ProxyUtil.borrowTcpProxyChanel(tcpProxyTunnelBootstrap, new ProxyChannelBorrowListener() {
+
+ @Override
+ public void success(Channel channel) {
+ // 连接绑定
+ channel.attr(Constants.NEXT_CHANNEL).set(realServerChannel);
+ realServerChannel.attr(Constants.NEXT_CHANNEL).set(channel);
+
+ // 远程绑定
+ channel.writeAndFlush(ProxyMessage.buildConnectMessage(visitorId + "@" + proxyConfig.getTunnel().getLicenseKey()));
+
+ realServerChannel.config().setOption(ChannelOption.AUTO_READ, true);
+ ProxyUtil.addRealServerChannel(visitorId, realServerChannel);
+ ProxyUtil.setRealServerChannelVisitorId(realServerChannel, visitorId);
+ }
+
+ @Override
+ public void error(Throwable cause) {
+ ProxyMessage proxyMessage = new ProxyMessage();
+ proxyMessage.setType(ProxyMessage.TYPE_DISCONNECT);
+ proxyMessage.setInfo(visitorId);
+ cmdChannel.writeAndFlush(proxyMessage);
+ }
+ });
+
+ } else {
+ cmdChannel.writeAndFlush(ProxyMessage.buildDisconnectMessage(visitorId));
+ }
+ }
+ });
+ }
+
+ @Override
+ public String name() {
+ return ProxyDataTypeEnum.CONNECT.getDesc();
+ }
+
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/ProxyMessageDisconnectHandler.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/ProxyMessageDisconnectHandler.java
new file mode 100644
index 00000000..1534aae1
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/ProxyMessageDisconnectHandler.java
@@ -0,0 +1,40 @@
+package org.dromara.neutrinoproxy.client.sdk.handler;
+
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.neutrinoproxy.client.sdk.util.ProxyUtil;
+import org.dromara.neutrinoproxy.core.Constants;
+import org.dromara.neutrinoproxy.core.ProxyDataTypeEnum;
+import org.dromara.neutrinoproxy.core.ProxyMessage;
+import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
+import org.dromara.neutrinoproxy.core.dispatcher.Match;
+
+/**
+ * 断开连接信息处理器
+ * @author: aoshiguchen
+ * @date: 2022/6/16
+ */
+@Slf4j
+@Match(type = Constants.ProxyDataTypeName.DISCONNECT)
+public class ProxyMessageDisconnectHandler implements ProxyMessageHandler {
+
+ @Override
+ public void handle(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
+ Channel realServerChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
+ if (null != realServerChannel) {
+ ctx.channel().attr(Constants.NEXT_CHANNEL).remove();
+ ProxyUtil.returnTcpProxyChanel(ctx.channel());
+ realServerChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
+ }
+ ctx.close();
+ }
+
+ @Override
+ public String name() {
+ return ProxyDataTypeEnum.DISCONNECT.getDesc();
+ }
+
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/ProxyMessageErrorHandler.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/ProxyMessageErrorHandler.java
new file mode 100644
index 00000000..66ce7fea
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/ProxyMessageErrorHandler.java
@@ -0,0 +1,33 @@
+package org.dromara.neutrinoproxy.client.sdk.handler;
+
+import io.netty.channel.ChannelHandlerContext;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.neutrinoproxy.core.*;
+import org.dromara.neutrinoproxy.core.dispatcher.Match;
+import org.noear.snack.ONode;
+
+/**
+ * 异常信息处理器
+ * @author: aoshiguchen
+ * @date: 2022/6/16
+ */
+@Slf4j
+@Match(type = Constants.ProxyDataTypeName.ERROR)
+public class ProxyMessageErrorHandler implements ProxyMessageHandler {
+
+ @Override
+ public void handle(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
+ log.info("error: {}", proxyMessage.getInfo());
+ ONode load = ONode.load(proxyMessage.getInfo());
+ Integer code = load.get("code").getInt();
+ if (ExceptionEnum.AUTH_FAILED.getCode().equals(code)) {
+ System.exit(0);
+ }
+ }
+
+ @Override
+ public String name() {
+ return ProxyDataTypeEnum.DISCONNECT.getDesc();
+ }
+
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/ProxyMessageTransferHandler.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/ProxyMessageTransferHandler.java
new file mode 100644
index 00000000..1d2199aa
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/ProxyMessageTransferHandler.java
@@ -0,0 +1,41 @@
+package org.dromara.neutrinoproxy.client.sdk.handler;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.neutrinoproxy.core.Constants;
+import org.dromara.neutrinoproxy.core.ProxyDataTypeEnum;
+import org.dromara.neutrinoproxy.core.ProxyMessage;
+import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
+import org.dromara.neutrinoproxy.core.dispatcher.Match;
+
+/**
+ * 传输信息处理器
+ * @author: aoshiguchen
+ * @date: 2022/6/16
+ */
+@Match(type = Constants.ProxyDataTypeName.TRANSFER)
+@Slf4j
+public class ProxyMessageTransferHandler implements ProxyMessageHandler {
+
+ @Override
+ public void handle(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
+ Channel realServerChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
+ if (realServerChannel != null) {
+
+ // 自己可写,则设置来源可读。自己不可写,则设置来源不可读
+ ctx.channel().config().setAutoRead(realServerChannel.isWritable());
+
+ ByteBuf buf = ctx.alloc().buffer(proxyMessage.getData().length);
+ buf.writeBytes(proxyMessage.getData());
+ realServerChannel.writeAndFlush(buf);
+ }
+ }
+
+ @Override
+ public String name() {
+ return ProxyDataTypeEnum.TRANSFER.getDesc();
+ }
+
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/UdpProxyMessageConnectHandler.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/UdpProxyMessageConnectHandler.java
new file mode 100644
index 00000000..81dc9651
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/UdpProxyMessageConnectHandler.java
@@ -0,0 +1,66 @@
+package org.dromara.neutrinoproxy.client.sdk.handler;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.neutrinoproxy.client.sdk.config.ProxyConfig;
+import org.dromara.neutrinoproxy.client.sdk.core.ProxyChannelBorrowListener;
+import org.dromara.neutrinoproxy.client.sdk.util.ProxyUtil;
+import org.dromara.neutrinoproxy.core.Constants;
+import org.dromara.neutrinoproxy.core.ProxyDataTypeEnum;
+import org.dromara.neutrinoproxy.core.ProxyMessage;
+import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
+import org.dromara.neutrinoproxy.core.dispatcher.Match;
+import org.noear.snack.ONode;
+
+/**
+ * @author: aoshiguchen
+ * @date: 2023/9/19
+ */
+@Slf4j
+@Match(type = Constants.ProxyDataTypeName.UDP_CONNECT)
+public class UdpProxyMessageConnectHandler implements ProxyMessageHandler {
+
+ private ProxyConfig proxyConfig;
+ private Bootstrap udpProxyTunnelBootstrap;
+
+ public UdpProxyMessageConnectHandler(ProxyConfig proxyConfig,Bootstrap udpProxyTunnelBootstrap){
+ this.proxyConfig=proxyConfig;
+ this.udpProxyTunnelBootstrap=udpProxyTunnelBootstrap;
+ }
+
+ @Override
+ public void handle(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
+ final Channel cmdChannel = ctx.channel();
+ final ProxyMessage.UdpBaseInfo udpBaseInfo = ONode.deserialize(proxyMessage.getInfo(), ProxyMessage.UdpBaseInfo.class);
+ log.info("[UDP connect]info:{}", proxyMessage.getInfo());
+
+ // 获取连接
+ ProxyUtil.borrowUdpProxyChanel(udpProxyTunnelBootstrap, new ProxyChannelBorrowListener() {
+
+ @Override
+ public void success(Channel channel) {
+ channel.writeAndFlush(ProxyMessage.buildUdpConnectMessage(new ProxyMessage.UdpBaseInfo()
+ .setVisitorId(udpBaseInfo.getVisitorId())
+ .setServerPort(udpBaseInfo.getServerPort())
+ .setTargetIp(udpBaseInfo.getTargetIp())
+ .setTargetPort(udpBaseInfo.getTargetPort())
+ ).setData(proxyConfig.getTunnel().getLicenseKey().getBytes()));
+ }
+
+ @Override
+ public void error(Throwable cause) {
+ cmdChannel.writeAndFlush(ProxyMessage.buildDisconnectMessage(udpBaseInfo.toJsonString()));
+ }
+ });
+
+
+ }
+
+ @Override
+ public String name() {
+ return ProxyDataTypeEnum.UDP_CONNECT.getDesc();
+ }
+
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/UdpProxyMessageTransferHandler.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/UdpProxyMessageTransferHandler.java
new file mode 100644
index 00000000..2e97adfa
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/handler/UdpProxyMessageTransferHandler.java
@@ -0,0 +1,47 @@
+package org.dromara.neutrinoproxy.client.sdk.handler;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.socket.DatagramPacket;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.neutrinoproxy.client.sdk.util.UdpServerUtil;
+import org.dromara.neutrinoproxy.core.Constants;
+import org.dromara.neutrinoproxy.core.ProxyDataTypeEnum;
+import org.dromara.neutrinoproxy.core.ProxyMessage;
+import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
+import org.dromara.neutrinoproxy.core.dispatcher.Match;
+import org.noear.snack.ONode;
+
+import java.net.InetSocketAddress;
+
+/**
+ * @author: aoshiguchen
+ * @date: 2023/9/20
+ */
+@Slf4j
+@Match(type = Constants.ProxyDataTypeName.UDP_TRANSFER)
+public class UdpProxyMessageTransferHandler implements ProxyMessageHandler {
+
+ @Override
+ public void handle(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
+ final ProxyMessage.UdpBaseInfo udpBaseInfo = ONode.deserialize(proxyMessage.getInfo(), ProxyMessage.UdpBaseInfo.class);
+ log.debug("[UDP transfer]info:{} data:{}", proxyMessage.getInfo(), new String(proxyMessage.getData()));
+ Channel channel = UdpServerUtil.takeChannel(udpBaseInfo, ctx.channel());
+ if (null == channel) {
+ log.error("[UDP transfer] take udp channel failed.");
+ return;
+ }
+ log.debug("chid--->:{} port:{}", ctx.channel().id().asLongText(), ((InetSocketAddress)channel.localAddress()).getPort());
+ InetSocketAddress address = new InetSocketAddress(udpBaseInfo.getTargetIp(), udpBaseInfo.getTargetPort());
+ ByteBuf byteBuf = Unpooled.copiedBuffer(proxyMessage.getData());
+ channel.writeAndFlush(new DatagramPacket(byteBuf, address));
+ }
+
+ @Override
+ public String name() {
+ return ProxyDataTypeEnum.UDP_TRANSFER.getDesc();
+ }
+
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/solon/BeanHandler.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/solon/BeanHandler.java
new file mode 100644
index 00000000..ed8f3fb6
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/solon/BeanHandler.java
@@ -0,0 +1,19 @@
+package org.dromara.neutrinoproxy.client.sdk.solon;
+
+import org.dromara.neutrinoproxy.client.sdk.config.IBeanHandler;
+import org.dromara.neutrinoproxy.client.sdk.config.ProxyConfig;
+import org.dromara.neutrinoproxy.core.dispatcher.Dispatcher;
+import org.noear.solon.Solon;
+
+
+public class BeanHandler implements IBeanHandler {
+ @Override
+ public Dispatcher getDispatcher(){
+ return Solon.context().getBean(Dispatcher.class);
+ }
+
+ @Override
+ public ProxyConfig getProxyConfig() {
+ return Solon.context().getBean(ProxyConfig.class);
+ }
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/solon/ProxyConfiguration.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/solon/ProxyConfiguration.java
new file mode 100644
index 00000000..eaeddcf1
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/solon/ProxyConfiguration.java
@@ -0,0 +1,119 @@
+package org.dromara.neutrinoproxy.client.sdk.solon;
+
+import com.google.common.collect.Lists;
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.nio.NioEventLoopGroup;
+import org.dromara.neutrinoproxy.client.sdk.config.IBeanHandler;
+import org.dromara.neutrinoproxy.client.sdk.config.IProxyConfiguration;
+import org.dromara.neutrinoproxy.client.sdk.config.ProxyConfig;
+import org.dromara.neutrinoproxy.client.sdk.handler.*;
+import org.dromara.neutrinoproxy.core.ProxyDataTypeEnum;
+import org.dromara.neutrinoproxy.core.ProxyMessage;
+import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
+import org.dromara.neutrinoproxy.core.aot.NeutrinoCoreRuntimeNativeRegistrar;
+import org.dromara.neutrinoproxy.core.dispatcher.DefaultDispatcher;
+import org.dromara.neutrinoproxy.core.dispatcher.Dispatcher;
+import org.noear.solon.Solon;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Configuration;
+import org.noear.solon.annotation.Inject;
+import org.noear.solon.core.bean.LifecycleBean;
+
+import java.util.List;
+
+/**
+ * 代理配置
+ * @author: aoshiguchen
+ * @date: 2022/10/8
+ */
+@Configuration
+public class ProxyConfiguration extends IProxyConfiguration implements LifecycleBean {
+ @Inject
+ private ProxyConfig proxyConfig;
+ @Inject("tcpProxyTunnelBootstrap")
+ private Bootstrap tcpProxyTunnelBootstrap;
+ @Inject("realServerBootstrap")
+ private Bootstrap realServerBootstrap;
+
+ @Override
+ public void start() throws Throwable {
+ List list = Lists.newArrayList(
+ new ProxyMessageAuthHandler(proxyConfig),
+ new ProxyMessageConnectHandler(tcpProxyTunnelBootstrap,realServerBootstrap,proxyConfig),
+ new ProxyMessageDisconnectHandler(),
+ new ProxyMessageErrorHandler(),
+ new ProxyMessageTransferHandler(),
+ new UdpProxyMessageConnectHandler(proxyConfig,tcpProxyTunnelBootstrap),
+ new UdpProxyMessageTransferHandler()
+ );
+ Dispatcher dispatcher = new DefaultDispatcher<>("MessageDispatcher", list,
+ proxyMessage -> ProxyDataTypeEnum.of((int)proxyMessage.getType()) == null ?
+ null : ProxyDataTypeEnum.of((int)proxyMessage.getType()).getName());
+ Solon.context().wrapAndPut(Dispatcher.class, dispatcher);
+ }
+
+ @Override
+ public IBeanHandler getBeanHandler() {
+ return new BeanHandler();
+ }
+ @Bean("tunnelWorkGroup")
+ public NioEventLoopGroup tunnelWorkGroup(@Inject ProxyConfig proxyConfig) {
+ return super.tunnelWorkGroup(proxyConfig);
+ }
+
+ @Bean("tcpRealServerWorkGroup")
+ public NioEventLoopGroup tcpRealServerWorkGroup(@Inject ProxyConfig proxyConfig) {
+ // 暂时先公用此配置
+ return super.tcpRealServerWorkGroup(proxyConfig);
+ }
+
+ @Bean("udpServerGroup")
+ public NioEventLoopGroup udpServerGroup(@Inject ProxyConfig proxyConfig) {
+ // 暂时先公用此配置
+ return super.udpServerGroup(proxyConfig);
+ }
+
+ @Bean("udpWorkGroup")
+ public NioEventLoopGroup udpWorkGroup(@Inject ProxyConfig proxyConfig) {
+ // 暂时先公用此配置
+ return super.udpWorkGroup(proxyConfig);
+ }
+
+ @Bean("cmdTunnelBootstrap")
+ public Bootstrap cmdTunnelBootstrap(@Inject ProxyConfig proxyConfig,
+ @Inject("tunnelWorkGroup") NioEventLoopGroup tunnelWorkGroup) {
+ return super.cmdTunnelBootstrap(proxyConfig,tunnelWorkGroup);
+ }
+
+ @Bean("tcpProxyTunnelBootstrap")
+ public Bootstrap tcpProxyTunnelBootstrap(@Inject ProxyConfig proxyConfig,
+ @Inject("tunnelWorkGroup") NioEventLoopGroup tunnelWorkGroup) {
+ return super.tcpProxyTunnelBootstrap(proxyConfig,tunnelWorkGroup);
+ }
+
+ @Bean("udpProxyTunnelBootstrap")
+ public Bootstrap udpProxyTunnelBootstrap(@Inject ProxyConfig proxyConfig,
+ @Inject("tunnelWorkGroup") NioEventLoopGroup tunnelWorkGroup) {
+ return super.udpProxyTunnelBootstrap(proxyConfig,tunnelWorkGroup);
+ }
+
+ @Bean("realServerBootstrap")
+ public Bootstrap realServerBootstrap(@Inject ProxyConfig proxyConfig,
+ @Inject("tcpRealServerWorkGroup") NioEventLoopGroup tcpRealServerWorkGroup
+ ) {
+ return super.realServerBootstrap(proxyConfig,tcpRealServerWorkGroup);
+ }
+
+ @Bean("udpServerBootstrap")
+ public Bootstrap udpServerBootstrap(@Inject ProxyConfig proxyConfig,
+ @Inject("udpServerGroup") NioEventLoopGroup udpServerGroup,
+ @Inject("udpWorkGroup") NioEventLoopGroup udpWorkGroup) {
+ return super.udpServerBootstrap(proxyConfig,udpServerGroup,udpWorkGroup);
+ }
+
+ @Bean
+ public NeutrinoCoreRuntimeNativeRegistrar neutrinoCoreRuntimeNativeRegistrar() {
+ return super.neutrinoCoreRuntimeNativeRegistrar();
+ }
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/util/LockChannel.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/util/LockChannel.java
new file mode 100644
index 00000000..765e0ba1
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/util/LockChannel.java
@@ -0,0 +1,28 @@
+package org.dromara.neutrinoproxy.client.sdk.util;
+
+import io.netty.channel.Channel;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.Date;
+
+/**
+ * @author: aoshiguchen
+ * @date: 2023/9/21
+ */
+@Accessors(chain = true)
+@Data
+public class LockChannel {
+ // 端口号
+ private int port;
+ // 通道
+ private Channel channel;
+ // 期望的响应次数
+ private int proxyResponses;
+ // 超时时间(毫秒)
+ private long proxyTimeoutMs;
+ // 被获取的时间
+ private Date takeTime;
+ // 已经响应的次数
+ private int responseCount;
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/util/ProxyUtil.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/util/ProxyUtil.java
new file mode 100644
index 00000000..3e28da24
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/util/ProxyUtil.java
@@ -0,0 +1,226 @@
+/**
+ * Copyright (c) 2022 aoshiguchen
+ *
+ * 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
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package org.dromara.neutrinoproxy.client.sdk.util;
+
+import io.netty.channel.ChannelHandler;
+import io.netty.handler.ssl.SslHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.dromara.neutrinoproxy.client.sdk.config.ProxyConfig;
+import org.dromara.neutrinoproxy.client.sdk.core.ProxyChannelBorrowListener;
+import org.dromara.neutrinoproxy.core.Constants;
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelOption;
+import io.netty.util.AttributeKey;
+import org.dromara.neutrinoproxy.core.util.FileUtil;
+import org.noear.solon.Solon;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+/**
+ * 代理工具
+ * @author: aoshiguchen
+ * @date: 2022/8/31
+ */
+@Slf4j
+public class ProxyUtil {
+ private static final AttributeKey USER_CHANNEL_WRITEABLE = AttributeKey.newInstance("user_channel_writeable");
+
+ private static final AttributeKey CLIENT_CHANNEL_WRITEABLE = AttributeKey.newInstance("client_channel_writeable");
+
+ private static final int MAX_POOL_SIZE = 100;
+
+ private static Map realServerChannels = new ConcurrentHashMap();
+
+ private static ConcurrentLinkedQueue tcpProxyChannelPool = new ConcurrentLinkedQueue();
+ private static ConcurrentLinkedQueue udpProxyChannelPool = new ConcurrentLinkedQueue<>();
+
+ private static volatile Channel cmdChannel;
+
+ private static String clientId;
+ private static final String CLIENT_ID_FILE = ".NEUTRINO_PROXY_CLIENT_ID";
+
+ public static void borrowTcpProxyChanel(Bootstrap tcpProxyTunnelBootstrap, final ProxyChannelBorrowListener borrowListener) {
+ Channel channel = tcpProxyChannelPool.poll();
+ if (null != channel) {
+ borrowListener.success(channel);
+ return;
+ }
+
+ tcpProxyTunnelBootstrap.connect().addListener((ChannelFutureListener) future -> {
+ if (future.isSuccess()) {
+ borrowListener.success(future.channel());
+ } else {
+ borrowListener.error(future.cause());
+ }
+ });
+ }
+
+ public static void returnTcpProxyChanel(Channel proxyChanel) {
+ if (tcpProxyChannelPool.size() > MAX_POOL_SIZE) {
+ proxyChanel.close();
+ } else {
+ proxyChanel.config().setOption(ChannelOption.AUTO_READ, true);
+ proxyChanel.attr(Constants.NEXT_CHANNEL).remove();
+ tcpProxyChannelPool.offer(proxyChanel);
+ }
+ }
+
+
+
+ public static void removeTcpProxyChanel(Channel proxyChanel) {
+ tcpProxyChannelPool.remove(proxyChanel);
+ }
+
+ public static void borrowUdpProxyChanel(Bootstrap tcpProxyTunnelBootstrap, final ProxyChannelBorrowListener borrowListener) {
+ Channel channel = udpProxyChannelPool.poll();
+ if (null != channel) {
+ borrowListener.success(channel);
+ return;
+ }
+
+ tcpProxyTunnelBootstrap.connect().addListener((ChannelFutureListener) future -> {
+ if (future.isSuccess()) {
+ borrowListener.success(future.channel());
+ } else {
+ borrowListener.error(future.cause());
+ }
+ });
+ }
+
+ public static void returnUdpProxyChanel(Channel proxyChanel) {
+ if (udpProxyChannelPool.size() > MAX_POOL_SIZE) {
+ proxyChanel.close();
+ } else {
+ proxyChanel.config().setOption(ChannelOption.AUTO_READ, true);
+ proxyChanel.attr(Constants.NEXT_CHANNEL).remove();
+ udpProxyChannelPool.offer(proxyChanel);
+ }
+ }
+
+
+
+ public static void removeUdpProxyChanel(Channel proxyChanel) {
+ udpProxyChannelPool.remove(proxyChanel);
+ }
+
+ public static void setCmdChannel(Channel cmdChannel) {
+ ProxyUtil.cmdChannel = cmdChannel;
+ }
+
+ public static Channel getCmdChannel() {
+ return cmdChannel;
+ }
+
+ public static void setRealServerChannelVisitorId(Channel realServerChannel, String visitorId) {
+ realServerChannel.attr(Constants.VISITOR_ID).set(visitorId);
+ }
+
+ public static String getVisitorIdByRealServerChannel(Channel realServerChannel) {
+ return realServerChannel.attr(Constants.VISITOR_ID).get();
+ }
+
+ public static Channel getRealServerChannel(String userId) {
+ return realServerChannels.get(userId);
+ }
+
+ public static void addRealServerChannel(String userId, Channel realServerChannel) {
+ realServerChannels.put(userId, realServerChannel);
+ }
+
+ public static Channel removeRealServerChannel(String userId) {
+ return realServerChannels.remove(userId);
+ }
+
+ public static boolean isRealServerReadable(Channel realServerChannel) {
+ return realServerChannel.attr(CLIENT_CHANNEL_WRITEABLE).get() && realServerChannel.attr(USER_CHANNEL_WRITEABLE).get();
+ }
+
+ public static void clearRealServerChannels() {
+ Iterator> ite = realServerChannels.entrySet().iterator();
+ while (ite.hasNext()) {
+ Channel realServerChannel = ite.next().getValue();
+ if (realServerChannel.isActive()) {
+ realServerChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
+ }
+ }
+
+ realServerChannels.clear();
+ }
+
+ public static String getClientId() {
+ if (StringUtils.isNotBlank(clientId)) {
+ return clientId;
+ }
+ ProxyConfig proxyConfig = Solon.context().getBean(ProxyConfig.class);
+ if (StringUtils.isNotBlank(proxyConfig.getTunnel().getClientId())) {
+ clientId = proxyConfig.getTunnel().getClientId();
+ return clientId;
+ }
+ String id = FileUtil.readContentAsString(CLIENT_ID_FILE);
+ if (StringUtils.isNotBlank(id)) {
+ clientId = id;
+ return id;
+ }
+ id = UUID.randomUUID().toString().replace("-", "");
+ FileUtil.write(CLIENT_ID_FILE, id);
+ clientId = id;
+ return id;
+ }
+
+ public static ChannelHandler createSslHandler(ProxyConfig proxyConfig) {
+ try {
+ InputStream jksInputStream = FileUtil.getInputStream(proxyConfig.getTunnel().getJksPath());
+
+ SSLContext clientContext = SSLContext.getInstance("TLS");
+ final KeyStore ks = KeyStore.getInstance("JKS");
+ ks.load(jksInputStream, proxyConfig.getTunnel().getKeyStorePassword().toCharArray());
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(ks);
+ TrustManager[] trustManagers = tmf.getTrustManagers();
+ clientContext.init(null, trustManagers, null);
+
+ SSLEngine sslEngine = clientContext.createSSLEngine();
+ sslEngine.setUseClientMode(true);
+
+ return new SslHandler(sslEngine);
+ } catch (Exception e) {
+ log.error("create SSL handler failed", e);
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/util/UdpChannelBindInfo.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/util/UdpChannelBindInfo.java
new file mode 100644
index 00000000..21fa5366
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/util/UdpChannelBindInfo.java
@@ -0,0 +1,22 @@
+package org.dromara.neutrinoproxy.client.sdk.util;
+
+import io.netty.channel.Channel;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * @author: aoshiguchen
+ * @date: 2023/9/21
+ */
+@Accessors(chain = true)
+@Data
+public class UdpChannelBindInfo {
+ private Channel tunnelChannel;
+ private LockChannel lockChannel;
+ private String visitorId;
+ private String visitorIp;
+ private int visitorPort;
+ private int serverPort;
+ private String targetIp;
+ private int targetPort;
+}
diff --git a/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/util/UdpServerUtil.java b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/util/UdpServerUtil.java
new file mode 100644
index 00000000..ba909d6d
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/java/org/dromara/neutrinoproxy/client/sdk/util/UdpServerUtil.java
@@ -0,0 +1,201 @@
+package org.dromara.neutrinoproxy.client.sdk.util;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.dromara.neutrinoproxy.client.sdk.config.ProxyConfig;
+import org.dromara.neutrinoproxy.client.sdk.constant.Constants;
+import org.dromara.neutrinoproxy.client.sdk.core.CustomThreadFactory;
+import org.dromara.neutrinoproxy.core.ProxyMessage;
+import org.noear.solon.core.runtime.NativeDetector;
+
+import java.util.*;
+import java.util.concurrent.*;
+
+/**
+ * @author: aoshiguchen
+ * @date: 2023/9/21
+ */
+@Slf4j
+public class UdpServerUtil {
+ private static final Boolean isSupportUdp = Boolean.FALSE;
+ private static int udpServerPortMin = 0;
+ private static int udpServerPortMax = 0;
+ private static int nextUdpServerPort = 0;
+ private static Bootstrap udpServerBootstrap;
+ private static int defaultUdpServerPort;
+ private static Channel defaultUdpServerChannel;
+ private static Map portToChannelMap = new ConcurrentHashMap<>();
+ /**
+ * udp服务空闲端口池
+ */
+ private static ConcurrentLinkedQueue udpServerFreePortPool = new ConcurrentLinkedQueue<>();
+ private static List lockChannelList = new ArrayList<>();
+ /**
+ * lockChannel扫描器
+ */
+ private static final ScheduledExecutorService lockChannelScanner = Executors.newSingleThreadScheduledExecutor(new CustomThreadFactory("lockChannelScanner"));
+
+ /**
+ * 初始化UDP缓存
+ * 1、初始化一个基础UDP服务,用于不需要响应的UDP转发
+ * 2、维护一个UDP服务池,用于需要响应的UDP转发
+ * @param proxyConfig
+ */
+ public static void initCache(ProxyConfig proxyConfig, Bootstrap udpServerBootstrap) {
+ // aot 阶段,不初始化UDP服务
+ if (NativeDetector.isAotRuntime()) {
+ return;
+ }
+ if (null == proxyConfig.getClient().getUdp() || StringUtils.isEmpty(proxyConfig.getClient().getUdp().getPuppetPortRange())) {
+ return;
+ }
+ ProxyConfig.Udp udpConfig = proxyConfig.getClient().getUdp();
+ if (StringUtils.isEmpty(udpConfig.getPuppetPortRange())) {
+ return;
+ }
+ String[] tmp = udpConfig.getPuppetPortRange().split("-");
+ if (null == tmp || tmp.length != 2) {
+ log.error("client udp config error!");
+ return;
+ }
+ try {
+ udpServerPortMin = Integer.parseInt(tmp[0]);
+ udpServerPortMax = Integer.parseInt(tmp[1]);
+ if (udpServerPortMax <= udpServerPortMin) {
+ // 至少得给1个udp端口,一个用于基础无响应UDP转发
+ throw new RuntimeException("client udp config error!");
+ }
+ nextUdpServerPort = udpServerPortMin;
+ UdpServerUtil.udpServerBootstrap = udpServerBootstrap;
+ log.info("udp proxy server port: {} ~ {}", udpServerPortMin, udpServerPortMax);
+ // 初始化udp服务
+ initUdpServer();
+ // 初始化lockChannel扫描器
+ lockChannelScanner.scheduleWithFixedDelay(UdpServerUtil::lockChannelScan, 5, 3, TimeUnit.SECONDS);
+ } catch (Exception e) {
+ log.error("client udp config error!", e);
+ return;
+ }
+ }
+
+ /**
+ * 初始化udp服务
+ */
+ private static void initUdpServer() {
+ defaultUdpServerPort = nextUdpServerPort();
+ defaultUdpServerChannel = bindPort(defaultUdpServerPort);
+ // 初始化默认最多额外开启5个udp服务,其他的需要时再启动
+ for (int i = 0; i < 5; i++) {
+ if (!hasNextUdpServerPort()) {
+ return;
+ }
+ int port = nextUdpServerPort();
+ Channel ch = bindPort(port);
+ portToChannelMap.put(port, ch);
+ udpServerFreePortPool.offer(port);
+ }
+ while (hasNextUdpServerPort()) {
+ udpServerFreePortPool.offer(nextUdpServerPort());
+ }
+ }
+
+ private static Channel bindPort(int port) {
+ try {
+ ChannelFuture channelFuture = udpServerBootstrap.bind(port).sync();
+ log.info("[udp server] bind port:{} success!", port);
+ return channelFuture.channel();
+ } catch (InterruptedException e) {
+ log.error("[udp server] bind port:{} error!", port);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static Boolean hasNextUdpServerPort() {
+ return nextUdpServerPort <= udpServerPortMax;
+ }
+
+ public static synchronized int nextUdpServerPort() {
+ return nextUdpServerPort++;
+ }
+
+ /**
+ * 获取一个可用的udp通道
+ * 1、如果期待的响应为0,或者超时时间<=0,则认为不需要响应,直接返回默认的udp服务,否则继续下一步
+ * 2、从可用端口队列中找到一个可用端口,若不存在可用端口,则降级为不需要响应,返回默认的udp服务。否则继续下一步
+ * 3、根据该端口找到udp服务通道,找不到则绑定端口开启一个通道并返回。将该端口添加到锁定列表
+ * 4、维护一个定时器的,定时扫描锁定列表,及时释放锁定的端口
+ * @param info
+ * @return
+ */
+ public static synchronized Channel takeChannel(ProxyMessage.UdpBaseInfo info, Channel tunnelChannel) {
+ if (info.getProxyResponses() <= 0 || info.getProxyTimeoutMs() <= 0) {
+ return defaultUdpServerChannel;
+ }
+ Integer port = udpServerFreePortPool.poll();
+ if (null == port) {
+ return defaultUdpServerChannel;
+ }
+ Channel channel = portToChannelMap.get(port);
+ if (null == channel) {
+ channel = bindPort(port);
+ portToChannelMap.put(port, channel);
+ }
+ // 添加到锁定队列
+ LockChannel lockChannel = new LockChannel()
+ .setPort(port)
+ .setChannel(channel)
+ .setProxyResponses(info.getProxyResponses())
+ .setProxyTimeoutMs(info.getProxyTimeoutMs())
+ .setTakeTime(new Date())
+ .setResponseCount(0);
+ lockChannelList.add(lockChannel);
+ channel.attr(Constants.UDP_CHANNEL_BIND_KEY).set(new UdpChannelBindInfo()
+ .setTunnelChannel(tunnelChannel)
+ .setVisitorId(info.getVisitorId())
+ .setVisitorIp(info.getVisitorIp())
+ .setVisitorPort(info.getVisitorPort())
+ .setServerPort(info.getServerPort())
+ .setTargetIp(info.getTargetIp())
+ .setTargetPort(info.getTargetPort())
+ .setLockChannel(lockChannel)
+ );
+ return channel;
+ }
+
+ /**
+ * lockChannel扫描
+ */
+ public static synchronized void lockChannelScan() {
+ if (lockChannelList.isEmpty()) {
+ return;
+ }
+ Iterator iter = lockChannelList.iterator();
+ if (iter.hasNext()) {
+ LockChannel lockChannel = iter.next();
+ if (lockChannel.getResponseCount() >= lockChannel.getProxyResponses() ||
+ System.currentTimeMillis() - lockChannel.getTakeTime().getTime() >= lockChannel.getProxyTimeoutMs()
+ ) {
+ iter.remove();
+ UdpChannelBindInfo udpChannelBindInfo = lockChannel.getChannel().attr(Constants.UDP_CHANNEL_BIND_KEY).get();
+ // 此处必须释放代理隧道
+ closeChannel(udpChannelBindInfo.getTunnelChannel());
+ lockChannel.getChannel().attr(Constants.UDP_CHANNEL_BIND_KEY).set(null);
+ udpServerFreePortPool.offer(lockChannel.getPort());
+ log.debug("[udp channel]release udp channel port:{}", lockChannel.getPort());
+ }
+ }
+ }
+
+ private static void closeChannel(Channel channel) {
+ try {
+ channel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+}
diff --git a/neutrino-proxy-client-sdk/src/main/resources/app.yml b/neutrino-proxy-client-sdk/src/main/resources/app.yml
new file mode 100644
index 00000000..a122416c
--- /dev/null
+++ b/neutrino-proxy-client-sdk/src/main/resources/app.yml
@@ -0,0 +1,68 @@
+solon:
+ config:
+ add: ./app.yml
+ app:
+ name: neutrino-proxy-client
+ version: 2.0.1
+# 日志级别
+solon.logging.appender:
+ console:
+ pattern: "%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %magenta(${PID:-}) --- %-15([%15.15thread]) %-56(%cyan(%-40.40logger{39}%L)) : %msg%n"
+ file:
+ enable: true
+ pattern: "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:-} --- %-15([%15.15thread]) %-56(%-40.40logger{39}%L) : %msg%n"
+ name: "logs/${neutrino.application.name}"
+ rolling: "logs/${neutrino.application.name}_%d{yyyy-MM-dd}_%i.log.gz"
+solon.logging.logger:
+ "root":
+ level: info
+
+neutrino:
+ application:
+ name: neutrino-proxy-client
+
+ proxy:
+ protocol:
+ max-frame-length: 2097152
+ length-field-offset: 0
+ length-field-length: 4
+ initial-bytes-to-strip: 0
+ length-adjustment: 0
+ read-idle-time: 120
+ write-idle-time: 20
+ all-idle-time-seconds: 0
+ tunnel:
+ # 线程池相关配置,用于技术调优,可忽略
+ thread-count: 50
+ # 隧道SSL证书配置
+ key-store-password: ${STORE_PASS:123456}
+ jks-path: ${JKS_PATH:classpath:/test.jks}
+ # 服务端IP
+ server-ip: ${SERVER_IP:localhost}
+ # 服务端端口(对应服务端app.yml中的tunnel.port、tunnel.ssl-port)
+ server-port: ${SERVER_PORT:9002}
+ # 是否启用SSL(注意:该配置必须和server-port对应上)
+ ssl-enable: ${SSL_ENABLE:true}
+ # 客户端连接唯一凭证
+ license-key: ${LICENSE_KEY:}
+ # 客户端唯一身份标识(可忽略,若不设置首次启动会自动生成)
+ client-id: ${CLIENT_ID:}
+ # 是否开启隧道传输报文日志(日志级别为debug时开启才有效)
+ transfer-log-enable: ${CLIENT_LOG:false}
+ # 是否开启心跳日志
+ heartbeat-log-enable: ${HEARTBEAT_LOG:false}
+ # 重连设置
+ reconnection:
+ # 重连间隔(秒)
+ interval-seconds: 10
+ # 是否开启无限重连(未开启时,客户端license不合法会自动停止应用,开启了则不会,请谨慎开启)
+ unlimited: false
+ client:
+ udp:
+ # 线程池相关配置,用于技术调优,可忽略
+ boss-thread-count: 5
+ work-thread-count: 20
+ # udp傀儡端口范围
+ puppet-port-range: 10000-10500
+ # 是否开启隧道传输报文日志(日志级别为debug时开启才有效)
+ transfer-log-enable: ${CLIENT_LOG:false}
diff --git a/neutrino-proxy-client-sdk/src/main/resources/test.jks b/neutrino-proxy-client-sdk/src/main/resources/test.jks
new file mode 100644
index 00000000..3a4d1ad8
Binary files /dev/null and b/neutrino-proxy-client-sdk/src/main/resources/test.jks differ
diff --git a/pom.xml b/pom.xml
index 46b7b52b..70cd80d5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,6 +20,7 @@
neutrino-proxy-core
neutrino-proxy-client
neutrino-proxy-server
+ neutrino-proxy-client-sdk