feat: 支持 ipv4/ipv6 双栈访问

This commit is contained in:
mofeng
2026-01-30 14:47:41 +08:00
parent 58f9020192
commit 6a110258b9
13 changed files with 465 additions and 107 deletions

View File

@@ -336,6 +336,7 @@ export const rustdeskConfigApi = {
export interface WebConfig {
http_port: number
https_port: number
bind_addresses: string[]
bind_address: string
https_enabled: boolean
}
@@ -344,6 +345,7 @@ export interface WebConfig {
export interface WebConfigUpdate {
http_port?: number
https_port?: number
bind_addresses?: string[]
bind_address?: string
https_enabled?: boolean
}

View File

@@ -480,10 +480,22 @@ export default {
configureHttpPort: 'Configure HTTP server port',
// Web server
webServer: 'Access Address',
webServerDesc: 'Configure HTTP/HTTPS ports and bind address. Restart required for changes to take effect.',
webServerDesc: 'Configure HTTP/HTTPS ports and listening addresses. Restart required for changes to take effect.',
httpsPort: 'HTTPS Port',
bindAddress: 'Bind Address',
bindAddressDesc: 'IP address the server listens on. 0.0.0.0 means all network interfaces.',
bindMode: 'Listening Address',
bindModeDesc: 'Choose which addresses the web server binds to.',
bindModeAll: 'All addresses',
bindModeLocal: 'Local only (127.0.0.1)',
bindModeCustom: 'Custom address list',
bindIpv6: 'Enable IPv6',
bindAllDesc: 'Also listen on :: (all IPv6 interfaces).',
bindLocalDesc: 'Also listen on ::1 (IPv6 loopback).',
bindAddressList: 'Address List',
bindAddressListDesc: 'One IP address per line (IPv4 or IPv6).',
addBindAddress: 'Add address',
bindAddressListEmpty: 'Add at least one IP address.',
httpsEnabled: 'Enable HTTPS',
httpsEnabledDesc: 'Enable HTTPS encrypted connection (self-signed certificate will be auto-generated)',
restartRequired: 'Restart Required',

View File

@@ -480,10 +480,22 @@ export default {
configureHttpPort: '配置 HTTP 服务器端口',
// Web server
webServer: '访问地址',
webServerDesc: '配置 HTTP/HTTPS 端口和绑定地址,修改后需要重启生效',
webServerDesc: '配置 HTTP/HTTPS 端口和监听地址,修改后需要重启生效',
httpsPort: 'HTTPS 端口',
bindAddress: '绑定地址',
bindAddressDesc: '服务器监听的 IP 地址0.0.0.0 表示监听所有网络接口',
bindMode: '监听地址',
bindModeDesc: '选择 Web 服务监听哪些地址。',
bindModeAll: '所有地址',
bindModeLocal: '仅本地 (127.0.0.1)',
bindModeCustom: '自定义地址列表',
bindIpv6: '启用 IPv6',
bindAllDesc: '同时监听 ::(所有 IPv6 地址)。',
bindLocalDesc: '同时监听 ::1IPv6 本地回环)。',
bindAddressList: '地址列表',
bindAddressListDesc: '每行一个 IPIPv4 或 IPv6。',
addBindAddress: '添加地址',
bindAddressListEmpty: '请至少填写一个 IP 地址。',
httpsEnabled: '启用 HTTPS',
httpsEnabledDesc: '启用 HTTPS 加密连接(将自动生成自签名证书)',
restartRequired: '需要重启',

View File

@@ -282,7 +282,9 @@ export interface WebConfig {
http_port: number;
/** HTTPS port */
https_port: number;
/** Bind address */
/** Bind addresses (preferred) */
bind_addresses: string[];
/** Bind address (legacy) */
bind_address: string;
/** Enable HTTPS */
https_enabled: boolean;
@@ -625,6 +627,7 @@ export interface VideoConfigUpdate {
export interface WebConfigUpdate {
http_port?: number;
https_port?: number;
bind_addresses?: string[];
bind_address?: string;
https_enabled?: boolean;
}

View File

@@ -72,6 +72,7 @@ import {
Square,
ChevronRight,
Plus,
Trash2,
ExternalLink,
Copy,
ScreenShare,
@@ -196,11 +197,32 @@ const webServerConfig = ref<WebConfig>({
http_port: 8080,
https_port: 8443,
bind_address: '0.0.0.0',
bind_addresses: ['0.0.0.0'],
https_enabled: false,
})
const webServerLoading = ref(false)
const showRestartDialog = ref(false)
const restarting = ref(false)
type BindMode = 'all' | 'loopback' | 'custom'
const bindMode = ref<BindMode>('all')
const bindAllIpv6 = ref(false)
const bindLocalIpv6 = ref(false)
const bindAddressList = ref<string[]>([])
const bindAddressError = computed(() => {
if (bindMode.value !== 'custom') return ''
return normalizeBindAddresses(bindAddressList.value).length
? ''
: t('settings.bindAddressListEmpty')
})
const effectiveBindAddresses = computed(() => {
if (bindMode.value === 'all') {
return bindAllIpv6.value ? ['0.0.0.0', '::'] : ['0.0.0.0']
}
if (bindMode.value === 'loopback') {
return bindLocalIpv6.value ? ['127.0.0.1', '::1'] : ['127.0.0.1']
}
return normalizeBindAddresses(bindAddressList.value)
})
// Config
interface DeviceConfig {
@@ -320,6 +342,12 @@ watch(() => config.value.msd_enabled, (enabled) => {
}
})
watch(bindMode, (mode) => {
if (mode === 'custom' && bindAddressList.value.length === 0) {
bindAddressList.value = ['']
}
})
// ATX config state
const atxConfig = ref({
enabled: false,
@@ -987,20 +1015,72 @@ function normalizeRustdeskServer(value: string, defaultPort: number): string | u
return `${trimmed}:${defaultPort}`
}
function normalizeBindAddresses(addresses: string[]): string[] {
return addresses.map(addr => addr.trim()).filter(Boolean)
}
function applyBindStateFromConfig(config: WebConfig) {
const rawAddrs =
config.bind_addresses && config.bind_addresses.length > 0
? config.bind_addresses
: config.bind_address
? [config.bind_address]
: []
const addrs = normalizeBindAddresses(rawAddrs)
const isAll = addrs.length > 0 && addrs.every(addr => addr === '0.0.0.0' || addr === '::') && addrs.includes('0.0.0.0')
const isLoopback =
addrs.length > 0 &&
addrs.every(addr => addr === '127.0.0.1' || addr === '::1') &&
addrs.includes('127.0.0.1')
if (isAll) {
bindMode.value = 'all'
bindAllIpv6.value = addrs.includes('::')
return
}
if (isLoopback) {
bindMode.value = 'loopback'
bindLocalIpv6.value = addrs.includes('::1')
return
}
bindMode.value = 'custom'
bindAddressList.value = addrs.length ? [...addrs] : ['']
}
function addBindAddress() {
bindAddressList.value.push('')
}
function removeBindAddress(index: number) {
bindAddressList.value.splice(index, 1)
if (bindAddressList.value.length === 0) {
bindAddressList.value.push('')
}
}
// Web server config functions
async function loadWebServerConfig() {
try {
const config = await webConfigApi.get()
webServerConfig.value = config
applyBindStateFromConfig(config)
} catch (e) {
console.error('Failed to load web server config:', e)
}
}
async function saveWebServerConfig() {
if (bindAddressError.value) return
webServerLoading.value = true
try {
await webConfigApi.update(webServerConfig.value)
const update = {
http_port: webServerConfig.value.http_port,
https_port: webServerConfig.value.https_port,
https_enabled: webServerConfig.value.https_enabled,
bind_addresses: effectiveBindAddresses.value,
}
const updated = await webConfigApi.update(update)
webServerConfig.value = updated
applyBindStateFromConfig(updated)
showRestartDialog.value = true
} catch (e) {
console.error('Failed to save web server config:', e)
@@ -1687,13 +1767,51 @@ onMounted(async () => {
</div>
<div class="space-y-2">
<Label>{{ t('settings.bindAddress') }}</Label>
<Input v-model="webServerConfig.bind_address" placeholder="0.0.0.0" />
<p class="text-sm text-muted-foreground">{{ t('settings.bindAddressDesc') }}</p>
<Label>{{ t('settings.bindMode') }}</Label>
<select v-model="bindMode" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
<option value="all">{{ t('settings.bindModeAll') }}</option>
<option value="loopback">{{ t('settings.bindModeLocal') }}</option>
<option value="custom">{{ t('settings.bindModeCustom') }}</option>
</select>
<p class="text-sm text-muted-foreground">{{ t('settings.bindModeDesc') }}</p>
</div>
<div v-if="bindMode === 'all'" class="flex items-center justify-between">
<div class="space-y-0.5">
<Label>{{ t('settings.bindIpv6') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.bindAllDesc') }}</p>
</div>
<Switch v-model="bindAllIpv6" />
</div>
<div v-if="bindMode === 'loopback'" class="flex items-center justify-between">
<div class="space-y-0.5">
<Label>{{ t('settings.bindIpv6') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.bindLocalDesc') }}</p>
</div>
<Switch v-model="bindLocalIpv6" />
</div>
<div v-if="bindMode === 'custom'" class="space-y-2">
<Label>{{ t('settings.bindAddressList') }}</Label>
<div class="space-y-2">
<div v-for="(_, i) in bindAddressList" :key="`bind-${i}`" class="flex gap-2">
<Input v-model="bindAddressList[i]" placeholder="192.168.1.10" />
<Button variant="ghost" size="icon" @click="removeBindAddress(i)">
<Trash2 class="h-4 w-4" />
</Button>
</div>
<Button variant="outline" size="sm" @click="addBindAddress">
<Plus class="h-4 w-4 mr-1" />
{{ t('settings.addBindAddress') }}
</Button>
</div>
<p class="text-xs text-muted-foreground">{{ t('settings.bindAddressListDesc') }}</p>
<p v-if="bindAddressError" class="text-xs text-destructive">{{ bindAddressError }}</p>
</div>
<div class="flex justify-end pt-4">
<Button @click="saveWebServerConfig" :disabled="webServerLoading">
<Button @click="saveWebServerConfig" :disabled="webServerLoading || !!bindAddressError">
<Save class="h-4 w-4 mr-2" />
{{ t('common.save') }}
</Button>