singbox_driver/
backend.rs1use crate::api::clash_api::SingBoxClashApiClient;
15use crate::api::controller_configuration::SingBoxControllerConfiguration;
16use crate::codec::profile_document_codec::SingBoxProfileDocumentCodec;
17use crate::driver::SingBoxProcess;
18use crate::outbound::SingBoxOutboundAdapter;
19use crate::readiness::SingBoxControllerReadiness;
20use crate::routing_intent::SingBoxRoutingIntentAdapter;
21use lurker_core_contract::identity::CoreInstanceId;
22use lurker_core_contract::identity::EngineId;
23use lurker_core_ports::engine_backend::EngineBackendOwnerScopeBinding;
24use lurker_core_ports::engine_backend::EngineBackendPorts;
25use lurker_core_ports::engine_backend::EngineBackendProvider;
26use lurker_core_ports::engine_backend::EngineOperationPorts;
27use lurker_core_ports::error::PortError;
28use lurker_core_ports::outbound::OutboundPort;
29use lurker_core_ports::routing_intent::RoutingIntentPort;
30use serde_json::json;
31use serde_json::to_vec;
32use std::net::SocketAddr;
33use std::path::PathBuf;
34use std::sync::Arc;
35use std::time::Duration;
36
37pub const DEFAULT_READINESS_TIMEOUT: Duration = Duration::from_secs(30);
39
40pub struct SingBoxBackendOptions {
46 binary_path: PathBuf,
47 config_path: PathBuf,
48 instance_id: CoreInstanceId,
49 controller: SingBoxControllerConfiguration,
50 readiness_timeout: Duration,
51}
52
53impl SingBoxBackendOptions {
54 pub fn new(
56 binary_path: impl Into<PathBuf>,
57 config_path: impl Into<PathBuf>,
58 instance_id: CoreInstanceId,
59 controller_endpoint: impl Into<String>,
60 secret: Option<String>,
61 ) -> Result<Self, PortError> {
62 Self::with_readiness_timeout(
63 binary_path,
64 config_path,
65 instance_id,
66 controller_endpoint,
67 secret,
68 DEFAULT_READINESS_TIMEOUT,
69 )
70 }
71
72 pub fn with_readiness_timeout(
74 binary_path: impl Into<PathBuf>,
75 config_path: impl Into<PathBuf>,
76 instance_id: CoreInstanceId,
77 controller_endpoint: impl Into<String>,
78 secret: Option<String>,
79 readiness_timeout: Duration,
80 ) -> Result<Self, PortError> {
81 let binary_path = binary_path.into();
82 let config_path = config_path.into();
83 let controller_endpoint = controller_endpoint.into();
84
85 if binary_path.as_os_str().is_empty() {
86 return Err(PortError::Failed(
87 "sing-box backend binary path must not be empty".to_owned(),
88 ));
89 }
90 if config_path.as_os_str().is_empty() {
91 return Err(PortError::Failed(
92 "sing-box backend config path must not be empty".to_owned(),
93 ));
94 }
95 if readiness_timeout.is_zero() {
96 return Err(PortError::Failed(
97 "sing-box backend readiness timeout must be greater than zero".to_owned(),
98 ));
99 }
100 let controller = controller_configuration(&controller_endpoint, secret.as_deref())?;
101
102 Ok(Self {
103 binary_path,
104 config_path,
105 instance_id,
106 controller,
107 readiness_timeout,
108 })
109 }
110
111 pub fn binary_path(&self) -> &PathBuf {
113 &self.binary_path
114 }
115
116 pub fn config_path(&self) -> &PathBuf {
118 &self.config_path
119 }
120
121 pub fn instance_id(&self) -> &CoreInstanceId {
123 &self.instance_id
124 }
125
126 pub fn controller_address(&self) -> SocketAddr {
128 self.controller.address()
129 }
130
131 pub fn controller_endpoint(&self) -> String {
133 self.controller.endpoint()
134 }
135
136 pub const fn readiness_timeout(&self) -> Duration {
139 self.readiness_timeout
140 }
141}
142
143fn controller_configuration(
146 endpoint: &str,
147 secret: Option<&str>,
148) -> Result<SingBoxControllerConfiguration, PortError> {
149 let document = json!({
150 "experimental": {
151 "clash_api": {
152 "external_controller": endpoint,
153 "secret": secret,
154 }
155 }
156 });
157 let raw = to_vec(&document).map_err(|error| {
158 PortError::Failed(format!(
159 "sing-box backend controller configuration could not be encoded: {error}"
160 ))
161 })?;
162 SingBoxControllerConfiguration::from_profile_json(&raw).map_err(|error| {
163 PortError::Failed(format!(
164 "sing-box backend controller endpoint is invalid: {error}"
165 ))
166 })
167}
168
169pub struct SingBoxEngineBackendProvider {
171 options: SingBoxBackendOptions,
172}
173
174impl SingBoxEngineBackendProvider {
175 pub fn new(options: SingBoxBackendOptions) -> Self {
177 Self { options }
178 }
179}
180
181impl EngineBackendProvider<EngineBackendPorts> for SingBoxEngineBackendProvider {
182 fn engine_id(&self) -> EngineId {
183 EngineId::sing_box()
184 }
185
186 fn create_backend(&self) -> Result<EngineBackendPorts, PortError> {
187 let engine_id = self.engine_id();
188 let controller = self.options.controller.clone();
189 let process = SingBoxProcess::with_tokio(
190 self.options.binary_path().clone(),
191 self.options.config_path().clone(),
192 self.options.instance_id().clone(),
193 controller.address(),
194 )?;
195 let api = SingBoxClashApiClient::with_reqwest(controller.clone())?;
196 let readiness = SingBoxControllerReadiness::with_reqwest(controller)?;
197 let configuration = SingBoxProfileDocumentCodec;
198
199 let owner_scope = Arc::new(EngineBackendOwnerScopeBinding::new(engine_id.clone()));
200 let outbound: Arc<dyn OutboundPort> = Arc::new(SingBoxOutboundAdapter::new(
201 api.clone(),
202 owner_scope.clone(),
203 ));
204 let routing_intent: Arc<dyn RoutingIntentPort> =
205 Arc::new(SingBoxRoutingIntentAdapter::new(api, owner_scope.clone()));
206 let operations = EngineOperationPorts::available(outbound, routing_intent, owner_scope);
207
208 EngineBackendPorts::new(
209 engine_id,
210 Arc::new(process),
211 Arc::new(readiness),
212 Arc::new(configuration),
213 operations,
214 )
215 }
216}