From 40274d8c92249043d8cddb6e40b4b23128e18df4 Mon Sep 17 00:00:00 2001 From: barkure <43804451+barkure@users.noreply.github.com> Date: Thu, 23 Jul 2026 22:02:49 +0800 Subject: [PATCH] web/frpc: support virtual net visitor plugin (#5414) --- web/frpc/components.d.ts | 1 + web/frpc/src/components/ConfigField.vue | 30 ++++++++++- .../visitor-form/VisitorConnectionSection.vue | 6 ++- .../visitor-form/VisitorFormLayout.vue | 6 +++ .../visitor-form/VisitorPluginSection.vue | 52 +++++++++++++++++++ web/frpc/src/types/proxy-converters.ts | 19 +++++++ web/frpc/src/types/proxy-form.ts | 7 +++ web/frpc/src/views/VisitorEdit.vue | 12 +++++ 8 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 web/frpc/src/components/visitor-form/VisitorPluginSection.vue diff --git a/web/frpc/components.d.ts b/web/frpc/components.d.ts index c58d0bca..675f376d 100644 --- a/web/frpc/components.d.ts +++ b/web/frpc/components.d.ts @@ -38,6 +38,7 @@ declare module 'vue' { VisitorBaseSection: typeof import('./src/components/visitor-form/VisitorBaseSection.vue')['default'] VisitorConnectionSection: typeof import('./src/components/visitor-form/VisitorConnectionSection.vue')['default'] VisitorFormLayout: typeof import('./src/components/visitor-form/VisitorFormLayout.vue')['default'] + VisitorPluginSection: typeof import('./src/components/visitor-form/VisitorPluginSection.vue')['default'] VisitorTransportSection: typeof import('./src/components/visitor-form/VisitorTransportSection.vue')['default'] VisitorXtcpSection: typeof import('./src/components/visitor-form/VisitorXtcpSection.vue')['default'] } diff --git a/web/frpc/src/components/ConfigField.vue b/web/frpc/src/components/ConfigField.vue index 27719847..67be4ba0 100644 --- a/web/frpc/src/components/ConfigField.vue +++ b/web/frpc/src/components/ConfigField.vue @@ -12,7 +12,7 @@ diff --git a/web/frpc/src/types/proxy-converters.ts b/web/frpc/src/types/proxy-converters.ts index 6587986e..eb4c4c17 100644 --- a/web/frpc/src/types/proxy-converters.ts +++ b/web/frpc/src/types/proxy-converters.ts @@ -190,6 +190,16 @@ export function formToStoreVisitor(form: VisitorFormData): VisitorDefinition { block.bindPort = form.bindPort } + if ( + form.pluginType === 'virtual_net' && + (form.type === 'stcp' || form.type === 'xtcp') + ) { + block.plugin = { + type: 'virtual_net', + destinationIP: form.pluginDestinationIP.trim(), + } + } + if (form.type === 'xtcp') { if (form.protocol && form.protocol !== 'quic') { block.protocol = form.protocol @@ -448,6 +458,15 @@ export function storeVisitorToForm( form.bindAddr = c.bindAddr || '127.0.0.1' form.bindPort = c.bindPort + // Visitor plugin (only supported by stcp/xtcp; sudp runtime never uses it) + if ( + c.plugin?.type === 'virtual_net' && + (form.type === 'stcp' || form.type === 'xtcp') + ) { + form.pluginType = 'virtual_net' + form.pluginDestinationIP = c.plugin.destinationIP || '' + } + // XTCP specific form.protocol = c.protocol || 'quic' form.keepTunnelOpen = c.keepTunnelOpen || false diff --git a/web/frpc/src/types/proxy-form.ts b/web/frpc/src/types/proxy-form.ts index 8f6bd614..793fe516 100644 --- a/web/frpc/src/types/proxy-form.ts +++ b/web/frpc/src/types/proxy-form.ts @@ -79,6 +79,10 @@ export interface VisitorFormData { bindAddr: string bindPort: number | undefined + // Visitor plugin + pluginType: '' | 'virtual_net' + pluginDestinationIP: string + // XTCP specific (XTCPVisitorConfig) protocol: string keepTunnelOpen: boolean @@ -156,6 +160,9 @@ export function createDefaultVisitorForm(): VisitorFormData { bindAddr: '127.0.0.1', bindPort: undefined, + pluginType: '', + pluginDestinationIP: '', + protocol: 'quic', keepTunnelOpen: false, maxRetriesAnHour: undefined, diff --git a/web/frpc/src/views/VisitorEdit.vue b/web/frpc/src/views/VisitorEdit.vue index 615e97e9..bf92e29e 100644 --- a/web/frpc/src/views/VisitorEdit.vue +++ b/web/frpc/src/views/VisitorEdit.vue @@ -107,6 +107,18 @@ const formRules: FormRules = { trigger: 'blur', }, ], + pluginDestinationIP: [ + { + validator: (_rule, value, callback) => { + if (form.value.pluginType === 'virtual_net' && !value?.trim()) { + callback(new Error('Destination IP is required for virtual_net')) + return + } + callback() + }, + trigger: 'blur', + }, + ], } const goBack = () => {