mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-02-02 02:51:53 +08:00
feat: 支持 ipv4/ipv6 双栈访问
This commit is contained in:
@@ -576,7 +576,9 @@ pub struct WebConfig {
|
|||||||
pub http_port: u16,
|
pub http_port: u16,
|
||||||
/// HTTPS port
|
/// HTTPS port
|
||||||
pub https_port: u16,
|
pub https_port: u16,
|
||||||
/// Bind address
|
/// Bind addresses (preferred)
|
||||||
|
pub bind_addresses: Vec<String>,
|
||||||
|
/// Bind address (legacy)
|
||||||
pub bind_address: String,
|
pub bind_address: String,
|
||||||
/// Enable HTTPS
|
/// Enable HTTPS
|
||||||
pub https_enabled: bool,
|
pub https_enabled: bool,
|
||||||
@@ -591,6 +593,7 @@ impl Default for WebConfig {
|
|||||||
Self {
|
Self {
|
||||||
http_port: 8080,
|
http_port: 8080,
|
||||||
https_port: 8443,
|
https_port: 8443,
|
||||||
|
bind_addresses: Vec::new(),
|
||||||
bind_address: "0.0.0.0".to_string(),
|
bind_address: "0.0.0.0".to_string(),
|
||||||
https_enabled: false,
|
https_enabled: false,
|
||||||
ssl_cert_path: None,
|
ssl_cert_path: None,
|
||||||
|
|||||||
109
src/main.rs
109
src/main.rs
@@ -1,9 +1,11 @@
|
|||||||
use std::net::SocketAddr;
|
use std::collections::HashSet;
|
||||||
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum_server::tls_rustls::RustlsConfig;
|
use axum_server::tls_rustls::RustlsConfig;
|
||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
|
use futures::{stream::FuturesUnordered, StreamExt};
|
||||||
use rustls::crypto::{ring, CryptoProvider};
|
use rustls::crypto::{ring, CryptoProvider};
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
@@ -19,6 +21,7 @@ use one_kvm::msd::MsdController;
|
|||||||
use one_kvm::otg::{configfs, OtgService};
|
use one_kvm::otg::{configfs, OtgService};
|
||||||
use one_kvm::rustdesk::RustDeskService;
|
use one_kvm::rustdesk::RustDeskService;
|
||||||
use one_kvm::state::AppState;
|
use one_kvm::state::AppState;
|
||||||
|
use one_kvm::utils::bind_tcp_listener;
|
||||||
use one_kvm::video::format::{PixelFormat, Resolution};
|
use one_kvm::video::format::{PixelFormat, Resolution};
|
||||||
use one_kvm::video::{Streamer, VideoStreamManager};
|
use one_kvm::video::{Streamer, VideoStreamManager};
|
||||||
use one_kvm::web;
|
use one_kvm::web;
|
||||||
@@ -134,7 +137,8 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// Apply CLI argument overrides to config (only if explicitly specified)
|
// Apply CLI argument overrides to config (only if explicitly specified)
|
||||||
if let Some(addr) = args.address {
|
if let Some(addr) = args.address {
|
||||||
config.web.bind_address = addr;
|
config.web.bind_address = addr.clone();
|
||||||
|
config.web.bind_addresses = vec![addr];
|
||||||
}
|
}
|
||||||
if let Some(port) = args.http_port {
|
if let Some(port) = args.http_port {
|
||||||
config.web.http_port = port;
|
config.web.http_port = port;
|
||||||
@@ -153,19 +157,18 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
config.web.ssl_key_path = Some(key_path.to_string_lossy().to_string());
|
config.web.ssl_key_path = Some(key_path.to_string_lossy().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log final configuration
|
let bind_ips = resolve_bind_addresses(&config.web)?;
|
||||||
if config.web.https_enabled {
|
let scheme = if config.web.https_enabled { "https" } else { "http" };
|
||||||
tracing::info!(
|
let bind_port = if config.web.https_enabled {
|
||||||
"Server will listen on: https://{}:{}",
|
config.web.https_port
|
||||||
config.web.bind_address,
|
|
||||||
config.web.https_port
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
tracing::info!(
|
config.web.http_port
|
||||||
"Server will listen on: http://{}:{}",
|
};
|
||||||
config.web.bind_address,
|
|
||||||
config.web.http_port
|
// Log final configuration
|
||||||
);
|
for ip in &bind_ips {
|
||||||
|
let addr = SocketAddr::new(*ip, bind_port);
|
||||||
|
tracing::info!("Server will listen on: {}://{}", scheme, addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize session store
|
// Initialize session store
|
||||||
@@ -598,12 +601,8 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
// Create router
|
// Create router
|
||||||
let app = web::create_router(state.clone());
|
let app = web::create_router(state.clone());
|
||||||
|
|
||||||
// Determine bind address based on HTTPS setting
|
// Bind sockets for configured addresses
|
||||||
let bind_addr: SocketAddr = if config.web.https_enabled {
|
let listeners = bind_tcp_listeners(&bind_ips, bind_port)?;
|
||||||
format!("{}:{}", config.web.bind_address, config.web.https_port).parse()?
|
|
||||||
} else {
|
|
||||||
format!("{}:{}", config.web.bind_address, config.web.http_port).parse()?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup graceful shutdown
|
// Setup graceful shutdown
|
||||||
let shutdown_signal = async move {
|
let shutdown_signal = async move {
|
||||||
@@ -640,33 +639,44 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
RustlsConfig::from_pem_file(&cert_path, &key_path).await?
|
RustlsConfig::from_pem_file(&cert_path, &key_path).await?
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!("Starting HTTPS server on {}", bind_addr);
|
let mut servers = FuturesUnordered::new();
|
||||||
|
for listener in listeners {
|
||||||
|
let local_addr = listener.local_addr()?;
|
||||||
|
tracing::info!("Starting HTTPS server on {}", local_addr);
|
||||||
|
|
||||||
let server = axum_server::bind_rustls(bind_addr, tls_config).serve(app.into_make_service());
|
let server = axum_server::from_tcp_rustls(listener, tls_config.clone())?
|
||||||
|
.serve(app.clone().into_make_service());
|
||||||
|
servers.push(async move { server.await });
|
||||||
|
}
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = shutdown_signal => {
|
_ = shutdown_signal => {
|
||||||
cleanup(&state).await;
|
cleanup(&state).await;
|
||||||
}
|
}
|
||||||
result = server => {
|
result = servers.next() => {
|
||||||
if let Err(e) = result {
|
if let Some(Err(e)) = result {
|
||||||
tracing::error!("HTTPS server error: {}", e);
|
tracing::error!("HTTPS server error: {}", e);
|
||||||
}
|
}
|
||||||
cleanup(&state).await;
|
cleanup(&state).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tracing::info!("Starting HTTP server on {}", bind_addr);
|
let mut servers = FuturesUnordered::new();
|
||||||
|
for listener in listeners {
|
||||||
|
let local_addr = listener.local_addr()?;
|
||||||
|
tracing::info!("Starting HTTP server on {}", local_addr);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(bind_addr).await?;
|
let listener = tokio::net::TcpListener::from_std(listener)?;
|
||||||
let server = axum::serve(listener, app);
|
let server = axum::serve(listener, app.clone());
|
||||||
|
servers.push(async move { server.await });
|
||||||
|
}
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = shutdown_signal => {
|
_ = shutdown_signal => {
|
||||||
cleanup(&state).await;
|
cleanup(&state).await;
|
||||||
}
|
}
|
||||||
result = server => {
|
result = servers.next() => {
|
||||||
if let Err(e) = result {
|
if let Some(Err(e)) = result {
|
||||||
tracing::error!("HTTP server error: {}", e);
|
tracing::error!("HTTP server error: {}", e);
|
||||||
}
|
}
|
||||||
cleanup(&state).await;
|
cleanup(&state).await;
|
||||||
@@ -719,6 +729,47 @@ fn get_data_dir() -> PathBuf {
|
|||||||
PathBuf::from("/etc/one-kvm")
|
PathBuf::from("/etc/one-kvm")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve bind IPs from config, preferring bind_addresses when set.
|
||||||
|
fn resolve_bind_addresses(web: &config::WebConfig) -> anyhow::Result<Vec<IpAddr>> {
|
||||||
|
let raw_addrs = if !web.bind_addresses.is_empty() {
|
||||||
|
web.bind_addresses.as_slice()
|
||||||
|
} else {
|
||||||
|
std::slice::from_ref(&web.bind_address)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
let mut addrs = Vec::new();
|
||||||
|
for addr in raw_addrs {
|
||||||
|
let ip: IpAddr = addr
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| anyhow::anyhow!("Invalid bind address: {}", addr))?;
|
||||||
|
if seen.insert(ip) {
|
||||||
|
addrs.push(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(addrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bind_tcp_listeners(addrs: &[IpAddr], port: u16) -> anyhow::Result<Vec<std::net::TcpListener>> {
|
||||||
|
let mut listeners = Vec::new();
|
||||||
|
for ip in addrs {
|
||||||
|
let addr = SocketAddr::new(*ip, port);
|
||||||
|
match bind_tcp_listener(addr) {
|
||||||
|
Ok(listener) => listeners.push(listener),
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!("Failed to bind {}: {}", addr, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if listeners.is_empty() {
|
||||||
|
anyhow::bail!("Failed to bind any addresses on port {}", port);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(listeners)
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse video format and resolution from config (avoids code duplication)
|
/// Parse video format and resolution from config (avoids code duplication)
|
||||||
fn parse_video_config(config: &AppConfig) -> (PixelFormat, Resolution) {
|
fn parse_video_config(config: &AppConfig) -> (PixelFormat, Resolution) {
|
||||||
let format = config
|
let format = config
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ pub mod protocol;
|
|||||||
pub mod punch;
|
pub mod punch;
|
||||||
pub mod rendezvous;
|
pub mod rendezvous;
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ use tracing::{debug, error, info, warn};
|
|||||||
use crate::audio::AudioController;
|
use crate::audio::AudioController;
|
||||||
use crate::hid::HidController;
|
use crate::hid::HidController;
|
||||||
use crate::video::stream_manager::VideoStreamManager;
|
use crate::video::stream_manager::VideoStreamManager;
|
||||||
|
use crate::utils::bind_tcp_listener;
|
||||||
|
|
||||||
use self::config::RustDeskConfig;
|
use self::config::RustDeskConfig;
|
||||||
use self::connection::ConnectionManager;
|
use self::connection::ConnectionManager;
|
||||||
@@ -84,7 +85,7 @@ pub struct RustDeskService {
|
|||||||
status: Arc<RwLock<ServiceStatus>>,
|
status: Arc<RwLock<ServiceStatus>>,
|
||||||
rendezvous: Arc<RwLock<Option<Arc<RendezvousMediator>>>>,
|
rendezvous: Arc<RwLock<Option<Arc<RendezvousMediator>>>>,
|
||||||
rendezvous_handle: Arc<RwLock<Option<JoinHandle<()>>>>,
|
rendezvous_handle: Arc<RwLock<Option<JoinHandle<()>>>>,
|
||||||
tcp_listener_handle: Arc<RwLock<Option<JoinHandle<()>>>>,
|
tcp_listener_handle: Arc<RwLock<Option<Vec<JoinHandle<()>>>>>,
|
||||||
listen_port: Arc<RwLock<u16>>,
|
listen_port: Arc<RwLock<u16>>,
|
||||||
connection_manager: Arc<ConnectionManager>,
|
connection_manager: Arc<ConnectionManager>,
|
||||||
video_manager: Arc<VideoStreamManager>,
|
video_manager: Arc<VideoStreamManager>,
|
||||||
@@ -212,8 +213,8 @@ impl RustDeskService {
|
|||||||
|
|
||||||
// Start TCP listener BEFORE the rendezvous mediator to ensure port is set correctly
|
// Start TCP listener BEFORE the rendezvous mediator to ensure port is set correctly
|
||||||
// This prevents race condition where mediator starts registration with wrong port
|
// This prevents race condition where mediator starts registration with wrong port
|
||||||
let (tcp_handle, listen_port) = self.start_tcp_listener_with_port().await?;
|
let (tcp_handles, listen_port) = self.start_tcp_listener_with_port().await?;
|
||||||
*self.tcp_listener_handle.write() = Some(tcp_handle);
|
*self.tcp_listener_handle.write() = Some(tcp_handles);
|
||||||
|
|
||||||
// Set the listen port on mediator before starting the registration loop
|
// Set the listen port on mediator before starting the registration loop
|
||||||
mediator.set_listen_port(listen_port);
|
mediator.set_listen_port(listen_port);
|
||||||
@@ -373,52 +374,83 @@ impl RustDeskService {
|
|||||||
|
|
||||||
/// Start TCP listener for direct peer connections
|
/// Start TCP listener for direct peer connections
|
||||||
/// Returns the join handle and the port that was bound
|
/// Returns the join handle and the port that was bound
|
||||||
async fn start_tcp_listener_with_port(&self) -> anyhow::Result<(JoinHandle<()>, u16)> {
|
async fn start_tcp_listener_with_port(&self) -> anyhow::Result<(Vec<JoinHandle<()>>, u16)> {
|
||||||
// Try to bind to the default port, or find an available port
|
// Try to bind to the default port, or find an available port
|
||||||
let listener = match TcpListener::bind(format!("0.0.0.0:{}", DIRECT_LISTEN_PORT)).await {
|
let (listeners, listen_port) = match self.bind_direct_listeners(DIRECT_LISTEN_PORT) {
|
||||||
Ok(l) => l,
|
Ok(result) => result,
|
||||||
Err(_) => {
|
Err(err) => {
|
||||||
// Try binding to port 0 to get an available port
|
warn!(
|
||||||
TcpListener::bind("0.0.0.0:0").await?
|
"Failed to bind RustDesk TCP on port {}: {}, falling back to random port",
|
||||||
|
DIRECT_LISTEN_PORT, err
|
||||||
|
);
|
||||||
|
self.bind_direct_listeners(0)?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let local_addr = listener.local_addr()?;
|
|
||||||
let listen_port = local_addr.port();
|
|
||||||
*self.listen_port.write() = listen_port;
|
*self.listen_port.write() = listen_port;
|
||||||
info!("RustDesk TCP listener started on {}", local_addr);
|
|
||||||
|
|
||||||
let connection_manager = self.connection_manager.clone();
|
let connection_manager = self.connection_manager.clone();
|
||||||
let mut shutdown_rx = self.shutdown_tx.subscribe();
|
let mut handles = Vec::new();
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
for listener in listeners {
|
||||||
loop {
|
let local_addr = listener.local_addr()?;
|
||||||
tokio::select! {
|
info!("RustDesk TCP listener started on {}", local_addr);
|
||||||
result = listener.accept() => {
|
|
||||||
match result {
|
let conn_mgr = connection_manager.clone();
|
||||||
Ok((stream, peer_addr)) => {
|
let mut shutdown_rx = self.shutdown_tx.subscribe();
|
||||||
info!("Accepted direct connection from {}", peer_addr);
|
let handle = tokio::spawn(async move {
|
||||||
let conn_mgr = connection_manager.clone();
|
loop {
|
||||||
tokio::spawn(async move {
|
tokio::select! {
|
||||||
if let Err(e) = conn_mgr.accept_connection(stream, peer_addr).await {
|
result = listener.accept() => {
|
||||||
error!("Failed to handle direct connection from {}: {}", peer_addr, e);
|
match result {
|
||||||
}
|
Ok((stream, peer_addr)) => {
|
||||||
});
|
info!("Accepted direct connection from {}", peer_addr);
|
||||||
}
|
let conn_mgr = conn_mgr.clone();
|
||||||
Err(e) => {
|
tokio::spawn(async move {
|
||||||
error!("TCP accept error: {}", e);
|
if let Err(e) = conn_mgr.accept_connection(stream, peer_addr).await {
|
||||||
|
error!("Failed to handle direct connection from {}: {}", peer_addr, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("TCP accept error: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
_ = shutdown_rx.recv() => {
|
||||||
_ = shutdown_rx.recv() => {
|
info!("TCP listener shutting down");
|
||||||
info!("TCP listener shutting down");
|
break;
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
handles.push(handle);
|
||||||
|
}
|
||||||
|
|
||||||
Ok((handle, listen_port))
|
Ok((handles, listen_port))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bind_direct_listeners(&self, port: u16) -> anyhow::Result<(Vec<TcpListener>, u16)> {
|
||||||
|
let v4_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port);
|
||||||
|
let v4_listener = bind_tcp_listener(v4_addr)?;
|
||||||
|
let listen_port = v4_listener.local_addr()?.port();
|
||||||
|
|
||||||
|
let mut listeners = vec![TcpListener::from_std(v4_listener)?];
|
||||||
|
|
||||||
|
let v6_addr = SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), listen_port);
|
||||||
|
match bind_tcp_listener(v6_addr) {
|
||||||
|
Ok(v6_listener) => {
|
||||||
|
listeners.push(TcpListener::from_std(v6_listener)?);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"IPv6 listener unavailable on port {}: {}, continuing with IPv4 only",
|
||||||
|
listen_port, err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((listeners, listen_port))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop the RustDesk service
|
/// Stop the RustDesk service
|
||||||
@@ -446,8 +478,10 @@ impl RustDeskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait for TCP listener task to finish
|
// Wait for TCP listener task to finish
|
||||||
if let Some(handle) = self.tcp_listener_handle.write().take() {
|
if let Some(handles) = self.tcp_listener_handle.write().take() {
|
||||||
handle.abort();
|
for handle in handles {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*self.rendezvous.write() = None;
|
*self.rendezvous.write() = None;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
//! It registers the device ID and public key, handles punch hole requests,
|
//! It registers the device ID and public key, handles punch hole requests,
|
||||||
//! and relay requests.
|
//! and relay requests.
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ use tokio::sync::broadcast;
|
|||||||
use tokio::time::interval;
|
use tokio::time::interval;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
|
use crate::utils::bind_udp_socket;
|
||||||
|
|
||||||
use super::config::RustDeskConfig;
|
use super::config::RustDeskConfig;
|
||||||
use super::crypto::{KeyPair, SigningKeyPair};
|
use super::crypto::{KeyPair, SigningKeyPair};
|
||||||
use super::protocol::{
|
use super::protocol::{
|
||||||
@@ -288,8 +290,13 @@ impl RendezvousMediator {
|
|||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| anyhow::anyhow!("Failed to resolve {}", addr))?;
|
.ok_or_else(|| anyhow::anyhow!("Failed to resolve {}", addr))?;
|
||||||
|
|
||||||
// Create UDP socket
|
// Create UDP socket (match address family, enforce IPV6_V6ONLY)
|
||||||
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
let bind_addr = match server_addr {
|
||||||
|
SocketAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
|
||||||
|
SocketAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0),
|
||||||
|
};
|
||||||
|
let std_socket = bind_udp_socket(bind_addr)?;
|
||||||
|
let socket = UdpSocket::from_std(std_socket)?;
|
||||||
socket.connect(server_addr).await?;
|
socket.connect(server_addr).await?;
|
||||||
|
|
||||||
info!("Connected to rendezvous server at {}", server_addr);
|
info!("Connected to rendezvous server at {}", server_addr);
|
||||||
|
|||||||
@@ -3,5 +3,7 @@
|
|||||||
//! This module contains common utilities used across the codebase.
|
//! This module contains common utilities used across the codebase.
|
||||||
|
|
||||||
pub mod throttle;
|
pub mod throttle;
|
||||||
|
pub mod net;
|
||||||
|
|
||||||
pub use throttle::LogThrottler;
|
pub use throttle::LogThrottler;
|
||||||
|
pub use net::{bind_tcp_listener, bind_udp_socket};
|
||||||
|
|||||||
84
src/utils/net.rs
Normal file
84
src/utils/net.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
//! Networking helpers for binding sockets with explicit IPv6-only behavior.
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::net::{SocketAddr, TcpListener, UdpSocket};
|
||||||
|
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
|
||||||
|
|
||||||
|
use nix::sys::socket::{
|
||||||
|
self, sockopt, AddressFamily, Backlog, SockFlag, SockProtocol, SockType, SockaddrIn,
|
||||||
|
SockaddrIn6,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn socket_addr_family(addr: &SocketAddr) -> AddressFamily {
|
||||||
|
match addr {
|
||||||
|
SocketAddr::V4(_) => AddressFamily::Inet,
|
||||||
|
SocketAddr::V6(_) => AddressFamily::Inet6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bind a TCP listener with IPv6-only set for IPv6 sockets.
|
||||||
|
pub fn bind_tcp_listener(addr: SocketAddr) -> io::Result<TcpListener> {
|
||||||
|
let domain = socket_addr_family(&addr);
|
||||||
|
let fd = socket::socket(
|
||||||
|
domain,
|
||||||
|
SockType::Stream,
|
||||||
|
SockFlag::SOCK_CLOEXEC,
|
||||||
|
SockProtocol::Tcp,
|
||||||
|
)
|
||||||
|
.map_err(io::Error::from)?;
|
||||||
|
|
||||||
|
socket::setsockopt(&fd, sockopt::ReuseAddr, &true).map_err(io::Error::from)?;
|
||||||
|
|
||||||
|
if matches!(addr, SocketAddr::V6(_)) {
|
||||||
|
socket::setsockopt(&fd, sockopt::Ipv6V6Only, &true).map_err(io::Error::from)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
match addr {
|
||||||
|
SocketAddr::V4(v4) => {
|
||||||
|
let sockaddr = SockaddrIn::from(v4);
|
||||||
|
socket::bind(fd.as_raw_fd(), &sockaddr).map_err(io::Error::from)?;
|
||||||
|
}
|
||||||
|
SocketAddr::V6(v6) => {
|
||||||
|
let sockaddr = SockaddrIn6::from(v6);
|
||||||
|
socket::bind(fd.as_raw_fd(), &sockaddr).map_err(io::Error::from)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
socket::listen(&fd, Backlog::MAXCONN).map_err(io::Error::from)?;
|
||||||
|
|
||||||
|
let listener = unsafe { TcpListener::from_raw_fd(fd.into_raw_fd()) };
|
||||||
|
listener.set_nonblocking(true)?;
|
||||||
|
Ok(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bind a UDP socket with IPv6-only set for IPv6 sockets.
|
||||||
|
pub fn bind_udp_socket(addr: SocketAddr) -> io::Result<UdpSocket> {
|
||||||
|
let domain = socket_addr_family(&addr);
|
||||||
|
let fd = socket::socket(
|
||||||
|
domain,
|
||||||
|
SockType::Datagram,
|
||||||
|
SockFlag::SOCK_CLOEXEC,
|
||||||
|
SockProtocol::Udp,
|
||||||
|
)
|
||||||
|
.map_err(io::Error::from)?;
|
||||||
|
|
||||||
|
socket::setsockopt(&fd, sockopt::ReuseAddr, &true).map_err(io::Error::from)?;
|
||||||
|
|
||||||
|
if matches!(addr, SocketAddr::V6(_)) {
|
||||||
|
socket::setsockopt(&fd, sockopt::Ipv6V6Only, &true).map_err(io::Error::from)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
match addr {
|
||||||
|
SocketAddr::V4(v4) => {
|
||||||
|
let sockaddr = SockaddrIn::from(v4);
|
||||||
|
socket::bind(fd.as_raw_fd(), &sockaddr).map_err(io::Error::from)?;
|
||||||
|
}
|
||||||
|
SocketAddr::V6(v6) => {
|
||||||
|
let sockaddr = SockaddrIn6::from(v6);
|
||||||
|
socket::bind(fd.as_raw_fd(), &sockaddr).map_err(io::Error::from)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let socket = unsafe { UdpSocket::from_raw_fd(fd.into_raw_fd()) };
|
||||||
|
socket.set_nonblocking(true)?;
|
||||||
|
Ok(socket)
|
||||||
|
}
|
||||||
@@ -610,6 +610,7 @@ impl RustDeskConfigUpdate {
|
|||||||
pub struct WebConfigUpdate {
|
pub struct WebConfigUpdate {
|
||||||
pub http_port: Option<u16>,
|
pub http_port: Option<u16>,
|
||||||
pub https_port: Option<u16>,
|
pub https_port: Option<u16>,
|
||||||
|
pub bind_addresses: Option<Vec<String>>,
|
||||||
pub bind_address: Option<String>,
|
pub bind_address: Option<String>,
|
||||||
pub https_enabled: Option<bool>,
|
pub https_enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
@@ -626,6 +627,13 @@ impl WebConfigUpdate {
|
|||||||
return Err(AppError::BadRequest("HTTPS port cannot be 0".into()));
|
return Err(AppError::BadRequest("HTTPS port cannot be 0".into()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(ref addrs) = self.bind_addresses {
|
||||||
|
for addr in addrs {
|
||||||
|
if addr.parse::<std::net::IpAddr>().is_err() {
|
||||||
|
return Err(AppError::BadRequest("Invalid bind address".into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(ref addr) = self.bind_address {
|
if let Some(ref addr) = self.bind_address {
|
||||||
if addr.parse::<std::net::IpAddr>().is_err() {
|
if addr.parse::<std::net::IpAddr>().is_err() {
|
||||||
return Err(AppError::BadRequest("Invalid bind address".into()));
|
return Err(AppError::BadRequest("Invalid bind address".into()));
|
||||||
@@ -641,8 +649,16 @@ impl WebConfigUpdate {
|
|||||||
if let Some(port) = self.https_port {
|
if let Some(port) = self.https_port {
|
||||||
config.https_port = port;
|
config.https_port = port;
|
||||||
}
|
}
|
||||||
if let Some(ref addr) = self.bind_address {
|
if let Some(ref addrs) = self.bind_addresses {
|
||||||
|
config.bind_addresses = addrs.clone();
|
||||||
|
if let Some(first) = addrs.first() {
|
||||||
|
config.bind_address = first.clone();
|
||||||
|
}
|
||||||
|
} else if let Some(ref addr) = self.bind_address {
|
||||||
config.bind_address = addr.clone();
|
config.bind_address = addr.clone();
|
||||||
|
if config.bind_addresses.is_empty() {
|
||||||
|
config.bind_addresses = vec![addr.clone()];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(enabled) = self.https_enabled {
|
if let Some(enabled) = self.https_enabled {
|
||||||
config.https_enabled = enabled;
|
config.https_enabled = enabled;
|
||||||
|
|||||||
@@ -316,28 +316,11 @@ fn get_network_addresses() -> Vec<NetworkAddress> {
|
|||||||
Err(_) => return Vec::new(),
|
Err(_) => return Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build a map of interface name -> IPv4 address
|
// Check which interfaces are up
|
||||||
let mut ipv4_map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
let mut up_ifaces = std::collections::HashSet::new();
|
||||||
for ifaddr in all_addrs {
|
|
||||||
// Skip loopback
|
|
||||||
if ifaddr.interface_name == "lo" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Only collect IPv4 addresses (skip if already have one for this interface)
|
|
||||||
if !ipv4_map.contains_key(&ifaddr.interface_name) {
|
|
||||||
if let Some(addr) = ifaddr.address {
|
|
||||||
if let Some(sockaddr_in) = addr.as_sockaddr_in() {
|
|
||||||
ipv4_map.insert(ifaddr.interface_name.clone(), sockaddr_in.ip().to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now check which interfaces are up
|
|
||||||
let mut addresses = Vec::new();
|
|
||||||
let net_dir = match std::fs::read_dir("/sys/class/net") {
|
let net_dir = match std::fs::read_dir("/sys/class/net") {
|
||||||
Ok(dir) => dir,
|
Ok(dir) => dir,
|
||||||
Err(_) => return addresses,
|
Err(_) => return Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
for entry in net_dir.flatten() {
|
for entry in net_dir.flatten() {
|
||||||
@@ -361,12 +344,43 @@ fn get_network_addresses() -> Vec<NetworkAddress> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get IP from pre-fetched map
|
up_ifaces.insert(iface_name);
|
||||||
if let Some(ip) = ipv4_map.remove(&iface_name) {
|
}
|
||||||
addresses.push(NetworkAddress {
|
|
||||||
interface: iface_name,
|
let mut addresses = Vec::new();
|
||||||
ip,
|
let mut seen = std::collections::HashSet::new();
|
||||||
});
|
for ifaddr in all_addrs {
|
||||||
|
let iface_name = &ifaddr.interface_name;
|
||||||
|
if iface_name == "lo" || !up_ifaces.contains(iface_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(addr) = ifaddr.address {
|
||||||
|
if let Some(sockaddr_in) = addr.as_sockaddr_in() {
|
||||||
|
let ip = sockaddr_in.ip();
|
||||||
|
if ip.is_loopback() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ip_str = ip.to_string();
|
||||||
|
if seen.insert((iface_name.clone(), ip_str.clone())) {
|
||||||
|
addresses.push(NetworkAddress {
|
||||||
|
interface: iface_name.clone(),
|
||||||
|
ip: ip_str,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if let Some(sockaddr_in6) = addr.as_sockaddr_in6() {
|
||||||
|
let ip = sockaddr_in6.ip();
|
||||||
|
if ip.is_loopback() || ip.is_unspecified() || ip.is_unicast_link_local() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ip_str = ip.to_string();
|
||||||
|
if seen.insert((iface_name.clone(), ip_str.clone())) {
|
||||||
|
addresses.push(NetworkAddress {
|
||||||
|
interface: iface_name.clone(),
|
||||||
|
ip: ip_str,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -336,6 +336,7 @@ export const rustdeskConfigApi = {
|
|||||||
export interface WebConfig {
|
export interface WebConfig {
|
||||||
http_port: number
|
http_port: number
|
||||||
https_port: number
|
https_port: number
|
||||||
|
bind_addresses: string[]
|
||||||
bind_address: string
|
bind_address: string
|
||||||
https_enabled: boolean
|
https_enabled: boolean
|
||||||
}
|
}
|
||||||
@@ -344,6 +345,7 @@ export interface WebConfig {
|
|||||||
export interface WebConfigUpdate {
|
export interface WebConfigUpdate {
|
||||||
http_port?: number
|
http_port?: number
|
||||||
https_port?: number
|
https_port?: number
|
||||||
|
bind_addresses?: string[]
|
||||||
bind_address?: string
|
bind_address?: string
|
||||||
https_enabled?: boolean
|
https_enabled?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -480,10 +480,22 @@ export default {
|
|||||||
configureHttpPort: 'Configure HTTP server port',
|
configureHttpPort: 'Configure HTTP server port',
|
||||||
// Web server
|
// Web server
|
||||||
webServer: 'Access Address',
|
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',
|
httpsPort: 'HTTPS Port',
|
||||||
bindAddress: 'Bind Address',
|
bindAddress: 'Bind Address',
|
||||||
bindAddressDesc: 'IP address the server listens on. 0.0.0.0 means all network interfaces.',
|
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',
|
httpsEnabled: 'Enable HTTPS',
|
||||||
httpsEnabledDesc: 'Enable HTTPS encrypted connection (self-signed certificate will be auto-generated)',
|
httpsEnabledDesc: 'Enable HTTPS encrypted connection (self-signed certificate will be auto-generated)',
|
||||||
restartRequired: 'Restart Required',
|
restartRequired: 'Restart Required',
|
||||||
|
|||||||
@@ -480,10 +480,22 @@ export default {
|
|||||||
configureHttpPort: '配置 HTTP 服务器端口',
|
configureHttpPort: '配置 HTTP 服务器端口',
|
||||||
// Web server
|
// Web server
|
||||||
webServer: '访问地址',
|
webServer: '访问地址',
|
||||||
webServerDesc: '配置 HTTP/HTTPS 端口和绑定地址,修改后需要重启生效',
|
webServerDesc: '配置 HTTP/HTTPS 端口和监听地址,修改后需要重启生效',
|
||||||
httpsPort: 'HTTPS 端口',
|
httpsPort: 'HTTPS 端口',
|
||||||
bindAddress: '绑定地址',
|
bindAddress: '绑定地址',
|
||||||
bindAddressDesc: '服务器监听的 IP 地址,0.0.0.0 表示监听所有网络接口',
|
bindAddressDesc: '服务器监听的 IP 地址,0.0.0.0 表示监听所有网络接口',
|
||||||
|
bindMode: '监听地址',
|
||||||
|
bindModeDesc: '选择 Web 服务监听哪些地址。',
|
||||||
|
bindModeAll: '所有地址',
|
||||||
|
bindModeLocal: '仅本地 (127.0.0.1)',
|
||||||
|
bindModeCustom: '自定义地址列表',
|
||||||
|
bindIpv6: '启用 IPv6',
|
||||||
|
bindAllDesc: '同时监听 ::(所有 IPv6 地址)。',
|
||||||
|
bindLocalDesc: '同时监听 ::1(IPv6 本地回环)。',
|
||||||
|
bindAddressList: '地址列表',
|
||||||
|
bindAddressListDesc: '每行一个 IP(IPv4 或 IPv6)。',
|
||||||
|
addBindAddress: '添加地址',
|
||||||
|
bindAddressListEmpty: '请至少填写一个 IP 地址。',
|
||||||
httpsEnabled: '启用 HTTPS',
|
httpsEnabled: '启用 HTTPS',
|
||||||
httpsEnabledDesc: '启用 HTTPS 加密连接(将自动生成自签名证书)',
|
httpsEnabledDesc: '启用 HTTPS 加密连接(将自动生成自签名证书)',
|
||||||
restartRequired: '需要重启',
|
restartRequired: '需要重启',
|
||||||
|
|||||||
@@ -282,7 +282,9 @@ export interface WebConfig {
|
|||||||
http_port: number;
|
http_port: number;
|
||||||
/** HTTPS port */
|
/** HTTPS port */
|
||||||
https_port: number;
|
https_port: number;
|
||||||
/** Bind address */
|
/** Bind addresses (preferred) */
|
||||||
|
bind_addresses: string[];
|
||||||
|
/** Bind address (legacy) */
|
||||||
bind_address: string;
|
bind_address: string;
|
||||||
/** Enable HTTPS */
|
/** Enable HTTPS */
|
||||||
https_enabled: boolean;
|
https_enabled: boolean;
|
||||||
@@ -625,6 +627,7 @@ export interface VideoConfigUpdate {
|
|||||||
export interface WebConfigUpdate {
|
export interface WebConfigUpdate {
|
||||||
http_port?: number;
|
http_port?: number;
|
||||||
https_port?: number;
|
https_port?: number;
|
||||||
|
bind_addresses?: string[];
|
||||||
bind_address?: string;
|
bind_address?: string;
|
||||||
https_enabled?: boolean;
|
https_enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ import {
|
|||||||
Square,
|
Square,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Plus,
|
Plus,
|
||||||
|
Trash2,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Copy,
|
Copy,
|
||||||
ScreenShare,
|
ScreenShare,
|
||||||
@@ -196,11 +197,32 @@ const webServerConfig = ref<WebConfig>({
|
|||||||
http_port: 8080,
|
http_port: 8080,
|
||||||
https_port: 8443,
|
https_port: 8443,
|
||||||
bind_address: '0.0.0.0',
|
bind_address: '0.0.0.0',
|
||||||
|
bind_addresses: ['0.0.0.0'],
|
||||||
https_enabled: false,
|
https_enabled: false,
|
||||||
})
|
})
|
||||||
const webServerLoading = ref(false)
|
const webServerLoading = ref(false)
|
||||||
const showRestartDialog = ref(false)
|
const showRestartDialog = ref(false)
|
||||||
const restarting = 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
|
// Config
|
||||||
interface DeviceConfig {
|
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
|
// ATX config state
|
||||||
const atxConfig = ref({
|
const atxConfig = ref({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -987,20 +1015,72 @@ function normalizeRustdeskServer(value: string, defaultPort: number): string | u
|
|||||||
return `${trimmed}:${defaultPort}`
|
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
|
// Web server config functions
|
||||||
async function loadWebServerConfig() {
|
async function loadWebServerConfig() {
|
||||||
try {
|
try {
|
||||||
const config = await webConfigApi.get()
|
const config = await webConfigApi.get()
|
||||||
webServerConfig.value = config
|
webServerConfig.value = config
|
||||||
|
applyBindStateFromConfig(config)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load web server config:', e)
|
console.error('Failed to load web server config:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveWebServerConfig() {
|
async function saveWebServerConfig() {
|
||||||
|
if (bindAddressError.value) return
|
||||||
webServerLoading.value = true
|
webServerLoading.value = true
|
||||||
try {
|
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
|
showRestartDialog.value = true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to save web server config:', e)
|
console.error('Failed to save web server config:', e)
|
||||||
@@ -1687,13 +1767,51 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label>{{ t('settings.bindAddress') }}</Label>
|
<Label>{{ t('settings.bindMode') }}</Label>
|
||||||
<Input v-model="webServerConfig.bind_address" placeholder="0.0.0.0" />
|
<select v-model="bindMode" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
|
||||||
<p class="text-sm text-muted-foreground">{{ t('settings.bindAddressDesc') }}</p>
|
<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>
|
||||||
|
|
||||||
<div class="flex justify-end pt-4">
|
<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" />
|
<Save class="h-4 w-4 mr-2" />
|
||||||
{{ t('common.save') }}
|
{{ t('common.save') }}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user