新增限速功能

This commit is contained in:
aoshiguchen
2023-12-15 17:09:16 +08:00
parent 99c79f612a
commit 5db0bf94cb
34 changed files with 538 additions and 64 deletions
@@ -36,6 +36,11 @@
<span>{{scope.row.key}}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('限速')" width="100">
<template slot-scope="scope">
<span>{{scope.row.upLimitRate ? scope.row.upLimitRate : '--'}} / {{scope.row.downLimitRate ? scope.row.downLimitRate : '--'}}</span>
</template>
</el-table-column>
<el-table-column width="150px" align="center" :label="$t('table.createTime')">
<template slot-scope="scope">
<span>{{scope.row.createTime | parseTime('{y}-{m}-{d} {h}:{i}')}}</span>
@@ -85,6 +90,12 @@
<el-form-item :label="$t('License名称')" prop="name">
<el-input v-model="temp.name"></el-input>
</el-form-item>
<el-form-item :label="$t('上传限速')" prop="upLimitRate">
<el-input v-model="temp.upLimitRate" placeholder="如:10240B、500K、1M"></el-input>
</el-form-item>
<el-form-item :label="$t('下载限速')" prop="downLimitRate">
<el-input v-model="temp.downLimitRate" placeholder="如:10240B、500K、1M"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">{{$t('table.cancel')}}</el-button>
@@ -39,17 +39,17 @@
<span>{{ scope.row.userName }}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.licenseName')" width="130">
<el-table-column align="center" :label="$t('table.licenseName')" width="120">
<template slot-scope="scope">
<span>{{ scope.row.licenseName }}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.protocalName')" width="100">
<el-table-column align="center" :label="$t('table.protocalName')" width="80">
<template slot-scope="scope">
<span>{{ scope.row.protocal }}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('table.domainName')" width="200">
<el-table-column align="center" :label="$t('table.domainName')" width="180">
<template slot-scope="scope">
<span>{{ scope.row.domain }}</span>
</template>
@@ -64,17 +64,22 @@
<span>{{ scope.row.clientIp }}:{{ scope.row.clientPort }}</span>
</template>
</el-table-column>
<el-table-column align="center" :label="$t('限速')" width="100">
<template slot-scope="scope">
<span>{{scope.row.upLimitRate ? scope.row.upLimitRate : '--'}} / {{scope.row.downLimitRate ? scope.row.downLimitRate : '--'}}</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 width="150px" align="center" :label="$t('table.createTime')">
<template slot-scope="scope">
<span>{{ scope.row.createTime | parseTime('{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<!-- <el-table-column width="150px" align="center" :label="$t('table.createTime')">-->
<!-- <template slot-scope="scope">-->
<!-- <span>{{ scope.row.createTime | parseTime('{y}-{m}-{d} {h}:{i}') }}</span>-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column width="150px" align="center" :label="$t('table.updateTime')">
<template slot-scope="scope">
<span>{{ scope.row.updateTime | parseTime('{y}-{m}-{d} {h}:{i}') }}</span>
@@ -181,6 +186,12 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('上传限速')" prop="upLimitRate">
<el-input v-model="temp.upLimitRate" placeholder="如:10240B、500K、1M"></el-input>
</el-form-item>
<el-form-item :label="$t('下载限速')" prop="downLimitRate">
<el-input v-model="temp.downLimitRate" placeholder="如:10240B、500K、1M"></el-input>
</el-form-item>
<el-form-item :label="$t('描述')" prop="description">
<el-input v-model="temp.description"></el-input>
</el-form-item>
@@ -51,6 +51,8 @@ public interface Constants {
AttributeKey<Integer> SERVER_PORT = AttributeKey.newInstance("serverPort");
AttributeKey<Boolean> FLOW_LIMITER_FLAG = AttributeKey.newInstance("flowLimiterFlag");
int HEADER_SIZE = 4;
int TYPE_SIZE = 1;
@@ -2,7 +2,6 @@ package org.dromara.neutrinoproxy.server;
import lombok.extern.slf4j.Slf4j;
import org.dromara.solonplugins.job.annotation.EnableJob;
import org.noear.snack.core.utils.StringUtil;
import org.noear.solon.Solon;
import org.noear.solon.Utils;
import org.noear.solon.annotation.SolonMain;
@@ -22,6 +22,7 @@ import org.dromara.neutrinoproxy.server.proxy.core.TcpVisitorChannelHandler;
import org.dromara.neutrinoproxy.server.proxy.core.UdpVisitorChannelHandler;
import org.dromara.neutrinoproxy.server.proxy.security.TcpVisitorSecurityChannelHandler;
import org.dromara.neutrinoproxy.server.proxy.security.UdpVisitorSecurityChannelHandler;
import org.dromara.neutrinoproxy.server.proxy.security.VisitorFlowLimiterChannelHandler;
import org.noear.solon.Solon;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
@@ -76,7 +77,8 @@ public class ProxyConfiguration implements LifecycleBean {
}
ch.pipeline().addFirst(new BytesMetricsHandler());
// ch.pipeline().addLast(new ChannelTrafficShapingHandler(1024 * 1024 * 20, 1024 * 1024 * 20, 100, 20000));
ch.pipeline().addLast(new TcpVisitorSecurityChannelHandler());
ch.pipeline().addLast("security", new TcpVisitorSecurityChannelHandler());
ch.pipeline().addLast(new VisitorFlowLimiterChannelHandler());
ch.pipeline().addLast(new TcpVisitorChannelHandler());
}
});
@@ -114,7 +116,8 @@ public class ProxyConfiguration implements LifecycleBean {
if (null != proxyConfig.getServer().getUdp().getTransferLogEnable() && proxyConfig.getServer().getUdp().getTransferLogEnable()) {
ch.pipeline().addFirst(new LoggingHandler(UdpVisitorChannelHandler.class));
}
pipeline.addLast(udpServerWorkerGroup, new UdpVisitorSecurityChannelHandler());
pipeline.addLast(udpServerWorkerGroup, "security", new UdpVisitorSecurityChannelHandler());
ch.pipeline().addLast(new VisitorFlowLimiterChannelHandler());
pipeline.addLast(udpServerWorkerGroup, new UdpVisitorChannelHandler());
}
});
@@ -40,8 +40,8 @@ public enum ExceptionConstant {
NO_PERMISSION_VISIT(5, "当前用户无权访问该资源"),
PARAMS_NOT_NULL(10, "参数[{}]不能为空"),
PARAMS_NOT_EMPTY(11, "参数[{}]不能为空"),
FILED_LENGTH_OUT(12 ,"{}不能超出长度{}"),
BYTES_DESC_INVALID(13, "参数[{}]字节描述不合法"),
// 用户管理(11000)
// license管理(12000)
@@ -70,6 +70,8 @@ public class LicenseController {
ParamCheckUtil.checkNotNull(req, "req");
ParamCheckUtil.checkNotEmpty(req.getName(), "name");
ParamCheckUtil.checkNotNull(req.getUserId(), "userId");
ParamCheckUtil.checkBytesDesc(req.getUpLimitRate(), "upLimitRate");
ParamCheckUtil.checkBytesDesc(req.getDownLimitRate(), "downLimitRate");
return licenseService.create(req);
}
@@ -81,6 +83,8 @@ public class LicenseController {
ParamCheckUtil.checkNotNull(req, "req");
ParamCheckUtil.checkNotNull(req.getId(), "id");
ParamCheckUtil.checkNotEmpty(req.getName(), "name");
ParamCheckUtil.checkBytesDesc(req.getUpLimitRate(), "upLimitRate");
ParamCheckUtil.checkBytesDesc(req.getDownLimitRate(), "downLimitRate");
return licenseService.update(req);
}
@@ -43,6 +43,8 @@ public class PortMappingController {
ParamCheckUtil.checkNotNull(req.getClientPort(), "clientPort");
ParamCheckUtil.checkNotEmpty(req.getProtocal(), "protocal");
ParamCheckUtil.checkMaxLength(req.getDescription(), 50, "描述", "50");
ParamCheckUtil.checkBytesDesc(req.getUpLimitRate(), "upLimitRate");
ParamCheckUtil.checkBytesDesc(req.getDownLimitRate(), "downLimitRate");
if (StringUtils.isBlank(req.getClientIp())) {
// 没传客户端ip,默认为127.0.0.1
req.setClientIp("127.0.0.1");
@@ -60,6 +62,9 @@ public class PortMappingController {
if (null == req.getProxyTimeoutMs()) {
req.setProxyTimeoutMs(0L);
}
if (null == req.getSecurityGroupId()) {
req.setSecurityGroupId(0);
}
return portMappingService.create(req);
}
@@ -73,6 +78,8 @@ public class PortMappingController {
ParamCheckUtil.checkNotNull(req.getClientPort(), "clientPort");
ParamCheckUtil.checkNotEmpty(req.getProtocal(), "protocal");
ParamCheckUtil.checkMaxLength(req.getDescription(), 50, "描述", "50");
ParamCheckUtil.checkBytesDesc(req.getUpLimitRate(), "upLimitRate");
ParamCheckUtil.checkBytesDesc(req.getDownLimitRate(), "downLimitRate");
if (StringUtils.isBlank(req.getClientIp())) {
// 没传客户端ip,默认为127.0.0.1
req.setClientIp("127.0.0.1");
@@ -90,6 +97,9 @@ public class PortMappingController {
if (null == req.getProxyTimeoutMs()) {
req.setProxyTimeoutMs(0L);
}
if (null == req.getSecurityGroupId()) {
req.setSecurityGroupId(0);
}
portMappingService.update(req);
}
@@ -38,4 +38,12 @@ public class LicenseCreateReq {
* 用户ID
*/
private Integer userId;
/**
* 上传限速
*/
private String upLimitRate;
/**
* 下载限速
*/
private String downLimitRate;
}
@@ -38,4 +38,12 @@ public class LicenseUpdateReq {
* license名称
*/
private String name;
/**
* 上传限速
*/
private String upLimitRate;
/**
* 下载限速
*/
private String downLimitRate;
}
@@ -54,6 +54,14 @@ public class PortMappingCreateReq {
* 客户端端口
*/
private Integer clientPort;
/**
* 上传限速
*/
private String upLimitRate;
/**
* 下载限速
*/
private String downLimitRate;
/**
* 代理响应数量(响应数据包数量,如果没有拆包则等于数据条数)
*/
@@ -58,6 +58,14 @@ public class PortMappingUpdateReq {
* 客户端端口
*/
private Integer clientPort;
/**
* 上传限速
*/
private String upLimitRate;
/**
* 下载限速
*/
private String downLimitRate;
/**
* 代理响应数量(响应数据包数量,如果没有拆包则等于数据条数)
*/
@@ -32,6 +32,14 @@ public class LicenseListRes {
* 用户名
*/
private String userName;
/**
* 上传限速
*/
private String upLimitRate;
/**
* 下载限速
*/
private String downLimitRate;
/**
* 是否在线
* {@link OnlineStatusEnum}
@@ -75,6 +75,14 @@ public class PortMappingListRes {
* 客户端端口
*/
private Integer clientPort;
/**
* 上传限速
*/
private String upLimitRate;
/**
* 下载限速
*/
private String downLimitRate;
/**
* 客户端端口
*/
@@ -89,14 +89,6 @@ public interface LicenseMapper extends BaseMapper<LicenseDO> {
return this.selectById(id);
}
default void update(Integer id, String name, Date updateTime) {
this.update(null, new LambdaUpdateWrapper<LicenseDO>()
.eq(LicenseDO::getId, id)
.set(LicenseDO::getName, name)
.set(LicenseDO::getUpdateTime, updateTime)
);
}
default List<LicenseDO> findByIds(Set<Integer> ids) {
return selectBatchIds(ids);
}
@@ -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.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -59,6 +60,14 @@ public class LicenseDO {
* 用户ID
*/
private Integer userId;
/**
* 上传限速
*/
private String upLimitRate;
/**
* 下载限速
*/
private String downLimitRate;
/**
* 是否在线
* {@link OnlineStatusEnum}
@@ -80,14 +89,7 @@ public class LicenseDO {
public LicenseListRes toRes() {
LicenseListRes res = new LicenseListRes();
res.setId(id);
res.setName(name);
res.setKey(key);
res.setUserId(userId);
res.setIsOnline(isOnline);
res.setEnable(enable);
res.setCreateTime(createTime);
res.setUpdateTime(updateTime);
BeanUtil.copyProperties(this, res);
return res;
}
}
@@ -70,6 +70,14 @@ public class PortMappingDO {
* 客户端端口
*/
private Integer clientPort;
/**
* 上传限速
*/
private String upLimitRate;
/**
* 下载限速
*/
private String downLimitRate;
/**
* 描述
*/
@@ -12,6 +12,7 @@ import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig;
import org.dromara.neutrinoproxy.server.proxy.core.BytesMetricsHandler;
import org.dromara.neutrinoproxy.server.proxy.core.ProxyTunnelServer;
import org.dromara.neutrinoproxy.server.proxy.security.HttpVisitorSecurityChannelHandler;
import org.dromara.neutrinoproxy.server.proxy.security.VisitorFlowLimiterChannelHandler;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Inject;
import org.noear.solon.core.event.AppLoadEndEvent;
@@ -47,7 +48,8 @@ public class HttpProxy implements EventListener<AppLoadEndEvent> {
ch.pipeline().addFirst(new LoggingHandler(HttpProxy.class));
}
ch.pipeline().addFirst(new BytesMetricsHandler());
ch.pipeline().addLast(new HttpVisitorSecurityChannelHandler(proxyConfig.getServer().getTcp().getDomainName()));
ch.pipeline().addLast("security", new HttpVisitorSecurityChannelHandler(proxyConfig.getServer().getTcp().getDomainName()));
ch.pipeline().addLast(new VisitorFlowLimiterChannelHandler());
ch.pipeline().addLast(new HttpVisitorChannelHandler());
}
});
@@ -15,6 +15,7 @@ import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig;
import org.dromara.neutrinoproxy.server.proxy.core.BytesMetricsHandler;
import org.dromara.neutrinoproxy.server.proxy.core.ProxyTunnelServer;
import org.dromara.neutrinoproxy.server.proxy.security.HttpVisitorSecurityChannelHandler;
import org.dromara.neutrinoproxy.server.proxy.security.VisitorFlowLimiterChannelHandler;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Inject;
import org.noear.solon.core.event.AppLoadEndEvent;
@@ -56,7 +57,8 @@ public class HttpsProxy implements EventListener<AppLoadEndEvent> {
}
ch.pipeline().addLast(createSslHandler());
ch.pipeline().addFirst(new BytesMetricsHandler());
ch.pipeline().addLast(new HttpVisitorSecurityChannelHandler(proxyConfig.getServer().getTcp().getDomainName()));
ch.pipeline().addLast("security", new HttpVisitorSecurityChannelHandler(proxyConfig.getServer().getTcp().getDomainName()));
ch.pipeline().addLast(new VisitorFlowLimiterChannelHandler());
ch.pipeline().addLast(new HttpVisitorChannelHandler());
}
});
@@ -82,7 +82,8 @@ public class ProxyMessageConnectHandler implements ProxyMessageHandler {
ctx.channel().attr(Constants.LICENSE_ID).set(licenseDO.getId());
ctx.channel().attr(Constants.NEXT_CHANNEL).set(visitorChannel);
visitorChannel.attr(Constants.NEXT_CHANNEL).set(ctx.channel());
// 代理客户端与后端服务器连接成功,修改用户连接为可读状态
visitorChannel.attr(Constants.LICENSE_ID).set(licenseDO.getId());
// 代理客户端与后端服务器连接成功,修改用户连接为可读状态
visitorChannel.config().setOption(ChannelOption.AUTO_READ, true);
// 获取代理附加对象
@@ -66,7 +66,6 @@ public class HttpVisitorSecurityChannelHandler extends ChannelInboundHandlerAdap
ctx.channel().close();
return;
}
ctx.channel().attr(Constants.SERVER_PORT).set(serverPort);
// 判断IP是否在该端口绑定的安全组允许的规则内
String ip = IpUtil.getRealRemoteIp(httpContent);
@@ -80,6 +79,7 @@ public class HttpVisitorSecurityChannelHandler extends ChannelInboundHandlerAdap
}
// 继续传播
ctx.channel().attr(Constants.SERVER_PORT).set(serverPort);
buf.resetReaderIndex();
ctx.fireChannelRead(buf);
}
@@ -6,6 +6,7 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.neutrinoproxy.core.Constants;
import org.dromara.neutrinoproxy.core.util.IpUtil;
import org.dromara.neutrinoproxy.server.service.PortMappingService;
import org.dromara.neutrinoproxy.server.service.SecurityGroupService;
@@ -41,7 +42,9 @@ public class TcpVisitorSecurityChannelHandler extends ChannelInboundHandlerAdapt
ctx.channel().close();
return;
}
// 继续传播
ctx.channel().attr(Constants.SERVER_PORT).set(sa.getPort());
buf.resetReaderIndex();
ctx.fireChannelRead(buf);
}
@@ -5,6 +5,7 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.socket.DatagramPacket;
import lombok.extern.slf4j.Slf4j;
import org.dromara.neutrinoproxy.core.Constants;
import org.dromara.neutrinoproxy.server.service.PortMappingService;
import org.dromara.neutrinoproxy.server.service.SecurityGroupService;
import org.noear.solon.Solon;
@@ -32,6 +33,7 @@ public class UdpVisitorSecurityChannelHandler extends ChannelInboundHandlerAdapt
}
// 继续传播
ctx.channel().attr(Constants.SERVER_PORT).set(sa.getPort());
ctx.fireChannelRead(msg);
}
}
@@ -0,0 +1,52 @@
package org.dromara.neutrinoproxy.server.proxy.security;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.traffic.ChannelTrafficShapingHandler;
import lombok.extern.slf4j.Slf4j;
import org.dromara.neutrinoproxy.core.Constants;
import org.dromara.neutrinoproxy.server.service.LicenseService;
import org.dromara.neutrinoproxy.server.service.PortMappingService;
import org.dromara.neutrinoproxy.server.service.SecurityGroupService;
import org.dromara.neutrinoproxy.server.service.bo.FlowLimitBO;
import org.noear.solon.Solon;
/**
* 访问者流量限制器
* @author: aoshiguchen
* @date: 2023/12/15
*/
@Slf4j
public class VisitorFlowLimiterChannelHandler extends ChannelInboundHandlerAdapter {
private final LicenseService licenseService = Solon.context().getBean(LicenseService.class);
private final PortMappingService portMappingService = Solon.context().getBean(PortMappingService.class);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Boolean flowLimiterFlag = ctx.channel().attr(Constants.FLOW_LIMITER_FLAG).get();
if (null == flowLimiterFlag || !flowLimiterFlag) {
Integer serverPort = ctx.channel().attr(Constants.SERVER_PORT).get();
Long upLimitRate = null;
Long downLimitRate = null;
// 先获取端口映射上的限速设置
FlowLimitBO flowLimitBO = portMappingService.getFlowLimitByServerPort(serverPort);
if (null != flowLimitBO) {
upLimitRate = flowLimitBO.getUpLimitRate();
downLimitRate = flowLimitBO.getDownLimitRate();
}
if (null != upLimitRate || null != downLimitRate) {
// 如果不全为空,则需要做限速
ctx.pipeline().addAfter("security", "trafficShaping", new ChannelTrafficShapingHandler(downLimitRate == null ? 0 : downLimitRate, upLimitRate == null ? 0 : upLimitRate, 100, 600000));
}
// 每个连接第一次处理之后。无论是否限速,该连接后续都不在处理,避免频繁执行影响性能
ctx.channel().attr(Constants.FLOW_LIMITER_FLAG).set(Boolean.TRUE);
}
// 继续传播
ctx.fireChannelRead(msg);
}
}
@@ -1,7 +1,10 @@
package org.dromara.neutrinoproxy.server.service;
import cn.hutool.cache.Cache;
import cn.hutool.cache.CacheUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.solon.plugins.pagination.Page;
import com.google.common.collect.Sets;
@@ -29,7 +32,9 @@ import org.dromara.neutrinoproxy.server.dal.UserMapper;
import org.dromara.neutrinoproxy.server.dal.entity.LicenseDO;
import org.dromara.neutrinoproxy.server.dal.entity.PortMappingDO;
import org.dromara.neutrinoproxy.server.dal.entity.UserDO;
import org.dromara.neutrinoproxy.server.service.bo.FlowLimitBO;
import org.dromara.neutrinoproxy.server.util.ParamCheckUtil;
import org.dromara.neutrinoproxy.server.util.StringUtil;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Init;
import org.noear.solon.annotation.Inject;
@@ -63,6 +68,8 @@ public class LicenseService implements LifecycleBean {
private VisitorChannelService visitorChannelService;
@Inject
private DBInitialize dbInitialize;
// 流量限制缓存
private final Cache<Integer, FlowLimitBO> flowLimitCache = CacheUtil.newLRUCache(200, 1000 * 60 * 5);
public PageInfo<LicenseListRes> page(PageQuery pageQuery, LicenseListReq req) {
Page<LicenseDO> page = licenseMapper.selectPage(new Page<>(pageQuery.getCurrent(), pageQuery.getSize()), new LambdaQueryWrapper<LicenseDO>()
@@ -128,15 +135,21 @@ public class LicenseService implements LifecycleBean {
String key = UUID.randomUUID().toString().replaceAll("-", "");
Date now = new Date();
licenseMapper.insert(new LicenseDO()
.setName(req.getName())
.setKey(key)
.setUserId(req.getUserId())
.setIsOnline(OnlineStatusEnum.OFFLINE.getStatus())
.setEnable(EnableStatusEnum.ENABLE.getStatus())
.setCreateTime(now)
.setUpdateTime(now)
);
licenseDO = new LicenseDO()
.setName(req.getName())
.setKey(key)
.setUserId(req.getUserId())
.setUpLimitRate(req.getUpLimitRate())
.setDownLimitRate(req.getDownLimitRate())
.setIsOnline(OnlineStatusEnum.OFFLINE.getStatus())
.setEnable(EnableStatusEnum.ENABLE.getStatus())
.setCreateTime(now)
.setUpdateTime(now);
licenseMapper.insert(licenseDO);
// 刷新流量限制缓存
refreshFlowLimitCache(licenseDO.getId(), licenseDO.getUpLimitRate(), licenseDO.getDownLimitRate());
return new LicenseCreateRes();
}
@@ -147,7 +160,17 @@ public class LicenseService implements LifecycleBean {
LicenseDO licenseCheck = licenseMapper.checkRepeat(oldLicenseDO.getUserId(), req.getName(), Sets.newHashSet(oldLicenseDO.getId()));
ParamCheckUtil.checkMustNull(licenseCheck, ExceptionConstant.LICENSE_NAME_CANNOT_REPEAT);
licenseMapper.update(req.getId(), req.getName(), new Date());
licenseMapper.update(null, new LambdaUpdateWrapper<LicenseDO>()
.eq(LicenseDO::getId, req.getId())
.set(LicenseDO::getName, req.getName())
.set(LicenseDO::getUpLimitRate, req.getUpLimitRate())
.set(LicenseDO::getDownLimitRate, req.getDownLimitRate())
.set(LicenseDO::getUpdateTime, new Date())
);
// 刷新流量限制缓存
refreshFlowLimitCache(req.getId(), req.getUpLimitRate(), req.getDownLimitRate());
return new LicenseUpdateRes();
}
@@ -200,6 +223,8 @@ public class LicenseService implements LifecycleBean {
licenseMapper.deleteById(id);
// 更新VisitorChannel
visitorChannelService.updateVisitorChannelByLicenseId(id, EnableStatusEnum.DISABLE.getStatus());
// 删除流量限制缓存
flowLimitCache.remove(id);
}
/**
@@ -243,10 +268,51 @@ public class LicenseService implements LifecycleBean {
if (NativeDetector.isAotRuntime()) {
return;
}
// 服务刚启动,所以默认所有license都是离线状态。解决服务突然关闭,在线状态来不及更新的问题
licenseMapper.updateOnlineStatus(OnlineStatusEnum.OFFLINE.getStatus(), new Date());
// 刷新流量限制缓存
List<LicenseDO> licenseDOList = licenseMapper.listAll();
if (CollectionUtils.isEmpty(licenseDOList)) {
for (LicenseDO licenseDO : licenseDOList) {
refreshFlowLimitCache(licenseDO.getId(), licenseDO.getUpLimitRate(), licenseDO.getDownLimitRate());
}
}
}
/**
* 刷新流量限制缓存
* @param id
* @param upLimitRate
* @param downLimitRate
*/
private void refreshFlowLimitCache(Integer id, String upLimitRate, String downLimitRate) {
if (null == id) {
return;
}
flowLimitCache.put(id, new FlowLimitBO()
.setUpLimitRate(StringUtil.parseBytes(upLimitRate))
.setDownLimitRate(StringUtil.parseBytes(downLimitRate))
);
}
/**
* 获取license的流量限制
* @param licenseId
* @return
*/
public FlowLimitBO getFlowLimit(Integer licenseId) {
FlowLimitBO res = flowLimitCache.get(licenseId);
if (null == res) {
LicenseDO licenseDO = licenseMapper.queryById(licenseId);
if (null != licenseDO) {
refreshFlowLimitCache(licenseId, licenseDO.getUpLimitRate(), licenseDO.getDownLimitRate());
res = flowLimitCache.get(licenseId);
}
}
return res;
}
@Override
public void start() throws Throwable {
@@ -1,9 +1,12 @@
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.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -36,19 +39,17 @@ import org.dromara.neutrinoproxy.server.dal.entity.LicenseDO;
import org.dromara.neutrinoproxy.server.dal.entity.PortMappingDO;
import org.dromara.neutrinoproxy.server.dal.entity.PortPoolDO;
import org.dromara.neutrinoproxy.server.dal.entity.UserDO;
import org.dromara.neutrinoproxy.server.service.bo.FlowLimitBO;
import org.dromara.neutrinoproxy.server.util.ParamCheckUtil;
import org.dromara.neutrinoproxy.server.util.ProxyUtil;
import org.dromara.neutrinoproxy.server.util.StringUtil;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Init;
import org.noear.solon.annotation.Inject;
import org.noear.solon.core.bean.LifecycleBean;
import org.noear.solon.core.runtime.NativeDetector;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -76,9 +77,17 @@ public class PortMappingService implements LifecycleBean {
private ProxyConfig proxyConfig;
@Inject
private DBInitialize dbInitialize;
@Inject
private LicenseService licenseService;
/** 端口到安全组Id的映射 */
private final Map<Integer, Integer> mappingPortToSecurityGroupMap = new ConcurrentHashMap<>();
// 服务端端口到端口映射id的映射
private final Cache<Integer, Integer> serverPortToPortMappingIdCache = CacheUtil.newLRUCache(500, 1000 * 60 * 10);
// 端口映射id到licenseId
private final Cache<Integer, Integer> idToLicenseIdCache = CacheUtil.newLRUCache(500, 1000 * 60 * 10);
// 流量限制缓存
private final Cache<Integer, FlowLimitBO> flowLimitCache = CacheUtil.newLRUCache(500, 1000 * 60 * 5);
public PageInfo<PortMappingListRes> page(PageQuery pageQuery, PortMappingListReq req) {
if (StringUtils.isNotEmpty(req.getDescription())) {
@@ -153,6 +162,8 @@ public class PortMappingService implements LifecycleBean {
portMappingDO.setServerPort(req.getServerPort());
portMappingDO.setClientIp(req.getClientIp());
portMappingDO.setClientPort(req.getClientPort());
portMappingDO.setUpLimitRate(req.getUpLimitRate());
portMappingDO.setDownLimitRate(req.getDownLimitRate());
portMappingDO.setProxyResponses(req.getProxyResponses());
portMappingDO.setProxyTimeoutMs(req.getProxyTimeoutMs());
portMappingDO.setDescription(req.getDescription());
@@ -170,6 +181,13 @@ public class PortMappingService implements LifecycleBean {
updateMappingPortToSecurityGroupMap(portMappingDO.getServerPort(), req.getSecurityGroupId());
// 更新端口到映射的缓存
serverPortToPortMappingIdCache.put(req.getServerPort(), portMappingDO.getId());
// 更新端口映射到licenseId的缓存
idToLicenseIdCache.put(portMappingDO.getId(), portMappingDO.getLicenseId());
// 刷新流量限制缓存
refreshFlowLimitCache(portMappingDO.getId(), portMappingDO.getUpLimitRate(), portMappingDO.getDownLimitRate());
return new PortMappingCreateRes();
}
@@ -189,15 +207,25 @@ public class PortMappingService implements LifecycleBean {
PortMappingDO oldPortMappingDO = portMappingMapper.findById(req.getId());
ParamCheckUtil.checkNotNull(oldPortMappingDO, ExceptionConstant.PORT_MAPPING_NOT_EXIST);
PortMappingDO portMappingDO = new PortMappingDO();
BeanUtil.copyProperties(req, portMappingDO);
if (req.getSecurityGroupId() == null) {
portMappingDO.setSecurityGroupId(0);
}
portMappingDO.setUpdateTime(new Date());
portMappingDO.setEnable(EnableStatusEnum.ENABLE.getStatus());
portMappingMapper.updateById(portMappingDO);
// 更新端口映射
portMappingMapper.update(null, new LambdaUpdateWrapper<PortMappingDO>()
.eq(PortMappingDO::getId, req.getId())
.set(PortMappingDO::getProtocal, req.getProtocal())
.set(PortMappingDO::getSubdomain, req.getSubdomain())
.set(PortMappingDO::getServerPort, req.getServerPort())
.set(PortMappingDO::getClientIp, req.getClientIp())
.set(PortMappingDO::getClientPort, req.getClientPort())
.set(PortMappingDO::getUpLimitRate, req.getUpLimitRate())
.set(PortMappingDO::getDownLimitRate, req.getDownLimitRate())
.set(PortMappingDO::getProxyTimeoutMs, req.getProxyTimeoutMs())
.set(PortMappingDO::getProxyResponses, req.getProxyResponses())
.set(PortMappingDO::getSecurityGroupId, req.getSecurityGroupId())
.set(PortMappingDO::getDescription, req.getDescription())
.set(PortMappingDO::getUpdateTime, new Date())
);
// 更新VisitorChannel
PortMappingDO portMappingDO = portMappingMapper.findById(req.getId());
visitorChannelService.updateVisitorChannelByPortMapping(oldPortMappingDO, portMappingDO);
// 删除老的域名映射
if (NetworkProtocolEnum.isHttp(oldPortMappingDO.getProtocal()) && StrUtil.isNotBlank(oldPortMappingDO.getSubdomain())) {
@@ -209,6 +237,13 @@ public class PortMappingService implements LifecycleBean {
}
updateMappingPortToSecurityGroupMap(portMappingDO.getServerPort(), req.getSecurityGroupId());
// 更新端口到映射的缓存
serverPortToPortMappingIdCache.put(req.getServerPort(), req.getId());
// 更新端口映射到licenseId的缓存
idToLicenseIdCache.put(portMappingDO.getId(), portMappingDO.getLicenseId());
// 刷新流量限制缓存
refreshFlowLimitCache(req.getId(), req.getUpLimitRate(), req.getDownLimitRate());
}
public PortMappingDetailRes detail(Integer id) {
@@ -285,6 +320,11 @@ public class PortMappingService implements LifecycleBean {
}
updateMappingPortToSecurityGroupMap(portMappingDO.getServerPort(), null);
// 删除id到licenseId的映射
idToLicenseIdCache.remove(id);
// 删除流量限制缓存
flowLimitCache.remove(id);
}
public void portBindSecurityGroup(Integer portMappingId, Integer groupId) {
@@ -333,6 +373,7 @@ public class PortMappingService implements LifecycleBean {
if (NativeDetector.isAotRuntime()) {
return;
}
// 服务刚启动,所以默认所有license都是离线状态。解决服务突然关闭,在线状态来不及更新的问题
portMappingMapper.updateOnlineStatus(OnlineStatusEnum.OFFLINE.getStatus(), new Date());
List<PortMappingDO> allMappingDOList = portMappingMapper.selectList(Wrappers.lambdaQuery(PortMappingDO.class));
@@ -341,6 +382,12 @@ public class PortMappingService implements LifecycleBean {
if (securityGroupId != null && securityGroupId > 0) {
updateMappingPortToSecurityGroupMap(item.getServerPort(), item.getSecurityGroupId());
}
// 更新端口到映射的缓存
serverPortToPortMappingIdCache.put(item.getServerPort(), item.getId());
// 更新端口映射到licenseId的缓存
idToLicenseIdCache.put(item.getId(), item.getLicenseId());
// 刷新流量限制缓存
refreshFlowLimitCache(item.getId(), item.getUpLimitRate(), item.getDownLimitRate());
});
// 未配置域名,则不需要处理域名映射逻辑
@@ -350,7 +397,6 @@ public class PortMappingService implements LifecycleBean {
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;
}
@@ -363,6 +409,84 @@ public class PortMappingService implements LifecycleBean {
});
}
/**
* 刷新流量限制缓存
* @param id
* @param upLimitRate
* @param downLimitRate
*/
private void refreshFlowLimitCache(Integer id, String upLimitRate, String downLimitRate) {
if (null == id) {
return;
}
flowLimitCache.put(id, new FlowLimitBO()
.setUpLimitRate(StringUtil.parseBytes(upLimitRate))
.setDownLimitRate(StringUtil.parseBytes(downLimitRate))
);
}
/**
* 获取license的流量限制
* @param id
* @return
*/
public FlowLimitBO getFlowLimit(Integer id) {
FlowLimitBO res = flowLimitCache.get(id);
if (null == res) {
PortMappingDO portMappingDO = portMappingMapper.findById(id);
if (null != portMappingDO) {
refreshFlowLimitCache(id, portMappingDO.getUpLimitRate(), portMappingDO.getDownLimitRate());
res = flowLimitCache.get(id);
}
}
return res;
}
public Integer getPortMappingIdByServerPort(Integer serverPort) {
if (null == serverPort) {
return null;
}
Integer id = serverPortToPortMappingIdCache.get(serverPort);
if (null != id) {
return id;
}
List<PortMappingDO> portMappingDOList = portMappingMapper.findListByServerPort(serverPort);
// 不存在 或者 有多条记录,都不处理
if (CollectionUtils.isEmpty(portMappingDOList) || portMappingDOList.size() > 1) {
return null;
}
id = portMappingDOList.get(0).getId();
serverPortToPortMappingIdCache.put(serverPort, id);
return id;
}
public Integer getLicenseIdById(Integer id) {
Integer licenseId = idToLicenseIdCache.get(id);
if (null == licenseId) {
PortMappingDO portMappingDO = portMappingMapper.findById(id);
if (null != portMappingDO) {
licenseId = portMappingDO.getLicenseId();
idToLicenseIdCache.put(id, licenseId);
}
}
return licenseId;
}
public FlowLimitBO getFlowLimitByServerPort(Integer serverPort) {
Integer id = getPortMappingIdByServerPort(serverPort);
if (null == id) {
return null;
}
FlowLimitBO res = getFlowLimit(id);
if (null == res || (null == res.getUpLimitRate() && null == res.getDownLimitRate())) {
Integer licenseId = getLicenseIdById(id);
if (null != licenseId) {
res = licenseService.getFlowLimit(licenseId);
}
}
return res;
}
private void updateMappingPortToSecurityGroupMap(Integer serverPort, Integer securityGroupId) {
if (securityGroupId == null || securityGroupId == 0) {
mappingPortToSecurityGroupMap.remove(serverPort);
@@ -0,0 +1,15 @@
package org.dromara.neutrinoproxy.server.service.bo;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @author: aoshiguchen
* @date: 2023/12/15
*/
@Accessors(chain = true)
@Data
public class FlowLimitBO {
private Long upLimitRate;
private Long downLimitRate;
}
@@ -103,4 +103,20 @@ public class ParamCheckUtil {
}
}
/**
* 校验字节描述
* @param str
* @param params
*/
public static void checkBytesDesc(String str, Object... params) {
// 允许为空
if (StrUtil.isEmpty(str)) {
return;
}
Long bytes = StringUtil.parseBytes(str);
if (null == bytes || bytes <= 0) {
throw ServiceException.create(ExceptionConstant.BYTES_DESC_INVALID, params);
}
}
}
@@ -0,0 +1,75 @@
package org.dromara.neutrinoproxy.server.util;
import cn.hutool.core.util.StrUtil;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author: aoshiguchen
* @date: 2023/12/15
*/
public class StringUtil {
private static final Integer BYTES_MUL_KB = 1024;
private static final Integer BYTES_MUL_MB = BYTES_MUL_KB * 1024;
private static final Integer BYTES_MUL_GB = BYTES_MUL_MB * 1024;
private static final String[] BYTES_UNIT_STR = {"B", "K", "KB", "M", "MB", "G", "GB"};
private static final Integer[] BYTES_UNIT_MUL = {1, BYTES_MUL_KB, BYTES_MUL_KB, BYTES_MUL_MB, BYTES_MUL_MB, BYTES_MUL_GB, BYTES_MUL_GB};
private static final String BYTES_DESC_REGEX = "\\s*(\\d+)\\s*(B|K|KB|M|MB|G|GB)\\s*";
private static final Pattern BYTES_DESC_PATTERN = Pattern.compile(BYTES_DESC_REGEX);
/**
* 校验是否符合字节描述
* @param desc
* @return
*/
public static boolean isBytesDesc(String desc) {
if (StrUtil.isBlank(desc)) {
return false;
}
return desc.toUpperCase().matches(BYTES_DESC_REGEX);
}
/**
* 解析字节数
* 支持B、K、KB、M、MB、G、GB 忽略大小写、忽略首尾空格、忽略数字与单位之间的空格
* @param desc
* @return
*/
public static Long parseBytes(String desc) {
try {
if (!isBytesDesc(desc)) {
return null;
}
Matcher matcher = BYTES_DESC_PATTERN.matcher(desc.toUpperCase());
boolean found = matcher.find();
if (!found) {
return null;
}
Integer n = Integer.parseInt(matcher.group(1));
String unit = matcher.group(2);
Integer unitIndex = getBytesUnitIndex(unit);
if (null == unitIndex) {
return null;
}
return (long)n * BYTES_UNIT_MUL[unitIndex];
} catch (Exception e) {
// ignore
}
return null;
}
private static Integer getBytesUnitIndex(String unit) {
if (StrUtil.isBlank(unit)) {
return null;
}
for (int i = 0; i < BYTES_UNIT_STR.length; i++) {
if (unit.equals(BYTES_UNIT_STR[i])) {
return i;
}
}
return null;
}
}
@@ -83,6 +83,8 @@ CREATE TABLE IF NOT EXISTS `license` (
`name` VARCHAR(50) NOT NULL,
`key` VARCHAR(100) NOT NULL,
`user_id` INTEGER NOT NULL,
`up_limit_rate` VARCHAR(20) DEFAULT NULL,
`down_limit_rate` VARCHAR(20) DEFAULT NULL,
`is_online` INTEGER(2) NOT NULL,
`enable` INTEGER(2) NOT NULL,
`create_time` TIMESTAMP NOT NULL,
@@ -99,6 +101,8 @@ CREATE TABLE IF NOT EXISTS `port_mapping` (
`server_port` INTEGER NOT NULL,
`client_ip` VARCHAR(20) NOT NULL,
`client_port` INTEGER NOT NULL,
`up_limit_rate` VARCHAR(20) DEFAULT NULL,
`down_limit_rate` VARCHAR(20) DEFAULT NULL,
`is_online` INTEGER(2) NOT NULL,
`description` VARCHAR(100) DEFAULT NULL,
`proxy_responses` INTEGER(20) NOT NULL DEFAULT 0,
@@ -167,8 +171,8 @@ CREATE TABLE IF NOT EXISTS `flow_report_minute` (
`id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
`user_id` INTEGER(20) NOT NULL,
`license_id` INTEGER(20) NOT NULL,
`write_bytes` INTEGER(20) NOT NULL,
`read_bytes` INTEGER(20) NOT NULL,
`write_bytes` bigint NOT NULL,
`read_bytes` bigint NOT NULL,
`date` TIMESTAMP NOT NULL,
`date_str` VARCHAR(20) NOT NULL,
`create_time` TIMESTAMP NOT NULL
@@ -183,8 +187,8 @@ CREATE TABLE IF NOT EXISTS `flow_report_hour` (
`id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
`user_id` INTEGER(20) NOT NULL,
`license_id` INTEGER(20) NOT NULL,
`write_bytes` INTEGER(20) NOT NULL,
`read_bytes` INTEGER(20) NOT NULL,
`write_bytes` bigint NOT NULL,
`read_bytes` bigint NOT NULL,
`date` TIMESTAMP NOT NULL,
`date_str` VARCHAR(20) NOT NULL,
`create_time` TIMESTAMP NOT NULL
@@ -199,8 +203,8 @@ CREATE TABLE IF NOT EXISTS `flow_report_day` (
`id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
`user_id` INTEGER(20) NOT NULL,
`license_id` INTEGER(20) NOT NULL,
`write_bytes` INTEGER(20) NOT NULL,
`read_bytes` INTEGER(20) NOT NULL,
`write_bytes` bigint NOT NULL,
`read_bytes` bigint NOT NULL,
`date` TIMESTAMP NOT NULL,
`date_str` VARCHAR(20) NOT NULL,
`create_time` TIMESTAMP NOT NULL
@@ -215,8 +219,8 @@ CREATE TABLE IF NOT EXISTS `flow_report_month` (
`id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
`user_id` INTEGER(20) NOT NULL,
`license_id` INTEGER(20) NOT NULL,
`write_bytes` INTEGER(20) NOT NULL,
`read_bytes` INTEGER(20) NOT NULL,
`write_bytes` bigint NOT NULL,
`read_bytes` bigint NOT NULL,
`date` TIMESTAMP NOT NULL,
`date_str` VARCHAR(20) NOT NULL,
`create_time` TIMESTAMP NOT NULL
@@ -27,4 +27,12 @@ CREATE TABLE IF NOT EXISTS `security_rule` (
PRIMARY KEY (`id`)
);
CREATE INDEX IF NOT EXISTS I_security_rule_group_id ON security_rule(group_id);
# port_mapping表增加字段
ALTER TABLE `port_mapping` ADD COLUMN `security_group_id` INTEGER DEFAULT 0;
ALTER TABLE `port_mapping` ADD COLUMN `up_limit_rate` varchar(20) DEFAULT NULL;
ALTER TABLE `port_mapping` ADD COLUMN `down_limit_rate` varchar(20) DEFAULT NULL;
# license表增加字段
ALTER TABLE `license` ADD COLUMN `up_limit_rate` varchar(20) DEFAULT NULL;
ALTER TABLE `license` ADD COLUMN `down_limit_rate` varchar(20) DEFAULT NULL;
@@ -87,6 +87,8 @@ CREATE TABLE IF NOT EXISTS `license` (
`name` varchar(50) NOT NULL COMMENT 'license名称',
`key` varchar(100) NOT NULL COMMENT 'license key',
`user_id` int NOT NULL COMMENT '用户ID',
`up_limit_rate` varchar(20) DEFAULT NULL COMMENT '上传限速',
`down_limit_rate` varchar(20) DEFAULT NULL COMMENT '下载限速',
`is_online` int NOT NULL COMMENT '是否在线(1、在线 2、离线)',
`enable` int NOT NULL COMMENT '是否启用(1、启用 2、禁用)',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
@@ -104,6 +106,8 @@ CREATE TABLE IF NOT EXISTS `port_mapping` (
`server_port` int NOT NULL COMMENT '服务端端口',
`client_ip` varchar(20) NOT NULL COMMENT '客户端IP',
`client_port` int NOT NULL COMMENT '客户端端口',
`up_limit_rate` varchar(20) DEFAULT NULL COMMENT '上传限速',
`down_limit_rate` varchar(20) DEFAULT NULL COMMENT '下载限速',
`is_online` int NOT NULL COMMENT '是否在线(1、在线 2、离线)',
`description` varchar(100) DEFAULT NULL COMMENT '描述',
`proxy_responses` int NOT NULL DEFAULT 0 COMMENT '代理响应数据包数量',
@@ -87,6 +87,8 @@ CREATE TABLE IF NOT EXISTS `license` (
`name` varchar(50) NOT NULL COMMENT 'license名称',
`key` varchar(100) NOT NULL COMMENT 'license key',
`user_id` int NOT NULL COMMENT '用户ID',
`up_limit_rate` varchar(20) DEFAULT NULL COMMENT '上传限速',
`down_limit_rate` varchar(20) DEFAULT NULL COMMENT '下载限速',
`is_online` int NOT NULL COMMENT '是否在线(1、在线 2、离线)',
`enable` int NOT NULL COMMENT '是否启用(1、启用 2、禁用)',
`create_time` datetime(3) NOT NULL COMMENT '创建时间',
@@ -104,6 +106,8 @@ CREATE TABLE IF NOT EXISTS `port_mapping` (
`server_port` int NOT NULL COMMENT '服务端端口',
`client_ip` varchar(20) NOT NULL COMMENT '客户端IP',
`client_port` int NOT NULL COMMENT '客户端端口',
`up_limit_rate` varchar(20) DEFAULT NULL COMMENT '上传限速',
`down_limit_rate` varchar(20) DEFAULT NULL COMMENT '下载限速',
`is_online` int NOT NULL COMMENT '是否在线(1、在线 2、离线)',
`description` varchar(100) DEFAULT NULL COMMENT '描述',
`proxy_responses` int NOT NULL DEFAULT 0 COMMENT '代理响应数据包数量',
@@ -30,3 +30,9 @@ CREATE TABLE IF NOT EXISTS `security_rule` (
# port_mapping表增加字段
ALTER TABLE `port_mapping` ADD COLUMN `security_group_id` int DEFAULT 0 COMMENT '安全组Id';
ALTER TABLE `port_mapping` ADD COLUMN `up_limit_rate` varchar(20) DEFAULT NULL COMMENT '上传限速';
ALTER TABLE `port_mapping` ADD COLUMN `down_limit_rate` varchar(20) DEFAULT NULL COMMENT '下载限速';
# license表增加字段
ALTER TABLE `license` ADD COLUMN `up_limit_rate` varchar(20) DEFAULT NULL COMMENT '上传限速';
ALTER TABLE `license` ADD COLUMN `down_limit_rate` varchar(20) DEFAULT NULL COMMENT '下载限速';