Refine WireGuard client routing and authentication UI

This commit is contained in:
opsx committed 2026-10-21 21:10:53 +08:00
1 parent eabb3e4472
commit 41fb9d5631
33 files changed
+1709 -469

No files matched your search

+16
View File
@@ -129,6 +129,10 @@
"noTunnels": "No tunnels yet",
"expireTime": "Expiry Time",
"localNode": "Local",
"authMethod": "Authentication",
"authUnknown": "Unspecified",
"wgAuthPassword": "Client account + device key",
"wgAuthHint": "The client account obtains device authorization; WireGuard authenticates using keys. Manually added static peers still connect by key.",
"start": "Start",
"stop": "Stop",
"started": "Started",
@@ -768,6 +772,14 @@
"label": "Server VPN Subnet Address",
"tip": "Server's address within the VPN subnet (CIDR)"
},
"auth_mode": {
"label": "Client Authentication",
"tip": "Account login grants a device; the tunnel uses key authentication. Static peers still connect by key. Browser OIDC requires a client callback and explicit user grants.",
"options": {
"password": "Client account + device key",
"oidc": "Third-party OAuth2 / OIDC (not available yet)"
}
},
"listen_port": {
"label": "Listen Port"
},
@@ -786,6 +798,10 @@
"label": "Client AllowedIPs",
"tip": "Use 0.0.0.0/0 for full tunnel; specify internal CIDRs for split tunnel"
},
"latency_probe": {
"label": "Tunnel probe target",
"tip": "Reachable IP:port through this line, e.g. 10.88.0.1:22; defaults to 223.5.5.5:443. Measures TCP setup time."
},
"keepalive": {
"label": "Keepalive (sec)",
"tip": "Recommended 15–25 for clients behind NAT"
+16
View File
@@ -129,6 +129,10 @@
"noTunnels": "还没有专线",
"expireTime": "到期时间",
"localNode": "本机节点",
"authMethod": "认证方式",
"authUnknown": "未指定",
"wgAuthPassword": "客户端账号 + 设备密钥",
"wgAuthHint": "客户端账号用于领取设备授权;WireGuard 用密钥握手。手动添加的静态 Peer 仍凭密钥接入。",
"start": "启动",
"stop": "停止",
"started": "已启动",
@@ -768,6 +772,14 @@
"label": "服务端 VPN 网段地址",
"tip": "服务端在 VPN 网段中的地址(CIDR)"
},
"auth_mode": {
"label": "客户端认证方式",
"tip": "账号验证后领取设备授权,隧道使用密钥握手。静态 Peer 仍凭密钥接入;飞书/OIDC 需独立实现客户端回调和用户授权映射。",
"options": {
"password": "客户端账号 + 设备密钥",
"oidc": "第三方 OAuth2 / OIDC(尚未接入)"
}
},
"listen_port": {
"label": "监听端口"
},
@@ -786,6 +798,10 @@
"label": "客户端 AllowedIPs",
"tip": "全流量代理用 0.0.0.0/0;仅内网填内网网段"
},
"latency_probe": {
"label": "隧道测速目标",
"tip": "可经该专线访问的 IP:端口,如 10.88.0.1:22;未设置时测试公网 223.5.5.5:443。记录 TCP 建链耗时。"
},
"keepalive": {
"label": "Keepalive(秒)",
"tip": "NAT 后客户端建议 15–25"
+5
View File
@@ -9,6 +9,10 @@ export const PROTOCOL_META = {
icon: 'wireguard',
scenes: 'protocols.wireguard.scenes',
fields: [
{ key: 'auth_mode', label: 'protocols.wireguard.fields.auth_mode.label', type: 'select', options: [
{ value: 'password', label: 'protocols.wireguard.fields.auth_mode.options.password' },
{ value: 'oidc', label: 'protocols.wireguard.fields.auth_mode.options.oidc', disabled: true }
], default: 'password', tip: 'protocols.wireguard.fields.auth_mode.tip', group: 'basic' },
{ key: 'address', label: 'protocols.wireguard.fields.address.label', placeholder: '10.8.0.1/24', required: true, tip: 'protocols.wireguard.fields.address.tip', group: 'basic' },
{ key: 'listen_port', label: 'protocols.wireguard.fields.listen_port.label', type: 'number', default: 51820, group: 'basic' },
{ key: 'client_address', label: 'protocols.wireguard.fields.client_address.label', placeholder: '10.8.0.2/32', tip: 'protocols.wireguard.fields.client_address.tip', group: 'basic' },
@@ -17,6 +21,7 @@ export const PROTOCOL_META = {
{ key: 'allowed_ips', label: 'protocols.wireguard.fields.allowed_ips.label', default: '0.0.0.0/0, ::/0', tip: 'protocols.wireguard.fields.allowed_ips.tip', group: 'routing' },
{ key: 'keepalive', label: 'protocols.wireguard.fields.keepalive.label', type: 'number', default: 25, tip: 'protocols.wireguard.fields.keepalive.tip', group: 'advanced' },
{ key: 'mtu', label: 'protocols.wireguard.fields.mtu.label', type: 'number', default: 0, tip: 'protocols.wireguard.fields.mtu.tip', group: 'advanced' },
{ key: 'latency_probe', label: 'protocols.wireguard.fields.latency_probe.label', placeholder: '10.88.0.1:22', tip: 'protocols.wireguard.fields.latency_probe.tip', group: 'advanced' },
{ key: 'enable_nat', label: 'protocols.wireguard.fields.enable_nat.label', type: 'switch', default: false, tip: 'protocols.wireguard.fields.enable_nat.tip', group: 'routing' },
{ key: 'egress_interface', label: 'protocols.wireguard.fields.egress_interface.label', placeholder: 'eth0', tip: 'protocols.wireguard.fields.egress_interface.tip', showIf: { enable_nat: true }, group: 'routing' }
]
+2 -2
View File
@@ -22,7 +22,7 @@
<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-option v-for="option in field.options" :key="optionValue(option)" :label="$t(optionLabel(option))" :value="optionValue(option)" :disabled="option.disabled" />
</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')" />
@@ -49,7 +49,7 @@
<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-option v-for="option in field.options" :key="optionValue(option)" :label="$t(optionLabel(option))" :value="optionValue(option)" :disabled="option.disabled" />
</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')" />
+2 -2
View File
@@ -56,7 +56,7 @@
<el-input-number v-if="f.type === 'number'" v-model="form.config[f.key]" :min="0" :max="65535" />
<el-switch v-else-if="f.type === 'switch'" v-model="form.config[f.key]" />
<el-select v-else-if="f.type === 'select'" v-model="form.config[f.key]" style="width:100%">
<el-option v-for="o in f.options" :key="optionValue(o)" :label="$t(optionLabel(o))" :value="optionValue(o)" />
<el-option v-for="o in f.options" :key="optionValue(o)" :label="$t(optionLabel(o))" :value="optionValue(o)" :disabled="o.disabled" />
</el-select>
<el-input v-else-if="f.type === 'password'" v-model="form.config[f.key]" type="password"
show-password :placeholder="f.placeholder" />
@@ -86,7 +86,7 @@
<el-input-number v-if="f.type === 'number'" v-model="form.config[f.key]" :min="0" :max="65535" />
<el-switch v-else-if="f.type === 'switch'" v-model="form.config[f.key]" />
<el-select v-else-if="f.type === 'select'" v-model="form.config[f.key]" style="width:100%">
<el-option v-for="o in f.options" :key="optionValue(o)" :label="$t(optionLabel(o))" :value="optionValue(o)" />
<el-option v-for="o in f.options" :key="optionValue(o)" :label="$t(optionLabel(o))" :value="optionValue(o)" :disabled="o.disabled" />
</el-select>
<el-input v-else-if="f.type === 'password'" v-model="form.config[f.key]" type="password"
show-password :placeholder="f.placeholder" />
+24 -4
View File
@@ -46,6 +46,14 @@
</span>
</template>
</el-table-column>
<el-table-column :label="$t('tunnels.authMethod')" min-width="170">
<template #default="{ row }">
<el-tooltip v-if="row.protocol === 'wireguard'" :content="$t('tunnels.wgAuthHint')" placement="top">
<span class="auth-cell">{{ authMethod(row) }}</span>
</el-tooltip>
<span v-else class="auth-cell">{{ authMethod(row) }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('common.status')" width="110">
<template #default="{ row }">
<span :style="{ color: stateColor(row) }">{{ stateText(row) }}</span>
@@ -67,10 +75,10 @@
<el-table-column :label="$t('tunnels.node')" width="110">
<template #default="{ row }">{{ row.node?.name || $t('tunnels.localNode') }}</template>
</el-table-column>
<el-table-column :label="$t('common.operation')" width="240" fixed="right">
<el-table-column :label="$t('common.operation')" width="285" fixed="right">
<template #default="{ row }">
<el-button v-if="stateOf(row) !== 'running'" type="success" size="small" link @click="start(row)">{{ $t('tunnels.start') }}</el-button>
<el-button v-else type="warning" size="small" link @click="stop(row)">{{ $t('tunnels.stop') }}</el-button>
<el-button v-if="stateOf(row) !== 'running'" type="success" size="small" plain @click="start(row)">{{ $t('tunnels.start') }}</el-button>
<el-button v-else type="warning" size="small" plain @click="stop(row)">{{ $t('tunnels.stop') }}</el-button>
<el-button size="small" link @click="showDetail(row)">{{ $t('tunnels.detail') }}</el-button>
<el-button size="small" link @click="$router.push(`/tunnels/${row.id}/edit`)">{{ $t('common.edit') }}</el-button>
<el-dropdown trigger="click" @command="cmd => onMoreCommand(cmd, row)" style="margin-left:8px">
@@ -84,7 +92,7 @@
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button type="danger" size="small" link style="margin-left:12px" @click="remove(row)">{{ $t('common.delete') }}</el-button>
<el-button type="danger" size="small" link @click="remove(row)">{{ $t('common.delete') }}</el-button>
</template>
</el-table-column>
<template #empty>
@@ -99,6 +107,7 @@
<template v-if="detail">
<el-descriptions :column="1" border size="small">
<el-descriptions-item :label="$t('common.protocol')">{{ $t(protocolDisplay(detail.tunnel.protocol)) }}</el-descriptions-item>
<el-descriptions-item :label="$t('tunnels.authMethod')">{{ authMethod(detail.tunnel) }}</el-descriptions-item>
<el-descriptions-item :label="$t('common.status')">{{ stateText(detail.tunnel) }}</el-descriptions-item>
<el-descriptions-item :label="$t('tunnels.createTime')">{{ fmtTime(detail.tunnel.created_at) }}</el-descriptions-item>
<el-descriptions-item :label="$t('tunnels.autoStart')">{{ detail.tunnel.auto_start ? $t('common.yes') : $t('common.no') }}</el-descriptions-item>
@@ -301,6 +310,17 @@ function stateColor(row) {
return { running: '#67c23a', stopped: '#909399', error: '#f56c6c', undeployed: '#e6a23c' }[stateOf(row)]
}
function stateOf(row) { return row.runtime?.state || row.state }
function authMethod(row) {
let config = {}
try { config = typeof row.config === 'string' ? JSON.parse(row.config) : (row.config || {}) } catch { /* old malformed config */ }
if (row.protocol === 'wireguard') return !config.auth_mode || config.auth_mode === 'password' ? t('tunnels.wgAuthPassword') : t('tunnels.authUnknown')
if (row.protocol === 'openvpn') {
const mode = config.auth_mode || 'password'
const key = `protocols.openvpn.fields.auth_mode.options.${mode}`
return ['certificate', 'password', 'certificate+password', 'oidc'].includes(mode) ? t(key) : t('tunnels.authUnknown')
}
return t('tunnels.authUnknown')
}
function prettyConfig(c) {
try { return JSON.stringify(JSON.parse(c), null, 2) } catch { return c }
}