Compare commits

..
55 Commits
Author SHA1 Message Date
aoshiguchen cd4c9f2aae 调整url命名 2023-07-04 21:03:26 +08:00
傲世孤尘 30b115610b !29 端口池管理和端口映射修改
Merge pull request !29 from Metal/dev
2023-07-04 12:41:45 +00:00
aoshiguchen c971bee96c 更新前端打包脚本 2023-07-04 20:39:55 +08:00
Metal 7ea723b578 1、系统管理-端口池管理添加批量删除的功能
2、代理配置-端口映射添加和编辑页面服务端端口字段,支持输入搜索的功能
2023-07-04 20:08:27 +08:00
aoshiguchen 1625187bb1 报文相关日志优化. 2023-07-01 17:02:07 +08:00
aoshiguchen 934ceaadca 报文相关日志优化 2023-07-01 16:09:01 +08:00
aoshiguchen bb7fe668b1 更新官网文档,项目README. 2023-07-01 14:17:16 +08:00
aoshiguchen c385030fc4 更新官网文档,项目README 2023-07-01 14:04:53 +08:00
aoshiguchen 767860643d 新增心跳日志开关处理 2023-06-30 18:16:52 +08:00
aoshiguchen c20a6db44a 服务端客户端,cmd通道读写超时处理调整. 2023-06-30 18:06:03 +08:00
aoshiguchen f423056361 更新赞赏记录 2023-06-30 14:05:09 +08:00
aoshiguchen 86a3cf92b5 更新赞赏记录. 2023-06-26 18:04:40 +08:00
aoshiguchen b28d476081 更新赞赏记录 2023-06-26 18:00:25 +08:00
aoshiguchen 48c8df15e7 1、升级solon版本为2.3.6
2、管理后台支持带context-path访问
2023-06-26 10:30:44 +08:00
傲世孤尘 a31b0ebc95 !28 修复端口映射页面分页查询BUG
Merge pull request !28 from tan90/dev
2023-06-22 14:17:23 +00:00
陈浩杰 fe86d786f4 fix: 修复端口映射查询BUG 2023-06-22 15:51:50 +08:00
aoshiguchen c48404e3bb 更新官网赞助记录 2023-06-12 21:19:50 +08:00
aoshiguchen 5daec7195f 更新文档. 2023-06-09 23:22:56 +08:00
aoshiguchen 533675b367 更新文档.新增打赏记录 2023-06-09 21:43:32 +08:00
aoshiguchen e547895a9c 更新文档. 2023-06-09 21:27:23 +08:00
aoshiguchen 42b7163f5b 支持指定重连间隔、是否无限重连 2023-06-09 20:54:15 +08:00
aoshiguchen 2ceacc6916 支持配置文件、启动参数指定日志级别 2023-06-09 20:41:11 +08:00
aoshiguchen 124273e8d7 日志调整. 2023-06-09 13:07:25 +08:00
aoshiguchen 5a36c88875 支持域名带端口访问 2023-06-09 12:55:24 +08:00
aoshiguchen 70e6d2ab45 更新官网文档 2023-06-08 22:13:52 +08:00
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
71 changed files with 1277 additions and 427 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
+4 -115
View File
@@ -34,130 +34,19 @@
![License管理](assets/run-example/license1.png)
![端口映射管理](assets/run-example/port-mapping1.png)
# 4、项目结构
- neutrino-proxy
- neutrino-proxy-core 与代理相关的公共常量、编解码器
- neutrino-proxy-client 代理客户端项目
- neutrino-proxy-server 代理服务端项目
- neutrino-proxy-admin 代理监控项目(基于vue-element-admin开发)
# 5、运行
## 5.1、使用keytool工具生成ssl证书, 若不需要ssl加密可跳过
```shell
keytool -genkey -alias test1 -keyalg RSA -keysize 1024 -validity 3650 -keypass 123456 -storepass 123456 -keystore "./test.jks"
```
## 5.2、修改服务端配置(application.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:
```
## 5.3、启动服务端
> java -jar neutrino-proxy-server.jar
## 5.4、修改客户端配置
```yml
application:
name: neutrino-proxy-client
proxy:
protocol:
max-frame-length: 2097152
length-field-offset: 0
length-field-length: 4
initial-bytes-to-strip: 0
length-adjustment: 0
read-idle-time: 60
write-idle-time: 30
all-idle-time-seconds: 0
client:
# ssl证书密码
key-store-password: 123456
# ssl证书存放位置
jks-path: classpath:/test.jks
# 服务端ip,若部署到服务器,则配置服务器的ip
server-ip: localhost
# 服务端端口,若使用ssl,则需要配置为服务端的"ssl-port"
server-port: 9000
# 是否启用ssl,启用则必须配置ssl相关参数
ssl-enable: false
```
## 5.5、代理示意图
# 4、代理示意图
![代理流程](assets/design/neutrino-proxy-process.jpg)
## 5.6、启动客户端
### 5.6.1、启动参数直接指定配置
> java -jar neutrino-proxy-client.jar serverIp=localhost serverPort=9000 licenseKey=b0a907332b474b25897c4dcb31fc7eb6
### 5.6.2、启动参数指定外部配置文件
> java -jar neutrino-proxy-client.jar config=app.properties
配置文件格式如下:
```
neutrino.proxy.client.server-ip=localhost
neutrino.proxy.client.server-port=9002
neutrino.proxy.client.ssl-enable=true
neutrino.proxy.client.key-store-password=123456
neutrino.proxy.client.jks-path=classpath:/test.jks
neutrino.proxy.client.license-key=b0a907332b474b25897c4dcb31fc7eb6
```
## 5.7、Docker快速启动
```shell script
# 一键部署前端和后端,客户端按上方部署即可
docker run -it -p 9000-9200:9000-9200/tcp -p 8888:8888 -d --name neutrino registry.cn-hangzhou.aliyuncs.com/asgc/aoshiguchen-docker-images:1.64
# 如没docker环境,可以一键部署docker环境(centos 7.8)系统
curl http://www.wangke666.cn/static/createDocker.sh | bash
```
# 6、演示环境
# 5、演示环境
> 可使用分配好的游客license试用。服务器带宽较低,仅供学习使用!
- 管理后台地址:http://103.163.47.16:9527
- 游客账号:visitor/123456
# 7、未来迭代方向
- 优化代码、增强稳定性
- 服务端增加管理页面,提供报表、授权、限流等功能
- 从项目中分离、孵化出另一个开源项目(neutrino-framework)
# 8、技术文档
- [Aop](./docs/Aop.MD)
- [Channel](./docs/Channel.MD)
# 9、联系我们
# 6、联系我们
- 微信: yuyunshize
- Gitee: https://gitee.com/asgc/neutrino-proxy
# 10、贡献者列表
# 7、贡献者列表
<p>
<a href="https://gitee.com/zcans" target="_blank">
<img src="assets/developer/zcans.png" width="12%">
+1 -1
View File
@@ -1,5 +1,5 @@
module.exports = {
NODE_ENV: '"development"',
ENV_CONFIG: '"docker"',
BASE_API: '"/"'
BASE_API: '"/neutrino-proxy-server"'
}
+10
View File
@@ -60,3 +60,13 @@ export function deletePortPool(id) {
}
})
}
export function deleteBatchPortPool(ids) {
return request({
url: '/port-pool/delete-batch',
method: 'post',
data: {
ids: ids
}
})
}
@@ -8,60 +8,68 @@
<el-button size="mini" @click="handleCancelClick">{{cancelText}}</el-button>
<el-button type="primary" size="mini" @click="handleCommitClick">{{okText}}</el-button>
</div>
<el-button slot="reference" :type="type" :size="size">{{buttonText}}</el-button>
<el-button slot="reference" :type="type" :size="size" :icon="icon" :disabled="disabled">{{buttonText}}</el-button>
</el-popover>
</template>
<script>
export default {
name: 'deleteButton',
props: {
width: {
type: Number,
default: 160
export default {
name: 'deleteButton',
props: {
width: {
type: Number,
default: 160
},
buttonText: {
type: String,
default: '删除'
},
type: {
type: String,
default: 'danger'
},
size: {
type: String,
default: 'mini'
},
icon: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
title: {
type: String,
default: '确定删除吗?'
},
okText: {
type: String,
default: '确定'
},
cancelText: {
type: String,
default: '取消'
}
},
buttonText: {
type: String,
default: '删除'
data() {
return {
visible: false
}
},
type: {
type: String,
default: 'danger'
},
size: {
type: String,
default: 'mini'
},
title: {
type: String,
default: '确定删除吗?'
},
okText: {
type: String,
default: '确定'
},
cancelText: {
type: String,
default: '取消'
}
},
data() {
return {
visible: false
}
},
methods: {
handleCancelClick() {
this.visible = false
this.$emit('handleCancelClick')
},
handleCommitClick() {
this.visible = false
this.$emit('handleCommitClick')
methods: {
handleCancelClick() {
this.visible = false
this.$emit('handleCancelClick')
},
handleCommitClick() {
this.visible = false
this.$emit('handleCommitClick')
}
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
+1
View File
@@ -101,6 +101,7 @@ export default {
publish: 'Publish',
draft: 'Draft',
delete: 'Delete',
deleteBatch: 'DeleteBatch',
cancel: 'Cancel',
confirm: 'Confirm',
portOrRange: 'example: 3306 or 1000-2000'
+1
View File
@@ -121,6 +121,7 @@ export default {
publish: '发布',
draft: '草稿',
delete: '删除',
deleteBatch: '批量删除',
cancel: '取 消',
confirm: '确 定',
userId: '用户ID',
@@ -143,7 +143,7 @@
</el-select>
</el-form-item>
<el-form-item :label="$t('服务端端口')" prop="serverPort">
<el-select style="width: 280px;" class="filter-item" v-model="temp.serverPort" placeholder="请选择">
<el-select style="width: 280px;" class="filter-item" v-model="temp.serverPort" placeholder="请选择" filterable>
<el-option v-for="item in serverPortList" :key="item.port" :label="item.port" :value="item.port">
</el-option>
</el-select>
@@ -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>
@@ -6,10 +6,13 @@
</el-select>
<el-button class="filter-item" type="primary" v-waves icon="el-icon-search" @click="handleFilter">{{$t('table.search')}}</el-button>
<el-button class="filter-item" style="margin-left: 10px;" @click="handleCreate" type="primary" icon="el-icon-edit">{{$t('table.add')}}</el-button>
<button-popover class="filter-item" style="margin-left: 10px;" size="medium" icon="el-icon-delete"
:buttonText="$t('table.deleteBatch')" :disabled="checkBoxData.length==0" @handleCommitClick="handleDeleteBatch" />
</div>
<el-table :key='tableKey' :data="list" v-loading="listLoading" element-loading-text="给我一点时间" border fit highlight-current-row
style="width: 100%">
style="width: 100%" @selection-change="onTableRowChange">
<el-table-column align="center" width="40" type="selection" />
<el-table-column align="center" :label="$t('table.id')" width="120">
<template slot-scope="scope">
<span>{{scope.row.id}}</span>
@@ -92,7 +95,7 @@
</template>
<script>
import { createPortPool, deletePortPool, fetchList, updateEnableStatus, updatePortPool } from '@/api/portPool'
import { createPortPool, deletePortPool, deleteBatchPortPool, fetchList, updateEnableStatus, updatePortPool } from '@/api/portPool'
import { portGroupList } from '@/api/portGroup'
import waves from '@/directive/waves' // 水波纹指令
import { parseTime } from '@/utils'
@@ -157,7 +160,8 @@ const calendarTypeOptions = [
rules: {
port: [{ required: true, message: '端口必填', trigger: 'blur' }]
},
downloadLoading: false
downloadLoading: false,
checkBoxData:[], //表单勾选的行
}
},
filters: {
@@ -320,6 +324,24 @@ const calendarTypeOptions = [
return v[j]
}
}))
},
onTableRowChange(val){ //表单勾选后赋值
this.checkBoxData = val
},
handleDeleteBatch(){ //批量删除操作
let ids = []
this.checkBoxData.forEach(item => {ids.push(item.id)})
deleteBatchPortPool(ids).then(response => {
if (response.data.code === 0) {
this.$notify({
title: '成功',
message: '删除成功',
type: 'success',
duration: 2000
})
this.getList()
}
})
}
}
}
+1 -1
View File
@@ -33,7 +33,7 @@
<filtering>false</filtering>
<excludes>
<exclude>app-*.yml</exclude>
<exclude>app-dev.yml</exclude>
</excludes>
</resource>
</resources>
@@ -1,14 +1,22 @@
package org.dromara.neutrinoproxy.client;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.noear.solon.Solon;
import org.noear.solon.SolonApp;
import org.noear.solon.annotation.SolonMain;
import org.slf4j.LoggerFactory;
/**
*
* @author: aoshiguchen
* @date: 2022/6/16
*/
@Slf4j
@SolonMain
public class ProxyClient {
@@ -20,6 +28,8 @@ public class ProxyClient {
setAlias("neutrino.proxy.client.jksPath", "jksPath");
setAlias("neutrino.proxy.client.keyStorePassword", "keyStorePassword");
setAlias("neutrino.proxy.client.licenseKey", "licenseKey");
// 设置日志级别
setLogLevel(app);
});
}
@@ -34,4 +44,21 @@ public class ProxyClient {
Solon.cfg().put(key, val);
}
}
private static void setLogLevel(SolonApp app) {
String loggerLevel = app.cfg().get("neutrino.proxy.logger.level");
if (StringUtils.isBlank(loggerLevel)) {
return;
}
try {
Level level = Level.toLevel(loggerLevel);
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
for (Logger logger : loggerContext.getLoggerList()) {
logger.setLevel(level);
}
} catch (Exception e) {
log.error("日志级别设置失败", e);
}
}
}
@@ -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,15 @@ public class ProxyConfig {
private Integer obtainLicenseInterval;
private String licenseKey;
private Integer threadCount;
private String clientId;
private Boolean transferLogEnable;
private Boolean heartbeatLogEnable;
private Reconnection reconnection;
}
@Data
public static class Reconnection {
private Integer intervalSeconds;
private Boolean unlimited;
}
}
@@ -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,85 @@
package org.dromara.neutrinoproxy.client.core;
import org.dromara.neutrinoproxy.client.config.ProxyConfig;
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> {
private static volatile Boolean transferLogEnable = Boolean.FALSE;
public CmdChannelHandler() {
ProxyConfig proxyConfig = Solon.context().getBean(ProxyConfig.class);
if (null != proxyConfig.getClient() && null != proxyConfig.getClient().getHeartbeatLogEnable()) {
transferLogEnable = proxyConfig.getClient().getHeartbeatLogEnable();
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ProxyMessage proxyMessage) throws Exception {
if (ProxyMessage.TYPE_HEARTBEAT != proxyMessage.getType() || transferLogEnable) {
log.debug("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.error("Client CmdChannel 读超时,断开连接");
ctx.channel().close();
break;
case WRITER_IDLE:
ctx.channel().writeAndFlush(ProxyMessage.buildHeartbeatMessage());
break;
case ALL_IDLE:
log.error("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.debug("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
@@ -80,8 +71,8 @@ public class ClientChannelHandler extends SimpleChannelInboundHandler<ProxyMessa
ctx.channel().writeAndFlush(ProxyMessage.buildHeartbeatMessage());
break;
case ALL_IDLE:
log.info("读写超时");
ctx.channel().close();
log.debug("读写超时");
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,23 +42,26 @@ 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;
/**
* 重连间隔(秒)
*/
private static final long RECONNECT_INTERVAL_SECONDS = 5;
// /**
// * 重连间隔(秒)
// */
// private static final long RECONNECT_INTERVAL_SECONDS = 5;
/**
* 重连次数
*/
private volatile int reconnectCount = 0;
/**
* 启用重连服务
*/
private volatile boolean reconnectServiceEnable = false;
// /**
// * 启用重连服务
// */
// private volatile boolean reconnectServiceEnable = false;
private NioEventLoopGroup workerGroup;
/**
* 重连服务执行器
*/
@@ -65,38 +69,80 @@ public class ProxyClientService {
@Init
public void init() {
this.reconnectExecutor.scheduleWithFixedDelay(this::reconnect, 0, RECONNECT_INTERVAL_SECONDS, TimeUnit.SECONDS);
this.reconnectExecutor.scheduleWithFixedDelay(this::reconnect, 10, proxyConfig.getClient().getReconnection().getIntervalSeconds(), 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>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
if (null != proxyConfig.getClient().getTransferLogEnable() && proxyConfig.getClient().getTransferLogEnable()) {
ch.pipeline().addFirst(new LoggingHandler(RealServerChannelHandler.class));
}
ch.pipeline().addLast(new RealServerChannelHandler());
}
});
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 {
if (proxyConfig.getClient().getSslEnable()) {
ch.pipeline().addLast(createSslHandler());
}
// ch.pipeline().addFirst(new LoggingHandler(ProxyClientService.class));
if (null != proxyConfig.getClient().getTransferLogEnable() && proxyConfig.getClient().getTransferLogEnable()) {
ch.pipeline().addFirst(new LoggingHandler(ProxyChannelHandler.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 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(CmdChannelHandler.class));
}
ch.pipeline().addLast(new ProxyMessageDecoder(proxyConfig.getProtocol().getMaxFrameLength(),
proxyConfig.getProtocol().getLengthFieldOffset(), proxyConfig.getProtocol().getLengthFieldLength(),
proxyConfig.getProtocol().getLengthAdjustment(), proxyConfig.getProtocol().getInitialBytesToStrip()));
ch.pipeline().addLast(new ProxyMessageEncoder());
ch.pipeline().addLast(new IdleStateHandler(proxyConfig.getProtocol().getReadIdleTime(), proxyConfig.getProtocol().getWriteIdleTime(), proxyConfig.getProtocol().getAllIdleTimeSeconds()));
ch.pipeline().addLast(new CmdChannelHandler());
}
});
try {
this.start();
} catch (Exception e) {
// 启动连不上也做一下重连,因此先catch异常
log.error("[客户端指令隧道] 启动异常", e);
}
}
public void start() {
@@ -111,7 +157,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 +174,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 +182,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 +191,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 +227,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);
}
}
@@ -1,13 +1,17 @@
package org.dromara.neutrinoproxy.client.handler;
import com.alibaba.fastjson.JSONObject;
import org.dromara.neutrinoproxy.client.config.ProxyConfig;
import org.dromara.neutrinoproxy.core.Constants;
import org.dromara.neutrinoproxy.core.ExceptionEnum;
import org.dromara.neutrinoproxy.core.ProxyMessage;
import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
import org.dromara.neutrinoproxy.core.dispatcher.Match;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.noear.solon.Solon;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Inject;
/**
* 认证信息处理器
@@ -18,11 +22,25 @@ import org.noear.solon.annotation.Component;
@Match(type = Constants.ProxyDataTypeName.AUTH)
@Component
public class ProxyMessageAuthHandler implements ProxyMessageHandler {
@Inject
private ProxyConfig proxyConfig;
@Override
public void handle(ChannelHandlerContext context, ProxyMessage proxyMessage) {
String info = proxyMessage.getInfo();
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();
if (!proxyConfig.getClient().getReconnection().getUnlimited()) {
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,15 +9,35 @@ 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
logger:
# 日志级别
level: ${LOG_LEVEL:info}
client:
# 线程池相关配置,用于技术调优,可忽略
thread-count: 50
# 隧道SSL证书配置
key-store-password: ${STORE_PASS:123456}
jks-path: ${JKS_PATH:classpath:/test.jks}
# 服务端IP
server-ip: ${SERVER_IP:localhost}
# 服务端端口(对应服务端app.yml中的tunnel.port、tunnel.ssl-port)
server-port: ${SERVER_PORT:9002}
# 是否启用SSL(注意:该配置必须和server-port对应上)
ssl-enable: ${SSL_ENABLE:true}
obtain-license-interval: 5
license-key: ${LICENSE_KEY:b0a907332b474b25897c4dcb31fc7eb6}
# 客户端连接唯一凭证
license-key: ${LICENSE_KEY:}
# 客户端唯一身份标识(可忽略,若不设置首次启动会自动生成)
client-id: ${CLIENT_ID:}
# 是否开启隧道传输报文日志(日志级别为debug时开启才有效)
transfer-log-enable: ${CLIENT_LOG:false}
# 是否开启心跳日志
heartbeat-log-enable: ${HEARTBEAT_LOG:false}
# 重连设置
reconnection:
# 重连间隔(秒)
interval-seconds: 10
# 是否开启无限重连(未开启时,客户端license不合法会自动停止应用,开启了则不会,请谨慎开启)
unlimited: false
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_FILE" value="./neutrino-proxy-client.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) {
+5 -1
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>
@@ -66,7 +70,7 @@
<filtering>false</filtering>
<excludes>
<exclude>app-*.yml</exclude>
<exclude>app-dev.yml</exclude>
</excludes>
</resource>
</resources>
@@ -1,15 +1,25 @@
package org.dromara.neutrinoproxy.server;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.solonplugins.job.annotation.EnableJob;
import org.noear.solon.Solon;
import org.noear.solon.SolonApp;
import org.noear.solon.annotation.SolonMain;
import org.noear.solon.web.cors.CrossFilter;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
*
* @author: aoshiguchen
* @date: 2022/6/16
*/
@Slf4j
@EnableJob
@SolonMain
public class ProxyServer {
@@ -18,6 +28,25 @@ public class ProxyServer {
Solon.start(ProxyServer.class, args, app -> {
// 跨域支持。加-1 优先级更高
app.filter(-1, new CrossFilter().allowedOrigins("*"));
// 设置日志级别
setLogLevel(app);
});
}
private static void setLogLevel(SolonApp app) {
String loggerLevel = app.cfg().get("neutrino.proxy.logger.level");
if (StringUtils.isBlank(loggerLevel)) {
return;
}
try {
Level level = Level.toLevel(loggerLevel);
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
for (Logger logger : loggerContext.getLoggerList()) {
logger.setLevel(level);
}
} catch (Exception e) {
log.error("日志级别设置失败", e);
}
}
}
@@ -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();
@@ -49,6 +49,7 @@ public class ProxyConfig {
private Integer httpsProxyPort;
private String keyStorePassword;
private String jksPath;
private Boolean transferLogEnable;
}
@Data
@@ -60,6 +61,8 @@ public class ProxyConfig {
private String keyStorePassword;
private String keyManagerPassword;
private String jksPath;
private Boolean transferLogEnable;
private Boolean heartbeatLogEnable;
}
}
@@ -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,6 +51,27 @@ 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(TcpVisitorChannelHandler.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());
@@ -31,7 +31,7 @@ public class VisitLogInterceptor implements RouterInterceptor {
} finally {
Date now = new Date();
long elapsedTime = now.getTime() - startTime.getTime();
log.info("\n-----------------------------------------------------------------接口请求日志:\n{} url:{} 执行耗时:{}\n请求体参数:{}\n响应结果:{}\n客户端IP:{}\n",
log.debug("\n-----------------------------------------------------------------接口请求日志:\n{} url:{} 执行耗时:{}\n请求体参数:{}\n响应结果:{}\n客户端IP:{}\n",
ctx.method(), ctx.path(), getElapsedTimeStr(elapsedTime),
JSONObject.toJSONString(ctx.paramMap()),
JSONObject.toJSONString(JSONObject.toJSONString(ctx.result)),
@@ -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);
}
@@ -123,4 +123,14 @@ public class PortPoolController {
ParamCheckUtil.checkNotEmpty(req.getPortIdList(), "portIdList");
return portPoolService.updateGroup(req);
}
@Post
@Mapping("/delete-batch")
@Authorization(onlyAdmin = true)
public void deleteBatch(PortPoolDeleteBatchReq req) {
ParamCheckUtil.checkNotNull(req, "req");
ParamCheckUtil.checkNotNull(req.getIds(), "ids");
portPoolService.deleteBatch(req.getIds());
}
}
@@ -0,0 +1,14 @@
package org.dromara.neutrinoproxy.server.controller.req.system;
import lombok.Data;
import java.util.List;
/**
* @author: Metal
* @date: 2023/7/3
*/
@Data
public class PortPoolDeleteBatchReq {
private List<Integer> ids;
}
@@ -17,6 +17,6 @@ public class DemoJob implements IJobHandler {
@Override
public void execute(String param) throws Exception {
System.out.println("DemoJob execute param:" + param);
log.debug("DemoJob execute param: {}", param);
}
}
@@ -25,6 +25,7 @@ package org.dromara.neutrinoproxy.server.proxy.core;
import org.dromara.neutrinoproxy.core.Constants;
import org.dromara.neutrinoproxy.core.ProxyMessage;
import org.dromara.neutrinoproxy.core.dispatcher.Dispatcher;
import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig;
import org.dromara.neutrinoproxy.server.constant.ClientConnectTypeEnum;
import org.dromara.neutrinoproxy.server.constant.SuccessCodeEnum;
import org.dromara.neutrinoproxy.server.dal.entity.ClientConnectRecordDO;
@@ -47,15 +48,23 @@ 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;
private static volatile Boolean transferLogEnable = Boolean.FALSE;
public ServerChannelHandler() {
public ProxyTunnelChannelHandler() {
dispatcher = Solon.context().getBean(Dispatcher.class);
ProxyConfig proxyConfig = Solon.context().getBean(ProxyConfig.class);
if (null != proxyConfig.getTunnel() && null != proxyConfig.getTunnel().getHeartbeatLogEnable()) {
transferLogEnable = proxyConfig.getTunnel().getHeartbeatLogEnable();
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ProxyMessage proxyMessage) throws Exception {
if (ProxyMessage.TYPE_HEARTBEAT != proxyMessage.getType() || transferLogEnable) {
log.debug("Server CmdChannel recieved proxy message, type is {}", proxyMessage.getType());
}
dispatcher.dispatch(ctx, proxyMessage);
}
@@ -89,17 +98,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);
@@ -120,10 +136,11 @@ public class ServerChannelHandler extends SimpleChannelInboundHandler<ProxyMessa
switch (event.state()) {
case READER_IDLE:
// 读超时断开连接
log.info("读超时");
log.debug("读超时");
ctx.channel().close();
break;
case WRITER_IDLE:
ctx.channel().writeAndFlush(ProxyMessage.buildHeartbeatMessage());
break;
case ALL_IDLE:
break;
@@ -27,13 +27,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")
@@ -112,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());
}
}
@@ -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;
@@ -6,24 +6,24 @@ 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("serverBossGroup")
private NioEventLoopGroup serverBossGroup;
@Inject("serverWorkerGroup")
private NioEventLoopGroup serverWorkerGroup;
@Inject
private ProxyConfig proxyConfig;
@Override
@@ -38,16 +38,19 @@ 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 {
if (null != proxyConfig.getServer().getTransferLogEnable() && proxyConfig.getServer().getTransferLogEnable()) {
ch.pipeline().addFirst(new LoggingHandler(HttpProxy.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代理服务启动成功!");
log.info("Http代理服务启动成功!port:{}", proxyConfig.getServer().getHttpProxyPort());
} catch (Exception e) {
log.error("http proxy start err!", e);
}
@@ -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;
@@ -68,11 +68,11 @@ public class HttpVisitorChannelHandler extends SimpleChannelInboundHandler<ByteB
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;
}
log.debug("HttpProxy host: {}", host);
if (!host.endsWith(domainName)) {
ctx.channel().close();
return;
@@ -163,7 +163,8 @@ public class HttpVisitorChannelHandler extends SimpleChannelInboundHandler<ByteB
}
// 域名
String domain = line.substring(6);
return domain;
// 去掉域名后面的端口号
return domain.replaceAll(":.*", "");
}
return null;
}
@@ -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;
@@ -6,11 +6,14 @@ 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;
@@ -21,16 +24,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,17 +46,20 @@ 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 {
if (null != proxyConfig.getServer().getTransferLogEnable() && proxyConfig.getServer().getTransferLogEnable()) {
ch.pipeline().addFirst(new LoggingHandler(HttpsProxy.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代理服务启动成功!");
log.info("Https代理服务启动成功!port:{}", proxyConfig.getServer().getHttpsProxyPort());
} catch (Exception e) {
log.error("https proxy start err!", e);
}
@@ -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
@@ -61,7 +61,7 @@ public class JobLogService implements IJobCallback {
String msg = "";
if (null == throwable) {
msg = "执行成功";
log.info("job[id={},name={}]执行完毕", jobInfo.getId(), jobInfo.getName());
log.debug("job[id={},name={}]执行完毕", jobInfo.getId(), jobInfo.getName());
} else {
log.error("job[id={},name={}]执行异常", jobInfo.getId(), jobInfo.getName(), throwable);
msg = "执行异常:\r\n" + ExceptionUtils.getStackTrace(throwable);
@@ -26,7 +26,6 @@ import org.dromara.neutrinoproxy.server.util.ParamCheckUtil;
import ma.glasnost.orika.MapperFacade;
import org.apache.ibatis.solon.annotation.Db;
import org.dromara.neutrinoproxy.server.controller.res.proxy.*;
import org.jetbrains.annotations.Nullable;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Init;
import org.noear.solon.annotation.Inject;
@@ -90,7 +89,6 @@ public class LicenseService implements LifecycleBean {
return licenseList;
}
@Nullable
private List<LicenseListRes> assembleConvertLicenses(List<LicenseDO> list) {
List<LicenseListRes> licenseList = mapperFacade.mapAsList(list, LicenseListRes.class);
if (!CollectionUtil.isEmpty(licenseList)) {
@@ -8,6 +8,8 @@ import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.google.common.collect.Sets;
import ma.glasnost.orika.MapperFacade;
import org.apache.ibatis.solon.annotation.Db;
import org.dromara.neutrinoproxy.server.base.db.DBInitialize;
import org.dromara.neutrinoproxy.server.base.page.PageInfo;
import org.dromara.neutrinoproxy.server.base.page.PageQuery;
@@ -31,14 +33,10 @@ 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.util.ParamCheckUtil;
import ma.glasnost.orika.MapperFacade;
import org.apache.ibatis.solon.annotation.Db;
import org.dromara.neutrinoproxy.server.controller.res.proxy.*;
import org.dromara.neutrinoproxy.server.util.ProxyUtil;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Init;
import org.noear.solon.annotation.Inject;
import org.noear.solon.core.Lifecycle;
import org.noear.solon.core.bean.LifecycleBean;
import java.util.*;
@@ -51,25 +49,25 @@ import java.util.stream.Collectors;
*/
@Component
public class PortMappingService implements LifecycleBean {
@Inject
private MapperFacade mapperFacade;
@Db
private PortMappingMapper portMappingMapper;
@Db
private LicenseMapper licenseMapper;
@Db
private UserMapper userMapper;
@Db
private PortPoolMapper portPoolMapper;
@Inject
private VisitorChannelService visitorChannelService;
@Inject
private MapperFacade mapperFacade;
@Db
private PortMappingMapper portMappingMapper;
@Db
private LicenseMapper licenseMapper;
@Db
private UserMapper userMapper;
@Db
private PortPoolMapper portPoolMapper;
@Inject
private VisitorChannelService visitorChannelService;
@Inject
private PortPoolService portPoolService;
@Inject
private ProxyConfig proxyConfig;
@Inject
private DBInitialize dbInitialize;
@Inject
private PortPoolService portPoolService;
@Inject
private ProxyConfig proxyConfig;
@Inject
private DBInitialize dbInitialize;
public PageInfo<PortMappingListRes> page(PageQuery pageQuery, PortMappingListReq req) {
Page<PortMappingListRes> result = PageHelper.startPage(pageQuery.getCurrent(), pageQuery.getSize());
@@ -77,6 +75,13 @@ public class PortMappingService implements LifecycleBean {
//描述字段为模糊查询在应用层处理否则sqlite不支持
req.setDescription("%" + req.getDescription() + "%");
}
// 协议名称转换
if (StringUtils.isNotBlank(req.getProtocal())) {
NetworkProtocolEnum networkProtocolEnum = NetworkProtocolEnum.of(req.getProtocal());
req.setProtocal(networkProtocolEnum.getDesc());
}
List<PortMappingDO> list = portMappingMapper.selectPortMappingByCondition(req);
List<PortMappingListRes> respList = mapperFacade.mapAsList(list, PortMappingListRes.class);
if (CollectionUtils.isEmpty(list)) {
@@ -108,12 +113,12 @@ 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)
.thenComparing(PortMappingListRes::getLicenseId)
.thenComparing(PortMappingListRes::getCreateTime))
.collect(Collectors.toList());
respList = respList.stream().sorted(Comparator.comparing(PortMappingListRes::getUserId).thenComparing(PortMappingListRes::getLicenseId).thenComparing(PortMappingListRes::getCreateTime)).collect(Collectors.toList());
return PageInfo.of(respList, result.getTotal(), pageQuery.getCurrent(), pageQuery.getSize());
}
@@ -146,9 +151,7 @@ public class PortMappingService implements LifecycleBean {
// 更新VisitorChannel
visitorChannelService.addVisitorChannelByPortMapping(portMappingDO);
// 更新域名映射
if (NetworkProtocolEnum.HTTP.getDesc().equals(portMappingDO.getProtocal()) &&
StrUtil.isNotBlank(proxyConfig.getServer().getDomainName()) &&
StrUtil.isNotBlank(portMappingDO.getSubdomain())) {
if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) && StrUtil.isNotBlank(proxyConfig.getServer().getDomainName()) && StrUtil.isNotBlank(portMappingDO.getSubdomain())) {
ProxyUtil.setSubdomainToServerPort(portMappingDO.getSubdomain(), portMappingDO.getServerPort());
}
return new PortMappingCreateRes();
@@ -185,14 +188,11 @@ public class PortMappingService implements LifecycleBean {
// 更新VisitorChannel
visitorChannelService.updateVisitorChannelByPortMapping(oldPortMappingDO, portMappingDO);
// 删除老的域名映射
if (NetworkProtocolEnum.HTTP.getDesc().equals(oldPortMappingDO.getProtocal()) &&
StrUtil.isNotBlank(oldPortMappingDO.getSubdomain())) {
if (NetworkProtocolEnum.isHttp(oldPortMappingDO.getProtocal()) && StrUtil.isNotBlank(oldPortMappingDO.getSubdomain())) {
ProxyUtil.removeSubdomainToServerPort(oldPortMappingDO.getSubdomain());
}
// 更新域名映射
if (NetworkProtocolEnum.HTTP.getDesc().equals(portMappingDO.getProtocal()) &&
StrUtil.isNotBlank(proxyConfig.getServer().getDomainName()) &&
StrUtil.isNotBlank(portMappingDO.getSubdomain())) {
if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) && StrUtil.isNotBlank(proxyConfig.getServer().getDomainName()) && StrUtil.isNotBlank(portMappingDO.getSubdomain())) {
ProxyUtil.setSubdomainToServerPort(portMappingDO.getSubdomain(), portMappingDO.getServerPort());
}
return new PortMappingUpdateRes();
@@ -203,16 +203,7 @@ public class PortMappingService implements LifecycleBean {
if (null == portMappingDO) {
return null;
}
PortMappingDetailRes res = new PortMappingDetailRes()
.setId(portMappingDO.getId())
.setLicenseId(portMappingDO.getLicenseId())
.setServerPort(portMappingDO.getServerPort())
.setClientIp(portMappingDO.getClientIp())
.setClientPort(portMappingDO.getClientPort())
.setIsOnline(portMappingDO.getIsOnline())
.setEnable(portMappingDO.getEnable())
.setCreateTime(portMappingDO.getCreateTime())
.setUpdateTime(portMappingDO.getUpdateTime());
PortMappingDetailRes res = new PortMappingDetailRes().setId(portMappingDO.getId()).setLicenseId(portMappingDO.getLicenseId()).setServerPort(portMappingDO.getServerPort()).setClientIp(portMappingDO.getClientIp()).setClientPort(portMappingDO.getClientPort()).setIsOnline(portMappingDO.getIsOnline()).setEnable(portMappingDO.getEnable()).setCreateTime(portMappingDO.getCreateTime()).setUpdateTime(portMappingDO.getUpdateTime());
LicenseDO license = licenseMapper.findById(portMappingDO.getLicenseId());
if (null != license) {
@@ -265,8 +256,7 @@ public class PortMappingService implements LifecycleBean {
// 更新VisitorChannel
visitorChannelService.removeVisitorChannelByPortMapping(portMappingDO);
// 更新域名映射
if (NetworkProtocolEnum.HTTP.getDesc().equals(portMappingDO.getProtocal()) &&
StrUtil.isNotBlank(portMappingDO.getSubdomain())) {
if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) && StrUtil.isNotBlank(portMappingDO.getSubdomain())) {
ProxyUtil.removeSubdomainToServerPort(portMappingDO.getSubdomain());
}
}
@@ -292,10 +282,7 @@ public class PortMappingService implements LifecycleBean {
if (StrUtil.isBlank(proxyConfig.getServer().getDomainName())) {
return;
}
List<PortMappingDO> portMappingDOList = portMappingMapper.selectList(new LambdaQueryWrapper<PortMappingDO>()
.eq(PortMappingDO::getProtocal, NetworkProtocolEnum.HTTP.getDesc())
.isNotNull(PortMappingDO::getSubdomain)
);
List<PortMappingDO> portMappingDOList = portMappingMapper.selectList(new LambdaQueryWrapper<PortMappingDO>().eq(PortMappingDO::getProtocal, NetworkProtocolEnum.HTTP.getDesc()).isNotNull(PortMappingDO::getSubdomain));
if (CollectionUtil.isEmpty(portMappingDOList)) {
return;
}
@@ -179,4 +179,15 @@ public class PortPoolService {
LicenseDO licenseDO = licenseMapper.queryById(req.getLicenseId());
return portPoolMapper.getAvailablePortList(req.getLicenseId(), licenseDO.getUserId());
}
public void deleteBatch(List<Integer> ids) {
List<PortPoolDO> portPoolDOList = portPoolMapper.selectBatchIds(ids);
ParamCheckUtil.checkNotNull(portPoolDOList, ExceptionConstant.PORT_NOT_EXIST);
portPoolMapper.deleteBatchIds(ids);
// 更新visitorChannel
portPoolDOList.stream().forEach(portPoolDO -> {
visitorChannelService.updateVisitorChannelByPortPool(portPoolDO.getPort(), EnableStatusEnum.DISABLE.getStatus());
});
}
}
@@ -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表示该端口已经绑定过
@@ -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);
}
}
@@ -1,5 +1,7 @@
server:
# 服务端web端口,用于支持HTTP接口,管理后台页面访问
port: ${WEB_PORT:8888}
context-path: /neutrino-proxy-server
neutrino:
proxy:
@@ -9,31 +11,53 @@ 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
logger:
# 日志级别
level: ${LOG_LEVEL:info}
# 隧道相关配置-用于维持服务端与客户端的通信
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}
# 是否开启隧道传输报文日志(日志级别为debug时开启才有效)
transfer-log-enable: ${TUNNEL_LOG:false}
# 是否开启心跳日志
heartbeat-log-enable: ${HEARTBEAT_LOG:false}
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:}
# 是否开启代理服务报文日志(日志级别为debug时开启才有效)
transfer-log-enable: ${SERVER_LOG:false}
data:
db:
# 数据库类型,目前支持sqlite、mysql、mariadb
type: ${DB_TYPE:sqlite}
# 数据库连接URL
url: ${DB_URL:jdbc:sqlite:data.db}
# 数据库用户名
username: ${DB_USER:}
# 数据库密码
password: ${DB_PASSWORD:}
#添加MIME印射(如果有需要?)
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_FILE" value="./neutrino-proxy-server.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.4,下面的脚本中,可以使用:`registry.cn-hangzhou.aliyuncs.com/asgc/neutrino-proxy:1.8.4` 指定版本安装,推荐使用`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,65 @@
---
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:
logger:
# 日志级别
level: ${LOG_LEVEL:info}
# 隧道相关配置-用于维持服务端与客户端的通信
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}
# 是否开启隧道传输报文日志(日志级别为debug时开启才有效)
transfer-log-enable: ${TUNNEL_LOG:false}
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:}
# 是否开启代理服务报文日志(日志级别为debug时开启才有效)
transfer-log-enable: ${SERVER_LOG:false}
data:
db:
# 数据库类型,目前支持sqlite、mysql、mariadb
type: ${DB_TYPE:sqlite}
# 数据库连接URL
url: ${DB_URL:jdbc:sqlite:data.db}
# 数据库用户名
username: ${DB_USER:}
# 数据库密码
password: ${DB_PASSWORD:}
```
@@ -0,0 +1,38 @@
---
title: 客户端配置
date: 2023-06-09 21:15:35
permalink: /pages/50ce10/
---
# 以下是最新的客户端配置格式(app.yml)
```yml
neutrino:
proxy:
logger:
# 日志级别
level: ${LOG_LEVEL:info}
client:
# 线程池相关配置,用于技术调优,可忽略
thread-count: 50
# 隧道SSL证书配置
key-store-password: ${STORE_PASS:123456}
jks-path: ${JKS_PATH:classpath:/test.jks}
# 服务端IP
server-ip: ${SERVER_IP:localhost}
# 服务端端口(对应服务端app.yml中的tunnel.port、tunnel.ssl-port)
server-port: ${SERVER_PORT:9002}
# 是否启用SSL(注意:该配置必须和server-port对应上)
ssl-enable: ${SSL_ENABLE:true}
# 客户端连接唯一凭证
license-key: ${LICENSE_KEY:}
# 客户端唯一身份标识(可忽略,若不设置首次启动会自动生成)
client-id: ${CLIENT_ID:}
# 是否开启隧道传输报文日志(日志级别为debug时开启才有效)
transfer-log-enable: ${CLIENT_LOG:false}
# 重连设置
reconnection:
# 重连间隔(秒)
interval-seconds: 10
# 是否开启无限重连(未开启时,客户端license不合法会自动停止应用,开启了则不会,请谨慎开启)
unlimited: false
```
@@ -0,0 +1,31 @@
---
title: 常见问题汇总
date: 2023-07-01 13:35:00
permalink: /pages/a00001/
---
## 1、客户端启动后连不上服务端,一直在重连
### 1.1、启动日志包含`not found license-key config`相关字样
出现这种情况,说明客户端没获取到配置的license,可按如下检查:
- 客户端是否有配置app.yml,app.yml中是否有配置`license-key`
- 客户端启动参数是否指定app.yml配置,若未指定客户端不会加载任何jar外部的配置文件
- 客户端启动参数指定的配置文件路径是否正确?如:`java -jar neutrino-proxy-client.jar config=app.yml`,代表`app.yml`文件与jar在同级别目录下,如果不在同级别目录,请使用绝对/相对路径指定
- 除此之外license还可以用以下方法直接在启动参数中指定:
- `java -jar -DLICENSE_KEY=xxxx -DSERVER_IP=x.x.x.x -DSERVER_PORT=9000 -DSSL_ENABLE=false neutrino-proxy-client.jar`
- `java -jar neutrino-proxy-client.jar serverIp=x.x.x.x serverPort=9000 sslEnable=false licenseKey=xxxx`
### 1.2、配置文件确定都已经配置,且启动参数正确指定了,但是没有出现`认证成功日志`
- 确认客户端配置的`server-ip`,必须是服务端所在机器的ip,且保证客户端所在机器能正常ping通
- 确认客户端配置的`server-port``ssl-enable`,默认情况应配置为9000、false或9002、true,除非服务端app.yml有改动隧道对外端口
- 确认服务端对外的9000、9002端口是否已经放开
- 服务端如果采用docker部署,需要确保容器的9000、9002端口映射到宿主机
## 2、关于中微子代理涉及的2类SSL证书
### 2.1、HTTPS证书
该证书用于服务端支持HTTPS,若需要在服务端`app.yml`中配置即可
### 2.2、隧道SSL证书
该证书用于对客户端-服务端之间的数据通信进行加密,因此客户端/服务端均需要配置该证书。可参考如下命令生成:
```
keytool -genkey -alias test1 -keyalg RSA -keysize 1024 -validity 3650 -keypass 123456 -storepass 123456 -keystore "./test.jks"
```
@@ -0,0 +1,25 @@
---
title: 2023年
date: 2023-06-09 21:33:17
permalink: /pages/cded59/
---
| 日期 | 渠道 | 金额 |昵称| 备注 |
|:-----------|:---|:---|:-|:-----------------|
| 2023-06-29 |微信红包| 16.8 |迟迟🌱| 加油 |
| 2023-06-27 |微信红包| 30 |Arno| |
| 2023-06-25 |Gitee捐助| 10 |zhujue888| 感谢您的开源项目! |
| 2023-06-16 |微信红包| 50 |小跟班| |
| 2023-06-10 |Gitee捐助| 10 |失败女神| 感谢您的开源项目! |
| 2023-06-09 |Gitee捐助| 50 |Admin| 感谢您的开源项目! |
| 2023-06-09 |微信红包| 50 |李阳| 开源无限好 |
| 2023-06-09 |Gitee捐助| 50 |罗宾| 感谢您的开源项目! |
| 2023-06-07 |Gitee捐助| 50 |罗宾| 感谢您的开源项目! |
| 2023-06-03 |Gitee捐助| 50 |Admin| 感谢您的开源项目! |
| 2023-05-29 |微信红包| 20 |TYY| |
| 2023-05-29 |Gitee捐助| 10 |笑看| 感谢您的开源项目! |
| 2023-05-26 |微信红包| 50 |至少还有满天星光照耀你| |
| 2023-02-23 |Gitee捐助| 10 |Yohanes| 感谢您的开源项目! |
| 2023-02-23 |Gitee捐助| 20 |jam_lee| 感谢您的开源项目!希望能支持域名映射|
| 2023-02-10 |Gitee捐助| 5 |实习两年半| 感谢您的开源项目! |
| 2023-02-02 |Gitee捐助| 10 |阳光很暖| 感谢您的开源项目! |
+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 -->
+6 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.noear</groupId>
<artifactId>solon-parent</artifactId>
<version>2.2.8</version>
<version>2.3.6</version>
<relativePath />
</parent>
@@ -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>
+3 -1
View File
@@ -41,4 +41,6 @@ npm run build:$env
#拷贝
cd ..
cp -rf ./neutrino-proxy-admin/dist $adminDeployDir/
cp -rf ./neutrino-proxy-admin/dist/ $giteePagesDir
cp -rf ./neutrino-proxy-admin/dist/ $giteePagesDir
cd $serverDeployDir
zip -r neutrino-proxy-admin.zip "neutrino-proxy-admin/"
+17 -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规划
- 插件开发
@@ -19,6 +30,8 @@
- [ ] 支持调整服务端端口、证书
- [ ] 服务端版本、协议版本、客户端版本管理
- [ ] 服务端日志
- [ ] 支持持管理后台动态调整日志级别。
- [ ] 动态调整转发、报文、心跳输出
- client+计划启动
- [ ] 安卓客户端
- [ ] 基于electron-egg的多平台客户端
@@ -26,4 +39,4 @@
# 3.x规划
- [ ] 支持针对用户限速、限流
- [ ] 支持P2P穿透
- [ ] 支持原生编译
- [ ] 支持原生编译