refactor(web): use generated proto network types (#2373)

* refactor(web): use generated proto network types
* fix(core): preserve dumped config flags
* test(web): cover config flag save paths
* fix(ci): use system protoc before frontend codegen
* fix(ci): serialize frontend-lib builds
This commit is contained in:
KKRainbow
2026-06-27 13:09:28 +08:00
committed by GitHub
parent 034f5066cd
commit f0d00d6161
22 changed files with 5035 additions and 247 deletions
@@ -9,7 +9,7 @@ import {
normalizeNetworkConfig,
removeRow
} from '../types/network'
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import AclManager from './acl/AclManager.vue'
import UrlListInput from './UrlListInput.vue'
@@ -134,6 +134,7 @@ function savePortForward() {
const portForwardContainer = ref<HTMLElement | null>(null);
const isCompact = ref(false);
const UINT64_MAX = (1n << 64n) - 1n
onMounted(() => {
if (portForwardContainer.value) {
@@ -161,6 +162,39 @@ function syncNormalizedNetwork(network: NetworkConfig | undefined): void {
}
watch(() => curNetwork.value, syncNormalizedNetwork, { immediate: true, deep: false })
function parseInstanceRecvBpsLimitInput(value: string): number | string | null | undefined {
const trimmed = value.trim()
if (trimmed.length === 0) {
return null
}
if (!/^\d+$/.test(trimmed)) {
return undefined
}
const limit = BigInt(trimmed)
if (limit === 0n) {
return null
}
if (limit > UINT64_MAX) {
return undefined
}
return limit <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(limit) : limit.toString()
}
const instanceRecvBpsLimitInput = computed<string>({
get: () => {
const limit = curNetwork.value.instance_recv_bps_limit
return limit == null ? '' : String(limit)
},
set: (value) => {
const limit = parseInstanceRecvBpsLimitInput(value)
if (limit !== undefined) {
curNetwork.value.instance_recv_bps_limit = limit
}
},
})
</script>
<template>
@@ -317,9 +351,9 @@ watch(() => curNetwork.value, syncNormalizedNetwork, { immediate: true, deep: fa
<span class="pi pi-question-circle ml-2 self-center"
v-tooltip="t('instance_recv_bps_limit_help')"></span>
</div>
<InputNumber id="instance_recv_bps_limit" v-model="curNetwork.instance_recv_bps_limit"
aria-describedby="instance_recv_bps_limit-help" :format="false"
:placeholder="t('instance_recv_bps_limit_placeholder')" :min="1" fluid />
<InputText id="instance_recv_bps_limit" v-model="instanceRecvBpsLimitInput"
aria-describedby="instance_recv_bps_limit-help" inputmode="numeric" pattern="[0-9]*"
:placeholder="t('instance_recv_bps_limit_placeholder')" fluid />
</div>
</div>
+95 -171
View File
@@ -1,166 +1,49 @@
import { v4 as uuidv4 } from 'uuid'
import {
NetworkConfig as NetworkConfigPb,
NetworkingMethod,
type NetworkConfig as ProtoNetworkConfig,
type PortForwardConfig,
} from '../generated/proto/api_manage'
import {
Action as AclAction,
ChainType as AclChainType,
Protocol as AclProtocol,
type Acl,
type AclV1,
type Chain as AclChain,
type GroupIdentity,
type GroupInfo,
type Rule as AclRule,
} from '../generated/proto/acl'
import { CompressionAlgoPb, NatType, type SecureModeConfig } from '../generated/proto/common'
import { prepareNetworkConfigForProtoJson } from './networkCompat'
export enum NetworkingMethod {
PublicServer = 0,
Manual = 1,
Standalone = 2,
}
export { AclAction, AclChainType, AclProtocol, CompressionAlgoPb, NatType, NetworkingMethod }
export type { Acl, AclChain, AclRule, AclV1, GroupIdentity, GroupInfo, PortForwardConfig, SecureModeConfig }
export interface SecureModeConfig {
enabled: boolean
// Keep protocol compatibility with backend/import-export flows even though the GUI
// does not render secure-mode or credential inputs.
local_private_key?: string
local_public_key?: string
}
export enum AclProtocol {
Unspecified = 0,
TCP = 1,
UDP = 2,
ICMP = 3,
ICMPv6 = 4,
Any = 5,
}
export enum AclAction {
Noop = 0,
Allow = 1,
Drop = 2,
}
export enum AclChainType {
UnspecifiedChain = 0,
Inbound = 1,
Outbound = 2,
Forward = 3,
}
export interface AclRule {
name: string
description: string
priority: number
enabled: boolean
protocol: AclProtocol
ports: string[]
source_ips: string[]
destination_ips: string[]
source_ports: string[]
action: AclAction
rate_limit: number
burst_limit: number
stateful: boolean
source_groups: string[]
destination_groups: string[]
}
export interface AclChain {
name: string
chain_type: AclChainType
description: string
enabled: boolean
rules: AclRule[]
default_action: AclAction
}
export interface GroupIdentity {
group_name: string
group_secret: string
}
export interface GroupInfo {
declares: GroupIdentity[]
members: string[]
}
export interface AclV1 {
chains: AclChain[]
group?: GroupInfo
}
export interface Acl {
acl_v1?: AclV1
}
export interface NetworkConfig {
export type NetworkConfig = Omit<
ProtoNetworkConfig,
'instance_id' | 'instance_recv_bps_limit' | 'mtu' | 'networking_method'
> & {
instance_id: string
dhcp: boolean
virtual_ipv4: string
network_length: number
hostname?: string
network_name: string
network_secret?: string
credential_file?: string
secure_mode?: SecureModeConfig
networking_method: NetworkingMethod
public_server_url: string
peer_urls: string[]
proxy_cidrs: string[]
enable_vpn_portal: boolean
vpn_portal_listen_port: number
vpn_portal_client_network_addr: string
vpn_portal_client_network_len: number
advanced_settings: boolean
listener_urls: string[]
latency_first: boolean
dev_name: string
use_smoltcp?: boolean
disable_ipv6?: boolean
ipv6_public_addr_auto?: boolean
enable_kcp_proxy?: boolean
disable_kcp_input?: boolean
enable_quic_proxy?: boolean
disable_quic_input?: boolean
disable_p2p?: boolean
p2p_only?: boolean
lazy_p2p?: boolean
bind_device?: boolean
no_tun?: boolean
enable_exit_node?: boolean
relay_all_peer_rpc?: boolean
need_p2p?: boolean
multi_thread?: boolean
proxy_forward_by_system?: boolean
disable_encryption?: boolean
disable_tcp_hole_punching?: boolean
disable_udp_hole_punching?: boolean
disable_upnp?: boolean
enable_udp_broadcast_relay?: boolean
disable_sym_hole_punching?: boolean
enable_relay_network_whitelist?: boolean
relay_network_whitelist: string[]
enable_manual_routes: boolean
routes: string[]
exit_nodes: string[]
enable_socks5?: boolean
socks5_port: number
mtu: number | null
instance_recv_bps_limit: number | null
mapped_listeners: string[]
instance_recv_bps_limit: number | string | null
networking_method: NetworkingMethod | string
}
enable_magic_dns?: boolean
enable_private_mode?: boolean
const UINT64_MAX = (1n << 64n) - 1n
port_forwards: PortForwardConfig[]
acl?: Acl
interface NetworkingConfigFields {
peer_urls: string[]
public_server_url?: string
networking_method?: NetworkingMethod | string
}
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
return {
...NetworkConfigPb.create(),
instance_id: uuidv4(),
dhcp: true,
@@ -243,33 +126,82 @@ function cleanPeerUrls(urls: string[] | undefined): string[] {
return (urls ?? []).map((url) => url.trim()).filter((url) => url.length > 0)
}
export function normalizeNetworkConfig(config: NetworkConfig): NetworkConfig {
const normalized: NetworkConfig = {
...config,
peer_urls: cleanPeerUrls(config.peer_urls),
function normalizeUint64ForInput(v: bigint | number | string | null | undefined): number | string | null {
if (v == null) return null
try {
const n = typeof v === 'bigint' ? v : BigInt(v)
if (n === 0n || n > UINT64_MAX) return null
return n <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(n) : n.toString()
} catch {
return null
}
}
const publicServerUrl = normalized.public_server_url?.trim() ?? ''
function normalizeNumberForInput(v: number | string | null | undefined): number | null {
if (v == null) return null
const n = Number(v)
return Number.isFinite(n) ? n : null
}
switch (normalized.networking_method) {
function toBackendUint64(v: number | bigint | string | null | undefined): bigint | undefined {
if (v == null || v === '') return undefined
try {
const n = typeof v === 'bigint' ? v : BigInt(v)
return n > 0n && n <= UINT64_MAX ? n : undefined
} catch {
return undefined
}
}
function applyNetworkingMethod(config: NetworkingConfigFields): void {
config.peer_urls = cleanPeerUrls(config.peer_urls)
const publicServerUrl = config.public_server_url?.trim() ?? ''
const networkingMethod = config.networking_method ?? NetworkingMethod.Manual
switch (networkingMethod) {
case NetworkingMethod.PublicServer:
normalized.peer_urls = publicServerUrl ? [publicServerUrl] : []
config.peer_urls = publicServerUrl ? [publicServerUrl] : []
break
case NetworkingMethod.Manual:
break
case NetworkingMethod.Standalone:
default:
normalized.peer_urls = []
config.peer_urls = []
break
}
normalized.networking_method = NetworkingMethod.Manual
normalized.public_server_url = ''
config.networking_method = NetworkingMethod.Manual
config.public_server_url = ''
}
export function normalizeNetworkConfig(config: NetworkConfig): NetworkConfig {
const normalized = NetworkConfigPb.fromJson(prepareNetworkConfigForProtoJson(config) as any, {
ignoreUnknownFields: true,
}) as unknown as NetworkConfig
applyNetworkingMethod(normalized)
normalized.mtu = normalizeNumberForInput(normalized.mtu)
normalized.instance_recv_bps_limit = normalizeUint64ForInput(
normalized.instance_recv_bps_limit as any,
)
return normalized
}
export function toBackendNetworkConfig(config: NetworkConfig): NetworkConfig {
return normalizeNetworkConfig(config)
const backend = NetworkConfigPb.fromJson(prepareNetworkConfigForProtoJson(config) as any, {
ignoreUnknownFields: true,
})
applyNetworkingMethod(backend)
backend.mtu = normalizeNumberForInput(config.mtu) ?? undefined
backend.instance_recv_bps_limit = toBackendUint64(config.instance_recv_bps_limit)
return NetworkConfigPb.toJson(backend, {
useProtoFieldName: true,
}) as unknown as NetworkConfig
}
export interface NetworkInstance {
@@ -397,14 +329,6 @@ export interface PeerConnStats {
latency_us: number
}
export interface PortForwardConfig {
bind_ip: string,
bind_port: number,
dst_ip: string,
dst_port: number,
proto: string
}
// 添加新行
export const addRow = (rows: PortForwardConfig[]) => {
rows.push({
@@ -0,0 +1,85 @@
import {
Action as AclAction,
ChainType as AclChainType,
Protocol as AclProtocol,
} from '../generated/proto/acl'
import type { NetworkConfig } from './network'
const UINT64_MAX = (1n << 64n) - 1n
type JsonRecord = Record<string, unknown>
export function prepareNetworkConfigForProtoJson(config: NetworkConfig): NetworkConfig {
const prepared = dropUnsupportedJsonValues(applyLegacyAclDefaults(config)) as NetworkConfig
normalizeLegacyOptionalUint64(prepared as JsonRecord, 'instance_recv_bps_limit')
return prepared
}
function applyLegacyAclDefaults(config: NetworkConfig): NetworkConfig {
const acl = config.acl
const aclV1 = acl?.acl_v1
if (!Array.isArray(aclV1?.chains)) return config
return {
...config,
acl: {
...acl,
acl_v1: {
...aclV1,
chains: aclV1.chains.map((chain) => ({
...chain,
chain_type: chain.chain_type ?? AclChainType.UnspecifiedChain,
default_action: chain.default_action ?? AclAction.Allow,
rules: (chain.rules ?? []).map((rule) => ({
...rule,
protocol: rule.protocol ?? AclProtocol.Any,
action: rule.action ?? AclAction.Allow,
})),
})),
},
},
}
}
function dropUnsupportedJsonValues(value: unknown): unknown {
if (value === undefined) return undefined
if (typeof value === 'number' && !Number.isFinite(value)) return undefined
if (Array.isArray(value)) {
return value.map(dropUnsupportedJsonValues).filter((v) => v !== undefined)
}
if (isJsonRecord(value)) {
return Object.fromEntries(
Object.entries(value)
.map(([k, v]) => [k, dropUnsupportedJsonValues(v)])
.filter(([, v]) => v !== undefined),
)
}
return value
}
function isJsonRecord(value: unknown): value is JsonRecord {
return typeof value === 'object' && value !== null
}
function normalizeLegacyOptionalUint64(obj: JsonRecord, key: string): void {
const value = obj[key]
if (typeof value !== 'string') return
const trimmed = value.trim()
if (!isPositiveUint64String(trimmed)) {
delete obj[key]
return
}
obj[key] = trimmed
}
function isPositiveUint64String(value: string): boolean {
if (!/^\d+$/.test(value)) return false
const n = BigInt(value)
return n > 0n && n <= UINT64_MAX
}