Compare commits

..
Author SHA1 Message Date
aoshiguchen 2dc8965cdb 适配mariadb. 2023-06-08 22:00:11 +08:00
aoshiguchen 0d997bbe92 适配mariadb 2023-06-08 21:58:39 +08:00
aoshiguchen f88bbb53cb 更新TODOLIST 2023-06-07 21:35:02 +08:00
aoshiguchen 2ae3267049 更新logback配置 2023-06-07 20:41:33 +08:00
aoshiguchen 62bbd48b33 更新TODOLIST 2023-06-07 20:37:26 +08:00
aoshiguchen aa131b0705 解决http域名映射的问题 2023-06-06 10:11:12 +08:00
aoshiguchen 1510e16e50 客户端服务端、日志优化 2023-06-05 18:02:03 +08:00
aoshiguchen a3b324c72b http代理日志调整. 2023-06-05 17:48:33 +08:00
aoshiguchen c959ce13a3 解决端口映射选择https,域名输入框没出来的问题 2023-06-05 10:00:59 +08:00
aoshiguchen 4c828f814c 服务端、客户端认证逻辑调整 2023-06-03 03:19:45 +08:00
aoshiguchen 5a65eebfd3 服务端、客户端认证逻辑调整 2023-06-03 01:34:22 +08:00
aoshiguchen 5a3375ce80 服务端、客户端认证逻辑调整. 2023-06-03 01:30:52 +08:00
aoshiguchen eb277b414a 服务端、客户端认证逻辑调整. 2023-06-03 00:47:30 +08:00
aoshiguchen fc4097529f 1、服务端、客户端认证逻辑调整.
2、解决端口映射被禁用时,客户端启动依然能访问的问题
2023-06-02 23:26:13 +08:00
aoshiguchen 375687aa5f 1、服务端、客户端认证逻辑调整.
2、解决端口映射被禁用时,客户端启动依然能访问的问题.
2023-06-02 22:46:15 +08:00
aoshiguchen 32504d33af 1、服务端、客户端认证逻辑调整.
2、解决端口映射被禁用时,客户端启动依然能访问的问题
2023-06-02 22:36:39 +08:00
aoshiguchen bcd429d54f 客户端连接时认证失败,直接断开连接 2023-06-02 22:24:20 +08:00
aoshiguchen 2ec87abda9 客户端重连机制调整,启动首次连接失败也进行重连 2023-06-02 22:09:01 +08:00
aoshiguchen 8c2e1b8f6c 客户端重连优化. 2023-06-02 21:20:19 +08:00
aoshiguchen e2195ab966 客户端重连优化 2023-06-01 22:34:41 +08:00
aoshiguchen 2d0b9132e9 协议枚举调整,维护协议关联的下层基础协议 2023-05-31 22:42:16 +08:00
aoshiguchen 5a502115b1 代码优化,避免新增端口映射时不断创建新的ServerBootstrap实例. 2023-05-31 22:32:22 +08:00
aoshiguchen a972e0f309 代码优化,避免新增端口映射时不断创建新的ServerBootstrap实例 2023-05-31 22:29:44 +08:00
aoshiguchen b52225937c http、https代理线程池分开 2023-05-31 22:23:12 +08:00
aoshiguchen 98737f0e9d 服务端代理相关命名调整,使得表义更加准确. 2023-05-31 21:41:22 +08:00
aoshiguchen ea67c2bce0 服务端代理相关命名调整,使得表义更加准确 2023-05-31 21:34:53 +08:00
aoshiguchen 7dda047d51 解决新增报不支持的协议HTTPS的问题 2023-05-29 22:57:16 +08:00
aoshiguchen b518dbfd58 更新官网 2023-05-29 22:23:19 +08:00
aoshiguchen 7054a1df0e 官网文档调整. 2023-05-27 12:05:43 +08:00
aoshiguchen da0d1946c6 更新README 2023-05-27 11:10:27 +08:00
aoshiguchen afff036641 新增HTTPS支持 2023-05-27 00:47:01 +08:00
aoshiguchen 9944e60825 启动脚本调整 2023-05-23 20:09:44 +08:00
aoshiguchen 74ff0c3b44 Merge branch 'feature/20230520' into dev 2023-05-20 14:28:53 +08:00
aoshiguchen 5bb6a09ce2 更改日志配置 2023-05-20 14:21:10 +08:00
aoshiguchen 3e9e7cb370 server端代码优化,将代理隧道配置分离出来 2023-05-03 18:07:12 +08:00
zCans 3cf6bcbe6e 首页图表剔除冗余 2023-04-24 22:52:27 +08:00
aoshiguchen 8f9375152e 管理后台首页折线图展示问题修复 2023-04-24 22:46:57 +08:00
aoshiguchen 501012d9bc 更新快速上手文档. 2023-04-22 15:44:50 +08:00
aoshiguchen f2b560fa1f 客户端优化,解决长时间数据写入,导致指令通道被关闭的问题 2023-04-20 23:04:39 +08:00
aoshiguchen 2b95967820 配置调整&服务端代码优化 2023-04-20 22:22:50 +08:00
59 changed files with 1280 additions and 492 deletions
+3 -1
View File
@@ -54,4 +54,6 @@ hs_err_pid*
**/memo/**
neutrino-proxy-vuepress/deploy.sh
neutrino-proxy-vuepress/deploy.sh
.NEUTRINO_PROXY_CLIENT_ID
logs
+42 -28
View File
@@ -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、启动服务端
@@ -50,12 +50,11 @@ export default {
this.chartDom = document.getElementById(this.chartId)
this.myChart = echarts.init(this.chartDom)
const seriesList = []
const legendList = []
this.data.list && this.data.list.forEach((item, index) => {
seriesList.push({
name: item.name,
type: 'line',
stack: 'Total',
// stack: 'Total',
data: item.value,
areaStyle: {
normal: {
@@ -74,7 +73,6 @@ export default {
},
smooth: true
})
legendList.push(item.name)
})
const option = {
@@ -87,13 +85,13 @@ export default {
formatter: (value) => {
let title = this.data.text + '<br/>'
value.forEach(item => {
title = title + item.marker + item.seriesName + ' : ' + this.data.list[item.seriesIndex].label[item.dataIndex] + '<br/>'
title = title + item.marker + item.seriesName + ' : ' + getSizeDescByByteCount(item.data) + '<br/>'
})
return title
}
},
legend: {
data: legendList,
data: this.data.legendList,
left: 'right'
},
grid: {
@@ -70,37 +70,19 @@ export default {
})
},
getChartData(last7dFlow) {
const title = []
const downFlowDesc = []
const totalFlowDesc = []
const upFlowDesc = []
last7dFlow.dataList.forEach(item => {
title.push(item.dateStr)
totalFlowDesc.push(item.totalFlowDesc)
downFlowDesc.push(item.downFlowDesc)
upFlowDesc.push(item.upFlowDesc)
})
const list = []
last7dFlow.seriesList.forEach(item => {
let label = []
if (item.seriesName.indexOf('上') > -1) {
label = upFlowDesc
} else if (item.seriesName.indexOf('下') > -1) {
label = downFlowDesc
} else if (item.seriesName.indexOf('总') > -1) {
label = totalFlowDesc
}
list.push({
name: item.seriesName,
value: item.seriesData,
label: label
value: item.seriesData
})
})
return {
text: '流量监控',
subtext: `最近${last7dFlow.dataList.length || 0}天流量监控`,
title: title,
list: list
title: last7dFlow.xDate,
list: list,
legendList: last7dFlow.legendData
}
}
}
@@ -154,7 +154,7 @@
<el-form-item :label="$t('客户端端口')" prop="clientPort">
<el-input v-model="temp.clientPort"></el-input>
</el-form-item>
<el-form-item :label="$t('域名')" prop="subdomain" v-if="temp.protocal === 'HTTP' && domainName && domainName != ''">
<el-form-item :label="$t('域名')" prop="subdomain" v-if="(temp.protocal === 'HTTP' || temp.protocal === 'HTTP(S)') && domainName && domainName != ''">
<el-input v-model="temp.subdomain">
<template slot="append">.{{ domainName }}</template>
</el-input>
@@ -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;
}
@@ -39,5 +39,7 @@ public class ProxyConfig {
private Integer obtainLicenseInterval;
private String licenseKey;
private Integer threadCount;
private String clientId;
private Boolean transferLogEnable;
}
}
@@ -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();
}
@@ -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;
}
}
}
}
@@ -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
@@ -73,13 +64,15 @@ public class ClientChannelHandler extends SimpleChannelInboundHandler<ProxyMessa
switch (event.state()) {
case READER_IDLE:
// 读超时断开连接
log.info("读超时");
ctx.channel().close();
// log.info("读超时");
// ctx.channel().close();
break;
case WRITER_IDLE:
ctx.channel().writeAndFlush(ProxyMessage.buildHeartbeatMessage());
break;
case ALL_IDLE:
log.info("读写超时");
ctx.close();
break;
}
}
@@ -26,13 +26,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 +42,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 +57,11 @@ public class ProxyClientService {
* 重连次数
*/
private volatile int reconnectCount = 0;
/**
* 启用重连服务
*/
private volatile boolean reconnectServiceEnable = false;
// /**
// * 启用重连服务
// */
// private volatile boolean reconnectServiceEnable = false;
private NioEventLoopGroup workerGroup;
/**
* 重连服务执行器
*/
@@ -65,9 +69,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 +82,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 +98,46 @@ 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());
}
if (null != proxyConfig.getClient().getTransferLogEnable() && proxyConfig.getClient().getTransferLogEnable()) {
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 +152,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 +169,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 +177,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 +186,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 +222,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);
}
}
}
@@ -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);
}
}
@@ -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();
}
}
}
@@ -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) {
@@ -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: 8
all-idle-time-seconds: 0
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,6 @@ 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:}
transfer-log-enable: ${CLIENT_LOG:false}
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_FILE" value="/work/projects/neutrino-proxy-client/app.log"/>
<property name="LOG_FILE" value="./logs/neutrino-proxy-client.log"/>
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{50} - %msg%n"/>
<!-- <property name="ENCODE" value="utf8" />-->
@@ -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) {
+4
View File
@@ -51,6 +51,10 @@
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
@@ -64,8 +64,7 @@ public class DBInitialize implements EventListener<AppLoadEndEvent> {
@Override
public void onEvent(AppLoadEndEvent appLoadEndEvent) throws Throwable {
// TODO 该事件有50%的概率不触发
System.out.println("11");
}
/**
@@ -31,7 +31,6 @@ public class DbConfiguration {
dataSource.setJournalMode(SQLiteConfig.JournalMode.WAL.getValue());
return dataSource;
} else if (DbTypeEnum.MYSQL == dbTypeEnum) {
HikariDataSource dataSource = new HikariDataSource();
String driver = "com.mysql.cj.jdbc.Driver";
try {
Class.forName(driver);
@@ -39,24 +38,26 @@ public class DbConfiguration {
// 对类名的判断,异常则说明不存在:
driver = "com.mysql.jdbc.Driver";
}
dataSource.setDriverClassName(driver);
dataSource.setJdbcUrl(dbConfig.getUrl());
dataSource.setMinimumIdle(5);
dataSource.setMaximumPoolSize(20);
dataSource.setMaxLifetime(60000);
// dataSource.setInitialSize(5);
// dataSource.setMinIdle(5);
// dataSource.setMaxActive(20);
// dataSource.setMaxWait(60000);
// dataSource.setPoolPreparedStatements(true);
dataSource.setUsername(dbConfig.getUsername());
dataSource.setPassword(dbConfig.getPassword());
return dataSource;
return newHikariDataSource(dbConfig, driver);
} else if (DbTypeEnum.MARIADB == dbTypeEnum) {
return newHikariDataSource(dbConfig, "org.mariadb.jdbc.Driver");
}
return null;
}
private HikariDataSource newHikariDataSource(DbConfig dbConfig, String driverClass) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName(driverClass);
dataSource.setJdbcUrl(dbConfig.getUrl());
dataSource.setMinimumIdle(5);
dataSource.setMaximumPoolSize(20);
dataSource.setMaxLifetime(60000);
dataSource.setUsername(dbConfig.getUsername());
dataSource.setPassword(dbConfig.getPassword());
return dataSource;
}
@Bean
public void db1_ext(@Db("db") GlobalConfig globalConfig) {
MetaObjectHandler metaObjectHandler = new MetaObjectHandlerImpl();
@@ -18,10 +18,15 @@ public class ProxyConfig {
@Inject("${neutrino.proxy.protocol}")
private Protocol protocol;
/**
* 服务配置
* 代理服务配置
*/
@Inject("${neutrino.proxy.server}")
private Server server;
/**
* 代理隧道配置
*/
@Inject("${neutrino.proxy.tunnel}")
private Tunnel tunnel;
@Data
public static class Protocol {
@@ -37,15 +42,26 @@ public class ProxyConfig {
@Data
public static class Server {
private Integer bossThreadCount;
private Integer workThreadCount;
private String domainName;
private Integer httpProxyPort;
private Integer httpsProxyPort;
private String keyStorePassword;
private String jksPath;
private Boolean transferLogEnable;
}
@Data
public static class Tunnel {
private Integer bossThreadCount;
private Integer workThreadCount;
private Integer port;
private Integer sslPort;
private String keyStorePassword;
private String keyManagerPassword;
private String jksPath;
private Integer bossThreadCount;
private Integer workThreadCount;
private String domainName;
private Integer httpProxyPort;
private Boolean transferLogEnable;
}
}
@@ -1,5 +1,10 @@
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 io.netty.handler.logging.LoggingHandler;
import org.dromara.neutrinoproxy.core.ProxyDataTypeEnum;
import org.dromara.neutrinoproxy.core.ProxyMessage;
import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
@@ -7,6 +12,9 @@ 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.ProxyTunnelServer;
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,4 +51,35 @@ public class ProxyConfiguration implements LifecycleBean {
return new NioEventLoopGroup(proxyConfig.getServer().getWorkThreadCount());
}
@Bean("tcpServerBootstrap")
public ServerBootstrap tcpServerBootstrap(@Inject("serverBossGroup") NioEventLoopGroup serverBossGroup,
@Inject("serverWorkerGroup") NioEventLoopGroup serverWorkerGroup,
@Inject ProxyConfig proxyConfig
) {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(serverBossGroup, serverWorkerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
if (null != proxyConfig.getServer().getTransferLogEnable() && proxyConfig.getServer().getTransferLogEnable()) {
ch.pipeline().addFirst(new LoggingHandler(ProxyTunnelServer.class));
}
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());
}
@Bean("tunnelWorkerGroup")
public NioEventLoopGroup tunnelWorkerGroup(@Inject ProxyConfig proxyConfig) {
return new NioEventLoopGroup(proxyConfig.getTunnel().getWorkThreadCount());
}
}
@@ -38,7 +38,9 @@ import java.util.stream.Stream;
@AllArgsConstructor
public enum DbTypeEnum {
SQLITE("sqlite"),
MYSQL("mysql");
MYSQL("mysql"),
MARIADB("mariadb"),
;
private String type;
@@ -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,26 @@ 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);
}
public static Boolean isHttp(String desc) {
NetworkProtocolEnum networkProtocolEnum = of(desc);
return NetworkProtocolEnum.HTTP == networkProtocolEnum;
}
}
@@ -51,6 +51,7 @@ public class PortMappingController {
// 目前仅HTTP支持绑定域名
req.setSubdomain(null);
}
req.setProtocal(networkProtocolEnum.getDesc());
return portMappingService.create(req);
}
@@ -74,6 +75,7 @@ public class PortMappingController {
// 目前仅HTTP支持绑定域名
req.setSubdomain(null);
}
req.setProtocal(networkProtocolEnum.getDesc());
return portMappingService.update(req);
}
@@ -1,204 +0,0 @@
package org.dromara.neutrinoproxy.server.proxy.core;
import cn.hutool.core.util.StrUtil;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.neutrinoproxy.core.Constants;
import org.dromara.neutrinoproxy.core.ProxyMessage;
import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig;
import org.dromara.neutrinoproxy.server.proxy.domain.ProxyAttachment;
import org.dromara.neutrinoproxy.server.proxy.domain.VisitorChannelAttachInfo;
import org.dromara.neutrinoproxy.server.service.FlowReportService;
import org.dromara.neutrinoproxy.server.util.ProxyUtil;
import org.noear.solon.Solon;
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;
import java.net.InetSocketAddress;
/**
* @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
public void onEvent(AppLoadEndEvent appLoadEndEvent) throws Throwable {
if (StrUtil.isBlank(proxyConfig.getServer().getDomainName()) || null == proxyConfig.getServer().getHttpProxyPort()) {
log.info("no config domain name,nonsupport http proxy.");
return;
}
this.start();
}
private void start() {
try {
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 VisitorChannelHandler());
}
});
bootstrap.bind("0.0.0.0", proxyConfig.getServer().getHttpProxyPort()).sync();
log.info("Http代理服务启动成功!");
} catch (Exception e) {
log.error("http proxy start err!", e);
}
}
private class VisitorChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf byteBuf) throws Exception {
if (StrUtil.isBlank(proxyConfig.getServer().getDomainName())) {
ctx.channel().close();
return;
}
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
byteBuf.resetReaderIndex();
ProxyAttachment proxyAttachment = new ProxyAttachment(ctx.channel(), bytes, (channel, buf) -> {
Channel proxyChannel = channel.attr(Constants.NEXT_CHANNEL).get();
if (null == proxyChannel) {
// 该端口还没有代理客户端
ctx.channel().close();
return;
}
proxyChannel.writeAndFlush(ProxyMessage.buildTransferMessage(ProxyUtil.getVisitorIdByChannel(channel), bytes));
// 增加流量计数
VisitorChannelAttachInfo visitorChannelAttachInfo = ProxyUtil.getAttachInfo(channel);
Solon.context().getBean(FlowReportService.class).addWriteByte(visitorChannelAttachInfo.getLicenseId(), bytes.length);
});
String visitorId = ProxyUtil.getVisitorIdByChannel(ctx.channel());
if (StringUtils.isNotBlank(visitorId)) {
proxyAttachment.execute();
return;
}
// 用户连接到代理服务器时,设置用户连接不可读,等待代理后端服务器连接成功后再改变为可读状态
ctx.channel().config().setOption(ChannelOption.AUTO_READ, false);
String host = getHost(bytes);
if (StringUtils.isBlank(host)) {
ctx.channel().close();
return;
}
log.debug("HttpProxy host: {}", host);
if (!host.endsWith(proxyConfig.getServer().getDomainName())) {
ctx.channel().close();
return;
}
int index = host.lastIndexOf("." + proxyConfig.getServer().getDomainName());
String subdomain = host.substring(0, index);
// 根据域名拿到绑定的映射对应的cmdChannel
Integer serverPort = ProxyUtil.getServerPortBySubdomain(subdomain);
if (null == serverPort) {
ctx.channel().close();
return;
}
Channel cmdChannel = ProxyUtil.getCmdChannelByServerPort(serverPort);
if (null == cmdChannel) {
ctx.channel().close();
return;
}
String lanInfo = ProxyUtil.getClientLanInfoByServerPort(serverPort);
if (StringUtils.isBlank(lanInfo)) {
ctx.channel().close();
return;
}
visitorId = ProxyUtil.newVisitorId();
ProxyUtil.addVisitorChannelToCmdChannel(cmdChannel, visitorId, ctx.channel(), serverPort);
ProxyUtil.addProxyConnectAttachment(visitorId, proxyAttachment);
cmdChannel.writeAndFlush(ProxyMessage.buildConnectMessage(visitorId).setData(lanInfo.getBytes()));
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 通知代理客户端
Channel visitorChannel = ctx.channel();
InetSocketAddress sa = (InetSocketAddress) visitorChannel.localAddress();
Channel cmdChannel = ProxyUtil.getCmdChannelByServerPort(sa.getPort());
if (cmdChannel == null) {
// 该端口还没有代理客户端
ctx.channel().close();
} else {
// 用户连接断开,从控制连接中移除
String visitorId = ProxyUtil.getVisitorIdByChannel(visitorChannel);
ProxyUtil.removeVisitorChannelFromCmdChannel(cmdChannel, visitorId);
// 删除代理附加对象
ProxyUtil.remoteProxyConnectAttachment(visitorId);
Channel proxyChannel = visitorChannel.attr(Constants.NEXT_CHANNEL).get();
if (proxyChannel != null && proxyChannel.isActive()) {
proxyChannel.attr(Constants.NEXT_CHANNEL).remove();
proxyChannel.attr(Constants.LICENSE_ID).remove();
proxyChannel.attr(Constants.VISITOR_ID).remove();
proxyChannel.config().setOption(ChannelOption.AUTO_READ, true);
// 通知客户端,用户连接已经断开
proxyChannel.writeAndFlush(ProxyMessage.buildDisconnectMessage(visitorId));
}
}
super.channelInactive(ctx);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 当出现异常就关闭连接
ctx.close();
}
private String getHost(byte[] buf) {
String req = new String(buf);
String[] lines = req.split("\r\n");
String firstLine = lines[0];
if (!(firstLine.endsWith("HTTP/1.1") || firstLine.endsWith("HTTP/1.0"))) {
return null;
}
for (int i = 1; i < lines.length; i++) {
String line = lines[i];
if (!line.startsWith("Host: ")) {
continue;
}
// 域名
String domain = line.substring(6);
return domain;
}
return null;
}
}
}
@@ -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);
}
@@ -71,33 +71,42 @@ public class ServerChannelHandler extends SimpleChannelInboundHandler<ProxyMessa
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Channel userChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
if (userChannel != null && userChannel.isActive()) {
Channel visitorChannel = ctx.channel().attr(Constants.NEXT_CHANNEL).get();
if (null != visitorChannel) {
Integer licenseId = ctx.channel().attr(Constants.LICENSE_ID).get();
String visitorId = ctx.channel().attr(Constants.VISITOR_ID).get();
Channel cmdChannel = ProxyUtil.getCmdChannelByLicenseId(licenseId);
if (cmdChannel != null) {
if (null != cmdChannel) {
ProxyUtil.removeVisitorChannelFromCmdChannel(cmdChannel, visitorId);
}
// 数据发送完成后再关闭连接解决http1.0数据传输问题
userChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
userChannel.close();
if (visitorChannel.isActive()) {
// 数据发送完成后再关闭连接解决http1.0数据传输问题
visitorChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
visitorChannel.close();
}
} 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())
);
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());
}
}
ProxyUtil.removeCmdChannel(ctx.channel());
// 即便是因为上述原因断开断开的日志依然要记录方便排查问题
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);
@@ -122,7 +131,6 @@ public class ServerChannelHandler extends SimpleChannelInboundHandler<ProxyMessa
ctx.channel().close();
break;
case WRITER_IDLE:
log.info("写超时");
break;
case ALL_IDLE:
break;
@@ -27,29 +27,19 @@ 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("serverBossGroup")
@Inject("tunnelBossGroup")
private NioEventLoopGroup serverBossGroup;
@Inject("serverWorkerGroup")
@Inject("tunnelWorkerGroup")
private NioEventLoopGroup serverWorkerGroup;
@Inject("${neutrino.proxy.server.port}")
private Integer port;
@Inject("${neutrino.proxy.server.ssl-port}")
private Integer sslPort;
@Inject("${neutrino.proxy.server.jks-path}")
private String jksPath;
@Inject("${neutrino.proxy.server.key-store-password}")
private String keyStorePassword;
@Inject("${neutrino.proxy.server.key-manager-password}")
private String keyManagerPassword;
@Override
public void onEvent(AppLoadEndEvent appLoadEndEvent) throws Throwable {
startProxyServer();
@@ -68,15 +58,15 @@ public class ProxyServerRunner implements EventListener<AppLoadEndEvent> {
}
});
try {
bootstrap.bind(port).sync();
log.info("代理服务启动,端口:{}", port);
bootstrap.bind(proxyConfig.getTunnel().getPort()).sync();
log.info("代理服务启动,端口:{}", proxyConfig.getTunnel().getPort());
} catch (Exception e) {
log.error("代理服务异常", e);
}
}
private void startProxyServerForSSL() {
if (null == sslPort) {
if (null == proxyConfig.getTunnel().getSslPort()) {
return;
}
ServerBootstrap bootstrap = new ServerBootstrap();
@@ -89,8 +79,8 @@ public class ProxyServerRunner implements EventListener<AppLoadEndEvent> {
}
});
try {
bootstrap.bind(sslPort).sync();
log.info("代理服务启动,SSL端口: {}", sslPort);
bootstrap.bind(proxyConfig.getTunnel().getSslPort()).sync();
log.info("代理服务启动,SSL端口: {}", proxyConfig.getTunnel().getSslPort());
} catch (Exception e) {
log.error("代理服务异常", e);
}
@@ -98,13 +88,13 @@ public class ProxyServerRunner implements EventListener<AppLoadEndEvent> {
private ChannelHandler createSslHandler() {
try {
InputStream jksInputStream = FileUtil.getInputStream(jksPath);
InputStream jksInputStream = FileUtil.getInputStream(proxyConfig.getTunnel().getJksPath());
SSLContext serverContext = SSLContext.getInstance("TLS");
final KeyStore ks = KeyStore.getInstance("JKS");
ks.load(jksInputStream, keyStorePassword.toCharArray());
ks.load(jksInputStream, proxyConfig.getTunnel().getKeyStorePassword().toCharArray());
final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, keyManagerPassword.toCharArray());
kmf.init(ks, proxyConfig.getTunnel().getKeyManagerPassword().toCharArray());
TrustManager[] trustManagers = null;
serverContext.init(kmf.getKeyManagers(), trustManagers, null);
@@ -122,12 +112,14 @@ public class ProxyServerRunner implements EventListener<AppLoadEndEvent> {
}
private void proxyServerCommonInitHandler(SocketChannel ch) {
// ch.pipeline().addFirst(new LoggingHandler(ProxyServerRunner.class));
if (null != proxyConfig.getTunnel().getTransferLogEnable() && proxyConfig.getTunnel().getTransferLogEnable()) {
ch.pipeline().addFirst(new LoggingHandler(ProxyTunnelServer.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 ServerChannelHandler());
ch.pipeline().addLast(new ProxyTunnelChannelHandler());
}
}
@@ -4,7 +4,6 @@ import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.neutrinoproxy.core.Constants;
import org.dromara.neutrinoproxy.core.ProxyMessage;
import org.dromara.neutrinoproxy.server.proxy.domain.ProxyAttachment;
import org.dromara.neutrinoproxy.server.proxy.domain.VisitorChannelAttachInfo;
import org.dromara.neutrinoproxy.server.service.FlowReportService;
import org.dromara.neutrinoproxy.server.util.ProxyUtil;
@@ -23,7 +22,7 @@ import java.net.InetSocketAddress;
* @date: 2022/6/16
*/
@Slf4j
public class VisitorChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
public class TcpVisitorChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
@@ -91,8 +90,7 @@ public class VisitorChannelHandler extends SimpleChannelInboundHandler<ByteBuf>
InetSocketAddress sa = (InetSocketAddress) visitorChannel.localAddress();
Channel cmdChannel = ProxyUtil.getCmdChannelByServerPort(sa.getPort());
if (cmdChannel == null) {
if (null == cmdChannel) {
// 该端口还没有代理客户端
ctx.channel().close();
} else {
@@ -127,12 +125,12 @@ public class VisitorChannelHandler extends SimpleChannelInboundHandler<ByteBuf>
InetSocketAddress sa = (InetSocketAddress) visitorChannel.localAddress();
Channel cmdChannel = ProxyUtil.getCmdChannelByServerPort(sa.getPort());
if (cmdChannel == null) {
if (null == cmdChannel) {
// 该端口还没有代理客户端
ctx.channel().close();
} else {
Channel proxyChannel = visitorChannel.attr(Constants.NEXT_CHANNEL).get();
if (proxyChannel != null) {
if (null != proxyChannel) {
proxyChannel.config().setOption(ChannelOption.AUTO_READ, visitorChannel.isWritable());
}
}
@@ -0,0 +1,58 @@
package org.dromara.neutrinoproxy.server.proxy.enhance;
import cn.hutool.core.util.StrUtil;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig;
import org.dromara.neutrinoproxy.server.proxy.core.BytesMetricsHandler;
import org.dromara.neutrinoproxy.server.proxy.core.ProxyTunnelServer;
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
private ProxyConfig proxyConfig;
@Override
public void onEvent(AppLoadEndEvent appLoadEndEvent) throws Throwable {
if (StrUtil.isBlank(proxyConfig.getServer().getDomainName()) || null == proxyConfig.getServer().getHttpProxyPort()) {
log.info("no config domain name,nonsupport http proxy.");
return;
}
this.start();
}
private void start() {
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(new NioEventLoopGroup(1), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
if (null != proxyConfig.getServer().getTransferLogEnable() && proxyConfig.getServer().getTransferLogEnable()) {
ch.pipeline().addFirst(new LoggingHandler(ProxyTunnelServer.class));
}
ch.pipeline().addFirst(new BytesMetricsHandler());
ch.pipeline().addLast(new HttpVisitorChannelHandler(proxyConfig.getServer().getDomainName()));
}
});
bootstrap.bind("0.0.0.0", proxyConfig.getServer().getHttpProxyPort()).sync();
log.info("Http代理服务启动成功!");
} catch (Exception e) {
log.error("http proxy start err!", e);
}
}
}
@@ -0,0 +1,170 @@
package org.dromara.neutrinoproxy.server.proxy.enhance;
import cn.hutool.core.util.StrUtil;
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;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.neutrinoproxy.core.Constants;
import org.dromara.neutrinoproxy.core.ProxyMessage;
import org.dromara.neutrinoproxy.server.proxy.domain.ProxyAttachment;
import org.dromara.neutrinoproxy.server.proxy.domain.VisitorChannelAttachInfo;
import org.dromara.neutrinoproxy.server.service.FlowReportService;
import org.dromara.neutrinoproxy.server.util.ProxyUtil;
import org.noear.solon.Solon;
import java.net.InetSocketAddress;
/**
* @author: aoshiguchen
* @date: 2023/5/27
*/
@Slf4j
public class HttpVisitorChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
/**
* 域名
*/
private String domainName;
public HttpVisitorChannelHandler(String domainName) {
this.domainName = domainName;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf byteBuf) throws Exception {
if (StrUtil.isBlank(domainName)) {
ctx.channel().close();
return;
}
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
byteBuf.resetReaderIndex();
ProxyAttachment proxyAttachment = new ProxyAttachment(ctx.channel(), bytes, (channel, buf) -> {
Channel proxyChannel = channel.attr(Constants.NEXT_CHANNEL).get();
if (null == proxyChannel) {
// 该端口还没有代理客户端
ctx.channel().close();
return;
}
proxyChannel.writeAndFlush(ProxyMessage.buildTransferMessage(ProxyUtil.getVisitorIdByChannel(channel), bytes));
// 增加流量计数
VisitorChannelAttachInfo visitorChannelAttachInfo = ProxyUtil.getAttachInfo(channel);
Solon.context().getBean(FlowReportService.class).addWriteByte(visitorChannelAttachInfo.getLicenseId(), bytes.length);
});
String visitorId = ProxyUtil.getVisitorIdByChannel(ctx.channel());
if (StringUtils.isNotBlank(visitorId)) {
proxyAttachment.execute();
return;
}
// 用户连接到代理服务器时,设置用户连接不可读,等待代理后端服务器连接成功后再改变为可读状态
ctx.channel().config().setOption(ChannelOption.AUTO_READ, false);
String host = getHost(bytes);
log.debug("HttpProxy host: {}", host);
if (StringUtils.isBlank(host)) {
ctx.channel().close();
return;
}
if (!host.endsWith(domainName)) {
ctx.channel().close();
return;
}
int index = host.lastIndexOf("." + domainName);
String subdomain = host.substring(0, index);
// 根据域名拿到绑定的映射对应的cmdChannel
Integer serverPort = ProxyUtil.getServerPortBySubdomain(subdomain);
if (null == serverPort) {
ctx.channel().close();
return;
}
Channel cmdChannel = ProxyUtil.getCmdChannelByServerPort(serverPort);
if (null == cmdChannel) {
ctx.channel().close();
return;
}
String lanInfo = ProxyUtil.getClientLanInfoByServerPort(serverPort);
if (StringUtils.isBlank(lanInfo)) {
ctx.channel().close();
return;
}
visitorId = ProxyUtil.newVisitorId();
ProxyUtil.addVisitorChannelToCmdChannel(cmdChannel, visitorId, ctx.channel(), serverPort);
ProxyUtil.addProxyConnectAttachment(visitorId, proxyAttachment);
cmdChannel.writeAndFlush(ProxyMessage.buildConnectMessage(visitorId).setData(lanInfo.getBytes()));
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 通知代理客户端
Channel visitorChannel = ctx.channel();
InetSocketAddress sa = (InetSocketAddress) visitorChannel.localAddress();
Channel cmdChannel = ProxyUtil.getCmdChannelByServerPort(sa.getPort());
if (cmdChannel == null) {
// 该端口还没有代理客户端
ctx.channel().close();
} else {
// 用户连接断开,从控制连接中移除
String visitorId = ProxyUtil.getVisitorIdByChannel(visitorChannel);
ProxyUtil.removeVisitorChannelFromCmdChannel(cmdChannel, visitorId);
// 删除代理附加对象
ProxyUtil.remoteProxyConnectAttachment(visitorId);
Channel proxyChannel = visitorChannel.attr(Constants.NEXT_CHANNEL).get();
if (proxyChannel != null && proxyChannel.isActive()) {
proxyChannel.attr(Constants.NEXT_CHANNEL).remove();
proxyChannel.attr(Constants.LICENSE_ID).remove();
proxyChannel.attr(Constants.VISITOR_ID).remove();
proxyChannel.config().setOption(ChannelOption.AUTO_READ, true);
// 通知客户端,用户连接已经断开
proxyChannel.writeAndFlush(ProxyMessage.buildDisconnectMessage(visitorId));
}
}
super.channelInactive(ctx);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 当出现异常就关闭连接
ctx.close();
}
private String getHost(byte[] buf) {
String req = new String(buf);
String[] lines = req.split("\r\n");
String firstLine = lines[0];
if (!(firstLine.endsWith("HTTP/1.1") || firstLine.endsWith("HTTP/1.0"))) {
return null;
}
for (int i = 1; i < lines.length; i++) {
String line = lines[i];
if (!line.startsWith("Host: ")) {
continue;
}
// 域名
String domain = line.substring(6);
return domain;
}
return null;
}
}
@@ -0,0 +1,92 @@
package org.dromara.neutrinoproxy.server.proxy.enhance;
import cn.hutool.core.util.StrUtil;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslHandler;
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.dromara.neutrinoproxy.server.proxy.core.ProxyTunnelServer;
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;
import javax.net.ssl.*;
import java.io.InputStream;
import java.security.KeyStore;
/**
* HTTPS代理
* @author: aoshiguchen
* @date: 2023/4/2
*/
@Slf4j
@Component
public class HttpsProxy implements EventListener<AppLoadEndEvent> {
@Inject
private ProxyConfig proxyConfig;
@Override
public void onEvent(AppLoadEndEvent appLoadEndEvent) throws Throwable {
if (StrUtil.isBlank(proxyConfig.getServer().getDomainName()) || null == proxyConfig.getServer().getHttpsProxyPort() ||
StringUtils.isEmpty(proxyConfig.getServer().getJksPath()) || StringUtils.isEmpty(proxyConfig.getServer().getKeyStorePassword())) {
log.info("no config domain name,nonsupport https proxy.");
return;
}
this.start();
}
private void start() {
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(new NioEventLoopGroup(1), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
if (null != proxyConfig.getServer().getTransferLogEnable() && proxyConfig.getServer().getTransferLogEnable()) {
ch.pipeline().addFirst(new LoggingHandler(ProxyTunnelServer.class));
}
ch.pipeline().addLast(createSslHandler());
ch.pipeline().addFirst(new BytesMetricsHandler());
ch.pipeline().addLast(new HttpVisitorChannelHandler(proxyConfig.getServer().getDomainName()));
}
});
bootstrap.bind("0.0.0.0", proxyConfig.getServer().getHttpsProxyPort()).sync();
log.info("Https代理服务启动成功!");
} catch (Exception e) {
log.error("https proxy start err!", e);
}
}
private ChannelHandler createSslHandler() {
try {
InputStream jksInputStream = FileUtil.getInputStream(proxyConfig.getServer().getJksPath());
SSLContext serverContext = SSLContext.getInstance("TLS");
final KeyStore ks = KeyStore.getInstance("JKS");
ks.load(jksInputStream, proxyConfig.getServer().getKeyStorePassword().toCharArray());
final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, proxyConfig.getServer().getKeyStorePassword().toCharArray());
TrustManager[] trustManagers = null;
serverContext.init(kmf.getKeyManagers(), trustManagers, null);
SSLEngine sslEngine = serverContext.createSSLEngine();
sslEngine.setUseClientMode(false);
sslEngine.setNeedClientAuth(false);
return new SslHandler(sslEngine);
} catch (Exception e) {
log.error("创建SSL处理器失败", e);
e.printStackTrace();
}
return null;
}
}
@@ -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
@@ -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)
@@ -146,7 +149,7 @@ public class PortMappingService implements LifecycleBean {
// 更新VisitorChannel
visitorChannelService.addVisitorChannelByPortMapping(portMappingDO);
// 更新域名映射
if (NetworkProtocolEnum.HTTP.getDesc().equals(portMappingDO.getProtocal()) &&
if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) &&
StrUtil.isNotBlank(proxyConfig.getServer().getDomainName()) &&
StrUtil.isNotBlank(portMappingDO.getSubdomain())) {
ProxyUtil.setSubdomainToServerPort(portMappingDO.getSubdomain(), portMappingDO.getServerPort());
@@ -185,12 +188,12 @@ public class PortMappingService implements LifecycleBean {
// 更新VisitorChannel
visitorChannelService.updateVisitorChannelByPortMapping(oldPortMappingDO, portMappingDO);
// 删除老的域名映射
if (NetworkProtocolEnum.HTTP.getDesc().equals(oldPortMappingDO.getProtocal()) &&
if (NetworkProtocolEnum.isHttp(oldPortMappingDO.getProtocal()) &&
StrUtil.isNotBlank(oldPortMappingDO.getSubdomain())) {
ProxyUtil.removeSubdomainToServerPort(oldPortMappingDO.getSubdomain());
}
// 更新域名映射
if (NetworkProtocolEnum.HTTP.getDesc().equals(portMappingDO.getProtocal()) &&
if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) &&
StrUtil.isNotBlank(proxyConfig.getServer().getDomainName()) &&
StrUtil.isNotBlank(portMappingDO.getSubdomain())) {
ProxyUtil.setSubdomainToServerPort(portMappingDO.getSubdomain(), portMappingDO.getServerPort());
@@ -265,7 +268,7 @@ public class PortMappingService implements LifecycleBean {
// 更新VisitorChannel
visitorChannelService.removeVisitorChannelByPortMapping(portMappingDO);
// 更新域名映射
if (NetworkProtocolEnum.HTTP.getDesc().equals(portMappingDO.getProtocal()) &&
if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) &&
StrUtil.isNotBlank(portMappingDO.getSubdomain())) {
ProxyUtil.removeSubdomainToServerPort(portMappingDO.getSubdomain());
}
@@ -2,6 +2,7 @@ package org.dromara.neutrinoproxy.server.service;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.dromara.neutrinoproxy.server.constant.NetworkProtocolEnum;
import org.dromara.neutrinoproxy.server.controller.res.system.ProtocalListRes;
import org.noear.solon.annotation.Component;
@@ -22,7 +23,7 @@ public class ProtocalService {
public List<ProtocalListRes> list() {
return Lists.newArrayList(
new ProtocalListRes().setName("TCP").setEnable(Boolean.TRUE).setRemark("支持一切TCP之上的协议"),
new ProtocalListRes().setName("HTTP").setEnable(Boolean.TRUE).setRemark("支持绑定子域名,未绑定时等价于时使用TCP"),
new ProtocalListRes().setName("HTTP(S)").setEnable(Boolean.TRUE).setRemark("支持绑定子域名,未绑定时等价于时使用TCP。 若配置了证书,则同时支持HTTPS。"),
new ProtocalListRes().setName("UDP").setEnable(Boolean.FALSE).setRemark("暂不支持")
);
}
@@ -14,7 +14,7 @@ import org.dromara.neutrinoproxy.server.dal.entity.PortMappingDO;
import org.dromara.neutrinoproxy.server.dal.entity.PortPoolDO;
import org.dromara.neutrinoproxy.server.dal.entity.UserDO;
import org.dromara.neutrinoproxy.server.proxy.core.BytesMetricsHandler;
import org.dromara.neutrinoproxy.server.proxy.core.VisitorChannelHandler;
import org.dromara.neutrinoproxy.server.proxy.core.TcpVisitorChannelHandler;
import org.dromara.neutrinoproxy.server.proxy.domain.CmdChannelAttachInfo;
import org.dromara.neutrinoproxy.server.proxy.domain.ProxyMapping;
import org.dromara.neutrinoproxy.server.util.ProxyUtil;
@@ -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 VisitorChannelHandler());
}
});
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表示该端口已经绑定过
@@ -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,20 +9,28 @@ neutrino:
length-field-length: 4
initial-bytes-to-strip: 0
length-adjustment: 0
read-idle-time: 40
write-idle-time: 10
read-idle-time: 30
write-idle-time: 5
all-idle-time-seconds: 0
server:
boss-thread-count: 10
work-thread-count: 60
tunnel:
boss-thread-count: 2
work-thread-count: 10
port: ${OPEN_PORT:9000}
ssl-port: ${SSL_PORT:9002}
key-store-password: ${STORE_PASS:123456}
key-manager-password: ${MGR_PASS:123456}
jks-path: ${JKS_PATH:classpath:/test.jks}
transfer-log-enable: ${TUNNEL_LOG:false}
server:
boss-thread-count: 5
work-thread-count: 20
http-proxy-port: ${HTTP_PROXY_PORT:80}
https-proxy-port: ${HTTPS_PROXY_PORT:443}
# 如果不配置,则不支持域名映射
domain-name: ${DOMAIN_NAME:}
key-store-password: ${HTTPS_STORE_PASS:}
jks-path: ${HTTPS_JKS_PATH:}
transfer-log-enable: ${SERVER_LOG:false}
data:
db:
type: ${DB_TYPE:sqlite}
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_FILE" value="/work/projects/neutrino-proxy-server/app.log"/>
<property name="LOG_FILE" value="./logs/neutrino-proxy-server.log"/>
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{50} - %msg%n"/>
<!-- <property name="ENCODE" value="utf8" />-->
@@ -0,0 +1,207 @@
##########################################################
#
CREATE TABLE IF NOT EXISTS `user` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(50) NOT NULL COMMENT '用户名',
`login_name` varchar(50) NOT NULL COMMENT '登录名',
`login_password` varchar(255) NOT NULL COMMENT '登录密码',
`enable` int NOT NULL COMMENT '是否启用(1、启用 2、禁用)',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
`update_time` datetime(3) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `I_user_login_name` (`login_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
#token表
CREATE TABLE IF NOT EXISTS `user_token` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`token` varchar(50) NOT NULL COMMENT 'token',
`user_id` int NOT NULL COMMENT '用户ID',
`expiration_time` datetime(3) NOT NULL COMMENT '过期时间',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
`update_time` datetime(3) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `I_user_token_user_id` (`user_id`),
KEY `I_user_token_token` (`token`),
KEY `I_user_token_expiration_time` (`expiration_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
#
CREATE TABLE IF NOT EXISTS `port_pool` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` int NOT NULL DEFAULT 1 COMMENT '分组ID',
`port` int NOT NULL COMMENT '端口',
`enable` int NOT NULL COMMENT '是否启用(1、启用 2、禁用)',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
`update_time` datetime(3) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `I_port_pool_port` (`port`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
#
CREATE TABLE IF NOT EXISTS `port_group` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(255) NOT NULL COMMENT '分组名称',
`possessor_type` int NOT NULL DEFAULT '0' COMMENT '所有者类型 (0、全局共享 1、用户所有 2License所有) ',
`possessor_id` int NOT NULL DEFAULT '-1' COMMENT '所有者id(当type为0时 固定为-1、当type为1时为用户id 、当type为2时为licenseid)',
`enable` int NOT NULL COMMENT '是否启用(1、启用 2、禁用)',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
`update_time` datetime(3) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
##########################################################
#license表
CREATE TABLE IF NOT EXISTS `license` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(50) NOT NULL COMMENT 'license名称',
`key` varchar(100) NOT NULL COMMENT 'license key',
`user_id` int NOT NULL COMMENT '用户ID',
`is_online` int NOT NULL COMMENT '是否在线(1、在线 2、离线)',
`enable` int NOT NULL COMMENT '是否启用(1、启用 2、禁用)',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
`update_time` datetime(3) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `I_license_key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
#
CREATE TABLE IF NOT EXISTS `port_mapping` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`license_id` int NOT NULL COMMENT 'licenseID',
`protocal` varchar(10) NOT NULL DEFAULT 'TCP' COMMENT '协议',
`subdomain` varchar(50) DEFAULT NULL COMMENT '子域名(仅HTTP时有效)',
`server_port` int NOT NULL COMMENT '服务端端口',
`client_ip` varchar(20) NOT NULL COMMENT '客户端IP',
`client_port` int NOT NULL COMMENT '客户端端口',
`is_online` int NOT NULL COMMENT '是否在线(1、在线 2、离线)',
`description` varchar(100) DEFAULT NULL COMMENT '描述',
`enable` int NOT NULL COMMENT '是否启用(1、启用 2、禁用)',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
`update_time` datetime(3) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `I_port_mapping_server_port` (`server_port`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
##########################################################
#
CREATE TABLE IF NOT EXISTS `user_login_record` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int NOT NULL COMMENT '用户ID',
`ip` varchar(50) NOT NULL COMMENT 'IP',
`token` varchar(100) NOT NULL COMMENT 'token',
`type` int NOT NULL COMMENT '类型(1、登录 2、登出)',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
#
CREATE TABLE IF NOT EXISTS `client_connect_record` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`ip` varchar(50) NOT NULL COMMENT 'IP',
`license_id` int NOT NULL COMMENT 'licenseId',
`type` int NOT NULL COMMENT '类型(1、连接 2、断开连接)',
`msg` varchar(512) DEFAULT NULL COMMENT '消息',
`code` int NOT NULL COMMENT '结果 1、成功 2、失败)',
`err` text DEFAULT NULL COMMENT '异常信息',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
##########################################################
#
CREATE TABLE IF NOT EXISTS `job_info` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`desc` varchar(255) NOT NULL COMMENT '描述',
`handler` varchar(255) NOT NULL COMMENT '处理器',
`cron` varchar(128) NOT NULL COMMENT 'cron',
`param` varchar(512) DEFAULT NULL COMMENT '参数',
`alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮箱',
`alarm_ding` varchar(255) DEFAULT NULL COMMENT '报警钉钉配置',
`enable` int NOT NULL COMMENT '是否启用(1、启用 2、禁用)',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
`update_time` datetime(3) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `I_job_info_handler` (`handler`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
#
CREATE TABLE IF NOT EXISTS `job_log` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`job_id` int NOT NULL COMMENT 'JobId',
`handler` varchar(255) NOT NULL COMMENT '处理器',
`param` varchar(512) DEFAULT NULL COMMENT '参数',
`code` int NOT NULL COMMENT '结果(1、成功 2、失败)',
`msg` text COMMENT '消息',
`alarm_status` int NOT NULL COMMENT '报警状态(1、未报警 2、已报警)',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `I_job_log_create_time` (`create_time`) USING BTREE,
KEY `I_job_log_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
##########################################################
#-(24)
CREATE TABLE IF NOT EXISTS `flow_report_minute` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int NOT NULL COMMENT '用户ID',
`license_id` int NOT NULL COMMENT 'licenseId',
`write_bytes` int NOT NULL COMMENT '写入流量',
`read_bytes` int NOT NULL COMMENT '读取流量',
`date` datetime(3) NOT NULL COMMENT '时间',
`date_str` varchar(20) NOT NULL COMMENT '时间 yyyy-MM-dd HH:mm',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `I_flow_report_minute_create_time` (`create_time`) USING BTREE,
KEY `I_flow_report_minute_date` (`date`) USING BTREE,
KEY `I_flow_report_minute_user_id` (`user_id`),
KEY `I_flow_report_minute_license_id` (`license_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
#-(60)
CREATE TABLE IF NOT EXISTS `flow_report_hour` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int NOT NULL COMMENT '用户ID',
`license_id` int NOT NULL COMMENT 'licenseId',
`write_bytes` int NOT NULL COMMENT '写入流量',
`read_bytes` int NOT NULL COMMENT '读取流量',
`date` datetime(3) NOT NULL COMMENT '时间',
`date_str` varchar(20) NOT NULL COMMENT '时间 yyyy-MM-dd HH',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `I_flow_report_hour_create_time` (`create_time`) USING BTREE,
KEY `I_flow_report_hour_date` (`date`) USING BTREE,
KEY `I_flow_report_hour_user_id` (`user_id`),
KEY `I_flow_report_hour_license_id` (`license_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
#-(1)
CREATE TABLE IF NOT EXISTS `flow_report_day` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int NOT NULL COMMENT '用户ID',
`license_id` int NOT NULL COMMENT 'licenseId',
`write_bytes` int NOT NULL COMMENT '写入流量',
`read_bytes` int NOT NULL COMMENT '读取流量',
`date` datetime(3) NOT NULL COMMENT '时间',
`date_str` varchar(20) NOT NULL COMMENT '时间 yyyy-MM-dd',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `I_flow_report_day_create_time` (`create_time`) USING BTREE,
KEY `I_flow_report_day_date` (`date`) USING BTREE,
KEY `I_flow_report_day_user_id` (`user_id`),
KEY `I_flow_report_day_license_id` (`license_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
#-()
CREATE TABLE IF NOT EXISTS `flow_report_month` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int NOT NULL COMMENT '用户ID',
`license_id` int NOT NULL COMMENT 'licenseId',
`write_bytes` int NOT NULL COMMENT '写入流量',
`read_bytes` int NOT NULL COMMENT '读取流量',
`date` datetime(3) NOT NULL COMMENT '时间',
`date_str` varchar(20) NOT NULL COMMENT '时间 yyyy-MM',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `I_flow_report_month_create_time` (`create_time`) USING BTREE,
KEY `I_flow_report_month_date` (`date`) USING BTREE,
KEY `I_flow_report_month_user_id` (`user_id`),
KEY `I_flow_report_month_license_id` (`license_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -0,0 +1,13 @@
#job_qrtz_trigger_info
INSERT INTO job_info(`id`, `desc`, `handler`, `cron`, `param`, `enable`, `create_time`, `update_time`) VALUES
(1, '示例Job', 'DemoJob', '0/10 * * * * ?', '{"a":101}', 1, now(), now());
INSERT INTO job_info(`id`, `desc`, `handler`, `cron`, `param`, `enable`, `create_time`, `update_time`) VALUES
(2, '数据清理任务', 'DataCleanJob', '0 0 1 * * ?', '', 1, now(), now());
INSERT INTO job_info(`id`, `desc`, `handler`, `cron`, `param`, `enable`, `create_time`, `update_time`) VALUES
(3, '流量统计报表-分钟', 'FlowReportForMinuteJob', '0 */1 * * * ?', '', 1, now(), now());
INSERT INTO job_info(`id`, `desc`, `handler`, `cron`, `param`, `enable`, `create_time`, `update_time`) VALUES
(4, '流量统计报表-小时', 'FlowReportForHourJob', '0 0 */1 * * ?', '', 1, now(), now());
INSERT INTO job_info(`id`, `desc`, `handler`, `cron`, `param`, `enable`, `create_time`, `update_time`) VALUES
(5, '流量统计报表-天', 'FlowReportForDayJob', '0 0 1 * * ?', '', 1, now(), now());
INSERT INTO job_info(`id`, `desc`, `handler`, `cron`, `param`, `enable`, `create_time`, `update_time`) VALUES
(6, '流量统计报表-月', 'FlowReportForMonthJob', '0 30 1 1 * ?', '', 1, now(), now());
@@ -0,0 +1,3 @@
#license
INSERT INTO license(`id`, `name`, `key`, `user_id`, `is_online`, `enable`, `create_time`, `update_time`) VALUES
(1, '我的mac', 'b0a907332b474b25897c4dcb31fc7eb6', 1, 2, 1, now(), now());
@@ -0,0 +1,3 @@
#port_group
INSERT INTO `port_group`(`id`,`name`,`possessor_type`,`possessor_id`,`enable`,`create_time`,`update_time`) VALUES
(1, '全局(默认)', 0, -1, 1, now(), now());
@@ -0,0 +1,7 @@
#port_mapping
INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `subdomain`, `client_ip`, `client_port`, `is_online`, `enable`, `create_time`, `update_time`) VALUES
(1, 1, 9101, 'HTTP', 'test1', '127.0.0.1', 8080, 2, 1, now(), now());
INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `subdomain`, `client_ip`, `client_port`, `is_online`, `enable`, `create_time`, `update_time`) VALUES
(2, 1, 9102, 'TCP', '', '127.0.0.1', 3306, 2, 1, now(), now());
INSERT INTO port_mapping(`id`, `license_id`, `server_port`, `protocal`, `subdomain`, `client_ip`, `client_port`, `is_online`, `enable`, `create_time`, `update_time`) VALUES
(3, 1, 9103, 'HTTP', 'test2', '127.0.0.1', 8081, 2, 1, now(), now());
@@ -0,0 +1,41 @@
#
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(1, 1, 9101, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(2, 1, 9102, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(3, 1, 9103, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(4, 1, 9104, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(5, 1, 9105, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(6, 1, 9106, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(7, 1, 9107, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(8, 1, 9108, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(9, 1, 9109, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(10, 1, 9110, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(11, 1, 9111, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(12, 1, 9112, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(13, 1, 9113, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(14, 1, 9114, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(15, 1, 9115, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(16, 1, 9116, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(17, 1, 9117, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(18, 1, 9118, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(19, 1, 9119, 1, now(), now());
INSERT INTO port_pool(`id`, `group_id`, `port`, `enable`, `create_time`, `update_time`) VALUES
(20, 1, 9120, 1, now(), now());
@@ -0,0 +1,5 @@
# 6613b92b77056faeb72068f184ed4c4f
INSERT INTO `user`(`id`, `name`,`login_name`,`login_password`,`enable`,`create_time`, `update_time`) VALUES
(1, '管理员', 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, now(), now());
INSERT INTO `user`(`id`, `name`,`login_name`,`login_password`,`enable`,`create_time`, `update_time`) VALUES
(2, '游客', 'visitor', 'e10adc3949ba59abbe56e057f20f883e', 1, now(), now());
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
@@ -46,8 +46,8 @@ registry.cn-hangzhou.aliyuncs.com/asgc/neutrino-proxy:latest
- 在服务器上新建部署目录:`/work/projects/neutrino-proxy-server`
- 将` neutrino-proxy-server.jar``neutrino-proxy-admin.zip`上传至服务器部署目录。
- 解压`neutrino-proxy-admin.zip`文件
- 执行命令`java -jar neutrino-proxy-server.jar`启动服务端完成部署,默认使用sqlite数据库。
- 若需要指定自己的mysql数据库,同样的需要在当前目录下新建`app.yml`文件,文件内容同上。执行命令`java -jar neutrino-proxy-server.jar config=app.yml`启动服务端完成部署
- 执行命令`java -Dfile.encoding=utf-8 -jar neutrino-proxy-server.jar`启动服务端完成部署,默认使用sqlite数据库。
- 若需要指定自己的mysql数据库,同样的需要在当前目录下新建`app.yml`文件,文件内容同上。执行命令`java -Dfile.encoding=utf-8 -jar neutrino-proxy-server.jar config=app.yml`启动服务端完成部署
- 可参照 https://gitee.com/dromara/neutrino-proxy/blob/master/bin/server_start.sh 使用shell脚本启动服务端。
## 2、管理后台配置
@@ -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:}
```
+11 -4
View File
@@ -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 -->
+5
View File
@@ -97,6 +97,11 @@
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>2.7.4</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
+1 -1
View File
@@ -37,7 +37,7 @@ cp $OUT $JAR_PATH/logs/back_$time.out
fi
rm -f $OUT
cd $JAR_PATH
nohup java $JAVA_OPS -jar $NAME.jar $startupParams > $OUT 2>&1 &
nohup java -Dfile.encoding=utf-8 $JAVA_OPS -jar $NAME.jar $startupParams > $OUT 2>&1 &
echo "sleep 15s wating service start"
sleep 15
tail -200 $OUT
+1 -1
View File
@@ -29,7 +29,7 @@ cp $OUT $JAR_PATH/logs/back_$time.out
fi
rm -f $OUT
cd $JAR_PATH
nohup java $JAVA_OPS -jar $NAME.jar > $OUT 2>&1 &
nohup java -Dfile.encoding=utf-8 $JAVA_OPS -jar $NAME.jar > $OUT 2>&1 &
echo "sleep 15s wating service start"
sleep 15
tail -200 $OUT
+15 -4
View File
@@ -1,10 +1,21 @@
# 1.x剩余规划
- [ ] 增加针对https的支持
# 1.x规划
- Bug
- 优化
- 心跳日志开关
- 客户端启动无限重连开关
- UDP支持
- 官网文档完善
- 常见问题汇总
- HTTPS配置说明
- 协议重构
- 代码重构
- [x] 适配mariadb
# 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 +37,4 @@
# 3.x规划
- [ ] 支持针对用户限速、限流
- [ ] 支持P2P穿透
- [ ] 支持原生编译
- [ ] 支持原生编译