feat: 新增 GRE 隧道协议,管理端口调整为 18080

- backend: 新增 GRE 隧道实现(ip tunnel 点对点互联),binmgr 支持 iproute2
- backend: api/identity 调整,各协议 service(gre/ikev2/ipsec/l2tp/openvpn)统一改动
- backend: OpenVPN 认证辅助与用户口令同步逻辑完善
- frontend: 协议图标/向导/编辑器支持 GRE,VPN 用户页重构(234 行)
- frontend: 新增 RouteListInput 路由列表输入组件,Dashboard/Layout 优化
- i18n: 中英文文案同步更新
- test: 新增 binmgr/common/gre 单测,openvpn 测试补充
- docs: README 补充 GRE 说明,默认监听端口 8080 → 18080
- chore: .gitignore 增加 opsx-vpn.previous(构建备份二进制)
This commit is contained in:
opsx committed 2026-10-20 20:58:58 +08:00
1 parent c7f59f2cbf
commit ef7eb378d2
28 files changed
+1363 -439

No files matched your search

+4
View File
@@ -33,6 +33,7 @@ const NAMES = computed(() => ({
ipsec: 'IPSec',
ikev2: 'IKEv2',
l2tp: 'L2TP',
gre: 'GRE',
frp: 'FRP',
openvpn: 'OpenVPN',
custom: t('protocolIcon.custom'),
@@ -62,6 +63,9 @@ const ICON_PATHS = {
// L2TP — 手机+信号波(移动设备拨号语义)
l2tp: 'M7 1C5.9 1 5 1.9 5 3v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-2-2-2H7zm5 19c-.83 0-1.5-.67-1.5-1.5S11.17 17 12 17s1.5.67 1.5 1.5S12.83 20 12 20zm5-4H7V4h10v12z M9.5 6.5l2 2-2 2-1-1 1-1-1-1 1-1z M14.5 6.5l1 1-1 1 1 1-1 1-2-2 2-2z',
// GRE — two routed sites connected by a tunnel
gre: 'M3 4h6v3H6v10h3v3H3V4zm12 0h6v16h-6v-3h3V7h-3V4zM9 10h6v2H9v-2zm0 4h6v2H9v-2z',
// FRP — 隧道+箭头穿透(内网穿透语义)
frp: 'M3 8h2v8H3V8zm16 0h2v8h-2V8zM7 10h10v4H7v-4zm2 1.5l2 1.5-2 1.5v-3zm6 0v3l-2-1.5 2-1.5z M12 6l3 2H9l3-2zm0 12l-3-2h6l-3 2z',
@@ -0,0 +1,44 @@
<template>
<div class="route-list">
<el-input v-model="content" type="textarea" :rows="7" resize="vertical"
:placeholder="t('protocols.openvpn.fields.push_routes.placeholder')" @blur="validate" />
<span v-if="error" class="route-error">{{ error }}</span>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const props = defineProps({ modelValue: { type: String, default: '' } })
const emit = defineEmits(['update:modelValue'])
const { t } = useI18n()
const content = ref(props.modelValue || '')
const error = ref('')
watch(() => props.modelValue, value => {
if (value !== content.value) content.value = value || ''
})
watch(content, value => emit('update:modelValue', value))
function validRoute(value) {
const [ip, prefix] = value.split('/')
const parts = ip.split('.')
if (parts.length !== 4 || parts.some(part => !/^\d+$/.test(part) || Number(part) > 255)) return false
return prefix === undefined || (/^\d+$/.test(prefix) && Number(prefix) >= 0 && Number(prefix) <= 32)
}
function validate() {
const filled = String(content.value).split('\n').flatMap(line => line.split('#', 1)[0].split(',')).map(item => item.trim()).filter(Boolean)
const invalid = filled.find(item => !validRoute(item))
error.value = invalid ? t('protocols.openvpn.fields.push_routes.format_error', { route: invalid }) : ''
if (!invalid && content.value !== content.value.trim()) {
content.value = content.value.trim()
}
return !invalid
}
defineExpose({ validate })
</script>
<style scoped>
.route-list { display:grid; gap:8px; width:100%; }
.route-error { color:var(--el-color-danger); font-size:12px; }
</style>