From 5b61e63a5903be32582c788840981dbfa95fee2e Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Mon, 25 Aug 2025 00:00:00 +0000 Subject: [PATCH] refactor(app/inbound): move fuzzing into submodule this commit moves the fuzzing facilities in `linkerd-app-inbound` into a submodule. no changes are made, besides code motion. Signed-off-by: katelyn martin --- linkerd/app/inbound/src/http.rs | 255 +-------------------------- linkerd/app/inbound/src/http/fuzz.rs | 243 +++++++++++++++++++++++++ 2 files changed, 246 insertions(+), 252 deletions(-) create mode 100644 linkerd/app/inbound/src/http/fuzz.rs diff --git a/linkerd/app/inbound/src/http.rs b/linkerd/app/inbound/src/http.rs index d896d44b3e..d47ab04be5 100644 --- a/linkerd/app/inbound/src/http.rs +++ b/linkerd/app/inbound/src/http.rs @@ -4,260 +4,11 @@ mod set_identity_header; #[cfg(test)] mod tests; +#[cfg(fuzzing)] +pub mod fuzz; + fn trace_labels() -> std::collections::HashMap { let mut l = std::collections::HashMap::new(); l.insert("direction".to_string(), "inbound".to_string()); l } - -#[cfg(fuzzing)] -pub mod fuzz { - use crate::{ - http::router::Http, - policy, - test_util::{support::connect::Connect, *}, - Config, Inbound, - }; - use hyper::{Body, Request, Response}; - use libfuzzer_sys::arbitrary::Arbitrary; - use linkerd_app_core::{ - identity, io, - proxy::http, - svc::{self, NewService}, - tls, - transport::{ClientAddr, OrigDstAddr, Remote, ServerAddr}, - NameAddr, ProxyRuntime, - }; - pub use linkerd_app_test as support; - use linkerd_app_test::*; - use std::{fmt, str, sync::Arc}; - - #[derive(Arbitrary)] - pub struct HttpRequestSpec { - pub uri: Vec, - pub header_name: Vec, - pub header_value: Vec, - pub http_method: bool, - } - - pub async fn fuzz_entry_raw(requests: Vec) { - let server = hyper::server::conn::http1::Builder::new(); - let mut client = hyper::client::conn::http1::Builder::new(); - let connect = - support::connect().endpoint_fn_boxed(Target::addr(), hello_fuzz_server(server)); - let profiles = profile::resolver(); - let profile_tx = profiles - .profile_tx(NameAddr::from_str_and_port("foo.svc.cluster.local", 5550).unwrap()); - profile_tx.send(profile::Profile::default()).unwrap(); - - // Build the outbound server - let cfg = default_config(); - let (rt, _shutdown) = runtime(); - let server = build_fuzz_server(cfg, rt, profiles, connect).new_service(Target::HTTP1); - let (mut client, bg) = http_util::connect_and_accept_http1(&mut client, server).await; - - // Now send all of the requests - for inp in requests.iter() { - if let Ok(uri) = std::str::from_utf8(&inp.uri[..]) { - if let Ok(header_name) = std::str::from_utf8(&inp.header_name[..]) { - if let Ok(header_value) = std::str::from_utf8(&inp.header_value[..]) { - let http_method = if inp.http_method { - hyper::http::Method::GET - } else { - hyper::http::Method::POST - }; - - if let Ok(req) = Request::builder() - .method(http_method) - .uri(uri) - .header(header_name, header_value) - .body(Body::default()) - { - let rsp = client.send_request(req).await; - tracing::info!(?rsp); - if let Ok(rsp) = rsp { - let body = http_util::body_to_string(rsp.into_body()).await; - tracing::info!(?body); - } - } - } - } - } - } - - // It's okay if the background task returns an error, as this would - // indicate that the proxy closed the connection --- which it will do on - // invalid inputs. We want to ensure that the proxy doesn't crash in the - // face of these inputs, and the background task will panic in this - // case. - drop(client); - let res = bg.join_all().await; - tracing::info!(?res, "background tasks completed") - } - - fn hello_fuzz_server( - http: hyper::server::conn::http1::Builder, - ) -> impl Fn(Remote) -> io::Result { - move |_endpoint| { - let (client_io, server_io) = support::io::duplex(4096); - let hello_svc = hyper::service::service_fn(|_request: Request| async move { - Ok::<_, io::Error>(Response::new(Body::from("Hello world!"))) - }); - tokio::spawn( - http.serve_connection(server_io, hello_svc) - .in_current_span(), - ); - Ok(io::BoxedIo::new(client_io)) - } - } - - fn build_fuzz_server( - cfg: Config, - rt: ProxyRuntime, - profiles: resolver::Profiles, - connect: Connect>, - ) -> svc::ArcNewTcp - where - I: io::AsyncRead + io::AsyncWrite + io::PeerAddr + Send + Unpin + 'static, - { - let connect = svc::stack(connect) - .push_map_target(|t: Http| svc::Param::>::param(&t)) - .into_inner(); - Inbound::new(cfg, rt) - .with_stack(connect) - .push_http_router(profiles) - .push_http_server() - .push_http_tcp_server() - .into_inner() - } - - impl fmt::Debug for HttpRequestSpec { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Custom `Debug` impl that formats the URI, header name, and header - // value as strings if they are UTF-8, or falls back to raw bytes - // otherwise. - let mut dbg = f.debug_struct("HttpRequestSpec"); - dbg.field("http_method", &self.http_method); - - if let Ok(uri) = str::from_utf8(&self.uri[..]) { - dbg.field("uri", &uri); - } else { - dbg.field("uri", &self.uri); - } - - if let Ok(name) = str::from_utf8(&self.header_name[..]) { - dbg.field("header_name", &name); - } else { - dbg.field("header_name", &self.header_name); - } - - if let Ok(value) = str::from_utf8(&self.header_value[..]) { - dbg.field("header_value", &value); - } else { - dbg.field("header_value", &self.header_value); - } - - dbg.finish() - } - } - - #[derive(Clone, Debug)] - struct Target(http::Variant); - - // === impl Target === - - impl Target { - const HTTP1: Self = Self(http::Variant::Http1); - - fn addr() -> SocketAddr { - ([127, 0, 0, 1], 80).into() - } - } - - impl svc::Param for Target { - fn param(&self) -> OrigDstAddr { - OrigDstAddr(Self::addr()) - } - } - - impl svc::Param> for Target { - fn param(&self) -> Remote { - Remote(ServerAddr(Self::addr())) - } - } - - impl svc::Param> for Target { - fn param(&self) -> Remote { - Remote(ClientAddr(([192, 0, 2, 3], 50000).into())) - } - } - - impl svc::Param for Target { - fn param(&self) -> http::Variant { - self.0 - } - } - - impl svc::Param for Target { - fn param(&self) -> tls::ConditionalServerTls { - tls::ConditionalServerTls::None(tls::NoServerTls::NoClientHello) - } - } - - impl svc::Param for Target { - fn param(&self) -> policy::AllowPolicy { - let (policy, _) = policy::AllowPolicy::for_test( - self.param(), - policy::ServerPolicy { - protocol: policy::Protocol::Http1(Arc::new([ - linkerd_proxy_server_policy::http::default(Arc::new([ - policy::Authorization { - authentication: policy::Authentication::Unauthenticated, - networks: vec![std::net::IpAddr::from([192, 0, 2, 3]).into()], - meta: Arc::new(policy::Meta::Resource { - group: "policy.linkerd.io".into(), - kind: "server".into(), - name: "testsaz".into(), - }), - }, - ])), - ])), - meta: Arc::new(policy::Meta::Resource { - group: "policy.linkerd.io".into(), - kind: "server".into(), - name: "testsrv".into(), - }), - local_rate_limit: Arc::new( - linkerd_proxy_server_policy::LocalRateLimit::default(), - ), - }, - ); - policy - } - } - - impl svc::Param for Target { - fn param(&self) -> policy::ServerLabel { - policy::ServerLabel( - Arc::new(policy::Meta::Resource { - group: "policy.linkerd.io".into(), - kind: "server".into(), - name: "testsrv".into(), - }), - 1000, - ) - } - } - - impl svc::Param for Target { - fn param(&self) -> http::normalize_uri::DefaultAuthority { - http::normalize_uri::DefaultAuthority(None) - } - } - - impl svc::Param> for Target { - fn param(&self) -> Option { - None - } - } -} diff --git a/linkerd/app/inbound/src/http/fuzz.rs b/linkerd/app/inbound/src/http/fuzz.rs new file mode 100644 index 0000000000..776eeaa4a6 --- /dev/null +++ b/linkerd/app/inbound/src/http/fuzz.rs @@ -0,0 +1,243 @@ +use crate::{ + http::router::Http, + policy, + test_util::{support::connect::Connect, *}, + Config, Inbound, +}; +use hyper::{Body, Request, Response}; +use libfuzzer_sys::arbitrary::Arbitrary; +use linkerd_app_core::{ + identity, io, + proxy::http, + svc::{self, NewService}, + tls, + transport::{ClientAddr, OrigDstAddr, Remote, ServerAddr}, + NameAddr, ProxyRuntime, +}; +pub use linkerd_app_test as support; +use linkerd_app_test::*; +use std::{fmt, str, sync::Arc}; + +#[derive(Arbitrary)] +pub struct HttpRequestSpec { + pub uri: Vec, + pub header_name: Vec, + pub header_value: Vec, + pub http_method: bool, +} + +pub async fn fuzz_entry_raw(requests: Vec) { + let server = hyper::server::conn::http1::Builder::new(); + let mut client = hyper::client::conn::http1::Builder::new(); + let connect = support::connect().endpoint_fn_boxed(Target::addr(), hello_fuzz_server(server)); + let profiles = profile::resolver(); + let profile_tx = + profiles.profile_tx(NameAddr::from_str_and_port("foo.svc.cluster.local", 5550).unwrap()); + profile_tx.send(profile::Profile::default()).unwrap(); + + // Build the outbound server + let cfg = default_config(); + let (rt, _shutdown) = runtime(); + let server = build_fuzz_server(cfg, rt, profiles, connect).new_service(Target::HTTP1); + let (mut client, bg) = http_util::connect_and_accept_http1(&mut client, server).await; + + // Now send all of the requests + for inp in requests.iter() { + if let Ok(uri) = std::str::from_utf8(&inp.uri[..]) { + if let Ok(header_name) = std::str::from_utf8(&inp.header_name[..]) { + if let Ok(header_value) = std::str::from_utf8(&inp.header_value[..]) { + let http_method = if inp.http_method { + hyper::http::Method::GET + } else { + hyper::http::Method::POST + }; + + if let Ok(req) = Request::builder() + .method(http_method) + .uri(uri) + .header(header_name, header_value) + .body(Body::default()) + { + let rsp = client.send_request(req).await; + tracing::info!(?rsp); + if let Ok(rsp) = rsp { + let body = http_util::body_to_string(rsp.into_body()).await; + tracing::info!(?body); + } + } + } + } + } + } + + // It's okay if the background task returns an error, as this would + // indicate that the proxy closed the connection --- which it will do on + // invalid inputs. We want to ensure that the proxy doesn't crash in the + // face of these inputs, and the background task will panic in this + // case. + drop(client); + let res = bg.join_all().await; + tracing::info!(?res, "background tasks completed") +} + +fn hello_fuzz_server( + http: hyper::server::conn::http1::Builder, +) -> impl Fn(Remote) -> io::Result { + move |_endpoint| { + let (client_io, server_io) = support::io::duplex(4096); + let hello_svc = hyper::service::service_fn(|_request: Request| async move { + Ok::<_, io::Error>(Response::new(Body::from("Hello world!"))) + }); + tokio::spawn( + http.serve_connection(server_io, hello_svc) + .in_current_span(), + ); + Ok(io::BoxedIo::new(client_io)) + } +} + +fn build_fuzz_server( + cfg: Config, + rt: ProxyRuntime, + profiles: resolver::Profiles, + connect: Connect>, +) -> svc::ArcNewTcp +where + I: io::AsyncRead + io::AsyncWrite + io::PeerAddr + Send + Unpin + 'static, +{ + let connect = svc::stack(connect) + .push_map_target(|t: Http| svc::Param::>::param(&t)) + .into_inner(); + Inbound::new(cfg, rt) + .with_stack(connect) + .push_http_router(profiles) + .push_http_server() + .push_http_tcp_server() + .into_inner() +} + +impl fmt::Debug for HttpRequestSpec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Custom `Debug` impl that formats the URI, header name, and header + // value as strings if they are UTF-8, or falls back to raw bytes + // otherwise. + let mut dbg = f.debug_struct("HttpRequestSpec"); + dbg.field("http_method", &self.http_method); + + if let Ok(uri) = str::from_utf8(&self.uri[..]) { + dbg.field("uri", &uri); + } else { + dbg.field("uri", &self.uri); + } + + if let Ok(name) = str::from_utf8(&self.header_name[..]) { + dbg.field("header_name", &name); + } else { + dbg.field("header_name", &self.header_name); + } + + if let Ok(value) = str::from_utf8(&self.header_value[..]) { + dbg.field("header_value", &value); + } else { + dbg.field("header_value", &self.header_value); + } + + dbg.finish() + } +} + +#[derive(Clone, Debug)] +struct Target(http::Variant); + +// === impl Target === + +impl Target { + const HTTP1: Self = Self(http::Variant::Http1); + + fn addr() -> SocketAddr { + ([127, 0, 0, 1], 80).into() + } +} + +impl svc::Param for Target { + fn param(&self) -> OrigDstAddr { + OrigDstAddr(Self::addr()) + } +} + +impl svc::Param> for Target { + fn param(&self) -> Remote { + Remote(ServerAddr(Self::addr())) + } +} + +impl svc::Param> for Target { + fn param(&self) -> Remote { + Remote(ClientAddr(([192, 0, 2, 3], 50000).into())) + } +} + +impl svc::Param for Target { + fn param(&self) -> http::Variant { + self.0 + } +} + +impl svc::Param for Target { + fn param(&self) -> tls::ConditionalServerTls { + tls::ConditionalServerTls::None(tls::NoServerTls::NoClientHello) + } +} + +impl svc::Param for Target { + fn param(&self) -> policy::AllowPolicy { + let (policy, _) = policy::AllowPolicy::for_test( + self.param(), + policy::ServerPolicy { + protocol: policy::Protocol::Http1(Arc::new([ + linkerd_proxy_server_policy::http::default(Arc::new([policy::Authorization { + authentication: policy::Authentication::Unauthenticated, + networks: vec![std::net::IpAddr::from([192, 0, 2, 3]).into()], + meta: Arc::new(policy::Meta::Resource { + group: "policy.linkerd.io".into(), + kind: "server".into(), + name: "testsaz".into(), + }), + }])), + ])), + meta: Arc::new(policy::Meta::Resource { + group: "policy.linkerd.io".into(), + kind: "server".into(), + name: "testsrv".into(), + }), + local_rate_limit: Arc::new(linkerd_proxy_server_policy::LocalRateLimit::default()), + }, + ); + policy + } +} + +impl svc::Param for Target { + fn param(&self) -> policy::ServerLabel { + policy::ServerLabel( + Arc::new(policy::Meta::Resource { + group: "policy.linkerd.io".into(), + kind: "server".into(), + name: "testsrv".into(), + }), + 1000, + ) + } +} + +impl svc::Param for Target { + fn param(&self) -> http::normalize_uri::DefaultAuthority { + http::normalize_uri::DefaultAuthority(None) + } +} + +impl svc::Param> for Target { + fn param(&self) -> Option { + None + } +}