spin_factor_outbound_networking/
lib.rs1mod allowed_hosts;
2pub mod runtime_config;
3mod tls;
4
5use std::{collections::HashMap, net::SocketAddr, sync::Arc};
6
7use futures_util::FutureExt as _;
8use opentelemetry_semantic_conventions::attribute as otel_attribute;
9use spin_factor_variables::VariablesFactor;
10use spin_factor_wasi::{SocketAddrUse, SocketPermitState, WasiFactor};
11use spin_factors::{
12 ConfigureAppContext, Error, Factor, FactorInstanceBuilder, PrepareContext, RuntimeFactors,
13 anyhow::{self, Context},
14};
15use spin_locked_app::APP_NAME_KEY;
16use spin_outbound_networking_config::allowed_hosts::{DisallowedHostHandler, OutboundAllowedHosts};
17use url::Url;
18
19use crate::{
20 allowed_hosts::allowed_outbound_hosts, runtime_config::RuntimeConfig, tls::TlsClientConfigs,
21};
22pub use allowed_hosts::validate_service_chaining_for_components;
23pub use spin_connection_semaphore::{ConnectionPermit, ConnectionSemaphore, LimitedSemaphore};
24
25pub use crate::tls::{ComponentTlsClientConfigs, TlsClientConfig};
26use config::allowed_hosts::AllowedHostsConfig;
27use config::blocked_networks::BlockedNetworks;
28pub use spin_outbound_networking_config as config;
29
30#[derive(Default)]
31pub struct OutboundNetworkingFactor {
32 disallowed_host_handler: Option<Arc<dyn DisallowedHostHandler>>,
33}
34
35impl OutboundNetworkingFactor {
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn set_disallowed_host_handler(&mut self, handler: impl DisallowedHostHandler + 'static) {
43 self.disallowed_host_handler = Some(Arc::new(handler));
44 }
45}
46
47impl Factor for OutboundNetworkingFactor {
48 type RuntimeConfig = RuntimeConfig;
49 type AppState = AppState;
50 type InstanceBuilder = InstanceBuilder;
51
52 fn configure_app<T: RuntimeFactors>(
53 &self,
54 mut ctx: ConfigureAppContext<T, Self>,
55 ) -> anyhow::Result<Self::AppState> {
56 let component_allowed_hosts = ctx
58 .app()
59 .components()
60 .map(|component| {
61 Ok((
62 component.id().to_string(),
63 allowed_outbound_hosts(&component)?
64 .into_boxed_slice()
65 .into(),
66 ))
67 })
68 .collect::<anyhow::Result<_>>()?;
69
70 let RuntimeConfig {
71 client_tls_configs,
72 blocked_ip_networks: block_networks,
73 block_private_networks,
74 max_socket_connections,
75 max_total_connections,
76 wait_timeout,
77 } = ctx.take_runtime_config().unwrap_or_default();
78
79 let blocked_networks = BlockedNetworks::new(block_networks, block_private_networks);
80 let tls_client_configs = TlsClientConfigs::new(client_tls_configs)?;
81 let global_connection_semaphore = max_total_connections.map(LimitedSemaphore::new);
84
85 if let (Some(socket_cap), Some(global_cap)) =
86 (max_socket_connections, max_total_connections)
87 && socket_cap > global_cap
88 {
89 tracing::warn!(
90 "outbound_networking max_socket_connections ({socket_cap}) exceeds \
91 max_total_connections ({global_cap}); the global limit will be the effective \
92 cap for TCP/UDP sockets"
93 );
94 }
95
96 let app_id: Arc<str> = ctx
97 .app()
98 .get_metadata(APP_NAME_KEY)?
99 .unwrap_or_else(|| "<unnamed>".into())
100 .into();
101
102 let socket_connection_semaphore =
103 if max_socket_connections.is_some() || global_connection_semaphore.is_some() {
104 Some(ConnectionSemaphore::new(
105 global_connection_semaphore.clone(),
106 max_socket_connections.map(LimitedSemaphore::new),
107 "wasi-sockets",
108 app_id.clone(),
109 wait_timeout,
110 ))
111 } else {
112 None
113 };
114
115 Ok(AppState {
116 component_allowed_hosts,
117 blocked_networks,
118 tls_client_configs,
119 socket_connection_semaphore,
120 global_connection_semaphore,
121 app_id,
122 })
123 }
124
125 fn prepare<T: RuntimeFactors>(
126 &self,
127 mut ctx: PrepareContext<T, Self>,
128 ) -> anyhow::Result<Self::InstanceBuilder> {
129 let hosts = ctx
130 .app_state()
131 .component_allowed_hosts
132 .get(ctx.app_component().id())
133 .cloned()
134 .context("missing component allowed hosts")?;
135 let is_empty = hosts.is_empty();
136 let resolver = ctx
137 .instance_builder::<VariablesFactor>()?
138 .expression_resolver()
139 .clone();
140 let component_ids = ctx
141 .app_component()
142 .app
143 .components()
144 .map(|c| c.id().to_string())
145 .collect::<Vec<_>>();
146 let allowed_hosts_future = async move {
147 let prepared = resolver.prepare().await.inspect_err(|err| {
148 tracing::error!(
149 %err, "error.type" = "variable_resolution_failed",
150 "Error resolving variables when checking request against allowed outbound hosts",
151 );
152 })?;
153 AllowedHostsConfig::parse(&hosts, &prepared, &component_ids).inspect_err(|err| {
154 tracing::error!(
155 %err, "error.type" = "invalid_allowed_hosts",
156 "Error parsing allowed outbound hosts",
157 );
158 })
159 }
160 .map(|res| res.map(Arc::new).map_err(Arc::new))
161 .boxed()
162 .shared();
163 let allowed_hosts = OutboundAllowedHosts::new(
164 allowed_hosts_future.clone(),
165 self.disallowed_host_handler.clone(),
166 is_empty,
167 );
168 let blocked_networks = ctx.app_state().blocked_networks.clone();
169 let permit_state = ctx
170 .app_state()
171 .socket_connection_semaphore
172 .clone()
173 .map(SocketPermitState::new);
174
175 match ctx.instance_builder::<WasiFactor>() {
176 Ok(wasi_builder) => {
177 if let Some(state) = permit_state {
178 wasi_builder.set_socket_permit_state(state);
179 }
180
181 let allowed_hosts = allowed_hosts.clone();
182
183 wasi_builder.allow_tcp_udp(!allowed_hosts.is_empty());
184
185 wasi_builder.outbound_socket_addr_check(move |addr, addr_use| {
186 socket_addr_use_allowed(
187 allowed_hosts.clone(),
188 blocked_networks.clone(),
189 addr,
190 addr_use,
191 )
192 });
193 }
194 Err(Error::NoSuchFactor(_)) => (), Err(err) => return Err(err.into()),
196 }
197
198 let component_tls_configs = ctx
199 .app_state()
200 .tls_client_configs
201 .get_component_tls_configs(ctx.app_component().id());
202
203 Ok(InstanceBuilder {
204 allowed_hosts,
205 blocked_networks: ctx.app_state().blocked_networks.clone(),
206 component_tls_client_configs: component_tls_configs,
207 })
208 }
209}
210
211pub struct AppState {
212 component_allowed_hosts: HashMap<String, Arc<[String]>>,
214 blocked_networks: BlockedNetworks,
216 tls_client_configs: TlsClientConfigs,
218 socket_connection_semaphore: Option<ConnectionSemaphore>,
221 global_connection_semaphore: Option<LimitedSemaphore>,
225 app_id: Arc<str>,
228}
229
230impl AppState {
231 pub fn app_id(&self) -> Arc<str> {
234 self.app_id.clone()
235 }
236}
237
238pub fn build_connection_semaphore(
248 networking: Option<&AppState>,
249 factor: &'static str,
250 factor_limit: Option<usize>,
251 wait_timeout: Option<std::time::Duration>,
252) -> ConnectionSemaphore {
253 let app_id = networking
254 .map(|n| n.app_id())
255 .unwrap_or_else(|| Arc::from("<unnamed>"));
256 let global = networking.and_then(|n| n.global_connection_semaphore.clone());
257 if let (Some(per_factor), Some(global_limit)) =
258 (factor_limit, global.as_ref().map(LimitedSemaphore::limit))
259 && per_factor > global_limit
260 {
261 tracing::warn!(
262 "outbound_{factor} max_connections ({per_factor}) exceeds global \
263 max_total_connections ({global_limit}); the global limit will be the \
264 effective cap"
265 );
266 }
267 ConnectionSemaphore::new(
268 global,
269 factor_limit.map(LimitedSemaphore::new),
270 factor,
271 app_id,
272 wait_timeout,
273 )
274}
275
276pub struct InstanceBuilder {
277 allowed_hosts: OutboundAllowedHosts,
278 blocked_networks: BlockedNetworks,
279 component_tls_client_configs: ComponentTlsClientConfigs,
280}
281
282impl InstanceBuilder {
283 pub fn allowed_hosts(&self) -> OutboundAllowedHosts {
284 self.allowed_hosts.clone()
285 }
286
287 pub fn blocked_networks(&self) -> BlockedNetworks {
288 self.blocked_networks.clone()
289 }
290
291 pub fn component_tls_configs(&self) -> ComponentTlsClientConfigs {
292 self.component_tls_client_configs.clone()
293 }
294}
295
296impl FactorInstanceBuilder for InstanceBuilder {
297 type InstanceState = Self;
298
299 fn build(self) -> anyhow::Result<Self::InstanceState> {
300 Ok(self)
301 }
302}
303
304pub fn record_address_fields(address: &str) {
316 if let Ok(url) = Url::parse(address) {
317 let span = tracing::Span::current();
318 span.record(
319 otel_attribute::SERVER_ADDRESS,
320 url.host_str().unwrap_or_default(),
321 );
322 span.record(otel_attribute::SERVER_PORT, url.port().unwrap_or_default());
323 span.record(
324 otel_attribute::DB_NAMESPACE,
325 url.path().trim_start_matches('/'),
326 );
327 }
328}
329
330async fn socket_addr_use_allowed(
331 allowed_hosts: OutboundAllowedHosts,
332 blocked_networks: BlockedNetworks,
333 addr: SocketAddr,
334 addr_use: SocketAddrUse,
335) -> bool {
336 let scheme = match addr_use {
337 SocketAddrUse::TcpListen | SocketAddrUse::TcpAccept => {
338 return false;
339 }
340 SocketAddrUse::TcpBind => {
341 return addr.ip().is_unspecified() && addr.port() == 0;
344 }
345 SocketAddrUse::TcpConnect => "tcp",
346 SocketAddrUse::UdpBind | SocketAddrUse::UdpReceive | SocketAddrUse::UdpSend => "udp",
347 };
348 if !allowed_hosts
349 .check_url(&addr.to_string(), scheme)
350 .await
351 .unwrap_or(
352 false,
354 )
355 {
356 return false;
357 }
358 if blocked_networks.is_blocked(&addr) {
359 tracing::error!(
360 "error.type" = "destination_ip_prohibited",
361 ?addr,
362 "destination IP prohibited by runtime config"
363 );
364 return false;
365 }
366 true
367}
368
369pub async fn check_socket_addr_use(
370 allowed_hosts: OutboundAllowedHosts,
371 blocked_networks: BlockedNetworks,
372 addr: SocketAddr,
373 addr_use: SocketAddrUse,
374) -> std::io::Result<()> {
375 if socket_addr_use_allowed(allowed_hosts, blocked_networks, addr, addr_use).await {
376 Ok(())
377 } else {
378 Err(std::io::Error::new(
379 std::io::ErrorKind::PermissionDenied,
380 "An address was not permitted by the socket address check.",
381 ))
382 }
383}