Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ aws-sdk-s3 = { version = "1.129.0", default-features = false, features = ["rt-to
aws-sdk-secretsmanager = { version = "1.103.0", default-features = false, features = ["rt-tokio"] }
aws-sdk-sts = { version = "1.41.0", default-features = false, features = ["rt-tokio"] }
aws-sigv4 = "1.3.6"
# We deliberately select the non-FIPS `aws_lc_rs` provider (via `rustls-aws-lc`)
# to match the rest of the crypto stack. A follow-up flips to `rustls-aws-lc-fips`
# once the NIST certificate lands.
aws-smithy-http-client = { version = "1.1.12", default-features = false, features = ["rustls-aws-lc"] }
aws-smithy-runtime = { version = "1.9.8", features = ["connector-hyper-0-14-x"] }
aws-smithy-runtime-api = "1.10.0"
aws-smithy-types = { version = "1.1.8", features = ["byte-stream-poll-next"] }
Expand Down
5 changes: 1 addition & 4 deletions src/aws-util/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,15 @@ workspace = true
anyhow.workspace = true
aws-config.workspace = true
aws-sdk-s3 = { workspace = true, optional = true }
aws-smithy-http-client.workspace = true
aws-smithy-runtime-api.workspace = true
aws-smithy-runtime.workspace = true
aws-smithy-types.workspace = true
aws-types.workspace = true
bytes.workspace = true
bytesize.workspace = true
futures.workspace = true
http.workspace = true
hyper-0-14.workspace = true
hyper-tls = "0.5.0"
mz-ore = { path = "../ore", features = ["async", "network"], default-features = false }
tower-service.workspace = true
pin-project.workspace = true
thiserror.workspace = true
tokio.workspace = true
Expand Down
126 changes: 58 additions & 68 deletions src/aws-util/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,10 @@
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0.

use std::future::Future;
use std::net::SocketAddr;
use std::pin::Pin;
use std::task::{Context, Poll};

use aws_config::{BehaviorVersion, ConfigLoader};
use aws_smithy_runtime::client::http::hyper_014::HyperClientBuilder;
use aws_smithy_runtime_api::client::http::{HttpClient, SharedHttpClient};
use hyper_0_14::client::HttpConnector;
use hyper_0_14::client::connect::dns::Name;
use hyper_tls::HttpsConnector;
use tower_service::Service;
use aws_smithy_http_client::{Builder, tls};
use aws_smithy_runtime_api::client::dns::{DnsFuture, ResolveDns, ResolveDnsError};
use aws_smithy_runtime_api::client::http::SharedHttpClient;

#[cfg(feature = "s3")]
pub mod s3;
Expand Down Expand Up @@ -47,67 +39,62 @@ pub fn defaults() -> ConfigLoader {

/// Returns an HTTP client for use with the AWS SDK that is appropriately
/// configured for Materialize.
pub fn http_client() -> impl HttpClient {
// The default AWS HTTP client uses rustls, while our company policy is to
// use native TLS.
HyperClientBuilder::new().build(HttpsConnector::new())
pub fn http_client() -> SharedHttpClient {
// Company policy is to use rustls with the aws-lc-rs crypto provider so the
// AWS SDK path fits the FIPS 140-3 crypto story, rather than the SDK's
// default OS-native TLS stack. We use the non-FIPS `AwsLc` mode to match the
// rest of the stack. A follow-up flips to the FIPS-validated provider once
// the NIST certificate lands.
Builder::new()
.tls_provider(tls::Provider::Rustls(
tls::rustls_provider::CryptoMode::AwsLc,
))
.build_https()
}

/// Returns an AWS SDK HTTP client whose DNS resolver delegates to
/// [`mz_ore::netio::resolve_address`].
///
/// Only the IP resolution step is overridden — the SDK still uses the original
/// Only the IP resolution step is overridden. The SDK still uses the original
/// hostname for SNI and TLS certificate validation, so HTTPS endpoints work
/// unchanged.
pub fn http_client_with_resolver(enforce_external_addresses: bool) -> SharedHttpClient {
let resolver = MzAwsResolver {
enforce_external_addresses,
};
let mut http = HttpConnector::new_with_resolver(resolver);
// The SDK speaks HTTPS to the public AWS API; the wrapper we build below
// handles TLS, but the underlying HTTP connector must allow non-`http://`
// schemes.
http.enforce_http(false);
let https = HttpsConnector::new_with_connector(http);
HyperClientBuilder::new().build(https)
Builder::new()
.tls_provider(tls::Provider::Rustls(
tls::rustls_provider::CryptoMode::AwsLc,
))
.build_with_resolver(MzAwsResolver {
enforce_external_addresses,
})
}

/// A `tower_service::Service<Name>` resolver that delegates to
/// [`mz_ore::netio::resolve_address`], used by [`http_client_with_resolver`].
#[derive(Clone)]
/// A [`ResolveDns`] resolver that delegates to [`mz_ore::netio::resolve_address`],
/// used by [`http_client_with_resolver`].
///
/// Smithy applies the endpoint's port to the resolved IPs itself, so this only
/// yields bare [`IpAddr`](std::net::IpAddr)s.
#[derive(Debug, Clone)]
struct MzAwsResolver {
enforce_external_addresses: bool,
}

impl Service<Name> for MzAwsResolver {
type Response = std::vec::IntoIter<SocketAddr>;
type Error = mz_ore::netio::DnsResolutionError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}

fn call(&mut self, name: Name) -> Self::Future {
impl ResolveDns for MzAwsResolver {
fn resolve_dns<'a>(&'a self, name: &'a str) -> DnsFuture<'a> {
let enforce = self.enforce_external_addresses;
let host = name.as_str().to_string();
Box::pin(async move {
let ips = mz_ore::netio::resolve_address(&host, enforce).await?;
// Hyper substitutes the URL's port (or the default for the scheme)
// when the SocketAddr's port is 0.
Ok(ips
.into_iter()
.map(|ip| SocketAddr::new(ip, 0))
.collect::<Vec<_>>()
.into_iter())
let host = name.to_string();
DnsFuture::new(async move {
let ips = mz_ore::netio::resolve_address(&host, enforce)
.await
.map_err(ResolveDnsError::new)?;
Ok(ips.into_iter().collect())
})
}
}

#[cfg(test)]
mod tests {
use std::error::Error;
use std::net::IpAddr;
use std::str::FromStr;

use mz_ore::netio::DnsResolutionError;

Expand All @@ -116,44 +103,47 @@ mod tests {
#[mz_ore::test(tokio::test)]
#[cfg_attr(miri, ignore)]
async fn resolver_rejects_loopback_when_enforced() {
let mut resolver = MzAwsResolver {
let resolver = MzAwsResolver {
enforce_external_addresses: true,
};
let name = Name::from_str("127.0.0.1").unwrap();
let err = resolver.call(name).await.expect_err("must reject loopback");
let err = resolver
.resolve_dns("127.0.0.1")
.await
.expect_err("must reject loopback");
// Smithy wraps our `DnsResolutionError` in a `ResolveDnsError`, so peel
// back the source to confirm the rejection reason is the private address.
let source = err
.source()
.and_then(|src| src.downcast_ref::<DnsResolutionError>());
assert!(
matches!(err, DnsResolutionError::PrivateAddress),
matches!(source, Some(DnsResolutionError::PrivateAddress)),
"got {err:?}"
);
}

#[mz_ore::test(tokio::test)]
#[cfg_attr(miri, ignore)]
async fn resolver_allows_loopback_when_not_enforced() {
let mut resolver = MzAwsResolver {
let resolver = MzAwsResolver {
enforce_external_addresses: false,
};
let name = Name::from_str("127.0.0.1").unwrap();
let addrs: Vec<SocketAddr> = resolver
.call(name)
let addrs: Vec<IpAddr> = resolver
.resolve_dns("127.0.0.1")
.await
.expect("loopback should resolve when enforcement is off")
.collect();
assert!(addrs.iter().any(|a| a.ip() == IpAddr::from([127, 0, 0, 1])));
.expect("loopback should resolve when enforcement is off");
assert!(addrs.iter().any(|ip| *ip == IpAddr::from([127, 0, 0, 1])));
}

#[mz_ore::test(tokio::test)]
#[cfg_attr(miri, ignore)]
async fn resolver_allows_public_when_enforced() {
let mut resolver = MzAwsResolver {
let resolver = MzAwsResolver {
enforce_external_addresses: true,
};
let name = Name::from_str("8.8.8.8").unwrap();
let addrs: Vec<SocketAddr> = resolver
.call(name)
let addrs: Vec<IpAddr> = resolver
.resolve_dns("8.8.8.8")
.await
.expect("public IP should resolve")
.collect();
assert!(addrs.iter().any(|a| a.ip() == IpAddr::from([8, 8, 8, 8])));
.expect("public IP should resolve");
assert!(addrs.iter().any(|ip| *ip == IpAddr::from([8, 8, 8, 8])));
}
}
Loading