spin_factor_outbound_networking/
lib.rs1mod allowed_hosts;
2pub mod runtime_config;
3mod tls;
4
5use std::{collections::HashMap, 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 resolver = ctx
136 .instance_builder::<VariablesFactor>()?
137 .expression_resolver()
138 .clone();
139 let component_ids = ctx
140 .app_component()
141 .app
142 .components()
143 .map(|c| c.id().to_string())
144 .collect::<Vec<_>>();
145 let allowed_hosts_future = async move {
146 let prepared = resolver.prepare().await.inspect_err(|err| {
147 tracing::error!(
148 %err, "error.type" = "variable_resolution_failed",
149 "Error resolving variables when checking request against allowed outbound hosts",
150 );
151 })?;
152 AllowedHostsConfig::parse(&hosts, &prepared, &component_ids).inspect_err(|err| {
153 tracing::error!(
154 %err, "error.type" = "invalid_allowed_hosts",
155 "Error parsing allowed outbound hosts",
156 );
157 })
158 }
159 .map(|res| res.map(Arc::new).map_err(Arc::new))
160 .boxed()
161 .shared();
162 let allowed_hosts = OutboundAllowedHosts::new(
163 allowed_hosts_future.clone(),
164 self.disallowed_host_handler.clone(),
165 );
166 let blocked_networks = ctx.app_state().blocked_networks.clone();
167 let permit_state = ctx
168 .app_state()
169 .socket_connection_semaphore
170 .clone()
171 .map(SocketPermitState::new);
172
173 match ctx.instance_builder::<WasiFactor>() {
174 Ok(wasi_builder) => {
175 if let Some(state) = permit_state {
176 wasi_builder.set_socket_permit_state(state);
177 }
178
179 let allowed_hosts = allowed_hosts.clone();
180 wasi_builder.outbound_socket_addr_check(move |addr, addr_use| {
181 let allowed_hosts = allowed_hosts.clone();
182 let blocked_networks = blocked_networks.clone();
183 async move {
184 let scheme = match addr_use {
185 SocketAddrUse::TcpBind => return false,
186 SocketAddrUse::TcpConnect => "tcp",
187 SocketAddrUse::UdpBind
188 | SocketAddrUse::UdpConnect
189 | SocketAddrUse::UdpOutgoingDatagram => "udp",
190 };
191 if !allowed_hosts
192 .check_url(&addr.to_string(), scheme)
193 .await
194 .unwrap_or(
195 false,
197 )
198 {
199 return false;
200 }
201 if blocked_networks.is_blocked(&addr) {
202 tracing::error!(
203 "error.type" = "destination_ip_prohibited",
204 ?addr,
205 "destination IP prohibited by runtime config"
206 );
207 return false;
208 }
209 true
210 }
211 });
212 }
213 Err(Error::NoSuchFactor(_)) => (), Err(err) => return Err(err.into()),
215 }
216
217 let component_tls_configs = ctx
218 .app_state()
219 .tls_client_configs
220 .get_component_tls_configs(ctx.app_component().id());
221
222 Ok(InstanceBuilder {
223 allowed_hosts,
224 blocked_networks: ctx.app_state().blocked_networks.clone(),
225 component_tls_client_configs: component_tls_configs,
226 })
227 }
228}
229
230pub struct AppState {
231 component_allowed_hosts: HashMap<String, Arc<[String]>>,
233 blocked_networks: BlockedNetworks,
235 tls_client_configs: TlsClientConfigs,
237 socket_connection_semaphore: Option<ConnectionSemaphore>,
240 global_connection_semaphore: Option<LimitedSemaphore>,
244 app_id: Arc<str>,
247}
248
249impl AppState {
250 pub fn app_id(&self) -> Arc<str> {
253 self.app_id.clone()
254 }
255}
256
257pub fn build_connection_semaphore(
267 networking: Option<&AppState>,
268 factor: &'static str,
269 factor_limit: Option<usize>,
270 wait_timeout: Option<std::time::Duration>,
271) -> ConnectionSemaphore {
272 let app_id = networking
273 .map(|n| n.app_id())
274 .unwrap_or_else(|| Arc::from("<unnamed>"));
275 let global = networking.and_then(|n| n.global_connection_semaphore.clone());
276 if let (Some(per_factor), Some(global_limit)) =
277 (factor_limit, global.as_ref().map(LimitedSemaphore::limit))
278 && per_factor > global_limit
279 {
280 tracing::warn!(
281 "outbound_{factor} max_connections ({per_factor}) exceeds global \
282 max_total_connections ({global_limit}); the global limit will be the \
283 effective cap"
284 );
285 }
286 ConnectionSemaphore::new(
287 global,
288 factor_limit.map(LimitedSemaphore::new),
289 factor,
290 app_id,
291 wait_timeout,
292 )
293}
294
295pub struct InstanceBuilder {
296 allowed_hosts: OutboundAllowedHosts,
297 blocked_networks: BlockedNetworks,
298 component_tls_client_configs: ComponentTlsClientConfigs,
299}
300
301impl InstanceBuilder {
302 pub fn allowed_hosts(&self) -> OutboundAllowedHosts {
303 self.allowed_hosts.clone()
304 }
305
306 pub fn blocked_networks(&self) -> BlockedNetworks {
307 self.blocked_networks.clone()
308 }
309
310 pub fn component_tls_configs(&self) -> ComponentTlsClientConfigs {
311 self.component_tls_client_configs.clone()
312 }
313}
314
315impl FactorInstanceBuilder for InstanceBuilder {
316 type InstanceState = ();
317
318 fn build(self) -> anyhow::Result<Self::InstanceState> {
319 Ok(())
320 }
321}
322
323pub fn record_address_fields(address: &str) {
335 if let Ok(url) = Url::parse(address) {
336 let span = tracing::Span::current();
337 span.record(
338 otel_attribute::SERVER_ADDRESS,
339 url.host_str().unwrap_or_default(),
340 );
341 span.record(otel_attribute::SERVER_PORT, url.port().unwrap_or_default());
342 span.record(
343 otel_attribute::DB_NAMESPACE,
344 url.path().trim_start_matches('/'),
345 );
346 }
347}