Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 11 additions & 16 deletions clash_lib/src/app/api/handlers/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use axum::{
use http::StatusCode;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use tracing::warn;

use crate::{
GlobalState,
Expand Down Expand Up @@ -54,6 +53,7 @@ async fn get_configs(State(state): State<ConfigState>) -> impl IntoResponse {
let run_mode = state.dispatcher.get_mode().await;
let global_state = state.global_state.lock().await;
let dns_resolver = state.dns_resolver;
let inbound_manager = state.inbound_manager.clone();

let ports = state.inbound_manager.get_ports().await;

Expand All @@ -70,14 +70,7 @@ async fn get_configs(State(state): State<ConfigState>) -> impl IntoResponse {
mode: Some(run_mode),
log_level: Some(global_state.log_level),
ipv6: Some(dns_resolver.ipv6()),
allow_lan: Some(
state
.inbound_manager
.get_bind_address()
.await
.0
.is_unspecified(),
),
allow_lan: Some(inbound_manager.get_allow_lan().await),
})
}

Expand Down Expand Up @@ -182,13 +175,6 @@ async fn patch_configs(
State(state): State<ConfigState>,
Json(payload): Json<PatchConfigRequest>,
) -> impl IntoResponse {
if payload.allow_lan.is_some() {
warn!(
"setting allow_lan doesn't do anything. please set bind_address to a \
LAN address instead."
);
}

let inbound_manager = state.inbound_manager.clone();
let mut need_restart = false;
if let Some(bind_address) = payload.bind_address.clone() {
Expand Down Expand Up @@ -220,6 +206,15 @@ async fn patch_configs(
inbound_manager.change_ports(ports).await;
need_restart = true;
}

if let Some(allow_lan) = payload.allow_lan
&& allow_lan != inbound_manager.get_allow_lan().await
{
inbound_manager.set_allow_lan(allow_lan).await;
// TODO: can be done with AtomicBool, but requires more changes
need_restart = true;
}

if need_restart {
inbound_manager.restart().await;
}
Expand Down
35 changes: 33 additions & 2 deletions clash_lib/src/app/inbound/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use tracing::warn;
use tracing::{trace, warn};

/// Legacy ports configuration for inbounds.
/// Newer inbounds have their own port configuration
Expand Down Expand Up @@ -56,14 +56,24 @@ impl InboundManager {
/// If a listener is already running, it will be restarted.
pub async fn start_all_listeners(&self) {
for (opts, handler) in self.inbound_handlers.write().await.iter_mut() {
if let Some(handler) = handler {
if let Some(handler) = handler.take() {
warn!(
"Restarting inbound handler for: {}",
opts.common_opts().name
);
handler.abort();
let _ = handler.await.map_err(|e| {
trace!(
"Inbound {} listener task aborted: {}",
opts.common_opts().name,
e
);
});
}
*handler = None;
}

for (opts, handler) in self.inbound_handlers.write().await.iter_mut() {
*handler = build_network_listeners(
opts,
self.dispatcher.clone(),
Expand Down Expand Up @@ -117,6 +127,27 @@ impl InboundManager {
ports
}

pub async fn get_allow_lan(&self) -> bool {
let guard = self.inbound_handlers.read().await;
if let Some((opts, _)) = guard.iter().next() {
opts.common_opts().allow_lan
} else {
false
}
}

pub async fn set_allow_lan(&self, allow_lan: bool) {
let mut guard = self.inbound_handlers.write().await;
let new_map = guard
.drain()
.map(|(mut opts, handler)| {
opts.common_opts_mut().allow_lan = allow_lan;
(opts, handler)
})
.collect::<HashMap<_, _>>();
*guard = new_map;
Comment on lines +141 to +148
Copy link

Copilot AI Jun 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebuilding the entire handlers map on each allow_lan toggle can be inefficient. Instead, iterate over guard.values_mut() and update common_opts_mut().allow_lan in place to avoid unnecessary allocations.

Suggested change
let new_map = guard
.drain()
.map(|(mut opts, handler)| {
opts.common_opts_mut().allow_lan = allow_lan;
(opts, handler)
})
.collect::<HashMap<_, _>>();
*guard = new_map;
for (_, handler) in guard.values_mut() {
if let Some(opts) = handler.as_mut() {
opts.common_opts_mut().allow_lan = allow_lan;
}
}

Copilot uses AI. Check for mistakes.
}

pub async fn get_bind_address(&self) -> BindAddress {
let guard = self.inbound_handlers.read().await;
if let Some((opts, _)) = guard.iter().next() {
Expand Down
5 changes: 2 additions & 3 deletions clash_lib/src/app/inbound/network_listener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,11 @@ pub(crate) fn build_network_listeners(
let mut runners: Vec<Runner> = Vec::new();

if handler.handle_tcp() {
info!("{} TCP listening at: {}:{}", name, addr, port,);

let tcp_listener = handler.clone();

let name = name.clone();
runners.push(Box::pin(async move {
info!("{} TCP listening at: {}:{}", name, addr, port,);
tcp_listener
.listen_tcp()
.await
Expand All @@ -47,10 +46,10 @@ pub(crate) fn build_network_listeners(
}

if handler.handle_udp() {
info!("{} UDP listening at: {}:{}", name, addr, port,);
let udp_listener = handler.clone();
let name = name.clone();
runners.push(Box::pin(async move {
info!("{} UDP listening at: {}:{}", name, addr, port,);
udp_listener
.listen_udp()
.await
Expand Down
1 change: 0 additions & 1 deletion clash_lib/src/config/internal/listener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ impl InboundOpts {
pub struct CommonInboundOpts {
pub name: String,
pub listen: BindAddress,
// TODO: make this reloadable in inbound listeners
#[serde(default)]
pub allow_lan: bool,
pub port: u16,
Expand Down
79 changes: 79 additions & 0 deletions clash_lib/tests/api_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,85 @@ use std::{path::PathBuf, time::Duration};

mod common;

#[tokio::test(flavor = "current_thread")]
#[serial_test::serial]
async fn test_get_set_allow_lan() {
let wd =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data/config/client");
let config_path = wd.join("rules.yaml");
assert!(
config_path.exists(),
"Config file does not exist at: {}",
config_path.to_string_lossy()
);

std::thread::spawn(move || {
start_clash(Options {
config: Config::File(config_path.to_string_lossy().to_string()),
cwd: Some(wd.to_string_lossy().to_string()),
rt: None,
log_file: None,
})
.expect("Failed to start clash");
});

wait_port_ready(9090).expect("Clash server is not ready");

async fn get_allow_lan() -> bool {
let get_configs_url = "http://localhost:9090/configs";
let curl_cmd = format!(
"curl -s -H 'Authorization: Bearer {}' {}",
"clash-rs", get_configs_url
);
let output = tokio::process::Command::new("sh")
Copy link

Copilot AI Jun 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test shells out to sh and curl, introducing external dependencies and portability concerns. Consider using a Rust HTTP client (e.g., reqwest) for more robust, cross-platform tests.

Copilot uses AI. Check for mistakes.
.arg("-c")
.arg(curl_cmd)
.output()
.await
.expect("Failed to execute curl command");
assert!(
output.status.success(),
"Curl command failed with output: {}",
String::from_utf8_lossy(&output.stderr)
);
let response = String::from_utf8_lossy(&output.stdout);
let json = serde_json::from_str::<serde_json::Value>(&response)
.expect("Failed to parse JSON response");
json.get("allow-lan")
.expect("No 'allow-lan' field in response")
.as_bool()
.expect("'allow-lan' is not a boolean")
}

assert!(
get_allow_lan().await,
"'allow_lan' should be true by config"
);

let configs_url = "http://localhost:9090/configs";
let curl_cmd = format!(
"curl -s -X PATCH -H 'Authorization: Bearer {}' -H 'Content-Type: \
application/json' -d '{{\"allow-lan\": false}}' {configs_url}",
"clash-rs"
);
let output = tokio::process::Command::new("sh")
.arg("-c")
.arg(curl_cmd)
.output()
.await
.expect("Failed to execute curl command");
assert!(
output.status.success(),
"Curl command failed with output: {}",
String::from_utf8_lossy(&output.stderr)
);

assert!(
!get_allow_lan().await,
"'allow_lan' should be false after update"
);
}

#[tokio::test(flavor = "current_thread")]
#[serial_test::serial]
async fn test_connections_returns_proxy_chain_names() {
Expand Down
Loading