diff --git a/leaf/Cargo.toml b/leaf/Cargo.toml index 7425fed..7b8425d 100644 --- a/leaf/Cargo.toml +++ b/leaf/Cargo.toml @@ -22,7 +22,8 @@ default-ring = [ # quinn supports only rustls as tls backend for now "inbound-quic", "outbound-quic", - # "api", + "api", + "stat", ] default-openssl = [ @@ -30,7 +31,6 @@ default-openssl = [ "all-endpoints", "openssl-aead", "openssl-tls", - # "api", ] # Grouping all features @@ -109,6 +109,7 @@ inbound-quic = ["quinn", "rustls", "webpki-roots"] inbound-tls = [] inbound-chain = [] +stat = [] api = ["warp"] auto-reload = ["notify"] ctrlc = ["tokio/signal"] diff --git a/leaf/src/app/api/api_server.rs b/leaf/src/app/api/api_server.rs index 573dc4c..41f79f8 100644 --- a/leaf/src/app/api/api_server.rs +++ b/leaf/src/app/api/api_server.rs @@ -74,6 +74,43 @@ mod handlers { Ok(StatusCode::ACCEPTED) } } + + #[cfg(feature = "stat")] + pub async fn stat_html(rm: Arc) -> Result { + let mut body = String::from( + r#" + + + + "#, + ); + let sm = rm.stat_manager(); + let sm = sm.read().await; + for c in sm.counters.iter() { + body.push_str(&format!( + "", + &c.sess.network, + &c.sess.destination, + c.bytes_sent(), + c.bytes_recvd(), + c.send_completed(), + c.recv_completed(), + )); + } + body.push_str("
NetworkDestinationSentBytesRecvdBytesSendFinRecvFin
{}{}{}{}{}{}
"); + Ok(warp::reply::html(body)) + } } mod filters { @@ -126,6 +163,17 @@ mod filters { .and(with_runtime_manager(rm)) .and_then(handlers::runtime_shutdown) } + + // POST /api/v1/runtime/stat/html + #[cfg(feature = "stat")] + pub fn stat_html( + rm: Arc, + ) -> impl Filter + Clone { + warp::path!("api" / "v1" / "runtime" / "stat" / "html") + .and(warp::get()) + .and(with_runtime_manager(rm)) + .and_then(handlers::stat_html) + } } pub struct ApiServer { @@ -142,6 +190,10 @@ impl ApiServer { .or(filters::select_get(self.runtime_manager.clone())) .or(filters::runtime_reload(self.runtime_manager.clone())) .or(filters::runtime_shutdown(self.runtime_manager.clone())); + + #[cfg(feature = "stat")] + let routes = routes.or(filters::stat_html(self.runtime_manager.clone())); + log::info!("api server listening tcp {}", &listen_addr); Box::pin(warp::serve(routes).bind(listen_addr)) } diff --git a/leaf/src/app/dispatcher.rs b/leaf/src/app/dispatcher.rs index 3461955..154fab6 100644 --- a/leaf/src/app/dispatcher.rs +++ b/leaf/src/app/dispatcher.rs @@ -15,6 +15,9 @@ use crate::{ session::{Network, Session, SocksAddr}, }; +#[cfg(feature = "stat")] +use crate::app::SyncStatManager; + use super::outbound::manager::OutboundManager; use super::router::Router; @@ -52,6 +55,8 @@ pub struct Dispatcher { outbound_manager: Arc>, router: Arc>, dns_client: SyncDnsClient, + #[cfg(feature = "stat")] + stat_manager: SyncStatManager, } impl Dispatcher { @@ -59,11 +64,14 @@ impl Dispatcher { outbound_manager: Arc>, router: Arc>, dns_client: SyncDnsClient, + #[cfg(feature = "stat")] stat_manager: SyncStatManager, ) -> Self { Dispatcher { outbound_manager, router, dns_client, + #[cfg(feature = "stat")] + stat_manager, } } @@ -170,11 +178,19 @@ impl Dispatcher { } }; match TcpOutboundHandler::handle(h.as_ref(), sess, stream).await { + #[allow(unused_mut)] Ok(mut rhs) => { let elapsed = tokio::time::Instant::now().duration_since(handshake_start); log_request(sess, h.tag(), h.color(), Some(elapsed.as_millis())); + #[cfg(feature = "stat")] + let mut rhs = self + .stat_manager + .write() + .await + .stat_stream(rhs, sess.clone()); + match common::io::copy_buf_bidirectional_with_timeout( &mut lhs, &mut rhs, @@ -267,12 +283,19 @@ impl Dispatcher { let transport = crate::proxy::connect_udp_outbound(sess, self.dns_client.clone(), &h).await?; match UdpOutboundHandler::handle(h.as_ref(), sess, transport).await { - Ok(c) => { + Ok(d) => { let elapsed = tokio::time::Instant::now().duration_since(handshake_start); log_request(sess, h.tag(), h.color(), Some(elapsed.as_millis())); - Ok(c) + #[cfg(feature = "stat")] + let d = self + .stat_manager + .write() + .await + .stat_outbound_datagram(d, sess.clone()); + + Ok(d) } Err(e) => { debug!( diff --git a/leaf/src/app/mod.rs b/leaf/src/app/mod.rs index ce10bd8..bce5b2f 100644 --- a/leaf/src/app/mod.rs +++ b/leaf/src/app/mod.rs @@ -10,6 +10,9 @@ pub mod nat_manager; pub mod outbound; pub mod router; +#[cfg(feature = "stat")] +pub mod stat_manager; + #[cfg(feature = "api")] pub mod api; @@ -22,3 +25,6 @@ pub mod api; pub mod fake_dns; pub type SyncDnsClient = Arc>; + +#[cfg(feature = "stat")] +pub type SyncStatManager = Arc>; diff --git a/leaf/src/app/stat_manager.rs b/leaf/src/app/stat_manager.rs new file mode 100644 index 0000000..c88f54b --- /dev/null +++ b/leaf/src/app/stat_manager.rs @@ -0,0 +1,235 @@ +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::Arc; +use std::{io, pin::Pin}; + +use async_trait::async_trait; +use futures::{ + ready, + task::{Context, Poll}, +}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + +use crate::{proxy::*, session::*}; + +pub struct Stream { + pub inner: AnyStream, + pub bytes_recvd: Arc, + pub bytes_sent: Arc, + pub recv_completed: Arc, + pub send_completed: Arc, +} + +impl Drop for Stream { + fn drop(&mut self) { + // In case of abnormal shutdown. + self.recv_completed.store(true, Ordering::Relaxed); + self.send_completed.store(true, Ordering::Relaxed); + } +} + +impl AsyncRead for Stream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut ReadBuf, + ) -> Poll> { + ready!(Pin::new(&mut self.inner).poll_read(cx, buf))?; + if buf.filled().is_empty() { + self.recv_completed.store(true, Ordering::Relaxed); + } else { + self.bytes_recvd + .fetch_add(buf.filled().len() as u64, Ordering::Relaxed); + } + Poll::Ready(Ok(())) + } +} + +impl AsyncWrite for Stream { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + let n = ready!(Pin::new(&mut self.inner).poll_write(cx, buf))?; + self.bytes_sent.fetch_add(n as u64, Ordering::Relaxed); + Poll::Ready(Ok(n)) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + Pin::new(&mut self.inner).poll_flush(cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + ready!(Pin::new(&mut self.inner).poll_shutdown(cx))?; + self.send_completed.store(true, Ordering::Relaxed); + Poll::Ready(Ok(())) + } +} + +pub struct Datagram { + pub inner: AnyOutboundDatagram, + pub bytes_recvd: Arc, + pub bytes_sent: Arc, + pub recv_completed: Arc, + pub send_completed: Arc, +} + +impl OutboundDatagram for Datagram { + fn split( + self: Box, + ) -> ( + Box, + Box, + ) { + let (r, s) = self.inner.split(); + ( + Box::new(DatagramRecvHalf(r, self.bytes_recvd, self.recv_completed)), + Box::new(DatagramSendHalf(s, self.bytes_sent, self.send_completed)), + ) + } +} + +pub struct DatagramRecvHalf( + Box, + Arc, + Arc, +); + +impl Drop for DatagramRecvHalf { + fn drop(&mut self) { + self.2.store(true, Ordering::Relaxed); + } +} + +#[async_trait] +impl OutboundDatagramRecvHalf for DatagramRecvHalf { + async fn recv_from(&mut self, buf: &mut [u8]) -> io::Result<(usize, SocksAddr)> { + self.0.recv_from(buf).await.map(|(n, a)| { + self.1.fetch_add(n as u64, Ordering::Relaxed); + (n, a) + }) + } +} + +pub struct DatagramSendHalf( + Box, + Arc, + Arc, +); + +impl Drop for DatagramSendHalf { + fn drop(&mut self) { + self.2.store(true, Ordering::Relaxed); + } +} + +#[async_trait] +impl OutboundDatagramSendHalf for DatagramSendHalf { + async fn send_to(&mut self, buf: &[u8], target: &SocksAddr) -> io::Result { + self.0.send_to(buf, target).await.map(|n| { + self.1.fetch_add(n as u64, Ordering::Relaxed); + n + }) + } +} + +pub struct Counter { + pub sess: Session, + pub bytes_recvd: Arc, + pub bytes_sent: Arc, + pub recv_completed: Arc, + pub send_completed: Arc, +} + +impl Counter { + pub fn bytes_recvd(&self) -> u64 { + self.bytes_recvd.load(Ordering::Relaxed) + } + + pub fn bytes_sent(&self) -> u64 { + self.bytes_sent.load(Ordering::Relaxed) + } + + pub fn recv_completed(&self) -> bool { + self.recv_completed.load(Ordering::Relaxed) + } + + pub fn send_completed(&self) -> bool { + self.send_completed.load(Ordering::Relaxed) + } +} + +pub struct StatManager { + pub counters: Vec, +} + +impl StatManager { + pub fn new() -> Self { + Self { + counters: Vec::new(), + } + } + + pub fn cleanup_task(sm: super::SyncStatManager) -> crate::Runner { + Box::pin(async move { + loop { + tokio::time::sleep(std::time::Duration::from_secs(20)).await; + let mut sm = sm.write().await; + let mut i = 0; + while i < sm.counters.len() { + if sm.counters[i].recv_completed() && sm.counters[i].send_completed() { + sm.counters.remove(i); + } else { + i += 1; + } + } + } + }) + } + + pub fn stat_stream(&mut self, stream: AnyStream, sess: Session) -> AnyStream { + let bytes_recvd = Arc::new(AtomicU64::new(0)); + let bytes_sent = Arc::new(AtomicU64::new(0)); + let recv_completed = Arc::new(AtomicBool::new(false)); + let send_completed = Arc::new(AtomicBool::new(false)); + self.counters.push(Counter { + sess, + bytes_recvd: bytes_recvd.clone(), + bytes_sent: bytes_sent.clone(), + recv_completed: recv_completed.clone(), + send_completed: send_completed.clone(), + }); + Box::new(Stream { + inner: stream, + bytes_recvd, + bytes_sent, + recv_completed, + send_completed, + }) + } + + pub fn stat_outbound_datagram( + &mut self, + dgram: AnyOutboundDatagram, + sess: Session, + ) -> AnyOutboundDatagram { + let bytes_recvd = Arc::new(AtomicU64::new(0)); + let bytes_sent = Arc::new(AtomicU64::new(0)); + let recv_completed = Arc::new(AtomicBool::new(false)); + let send_completed = Arc::new(AtomicBool::new(false)); + self.counters.push(Counter { + sess, + bytes_recvd: bytes_recvd.clone(), + bytes_sent: bytes_sent.clone(), + recv_completed: recv_completed.clone(), + send_completed: send_completed.clone(), + }); + Box::new(Datagram { + inner: dgram, + bytes_recvd, + bytes_sent, + recv_completed, + send_completed, + }) + } +} diff --git a/leaf/src/config/conf/config.rs b/leaf/src/config/conf/config.rs index 1143bd1..358f7c1 100644 --- a/leaf/src/config/conf/config.rs +++ b/leaf/src/config/conf/config.rs @@ -1201,26 +1201,12 @@ pub fn to_internal(conf: &mut Config) -> Result { dns.hosts = hosts; } - let api = if let Some(ext_general) = &conf.general { - if ext_general.api_interface.is_some() && ext_general.api_port.is_some() { - let mut api_inner = internal::Api::new(); - api_inner.address = ext_general.api_interface.as_ref().unwrap().to_string(); - api_inner.port = ext_general.api_port.unwrap() as u32; - protobuf::SingularPtrField::some(api_inner) - } else { - protobuf::SingularPtrField::none() - } - } else { - protobuf::SingularPtrField::none() - }; - let mut config = internal::Config::new(); config.log = protobuf::SingularPtrField::some(log); config.inbounds = inbounds; config.outbounds = outbounds; config.router = router; config.dns = protobuf::SingularPtrField::some(dns); - config.api = api; Ok(config) } diff --git a/leaf/src/config/internal/config.proto b/leaf/src/config/internal/config.proto index ebe64fe..d3e11c1 100644 --- a/leaf/src/config/internal/config.proto +++ b/leaf/src/config/internal/config.proto @@ -1,9 +1,6 @@ -syntax = "proto3"; +// Every time you make changes to this file, run `make proto-gen` to re-generate protobuf files. -message Api { - string address = 1; - uint32 port = 2; -} +syntax = "proto3"; message Dns { message Ips { @@ -214,5 +211,4 @@ message Config { repeated Outbound outbounds = 3; Router router = 4; Dns dns = 5; - Api api = 6; } diff --git a/leaf/src/config/internal/config.rs b/leaf/src/config/internal/config.rs index 63ce574..2fae54c 100644 --- a/leaf/src/config/internal/config.rs +++ b/leaf/src/config/internal/config.rs @@ -23,145 +23,6 @@ /// of protobuf runtime. // const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_27_1; -#[derive(PartialEq,Clone,Default,Debug)] -pub struct Api { - // message fields - pub address: ::std::string::String, - pub port: u32, - // special fields - pub unknown_fields: ::protobuf::UnknownFields, - pub cached_size: ::protobuf::CachedSize, -} - -impl<'a> ::std::default::Default for &'a Api { - fn default() -> &'a Api { - ::default_instance() - } -} - -impl Api { - pub fn new() -> Api { - ::std::default::Default::default() - } - - // string address = 1; - - - pub fn get_address(&self) -> &str { - &self.address - } - - // uint32 port = 2; - - - pub fn get_port(&self) -> u32 { - self.port - } -} - -impl ::protobuf::Message for Api { - fn is_initialized(&self) -> bool { - true - } - - fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> { - while !is.eof()? { - let (field_number, wire_type) = is.read_tag_unpack()?; - match field_number { - 1 => { - ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.address)?; - }, - 2 => { - if wire_type != ::protobuf::wire_format::WireTypeVarint { - return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type)); - } - let tmp = is.read_uint32()?; - self.port = tmp; - }, - _ => { - ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; - }, - }; - } - ::std::result::Result::Ok(()) - } - - // Compute sizes of nested messages - #[allow(unused_variables)] - fn compute_size(&self) -> u32 { - let mut my_size = 0; - if !self.address.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.address); - } - if self.port != 0 { - my_size += ::protobuf::rt::value_size(2, self.port, ::protobuf::wire_format::WireTypeVarint); - } - my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); - self.cached_size.set(my_size); - my_size - } - - fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { - if !self.address.is_empty() { - os.write_string(1, &self.address)?; - } - if self.port != 0 { - os.write_uint32(2, self.port)?; - } - os.write_unknown_fields(self.get_unknown_fields())?; - ::std::result::Result::Ok(()) - } - - fn get_cached_size(&self) -> u32 { - self.cached_size.get() - } - - fn get_unknown_fields(&self) -> &::protobuf::UnknownFields { - &self.unknown_fields - } - - fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields { - &mut self.unknown_fields - } - - fn as_any(&self) -> &dyn (::std::any::Any) { - self as &dyn (::std::any::Any) - } - fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) { - self as &mut dyn (::std::any::Any) - } - fn into_any(self: ::std::boxed::Box) -> ::std::boxed::Box { - self - } - - fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor { - Self::descriptor_static() - } - - fn new() -> Api { - Api::new() - } - - fn default_instance() -> &'static Api { - static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; - instance.get(Api::new) - } -} - -impl ::protobuf::Clear for Api { - fn clear(&mut self) { - self.address.clear(); - self.port = 0; - self.unknown_fields.clear(); - } -} - -impl ::protobuf::reflect::ProtobufValue for Api { - fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef { - ::protobuf::reflect::ReflectValueRef::Message(self) - } -} - #[derive(PartialEq,Clone,Default,Debug)] pub struct Dns { // message fields @@ -5086,7 +4947,6 @@ pub struct Config { pub outbounds: ::protobuf::RepeatedField, pub router: ::protobuf::SingularPtrField, pub dns: ::protobuf::SingularPtrField, - pub api: ::protobuf::SingularPtrField, // special fields pub unknown_fields: ::protobuf::UnknownFields, pub cached_size: ::protobuf::CachedSize, @@ -5137,13 +4997,6 @@ impl Config { pub fn get_dns(&self) -> &Dns { self.dns.as_ref().unwrap_or_else(|| ::default_instance()) } - - // .Api api = 6; - - - pub fn get_api(&self) -> &Api { - self.api.as_ref().unwrap_or_else(|| ::default_instance()) - } } impl ::protobuf::Message for Config { @@ -5173,11 +5026,6 @@ impl ::protobuf::Message for Config { return false; } }; - for v in &self.api { - if !v.is_initialized() { - return false; - } - }; true } @@ -5200,9 +5048,6 @@ impl ::protobuf::Message for Config { 5 => { ::protobuf::rt::read_singular_message_into(wire_type, is, &mut self.dns)?; }, - 6 => { - ::protobuf::rt::read_singular_message_into(wire_type, is, &mut self.api)?; - }, _ => { ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; }, @@ -5235,10 +5080,6 @@ impl ::protobuf::Message for Config { let len = v.compute_size(); my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len; } - if let Some(ref v) = self.api.as_ref() { - let len = v.compute_size(); - my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len; - } my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); self.cached_size.set(my_size); my_size @@ -5270,11 +5111,6 @@ impl ::protobuf::Message for Config { os.write_raw_varint32(v.get_cached_size())?; v.write_to_with_cached_sizes(os)?; } - if let Some(ref v) = self.api.as_ref() { - os.write_tag(6, ::protobuf::wire_format::WireTypeLengthDelimited)?; - os.write_raw_varint32(v.get_cached_size())?; - v.write_to_with_cached_sizes(os)?; - } os.write_unknown_fields(self.get_unknown_fields())?; ::std::result::Result::Ok(()) } @@ -5322,7 +5158,6 @@ impl ::protobuf::Clear for Config { self.outbounds.clear(); self.router.clear(); self.dns.clear(); - self.api.clear(); self.unknown_fields.clear(); } } diff --git a/leaf/src/config/json/config.rs b/leaf/src/config/json/config.rs index 9a0a49f..a3a37ae 100644 --- a/leaf/src/config/json/config.rs +++ b/leaf/src/config/json/config.rs @@ -9,12 +9,6 @@ use serde_json::value::RawValue; use crate::config::{external_rule, internal}; -#[derive(Serialize, Deserialize, Debug)] -pub struct Api { - pub address: Option, - pub port: Option, -} - #[derive(Serialize, Deserialize, Debug)] pub struct Dns { pub servers: Option>, @@ -241,7 +235,6 @@ pub struct Config { pub outbounds: Option>, pub router: Option, pub dns: Option, - pub api: Option, } pub fn to_internal(json: &mut Config) -> Result { @@ -967,28 +960,12 @@ pub fn to_internal(json: &mut Config) -> Result { dns.hosts = hosts; } - let api = if let Some(ext_api) = json.api.as_ref() { - if let (Some(ext_address), Some(ext_port)) = - (ext_api.address.as_ref(), ext_api.port.as_ref()) - { - let mut api = internal::Api::new(); - api.address = ext_address.to_owned(); - api.port = ext_port.to_owned() as u32; - protobuf::SingularPtrField::some(api) - } else { - protobuf::SingularPtrField::none() - } - } else { - protobuf::SingularPtrField::none() - }; - let mut config = internal::Config::new(); config.log = protobuf::SingularPtrField::some(log); config.inbounds = inbounds; config.outbounds = outbounds; config.router = router; config.dns = protobuf::SingularPtrField::some(dns); - config.api = api; Ok(config) } diff --git a/leaf/src/lib.rs b/leaf/src/lib.rs index 1dab81a..d80826b 100644 --- a/leaf/src/lib.rs +++ b/leaf/src/lib.rs @@ -21,6 +21,9 @@ use app::{ nat_manager::NatManager, outbound::manager::OutboundManager, router::Router, }; +#[cfg(feature = "stat")] +use crate::app::{stat_manager::StatManager, SyncStatManager}; + #[cfg(feature = "api")] use crate::app::api::api_server::ApiServer; @@ -72,6 +75,8 @@ pub struct RuntimeManager { router: Arc>, dns_client: Arc>, outbound_manager: Arc>, + #[cfg(feature = "stat")] + stat_manager: SyncStatManager, #[cfg(feature = "auto-reload")] watcher: Mutex>, } @@ -87,6 +92,7 @@ impl RuntimeManager { router: Arc>, dns_client: Arc>, outbound_manager: Arc>, + #[cfg(feature = "stat")] stat_manager: SyncStatManager, ) -> Arc { Arc::new(Self { #[cfg(feature = "auto-reload")] @@ -99,11 +105,18 @@ impl RuntimeManager { router, dns_client, outbound_manager, + #[cfg(feature = "stat")] + stat_manager, #[cfg(feature = "auto-reload")] watcher: Mutex::new(None), }) } + #[cfg(feature = "stat")] + pub fn stat_manager(&self) -> SyncStatManager { + self.stat_manager.clone() + } + pub async fn set_outbound_selected(&self, outbound: &str, select: &str) -> Result<(), Error> { if let Some(selector) = self.outbound_manager.read().await.get_selector(outbound) { selector @@ -386,10 +399,16 @@ pub fn start(rt_id: RuntimeId, opts: StartOptions) -> Result<(), Error> { &mut config.router, dns_client.clone(), ))); + #[cfg(feature = "stat")] + let stat_manager = Arc::new(RwLock::new(StatManager::new())); + #[cfg(feature = "stat")] + runners.push(StatManager::cleanup_task(stat_manager.clone())); let dispatcher = Arc::new(Dispatcher::new( outbound_manager.clone(), router.clone(), dns_client.clone(), + #[cfg(feature = "stat")] + stat_manager.clone(), )); let nat_manager = Arc::new(NatManager::new(dispatcher.clone())); let inbound_manager = @@ -449,6 +468,8 @@ pub fn start(rt_id: RuntimeId, opts: StartOptions) -> Result<(), Error> { router, dns_client, outbound_manager, + #[cfg(feature = "stat")] + stat_manager, ); // Monitor config file changes. @@ -461,20 +482,13 @@ pub fn start(rt_id: RuntimeId, opts: StartOptions) -> Result<(), Error> { #[cfg(feature = "api")] { - use std::net::{IpAddr, SocketAddr}; + use std::net::SocketAddr; let listen_addr = if !(&*option::API_LISTEN).is_empty() { Some( (&*option::API_LISTEN) .parse::() .map_err(|e| Error::Config(anyhow!("parse SocketAddr failed: {}", e)))?, ) - } else if let Some(api) = config.api.as_ref() { - Some(SocketAddr::new( - api.address - .parse::() - .map_err(|e| Error::Config(anyhow!("parse IpAddr failed: {}", e)))?, - api.port as u16, - )) } else { None };