Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c828f814c | ||
|
|
5a65eebfd3 | ||
|
|
5a3375ce80 | ||
|
|
eb277b414a | ||
|
|
fc4097529f | ||
|
|
375687aa5f | ||
|
|
32504d33af | ||
|
|
bcd429d54f | ||
|
|
2ec87abda9 | ||
|
|
8c2e1b8f6c | ||
|
|
e2195ab966 | ||
|
|
2d0b9132e9 | ||
|
|
5a502115b1 | ||
|
|
a972e0f309 | ||
|
|
b52225937c | ||
|
|
98737f0e9d | ||
|
|
ea67c2bce0 | ||
|
|
7dda047d51 | ||
|
|
b518dbfd58 | ||
|
|
7054a1df0e | ||
|
|
da0d1946c6 |
+2
-1
@@ -54,4 +54,5 @@ hs_err_pid*
|
||||
|
||||
**/memo/**
|
||||
|
||||
neutrino-proxy-vuepress/deploy.sh
|
||||
neutrino-proxy-vuepress/deploy.sh
|
||||
.NEUTRINO_PROXY_CLIENT_ID
|
||||
|
||||
@@ -47,38 +47,52 @@
|
||||
keytool -genkey -alias test1 -keyalg RSA -keysize 1024 -validity 3650 -keypass 123456 -storepass 123456 -keystore "./test.jks"
|
||||
```
|
||||
|
||||
## 5.2、修改服务端配置(application.yml)
|
||||
## 5.2、修改服务端配置(app.yml)
|
||||
```yml
|
||||
application:
|
||||
name: neutrino-proxy-server
|
||||
|
||||
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: 60
|
||||
write-idle-time: 40
|
||||
all-idle-time-seconds: 0
|
||||
server:
|
||||
# 服务端端口,用于保持与客户端的连接,非SSL
|
||||
port: 9000
|
||||
# 服务端端口,用于保持与客户端的连接,SSL,需要jks证书文件,若不需要ssl支持,可不配置
|
||||
ssl-port: 9002
|
||||
# 证书密码
|
||||
key-store-password: 123456
|
||||
key-manager-password: 123456
|
||||
# 证书存放路径,若不想打进jar包,可不带classpath:前缀
|
||||
jks-path: classpath:/test.jks
|
||||
data:
|
||||
# 数据库配置(支持mysql)
|
||||
type: sqlite
|
||||
url: jdbc:sqlite:data.db
|
||||
driver-class: org.sqlite.JDBC
|
||||
username:
|
||||
password:
|
||||
neutrino:
|
||||
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: 60
|
||||
write-idle-time: 40
|
||||
all-idle-time-seconds: 0
|
||||
tunnel:
|
||||
boss-thread-count: 2
|
||||
work-thread-count: 10
|
||||
# 服务端端口,用于保持与客户端的连接,非SSL
|
||||
port: ${OPEN_PORT:9000}
|
||||
# 服务端端口,用于保持与客户端的连接,SSL,需要jks证书文件,若不需要ssl支持,可不配置
|
||||
ssl-port: ${SSL_PORT:9002}
|
||||
# 证书配置,用于隧道通信SSL加密
|
||||
key-store-password: ${STORE_PASS:123456}
|
||||
key-manager-password: ${MGR_PASS:123456}
|
||||
jks-path: ${JKS_PATH:classpath:/test.jks}
|
||||
server:
|
||||
boss-thread-count: 5
|
||||
work-thread-count: 20
|
||||
# HTTP代理端口,默认80,也可以用其他端口,走nginx转发
|
||||
http-proxy-port: ${HTTP_PROXY_PORT:80}
|
||||
# HTTPS代理端口,默认443,也可以用其他端口,走nginx转发
|
||||
https-proxy-port: ${HTTPS_PROXY_PORT:443}
|
||||
# 如果不配置,则不支持域名映射
|
||||
domain-name: ${DOMAIN_NAME:}
|
||||
# 证书配置,用于支持HTTPS
|
||||
key-store-password: ${HTTPS_STORE_PASS:}
|
||||
jks-path: ${HTTPS_JKS_PATH:}
|
||||
data:
|
||||
# 数据库配置(支持mysql)
|
||||
type: sqlite
|
||||
url: jdbc:sqlite:data.db
|
||||
driver-class: org.sqlite.JDBC
|
||||
username:
|
||||
password:
|
||||
```
|
||||
|
||||
## 5.3、启动服务端
|
||||
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* 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.config;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/10/18
|
||||
*/
|
||||
@Data
|
||||
public class CustomConfig {
|
||||
private String jksPath;
|
||||
private String serverIp;
|
||||
private Integer serverPort;
|
||||
private Boolean sslEnable;
|
||||
private String licenseKey;
|
||||
}
|
||||
+1
@@ -39,5 +39,6 @@ public class ProxyConfig {
|
||||
private Integer obtainLicenseInterval;
|
||||
private String licenseKey;
|
||||
private Integer threadCount;
|
||||
private String clientId;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -31,8 +31,13 @@ public class ProxyConfiguration implements LifecycleBean {
|
||||
Solon.context().wrapAndPut(Dispatcher.class, dispatcher);
|
||||
}
|
||||
|
||||
@Bean("bootstrap")
|
||||
public Bootstrap bootstrap() {
|
||||
@Bean("cmdTunnelBootstrap")
|
||||
public Bootstrap cmdTunnelBootstrap() {
|
||||
return new Bootstrap();
|
||||
}
|
||||
|
||||
@Bean("proxyTunnelBootstrap")
|
||||
public Bootstrap proxyTunnelBootstrap() {
|
||||
return new Bootstrap();
|
||||
}
|
||||
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package org.dromara.neutrinoproxy.client.core;
|
||||
|
||||
import org.dromara.neutrinoproxy.client.util.ProxyUtil;
|
||||
import org.dromara.neutrinoproxy.core.Constants;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessage;
|
||||
import org.dromara.neutrinoproxy.core.dispatcher.Dispatcher;
|
||||
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.noear.solon.Solon;
|
||||
|
||||
/**
|
||||
* 处理与服务端之间的数据传输
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
@Slf4j
|
||||
public class CmdChannelHandler extends SimpleChannelInboundHandler<ProxyMessage> {
|
||||
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, ProxyMessage proxyMessage) throws Exception {
|
||||
if (ProxyMessage.TYPE_HEARTBEAT != proxyMessage.getType()) {
|
||||
log.info("Client CmdChannel recieved proxy message, type is {}", proxyMessage.getType());
|
||||
}
|
||||
Solon.context().getBean(Dispatcher.class).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("Client CmdChannel 与服务端断开连接");
|
||||
ProxyUtil.setCmdChannel(null);
|
||||
ProxyUtil.clearRealServerChannels();
|
||||
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
log.error("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.info("读超时");
|
||||
// ctx.channel().close();
|
||||
break;
|
||||
case WRITER_IDLE:
|
||||
ctx.channel().writeAndFlush(ProxyMessage.buildHeartbeatMessage());
|
||||
break;
|
||||
case ALL_IDLE:
|
||||
log.info("Client CmdChannel 读写超时");
|
||||
ctx.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-22
@@ -1,15 +1,15 @@
|
||||
package org.dromara.neutrinoproxy.client.core;
|
||||
|
||||
import org.dromara.neutrinoproxy.client.util.ProxyUtil;
|
||||
import org.dromara.neutrinoproxy.core.Constants;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessage;
|
||||
import org.dromara.neutrinoproxy.core.dispatcher.Dispatcher;
|
||||
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.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;
|
||||
|
||||
/**
|
||||
@@ -18,13 +18,13 @@ import org.noear.solon.Solon;
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
@Slf4j
|
||||
public class ClientChannelHandler extends SimpleChannelInboundHandler<ProxyMessage> {
|
||||
public class ProxyChannelHandler extends SimpleChannelInboundHandler<ProxyMessage> {
|
||||
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, ProxyMessage proxyMessage) throws Exception {
|
||||
if (ProxyMessage.TYPE_HEARTBEAT != proxyMessage.getType()) {
|
||||
log.info("recieved proxy message, type is {}", proxyMessage.getType());
|
||||
log.info("Client ProxyChannel recieved proxy message, type is {}", proxyMessage.getType());
|
||||
}
|
||||
Solon.context().getBean(Dispatcher.class).dispatch(ctx, proxyMessage);
|
||||
}
|
||||
@@ -41,17 +41,10 @@ public class ClientChannelHandler extends SimpleChannelInboundHandler<ProxyMessa
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
// 控制连接
|
||||
if (ProxyUtil.getCmdChannel() == ctx.channel()) {
|
||||
log.info("与服务端断开连接");
|
||||
ProxyUtil.setCmdChannel(null);
|
||||
ProxyUtil.clearRealServerChannels();
|
||||
} else {
|
||||
// 数据传输连接
|
||||
Channel realServerChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
|
||||
if (realServerChannel != null && realServerChannel.isActive()) {
|
||||
realServerChannel.close();
|
||||
}
|
||||
// 数据传输连接
|
||||
Channel realServerChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
|
||||
if (realServerChannel != null && realServerChannel.isActive()) {
|
||||
realServerChannel.close();
|
||||
}
|
||||
|
||||
ProxyUtil.removeProxyChanel(ctx.channel());
|
||||
@@ -60,10 +53,8 @@ public class ClientChannelHandler extends SimpleChannelInboundHandler<ProxyMessa
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
super.exceptionCaught(ctx, cause);
|
||||
if (ctx.channel().isActive()) {
|
||||
ctx.channel().close();
|
||||
}
|
||||
log.error("Client ProxyChannel Error channelId:{}", ctx.channel().id().asLongText(), cause);
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,7 +72,7 @@ public class ClientChannelHandler extends SimpleChannelInboundHandler<ProxyMessa
|
||||
break;
|
||||
case ALL_IDLE:
|
||||
log.info("读写超时");
|
||||
ctx.channel().close();
|
||||
ctx.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
+71
-29
@@ -1,7 +1,6 @@
|
||||
package org.dromara.neutrinoproxy.client.core;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.netty.handler.logging.LoggingHandler;
|
||||
import org.dromara.neutrinoproxy.client.config.ProxyConfig;
|
||||
import org.dromara.neutrinoproxy.client.util.ProxyUtil;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessage;
|
||||
@@ -26,13 +25,14 @@ import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.security.KeyStore;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 客户端服务
|
||||
* 代理客户端服务
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
@@ -41,8 +41,10 @@ import java.util.concurrent.TimeUnit;
|
||||
public class ProxyClientService {
|
||||
@Inject
|
||||
private ProxyConfig proxyConfig;
|
||||
@Inject("bootstrap")
|
||||
private Bootstrap bootstrap;
|
||||
@Inject("cmdTunnelBootstrap")
|
||||
private Bootstrap cmdTunnelBootstrap;
|
||||
@Inject("proxyTunnelBootstrap")
|
||||
private Bootstrap proxyTunnelBootstrap;
|
||||
@Inject("realServerBootstrap")
|
||||
private Bootstrap realServerBootstrap;
|
||||
private volatile Channel channel;
|
||||
@@ -54,10 +56,11 @@ public class ProxyClientService {
|
||||
* 重连次数
|
||||
*/
|
||||
private volatile int reconnectCount = 0;
|
||||
/**
|
||||
* 启用重连服务
|
||||
*/
|
||||
private volatile boolean reconnectServiceEnable = false;
|
||||
// /**
|
||||
// * 启用重连服务
|
||||
// */
|
||||
// private volatile boolean reconnectServiceEnable = false;
|
||||
private NioEventLoopGroup workerGroup;
|
||||
/**
|
||||
* 重连服务执行器
|
||||
*/
|
||||
@@ -65,9 +68,9 @@ public class ProxyClientService {
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
this.reconnectExecutor.scheduleWithFixedDelay(this::reconnect, 0, RECONNECT_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
||||
this.reconnectExecutor.scheduleWithFixedDelay(this::reconnect, 10, RECONNECT_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
||||
this.workerGroup = new NioEventLoopGroup(proxyConfig.getClient().getThreadCount());
|
||||
|
||||
NioEventLoopGroup workerGroup = new NioEventLoopGroup(proxyConfig.getClient().getThreadCount());
|
||||
realServerBootstrap.group(workerGroup);
|
||||
realServerBootstrap.channel(NioSocketChannel.class);
|
||||
realServerBootstrap.handler(new ChannelInitializer<SocketChannel>() {
|
||||
@@ -78,9 +81,10 @@ public class ProxyClientService {
|
||||
}
|
||||
});
|
||||
|
||||
bootstrap.group(workerGroup);
|
||||
bootstrap.channel(NioSocketChannel.class);
|
||||
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
|
||||
proxyTunnelBootstrap.group(workerGroup);
|
||||
proxyTunnelBootstrap.channel(NioSocketChannel.class);
|
||||
proxyTunnelBootstrap.remoteAddress(InetSocketAddress.createUnresolved(proxyConfig.getClient().getServerIp(), proxyConfig.getClient().getServerPort()));
|
||||
proxyTunnelBootstrap.handler(new ChannelInitializer<SocketChannel>() {
|
||||
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) throws Exception {
|
||||
@@ -93,10 +97,44 @@ public class ProxyClientService {
|
||||
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 ClientChannelHandler());
|
||||
ch.pipeline().addLast(new ProxyChannelHandler());
|
||||
}
|
||||
});
|
||||
this.start();
|
||||
|
||||
cmdTunnelBootstrap.group(workerGroup);
|
||||
cmdTunnelBootstrap.channel(NioSocketChannel.class);
|
||||
// cmdTunnelBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000);
|
||||
// cmdTunnelBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
|
||||
// /**
|
||||
// * TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此,TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据)。
|
||||
// * Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。
|
||||
// */
|
||||
// cmdTunnelBootstrap.option(ChannelOption.TCP_NODELAY, true);
|
||||
cmdTunnelBootstrap.remoteAddress(InetSocketAddress.createUnresolved(proxyConfig.getClient().getServerIp(), proxyConfig.getClient().getServerPort()));
|
||||
|
||||
cmdTunnelBootstrap.handler(new ChannelInitializer<SocketChannel>() {
|
||||
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) throws Exception {
|
||||
if (proxyConfig.getClient().getSslEnable()) {
|
||||
ch.pipeline().addLast(createSslHandler());
|
||||
}
|
||||
// ch.pipeline().addFirst(new LoggingHandler(ProxyClientService.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());
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
this.start();
|
||||
} catch (Exception e) {
|
||||
// 启动连不上也做一下重连,因此先catch异常
|
||||
log.error("[客户端指令隧道] 启动异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@@ -111,7 +149,7 @@ public class ProxyClientService {
|
||||
return;
|
||||
}
|
||||
if (null != proxyConfig.getClient().getSslEnable() && proxyConfig.getClient().getSslEnable()
|
||||
&& StrUtil.isEmpty(proxyConfig.getClient().getJksPath())) {
|
||||
&& StrUtil.isEmpty(proxyConfig.getClient().getJksPath())) {
|
||||
log.error("not found jks-path config.");
|
||||
Solon.stop();
|
||||
return;
|
||||
@@ -128,7 +166,7 @@ public class ProxyClientService {
|
||||
log.error("client start error", e);
|
||||
}
|
||||
} else {
|
||||
channel.writeAndFlush(ProxyMessage.buildAuthMessage(proxyConfig.getClient().getLicenseKey()));
|
||||
channel.writeAndFlush(ProxyMessage.buildAuthMessage(proxyConfig.getClient().getLicenseKey(), ProxyUtil.getClientId()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +174,7 @@ public class ProxyClientService {
|
||||
* 连接代理服务器
|
||||
*/
|
||||
private void connectProxyServer() throws InterruptedException {
|
||||
bootstrap.connect(proxyConfig.getClient().getServerIp(), proxyConfig.getClient().getServerPort())
|
||||
cmdTunnelBootstrap.connect()
|
||||
.addListener(new ChannelFutureListener() {
|
||||
|
||||
@Override
|
||||
@@ -145,13 +183,13 @@ public class ProxyClientService {
|
||||
channel = future.channel();
|
||||
// 连接成功,向服务器发送客户端认证信息(licenseKey)
|
||||
ProxyUtil.setCmdChannel(future.channel());
|
||||
future.channel().writeAndFlush(ProxyMessage.buildAuthMessage(proxyConfig.getClient().getLicenseKey()));
|
||||
log.info("连接代理服务成功. channelId:{}", future.channel().id().asLongText());
|
||||
future.channel().writeAndFlush(ProxyMessage.buildAuthMessage(proxyConfig.getClient().getLicenseKey(), ProxyUtil.getClientId()));
|
||||
log.info("[客户端指令隧道] 连接代理服务成功. channelId:{}", future.channel().id().asLongText());
|
||||
|
||||
reconnectServiceEnable = true;
|
||||
// reconnectServiceEnable = true;
|
||||
reconnectCount = 0;
|
||||
} else {
|
||||
log.info("连接代理服务失败!");
|
||||
log.info("[客户端指令隧道] 连接代理服务失败!");
|
||||
}
|
||||
}
|
||||
}).sync();
|
||||
@@ -181,17 +219,21 @@ public class ProxyClientService {
|
||||
}
|
||||
|
||||
protected synchronized void reconnect() {
|
||||
if (!reconnectServiceEnable) {
|
||||
return;
|
||||
// if (!reconnectServiceEnable) {
|
||||
// return;
|
||||
// }
|
||||
if (null != channel) {
|
||||
if (channel.isActive()) {
|
||||
return;
|
||||
}
|
||||
channel.close();
|
||||
}
|
||||
if (null != channel && channel.isActive()) {
|
||||
return;
|
||||
}
|
||||
log.info("客户端重连 seq:{}", ++reconnectCount);
|
||||
|
||||
log.info("[客户端指令隧道] 客户端重连 seq:{}", ++reconnectCount);
|
||||
try {
|
||||
connectProxyServer();
|
||||
} catch (Exception e) {
|
||||
log.error("重连异常", e);
|
||||
log.error("[客户端指令隧道] 重连异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -22,6 +22,7 @@
|
||||
|
||||
package org.dromara.neutrinoproxy.client.core;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.neutrinoproxy.client.util.ProxyUtil;
|
||||
import org.dromara.neutrinoproxy.core.Constants;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessage;
|
||||
@@ -36,6 +37,7 @@ import io.netty.channel.SimpleChannelInboundHandler;
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
@Slf4j
|
||||
public class RealServerChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
|
||||
|
||||
|
||||
@@ -85,6 +87,6 @@ public class RealServerChannelHandler extends SimpleChannelInboundHandler<ByteBu
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
super.exceptionCaught(ctx, cause);
|
||||
log.error("Client ProxyChannel Error", cause);
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -2,11 +2,13 @@ package org.dromara.neutrinoproxy.client.handler;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
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 io.netty.channel.ChannelHandlerContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.noear.solon.Solon;
|
||||
import org.noear.solon.annotation.Component;
|
||||
|
||||
/**
|
||||
@@ -24,5 +26,15 @@ public class ProxyMessageAuthHandler implements ProxyMessageHandler {
|
||||
JSONObject data = JSONObject.parseObject(info);
|
||||
Integer code = data.getInteger("code");
|
||||
log.info("认证结果:{}", info);
|
||||
if (ExceptionEnum.AUTH_FAILED.getCode().equals(code)) {
|
||||
// 客户端认证失败,直接停止服务
|
||||
log.info("client auth failed , client stop.");
|
||||
context.channel().close();
|
||||
Solon.stop();
|
||||
} else if (ExceptionEnum.CONNECT_FAILED.getCode().equals(code) ||
|
||||
ExceptionEnum.LICENSE_CANNOT_REPEAT_CONNECT.getCode().equals(code)
|
||||
){
|
||||
context.channel().close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -21,8 +21,8 @@ import org.noear.solon.annotation.Inject;
|
||||
@Match(type = Constants.ProxyDataTypeName.CONNECT)
|
||||
@Component
|
||||
public class ProxyMessageConnectHandler implements ProxyMessageHandler {
|
||||
@Inject("bootstrap")
|
||||
private Bootstrap bootstrap;
|
||||
@Inject("proxyTunnelBootstrap")
|
||||
private Bootstrap proxyTunnelBootstrap;
|
||||
@Inject("realServerBootstrap")
|
||||
private Bootstrap realServerBootstrap;
|
||||
@Inject
|
||||
@@ -48,7 +48,7 @@ public class ProxyMessageConnectHandler implements ProxyMessageHandler {
|
||||
realServerChannel.config().setOption(ChannelOption.AUTO_READ, false);
|
||||
|
||||
// 获取连接
|
||||
ProxyUtil.borrowProxyChanel(bootstrap, new ProxyChannelBorrowListener() {
|
||||
ProxyUtil.borrowProxyChanel(proxyTunnelBootstrap, new ProxyChannelBorrowListener() {
|
||||
|
||||
@Override
|
||||
public void success(Channel channel) {
|
||||
|
||||
+29
-4
@@ -21,6 +21,8 @@
|
||||
*/
|
||||
package org.dromara.neutrinoproxy.client.util;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.dromara.neutrinoproxy.client.config.ProxyConfig;
|
||||
import org.dromara.neutrinoproxy.client.core.ProxyChannelBorrowListener;
|
||||
import org.dromara.neutrinoproxy.core.Constants;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
@@ -29,10 +31,12 @@ 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 java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
@@ -54,16 +58,17 @@ public class ProxyUtil {
|
||||
|
||||
private static volatile Channel cmdChannel;
|
||||
|
||||
public static void borrowProxyChanel(Bootstrap bootstrap, final ProxyChannelBorrowListener borrowListener) {
|
||||
private static String clientId;
|
||||
private static final String CLIENT_ID_FILE = ".NEUTRINO_PROXY_CLIENT_ID";
|
||||
|
||||
public static void borrowProxyChanel(Bootstrap proxyTunnelBootstrap, final ProxyChannelBorrowListener borrowListener) {
|
||||
Channel channel = proxyChannelPool.poll();
|
||||
if (null != channel) {
|
||||
borrowListener.success(channel);
|
||||
return;
|
||||
}
|
||||
|
||||
String serverIp = Solon.cfg().get("neutrino.proxy.client.server-ip");
|
||||
Integer serverPort = Solon.cfg().getInt("neutrino.proxy.client.server-port", 9000);
|
||||
bootstrap.connect(serverIp, serverPort).addListener((ChannelFutureListener) future -> {
|
||||
proxyTunnelBootstrap.connect().addListener((ChannelFutureListener) future -> {
|
||||
if (future.isSuccess()) {
|
||||
borrowListener.success(future.channel());
|
||||
} else {
|
||||
@@ -129,4 +134,24 @@ public class ProxyUtil {
|
||||
|
||||
realServerChannels.clear();
|
||||
}
|
||||
|
||||
public static String getClientId() {
|
||||
if (StringUtils.isNotBlank(clientId)) {
|
||||
return clientId;
|
||||
}
|
||||
ProxyConfig proxyConfig = Solon.context().getBean(ProxyConfig.class);
|
||||
if (StringUtils.isNotBlank(proxyConfig.getClient().getClientId())) {
|
||||
clientId = proxyConfig.getClient().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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ neutrino:
|
||||
length-field-length: 4
|
||||
initial-bytes-to-strip: 0
|
||||
length-adjustment: 0
|
||||
read-idle-time: 40
|
||||
write-idle-time: 5
|
||||
all-idle-time-seconds: 45
|
||||
read-idle-time: 8
|
||||
write-idle-time: 3
|
||||
all-idle-time-seconds: 8
|
||||
client:
|
||||
thread-count: 50
|
||||
key-store-password: ${STORE_PASS:123456}
|
||||
@@ -20,4 +20,5 @@ neutrino:
|
||||
server-port: ${SERVER_PORT:9002}
|
||||
ssl-enable: ${SSL_ENABLE:true}
|
||||
obtain-license-interval: 5
|
||||
license-key: ${LICENSE_KEY:b0a907332b474b25897c4dcb31fc7eb6}
|
||||
license-key: ${LICENSE_KEY:}
|
||||
client-id: ${CLIENT_ID:}
|
||||
|
||||
@@ -35,7 +35,10 @@ import lombok.Getter;
|
||||
public enum ExceptionEnum {
|
||||
SUCCESS(0, "成功"),
|
||||
AUTH_FAILED(1, "认证失败"),
|
||||
CONNECT_FAILED(2, "连接失败");
|
||||
CONNECT_FAILED(2, "连接失败"),
|
||||
@Deprecated
|
||||
LICENSE_CANNOT_REPEAT_CONNECT(3, "license不能多个客户端同时使用"),
|
||||
;
|
||||
|
||||
private Integer code;
|
||||
private String msg;
|
||||
|
||||
@@ -100,9 +100,9 @@ public class ProxyMessage {
|
||||
return create().setType(TYPE_HEARTBEAT);
|
||||
}
|
||||
|
||||
public static ProxyMessage buildAuthMessage(String info) {
|
||||
public static ProxyMessage buildAuthMessage(String info, String clientId) {
|
||||
return create().setType(TYPE_AUTH)
|
||||
.setInfo(info);
|
||||
.setInfo(info + "," + clientId);
|
||||
}
|
||||
|
||||
public static ProxyMessage buildAuthResultMessage(Integer code, String msg, String licenseKey) {
|
||||
|
||||
+22
@@ -1,5 +1,9 @@
|
||||
package org.dromara.neutrinoproxy.server.base.proxy;
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import org.dromara.neutrinoproxy.core.ProxyDataTypeEnum;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessage;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
|
||||
@@ -7,6 +11,8 @@ import org.dromara.neutrinoproxy.core.dispatcher.DefaultDispatcher;
|
||||
import org.dromara.neutrinoproxy.core.dispatcher.Dispatcher;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import org.dromara.neutrinoproxy.server.proxy.core.BytesMetricsHandler;
|
||||
import org.dromara.neutrinoproxy.server.proxy.core.TcpVisitorChannelHandler;
|
||||
import org.noear.solon.Solon;
|
||||
import org.noear.solon.annotation.Bean;
|
||||
import org.noear.solon.annotation.Configuration;
|
||||
@@ -43,6 +49,22 @@ public class ProxyConfiguration implements LifecycleBean {
|
||||
return new NioEventLoopGroup(proxyConfig.getServer().getWorkThreadCount());
|
||||
}
|
||||
|
||||
@Bean("tcpServerBootstrap")
|
||||
public ServerBootstrap tcpServerBootstrap(@Inject("serverBossGroup") NioEventLoopGroup serverBossGroup,
|
||||
@Inject("serverWorkerGroup") NioEventLoopGroup serverWorkerGroup) {
|
||||
ServerBootstrap bootstrap = new ServerBootstrap();
|
||||
bootstrap.group(serverBossGroup, serverWorkerGroup)
|
||||
.channel(NioServerSocketChannel.class)
|
||||
.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) throws Exception {
|
||||
ch.pipeline().addFirst(new BytesMetricsHandler());
|
||||
ch.pipeline().addLast(new TcpVisitorChannelHandler());
|
||||
}
|
||||
});
|
||||
return bootstrap;
|
||||
}
|
||||
|
||||
@Bean("tunnelBossGroup")
|
||||
public NioEventLoopGroup tunnelBossGroup(@Inject ProxyConfig proxyConfig) {
|
||||
return new NioEventLoopGroup(proxyConfig.getTunnel().getBossThreadCount());
|
||||
|
||||
+11
-3
@@ -2,6 +2,7 @@ package org.dromara.neutrinoproxy.server.constant;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
@@ -15,14 +16,21 @@ import java.util.stream.Stream;
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum NetworkProtocolEnum {
|
||||
TCP("TCP"),
|
||||
UDP("UDP"),
|
||||
HTTP("HTTP"),
|
||||
TCP("TCP", "TCP"),
|
||||
UDP("UDP", "UDP"),
|
||||
HTTP("HTTP", "TCP"),
|
||||
;
|
||||
private String desc;
|
||||
private String baseProtocol;
|
||||
private static final Map<String, NetworkProtocolEnum> map = Stream.of(NetworkProtocolEnum.values()).collect(Collectors.toMap(NetworkProtocolEnum::getDesc, Function.identity()));
|
||||
|
||||
public static NetworkProtocolEnum of(String desc) {
|
||||
if (StringUtils.isBlank(desc)) {
|
||||
return null;
|
||||
}
|
||||
if (desc.startsWith("HTTP")) {
|
||||
return NetworkProtocolEnum.HTTP;
|
||||
}
|
||||
return map.get(desc);
|
||||
}
|
||||
}
|
||||
|
||||
+19
-12
@@ -47,10 +47,10 @@ import java.util.Date;
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
@Slf4j
|
||||
public class ServerChannelHandler extends SimpleChannelInboundHandler<ProxyMessage> {
|
||||
public class ProxyTunnelChannelHandler extends SimpleChannelInboundHandler<ProxyMessage> {
|
||||
private static volatile Dispatcher<ChannelHandlerContext, ProxyMessage> dispatcher;
|
||||
|
||||
public ServerChannelHandler() {
|
||||
public ProxyTunnelChannelHandler() {
|
||||
dispatcher = Solon.context().getBean(Dispatcher.class);
|
||||
}
|
||||
|
||||
@@ -89,17 +89,24 @@ public class ServerChannelHandler extends SimpleChannelInboundHandler<ProxyMessa
|
||||
} else {
|
||||
CmdChannelAttachInfo cmdChannelAttachInfo = ProxyUtil.getAttachInfo(ctx.channel());
|
||||
if (null != cmdChannelAttachInfo) {
|
||||
Solon.context().getBean(ProxyMutualService.class).offline(cmdChannelAttachInfo);
|
||||
Solon.context().getBean(ClientConnectRecordService.class).add(new ClientConnectRecordDO()
|
||||
.setIp(((InetSocketAddress)ctx.channel().remoteAddress()).getAddress().getHostAddress())
|
||||
.setLicenseId(cmdChannelAttachInfo.getLicenseId())
|
||||
.setType(ClientConnectTypeEnum.DISCONNECT.getType())
|
||||
.setMsg("")
|
||||
.setCode(SuccessCodeEnum.SUCCESS.getCode())
|
||||
.setCreateTime(new Date())
|
||||
);
|
||||
ProxyUtil.removeCmdChannel(ctx.channel());
|
||||
Channel curCmdChannel = ProxyUtil.getCmdChannelByLicenseId(cmdChannelAttachInfo.getLicenseId());
|
||||
// 客户端切换网络后,连接断开,但服务端还未触发断开事件。此时客户端重连上了,然后服务端触发了断开,此时不应该更新在线状态
|
||||
if (curCmdChannel == ctx.channel()) {
|
||||
Solon.context().getBean(ProxyMutualService.class).offline(cmdChannelAttachInfo);
|
||||
ProxyUtil.removeCmdChannel(ctx.channel());
|
||||
// 防止下次换一个客户端,无法连接的情况
|
||||
ProxyUtil.removeClientIdByLicenseId(cmdChannelAttachInfo.getLicenseId());
|
||||
}
|
||||
}
|
||||
// 即便是因为上述原因断开,断开的日志依然要记录,方便排查问题
|
||||
Solon.context().getBean(ClientConnectRecordService.class).add(new ClientConnectRecordDO()
|
||||
.setIp(((InetSocketAddress)ctx.channel().remoteAddress()).getAddress().getHostAddress())
|
||||
.setLicenseId(cmdChannelAttachInfo.getLicenseId())
|
||||
.setType(ClientConnectTypeEnum.DISCONNECT.getType())
|
||||
.setMsg("")
|
||||
.setCode(SuccessCodeEnum.SUCCESS.getCode())
|
||||
.setCreateTime(new Date())
|
||||
);
|
||||
}
|
||||
|
||||
super.channelInactive(ctx);
|
||||
+3
-4
@@ -1,6 +1,5 @@
|
||||
package org.dromara.neutrinoproxy.server.proxy.core;
|
||||
|
||||
import io.netty.handler.logging.LoggingHandler;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessageDecoder;
|
||||
import org.dromara.neutrinoproxy.core.ProxyMessageEncoder;
|
||||
import org.dromara.neutrinoproxy.core.util.FileUtil;
|
||||
@@ -27,13 +26,13 @@ import java.io.InputStream;
|
||||
import java.security.KeyStore;
|
||||
|
||||
/**
|
||||
*
|
||||
* 代理隧道服务
|
||||
* @author: aoshiguchen
|
||||
* @date: 2022/6/16
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ProxyServerRunner implements EventListener<AppLoadEndEvent> {
|
||||
public class ProxyTunnelServer implements EventListener<AppLoadEndEvent> {
|
||||
@Inject
|
||||
private ProxyConfig proxyConfig;
|
||||
@Inject("tunnelBossGroup")
|
||||
@@ -118,6 +117,6 @@ public class ProxyServerRunner implements EventListener<AppLoadEndEvent> {
|
||||
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 ServerChannelHandler());
|
||||
ch.pipeline().addLast(new ProxyTunnelChannelHandler());
|
||||
}
|
||||
}
|
||||
+4
-6
@@ -1,4 +1,4 @@
|
||||
package org.dromara.neutrinoproxy.server.proxy.core;
|
||||
package org.dromara.neutrinoproxy.server.proxy.enhance;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
@@ -8,22 +8,20 @@ import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig;
|
||||
import org.dromara.neutrinoproxy.server.proxy.core.BytesMetricsHandler;
|
||||
import org.noear.solon.annotation.Component;
|
||||
import org.noear.solon.annotation.Inject;
|
||||
import org.noear.solon.core.event.AppLoadEndEvent;
|
||||
import org.noear.solon.core.event.EventListener;
|
||||
|
||||
/**
|
||||
* HTTP代理
|
||||
* @author: aoshiguchen
|
||||
* @date: 2023/4/2
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class HttpProxy implements EventListener<AppLoadEndEvent> {
|
||||
@Inject("serverBossGroup")
|
||||
private NioEventLoopGroup serverBossGroup;
|
||||
@Inject("serverWorkerGroup")
|
||||
private NioEventLoopGroup serverWorkerGroup;
|
||||
@Inject
|
||||
private ProxyConfig proxyConfig;
|
||||
@Override
|
||||
@@ -38,7 +36,7 @@ public class HttpProxy implements EventListener<AppLoadEndEvent> {
|
||||
private void start() {
|
||||
try {
|
||||
ServerBootstrap bootstrap = new ServerBootstrap();
|
||||
bootstrap.group(serverBossGroup, serverWorkerGroup)
|
||||
bootstrap.group(new NioEventLoopGroup(1), new NioEventLoopGroup())
|
||||
.channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) throws Exception {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.dromara.neutrinoproxy.server.proxy.core;
|
||||
package org.dromara.neutrinoproxy.server.proxy.enhance;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
+4
-6
@@ -1,4 +1,4 @@
|
||||
package org.dromara.neutrinoproxy.server.proxy.core;
|
||||
package org.dromara.neutrinoproxy.server.proxy.enhance;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
@@ -11,6 +11,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.dromara.neutrinoproxy.core.util.FileUtil;
|
||||
import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig;
|
||||
import org.dromara.neutrinoproxy.server.proxy.core.BytesMetricsHandler;
|
||||
import org.noear.solon.annotation.Component;
|
||||
import org.noear.solon.annotation.Inject;
|
||||
import org.noear.solon.core.event.AppLoadEndEvent;
|
||||
@@ -21,16 +22,13 @@ import java.io.InputStream;
|
||||
import java.security.KeyStore;
|
||||
|
||||
/**
|
||||
* HTTPS代理
|
||||
* @author: aoshiguchen
|
||||
* @date: 2023/4/2
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class HttpsProxy implements EventListener<AppLoadEndEvent> {
|
||||
@Inject("serverBossGroup")
|
||||
private NioEventLoopGroup serverBossGroup;
|
||||
@Inject("serverWorkerGroup")
|
||||
private NioEventLoopGroup serverWorkerGroup;
|
||||
@Inject
|
||||
private ProxyConfig proxyConfig;
|
||||
@Override
|
||||
@@ -46,7 +44,7 @@ public class HttpsProxy implements EventListener<AppLoadEndEvent> {
|
||||
private void start() {
|
||||
try {
|
||||
ServerBootstrap bootstrap = new ServerBootstrap();
|
||||
bootstrap.group(serverBossGroup, serverWorkerGroup)
|
||||
bootstrap.group(new NioEventLoopGroup(1), new NioEventLoopGroup())
|
||||
.channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) throws Exception {
|
||||
+37
-11
@@ -79,9 +79,19 @@ public class ProxyMessageAuthHandler implements ProxyMessageHandler {
|
||||
String ip = ((InetSocketAddress)ctx.channel().remoteAddress()).getAddress().getHostAddress();
|
||||
Date now = new Date();
|
||||
|
||||
String info = proxyMessage.getInfo();
|
||||
String[] tmp = info.split(",");
|
||||
String licenseKey = proxyMessage.getInfo();
|
||||
String clientId = "";
|
||||
if (tmp.length == 2) {
|
||||
licenseKey = tmp[0];
|
||||
clientId = tmp[1];
|
||||
}
|
||||
|
||||
if (StrUtil.isEmpty(licenseKey)) {
|
||||
log.warn("[客户端连接] license不能为空 info:{} ", info);
|
||||
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.AUTH_FAILED.getCode(), "license不能为空!", licenseKey));
|
||||
ctx.channel().close();
|
||||
clientConnectRecordService.add(new ClientConnectRecordDO()
|
||||
.setIp(ip)
|
||||
.setType(ClientConnectTypeEnum.CONNECT.getType())
|
||||
@@ -94,7 +104,9 @@ public class ProxyMessageAuthHandler implements ProxyMessageHandler {
|
||||
}
|
||||
LicenseDO licenseDO = licenseService.findByKey(licenseKey);
|
||||
if (null == licenseDO) {
|
||||
log.warn("[客户端连接] license不存在 info:{} ", info);
|
||||
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.AUTH_FAILED.getCode(), "license不存在!", licenseKey));
|
||||
ctx.channel().close();
|
||||
clientConnectRecordService.add(new ClientConnectRecordDO()
|
||||
.setIp(ip)
|
||||
.setType(ClientConnectTypeEnum.CONNECT.getType())
|
||||
@@ -106,7 +118,9 @@ public class ProxyMessageAuthHandler implements ProxyMessageHandler {
|
||||
return;
|
||||
}
|
||||
if (EnableStatusEnum.DISABLE.getStatus().equals(licenseDO.getEnable())) {
|
||||
log.warn("[客户端连接] 当前license已被禁用 info:{} ", info);
|
||||
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.AUTH_FAILED.getCode(), "当前license已被禁用!", licenseKey));
|
||||
ctx.channel().close();
|
||||
clientConnectRecordService.add(new ClientConnectRecordDO()
|
||||
.setIp(ip)
|
||||
.setLicenseId(licenseDO.getId())
|
||||
@@ -119,7 +133,9 @@ public class ProxyMessageAuthHandler implements ProxyMessageHandler {
|
||||
}
|
||||
UserDO userDO = userService.findById(licenseDO.getUserId());
|
||||
if (null == userDO || EnableStatusEnum.DISABLE.getStatus().equals(userDO.getEnable())) {
|
||||
log.warn("[客户端连接] 当前license无效 info:{} ", info);
|
||||
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.AUTH_FAILED.getCode(), "当前license无效!", licenseKey));
|
||||
ctx.channel().close();
|
||||
clientConnectRecordService.add(new ClientConnectRecordDO()
|
||||
.setIp(ip)
|
||||
.setLicenseId(licenseDO.getId())
|
||||
@@ -132,16 +148,21 @@ public class ProxyMessageAuthHandler implements ProxyMessageHandler {
|
||||
}
|
||||
Channel cmdChannel = ProxyUtil.getCmdChannelByLicenseId(licenseDO.getId());
|
||||
if (null != cmdChannel) {
|
||||
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.AUTH_FAILED.getCode(), "当前license已被另一节点使用!", licenseKey));
|
||||
clientConnectRecordService.add(new ClientConnectRecordDO()
|
||||
.setIp(ip)
|
||||
.setLicenseId(licenseDO.getId())
|
||||
.setType(ClientConnectTypeEnum.CONNECT.getType())
|
||||
.setMsg(licenseKey)
|
||||
.setCode(SuccessCodeEnum.FAIL.getCode())
|
||||
.setErr("当前license已被另一节点使用!")
|
||||
.setCreateTime(now));
|
||||
return;
|
||||
String _clientId = ProxyUtil.getClientIdByLicenseId(licenseDO.getId());
|
||||
if (!clientId.equals(_clientId)) {
|
||||
log.warn("[客户端连接] 当前license已被另一节点使用 info:{} _clientId:{}", info, _clientId);
|
||||
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.AUTH_FAILED.getCode(), "当前license已被另一节点使用!", licenseKey));
|
||||
ctx.channel().close();
|
||||
clientConnectRecordService.add(new ClientConnectRecordDO()
|
||||
.setIp(ip)
|
||||
.setLicenseId(licenseDO.getId())
|
||||
.setType(ClientConnectTypeEnum.CONNECT.getType())
|
||||
.setMsg(licenseKey)
|
||||
.setCode(SuccessCodeEnum.FAIL.getCode())
|
||||
.setErr("当前license已被另一节点使用!")
|
||||
.setCreateTime(now));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 发送认证成功消息
|
||||
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.SUCCESS.getCode(), "认证成功!", licenseKey));
|
||||
@@ -150,10 +171,15 @@ public class ProxyMessageAuthHandler implements ProxyMessageHandler {
|
||||
.setIp(ip)
|
||||
.setLicenseId(licenseDO.getId())
|
||||
.setType(ClientConnectTypeEnum.CONNECT.getType())
|
||||
.setMsg(licenseKey)
|
||||
.setMsg(info)
|
||||
.setCode(SuccessCodeEnum.SUCCESS.getCode())
|
||||
.setCreateTime(now));
|
||||
|
||||
// 设置当前licenseId对应的客户端ID
|
||||
ProxyUtil.setLicenseIdToClientIdMap(licenseDO.getId(), clientId);
|
||||
|
||||
log.warn("[客户端连接] 认证成功 info:{} ", info);
|
||||
|
||||
// 更新license在线状态
|
||||
licenseMapper.updateOnlineStatus(licenseDO.getId(), OnlineStatusEnum.ONLINE.getStatus(), now);
|
||||
// 初始化VisitorChannel
|
||||
|
||||
+3
@@ -108,6 +108,9 @@ public class PortMappingService implements LifecycleBean {
|
||||
if (StrUtil.isNotBlank(proxyConfig.getServer().getDomainName()) && StrUtil.isNotBlank(item.getSubdomain())) {
|
||||
item.setDomain(item.getSubdomain() + "." + proxyConfig.getServer().getDomainName());
|
||||
}
|
||||
if (NetworkProtocolEnum.HTTP.getDesc().equals(item.getProtocal())) {
|
||||
item.setProtocal("HTTP(S)");
|
||||
}
|
||||
});
|
||||
//sorted [userId asc] [licenseId asc] [createTime asc]
|
||||
respList = respList.stream().sorted(Comparator.comparing(PortMappingListRes::getUserId)
|
||||
|
||||
+8
-14
@@ -41,10 +41,8 @@ import java.util.stream.Collectors;
|
||||
@Slf4j
|
||||
@Component
|
||||
public class VisitorChannelService {
|
||||
@Inject("serverBossGroup")
|
||||
private NioEventLoopGroup serverBossGroup;
|
||||
@Inject("serverWorkerGroup")
|
||||
private NioEventLoopGroup serverWorkerGroup;
|
||||
@Inject("tcpServerBootstrap")
|
||||
private ServerBootstrap tcpServerBootstrap;
|
||||
@Inject
|
||||
private ProxyMutualService proxyMutualService;
|
||||
@Db
|
||||
@@ -218,20 +216,16 @@ public class VisitorChannelService {
|
||||
if (CollectionUtil.isEmpty(portMappingList)) {
|
||||
return;
|
||||
}
|
||||
ServerBootstrap bootstrap = new ServerBootstrap();
|
||||
bootstrap.group(serverBossGroup, serverWorkerGroup)
|
||||
.channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) throws Exception {
|
||||
ch.pipeline().addFirst(new BytesMetricsHandler());
|
||||
ch.pipeline().addLast(new TcpVisitorChannelHandler());
|
||||
}
|
||||
});
|
||||
|
||||
for (PortMappingDO portMapping : portMappingList) {
|
||||
if (EnableStatusEnum.DISABLE.getStatus().equals(portMapping.getEnable())) {
|
||||
// 端口映射被禁用了,忽略 TODO 端口被禁用了也需要处理
|
||||
continue;
|
||||
}
|
||||
// TODO 此处切入,TCP/UDP代理
|
||||
try {
|
||||
proxyMutualService.bindServerPort(cmdChannelAttachInfo, portMapping.getServerPort());
|
||||
bootstrap.bind(portMapping.getServerPort()).get();
|
||||
tcpServerBootstrap.bind(portMapping.getServerPort()).get();
|
||||
log.info("绑定用户端口: {}", portMapping.getServerPort());
|
||||
} catch (Exception ex) {
|
||||
// BindException表示该端口已经绑定过
|
||||
|
||||
+30
@@ -63,6 +63,10 @@ public class ProxyUtil {
|
||||
* 子域名 - 服务端端口映射
|
||||
*/
|
||||
private static Map<String, Integer> subdomainToServerPort = new HashMap<>();
|
||||
/**
|
||||
* licenseId - 客户端Id映射
|
||||
*/
|
||||
private static Map<Integer, String> licenseIdToClientIdMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 初始化代理信息
|
||||
@@ -387,4 +391,30 @@ public class ProxyUtil {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置licenseId - clientId映射
|
||||
* @param licenseId
|
||||
* @param clientId
|
||||
*/
|
||||
public static void setLicenseIdToClientIdMap(Integer licenseId, String clientId) {
|
||||
licenseIdToClientIdMap.put(licenseId, clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据licenseId获取clientId
|
||||
* @param licenseId
|
||||
* @return
|
||||
*/
|
||||
public static String getClientIdByLicenseId(Integer licenseId) {
|
||||
return licenseIdToClientIdMap.get(licenseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据licenseId删除clientId
|
||||
* @param licenseId
|
||||
*/
|
||||
public static void removeClientIdByLicenseId(Integer licenseId) {
|
||||
licenseIdToClientIdMap.remove(licenseId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ neutrino:
|
||||
length-field-length: 4
|
||||
initial-bytes-to-strip: 0
|
||||
length-adjustment: 0
|
||||
read-idle-time: 40
|
||||
read-idle-time: 30
|
||||
write-idle-time: 5
|
||||
all-idle-time-seconds: 0
|
||||
tunnel:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -7,7 +7,7 @@ article: false
|
||||
|
||||
## 1、 部署服务端
|
||||
### 1.1、 Docker一键部署
|
||||
> 当前最新版本为1.8.0,下面的脚本中,可以使用:`registry.cn-hangzhou.aliyuncs.com/asgc/neutrino-proxy:1.8.0` 指定版本安装,推荐使用`latest`直接安装最新版。
|
||||
> 当前最新版本为1.8.2,下面的脚本中,可以使用:`registry.cn-hangzhou.aliyuncs.com/asgc/neutrino-proxy:1.8.2` 指定版本安装,推荐使用`latest`直接安装最新版。
|
||||
|
||||
#### 使用默认sqlite数据库
|
||||
```shell
|
||||
@@ -115,4 +115,4 @@ module.exports = {
|
||||
}
|
||||
```
|
||||
:::
|
||||
-->
|
||||
-->
|
||||
|
||||
@@ -35,3 +35,28 @@ permalink: /pages/269a2e/
|
||||
# 所有的代理映射都能绑定域名吗?
|
||||
不行的。之所以能实现域名绑定,原因是HTTP请求附带了Host请求头,包含了请求的域名信息,代理服务端能根据请求的域名做分发。而TCP协议本身并不包含域名,因此无法实现。
|
||||
所以,目前只有HTTP(涵盖HTTPS)协议能支持。
|
||||
|
||||
# 代理服务端使用非80端口时,nginx配置示例
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
# 此处配不配貌似不影响
|
||||
server_name localhost *.neutrino-proxy.asgc.fun;
|
||||
|
||||
location / {
|
||||
if ($http_host ~* "(.*?).neutrino-proxy.asgc.fun") {
|
||||
# 转发到代理服务端HTTP代理端口
|
||||
proxy_pass http://localhost:8899;
|
||||
}
|
||||
|
||||
tcp_nodelay on;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
#以上三行,目的是将代理服务器收到的用户的信息传到真实服务器上
|
||||
|
||||
root html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: HTTPS配置
|
||||
date: 2023-05-27 11:40:12
|
||||
permalink: /pages/99a300/
|
||||
---
|
||||
|
||||
## 基础说明
|
||||
- 在中微子代理中,HTTP的定位是:为ip+端口的访问形式增加域名访问的支持。因此:
|
||||
- 如果端口映射正确配置了域名,那么该映射同时支持IP+端口、域名2种访问形式。
|
||||
- 如果端口映射未配置域名,那么只能以ip+端口的形式访问,此时等价于选择TCP协议。
|
||||
|
||||
- 在中微子代理中,HTTPS作为HTTP的一种增强行为,不作为一个单独的协议。因此:
|
||||
- 在端口映射中只需要选择HTTP即可
|
||||
- 若端口映射未正确配置域名,那么同理,此时等价于选择TCP协议。
|
||||
- 若端口映射配置了域名,但是未正确配置HTTPS端口、证书,那么此时可以通过域名访问,但不支持HTTPS
|
||||
- 若端口映射配置了域名,且正确配置了HTTPS端口、证书,那么此时可以通过IP+端口、http域名、https域名3种形式访问
|
||||
|
||||
|
||||
## HTTPS配置流程
|
||||
- 按照域名映射流程,完成域名的配置
|
||||
- 在服务端配置域名的HTTPS证书信息(此处应该是通配符证书)
|
||||
|
||||
## HTTPS端口问题
|
||||
- 与HTTP类似,HTTPS也有一个默认端口:443,默认情况下需要占用服务端443端口
|
||||
- 如果代理服务端不能独占443端口,此处可以指定任意端口,但需要在nginx中配置443端口的转发规则。此处与域名映射中代理服务端不使用80端口的情况类似
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: 服务端配置
|
||||
date: 2023-05-27 11:41:44
|
||||
permalink: /pages/f2d0f1/
|
||||
---
|
||||
|
||||
::: tip
|
||||
|
||||
1.8.2版本,服务端配置格式有所调整。从1.8.2之前的版本升级到1.8.2之后的版本,需要注意
|
||||
|
||||
:::
|
||||
|
||||
|
||||
# 以下是最新的服务端配置格式(app.yml)
|
||||
```yml
|
||||
server:
|
||||
# 服务端web端口,用于支持HTTP接口,管理后台页面访问
|
||||
port: ${WEB_PORT:8888}
|
||||
|
||||
neutrino:
|
||||
proxy:
|
||||
# 隧道相关配置-用于维持服务端与客户端的通信
|
||||
tunnel:
|
||||
# 线程池相关配置,用于技术调优,可忽略
|
||||
boss-thread-count: 2
|
||||
work-thread-count: 10
|
||||
# 隧道非SSL端口
|
||||
port: ${OPEN_PORT:9000}
|
||||
# 隧道SSL端口
|
||||
ssl-port: ${SSL_PORT:9002}
|
||||
# 隧道SSL证书配置
|
||||
key-store-password: ${STORE_PASS:123456}
|
||||
key-manager-password: ${MGR_PASS:123456}
|
||||
jks-path: ${JKS_PATH:classpath:/test.jks}
|
||||
# 代理服务相关配置
|
||||
server:
|
||||
# 线程池相关配置,用于技术调优,可忽略
|
||||
boss-thread-count: 5
|
||||
work-thread-count: 20
|
||||
# http代理端口,默认80
|
||||
http-proxy-port: ${HTTP_PROXY_PORT:80}
|
||||
# https代理端口,默认443 (需要配置域名、证书)
|
||||
https-proxy-port: ${HTTPS_PROXY_PORT:443}
|
||||
# 如果不配置,则不支持域名映射
|
||||
domain-name: ${DOMAIN_NAME:}
|
||||
# https证书配置
|
||||
key-store-password: ${HTTPS_STORE_PASS:}
|
||||
jks-path: ${HTTPS_JKS_PATH:}
|
||||
data:
|
||||
db:
|
||||
# 数据库类型,目前支持sqlite、mysql两种
|
||||
type: ${DB_TYPE:sqlite}
|
||||
# 数据库连接URL
|
||||
url: ${DB_URL:jdbc:sqlite:data.db}
|
||||
# 数据库用户名
|
||||
username: ${DB_USER:}
|
||||
# 数据库密码
|
||||
password: ${DB_PASSWORD:}
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
home: true
|
||||
|
||||
heroImage: /img/logo.png
|
||||
heroImage: /img/logo-0.75x.png
|
||||
heroText: Neutrino-Proxy
|
||||
tagline: 🚀一个基于Netty的开源内网穿透神器
|
||||
actionText: 开始使用 →
|
||||
@@ -21,8 +21,12 @@ features: # 可选的
|
||||
details: 新增、编辑、删除、禁用实时生效。
|
||||
- title: Docker
|
||||
details: 服务端支持Docker一键部署。
|
||||
# - title: SSL证书
|
||||
# details: 支持SSL,保护您的信息安全。
|
||||
- title: 隧道SSL加密
|
||||
details: 隧道通信支持SSL,保护您的数据安全
|
||||
- title: HTTPS
|
||||
details: 支持HTTPS
|
||||
- title: 多客户端支持
|
||||
details: 暂未支持
|
||||
|
||||
# 文章列表显示方式: detailed 默认,显示详细版文章列表(包括作者、分类、标签、摘要、分页等)| simple => 显示简约版文章列表(仅标题和日期)| none 不显示文章列表
|
||||
postList: none
|
||||
@@ -288,9 +292,12 @@ postList: none
|
||||
|
||||
<h2>🤝 友情开源项目</h2>
|
||||
<p>
|
||||
<a href="https://gitee.com/noear/solon" target="_blank" class="friends-item" style="display: flex;">
|
||||
<a href="https://gitee.com/noear/solon" target="_blank" class="friends-item" >
|
||||
<img class="no-zoom friends-item-img hover-alt" :src="$withBase('/img/logo/solon_logo_500_150.png')" msg="一个高效的应用开发框架:更快、更小、更简单。" />
|
||||
</a>
|
||||
<a href="https://gitee.com/xiaonuobase/snowy" target="_blank" class="friends-item">
|
||||
<img class="no-zoom friends-item-img hover-alt" :src="$withBase('/img/logo/xiaonuo.png')" msg="国内首个国密前后端分离快速开发平台" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<!-- AD -->
|
||||
|
||||
+3
-5
@@ -1,10 +1,8 @@
|
||||
# 1.x剩余规划
|
||||
- [ ] 增加针对https的支持
|
||||
|
||||
# Bug
|
||||
- 指令通达被close的问题,org.dromara.neutrinoproxy.server.proxy.core.ServerChannelHandler.channelInactive
|
||||
- 指令通达被close的问题,org.dromara.neutrinoproxy.server.proxy.core.ProxyTunnelChannelHandler.channelInactive
|
||||
- windows环境下直接运行发布版的jar包,日志输出乱码
|
||||
- 代理mysql时,使用未开启远程访问的账号走代理访问mysql,代理客户端出现断开现象
|
||||
- 客户端连接映射某个端口以后,如果在服务器端禁用了,没有立刻反映出来,要灯客户端重连以后才会屏蔽被禁用的端口。
|
||||
|
||||
# 2.x规划
|
||||
- 插件开发
|
||||
@@ -26,4 +24,4 @@
|
||||
# 3.x规划
|
||||
- [ ] 支持针对用户限速、限流
|
||||
- [ ] 支持P2P穿透
|
||||
- [ ] 支持原生编译
|
||||
- [ ] 支持原生编译
|
||||
|
||||
Reference in New Issue
Block a user