diff --git a/README.md b/README.md index e081f87..702abbe 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,51 @@ - 提供灵活的转发策略配置,适用于多种网络代理场景 --- +# 流量统计算法 + +## 流量流向定义 + +``` +用户端 ←→ 转发服务器 ←→ 目标服务器/出口节点 +``` + +### 数据流向说明 + +| 流向 | 描述 | 计费类型 | +|-----|-----|----------| +| 用户 → 转发服务器 | 用户发送到服务器的数据包 | 入站流量 | +| 转发服务器 → 目标服务器 | 服务器转发到目标的数据包 | 出站流量 | +| 目标服务器 → 转发服务器 | 目标服务器返回的数据包 | 入站流量 | +| 转发服务器 → 用户 | 服务器返回给用户的数据包 | 出站流量 | + +## 计费模式 + +### 🔄 双向计费 +统计所有方向的流量,包括: +- ✅ 用户 → 转发服务器 +- ✅ 转发服务器 → 目标服务器 +- ✅ 目标服务器 → 转发服务器 +- ✅ 转发服务器 → 用户 + +**计费公式**:`总流量 = 入站流量 + 出站流量` + +### ➡️ 单向计费 +仅统计有效的转发流量,包括: +- ❌ 用户 → 转发服务器 (不计费) +- ✅ 转发服务器 → 目标服务器 (计费) +- ❌ 目标服务器 → 转发服务器 (不计费) +- ✅ 转发服务器 → 用户 (计费) + +**计费公式**:`总流量 = 转发出站流量` + +## 使用场景 + +- **双向计费**:适用于需要精确计量所有网络资源消耗的场景 +- **单向计费**:适用于用户友好的计费模式,只对实际的数据转发服务收费 + +> 💡 **提示**:可以在创建隧道时选择不同的流量计算方式,灵活适配不同的业务需求 +--- + ## 部署流程 ### 源码编译部署 diff --git a/deployment.zip b/deployment.zip index 9469f7d..fb07bdf 100644 Binary files a/deployment.zip and b/deployment.zip differ diff --git a/deployment/admin-0.0.1-SNAPSHOT.jar b/deployment/admin-0.0.1-SNAPSHOT.jar index ddf2a89..6d011c8 100644 Binary files a/deployment/admin-0.0.1-SNAPSHOT.jar and b/deployment/admin-0.0.1-SNAPSHOT.jar differ diff --git a/java/src/main/java/com/admin/service/impl/NodeServiceImpl.java b/java/src/main/java/com/admin/service/impl/NodeServiceImpl.java index 9aafb1b..81bcd1c 100644 --- a/java/src/main/java/com/admin/service/impl/NodeServiceImpl.java +++ b/java/src/main/java/com/admin/service/impl/NodeServiceImpl.java @@ -36,7 +36,7 @@ public class NodeServiceImpl extends ServiceImpl implements No // ========== 常量定义 ========== /** 节点默认状态:启用 */ - private static final int NODE_STATUS_ACTIVE = 1; + private static final int NODE_STATUS_ACTIVE = 0; /** 成功响应消息 */ private static final String SUCCESS_CREATE_MSG = "节点创建成功"; diff --git a/java/src/main/java/com/admin/service/impl/TunnelServiceImpl.java b/java/src/main/java/com/admin/service/impl/TunnelServiceImpl.java index 4890113..1c09a90 100644 --- a/java/src/main/java/com/admin/service/impl/TunnelServiceImpl.java +++ b/java/src/main/java/com/admin/service/impl/TunnelServiceImpl.java @@ -69,6 +69,8 @@ public class TunnelServiceImpl extends ServiceImpl impleme private static final String ERROR_IN_PORT_RANGE_INVALID = "入口端口开始不能大于结束端口"; private static final String ERROR_OUT_PORT_RANGE_INVALID = "出口端口开始不能大于结束端口"; private static final String ERROR_NO_AVAILABLE_TUNNELS = "暂无可用隧道"; + private static final String ERROR_IN_NODE_IP_INVALID = "入口节点IP地址无效,请先安装节点端的gost(支持IPv4和IPv6)"; + private static final String ERROR_OUT_NODE_IP_INVALID = "出口节点IP地址无效,请先安装节点端的gost(支持IPv4和IPv6)"; /** 使用检查相关消息 */ private static final String ERROR_FORWARDS_IN_USE = "该隧道还有 %d 个转发在使用,请先删除相关转发"; @@ -221,6 +223,11 @@ public class TunnelServiceImpl extends ServiceImpl impleme return NodeValidationResult.error(ERROR_IN_NODE_NOT_FOUND); } + // 验证入口节点IP地址 + if (!isValidNodeIp(inNode.getIp())) { + return NodeValidationResult.error(ERROR_IN_NODE_IP_INVALID); + } + // 验证入口端口范围 if (tunnelDto.getInPortSta() > tunnelDto.getInPortEnd()) { return NodeValidationResult.error(ERROR_IN_PORT_RANGE_INVALID); @@ -306,6 +313,11 @@ public class TunnelServiceImpl extends ServiceImpl impleme return R.err(ERROR_OUT_NODE_NOT_FOUND); } + // 验证出口节点IP地址 + if (!isValidNodeIp(outNode.getIp())) { + return R.err(ERROR_OUT_NODE_IP_INVALID); + } + // 验证出口端口参数 if (tunnelDto.getOutIpSta() == null || tunnelDto.getOutIpEnd() == null) { return R.err(ERROR_OUT_PORT_REQUIRED); @@ -333,6 +345,115 @@ public class TunnelServiceImpl extends ServiceImpl impleme tunnel.setUpdatedTime(currentTime); } + /** + * 验证节点IP地址是否有效 + * 支持IPv4和IPv6格式 + * + * @param ip IP地址字符串 + * @return IP地址是否有效 + */ + private boolean isValidNodeIp(String ip) { + if (StrUtil.isBlank(ip)) { + return false; + } + + // 验证IPv4格式 + if (isValidIpv4(ip)) { + return true; + } + + // 验证IPv6格式 + return isValidIpv6(ip); + } + + /** + * 验证IPv4地址格式 + * + * @param ip IP地址字符串 + * @return 是否为有效的IPv4地址 + */ + private boolean isValidIpv4(String ip) { + if (StrUtil.isBlank(ip)) { + return false; + } + + String[] parts = ip.split("\\."); + if (parts.length != 4) { + return false; + } + + try { + for (String part : parts) { + int num = Integer.parseInt(part); + if (num < 0 || num > 255) { + return false; + } + } + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * 验证IPv6地址格式 + * + * @param ip IP地址字符串 + * @return 是否为有效的IPv6地址 + */ + private boolean isValidIpv6(String ip) { + if (StrUtil.isBlank(ip)) { + return false; + } + + // 简化的IPv6验证,支持标准格式和压缩格式 + try { + // 移除可能的端口号 + if (ip.startsWith("[") && ip.contains("]:")) { + ip = ip.substring(1, ip.indexOf("]:")); + } + + // 检查是否包含非法字符 + if (!ip.matches("^[0-9a-fA-F:]+$")) { + return false; + } + + // 处理双冒号压缩格式 + if (ip.contains("::")) { + if (ip.indexOf("::") != ip.lastIndexOf("::")) { + // 不能有多个双冒号 + return false; + } + // 展开双冒号 + String[] parts = ip.split("::"); + int leftParts = parts[0].isEmpty() ? 0 : parts[0].split(":").length; + int rightParts = parts.length > 1 && !parts[1].isEmpty() ? parts[1].split(":").length : 0; + int totalParts = leftParts + rightParts; + if (totalParts >= 8) { + return false; + } + } else { + // 标准格式,必须有8个部分 + String[] parts = ip.split(":"); + if (parts.length != 8) { + return false; + } + + // 验证每个部分都是有效的十六进制数 + for (String part : parts) { + if (part.length() > 4 || part.isEmpty()) { + return false; + } + Integer.parseInt(part, 16); // 如果不是有效的十六进制会抛出异常 + } + } + + return true; + } catch (Exception e) { + return false; + } + } + /** * 检查隧道是否存在 *