eggress_embed/
outbound.rs1use std::sync::Arc;
8use std::time::Duration;
9
10use crate::EggressError;
11
12#[derive(Debug, Clone)]
14pub struct OutboundInfo {
15 pub local_addr: Option<std::net::SocketAddr>,
17 pub peer_addr: Option<std::net::SocketAddr>,
19 pub hop_count: usize,
21}
22
23pub struct UdpAssociation {
28 pub relay_addr: std::net::SocketAddr,
30 pub control_stream: Option<eggress_core::BoxStream>,
32 pub target: eggress_core::TargetAddr,
34}
35
36async fn resolve_endpoint_addr(
40 endpoint: &eggress_uri::EndpointSpec,
41) -> Option<std::net::SocketAddr> {
42 if let Ok(ip) = endpoint.host.parse::<std::net::IpAddr>() {
43 return Some(std::net::SocketAddr::new(ip, endpoint.port));
44 }
45 let lookup = format!("{}:{}", endpoint.host, endpoint.port);
46 let result = tokio::net::lookup_host(&lookup).await.ok()?.next();
47 result
48}
49
50pub struct OutboundConnector {
56 runtime_config: Option<Arc<eggress_config::compile::RuntimeConfig>>,
57 chain_executor: eggress_core::chain::ChainExecutor,
58 direct: bool,
59}
60
61impl OutboundConnector {
62 pub fn from_toml(config_toml: &str) -> Result<Self, EggressError> {
64 let config: eggress_config::model::ConfigFile =
65 toml::from_str(config_toml).map_err(|e| EggressError::Config(e.to_string()))?;
66
67 if let Some(version) = config.version {
68 if version != 1 {
69 return Err(EggressError::Config(format!(
70 "unsupported config version: {version}"
71 )));
72 }
73 }
74
75 eggress_config::validate::validate_config(&config).map_err(|errors| {
76 let messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
77 EggressError::Config(messages.join("; "))
78 })?;
79
80 let runtime_config = eggress_config::compile::compile_config(&config)
81 .map_err(|e| EggressError::Config(e.to_string()))?;
82
83 if runtime_config.upstreams.is_empty() {
84 return Err(EggressError::Config("no upstreams configured".to_string()));
85 }
86
87 let upstream = &runtime_config.upstreams[0];
88 if upstream.chain.hops.is_empty() {
89 return Err(EggressError::Config("upstream chain is empty".to_string()));
90 }
91
92 #[cfg(feature = "ssh")]
93 let chain_executor = eggress_server::build_chain_executor(None, None, None);
94 #[cfg(not(feature = "ssh"))]
95 let chain_executor = eggress_server::build_chain_executor(None, None);
96
97 Ok(Self {
98 runtime_config: Some(Arc::new(runtime_config)),
99 chain_executor,
100 direct: false,
101 })
102 }
103
104 #[cfg(feature = "pproxy-compat")]
106 pub fn from_pproxy_uri(uri: &str) -> Result<Self, EggressError> {
107 let parsed = eggress_pproxy_compat::uri::parse_pproxy_uri(uri)
108 .map_err(|e| EggressError::Config(e.to_string()))?;
109 if parsed.scheme == "direct" {
110 #[cfg(feature = "ssh")]
111 let executor = eggress_server::build_chain_executor(None, None, None);
112 #[cfg(not(feature = "ssh"))]
113 let executor = eggress_server::build_chain_executor(None, None);
114 return Ok(Self {
115 runtime_config: None,
116 chain_executor: executor,
117 direct: true,
118 });
119 }
120 let chain = eggress_pproxy_compat::uri::PproxyChain {
121 raw: uri.to_string(),
122 hops: vec![parsed],
123 };
124 let default_args = eggress_pproxy_compat::PproxyArgs::default_args();
125 let output = eggress_pproxy_compat::translate_from_uris(&default_args, &[], &[chain])
126 .map_err(|e| EggressError::Config(e.to_string()))?;
127 Self::from_toml(&output.toml)
128 }
129
130 pub async fn connect_tcp(
134 &self,
135 host: &str,
136 port: u16,
137 ) -> Result<(eggress_core::BoxStream, OutboundInfo), EggressError> {
138 let target = eggress_core::TargetAddr {
139 host: if let Ok(ip) = host.parse::<std::net::IpAddr>() {
140 eggress_core::TargetHost::Ip(ip)
141 } else {
142 eggress_core::TargetHost::Domain(host.to_string())
143 },
144 port,
145 };
146
147 if self.direct {
148 let stream = eggress_core::connector::Connector::connect(
149 &eggress_core::connector::DirectConnector,
150 &target,
151 )
152 .await
153 .map_err(|e| EggressError::Runtime(e.to_string()))?;
154 return Ok((
155 stream,
156 OutboundInfo {
157 local_addr: None,
158 peer_addr: None,
159 hop_count: 0,
160 },
161 ));
162 }
163
164 let runtime_config = self.runtime_config.as_ref().ok_or_else(|| {
165 EggressError::Runtime("outbound runtime configuration is unavailable".to_string())
166 })?;
167 let upstream = &runtime_config.upstreams[0];
168 let chain = &upstream.chain;
169
170 let first_hop = &chain.hops[0];
172 let peer_addr = resolve_endpoint_addr(&first_hop.endpoint).await;
173
174 let stream = self
175 .chain_executor
176 .execute(&chain.hops, &target)
177 .await
178 .map_err(|e| EggressError::Runtime(e.to_string()))?;
179
180 let info = OutboundInfo {
181 local_addr: None,
182 peer_addr,
183 hop_count: chain.hops.len(),
184 };
185
186 Ok((stream, info))
187 }
188
189 pub async fn connect_tcp_timeout(
191 &self,
192 host: &str,
193 port: u16,
194 timeout: Duration,
195 ) -> Result<(eggress_core::BoxStream, OutboundInfo), EggressError> {
196 tokio::time::timeout(timeout, self.connect_tcp(host, port))
197 .await
198 .map_err(|_| EggressError::Runtime("connection timed out".to_string()))?
199 }
200
201 pub async fn associate_udp(
209 &self,
210 _target_host: &str,
211 _target_port: u16,
212 ) -> Result<UdpAssociation, EggressError> {
213 Err(EggressError::Runtime(
214 "UDP association through OutboundConnector is not yet implemented; \
215 use the listener-based approach for UDP"
216 .to_string(),
217 ))
218 }
219
220 pub fn upstream_count(&self) -> usize {
222 self.runtime_config
223 .as_ref()
224 .map_or(0, |config| config.upstreams.len())
225 }
226
227 pub fn validate_outbound_config(config_toml: &str) -> Result<usize, EggressError> {
231 let config: eggress_config::model::ConfigFile =
232 toml::from_str(config_toml).map_err(|e| EggressError::Config(e.to_string()))?;
233
234 if let Some(version) = config.version {
235 if version != 1 {
236 return Err(EggressError::Config(format!(
237 "unsupported config version: {version}"
238 )));
239 }
240 }
241
242 eggress_config::validate::validate_config(&config).map_err(|errors| {
243 let messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
244 EggressError::Config(messages.join("; "))
245 })?;
246
247 let runtime_config = eggress_config::compile::compile_config(&config)
248 .map_err(|e| EggressError::Config(e.to_string()))?;
249
250 if runtime_config.upstreams.is_empty() {
251 return Err(EggressError::Config(
252 "no upstreams configured; cannot make outbound connections".to_string(),
253 ));
254 }
255
256 let upstream = &runtime_config.upstreams[0];
257 let chain = &upstream.chain;
258
259 if chain.hops.is_empty() {
260 return Err(EggressError::Config(
261 "upstream chain is empty; cannot make outbound connections".to_string(),
262 ));
263 }
264
265 Ok(chain.hops.len())
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_outbound_connector_from_toml() {
275 let config = r#"
276 version = 1
277 [[listeners]]
278 name = "test"
279 bind = "127.0.0.1:0"
280 protocols = ["socks5"]
281 [[upstreams]]
282 id = "direct"
283 uri = "socks5://127.0.0.1:1080"
284 "#;
285 let connector = OutboundConnector::from_toml(config).unwrap();
286 assert_eq!(connector.upstream_count(), 1);
287 }
288
289 #[test]
290 fn test_validate_no_upstreams() {
291 let config = r#"
292 version = 1
293 [[listeners]]
294 name = "test"
295 bind = "127.0.0.1:0"
296 protocols = ["socks5"]
297 "#;
298 let result = OutboundConnector::validate_outbound_config(config);
299 assert!(result.is_err());
300 assert!(result.unwrap_err().to_string().contains("no upstreams"));
301 }
302
303 #[test]
304 fn test_validate_empty_chain() {
305 let config = r#"
306 version = 1
307 [[listeners]]
308 name = "test"
309 bind = "127.0.0.1:0"
310 protocols = ["socks5"]
311 [[upstreams]]
312 id = "up"
313 uri = "socks5://127.0.0.1:1080"
314 "#;
315 let result = OutboundConnector::validate_outbound_config(config);
316 assert!(result.is_ok());
317 }
318
319 #[test]
320 fn test_from_pproxy_uri() {
321 let connector = OutboundConnector::from_pproxy_uri("socks5://127.0.0.1:1080").unwrap();
322 assert_eq!(connector.upstream_count(), 1);
323 }
324}