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:
1 parent
c7f59f2cbf
commit
ef7eb378d2
28 files changed
+1363
-439
No files matched your search
@@ -17,7 +17,8 @@
|
||||
<el-form-item :label="$t('tunnelEditor.protocolType')">
|
||||
<el-tag :color="protocolColor(protocol)" effect="dark" style="border:none">{{ $t(protocolDisplay(protocol)) }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item v-for="field in visibleFields" :key="field.key" :label="$t(field.label)" :required="field.required">
|
||||
<el-divider v-if="isOpenVPN" content-position="left" class="config-section-divider">{{ $t('tunnelEditor.coreOptions') }}</el-divider>
|
||||
<el-form-item v-for="field in coreFields" :key="field.key" :label="$t(field.label)" :required="field.required">
|
||||
<el-input-number v-if="field.type === 'number'" v-model="form.config[field.key]" :min="0" :max="65535" />
|
||||
<el-switch v-else-if="field.type === 'switch'" v-model="form.config[field.key]" />
|
||||
<el-select v-else-if="field.type === 'select'" v-model="form.config[field.key]" style="width:100%">
|
||||
@@ -28,6 +29,34 @@
|
||||
<el-input v-else v-model="form.config[field.key]" :placeholder="field.placeholder || field.default" />
|
||||
<div v-if="field.tip" class="field-tip">{{ $t(field.tip) }}</div>
|
||||
</el-form-item>
|
||||
<template v-if="isOpenVPN">
|
||||
<el-form-item :label="$t('tunnelEditor.trafficMode')">
|
||||
<el-radio-group v-model="trafficMode">
|
||||
<el-radio-button value="gateway">{{ $t('tunnelEditor.trafficGateway') }}</el-radio-button>
|
||||
<el-radio-button value="static">{{ $t('tunnelEditor.trafficStatic') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div class="field-tip">{{ $t(`tunnelEditor.traffic${trafficMode[0].toUpperCase()}${trafficMode.slice(1)}Tip`) }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="trafficMode === 'static'" :label="$t('protocols.openvpn.fields.push_routes.label')">
|
||||
<RouteListInput ref="routeListRef" v-model="form.config.push_routes" />
|
||||
<div class="field-tip">{{ $t('protocols.openvpn.fields.push_routes.tip') }}</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-collapse v-if="advancedFields.length" v-model="advancedOpen" class="advanced-options">
|
||||
<el-collapse-item :title="$t('tunnelEditor.advancedOptions')" name="advanced">
|
||||
<el-form-item v-for="field in advancedFields" :key="field.key" :label="$t(field.label)" :required="field.required">
|
||||
<el-input-number v-if="field.type === 'number'" v-model="form.config[field.key]" :min="0" :max="65535" />
|
||||
<el-switch v-else-if="field.type === 'switch'" v-model="form.config[field.key]" />
|
||||
<el-select v-else-if="field.type === 'select'" v-model="form.config[field.key]" style="width:100%">
|
||||
<el-option v-for="option in field.options" :key="optionValue(option)" :label="$t(optionLabel(option))" :value="optionValue(option)" />
|
||||
</el-select>
|
||||
<el-input v-else-if="field.type === 'password'" v-model="form.config[field.key]" type="password"
|
||||
show-password :placeholder="field.placeholder || $t('tunnelEditor.keepOriginal')" />
|
||||
<el-input v-else v-model="form.config[field.key]" :placeholder="field.placeholder || field.default" />
|
||||
<div v-if="field.tip" class="field-tip">{{ $t(field.tip) }}</div>
|
||||
</el-form-item>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<el-form-item :label="$t('tunnelEditor.expireTime')">
|
||||
<el-date-picker v-model="form.expire_at" type="datetime" clearable :placeholder="$t('tunnelEditor.expirePlaceholder')" style="width:100%" />
|
||||
</el-form-item>
|
||||
@@ -47,6 +76,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import api from '../api'
|
||||
import RouteListInput from '../components/RouteListInput.vue'
|
||||
import { CUSTOM_FIELDS, PROTOCOL_META, protocolColor, protocolDisplay, optionValue, optionLabel } from '../protocols'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -56,6 +86,8 @@ const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const protocol = ref('')
|
||||
const form = ref(null)
|
||||
const advancedOpen = ref([])
|
||||
const routeListRef = ref(null)
|
||||
|
||||
const fields = computed(() => PROTOCOL_META[protocol.value]?.fields || CUSTOM_FIELDS)
|
||||
const visibleFields = computed(() => fields.value.filter(field => {
|
||||
@@ -64,6 +96,33 @@ const visibleFields = computed(() => fields.value.filter(field => {
|
||||
? value.includes(form.value.config[key])
|
||||
: form.value.config[key] === value)
|
||||
}))
|
||||
const isOpenVPN = computed(() => protocol.value === 'openvpn')
|
||||
const openVPNCoreKeys = new Set(['public_endpoint', 'port', 'proto', 'auth_mode', 'mfa_mode'])
|
||||
const openVPNTrafficKeys = new Set(['redirect_gateway', 'push_routes', 'enable_nat'])
|
||||
const trafficMode = computed({
|
||||
get() {
|
||||
const cfg = form.value?.config || {}
|
||||
return cfg.redirect_gateway === false ? 'static' : 'gateway'
|
||||
},
|
||||
set(mode) {
|
||||
if (!form.value) return
|
||||
if (mode === 'gateway') {
|
||||
form.value.config.redirect_gateway = true
|
||||
form.value.config.enable_nat = true
|
||||
form.value.config.push_routes = ''
|
||||
} else if (mode === 'static') {
|
||||
form.value.config.redirect_gateway = false
|
||||
form.value.config.enable_nat = false
|
||||
}
|
||||
},
|
||||
})
|
||||
const coreFields = computed(() => isOpenVPN.value
|
||||
? visibleFields.value.filter(field => openVPNCoreKeys.has(field.key))
|
||||
: visibleFields.value.filter(field => field.group !== 'advanced' && field.group !== 'crypto'))
|
||||
const advancedFields = computed(() => {
|
||||
if (!isOpenVPN.value) return visibleFields.value.filter(field => field.group === 'advanced' || field.group === 'crypto')
|
||||
return visibleFields.value.filter(field => !openVPNCoreKeys.has(field.key) && !openVPNTrafficKeys.has(field.key))
|
||||
})
|
||||
|
||||
function parseConfig(raw) {
|
||||
try { return JSON.parse(raw || '{}') } catch { return {} }
|
||||
@@ -95,6 +154,8 @@ async function load() {
|
||||
|
||||
async function save() {
|
||||
if (!form.value.name) return ElMessage.warning(t('tunnelEditor.fillName'))
|
||||
trafficMode.value = trafficMode.value
|
||||
if (isOpenVPN.value && routeListRef.value && !routeListRef.value.validate()) return
|
||||
saving.value = true
|
||||
try {
|
||||
await api.put(`/tunnels/${route.params.id}`, form.value)
|
||||
@@ -112,4 +173,6 @@ onMounted(load)
|
||||
|
||||
<style scoped>
|
||||
.field-tip { margin-top: 4px; color: #909399; font-size: 12px; line-height: 1.4; }
|
||||
.config-section-divider { margin: 4px 0 18px; }
|
||||
.advanced-options { margin: 12px 0 18px; }
|
||||
</style>
|
||||
Reference in new issue
Block a user