Compare commits

..
Author SHA1 Message Date
aoshiguchen 670f581185 udp代理下安全组放行逻辑调整 2023-12-09 22:28:34 +08:00
傲世孤尘 e642e5f103 !51 修改安全组在TCP协议和UDP协议控制生效的生命周期
Merge pull request !51 from NichenFly/dev
2023-12-09 13:52:08 +00:00
aoshiguchen d551aaeff6 更新sql脚本 2023-12-09 21:50:38 +08:00
az 78f2488fe9 1、安全组不允许修改默认放行类型
2、添加TCP协议和UDP协议的安全组判断在读取数据时判断是否放行
2023-12-09 21:41:14 +08:00
傲世孤尘 035283a757 !49 安全组管控带有域名的HTTP映射
Merge pull request !49 from NichenFly/dev
2023-12-09 12:39:58 +00:00
az 644102996b 添加安全组对http(s)的控制,并添加安全规则的默认配置 2023-12-09 20:33:01 +08:00
傲世孤尘 2ee2a5f38e !48 更新sql以及配置文件还原
Merge pull request !48 from NichenFly/dev
2023-12-07 08:16:32 +00:00
= b883cac767 更新sql以及配置文件还原 2023-12-07 15:33:33 +08:00
aoshiguchen 8fd9a24d7c 更新配置文件. 2023-12-07 13:53:58 +08:00
aoshiguchen 40cc5c0c7c 更新配置文件. 2023-12-07 13:45:02 +08:00
傲世孤尘 d23f907d03 !47 添加了安全组功能
Merge pull request !47 from NichenFly/dev
2023-12-07 05:42:13 +00:00
傲世孤尘 438714e9ee !46 添加了安全组功能
Merge pull request !46 from NichenFly/dev
2023-12-07 04:47:41 +00:00
= 7a173757f0 端口映射安全规则界面修改后即时生效 2023-12-07 11:03:54 +08:00
= 9f2198e32e 端口映射的安全组解绑设置值为0 2023-12-07 09:21:17 +08:00
az 6cbf2ecac4 更新vue版本,添加端口映射配置 2023-12-06 22:18:09 +08:00
= 781b14861c 添加安全组页面和安全规则页面 2023-12-06 17:22:14 +08:00
aoshiguchen 82d78d2e4b 更新配置文件 2023-12-06 15:04:34 +08:00
aoshiguchen d40028365b 更新原生部署文档 2023-12-06 14:47:37 +08:00
= 90623b9146 安全组返回数据修改 2023-12-06 11:06:10 +08:00
= aab26f5b9d 安全组相关接口 2023-12-06 09:58:07 +08:00
az 169551445c 添加安全组接口 2023-12-05 21:33:21 +08:00
= 00308c0f5c 端口映射添加安全组id字段 2023-12-05 17:00:30 +08:00
= 306f2ed4d3 规则判断 2023-12-05 16:16:06 +08:00
aoshiguchen bbdd1860cf 更新官网赞赏记录 2023-12-05 10:27:56 +08:00
= a9fc8f4cb4 安全规则判断 2023-12-05 09:14:57 +08:00
az 836252f85b 安全组和安全规则定义 2023-12-04 21:04:14 +08:00
aoshiguchen ce053be992 更新VLog 2023-12-04 09:45:17 +08:00
aoshiguchen 323e7c6277 更新官网首页赞助 2023-11-26 16:56:33 +08:00
aoshiguchen f4797d22ba 更新赞助列表 2023-11-26 16:46:32 +08:00
aoshiguchen 7c9aa1ebdc 更新SQL脚本 2023-11-26 16:33:08 +08:00
aoshiguchen 9564e51c91 每日流量报表job逻辑优化 2023-11-26 16:05:28 +08:00
72 changed files with 2604 additions and 976 deletions
+4
View File
@@ -72,3 +72,7 @@
- jdk版本升级为17
- 支持原生编译改造
- 默认支持的数据库由sqlite改为h2
## 2.0.1
- jdk版本升级为21
- native部署,后台新增提交到后端,数据库乱码问题
@@ -1,88 +0,0 @@
# 客户端与服务器端采用不加密、SM2+AES加密、SSL加密方式进行的性能测试比较
* 本测试不作为性能测试参考,仅作为三种数据加密方式的性能比较使用
* 本测试使用的操作系统为windows10,
* 本测试使用的测试环境配置:内存:16G,CPU:i716核
## 1、测试程序准备情况
* 将程序分别打包为`server``client``jar`包,在本地运行一个`server`
* 拷贝三个客户端配置文件,配置文件名称为`app.yml``app-sm2-aes.yml``app-ssl.yml`,并修改相应配置,适配不加密、SM2+AES加密和SSL加密
## 2、测试思路和实现
1)准备1KB、10KB、20KB、50KB、100KB、1MB、2MB、5MB、10MB、20MB、100MB、500MB的文件
2)使用Nodejs实现的anywhere工具,在本地运行简单http服务
3)在server端生成3个licenseKey,分别对应不加密、SM2+AES加密和SSL加密通道,端口分别为9101,9102和9103,并同时映射到anywhere的8000端口
4)使用Hutool里的HttpUtil工具包,对每个文件进行下载,记录下载使用时间,重复执行10次
## 3、测试结果
序号| 加密方式 | 文件大小 |响应时间(ms)
---|---|---|---
1| 不加密 | 1KB |4
2| SM2+AES | 1KB |21
3| SSL | 1KB |67
4| 不加密 | 10KB |3
5| SM2+AES | 10KB |7
6| SSL | 10KB |4
7| 不加密 | 20KB |4
8| SM2+AES | 20KB |6
9| SSL | 20KB |5
10| 不加密 | 50KB |4
11| SM2+AES | 50KB |7
12| SSL | 50KB |6
13| 不加密 | 100KB |6
14| SM2+AES | 100KB |8
15| SSL | 100KB |6
16| 不加密 | 1MB |19
17| SM2+AES | 1MB |30
18| SSL | 1MB |19
19| 不加密 | 2MB |21
20| SM2+AES | 2MB |40
21| SSL | 2MB |24
22| 不加密 | 5MB |31
23| SM2+AES | 5MB |51
24| SSL | 5MB |36
25| 不加密 | 10MB |44
26| SM2+AES | 10MB |85
27| SSL | 10MB |47
28| 不加密 | 20MB |63
29| SM2+AES | 20MB |139
30| SSL | 20MB |92
31| 不加密 | 100MB |323
32| SM2+AES | 100MB |590
33| SSL | 100MB |322
34| 不加密 | 500MB |1414
35| SM2+AES | 500MB |2797
36| SSL | 500MB |1561
## 4、测试结论
从测试结果可以看出,SSL加密的方式在大部分情况下比SM2+AES的加密方式效率高。
## 5、测试使用的代码
```java
public static void main(String[] args) {
int serialNumber = 1;
int[] ports = new int[]{9101, 9102, 9103};
Map<Integer, String> portMap = new HashMap<>();
portMap.put(9101, "不加密");
portMap.put(9102, "SM2+AES");
portMap.put(9103, "SSL");
HttpUtil.downloadBytes("http://127.0.0.1:9101/1KB"); // 使用不加密通道做一下测试,避免初始化时耗时过高
String[] fileNames = "1KB,10KB,20KB,50KB,100KB,1MB,2MB,5MB,10MB,20MB,100MB,500MB".split(",");
for (String fileName : fileNames) {
for (int port : ports) {
String url = String.format("http://127.0.0.1:%s/%s", port, fileName);
long startTime = System.currentTimeMillis();
HttpUtil.downloadBytes(url);
long endTime = System.currentTimeMillis();
long resTimeMs = endTime - startTime;
String record = String.format("%s|%s|%s|%s", serialNumber++, portMap.get(port), fileName, resTimeMs);
System.out.println(record);
}
}
}
```
+1 -1
View File
@@ -1,5 +1,5 @@
module.exports = {
NODE_ENV: '"production"',
ENV_CONFIG: '"prod"',
BASE_API: '""'
BASE_API: '"https://api-prod"'
}
+1 -1
View File
@@ -23,7 +23,7 @@
"codemirror": "5.32.0",
"dropzone": "5.2.0",
"echarts": "3.8.5",
"element-ui": "2.0.8",
"element-ui": "^2.15.14",
"file-saver": "1.3.3",
"font-awesome": "4.7.0",
"js-cookie": "2.2.0",
@@ -44,3 +44,22 @@ export function updateEnableStatus(id, enable) {
}
})
}
export function portMappingBindSecurityGroup(id, securityGroupId) {
return request({
url: '/port-mapping/bind/security-group',
method: 'post',
data: {
id: id,
securityGroupId: securityGroupId
}
})
}
export function portMappingUnbindSecurityGroup(id) {
return request({
url: `/port-mapping/unbind/security-group?id=${id}`,
method: 'post'
})
}
@@ -0,0 +1,97 @@
import request from '@/utils/request'
const baseUri = '/security';
export function fetchGroupList() {
return request({
url: `${baseUri}/group/s`,
method: 'get'
})
}
export function fetchGroupOne(groupId) {
return request({
url: `${baseUri}/group/getOne?groupId=${groupId}`,
method: 'get'
})
}
export function createGroup(data) {
return request({
url: `${baseUri}/group/create`,
method: 'post',
data
})
}
export function updateGroup(data) {
return request({
url: `${baseUri}/group/update`,
method: 'post',
data
})
}
export function deleteGroup(groupId) {
return request({
url: `${baseUri}/group/delete?groupId=${groupId}`,
method: 'post'
})
}
export function enableGroup(groupId) {
return request({
url: `${baseUri}/group/enable?groupId=${groupId}`,
method: 'post'
})
}
export function disableGroup(groupId) {
return request({
url: `${baseUri}/group/disable?groupId=${groupId}`,
method: 'post'
})
}
export function fetchRuleList(groupId) {
return request({
url: `${baseUri}/rule/s?groupId=${groupId}`,
method: 'get'
})
}
export function createRule(data) {
return request({
url: `${baseUri}/rule/create`,
method: 'post',
data
})
}
export function updateRule(data) {
return request({
url: `${baseUri}/rule/update`,
method: 'post',
data
})
}
export function deleteRule(ruleId) {
return request({
url: `${baseUri}/rule/delete?ruleId=${ruleId}`,
method: 'post'
})
}
export function enableRule(ruleId) {
return request({
url: `${baseUri}/rule/enable?ruleId=${ruleId}`,
method: 'post'
})
}
export function disableRule(ruleId) {
return request({
url: `${baseUri}/rule/disable?ruleId=${ruleId}`,
method: 'post'
})
}
@@ -0,0 +1,83 @@
<template>
<el-popover
placement="top"
:width="width"
v-model="visible">
<p class="popper-p-css"><i class="el-icon-warning" style="color: #e6a23c"/>{{title}}</p>
<div style="text-align: center; margin: 0">
<el-button size="mini" @click="handleCancelClick">{{cancelText}}</el-button>
<el-button type="primary" size="mini" @click="handleCommitClick">{{okText}}</el-button>
</div>
<el-link slot="reference" :underline="false" :type="type" :size="size" :icon="icon" :disabled="disabled" style="text-align: left; font-size: 12px">{{buttonText}}</el-link>
</el-popover>
</template>
<script>
export default {
name: 'deleteLink',
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: '取消'
}
},
data() {
return {
visible: false
}
},
methods: {
handleCancelClick() {
this.visible = false
this.$emit('handleCancelClick')
},
handleCommitClick() {
this.visible = false
this.$emit('handleCommitClick')
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.popper-p-css{
margin-top: 0px !important;
margin-bottom: 5px !important;
.el-icon-warning{
margin-right: 5px;
}
}
</style>
+16 -1
View File
@@ -48,6 +48,8 @@ export default {
user: '用户管理',
system: '系统管理',
portPool: '端口池管理',
securityGroup: '安全组管理',
securityRule: '安全规则管理',
portGroup: '端口分组管理',
protocal: '协议管理',
proxy: '代理配置',
@@ -128,6 +130,7 @@ export default {
confirm: '确 定',
userId: '用户ID',
userName: '用户名',
name: '名称',
group: '分组',
groupName: '分组名称',
groupPossessor: '分组所属',
@@ -169,7 +172,19 @@ export default {
totalFlow: '总流量',
protocalName: '协议',
supportStatus: '支持状态',
domainName: '域名'
domainName: '域名',
securityGroup: '安全组',
defaultPassType: '默认放行类型',
ruleName: '规则名称',
rule: '规则内容',
passType: '放行类型',
priority: '优先级',
ruleConfig: '配置规则',
portMappingBindSecurityGroup: '绑定安全组',
securityGroupBindPortMapping: '端口映射绑定',
bind: '绑定',
unbind: '解绑',
bindOtherSecurityGroup: '已绑定其他安全组'
},
button: {
lookOver: '查看'
+2
View File
@@ -77,6 +77,8 @@ export const asyncRouterMap = [
{ path: 'user', component: _import('system/user'), name: 'user', meta: { title: 'user' }},
{ path: 'portGroup', component: _import('system/portGroup'), name: 'portGroup', meta: { title: 'portGroup' }},
{ path: 'portPool', component: _import('system/portPool'), name: 'portPool', meta: { title: 'portPool' }},
{ path: 'securityGroup', component: _import('system/securityGroup'), name: 'securityGroup', meta: { title: 'securityGroup' }},
{ path: 'securityRule', component: _import('system/securityRule'), name: 'securityRule', meta: { title: 'securityRule' }, hidden: true},
{ path: 'protocal', component: _import('system/protocal'), name: 'protocal', meta: { title: 'protocal' }},
{ path: 'jobManager', component: _import('system/jobManager'), name: 'jobManager', meta: { title: 'jobManager' }}
]
@@ -176,6 +176,12 @@
<template slot="append">毫秒</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('table.securityGroup')" prop="securityGroup">
<el-select style="width: 280px;" class="filter-item" v-model="temp.securityGroupId" clearable >
<el-option v-for="item in securityGroupList" :key="item.id" :label="item.name" :value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('描述')" prop="description">
<el-input v-model="temp.description"></el-input>
</el-form-item>
@@ -204,6 +210,7 @@
<script>
import { fetchList, createUserPortMapping, updateUserPortMapping, updateEnableStatus, deletePortMapping } from '@/api/portMapping'
import { fetchGroupList } from '@/api/securityGroup'
import { availablePortList, portAvailable } from '@/api/portPool'
import { licenseList, licenseAuthList } from '@/api/license'
import { protocalList } from '@/api/protocal'
@@ -325,6 +332,7 @@ export default {
licenseId:null,
},
more: true,
securityGroupList: []
}
},
filters: {
@@ -359,6 +367,7 @@ export default {
this.getLicenseList()
this.getLicenseAuthList()
this.getProtocalList()
this.fetchSecurityGroupList()
},
methods: {
getList() {
@@ -380,6 +389,13 @@ export default {
this.getList()
})
},
fetchSecurityGroupList () {
fetchGroupList().then(res => {
if(res.data.code == 0) {
this.securityGroupList = res.data.data
}
})
},
getDomainNameBindInfo() {
domainNameBindInfo().then(response => {
this.domainName = response.data.data
@@ -484,6 +500,9 @@ export default {
},
handleUpdate(row) {
this.temp = Object.assign({}, row) // copy obj
if (row.securityGroupId === 0) {
this.temp.securityGroupId = null
}
this.temp.timestamp = new Date(this.temp.timestamp)
this.dialogStatus = 'update'
this.dialogFormVisible = true
@@ -0,0 +1,420 @@
<template>
<div class="app-container calendar-list-container">
<div class="filter-container">
<el-button class="filter-item" style="margin-left: 10px;" @click="handleCreate" type="primary" icon="el-icon-edit">{{$t('table.add')}}</el-button>
</div>
<el-table :key='tableKey' :data="list" v-loading="listLoading" element-loading-text="给我一点时间" border fit highlight-current-row
style="width: 100%">
<!-- <el-table-column align="center" width="40" type="selection" /> -->
<el-table-column align="center" :label="$t('table.id')" width="60">
<template slot-scope="scope">
<span>{{scope.row.id}}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.name')">
<template slot-scope="scope">
<span>{{scope.row.name}}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.desc')">
<template slot-scope="scope">
<span>{{scope.row.description}}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.defaultPassType')">
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.defaultPassType == 'allow'">允许</el-tag>
<el-tag type="info" v-if="scope.row.defaultPassType == 'deny'">拒绝</el-tag>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.createTime')">
<template slot-scope="scope">
<span>{{scope.row.createTime}}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.updateTime')">
<template slot-scope="scope">
<span>{{scope.row.updateTime}}</span>
</template>
</el-table-column>
<el-table-column class-name="status-col" :label="$t('table.enableStatus')">
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.enable == '启用'">{{scope.row.enable}}</el-tag>
<el-tag type="danger" v-if="scope.row.enable == '禁用'">{{scope.row.enable}}</el-tag>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.actions')" class-name="small-padding fixed-width" style="display:flex;justify-content:center">
<template slot-scope="scope">
<div >
<el-link :underline="false" type="primary" size="mini" @click="handleGoRulePage(scope.row)" style="font-size: 12px">{{$t('table.ruleConfig')}}</el-link>
<el-link :underline="false" type="primary" size="mini" @click="handleUpdate(scope.row)" style="font-size: 12px">{{$t('table.edit')}}</el-link>
<el-link :underline="false" v-if="scope.row.enable =='启用'" size="mini" type="warning" @click="handleDisableStatus(scope.row)" style="font-size: 12px">{{$t('table.disable')}}</el-link>
<el-link :underline="false" v-if="scope.row.enable =='禁用'" size="mini" type="success" @click="handleEnableStatus(scope.row)" style="font-size: 12px">{{$t('table.enable')}}</el-link>
</div>
<el-dropdown>
<span class="el-dropdown-link" style="font-size: 12px">
更多操作<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>
<LinkPopover @handleCommitClick="handleDelete(scope.row)" style="width: 100%"/>
</el-dropdown-item>
<el-dropdown-item>
<el-link :underline="false" type="primary" size="mini" @click="handlePortMapping(scope.row)" style="font-size: 12px">{{$t('table.securityGroupBindPortMapping')}}</el-link>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible">
<el-form :rules="rules" ref="dataForm" :model="temp" label-position="right" label-width="120px" style='width: 500px; margin-left:10px;'>
<el-form-item :label="$t('table.name')" prop="name">
<el-input :placeholder="$t('table.name')" v-model="temp.name"></el-input>
</el-form-item>
<el-form-item :label="$t('table.desc')" prop="desc">
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4}" :placeholder="$t('table.desc')" v-model="temp.description"></el-input>
</el-form-item>
<el-form-item :label="$t('table.defaultPassType')" prop="defaultPassType">
<el-tooltip class="item" effect="dark" content="当IP地址不能匹配任何规则时,默认执行的放行类型" placement="bottom">
<el-select style="width: 380px" class="filter-item" v-model="temp.defaultPassType" :disabled="dialogStatus === 'update'">
<el-option v-for="item in passTypeList" :key="item.key" :label="item.key" :value="item.value">
</el-option>
</el-select>
</el-tooltip>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">{{$t('table.cancel')}}</el-button>
<el-button v-if="dialogStatus=='create'" type="primary" @click="createData">{{$t('table.confirm')}}</el-button>
<el-button v-else type="primary" @click="updateData">{{$t('table.confirm')}}</el-button>
</div>
</el-dialog>
<el-dialog title="Reading statistics" :visible.sync="dialogPvVisible">
<el-table :data="pvData" border fit highlight-current-row style="width: 100%">
<el-table-column prop="key" label="Channel"> </el-table-column>
<el-table-column prop="pv" label="Pv"> </el-table-column>
</el-table>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="dialogPvVisible = false">{{$t('table.confirm')}}</el-button>
</span>
</el-dialog>
<el-dialog :title="$t('table.securityGroupBindPortMapping')+'---'+forBindProtMappingSecurityGroup.name+'安全组'" :visible.sync="dialogBindPortMappingVisible" width="90%">
<el-table :key='tableKey' :data="portMappingList" v-loading="listLoading" element-loading-text="给我一点时间" border fit align="center" width="100%"
highlight-current-row style="width: 100%">
<el-table-column align="center" :label="$t('table.id')" width="50">
<template slot-scope="scope">
<span>{{ scope.row.id }}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.licenseName')" width="130">
<template slot-scope="scope">
<span>{{ scope.row.licenseName }}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.protocalName')" width="100">
<template slot-scope="scope">
<span>{{ scope.row.protocal }}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.domainName')" width="200">
<template slot-scope="scope">
<span>{{ scope.row.domain }}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.serverPort')" width="80">
<template slot-scope="scope">
<span>{{ scope.row.serverPort }}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.proxyClient')" width="120">
<template slot-scope="scope">
<span>{{ scope.row.clientIp }}:{{ scope.row.clientPort }}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.desc')" width="120">
<template slot-scope="scope">
<span>{{ scope.row.description }}</span>
</template>
</el-table-column>
<el-table-column class-name="status-col" :label="$t('table.enableStatus')" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.enable | statusFilter">{{ scope.row.enable | statusName }}</el-tag>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.actions')" width="120" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button type="primary" size="mini" v-if="!scope.row.securityGroupId" @click="handleBind(scope.row)">{{$t('table.bind')}}</el-button>
<el-button type="danger" size="mini" v-if="scope.row.securityGroupId && scope.row.securityGroupId == forBindProtMappingSecurityGroup.id" @click="handleUnbind(scope.row)">{{$t('table.unbind')}}</el-button>
<span v-if="scope.row.securityGroupId && scope.row.securityGroupId != forBindProtMappingSecurityGroup.id" style="font-size: 12px;">{{$t('table.bindOtherSecurityGroup')}}</span>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination background @size-change="handlePortMappingSizeChange" @current-change="handlePortMappingCurrentChange"
:current-pageInfo.sync="portMappingListQuery.current" :pageInfo-sizes="[10, 20, 30, 50]" :pageInfo-size="portMappingListQuery.size"
layout="total, sizes, prev, pager, next, jumper" :total="portMappingTotal">
</el-pagination>
</div>
</el-dialog>
</div>
</template>
<script>
import {fetchGroupList, createGroup, updateGroup, deleteGroup, enableGroup, disableGroup} from '@/api/securityGroup'
import { fetchList as fetchPortMappingList, portMappingBindSecurityGroup, portMappingUnbindSecurityGroup} from '@/api/portMapping'
import waves from '@/directive/waves' // 水波纹指令
import { parseTime } from '@/utils'
import LinkPopover from '../../components/Link/linkPopover'
export default {
name: 'complexTable',
directives: {
waves
},
components: {
LinkPopover
},
data() {
return {
tableKey: 0,
list: [],
listLoading: true,
temp: {
id: undefined,
name: '',
description: '',
defaultPassType: undefined
},
dialogFormVisible: false,
dialogStatus: '',
textMap: {
update: '编辑',
create: '新建'
},
passTypeList: [{key: '允许', value: 1}, {key: '拒绝', value: 0}],
dialogPvVisible: false,
pvData: [],
rules: {
name: [{ required: true, message: '安全组名称必填', trigger: 'blur' }],
defaultPassType: [{ required: true, message: '默认放行类型必选', trigger: 'blur' }]
},
downloadLoading: false,
checkBoxData:[], //表单勾选的行
dialogBindPortMappingVisible: false,
forBindProtMappingSecurityGroup: {},
portMappingList: [],
portMappingTotal: null,
portMappingListLoading: false,
portMappingListQuery: {
current: 1,
size: 10,
importance: undefined,
title: undefined,
type: undefined,
userId: undefined,
license: undefined,
port: undefined,
isOnline: undefined,
enable: undefined,
description: undefined
},
}
},
filters: {
statusName(status) {
const statusMap = {
1: '启用',
2: '禁用'
}
return statusMap[status]
},
statusFilter(status) {
const statusMap = {
1: 'success',
2: 'danger'
}
return statusMap[status]
},
typeFilter(type) {
return calendarTypeKeyValue[type]
}
},
created() {
// eslint-disable-next-line no-sequences
this.getList()
this.getPortMappingList()
},
methods: {
getList() {
this.listLoading = true
fetchGroupList().then(response => {
this.list = response.data.data
this.listLoading = false
})
},
getPortMappingList() {
this.portMappingListLoading = true
fetchPortMappingList(this.portMappingListQuery).then(response => {
this.portMappingList = response.data.data.records
this.portMappingTotal = response.data.data.total
this.portMappingListQuery.current = response.data.data.current
this.portMappingListLoading = false
})
},
handleEnableStatus(row) {
enableGroup(row.id).then(response => {
if (response.data.code === 0) {
this.$message({
message: '操作成功',
type: 'success'
})
this.getList()
}
})
},
handleDisableStatus(row) {
disableGroup(row.id).then(response => {
if (response.data.code === 0) {
this.$message({
message: '操作成功',
type: 'success'
})
this.getList()
}
})
},
resetTemp() {
this.temp = {
id: undefined,
name: '',
description: '',
defaultPassType: undefined
}
},
handleCreate() {
this.resetTemp()
this.dialogStatus = 'create'
this.dialogFormVisible = true
this.$nextTick(() => {
this.$refs['dataForm'].clearValidate()
})
},
createData() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
createGroup(this.temp).then(response => {
if (response.data.code === 0) {
this.dialogFormVisible = false
this.$notify({
title: '成功',
message: '创建成功',
type: 'success',
duration: 2000
})
this.getList()
}
})
}
})
},
handleUpdate(row) {
this.temp = Object.assign({}, row) // copy obj
this.temp.defaultPassType = row.defaultPassType == 'allow' ? 1 : 0
this.temp.timestamp = new Date(this.temp.timestamp)
this.dialogStatus = 'update'
this.dialogFormVisible = true
this.$nextTick(() => {
this.$refs['dataForm'].clearValidate()
})
},
updateData() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
const tempData = Object.assign({}, this.temp)
updateGroup(tempData).then(response => {
if (response.data.code === 0) {
this.$notify({
title: '成功',
message: '更新成功',
type: 'success',
duration: 2000
})
this.dialogFormVisible = false
this.getList()
}
})
}
})
},
handleDelete(row) {
deleteGroup(row.id).then(response => {
if (response.data.code === 0) {
this.$notify({
title: '成功',
message: '删除成功',
type: 'success',
duration: 2000
})
this.getList()
}
})
},
handleGoRulePage (row) {
this.$router.push(`/system/securityRule?groupId=${row.id}`)
},
handlePortMapping(row) {
this.dialogBindPortMappingVisible = true
this.forBindProtMappingSecurityGroup = row
},
handlePortMappingSizeChange(val) {
this.portMappingListQuery.size = val
this.getPortMappingList()
},
handlePortMappingCurrentChange(val) {
this.portMappingListQuery.current = val
this.getPortMappingList()
},
handleBind(portMapping) {
portMappingBindSecurityGroup(portMapping.id, this.forBindProtMappingSecurityGroup.id).then(response => {
if (response.data.code === 0) {
this.$notify({
title: '成功',
message: '绑定成功',
type: 'success',
duration: 2000
})
this.getPortMappingList()
}
})
},
handleUnbind(portMapping) {
portMappingUnbindSecurityGroup(portMapping.id).then(response => {
if (response.data.code === 0) {
this.$notify({
title: '成功',
message: '解绑成功',
type: 'success',
duration: 2000
})
this.getPortMappingList()
}
})
}
}
}
</script>
<style>
.filter-container {
text-align: right;
}
</style>
@@ -0,0 +1,359 @@
<template>
<div class="app-container calendar-list-container">
<div>
<div style="text-align: center;line-height:48px;font-size:24px">{{group.name}}安全组</div>
<div style="text-align: center;font-size:14px; color: #606266">{{group.description}}</div>
</div>
<div class="filter-container" align="right">
<el-button class="filter-item" @click="handleCreate" type="primary" icon="el-icon-edit">{{$t('table.add')}}</el-button>
</div>
<el-table :key='tableKey' :data="list" v-loading="listLoading" element-loading-text="给我一点时间" border fit highlight-current-row
style="width: 100%">
<!-- <el-table-column align="center" width="40" type="selection" /> -->
<el-table-column align="center" :label="$t('table.id')" width="60">
<template slot-scope="scope">
<span>{{scope.row.id}}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.ruleName')">
<template slot-scope="scope">
<span>{{scope.row.name}}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.desc')">
<template slot-scope="scope">
<span>{{scope.row.description}}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.rule')">
<template slot-scope="scope">
<span>{{scope.row.rule}}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.passType')">
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.passType == 'allow'" effect="dark">允许</el-tag>
<el-tag type="info" v-if="scope.row.passType == 'deny'" effect="dark">拒绝</el-tag>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.priority')">
<template slot-scope="scope">
<span>{{scope.row.priority}}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.createTime')">
<template slot-scope="scope">
<span>{{scope.row.createTime}}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.updateTime')">
<template slot-scope="scope">
<span>{{scope.row.updateTime}}</span>
</template>
</el-table-column>
<el-table-column class-name="status-col" :label="$t('table.enableStatus')" width="150">
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.enable == '启用'">{{scope.row.enable}}</el-tag>
<el-tag type="warning" v-if="scope.row.enable == '禁用'">{{scope.row.enable}}</el-tag>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.actions')" width="250" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-link type="primary" :underline="false" size="mini" @click="handleUpdate(scope.row)" style="font-size:12px">{{$t('table.edit')}}</el-link>
<el-link :underline="false" v-if="scope.row.enable =='启用'" size="mini" type="warning" @click="handleDisableStatus(scope.row)" style="font-size:12px">{{$t('table.disable')}}</el-link>
<el-link :underline="false" v-if="scope.row.enable =='禁用'" size="mini" type="success" @click="handleEnableStatus(scope.row)" style="font-size:12px">{{$t('table.enable')}}</el-link>
<LinkPopover @handleCommitClick="handleDelete(scope.row)"/>
</template>
</el-table-column>
</el-table>
<el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible" top="4vh">
<el-form :rules="rules" ref="dataForm" :model="temp" label-position="right" label-width="100px" style='margin-left:50px;margin-right: 150px'>
<el-form-item :label="$t('table.name')" prop="name">
<el-input :placeholder="$t('table.name')" v-model="temp.name"></el-input>
</el-form-item>
<el-form-item :label="$t('table.desc')" prop="desc">
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4}" :placeholder="$t('table.desc')" v-model="temp.description"></el-input>
</el-form-item>
<el-form-item :label="$t('table.rule')" prop="rule">
<el-input type="textarea" :autosize="{ minRows: 4, maxRows: 10}" :placeholder="$t('table.rule')" v-model="temp.rule"></el-input>
</el-form-item>
<el-form-item>
<div style="line-height: 28px; color: cornflowerblue">
<div>规则描述:</div>
<div>单个ip192.168.1.1, AA22:BB11:1122:CDEF:1234:AA99:7654:7410, ipv6只支持单个ip判断</div>
<div>范围类型192.168.1.0-192.168.1.255</div>
<div>掩码类型192.168.1.0/24</div>
<div>泛型0.0.0.0/ALL</div>
<div>每个类型中间以英文逗号分隔,形如 192.168.1.1,192.168.3.0/24 是正确的 </div>
</div>
</el-form-item>
<el-form-item :label="$t('table.passType')" prop="passType">
<el-tooltip class="item" effect="dark" :content="temp.passTypeTooltip" placement="right">
<!-- <el-button>右边</el-button> -->
<el-select class="filter-item" v-model="temp.passType" disabled>
<el-option v-for="item in passTypeList" :key="item.key" :label="item.key" :value="item.value">
</el-option>
</el-select>
</el-tooltip>
</el-form-item>
<!-- <el-form-item :label="$t('table.priority')" prop="priority">
<el-input-number v-model="temp.priority" :min="1" :max="1000" :placeholder="$t('table.priority')"></el-input-number>
</el-form-item> -->
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">{{$t('table.cancel')}}</el-button>
<el-button v-if="dialogStatus=='create'" type="primary" @click="createData">{{$t('table.confirm')}}</el-button>
<el-button v-else type="primary" @click="updateData">{{$t('table.confirm')}}</el-button>
</div>
</el-dialog>
<el-dialog title="Reading statistics" :visible.sync="dialogPvVisible">
<el-table :data="pvData" border fit highlight-current-row style="width: 100%">
<el-table-column prop="key" label="Channel"> </el-table-column>
<el-table-column prop="pv" label="Pv"> </el-table-column>
</el-table>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="dialogPvVisible = false">{{$t('table.confirm')}}</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import {fetchGroupOne, fetchRuleList, createRule, updateRule, deleteRule, enableRule, disableRule} from '@/api/securityGroup'
import waves from '@/directive/waves' // 水波纹指令
import { parseTime } from '@/utils'
import LinkPopover from '../../components/Link/linkPopover'
export default {
name: 'complexTable',
directives: {
waves
},
components: {
LinkPopover
},
data() {
return {
groupId: 1,
group: {},
tableKey: 0,
list: [],
listLoading: true,
temp: {
id: undefined,
groupId: undefined,
name: '',
description: '',
rule: '',
passType: undefined,
passTypeTooltip: '',
priority: 1
},
dialogFormVisible: false,
dialogStatus: '',
textMap: {
update: '编辑',
create: '新建'
},
passTypeList: [{key: '允许', value: 1}, {key: '拒绝', value: 0}],
dialogPvVisible: false,
pvData: [],
rules: {
name: [{ required: true, message: '安全组名称必填', trigger: 'blur' }],
rule: [{ required: true, message: '规则内容必填', trigger: 'blur' }],
// passType: [{ required: true, message: '放行类型必选', trigger: 'blur' }],
// priority: [{ required: true, message: '优先级必填', trigger: 'blur' }]
},
downloadLoading: false,
checkBoxData:[], //表单勾选的行
}
},
filters: {
statusName(status) {
const statusMap = {
1: '启用',
2: '禁用'
}
return statusMap[status]
},
statusFilter(status) {
const statusMap = {
1: 'success',
2: 'danger'
}
return statusMap[status]
},
typeFilter(type) {
return calendarTypeKeyValue[type]
}
},
created() {
// eslint-disable-next-line no-sequences
const queryParam = this.$route.query
if (queryParam && typeof queryParam === 'object' && queryParam.groupId) {
this.groupId = queryParam.groupId
localStorage.setItem('groupId', this.groupId)
this.getGroupOne()
this.getList()
return
}
const groupId = localStorage.getItem('groupId')
if (groupId) {
this.groupId = parseInt(groupId)
this.getGroupOne()
this.getList()
return
}
this.$notify({
title: '错误',
message: '没有获取到安全组信息',
type: 'error',
duration: 3000
})
this.$router.push(`/system/securityGroup`)
},
methods: {
getGroupOne () {
fetchGroupOne(this.groupId).then(response => {
this.group = response.data.data
})
},
getList() {
if (!this.groupId) {
this.$notify({
title: '错误',
message: '没有获取到安全组信息',
type: 'error',
duration: 3000
})
return
}
this.listLoading = true
fetchRuleList(this.groupId).then(response => {
this.list = response.data.data
this.listLoading = false
})
},
handleEnableStatus(row) {
enableRule(row.id).then(response => {
if (response.data.code === 0) {
this.$message({
message: '操作成功',
type: 'success'
})
this.getList()
}
})
},
handleDisableStatus(row) {
disableRule(row.id).then(response => {
if (response.data.code === 0) {
this.$message({
message: '操作成功',
type: 'success'
})
this.getList()
}
})
},
resetTemp() {
this.temp = {
id: undefined,
groupId: this.groupId,
name: '',
description: '',
rule: '',
passType: this.group.defaultPassType == 'allow' ? 0 : 1,
passTypeTooltip: `安全组已设置默认${(this.group.defaultPassType == 'allow' ? '允许' : '拒绝')}`,
priority: 1
}
},
handleCreate() {
this.resetTemp()
this.dialogStatus = 'create'
this.dialogFormVisible = true
this.$nextTick(() => {
this.$refs['dataForm'].clearValidate()
})
},
createData() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.temp.groupId = this.groupId
createRule(this.temp).then(response => {
if (response.data.code === 0) {
this.dialogFormVisible = false
this.$notify({
title: '成功',
message: '创建成功',
type: 'success',
duration: 2000
})
this.getList()
}
})
}
})
},
handleUpdate(row) {
this.temp = Object.assign({}, row) // copy obj
this.temp.passType = row.passType == 'allow' ? 1 : 0
this.temp.passTypeTooltip = `安全组已设置默认${(this.group.defaultPassType == 'allow' ? '允许' : '拒绝')}`,
this.temp.timestamp = new Date(this.temp.timestamp)
this.dialogStatus = 'update'
this.dialogFormVisible = true
this.$nextTick(() => {
this.$refs['dataForm'].clearValidate()
})
},
updateData() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
const tempData = Object.assign({}, this.temp)
tempData.groupId = this.groupId
updateRule(tempData).then(response => {
if (response.data.code === 0) {
this.$notify({
title: '成功',
message: '更新成功',
type: 'success',
duration: 2000
})
this.dialogFormVisible = false
this.getList()
}
})
}
})
},
handleDelete(row) {
deleteRule(row.id).then(response => {
if (response.data.code === 0) {
this.$notify({
title: '成功',
message: '删除成功',
type: 'success',
duration: 2000
})
this.getList()
}
})
}
}
}
</script>
@@ -24,7 +24,6 @@ public class ProxyClient {
setAlias("neutrino.proxy.tunnel.server-ip", "serverIp");
setAlias("neutrino.proxy.tunnel.server-port", "serverPort");
setAlias("neutrino.proxy.tunnel.sm2-encrypt-enable", "sm2EncryptEnable");
setAlias("neutrino.proxy.tunnel.ssl-enable", "sslEnable");
setAlias("neutrino.proxy.tunnel.jks-path", "jksPath");
setAlias("neutrino.proxy.tunnel.key-store-password", "keyStorePassword");
@@ -37,7 +37,6 @@ public class ProxyConfig {
private String jksPath;
private String serverIp;
private Integer serverPort;
private Boolean sm2EncryptEnable;
private Boolean sslEnable;
private Integer obtainLicenseInterval;
private String licenseKey;
@@ -1,7 +1,6 @@
package org.dromara.neutrinoproxy.client.handler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.Attribute;
import lombok.extern.slf4j.Slf4j;
import org.dromara.neutrinoproxy.client.config.ProxyConfig;
import org.dromara.neutrinoproxy.core.Constants;
@@ -9,7 +8,6 @@ import org.dromara.neutrinoproxy.core.ExceptionEnum;
import org.dromara.neutrinoproxy.core.ProxyMessage;
import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
import org.dromara.neutrinoproxy.core.dispatcher.Match;
import org.dromara.neutrinoproxy.core.util.EncryptUtil;
import org.noear.snack.ONode;
import org.noear.solon.Solon;
import org.noear.solon.annotation.Component;
@@ -31,7 +29,7 @@ public class ProxyMessageAuthHandler implements ProxyMessageHandler {
String info = proxyMessage.getInfo();
ONode load = ONode.load(info);
Integer code = load.get("code").getInt();
log.info("Auth result: {}", load.get("msg").getString());
log.info("Auth result:{}", info);
if (ExceptionEnum.AUTH_FAILED.getCode().equals(code)) {
// 客户端认证失败,直接停止服务
log.info("client auth failed , client stop.");
@@ -44,26 +42,5 @@ public class ProxyMessageAuthHandler implements ProxyMessageHandler {
){
context.channel().close();
}
// 是否进行通道加密
if (!proxyConfig.getTunnel().getSm2EncryptEnable()) {
return;
}
// 默认设置为非安全链路,需要服务端确认后,再设置为安全链路
Attribute<Boolean> booleanAttribute = context.attr(Constants.IS_SECURITY);
booleanAttribute.set(false);
// 获取认证成功的后的公钥信息,并生成随机密码,加密发到服务端确认
String publicKey = load.get("publicKey").getString();
byte[] secureKey = EncryptUtil.generateAesKey();
// 存储密码
Attribute<byte[]> secureKeyAttr = context.attr(Constants.SECURE_KEY);
secureKeyAttr.set(secureKey);
// 使用SM2算法对密钥进行加密并发送到服务端
byte[] encryptSecureKey = EncryptUtil.encryptBySm2(publicKey, secureKey);
context.writeAndFlush(ProxyMessage.buildSecureKeyMessage(encryptSecureKey));
context.flush();
}
}
@@ -56,15 +56,12 @@ public class ProxyMessageConnectHandler implements ProxyMessageHandler {
channel.attr(Constants.NEXT_CHANNEL).set(realServerChannel);
realServerChannel.attr(Constants.NEXT_CHANNEL).set(channel);
// 通知服务端进行远程绑定,此绑定信息不加密,该条消息为身份标识
// 远程绑定
channel.writeAndFlush(ProxyMessage.buildConnectMessage(visitorId + "@" + proxyConfig.getTunnel().getLicenseKey()));
realServerChannel.config().setOption(ChannelOption.AUTO_READ, true);
ProxyUtil.addRealServerChannel(visitorId, realServerChannel);
ProxyUtil.setRealServerChannelVisitorId(realServerChannel, visitorId);
// 连接信息发送后,将该通道设置为加密
ProxyUtil.setChannelSecurity(channel);
}
@Override
@@ -1,49 +0,0 @@
package org.dromara.neutrinoproxy.client.handler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.Attribute;
import lombok.extern.slf4j.Slf4j;
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.ProxyMessageHandler;
import org.dromara.neutrinoproxy.core.dispatcher.Match;
import org.dromara.neutrinoproxy.core.util.EncryptUtil;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Inject;
@Slf4j
@Match(type = Constants.ProxyDataTypeName.SECURE_KEY)
@Component
public class ProxyMessageSecureKeyHandler implements ProxyMessageHandler {
@Inject
private ProxyConfig proxyConfig;
@Override
public void handle(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
if (!proxyConfig.getTunnel().getSm2EncryptEnable()) {
return;
}
log.info("收到服务端的加密确认");
Attribute<byte[]> secureKeyAttr = ctx.attr(Constants.SECURE_KEY);
byte[] secureKey = secureKeyAttr.get();
byte[] data = proxyMessage.getData();
byte[] decryptedData = EncryptUtil.decryptByAes(secureKey, data);
String m = new String(decryptedData);
if ("ok".equals(m)) {
// 设置当前cmd通道为安全,之后使用该通道传输的消息均会加密
Attribute<Boolean> booleanAttribute = ctx.attr(Constants.IS_SECURITY);
booleanAttribute.set(true);
// 全局存储密钥
ProxyUtil.setSecureKey(secureKey);
log.info("Encrypted link established successfully");
} else {
ctx.channel().close();
}
}
}
@@ -46,9 +46,6 @@ public class UdpProxyMessageConnectHandler implements ProxyMessageHandler {
.setTargetIp(udpBaseInfo.getTargetIp())
.setTargetPort(udpBaseInfo.getTargetPort())
).setData(proxyConfig.getTunnel().getLicenseKey().getBytes()));
// connect类型的消息不加密,用于标识身份,发送标识消息后,再将通道设置加密标识
ProxyUtil.setChannelSecurity(channel);
}
@Override
@@ -72,8 +72,6 @@ public class ProxyUtil {
private static String clientId;
private static final String CLIENT_ID_FILE = ".NEUTRINO_PROXY_CLIENT_ID";
private static byte[] secureKey = null;
public static void borrowTcpProxyChanel(Bootstrap tcpProxyTunnelBootstrap, final ProxyChannelBorrowListener borrowListener) {
Channel channel = tcpProxyChannelPool.poll();
if (null != channel) {
@@ -91,10 +89,6 @@ public class ProxyUtil {
}
public static void returnTcpProxyChanel(Channel proxyChanel) {
if (proxyChanel != null) {
proxyChanel.attr(Constants.IS_SECURITY).set(null);
proxyChanel.attr(Constants.SECURE_KEY).set(null);
}
if (tcpProxyChannelPool.size() > MAX_POOL_SIZE) {
proxyChanel.close();
} else {
@@ -127,10 +121,6 @@ public class ProxyUtil {
}
public static void returnUdpProxyChanel(Channel proxyChanel) {
if (proxyChanel != null) {
proxyChanel.attr(Constants.IS_SECURITY).set(null);
proxyChanel.attr(Constants.SECURE_KEY).set(null);
}
if (udpProxyChannelPool.size() > MAX_POOL_SIZE) {
proxyChanel.close();
} else {
@@ -233,16 +223,4 @@ public class ProxyUtil {
return null;
}
public static void setSecureKey(byte[] key) {
secureKey = key;
}
public static void setChannelSecurity(Channel channel) {
if (null == secureKey) {
return;
}
channel.attr(Constants.IS_SECURITY).set(true);
channel.attr(Constants.SECURE_KEY).set(secureKey);
}
}
@@ -23,7 +23,7 @@ neutrino:
proxy:
protocol:
max-frame-length: 1048576000
max-frame-length: 2097152
length-field-offset: 0
length-field-length: 4
initial-bytes-to-strip: 0
@@ -34,18 +34,17 @@ neutrino:
tunnel:
# 线程池相关配置,用于技术调优,可忽略
thread-count: 50
sm2-encrypt-enable: ${SM2_ENCRYPT_ENABLE:true}
# 隧道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:9000}
server-port: ${SERVER_PORT:9002}
# 是否启用SSL(注意:该配置必须和server-port对应上)
ssl-enable: ${SSL_ENABLE:false}
ssl-enable: ${SSL_ENABLE:true}
# 客户端连接唯一凭证
license-key: ${LICENSE_KEY:b0a907332b474b25897c4dcb31fc7eb6}
license-key: ${LICENSE_KEY:}
# 客户端唯一身份标识(可忽略,若不设置首次启动会自动生成)
client-id: ${CLIENT_ID:}
# 是否开启隧道传输报文日志(日志级别为debug时开启才有效)
+1 -7
View File
@@ -24,12 +24,6 @@
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
</dependency>
<!--hutool -->
<dependency>
<groupId>cn.hutool</groupId>
@@ -38,7 +32,7 @@
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<artifactId>hutool-cache</artifactId>
<version>${hutool.version}</version>
</dependency>
</dependencies>
@@ -38,12 +38,6 @@ public interface Constants {
AttributeKey<String> VISITOR_ID = AttributeKey.newInstance("visitor_id");
AttributeKey<String> SECURE_PRIVATE_KEY = AttributeKey.newInstance("secure_private_key");
AttributeKey<byte[]> SECURE_KEY = AttributeKey.newInstance("secure_key");
AttributeKey<Boolean> IS_SECURITY = AttributeKey.newInstance("is_security");
AttributeKey<Integer> LICENSE_ID = AttributeKey.newInstance("license_id");
AttributeKey<String> TARGET_IP = AttributeKey.newInstance("targetIp");
@@ -63,7 +57,6 @@ public interface Constants {
interface ProxyDataTypeName {
String HEARTBEAT = "HEARTBEAT";
String SECURE_KEY = "SECURE_KEY";
String AUTH = "AUTH";
String CONNECT = "CONNECT";
String DISCONNECT = "DISCONNECT";
@@ -1,31 +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.core;
/**
* 存储公钥和私钥
* @param privateKey
* @param publicKey
*/
public record KeyPairRecord(String privateKey, String publicKey) {
}
@@ -47,9 +47,7 @@ public enum ProxyDataTypeEnum {
PORT_MAPPING_SYNC(0x07, Constants.ProxyDataTypeName.PORT_MAPPING_SYNC, "PORT_MAPPING_SYNC"),
UDP_CONNECT(0x08, Constants.ProxyDataTypeName.UDP_CONNECT,"UDP_CONNECT"),
UDP_DISCONNECT(0x09, Constants.ProxyDataTypeName.UDP_DISCONNECT,"UDP_DISCONNECT"),
UDP_TRANSFER(0x10, Constants.ProxyDataTypeName.UDP_TRANSFER,"UDP_TRANSFER"),
SECURE_KEY(0x11, Constants.ProxyDataTypeName.SECURE_KEY, "SECURE_KEY"),
;
UDP_TRANSFER(0x10, Constants.ProxyDataTypeName.UDP_TRANSFER,"UDP_TRANSFER");
private static Map<Integer,ProxyDataTypeEnum> cache = Stream.of(values()).collect(Collectors.toMap(ProxyDataTypeEnum::getType, Function.identity()));
private int type;
@@ -24,7 +24,6 @@ package org.dromara.neutrinoproxy.core;
import lombok.Data;
import lombok.experimental.Accessors;
import org.dromara.neutrinoproxy.core.util.EncryptUtil;
import org.noear.snack.ONode;
import java.util.Arrays;
@@ -80,11 +79,6 @@ public class ProxyMessage {
*/
public static final byte TYPE_UDP_TRANSFER = 0x10;
/**
* 安全密钥协商
*/
public static final byte TYPE_SECURE_KEY = 0x11;
/**
* 消息类型
*/
@@ -123,12 +117,11 @@ public class ProxyMessage {
.setInfo(info + "," + clientId);
}
public static ProxyMessage buildAuthResultMessage(Integer code, String msg, String licenseKey, String publicKey) {
public static ProxyMessage buildAuthResultMessage(Integer code, String msg, String licenseKey) {
ONode data = ONode.newObject();
data.set("code", code);
data.set("msg", msg);
data.set("licenseKey", licenseKey);
data.set("publicKey", publicKey);
return create().setType(TYPE_AUTH)
.setInfo(data.toJson());
}
@@ -143,17 +136,6 @@ public class ProxyMessage {
.setInfo(info);
}
public static ProxyMessage buildSecureKeyMessage(byte[] secureKey) {
return create().setType(TYPE_SECURE_KEY)
.setInfo(EncryptUtil.digestBySm3(secureKey))
.setData(secureKey);
}
public static ProxyMessage buildSecureKeyReturnMessage(byte[] content) {
return create().setType(TYPE_SECURE_KEY)
.setData(content);
}
public static ProxyMessage buildTransferMessage(String visitorId, byte[] data) {
return create().setType(TYPE_TRANSFER)
.setInfo(visitorId)
@@ -22,18 +22,11 @@
package org.dromara.neutrinoproxy.core;
import cn.hutool.core.util.HexUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.util.Attribute;
import lombok.extern.slf4j.Slf4j;
import org.dromara.neutrinoproxy.core.util.EncryptUtil;
import static org.dromara.neutrinoproxy.core.Constants.*;
@Slf4j
/**
*
* @author: aoshiguchen
@@ -77,52 +70,28 @@ public class ProxyMessageDecoder extends LengthFieldBasedFrameDecoder {
return null;
}
Attribute<Boolean> booleanAttribute = ctx.attr(Constants.IS_SECURITY);
Boolean isSecurity = booleanAttribute.get();
ByteBuf buf;
// 考虑isSecurity为null的情况,null的情况也为false
if (isSecurity != null && isSecurity) {
int packageLength = in.readInt();
if (in.readableBytes() < packageLength) {
return null;
}
// 获取加密数据
byte[] encryptedBytes = new byte[packageLength];
in.readBytes(encryptedBytes);
in.release();
// 获取解密密钥
Attribute<byte[]> secureKeyAttr = ctx.attr(SECURE_KEY);
byte[] secureKey = secureKeyAttr.get();
// 解密
byte[] decryptedData = EncryptUtil.decryptByAes(secureKey, encryptedBytes);
buf = Unpooled.wrappedBuffer(decryptedData);
} else {
buf = in;
int frameLength = in.readInt();
if (in.readableBytes() < frameLength) {
return null;
}
ProxyMessage proxyMessage = new ProxyMessage();
int frameLength = buf.readInt();
byte type = buf.readByte();
long sn = buf.readLong();
byte type = in.readByte();
long sn = in.readLong();
proxyMessage.setSerialNumber(sn);
proxyMessage.setType(type);
int infoLength = buf.readInt();
int infoLength = in.readInt();
byte[] infoBytes = new byte[infoLength];
buf.readBytes(infoBytes);
in.readBytes(infoBytes);
proxyMessage.setInfo(new String(infoBytes));
byte[] data = new byte[frameLength - TYPE_SIZE - SERIAL_NUMBER_SIZE - INFO_LENGTH_SIZE - infoLength];
buf.readBytes(data);
in.readBytes(data);
proxyMessage.setData(data);
buf.release();
in.release();
return proxyMessage;
}
@@ -22,15 +22,9 @@
package org.dromara.neutrinoproxy.core;
import cn.hutool.core.util.HexUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.util.Attribute;
import lombok.extern.slf4j.Slf4j;
import org.dromara.neutrinoproxy.core.util.EncryptUtil;
import static org.dromara.neutrinoproxy.core.Constants.*;
/**
@@ -38,7 +32,6 @@ import static org.dromara.neutrinoproxy.core.Constants.*;
* @author: aoshiguchen
* @date: 2022/6/16
*/
@Slf4j
public class ProxyMessageEncoder extends MessageToByteEncoder<ProxyMessage> {
public ProxyMessageEncoder() {
@@ -47,7 +40,6 @@ public class ProxyMessageEncoder extends MessageToByteEncoder<ProxyMessage> {
@Override
protected void encode(ChannelHandlerContext ctx, ProxyMessage msg, ByteBuf out) throws Exception {
int bodyLength = TYPE_SIZE + SERIAL_NUMBER_SIZE + INFO_LENGTH_SIZE;
byte[] infoBytes = null;
if (msg.getInfo() != null) {
@@ -59,50 +51,21 @@ public class ProxyMessageEncoder extends MessageToByteEncoder<ProxyMessage> {
bodyLength += msg.getData().length;
}
Attribute<Boolean> booleanAttribute = ctx.attr(Constants.IS_SECURITY);
Boolean isSecurity = booleanAttribute.get();
ByteBuf buf;
// 考虑isSecurity为null的情况,null的情况也为false
if (isSecurity != null && isSecurity) {
buf = Unpooled.directBuffer(bodyLength);
} else {
buf = out;
}
// write the total packet length but without length field's length.
buf.writeInt(bodyLength);
out.writeInt(bodyLength);
buf.writeByte(msg.getType());
buf.writeLong(msg.getSerialNumber());
out.writeByte(msg.getType());
out.writeLong(msg.getSerialNumber());
if (infoBytes != null) {
buf.writeInt(infoBytes.length);
buf.writeBytes(infoBytes);
out.writeInt(infoBytes.length);
out.writeBytes(infoBytes);
} else {
buf.writeInt(0x00);
out.writeInt(0x00);
}
if (msg.getData() != null) {
buf.writeBytes(msg.getData());
out.writeBytes(msg.getData());
}
// 考虑isSecurity为null的情况,null的情况也为false
if (isSecurity != null && isSecurity) {
// 执行加密
byte[] data = new byte[buf.writerIndex()];
buf.readBytes(data);
// 获取加密密钥
Attribute<byte[]> secureKeyAttr = ctx.attr(SECURE_KEY);
byte[] secureKey = secureKeyAttr.get();
// 执行加密
byte[] encryptedData = EncryptUtil.encryptByAes(secureKey, data);
out.writeInt(encryptedData.length);
out.writeBytes(encryptedData);
buf.release();
}
}
}
@@ -1,58 +0,0 @@
package org.dromara.neutrinoproxy.core.util;
import cn.hutool.core.util.RandomUtil;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Random;
/**
* AES工具
*/
public class AesUtil {
public static byte[] generateKey() {
byte[] keyBytes = new byte[16];
Random random = RandomUtil.getRandom(true);
random.nextBytes(keyBytes);
return keyBytes;
}
/**
* AES解密
* @param decryptKey 秘钥,16位
* @param encryptBytes 密文
* @return 明文
* @throws Exception
*/
public static byte[] decrypt(byte[] decryptKey, byte[] encryptBytes) {
try{
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey, "AES"));
return cipher.doFinal(encryptBytes);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* AES加密
* @param encryptKey 秘钥,必须为16个字符组成
* @param data 明文
* @return 密文
* @throws Exception
*/
public static byte[] encrypt(byte[] encryptKey, byte[] data) {
try {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey, "AES"));
return cipher.doFinal(data);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
@@ -1,150 +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.core.util;
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.util.encoders.Hex;
import org.dromara.neutrinoproxy.core.KeyPairRecord;
import javax.crypto.SecretKey;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
/**
* 国密算法加解密工具
* @author: az
* @date: 2023/11/07
*/
public class EncryptUtil {
/**
* 生成SM2密钥对
* @return
*/
public static KeyPairRecord generateSm2KeyPair() {
String privateKeyHex = null;
String publicKeyHex = null;
KeyPair keyPair = Sm2Util.createECKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
if (privateKey instanceof BCECPrivateKey) {
//获取32字节十六进制私钥串
privateKeyHex = ((BCECPrivateKey) privateKey).getD().toString(16);
}
PublicKey publicKey = keyPair.getPublic();
if (publicKey instanceof BCECPublicKey) {
//获取65字节非压缩缩的十六进制公钥串(0x04)
publicKeyHex = Hex.toHexString(((BCECPublicKey) publicKey).getQ().getEncoded(false));
}
return new KeyPairRecord(privateKeyHex, publicKeyHex);
}
/**
* 使用SM2算法对数据进行加密
* @param publicKey 加密所需的公钥
* @param data 需要加密的数据
* @return 加密后的字节数组
*/
public static byte[] encryptBySm2(String publicKey, byte[] data) {
return Sm2Util.encrypt(publicKey, data);
}
/**
* 使用SM2算法对数据进行解密
* @param privateKey 解密所需私钥
* @param data 需要解密的数据
* @return 解密后的字节数组
*/
public static byte[] decryptBySm2(String privateKey, byte[] data) {
return Sm2Util.decrypt(privateKey, data);
}
public static byte[] generateSm4Key() {
return SecureUtil.generateKey("AES", 128).getEncoded();
}
/**
* 使用SM4算法加密数据
* @param key 密钥
* @param data 待加密的数据
* @return 已加密的数据
*/
public static byte[] encryptBySm4(byte[] key, byte[] data) {
return SmUtil.sm4(key).encrypt(data);
}
/**
* 使用SM4算法解密数据
* @param key 密钥
* @param encryptedData 已加密数据
* @return 解密后的数据
*/
public static byte[] decryptBySm4(byte[] key, byte[] encryptedData) {
return SmUtil.sm4(key).decrypt(encryptedData);
}
public static byte[] generateAesKey() {
return AesUtil.generateKey();
}
/**
* 使用AES算法加密数据
* @param key 密钥
* @param data 被加密数据
* @return 加密后的数据
*/
public static byte[] encryptByAes(byte[] key, byte[] data) {
return AesUtil.encrypt(key, data);
}
/**
* 使用AES法解密数据
* @param key 密钥
* @param encryptedData 已加密数据
* @return 解密后的数据
*/
public static byte[] decryptByAes(byte[] key, byte[] encryptedData) {
return AesUtil.decrypt(key, encryptedData);
}
/**
* 使用SM3算法对内容生成摘要
* @param data
* @return
*/
public static String digestBySm3(byte[] data) {
return SmUtil.sm3().digestHex(data);
}
}
@@ -0,0 +1,18 @@
package org.dromara.neutrinoproxy.core.util;
import io.netty.channel.ChannelHandlerContext;
import java.net.InetSocketAddress;
public class IpUtil extends org.noear.solon.core.util.IpUtil {
public static String getRemoteIp(ChannelHandlerContext ctx) {
String remoteAddress = "";
InetSocketAddress socketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
if (socketAddress != null) {
remoteAddress = socketAddress.getAddress().getHostAddress();
}
return remoteAddress;
}
}
@@ -1,216 +0,0 @@
package org.dromara.neutrinoproxy.core.util;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.jce.spec.ECPrivateKeySpec;
import org.bouncycastle.jce.spec.ECPublicKeySpec;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.spec.ECGenParameterSpec;
/**
* @ClassName SM2Utils
* @Description SM2算法工具类
*/
public class Sm2Util {
/**
* @Description 生成秘钥对
* @return KeyPair
*/
public static KeyPair createECKeyPair() {
//使用标准名称创建EC参数生成的参数规范
final ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");
// 获取一个椭圆曲线类型的密钥对生成器
final KeyPairGenerator kpg;
try {
kpg = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider());
// 使用SM2算法域参数集初始化密钥生成器(默认使用以最高优先级安装的提供者的 SecureRandom 的实现作为随机源)
// kpg.initialize(sm2Spec);
// 使用SM2的算法域参数集和指定的随机源初始化密钥生成器
kpg.initialize(sm2Spec, new SecureRandom());
// 通过密钥生成器生成密钥对
return kpg.generateKeyPair();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* @Description 公钥加密
* @param publicKeyHex SM2十六进制公钥
* @param data 明文数据
* @return String
*/
public static byte[] encrypt(String publicKeyHex, byte[] data) {
return encrypt(getECPublicKeyByPublicKeyHex(publicKeyHex), data, 1);
}
/**
* @Description 公钥加密
* @param publicKey SM2公钥
* @param data 明文数据
* @param modeType 加密模式
* @return String
*/
public static byte[] encrypt(BCECPublicKey publicKey, byte[] data, int modeType) {
//加密模式
SM2Engine.Mode mode = SM2Engine.Mode.C1C3C2;
if (modeType != 1) {
mode = SM2Engine.Mode.C1C2C3;
}
//通过公钥对象获取公钥的基本域参数。
ECParameterSpec ecParameterSpec = publicKey.getParameters();
ECDomainParameters ecDomainParameters = new ECDomainParameters(ecParameterSpec.getCurve(),
ecParameterSpec.getG(), ecParameterSpec.getN());
//通过公钥值和公钥基本参数创建公钥参数对象
ECPublicKeyParameters ecPublicKeyParameters = new ECPublicKeyParameters(publicKey.getQ(), ecDomainParameters);
//根据加密模式实例化SM2公钥加密引擎
SM2Engine sm2Engine = new SM2Engine(mode);
//初始化加密引擎
sm2Engine.init(true, new ParametersWithRandom(ecPublicKeyParameters, new SecureRandom()));
byte[] arrayOfBytes = null;
try {
//将明文字符串转换为指定编码的字节串
//通过加密引擎对字节数串行加密
arrayOfBytes = sm2Engine.processBlock(data, 0, data.length);
} catch (Exception e) {
System.out.println("SM2加密时出现异常:" + e.getMessage());
e.printStackTrace();
}
//将加密后的字节串转换为十六进制字符串
return arrayOfBytes;
}
/**
* @Description 私钥解密
* @param privateKeyHex SM2十六进制私钥
* @param cipherData 密文数据
* @return String
*/
public static byte[] decrypt(String privateKeyHex, byte[] cipherData) {
return decrypt(getBCECPrivateKeyByPrivateKeyHex(privateKeyHex), cipherData, 1);
}
/**
* @Description 私钥解密
* @param privateKey SM私钥
* @param cipherDataByte 密文数据
* @param modeType 解密模式
* @return
*/
public static byte[] decrypt(BCECPrivateKey privateKey, byte[] cipherDataByte, int modeType) {
//解密模式
SM2Engine.Mode mode = SM2Engine.Mode.C1C3C2;
if (modeType != 1)
mode = SM2Engine.Mode.C1C2C3;
//通过私钥对象获取私钥的基本域参数。
ECParameterSpec ecParameterSpec = privateKey.getParameters();
ECDomainParameters ecDomainParameters = new ECDomainParameters(ecParameterSpec.getCurve(),
ecParameterSpec.getG(), ecParameterSpec.getN());
//通过私钥值和私钥钥基本参数创建私钥参数对象
ECPrivateKeyParameters ecPrivateKeyParameters = new ECPrivateKeyParameters(privateKey.getD(),
ecDomainParameters);
//通过解密模式创建解密引擎并初始化
SM2Engine sm2Engine = new SM2Engine(mode);
sm2Engine.init(false, ecPrivateKeyParameters);
try {
//通过解密引擎对密文字节串进行解密
return sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length);
} catch (Exception e) {
System.out.println("SM2解密时出现异常" + e.getMessage());
}
return new byte[0];
}
//椭圆曲线ECParameters ASN.1 结构
private static X9ECParameters x9ECParameters = GMNamedCurves.getByName("sm2p256v1");
//椭圆曲线公钥或私钥的基本域参数。
private static ECParameterSpec ecDomainParameters = new ECParameterSpec(x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN());
/**
* @Description 公钥字符串转换为 BCECPublicKey 公钥对象
* @param pubKeyHex 64字节十六进制公钥字符串(如果公钥字符串为65字节首个字节为0x04:表示该公钥为非压缩格式,操作时需要删除)
* @return BCECPublicKey SM2公钥对象
*/
public static BCECPublicKey getECPublicKeyByPublicKeyHex(String pubKeyHex) {
//截取64字节有效的SM2公钥(如果公钥首个字节为0x04)
if (pubKeyHex.length() > 128) {
pubKeyHex = pubKeyHex.substring(pubKeyHex.length() - 128);
}
//将公钥拆分为x,y分量(各32字节)
String stringX = pubKeyHex.substring(0, 64);
String stringY = pubKeyHex.substring(stringX.length());
//将公钥x、y分量转换为BigInteger类型
BigInteger x = new BigInteger(stringX, 16);
BigInteger y = new BigInteger(stringY, 16);
//通过公钥x、y分量创建椭圆曲线公钥规范
ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(x9ECParameters.getCurve().createPoint(x, y), ecDomainParameters);
//通过椭圆曲线公钥规范,创建出椭圆曲线公钥对象(可用于SM2加密及验签)
return new BCECPublicKey("EC", ecPublicKeySpec, BouncyCastleProvider.CONFIGURATION);
}
/**
* @Description 私钥字符串转换为 BCECPrivateKey 私钥对象
* @param privateKeyHex 32字节十六进制私钥字符串
* @return BCECPrivateKey SM2私钥对象
*/
public static BCECPrivateKey getBCECPrivateKeyByPrivateKeyHex(String privateKeyHex) {
//将十六进制私钥字符串转换为BigInteger对象
BigInteger d = new BigInteger(privateKeyHex, 16);
//通过私钥和私钥域参数集创建椭圆曲线私钥规范
ECPrivateKeySpec ecPrivateKeySpec = new ECPrivateKeySpec(d, ecDomainParameters);
//通过椭圆曲线私钥规范,创建出椭圆曲线私钥对象(可用于SM2解密和签名)
return new BCECPrivateKey("EC", ecPrivateKeySpec, BouncyCastleProvider.CONFIGURATION);
}
public static void main(String[] args) {
/*String publicKeyHex = null;
String privateKeyHex = null;*/
/*KeyPair keyPair = createECKeyPair();
PublicKey publicKey = keyPair.getPublic();
if (publicKey instanceof BCECPublicKey) {
//获取65字节非压缩缩的十六进制公钥串(0x04)
publicKeyHex = Hex.toHexString(((BCECPublicKey) publicKey).getQ().getEncoded(false));
System.out.println("---->SM2公钥:" + publicKeyHex);
}
PrivateKey privateKey = keyPair.getPrivate();
if (privateKey instanceof BCECPrivateKey) {
//获取32字节十六进制私钥串
privateKeyHex = ((BCECPrivateKey) privateKey).getD().toString(16);
System.out.println("---->SM2私钥:" + privateKeyHex);
}*/
/**
* 公钥加密
*/
// String data = "az";
//将十六进制公钥串转换为 BCECPublicKey 公钥对象
/*String encryptData = encrypt(publicKeyHex, data);
System.out.println("---->加密结果:" + encryptData);*/
/**
* 私钥解密
*/
//将十六进制私钥串转换为 BCECPrivateKey 私钥对象
/*data = decrypt("xx", "xx");
System.out.println("---->解密结果:" + data);*/
}
}
@@ -0,0 +1,16 @@
package org.dromara.neutrinoproxy.server.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum SecurityRulePassTypeEnum {
DENY(0, "deny"),
ALLOW(1, "allow"),
NONE(-1, "none")
;
private final Integer code;
private final String desc;
}
@@ -13,6 +13,8 @@ import org.dromara.neutrinoproxy.server.util.ParamCheckUtil;
import org.apache.commons.lang3.StringUtils;
import org.noear.solon.annotation.*;
import java.util.List;
/**
* 端口映射
* @author: aoshiguchen
@@ -64,7 +66,7 @@ public class PortMappingController {
@Post
@Mapping("/update")
public PortMappingUpdateRes update(PortMappingUpdateReq req) {
public void update(PortMappingUpdateReq req) {
ParamCheckUtil.checkNotNull(req, "req");
ParamCheckUtil.checkNotNull(req.getLicenseId(), "licenseId");
ParamCheckUtil.checkNotNull(req.getServerPort(), "serverPort");
@@ -89,7 +91,7 @@ public class PortMappingController {
req.setProxyTimeoutMs(0L);
}
return portMappingService.update(req);
portMappingService.update(req);
}
@Get
@@ -119,4 +121,26 @@ public class PortMappingController {
portMappingService.delete(req.getId());
}
/**
* 绑定安全组
* @param req portMappingId和securityGroupId
*/
@Post
@Mapping("/bind/security-group")
public void bindSecurityGroup(PortMappingBindSecurityGroupReq req) {
portMappingService.portBindSecurityGroup(req.getId(), req.getSecurityGroupId());
}
/**
* 安全组解绑
* @param id 端口映射Id
*/
@Post
@Mapping("/unbind/security-group")
public void unbindSecurityGroup(Integer id) {
portMappingService.portUnbindSecurityGroup(id);
}
}
@@ -0,0 +1,117 @@
package org.dromara.neutrinoproxy.server.controller;
import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum;
import org.dromara.neutrinoproxy.server.controller.req.system.SecurityGroupCreateReq;
import org.dromara.neutrinoproxy.server.controller.req.system.SecurityGroupUpdateReq;
import org.dromara.neutrinoproxy.server.controller.req.system.SecurityRuleCreateReq;
import org.dromara.neutrinoproxy.server.controller.req.system.SecurityRuleUpdateReq;
import org.dromara.neutrinoproxy.server.controller.res.system.SecurityGroupRes;
import org.dromara.neutrinoproxy.server.controller.res.system.SecurityRuleRes;
import org.dromara.neutrinoproxy.server.dal.entity.SecurityGroupDO;
import org.dromara.neutrinoproxy.server.dal.entity.SecurityRuleDO;
import org.dromara.neutrinoproxy.server.service.PortMappingService;
import org.dromara.neutrinoproxy.server.service.SecurityGroupService;
import org.noear.solon.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@Controller
@Mapping("/security")
public class SecurityController {
@Inject
private SecurityGroupService groupService;
@Inject
private PortMappingService portMappingService;
/**
* 获取当前用户权限下的安全组
*/
@Get
@Mapping("/group/s")
public List<SecurityGroupRes> getGroups() {
List<SecurityGroupDO> groupDOList = groupService.queryGroupList();
return groupDOList.stream().map(SecurityGroupDO::toRes).collect(Collectors.toList());
}
@Get
@Mapping("/group/getOne")
public SecurityGroupRes getGroupOne(Integer groupId) {
return groupService.queryGroupOne(groupId).toRes();
}
@Post
@Mapping("/group/create")
public void createGroup(SecurityGroupCreateReq req) {
groupService.createGroup(req);
}
@Post
@Mapping("/group/update")
public void updateGroup(SecurityGroupUpdateReq req) {
groupService.updateGroup(req);
}
/**
* 将级联删除对应规则,并更新缓存
* @param groupId 安全组Id
*/
@Post
@Mapping("/group/delete")
public void deleteGroup(Integer groupId) {
groupService.deleteGroup(groupId);
}
@Post
@Mapping("/group/enable")
public void enableGroup(Integer groupId) {
groupService.setGroupStatus(groupId, EnableStatusEnum.ENABLE);
}
@Post
@Mapping("/group/disable")
public void disableGroup(Integer groupId) {
groupService.setGroupStatus(groupId, EnableStatusEnum.DISABLE);
}
@Get
@Mapping("/rule/s")
public List<SecurityRuleRes> getRulesByGroupId(Integer groupId) {
List<SecurityRuleDO> ruleDOList = groupService.queryRuleListByGroupId(groupId);
return ruleDOList.stream().map(SecurityRuleDO::toRes).collect(Collectors.toList());
}
@Post
@Mapping("/rule/create")
public void createRule(SecurityRuleCreateReq req) {
groupService.createRule(req);
}
@Post
@Mapping("/rule/update")
public void updateRule(SecurityRuleUpdateReq req) {
groupService.updateRule(req);
}
@Post
@Mapping("/rule/delete")
public void deleteRule(Integer ruleId) {
groupService.deleteRule(ruleId);
}
@Post
@Mapping("/rule/enable")
public void enableRule(Integer ruleId) {
groupService.setRuleStatus(ruleId, EnableStatusEnum.ENABLE);
}
@Post
@Mapping("/rule/disable")
public void disableRule(Integer ruleId) {
groupService.setRuleStatus(ruleId, EnableStatusEnum.DISABLE);
}
}
@@ -0,0 +1,12 @@
package org.dromara.neutrinoproxy.server.controller.req.proxy;
import lombok.Data;
@Data
public class PortMappingBindSecurityGroupReq {
private Integer id;
private Integer securityGroupId;
}
@@ -62,6 +62,12 @@ public class PortMappingCreateReq {
* 代理超时时间
*/
private Long proxyTimeoutMs;
/**
* 安全组Id
*/
private Integer securityGroupId;
/**
* 描述
*/
@@ -66,6 +66,12 @@ public class PortMappingUpdateReq {
* 代理超时时间
*/
private Long proxyTimeoutMs;
/**
* 安全组Id
*/
private Integer securityGroupId;
/**
* 描述
*/
@@ -0,0 +1,23 @@
package org.dromara.neutrinoproxy.server.controller.req.system;
import lombok.Data;
import org.dromara.neutrinoproxy.server.constant.SecurityRulePassTypeEnum;
@Data
public class SecurityGroupCreateReq {
/**
* 组名
*/
private String name;
/**
* 描述
*/
private String description;
/**
* 通过类型
*/
private SecurityRulePassTypeEnum defaultPassType;
}
@@ -0,0 +1,21 @@
package org.dromara.neutrinoproxy.server.controller.req.system;
import lombok.Data;
import org.dromara.neutrinoproxy.server.constant.SecurityRulePassTypeEnum;
@Data
public class SecurityGroupUpdateReq {
private Integer id;
/**
* 组名
*/
private String name;
/**
* 描述
*/
private String description;
}
@@ -0,0 +1,57 @@
package org.dromara.neutrinoproxy.server.controller.req.system;
import cn.hutool.core.net.Ipv4Util;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;
import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum;
import org.dromara.neutrinoproxy.server.constant.SecurityRulePassTypeEnum;
import java.util.Date;
@Data
@ToString
@Accessors(chain = true)
public class SecurityRuleCreateReq {
/**
* 所属安全组
*/
private Integer groupId;
/**
* 规则名
*/
private String name;
/**
* 规则描述
*/
private String description;
/**
* 规则,ipv6只支持单个ip判断
* 单个ip192.168.1.1,0:0:0:0:0:0:10.0.0.1
* 范围类型:192.168.1.0-192.168.1.255
* 掩码类型:192.168.1.0/24
* 泛型:0.0.0.0/ALL
* 每个类型中间以英文逗号分隔
*/
private String rule;
/**
* 放行类型,reject 或 allow
* {@link SecurityRulePassTypeEnum}
*/
private SecurityRulePassTypeEnum passType;
/**
* 优先级,数字越小,优先级越高
*/
private Integer priority = 1;
}
@@ -0,0 +1,51 @@
package org.dromara.neutrinoproxy.server.controller.req.system;
import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;
import org.dromara.neutrinoproxy.server.constant.SecurityRulePassTypeEnum;
@Data
@ToString
@Accessors(chain = true)
public class SecurityRuleUpdateReq {
private Integer id;
/**
* 所属安全组
*/
private Integer groupId;
/**
* 规则名
*/
private String name;
/**
* 规则描述
*/
private String description;
/**
* 规则,ipv6只支持单个ip判断
* 单个ip192.168.1.1,0:0:0:0:0:0:10.0.0.1
* 范围类型:192.168.1.0-192.168.1.255
* 掩码类型:192.168.1.0/24
* 泛型:0.0.0.0/ALL
* 每个类型中间以英文逗号分隔
*/
private String rule;
/**
* 放行类型,reject 或 allow
* {@link SecurityRulePassTypeEnum}
*/
private SecurityRulePassTypeEnum passType;
/**
* 优先级,数字越小,优先级越高
*/
private Integer priority;
}
@@ -101,6 +101,12 @@ public class PortMappingListRes {
* 描述
*/
private String description;
/**
* 安全组Id
*/
private Integer securityGroupId;
/**
* 创建时间
*/
@@ -0,0 +1,49 @@
package org.dromara.neutrinoproxy.server.controller.res.system;
import lombok.Data;
import lombok.experimental.Accessors;
import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum;
import org.dromara.neutrinoproxy.server.constant.SecurityRulePassTypeEnum;
import java.util.Date;
@Data
@Accessors(chain = true)
public class SecurityGroupRes {
private Integer id;
/**
* 组名
*/
private String name;
/**
* 描述
*/
private String description;
/**
* 启用状态
* {@link EnableStatusEnum}
*/
private String enable;
/**
* 默认放行类型
* {@link SecurityRulePassTypeEnum}
*/
private String defaultPassType;
/**
* 创建时间
*/
private String createTime;
/**
* 更新时间
*/
private String updateTime;
}
@@ -0,0 +1,68 @@
package org.dromara.neutrinoproxy.server.controller.res.system;
import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;
import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum;
import org.dromara.neutrinoproxy.server.constant.SecurityRulePassTypeEnum;
import java.util.Date;
@Data
@ToString
@Accessors(chain = true)
public class SecurityRuleRes {
private Integer id;
/**
* 所属安全组
*/
private Integer groupId;
/**
* 规则名
*/
private String name;
/**
* 规则描述
*/
private String description;
/**
* 规则,ipv6只支持单个ip判断
* 单个ip192.168.1.1,0:0:0:0:0:0:10.0.0.1
* 范围类型:192.168.1.0-192.168.1.255
* 掩码类型:192.168.1.0/24
* 泛型:0.0.0.0/ALL
* 每个类型中间以英文逗号分隔
*/
private String rule;
/**
* 放行类型,reject 或 allow
* {@link SecurityRulePassTypeEnum}
*/
private String passType;
/**
* 优先级,数字越小,优先级越高
*/
private Integer priority;
/**
* 启用状态
*/
private String enable;
/**
* 创建时间
*/
private String createTime;
/**
* 更新时间
*/
private String updateTime;
}
@@ -45,8 +45,8 @@ public interface FlowReportHourMapper extends BaseMapper<FlowReportHourDO> {
default List<FlowReportHourDO> findListByDateRange(Date startDate, Date endDate) {
return this.selectList(new LambdaQueryWrapper<FlowReportHourDO>()
.ge(FlowReportHourDO::getDateStr, startDate)
.le(FlowReportHourDO::getDateStr, endDate)
.ge(FlowReportHourDO::getDate, startDate)
.le(FlowReportHourDO::getDate, endDate)
);
}
@@ -0,0 +1,7 @@
package org.dromara.neutrinoproxy.server.dal;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.dromara.neutrinoproxy.server.dal.entity.SecurityGroupDO;
public interface SecurityGroupMapper extends BaseMapper<SecurityGroupDO> {
}
@@ -0,0 +1,7 @@
package org.dromara.neutrinoproxy.server.dal;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.dromara.neutrinoproxy.server.dal.entity.SecurityRuleDO;
public interface SecurityRuleMapper extends BaseMapper<SecurityRuleDO> {
}
@@ -21,6 +21,7 @@
*/
package org.dromara.neutrinoproxy.server.dal.entity;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -92,6 +93,12 @@ public class PortMappingDO {
* {@link EnableStatusEnum}
*/
private Integer enable;
/**
* 安全组Id
*/
private Integer securityGroupId = 0; // 设置为null不生效,不知道为啥
/**
* 创建时间
*/
@@ -101,23 +108,9 @@ public class PortMappingDO {
*/
private Date updateTime;
public PortMappingListRes toRes() {
PortMappingListRes res = new PortMappingListRes();
res.setId(id);
res.setLicenseId(licenseId);
res.setProtocal(protocal);
res.setSubdomain(subdomain);
res.setServerPort(serverPort);
res.setClientIp(clientIp);
res.setClientPort(clientPort);
res.setDescription(description);
res.setIsOnline(isOnline);
res.setProxyResponses(proxyResponses);
res.setProxyTimeoutMs(proxyTimeoutMs);
res.setEnable(enable);
res.setCreateTime(createTime);
res.setUpdateTime(updateTime);
BeanUtil.copyProperties(this, res);
return res;
}
}
@@ -0,0 +1,74 @@
package org.dromara.neutrinoproxy.server.dal.entity;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;
import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum;
import org.dromara.neutrinoproxy.server.constant.SecurityRulePassTypeEnum;
import org.dromara.neutrinoproxy.server.controller.res.system.SecurityGroupRes;
import java.util.Date;
@Data
@ToString
@Accessors(chain = true)
@TableName("security_group")
public class SecurityGroupDO {
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 组名
*/
private String name;
/**
* 描述
*/
private String description;
/**
* 用户id
*/
private Integer userId;
/**
* 启用状态
* {@link EnableStatusEnum}
*/
private EnableStatusEnum enable;
/**
* 默认放行类型
* {@link SecurityRulePassTypeEnum}
*/
private SecurityRulePassTypeEnum defaultPassType;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
public SecurityGroupRes toRes() {
SecurityGroupRes res = new SecurityGroupRes();
BeanUtil.copyProperties(this, res);
res.setEnable(enable.getDesc())
.setDefaultPassType(defaultPassType.getDesc())
.setCreateTime(DateUtil.format(this.getCreateTime(), DatePattern.NORM_DATETIME_FORMAT))
.setUpdateTime(DateUtil.format(this.getUpdateTime(), DatePattern.NORM_DATETIME_FORMAT));
return res;
}
}
@@ -0,0 +1,159 @@
package org.dromara.neutrinoproxy.server.dal.entity;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.net.Ipv4Util;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;
import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum;
import org.dromara.neutrinoproxy.server.constant.SecurityRulePassTypeEnum;
import org.dromara.neutrinoproxy.server.controller.res.system.SecurityRuleRes;
import java.util.Date;
@Data
@ToString
@Accessors(chain = true)
@TableName("security_rule")
public class SecurityRuleDO {
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 所属安全组
*/
private Integer groupId;
/**
* 规则名
*/
private String name;
/**
* 规则描述
*/
private String description;
/**
* 规则,ipv6只支持单个ip判断
* 单个ip192.168.1.1,0:0:0:0:0:0:10.0.0.1
* 范围类型:192.168.1.0-192.168.1.255
* 掩码类型:192.168.1.0/24
* 泛型:0.0.0.0/ALL
* 每个类型中间以英文逗号分隔
*/
private String rule;
/**
* 放行类型,reject 或 allow
* {@link SecurityRulePassTypeEnum}
*/
private SecurityRulePassTypeEnum passType;
/**
* 优先级,数字越小,优先级越高
*/
private Integer priority = 1;
/**
* 用户id
*/
private Integer userId;
/**
* 启用状态
* {@link EnableStatusEnum}
*/
private EnableStatusEnum enable;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 判断当前规则是否允许指定ip放行
* @param ip 指定的IP
* @return 放行状态
*/
public SecurityRulePassTypeEnum judge(String ip) {
// 被判断的IP地址为空,不做判断
if (StrUtil.isEmpty(ip)) {
return SecurityRulePassTypeEnum.NONE;
}
// 没有规则,默认允许访问
if (StrUtil.isEmpty(rule)) {
return SecurityRulePassTypeEnum.ALLOW;
}
// ipv6只适配单ip形式
boolean isIpv6 = ip.contains(":");
long ipLong = -1L;
if (!isIpv6) {
ipLong = Ipv4Util.ipv4ToLong(ip);
}
String[] rules = this.rule.split(",");
for (String rule : rules) {
rule = rule.trim();
// 单个ip,ipv6在此步已处理,后面不需要额外判断ipv6的情况
if (rule.matches("(\\d+\\.){3}\\d+") || isIpv6) {
if (rule.equalsIgnoreCase(ip)) {
return passType == SecurityRulePassTypeEnum.ALLOW ? SecurityRulePassTypeEnum.ALLOW : SecurityRulePassTypeEnum.DENY;
}
}
// 范围类型
if (rule.matches("(\\d+\\.){3}\\d+-(\\d+\\.){3}\\d+")) {
String[] ipRange = rule.split("-");
if (ipRange[0].compareTo(ip) <= 0 && ip.compareTo(ipRange[1]) <= 0) {
return passType == SecurityRulePassTypeEnum.ALLOW ? SecurityRulePassTypeEnum.ALLOW : SecurityRulePassTypeEnum.DENY;
}
}
// 掩码类型
if (rule.matches("(\\d+\\.){3}\\d+/\\d+")) {
String[] netIp = rule.split("/");
Long beginIp = Ipv4Util.getBeginIpLong(netIp[0], Integer.parseInt(netIp[1]));
Long endIp = Ipv4Util.getEndIpLong(netIp[0], Integer.parseInt(netIp[1]));
if (beginIp <= ipLong && ipLong <= endIp) {
return passType == SecurityRulePassTypeEnum.ALLOW ? SecurityRulePassTypeEnum.ALLOW : SecurityRulePassTypeEnum.DENY;
}
}
if (rule.equalsIgnoreCase("ALL") || rule.equals("0.0.0.0") || rule.equals("0.0.0.0/0")) {
return passType == SecurityRulePassTypeEnum.ALLOW ? SecurityRulePassTypeEnum.ALLOW : SecurityRulePassTypeEnum.DENY;
}
}
// 都没有匹配到
return SecurityRulePassTypeEnum.NONE;
}
public SecurityRuleRes toRes() {
SecurityRuleRes res = new SecurityRuleRes();
BeanUtil.copyProperties(this, res);
res.setPassType(this.passType.getDesc())
.setEnable(this.getEnable().getDesc())
.setCreateTime(DateUtil.format(this.getCreateTime(), DatePattern.NORM_DATETIME_FORMAT))
.setUpdateTime(DateUtil.format(this.getUpdateTime(), DatePattern.NORM_DATETIME_FORMAT))
;
return res;
}
}
@@ -1,19 +1,23 @@
package org.dromara.neutrinoproxy.server.proxy.core;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.neutrinoproxy.core.Constants;
import org.dromara.neutrinoproxy.core.ProxyMessage;
import org.dromara.neutrinoproxy.server.constant.NetworkProtocolEnum;
import org.dromara.neutrinoproxy.server.proxy.domain.VisitorChannelAttachInfo;
import org.dromara.neutrinoproxy.server.service.FlowReportService;
import org.dromara.neutrinoproxy.server.util.ProxyUtil;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOption;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import org.dromara.neutrinoproxy.core.Constants;
import org.dromara.neutrinoproxy.core.ProxyMessage;
import org.dromara.neutrinoproxy.core.util.IpUtil;
import org.dromara.neutrinoproxy.server.constant.NetworkProtocolEnum;
import org.dromara.neutrinoproxy.server.proxy.domain.VisitorChannelAttachInfo;
import org.dromara.neutrinoproxy.server.service.FlowReportService;
import org.dromara.neutrinoproxy.server.service.PortMappingService;
import org.dromara.neutrinoproxy.server.service.SecurityGroupService;
import org.dromara.neutrinoproxy.server.util.ProxyUtil;
import org.noear.solon.Solon;
import org.noear.solon.annotation.Inject;
import java.net.InetSocketAddress;
@@ -25,6 +29,10 @@ import java.net.InetSocketAddress;
@Slf4j
public class TcpVisitorChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
private final SecurityGroupService securityGroupService = Solon.context().getBean(SecurityGroupService.class);
private final PortMappingService portMappingService = Solon.context().getBean(PortMappingService.class);
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 当出现异常就关闭连接
@@ -44,6 +52,14 @@ public class TcpVisitorChannelHandler extends SimpleChannelInboundHandler<ByteBu
return;
}
// 判断IP是否在该端口绑定的安全组允许的规则内
InetSocketAddress sa = (InetSocketAddress) visitorChannel.localAddress();
if (!securityGroupService.judgeAllow(IpUtil.getRemoteIp(ctx), portMappingService.getSecurityGroupIdByMappingPort(sa.getPort()))) {
// 不在安全组规则放行范围内
ctx.channel().close();
return;
}
// 代理通道可写,则设置访问通道可读。代理通道不可写,则设置访问通道不可读
visitorChannel.config().setAutoRead(proxyChannel.isWritable());
@@ -62,8 +78,15 @@ public class TcpVisitorChannelHandler extends SimpleChannelInboundHandler<ByteBu
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Channel visitorChannel = ctx.channel();
InetSocketAddress sa = (InetSocketAddress) visitorChannel.localAddress();
Channel cmdChannel = ProxyUtil.getCmdChannelByServerPort(sa.getPort());
// 判断IP是否在该端口绑定的安全组允许的规则内
if (!securityGroupService.judgeAllow(IpUtil.getRemoteIp(ctx), portMappingService.getSecurityGroupIdByMappingPort(sa.getPort()))) {
// 不在安全组规则放行范围内
ctx.channel().close();
return;
}
Channel cmdChannel = ProxyUtil.getCmdChannelByServerPort(sa.getPort());
if (null == cmdChannel) {
// 该端口还没有代理客户端
ctx.channel().close();
@@ -10,12 +10,16 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.neutrinoproxy.core.Constants;
import org.dromara.neutrinoproxy.core.ProxyMessage;
import org.dromara.neutrinoproxy.core.util.IpUtil;
import org.dromara.neutrinoproxy.server.constant.NetworkProtocolEnum;
import org.dromara.neutrinoproxy.server.proxy.domain.ProxyAttachment;
import org.dromara.neutrinoproxy.server.proxy.domain.VisitorChannelAttachInfo;
import org.dromara.neutrinoproxy.server.service.FlowReportService;
import org.dromara.neutrinoproxy.server.service.PortMappingService;
import org.dromara.neutrinoproxy.server.service.SecurityGroupService;
import org.dromara.neutrinoproxy.server.util.ProxyUtil;
import org.noear.solon.Solon;
import org.noear.solon.annotation.Inject;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
@@ -27,9 +31,21 @@ import java.nio.charset.StandardCharsets;
@Slf4j
public class UdpVisitorChannelHandler extends SimpleChannelInboundHandler<DatagramPacket> {
private final SecurityGroupService securityGroupService = Solon.context().getBean(SecurityGroupService.class);
private final PortMappingService portMappingService = Solon.context().getBean(PortMappingService.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket datagramPacket) throws Exception {
log.debug("chid>>>{}", ctx.channel().id().asLongText());
Channel visitorChannel = ctx.channel();
InetSocketAddress sa = (InetSocketAddress) visitorChannel.localAddress();
// 判断IP是否在该端口绑定的安全组允许的规则内
if (!securityGroupService.judgeAllow(datagramPacket.sender().getAddress().getHostAddress(), portMappingService.getSecurityGroupIdByMappingPort(sa.getPort()))) {
return;
}
byte[] bytes = new byte[datagramPacket.content().readableBytes()];
datagramPacket.content().readBytes(bytes);
datagramPacket.content().resetReaderIndex();
@@ -78,8 +94,6 @@ public class UdpVisitorChannelHandler extends SimpleChannelInboundHandler<Datagr
return;
}
Channel visitorChannel = ctx.channel();
InetSocketAddress sa = (InetSocketAddress) visitorChannel.localAddress();
Channel cmdChannel = ProxyUtil.getCmdChannelByServerPort(sa.getPort());
// 没有指令通道,直接结束
@@ -10,12 +10,16 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.neutrinoproxy.core.Constants;
import org.dromara.neutrinoproxy.core.ProxyMessage;
import org.dromara.neutrinoproxy.core.util.IpUtil;
import org.dromara.neutrinoproxy.server.constant.NetworkProtocolEnum;
import org.dromara.neutrinoproxy.server.proxy.domain.ProxyAttachment;
import org.dromara.neutrinoproxy.server.proxy.domain.VisitorChannelAttachInfo;
import org.dromara.neutrinoproxy.server.service.FlowReportService;
import org.dromara.neutrinoproxy.server.service.PortMappingService;
import org.dromara.neutrinoproxy.server.service.SecurityGroupService;
import org.dromara.neutrinoproxy.server.util.ProxyUtil;
import org.noear.solon.Solon;
import org.noear.solon.annotation.Inject;
import java.net.InetSocketAddress;
@@ -25,6 +29,11 @@ import java.net.InetSocketAddress;
*/
@Slf4j
public class HttpVisitorChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
private final SecurityGroupService securityGroupService = Solon.context().getBean(SecurityGroupService.class);
private final PortMappingService portMappingService = Solon.context().getBean(PortMappingService.class);
/**
* 域名
*/
@@ -87,6 +96,14 @@ public class HttpVisitorChannelHandler extends SimpleChannelInboundHandler<ByteB
ctx.channel().close();
return;
}
// 判断IP是否在该端口绑定的安全组允许的规则内
if (!securityGroupService.judgeAllow(IpUtil.getRemoteIp(ctx), portMappingService.getSecurityGroupIdByMappingPort(serverPort))) {
// 不在安全组规则放行范围内
ctx.channel().close();
return;
}
Channel cmdChannel = ProxyUtil.getCmdChannelByServerPort(serverPort);
if (null == cmdChannel) {
ctx.channel().close();
@@ -23,10 +23,9 @@
package org.dromara.neutrinoproxy.server.proxy.handler;
import cn.hutool.core.util.StrUtil;
import io.netty.util.Attribute;
import org.dromara.neutrinoproxy.core.*;
import org.dromara.neutrinoproxy.core.*;
import org.dromara.neutrinoproxy.core.dispatcher.Match;
import org.dromara.neutrinoproxy.core.util.EncryptUtil;
import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig;
import org.dromara.neutrinoproxy.server.constant.ClientConnectTypeEnum;
import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum;
@@ -91,7 +90,7 @@ public class ProxyMessageAuthHandler implements ProxyMessageHandler {
if (StrUtil.isEmpty(licenseKey)) {
log.warn("[client connection] license cannot empty info:{} ", info);
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.AUTH_FAILED.getCode(), "license不能为空!", licenseKey, null));
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.AUTH_FAILED.getCode(), "license不能为空!", licenseKey));
ctx.channel().close();
clientConnectRecordService.add(new ClientConnectRecordDO()
.setIp(ip)
@@ -105,22 +104,22 @@ public class ProxyMessageAuthHandler implements ProxyMessageHandler {
}
LicenseDO licenseDO = licenseService.findByKey(licenseKey);
if (null == licenseDO) {
log.warn("[client connection] license not found info:{} ", info);
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.AUTH_FAILED.getCode(), "license不存在!", licenseKey, null));
log.warn("[client connection] license notfound 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())
.setMsg(licenseKey)
.setCode(SuccessCodeEnum.FAIL.getCode())
.setErr("license not found!")
.setErr("license notfound!")
.setCreateTime(now)
);
return;
}
if (EnableStatusEnum.DISABLE.getStatus().equals(licenseDO.getEnable())) {
log.warn("[client connection] the license disabled info:{} ", info);
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.AUTH_FAILED.getCode(), "the license disabled!", licenseKey, null));
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.AUTH_FAILED.getCode(), "the license disabled!", licenseKey));
ctx.channel().close();
clientConnectRecordService.add(new ClientConnectRecordDO()
.setIp(ip)
@@ -135,7 +134,7 @@ public class ProxyMessageAuthHandler implements ProxyMessageHandler {
UserDO userDO = userService.findById(licenseDO.getUserId());
if (null == userDO || EnableStatusEnum.DISABLE.getStatus().equals(userDO.getEnable())) {
log.warn("[client connection] the license invalid info:{} ", info);
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.AUTH_FAILED.getCode(), "the license invalid!", licenseKey, null));
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.AUTH_FAILED.getCode(), "the license invalid!", licenseKey));
ctx.channel().close();
clientConnectRecordService.add(new ClientConnectRecordDO()
.setIp(ip)
@@ -152,7 +151,7 @@ public class ProxyMessageAuthHandler implements ProxyMessageHandler {
String _clientId = ProxyUtil.getClientIdByLicenseId(licenseDO.getId());
if (!clientId.equals(_clientId)) {
log.warn("[client connection] the license on another no used info:{} _clientId:{}", info, _clientId);
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.AUTH_FAILED.getCode(), "the license on another no used!", licenseKey, null));
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.AUTH_FAILED.getCode(), "the license on another no used!", licenseKey));
ctx.channel().close();
clientConnectRecordService.add(new ClientConnectRecordDO()
.setIp(ip)
@@ -165,22 +164,8 @@ public class ProxyMessageAuthHandler implements ProxyMessageHandler {
return;
}
}
// 存储状态为非安全,如果客户端响应以下的公钥信息,则在响应中设置为安全
Attribute<Boolean> booleanAttribute = ctx.attr(Constants.IS_SECURITY);
booleanAttribute.set(false);
// 生成获取SM2密钥对,私钥存入ctx,公钥拼装参数随Auth数据包返回
KeyPairRecord record = EncryptUtil.generateSm2KeyPair();
// 私钥存入ctx
ctx.attr(Constants.SECURE_PRIVATE_KEY).set(record.privateKey());
// 存储licenseId
ctx.attr(Constants.LICENSE_ID).set(licenseDO.getId());
// 发送认证成功消息
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.SUCCESS.getCode(), "auth success!", licenseKey, record.publicKey()));
ctx.channel().writeAndFlush(ProxyMessage.buildAuthResultMessage(ExceptionEnum.SUCCESS.getCode(), "auth success!", licenseKey));
clientConnectRecordService.add(new ClientConnectRecordDO()
.setIp(ip)
@@ -92,9 +92,6 @@ public class ProxyMessageConnectHandler implements ProxyMessageHandler {
ProxyUtil.remoteProxyConnectAttachment(visitorId);
proxyAttachment.execute();
}
// 设置加密
ProxyUtil.setChannelSecurity(licenseDO.getId(), ctx.channel());
}
@Override
@@ -1,75 +0,0 @@
package org.dromara.neutrinoproxy.server.proxy.handler;
import cn.hutool.core.util.StrUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.Attribute;
import lombok.extern.slf4j.Slf4j;
import org.dromara.neutrinoproxy.core.Constants;
import org.dromara.neutrinoproxy.core.ProxyDataTypeEnum;
import org.dromara.neutrinoproxy.core.ProxyMessage;
import org.dromara.neutrinoproxy.core.ProxyMessageHandler;
import org.dromara.neutrinoproxy.core.dispatcher.Match;
import org.dromara.neutrinoproxy.core.util.EncryptUtil;
import org.dromara.neutrinoproxy.server.util.ProxyUtil;
import org.noear.solon.annotation.Component;
import java.util.Map;
@Slf4j
@Match(type= Constants.ProxyDataTypeName.SECURE_KEY)
@Component
public class ProxyMessageSecureKeyHandler implements ProxyMessageHandler {
@Override
public void handle(ChannelHandlerContext ctx, ProxyMessage proxyMessage) {
log.info("收到客户端的加密信息");
// data为加密后的密码,info为加密密码的摘要
byte[] data = proxyMessage.getData();
String receivedDigest = proxyMessage.getInfo();
String digest = EncryptUtil.digestBySm3(data);
if (!digest.equals(receivedDigest)) {
// 获取加密信息失败
log.warn("密码协商失败");
// TODO 应该断开连接
return;
}
// 获取私钥
Attribute<String> privateKeyAttr = ctx.attr(Constants.SECURE_PRIVATE_KEY);
String privateKey = privateKeyAttr.get();
if (StrUtil.isEmpty(privateKey)) {
// 获取私钥失败
log.warn("获取私钥失败");
// TODO 应该断开连接
return;
}
// 解密传输密码
byte[] secureKey = EncryptUtil.decryptBySm2(privateKey, data);
// 传输密码存储ctx中
Attribute<byte[]> secureKeyAttr = ctx.attr(Constants.SECURE_KEY);
secureKeyAttr.setIfAbsent(secureKey);
// 使用密码加密success给客户端表示密码已确认
byte[] encryptedSuccessInfoData = EncryptUtil.encryptByAes(secureKey, "ok".getBytes());
// 发送回去,以示确认
ctx.writeAndFlush(ProxyMessage.buildSecureKeyReturnMessage(encryptedSuccessInfoData));
ctx.flush();
// 设置该链路以及相关链路状态为安全,之后使用链路传输的数据均会加密
Integer licenseId = ctx.attr(Constants.LICENSE_ID).get();
ProxyUtil.setSecureKey(licenseId, secureKey);
ProxyUtil.setChannelSecurity(licenseId, ctx.channel());
}
@Override
public String name() {
return ProxyDataTypeEnum.SECURE_KEY.getDesc();
}
}
@@ -1,10 +1,12 @@
package org.dromara.neutrinoproxy.server.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.solon.plugins.pagination.Page;
import com.google.common.collect.Sets;
import org.apache.ibatis.solon.annotation.Db;
@@ -47,6 +49,7 @@ import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -74,6 +77,9 @@ public class PortMappingService implements LifecycleBean {
@Inject
private DBInitialize dbInitialize;
/** 端口到安全组Id的映射 */
private final Map<Integer, Integer> mappingPortToSecurityGroupMap = new ConcurrentHashMap<>();
public PageInfo<PortMappingListRes> page(PageQuery pageQuery, PortMappingListReq req) {
if (StringUtils.isNotEmpty(req.getDescription())) {
//描述字段为模糊查询,在应用层处理,否则sqlite不支持
@@ -161,10 +167,13 @@ public class PortMappingService implements LifecycleBean {
if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) && StrUtil.isNotBlank(proxyConfig.getServer().getTcp().getDomainName()) && StrUtil.isNotBlank(portMappingDO.getSubdomain())) {
ProxyUtil.setSubdomainToServerPort(portMappingDO.getSubdomain(), portMappingDO.getServerPort());
}
updateMappingPortToSecurityGroupMap(portMappingDO.getServerPort(), req.getSecurityGroupId());
return new PortMappingCreateRes();
}
public PortMappingUpdateRes update(PortMappingUpdateReq req) {
public void update(PortMappingUpdateReq req) {
LicenseDO licenseDO = licenseMapper.findById(req.getLicenseId());
ParamCheckUtil.checkNotNull(licenseDO, ExceptionConstant.LICENSE_NOT_EXIST);
if (!SystemContextHolder.isAdmin()) {
@@ -181,16 +190,10 @@ public class PortMappingService implements LifecycleBean {
ParamCheckUtil.checkNotNull(oldPortMappingDO, ExceptionConstant.PORT_MAPPING_NOT_EXIST);
PortMappingDO portMappingDO = new PortMappingDO();
portMappingDO.setId(req.getId());
portMappingDO.setProtocal(req.getProtocal());
portMappingDO.setSubdomain(req.getSubdomain());
portMappingDO.setLicenseId(req.getLicenseId());
portMappingDO.setServerPort(req.getServerPort());
portMappingDO.setClientIp(req.getClientIp());
portMappingDO.setClientPort(req.getClientPort());
portMappingDO.setProxyResponses(req.getProxyResponses());
portMappingDO.setProxyTimeoutMs(req.getProxyTimeoutMs());
portMappingDO.setDescription(req.getDescription());
BeanUtil.copyProperties(req, portMappingDO);
if (req.getSecurityGroupId() == null) {
portMappingDO.setSecurityGroupId(0);
}
portMappingDO.setUpdateTime(new Date());
portMappingDO.setEnable(EnableStatusEnum.ENABLE.getStatus());
portMappingMapper.updateById(portMappingDO);
@@ -204,7 +207,8 @@ public class PortMappingService implements LifecycleBean {
if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) && StrUtil.isNotBlank(proxyConfig.getServer().getTcp().getDomainName()) && StrUtil.isNotBlank(portMappingDO.getSubdomain())) {
ProxyUtil.setSubdomainToServerPort(portMappingDO.getSubdomain(), portMappingDO.getServerPort());
}
return new PortMappingUpdateRes();
updateMappingPortToSecurityGroupMap(portMappingDO.getServerPort(), req.getSecurityGroupId());
}
public PortMappingDetailRes detail(Integer id) {
@@ -279,6 +283,30 @@ public class PortMappingService implements LifecycleBean {
if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) && StrUtil.isNotBlank(portMappingDO.getSubdomain())) {
ProxyUtil.removeSubdomainToServerPort(portMappingDO.getSubdomain());
}
updateMappingPortToSecurityGroupMap(portMappingDO.getServerPort(), null);
}
public void portBindSecurityGroup(Integer portMappingId, Integer groupId) {
PortMappingDO mappingDO = portMappingMapper.findById(portMappingId);
if (mappingDO == null) {
throw new RuntimeException("指定的端口映射不存在");
}
mappingDO.setSecurityGroupId(groupId);
mappingDO.setUpdateTime(new Date());
portMappingMapper.updateById(mappingDO);
updateMappingPortToSecurityGroupMap(mappingDO.getServerPort(), groupId);
}
public void portUnbindSecurityGroup(Integer portMappingId) {
PortMappingDO mappingDO = portMappingMapper.findById(portMappingId);
if (mappingDO == null) {
throw new RuntimeException("指定的端口映射不存在");
}
mappingDO.setSecurityGroupId(0);
mappingDO.setUpdateTime(new Date());
portMappingMapper.updateById(mappingDO);
updateMappingPortToSecurityGroupMap(mappingDO.getServerPort(), null);
}
/**
@@ -291,6 +319,11 @@ public class PortMappingService implements LifecycleBean {
return portMappingMapper.findEnableListByLicenseId(licenseId);
}
public Integer getSecurityGroupIdByMappingPort(Integer port) {
return mappingPortToSecurityGroupMap.get(port);
}
/**
* 服务端项目停止、启动时,更新在线状态为离线
*/
@@ -302,11 +335,22 @@ public class PortMappingService implements LifecycleBean {
}
portMappingMapper.updateOnlineStatus(OnlineStatusEnum.OFFLINE.getStatus(), new Date());
List<PortMappingDO> allMappingDOList = portMappingMapper.selectList(Wrappers.lambdaQuery(PortMappingDO.class));
allMappingDOList.forEach(item -> {
Integer securityGroupId = item.getSecurityGroupId();
if (securityGroupId != null && securityGroupId > 0) {
updateMappingPortToSecurityGroupMap(item.getServerPort(), item.getSecurityGroupId());
}
});
// 未配置域名,则不需要处理域名映射逻辑
if (StrUtil.isBlank(proxyConfig.getServer().getTcp().getDomainName())) {
return;
}
List<PortMappingDO> portMappingDOList = portMappingMapper.selectList(new LambdaQueryWrapper<PortMappingDO>().eq(PortMappingDO::getProtocal, NetworkProtocolEnum.HTTP.getDesc()).isNotNull(PortMappingDO::getSubdomain));
List<PortMappingDO> portMappingDOList = allMappingDOList.stream()
.filter(item -> NetworkProtocolEnum.HTTP.getDesc().equals(item.getProtocal()) && item.getSubdomain() != null)
.collect(Collectors.toList());
// List<PortMappingDO> portMappingDOList = portMappingMapper.selectList(new LambdaQueryWrapper<PortMappingDO>().eq(PortMappingDO::getProtocal, NetworkProtocolEnum.HTTP.getDesc()).isNotNull(PortMappingDO::getSubdomain));
if (CollectionUtil.isEmpty(portMappingDOList)) {
return;
}
@@ -315,9 +359,18 @@ public class PortMappingService implements LifecycleBean {
return;
}
ProxyUtil.setSubdomainToServerPort(item.getSubdomain(), item.getServerPort());
});
}
private void updateMappingPortToSecurityGroupMap(Integer serverPort, Integer securityGroupId) {
if (securityGroupId == null || securityGroupId == 0) {
mappingPortToSecurityGroupMap.remove(serverPort);
return;
}
mappingPortToSecurityGroupMap.put(serverPort, securityGroupId);
}
@Override
public void start() throws Throwable {
@@ -0,0 +1,211 @@
package org.dromara.neutrinoproxy.server.service;
import cn.hutool.cache.Cache;
import cn.hutool.cache.CacheUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jdk.jshell.Snippet;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.solon.annotation.Db;
import org.dromara.neutrinoproxy.server.base.rest.SystemContextHolder;
import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum;
import org.dromara.neutrinoproxy.server.constant.SecurityRulePassTypeEnum;
import org.dromara.neutrinoproxy.server.controller.req.system.SecurityGroupCreateReq;
import org.dromara.neutrinoproxy.server.controller.req.system.SecurityGroupUpdateReq;
import org.dromara.neutrinoproxy.server.controller.req.system.SecurityRuleCreateReq;
import org.dromara.neutrinoproxy.server.controller.req.system.SecurityRuleUpdateReq;
import org.dromara.neutrinoproxy.server.dal.SecurityGroupMapper;
import org.dromara.neutrinoproxy.server.dal.SecurityRuleMapper;
import org.dromara.neutrinoproxy.server.dal.entity.SecurityGroupDO;
import org.dromara.neutrinoproxy.server.dal.entity.SecurityRuleDO;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Init;
import org.noear.solon.core.runtime.NativeDetector;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@Slf4j
public class SecurityGroupService {
@Db
private SecurityGroupMapper securityGroupMapper;
@Db
private SecurityRuleMapper securityRuleMapper;
private final Map<Integer, SecurityGroupDO> securityGroupMap = new ConcurrentHashMap<>();
// 允许通过控制的缓存,缓存类型最近最久未使用缓存,容量100,超时时间5分钟
private final Cache<String, Boolean> ipAllowControlCache = CacheUtil.newLRUCache(100, 1000 * 60 * 5);
@Init(index = 100)
public synchronized void init() {
securityGroupMap.clear();
List<SecurityGroupDO> groupDOList = securityGroupMapper.selectList(Wrappers.lambdaQuery(SecurityGroupDO.class)
.eq(SecurityGroupDO::getEnable, EnableStatusEnum.ENABLE));
groupDOList.forEach(securityGroupDO -> securityGroupMap.put(securityGroupDO.getId(), securityGroupDO));
ipAllowControlCache.clear();
}
public void clearCache() {
ipAllowControlCache.clear();
}
public List<SecurityGroupDO> queryGroupList() {
return securityGroupMapper.selectList(Wrappers.lambdaQuery(SecurityGroupDO.class)
.eq(SecurityGroupDO::getUserId, SystemContextHolder.getUserId()));
}
public SecurityGroupDO queryGroupOne(Integer groupId) {
return securityGroupMapper.selectById(groupId);
}
public void createGroup(SecurityGroupCreateReq req) {
SecurityGroupDO groupDO = new SecurityGroupDO();
BeanUtil.copyProperties(req, groupDO);
groupDO.setEnable(EnableStatusEnum.ENABLE)
.setUserId(SystemContextHolder.getUserId())
.setCreateTime(new Date())
.setUpdateTime(new Date());
securityGroupMapper.insert(groupDO);
init();
}
/**
* 更新时不允许更新默认放行类型
* @param req 安全组更新参数
*/
public void updateGroup(SecurityGroupUpdateReq req) {
SecurityGroupDO groupDO = securityGroupMapper.selectById(req.getId());
BeanUtil.copyProperties(req, groupDO, "defaultPassType");
securityGroupMapper.updateById(groupDO);
init();
}
public void setGroupStatus(Integer groupId, EnableStatusEnum statusEnum) {
SecurityGroupDO groupDO = securityGroupMapper.selectById(groupId);
if (groupDO == null) {
throw new RuntimeException("指定的安全组不存在");
}
groupDO.setEnable(statusEnum);
securityGroupMapper.updateById(groupDO);
init();
}
/**
* 删除安全组,并级联删除安全组下的规则,删除后,需缓存
* @param groupId 安全组Id
*/
public void deleteGroup(Integer groupId) {
securityGroupMapper.deleteById(groupId);
securityRuleMapper.delete(Wrappers.lambdaQuery(SecurityRuleDO.class)
.eq(SecurityRuleDO::getGroupId, groupId));
init();
}
public List<SecurityRuleDO> queryRuleListByGroupId(Integer groupId) {
return securityRuleMapper.selectList(Wrappers.lambdaQuery(SecurityRuleDO.class)
.eq(SecurityRuleDO::getGroupId, groupId)
.orderByAsc(SecurityRuleDO::getPriority)
);
}
public void createRule(SecurityRuleCreateReq req) {
SecurityRuleDO ruleDO = new SecurityRuleDO();
BeanUtil.copyProperties(req, ruleDO);
ruleDO.setUserId(SystemContextHolder.getUserId())
.setCreateTime(new Date())
.setEnable(EnableStatusEnum.ENABLE)
.setUpdateTime(new Date());
securityRuleMapper.insert(ruleDO);
clearCache();
}
public void updateRule(SecurityRuleUpdateReq req) {
SecurityRuleDO ruleDO = securityRuleMapper.selectById(req.getId());
BeanUtil.copyProperties(req, ruleDO);
securityRuleMapper.updateById(ruleDO);
clearCache();
}
public void deleteRule(Integer ruleId) {
securityRuleMapper.deleteById(ruleId);
clearCache();
}
public void setRuleStatus(Integer ruleId, EnableStatusEnum statusEnum) {
SecurityRuleDO ruleDO = securityRuleMapper.selectById(ruleId);
ruleDO.setEnable(statusEnum);
ruleDO.setUpdateTime(new Date());
securityRuleMapper.updateById(ruleDO);
clearCache();
}
/**
* 判断ip在该安全组下是否允许,如果安全组没有创建,则放行,默认黑名单规则
* @param ip 被判断的IP地址
* @param groupId 安全组Id
* @return 是否放行
*/
public boolean judgeAllow(String ip, Integer groupId) {
ip = ip.toLowerCase();
// 不能判断当前连接的IP,保守处理,拒绝放行
if (StrUtil.isEmpty(ip)) {
log.debug("【安全组】不能正确获取到IP地址,保守处理,拒绝放行");
return false;
}
// 黑名单规则,没有该安全组,则放行
if (groupId == null) {
log.debug("【安全组】{}:该IP访问的端口映射没有绑定安全组(1), 放行", ip);
return true;
}
SecurityGroupDO groupDO = securityGroupMap.get(groupId);
if (groupDO == null) {
log.debug("【安全组】{}:该IP访问的端口映射没有绑定安全组(2), 放行", ip);
return true;
}
Boolean allow = null;
String judgeAllowMapKey = ip + groupId;
if (ipAllowControlCache.containsKey(judgeAllowMapKey)) {
allow = ipAllowControlCache.get(judgeAllowMapKey);
log.debug("【安全组】{}-安全组{}:该IP在缓存中,缓存策略为{}", ip, groupId, allow ? "允许" : "拒绝");
return allow;
}
List<SecurityRuleDO> ruleDOList = securityRuleMapper.selectList(Wrappers.lambdaQuery(SecurityRuleDO.class)
.eq(SecurityRuleDO::getGroupId, groupId)
.eq(SecurityRuleDO::getEnable, EnableStatusEnum.ENABLE)
.orderByAsc(SecurityRuleDO::getPriority)
);
for (SecurityRuleDO ruleDO : ruleDOList) {
SecurityRulePassTypeEnum passType = ruleDO.judge(ip);
if (passType == SecurityRulePassTypeEnum.ALLOW) {
allow = true;
log.debug("【安全组】{}-安全组{}:匹配到安全规则{}行为:{}", ip, groupId, ruleDO.getId(), "允许");
break;
}
if (passType == SecurityRulePassTypeEnum.DENY) {
allow = false;
log.info("【安全组】{}-安全组{}:匹配到安全规则{}行为:{}", ip, groupId, ruleDO.getId(), "拒绝");
break;
}
}
// 当前IP没有匹配到任何一条规则,则使用安全组默认规则
if (allow == null) {
allow = groupDO.getDefaultPassType() == SecurityRulePassTypeEnum.ALLOW;
log.debug("【安全组】{}-安全组{}:使用安全组默认放行类型:{}", ip, groupId, allow ? "允许" : "拒绝");
}
ipAllowControlCache.put(judgeAllowMapKey, allow);
return allow;
}
}
@@ -38,15 +38,15 @@ public class ProxyUtil {
/**
* 服务端口 -> 指令通道映射
*/
private static final Map<Integer, Channel> serverPortToCmdChannelMap = new ConcurrentHashMap<>();
private static Map<Integer, Channel> serverPortToCmdChannelMap = new ConcurrentHashMap<>();
/**
* license -> 指令通道映射
*/
private static final Map<Integer, Channel> licenseToCmdChannelMap = new ConcurrentHashMap<>();
private static Map<Integer, Channel> licenseToCmdChannelMap = new ConcurrentHashMap<>();
/**
* 服务端口 -> 访问通道映射
*/
private static final Map<Integer, Channel> serverPortToVisitorChannel = new ConcurrentHashMap<>();
private static Map<Integer, Channel> serverPortToVisitorChannel = new ConcurrentHashMap<>();
/**
* cmdChannelAttachInfo.getUserChannelMap() 读写锁
@@ -55,21 +55,19 @@ public class ProxyUtil {
/**
* 访问者ID生成器
*/
private static final AtomicLong visitorIdProducer = new AtomicLong(0);
private static AtomicLong visitorIdProducer = new AtomicLong(0);
/**
* 代理 - connect附加映射
*/
private static final Map<String, ProxyAttachment> proxyConnectAttachmentMap = new HashMap<>();
private static Map<String, ProxyAttachment> proxyConnectAttachmentMap = new HashMap<>();
/**
* 子域名 - 服务端端口映射
*/
private static final Map<String, Integer> subdomainToServerPort = new HashMap<>();
private static Map<String, Integer> subdomainToServerPort = new HashMap<>();
/**
* licenseId - 客户端Id映射
*/
private static final Map<Integer, String> licenseIdToClientIdMap = new HashMap<>();
private static final Map<Integer, byte[]> licenseIdToSecureKeyMap = new ConcurrentHashMap<>();
private static Map<Integer, String> licenseIdToClientIdMap = new HashMap<>();
/**
* 初始化代理信息
@@ -423,22 +421,4 @@ public class ProxyUtil {
public static void removeClientIdByLicenseId(Integer licenseId) {
licenseIdToClientIdMap.remove(licenseId);
}
public static void setSecureKey(Integer licenseId, byte[] key) {
licenseIdToSecureKeyMap.put(licenseId, key);
}
public static void setLicenseIdRelativeProxyChannelSecurity(Integer licenseId) {
Set<Integer> portSet = licenseToServerPortMap.get(licenseId);
for(Integer port : portSet) {
// TODO 代理客户端
}
}
public static void setChannelSecurity(Integer licenseId, Channel channel) {
if (channel != null && licenseIdToSecureKeyMap.containsKey(licenseId)) {
channel.attr(Constants.IS_SECURITY).set(true);
channel.attr(Constants.SECURE_KEY).set(licenseIdToSecureKeyMap.get(licenseId));
}
}
}
@@ -2,9 +2,13 @@ server:
# 服务端web端口,用于支持HTTP接口,管理后台页面访问
port: 8888
# 日志级别
solon.logging.logger:
"root":
level: info
solon.logging:
logger:
"root":
level: info
appender:
file:
level: info
neutrino:
proxy:
@@ -24,7 +24,7 @@ solon.logging.logger:
neutrino:
proxy:
protocol:
max-frame-length: ${MAX_FRAME_LENGTH:1048576000}
max-frame-length: ${MAX_FRAME_LENGTH:2097152}
length-field-offset: 0
length-field-length: 4
initial-bytes-to-strip: 0
@@ -45,6 +45,37 @@ CREATE TABLE IF NOT EXISTS `port_group` (
`create_time` TIMESTAMP NOT NULL,
`update_time` TIMESTAMP NOT NULL
);
#
CREATE TABLE IF NOT EXISTS `security_group` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(20) NOT NULL,
`description` VARCHAR(255),
`user_id` INTEGER NOT NULL,
`enable` VARCHAR(20) NOT NULL,
`default_pass_type` VARCHAR(20) NOT NULL,
`create_time` TIMESTAMP NOT NULL,
`update_time` TIMESTAMP NOT NULL,
PRIMARY KEY (`id`)
);
#
CREATE TABLE IF NOT EXISTS `security_rule` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`group_id` INTEGER NOT NULL,
`name` VARCHAR(20) NOT NULL,
`description` VARCHAR(255) NOT NULL,
`rule` text NOT NULL,
`pass_type` VARCHAR(20) NOT NULL,
`priority` INTEGER NOT NULL,
`user_id` INTEGER NOT NULL,
`enable` VARCHAR(20) NOT NULL,
`create_time` TIMESTAMP NOT NULL,
`update_time` TIMESTAMP NOT NULL,
PRIMARY KEY (`id`)
);
CREATE INDEX IF NOT EXISTS I_security_rule_group_id ON security_rule(group_id);
##########################################################
#license表
CREATE TABLE IF NOT EXISTS `license` (
@@ -73,6 +104,7 @@ CREATE TABLE IF NOT EXISTS `port_mapping` (
`proxy_responses` INTEGER(20) NOT NULL DEFAULT 0,
`proxy_timeout_ms` INTEGER(20) NOT NULL DEFAULT 0,
`enable` INTEGER(2) NOT NULL,
`security_group_id` INTEGER(20) NOT NULL DEFAULT 0,
`create_time` TIMESTAMP NOT NULL,
`update_time` TIMESTAMP NOT NULL
);
@@ -0,0 +1,30 @@
#
CREATE TABLE IF NOT EXISTS `security_group` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(20) NOT NULL,
`description` VARCHAR(255),
`user_id` INTEGER NOT NULL,
`enable` VARCHAR(20) NOT NULL,
`default_pass_type` VARCHAR(20) NOT NULL,
`create_time` TIMESTAMP NOT NULL,
`update_time` TIMESTAMP NOT NULL,
PRIMARY KEY (`id`)
);
#
CREATE TABLE IF NOT EXISTS `security_rule` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`group_id` INTEGER NOT NULL,
`name` VARCHAR(20) NOT NULL,
`description` VARCHAR(255) NOT NULL,
`rule` text NOT NULL,
`pass_type` VARCHAR(20) NOT NULL,
`priority` INTEGER NOT NULL,
`user_id` INTEGER NOT NULL,
`enable` VARCHAR(20) NOT NULL,
`create_time` TIMESTAMP NOT NULL,
`update_time` TIMESTAMP NOT NULL,
PRIMARY KEY (`id`)
);
CREATE INDEX IF NOT EXISTS I_security_rule_group_id ON security_rule(group_id);
ALTER TABLE `port_mapping` ADD COLUMN IF NOT EXISTS `security_group_id` INTEGER DEFAULT 0;
@@ -50,6 +50,36 @@ CREATE TABLE IF NOT EXISTS `port_group` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
#
CREATE TABLE IF NOT EXISTS `security_group` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(20) NOT NULL COMMENT '安全组名称',
`description` varchar(255) COMMENT '安全组描述',
`user_id` int NOT NULL COMMENT '用户ID',
`enable` varchar(20) NOT NULL COMMENT '启用状态',
`default_pass_type` varchar(20) NOT NULL COMMENT '默认放行类型',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
`update_time` datetime(3) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
#
CREATE TABLE IF NOT EXISTS `security_rule` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` int NOT NULL COMMENT '关联安全组',
`name` varchar(20) NOT NULL COMMENT '规则名称',
`description` varchar(255) NOT NULL COMMENT '规则描述',
`rule` text NOT NULL COMMENT '规则内容',
`pass_type` varchar(20) NOT NULL COMMENT '放行类型',
`priority` int(1) NOT NULL COMMENT '优先级',
`user_id` int NOT NULL COMMENT '用户ID',
`enable` varchar(20) NOT NULL COMMENT '启用状态',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
`update_time` datetime(3) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `I_security_rule_group_id_priority` (`group_id`, `priority`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
##########################################################
#license表
CREATE TABLE IF NOT EXISTS `license` (
@@ -79,6 +109,7 @@ CREATE TABLE IF NOT EXISTS `port_mapping` (
`proxy_responses` int NOT NULL DEFAULT 0 COMMENT '代理响应数据包数量',
`proxy_timeout_ms` int NOT NULL DEFAULT 0 COMMENT '代理超时毫秒数',
`enable` int NOT NULL COMMENT '是否启用(1、启用 2、禁用)',
`security_group_id` int DEFAULT 0 COMMENT '安全组Id',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
`update_time` datetime(3) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
@@ -145,8 +176,8 @@ 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 '读取流量',
`write_bytes` bigint NOT NULL COMMENT '写入流量',
`read_bytes` bigint 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 '创建时间',
@@ -162,8 +193,8 @@ 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 '读取流量',
`write_bytes` bigint NOT NULL COMMENT '写入流量',
`read_bytes` bigint 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 '创建时间',
@@ -179,8 +210,8 @@ 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 '读取流量',
`write_bytes` bigint NOT NULL COMMENT '写入流量',
`read_bytes` bigint 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 '创建时间',
@@ -196,8 +227,8 @@ 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 '读取流量',
`write_bytes` bigint NOT NULL COMMENT '写入流量',
`read_bytes` bigint 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 '创建时间',
@@ -50,6 +50,36 @@ CREATE TABLE IF NOT EXISTS `port_group` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
#
CREATE TABLE IF NOT EXISTS `security_group` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(20) NOT NULL COMMENT '安全组名称',
`description` varchar(255) COMMENT '安全组描述',
`user_id` int NOT NULL COMMENT '用户ID',
`enable` varchar(20) NOT NULL COMMENT '启用状态',
`default_pass_type` varchar(20) NOT NULL COMMENT '默认放行类型',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
`update_time` datetime(3) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
#
CREATE TABLE IF NOT EXISTS `security_rule` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` int NOT NULL COMMENT '关联安全组',
`name` varchar(20) NOT NULL COMMENT '规则名称',
`description` varchar(255) NOT NULL COMMENT '规则描述',
`rule` text NOT NULL COMMENT '规则内容',
`pass_type` varchar(20) NOT NULL COMMENT '放行类型',
`priority` int(1) NOT NULL COMMENT '优先级',
`user_id` int NOT NULL COMMENT '用户ID',
`enable` varchar(20) NOT NULL COMMENT '启用状态',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
`update_time` datetime(3) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `I_security_rule_group_id_priority` (`group_id`, `priority`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
##########################################################
#license表
CREATE TABLE IF NOT EXISTS `license` (
@@ -79,6 +109,7 @@ CREATE TABLE IF NOT EXISTS `port_mapping` (
`proxy_responses` int NOT NULL DEFAULT 0 COMMENT '代理响应数据包数量',
`proxy_timeout_ms` int NOT NULL DEFAULT 0 COMMENT '代理超时毫秒数',
`enable` int NOT NULL COMMENT '是否启用(1、启用 2、禁用)',
`security_group_id` int DEFAULT 0 COMMENT '安全组Id',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
`update_time` datetime(3) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
@@ -145,8 +176,8 @@ 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 '读取流量',
`write_bytes` bigint NOT NULL COMMENT '写入流量',
`read_bytes` bigint 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 '创建时间',
@@ -162,8 +193,8 @@ 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 '读取流量',
`write_bytes` bigint NOT NULL COMMENT '写入流量',
`read_bytes` bigint 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 '创建时间',
@@ -179,8 +210,8 @@ 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 '读取流量',
`write_bytes` bigint NOT NULL COMMENT '写入流量',
`read_bytes` bigint 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 '创建时间',
@@ -196,8 +227,8 @@ 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 '读取流量',
`write_bytes` bigint NOT NULL COMMENT '写入流量',
`read_bytes` bigint 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 '创建时间',
@@ -207,3 +238,4 @@ CREATE TABLE IF NOT EXISTS `flow_report_month` (
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,8 @@
ALTER TABLE `flow_report_minute` MODIFY COLUMN `read_bytes` BIGINT NOT NULL COMMENT '读取流量';
ALTER TABLE `flow_report_minute` MODIFY COLUMN `write_bytes` BIGINT NOT NULL COMMENT '写入流量';
ALTER TABLE `flow_report_hour` MODIFY COLUMN `read_bytes` BIGINT NOT NULL COMMENT '读取流量';
ALTER TABLE `flow_report_hour` MODIFY COLUMN `write_bytes` BIGINT NOT NULL COMMENT '写入流量';
ALTER TABLE `flow_report_day` MODIFY COLUMN `read_bytes` BIGINT NOT NULL COMMENT '读取流量';
ALTER TABLE `flow_report_day` MODIFY COLUMN `write_bytes` BIGINT NOT NULL COMMENT '写入流量';
ALTER TABLE `flow_report_month` MODIFY COLUMN `read_bytes` BIGINT NOT NULL COMMENT '读取流量';
ALTER TABLE `flow_report_month` MODIFY COLUMN `write_bytes` BIGINT NOT NULL COMMENT '写入流量';
@@ -0,0 +1,32 @@
#
CREATE TABLE IF NOT EXISTS `security_group` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(20) NOT NULL COMMENT '安全组名称',
`description` varchar(255) COMMENT '安全组描述',
`user_id` int NOT NULL COMMENT '用户ID',
`enable` varchar(20) NOT NULL COMMENT '启用状态',
`default_pass_type` varchar(20) NOT NULL COMMENT '默认放行类型',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
`update_time` datetime(3) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
#
CREATE TABLE IF NOT EXISTS `security_rule` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` int NOT NULL COMMENT '关联安全组',
`name` varchar(20) NOT NULL COMMENT '规则名称',
`description` varchar(255) NOT NULL COMMENT '规则描述',
`rule` text NOT NULL COMMENT '规则内容',
`pass_type` varchar(20) NOT NULL COMMENT '放行类型',
`priority` int(1) NOT NULL COMMENT '优先级',
`user_id` int NOT NULL COMMENT '用户ID',
`enable` varchar(20) NOT NULL COMMENT '启用状态',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
`update_time` datetime(3) NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `I_security_rule_group_id_priority` (`group_id`, `priority`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
# port_mapping表增加字段
ALTER TABLE `port_mapping` ADD COLUMN `security_group_id` int DEFAULT 0 COMMENT '安全组Id';
Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

@@ -29,7 +29,7 @@ neutrino:
db:
type: mysql
# 自己的数据库实例,创建一个空的名为'neutrino-proxy'的数据库即可,首次启动服务端会自动初始化
url: jdbc:mysql://xxxx:3306/neutrino-proxy?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useAffectedRows=true&useSSL=false
url: jdbc:mysql://xxxx:3306/neutrino-proxy?useUnicode=true&allowMultiQueries=true&useAffectedRows=true&useSSL=false
driver-class: com.mysql.jdbc.Driver
# 数据库帐号
username: xxx
@@ -6,11 +6,15 @@ permalink: /pages/cded59/
| 日期 | 渠道 | 金额 |昵称| 备注 |
|:-----------|:---|:-----|:-|:-----------------|
|2023-11-01|微信红包|20|Sun|感谢大佬,请你喝杯奶茶[微笑]|
|2023-10-30|微信赞赏|50|杨娃娃||
|2023-10-16|微信转账|50|Mark Isaac赵方丈|金额不大,开源感恩。|
|2023-10-11|微信红包|50|喜鸽小宝||
|2023-10-10|微信红包|20|海洋||
|2023-11-30|Gitee捐助|10|26263|感谢您的开源项目!|
|2023-11-30|微信红包|10|姫野永遠||
| 2023-11-26 |微信转账|180|Teacher Du||
| 2023-11-10 |微信红包|50|喜鸽小宝||
| 2023-11-01 |微信红包|20|Sun|感谢大佬,请你喝杯奶茶[微笑]|
| 2023-10-30 |微信赞赏|50|杨娃娃||
| 2023-10-16 |微信转账|50|Mark Isaac赵方丈|金额不大,开源感恩。|
| 2023-10-11 |微信红包|50|喜鸽小宝||
| 2023-10-10 |微信红包|20|海洋||
| 2023-07-31 |Gitee捐助| 50 |失败女神| 感谢您的开源项目! |
| 2023-07-28 |微信转账| 50 |AdrianPteLtd.com-咨询| |
| 2023-07-12 |微信红包| 100 |MaxKeyTop| 请大佬抽包烟 |
+6 -2
View File
@@ -146,9 +146,13 @@ postList: none
<div class="cardListContainer">
<div class="card-list">
<a href="https://www.xigexb.com" target="_blank" class="card-item row-3" style="background-color:#102863;--random-color:#102863;color:#FFFFFF;">
<a href="https://www.xigexb.com?from=neutrino-proxy" target="_blank" class="card-item row-3" style="background-color:#102863;--random-color:#102863;color:#FFFFFF;">
<img src="img/sponsor/xigexiaobao.jpg" class="no-zoom">
<div><p class="name">喜鸽小宝</p> <p class="desc">一个爱好写代码的同学</p></div>
<div><p class="name">喜鸽小宝</p> <p class="desc">一个爱好写代码的同学</p></div>
</a>
<a href="https://7bu.top?from=neutrino-proxy" target="_blank" class="card-item row-3" style="background-color:#c6ddff;--random-color:#c6ddff;color:#000000;">
<img src="img/sponsor/7bu.ico" class="no-zoom">
<div><p class="name">去不图床</p> <p class="desc">杜老师说旗下付费图片外链平台。</p></div>
</a>
</div>
</div>
+262 -1
View File
@@ -1 +1,262 @@
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.noear</groupId> <artifactId>solon-parent</artifactId> <version>2.5.12</version> <relativePath /> </parent> <groupId>org.dromara.neutrino-proxy</groupId> <artifactId>neutrino-proxy</artifactId> <packaging>pom</packaging> <version>${revision}</version> <modules> <module>neutrino-proxy-core</module> <module>neutrino-proxy-client</module> <module>neutrino-proxy-server</module> </modules> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <revision>2.0.1-SNAPSHOT</revision> <native.version>0.9.28</native.version> <java.version>21</java.version> <maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version> <maven-flatten.version>1.1.0</maven-flatten.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.100.Final</version> </dependency> <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>1.33</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.9</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>28.0-jre</version> </dependency> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>2.2.224</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</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> <version>4.0.3</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15to18</artifactId> <version>1.69</version> </dependency> <dependency> <groupId>org.dromara.solon-plugins</groupId> <artifactId>job-solon-plugin</artifactId> <version>0.1.1</version> <exclusions> <exclusion> <groupId>cn.hutool</groupId> <artifactId>hutool-core</artifactId> </exclusion> </exclusions> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.noear</groupId> <artifactId>solon.logging.logback</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </dependency> <dependency> <groupId>org.noear</groupId> <artifactId>solon-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <resources> <resource> <directory>src/main/resources</directory> </resource> </resources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven-compiler-plugin.version}</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> <encoding>UTF-8</encoding> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> <!-- 添加flatten-maven-plugin插件 --> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>flatten-maven-plugin</artifactId> <version>${maven-flatten.version}</version> <configuration> <updatePomFile>true</updatePomFile> <flattenMode>resolveCiFriendliesOnly</flattenMode> </configuration> <executions> <execution> <id>flatten</id> <phase>process-resources</phase> <goals> <goal>flatten</goal> </goals> </execution> <execution> <id>flatten.clean</id> <phase>clean</phase> <goals> <goal>clean</goal> </goals> </execution> </executions> </plugin> </plugins> </build> <repositories> <repository> <id>tencent</id> <url>https://mirrors.cloud.tencent.com/nexus/repository/maven-public/</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> <repository> <id>sonatype-nexus-snapshots</id> <name>Sonatype Nexus Snapshots</name> <url>https://oss.sonatype.org/content/repositories/snapshots</url> <releases> <enabled>false</enabled> </releases> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>sonatype-nexus-snapshots</id> <name>Sonatype Nexus Snapshots</name> <url>https://oss.sonatype.org/content/repositories/snapshots</url> <releases> <enabled>false</enabled> </releases> </pluginRepository> </pluginRepositories> <profiles> <profile> <id>native</id> <build> <plugins> <plugin> <groupId>org.noear</groupId> <artifactId>solon-maven-plugin</artifactId> <version>${solon.version}</version> <executions> <execution> <id>process-aot</id> <goals> <goal>process-aot</goal> </goals> </execution> </executions> <dependencies> <dependency> <groupId>org.codehaus.plexus</groupId> <artifactId>plexus-utils</artifactId> <version>3.5.1</version> </dependency> </dependencies> </plugin> <plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> <version>${native.version}</version> <!-- 使用graalvm提供的可达性元数据,很多第三方库就直接可以构建成可执行文件了 --> <configuration> <metadataRepository> <enabled>true</enabled> </metadataRepository> </configuration> <executions> <execution> <id>add-reachability-metadata</id> <goals> <goal>add-reachability-metadata</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </profile> </profiles> </project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.noear</groupId>
<artifactId>solon-parent</artifactId>
<version>2.5.12</version>
<relativePath />
</parent>
<groupId>org.dromara.neutrino-proxy</groupId>
<artifactId>neutrino-proxy</artifactId>
<packaging>pom</packaging>
<version>${revision}</version>
<modules>
<module>neutrino-proxy-core</module>
<module>neutrino-proxy-client</module>
<module>neutrino-proxy-server</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<revision>2.0.1-SNAPSHOT</revision>
<native.version>0.9.28</native.version>
<java.version>21</java.version>
<maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
<maven-flatten.version>1.1.0</maven-flatten.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.100.Final</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.33</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</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>
<version>4.0.3</version>
</dependency>
<dependency>
<groupId>org.dromara.solon-plugins</groupId>
<artifactId>job-solon-plugin</artifactId>
<version>0.1.1</version>
<exclusions>
<exclusion>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon.logging.logback</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- 添加flatten-maven-plugin插件 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>${maven-flatten.version}</version>
<configuration>
<updatePomFile>true</updatePomFile>
<flattenMode>resolveCiFriendliesOnly</flattenMode>
</configuration>
<executions>
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten.clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>tencent</id>
<url>https://mirrors.cloud.tencent.com/nexus/repository/maven-public/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>sonatype-nexus-snapshots</id>
<name>Sonatype Nexus Snapshots</name>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>sonatype-nexus-snapshots</id>
<name>Sonatype Nexus Snapshots</name>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.noear</groupId>
<artifactId>solon-maven-plugin</artifactId>
<version>${solon.version}</version>
<executions>
<execution>
<id>process-aot</id>
<goals>
<goal>process-aot</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-utils</artifactId>
<version>3.5.1</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native.version}</version>
<!-- 使用graalvm提供的可达性元数据,很多第三方库就直接可以构建成可执行文件了 -->
<configuration>
<metadataRepository>
<enabled>true</enabled>
</metadataRepository>
</configuration>
<executions>
<execution>
<id>add-reachability-metadata</id>
<goals>
<goal>add-reachability-metadata</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>