diff --git a/ngrok/Cargo.toml b/ngrok/Cargo.toml index de7210d..637d25a 100644 --- a/ngrok/Cargo.toml +++ b/ngrok/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ngrok" -version = "0.14.0-pre.11" +version = "0.14.0-pre.12" edition = "2021" license = "MIT OR Apache-2.0" description = "The ngrok agent SDK" @@ -43,6 +43,7 @@ url = "2.4.0" rustls-native-certs = "0.7.0" proxy-protocol = "0.5.0" pin-project = "1.1.3" +bitflags = "2.4.2" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.45.0", features = ["Win32_Foundation"] } diff --git a/ngrok/examples/axum.rs b/ngrok/examples/axum.rs index b5281ae..dbb9ed9 100644 --- a/ngrok/examples/axum.rs +++ b/ngrok/examples/axum.rs @@ -46,6 +46,7 @@ async fn start_tunnel() -> anyhow::Result { // .circuit_breaker(0.5) // .compression() // .deny_cidr("10.1.1.1/32") + // .verify_upstream_tls(false) // .domain(".ngrok.io") // .forwards_to("example rust") // .mutual_tlsca(CA_CERT.into()) diff --git a/ngrok/examples/connect.rs b/ngrok/examples/connect.rs index 7882097..e3afc32 100644 --- a/ngrok/examples/connect.rs +++ b/ngrok/examples/connect.rs @@ -27,6 +27,7 @@ async fn main() -> anyhow::Result<()> { .tcp_endpoint() // .allow_cidr("0.0.0.0/0") // .deny_cidr("10.1.1.1/32") + // .verify_upstream_tls(false) // .forwards_to("example rust"), // .proxy_proto(ProxyProto::None) // .remote_addr(".tcp.ngrok.io:

") diff --git a/ngrok/examples/labeled.rs b/ngrok/examples/labeled.rs index 745abae..f91d2bc 100644 --- a/ngrok/examples/labeled.rs +++ b/ngrok/examples/labeled.rs @@ -41,7 +41,8 @@ async fn start_tunnel() -> anyhow::Result { let tun = sess .labeled_tunnel() - //.app_protocol("http2") + // .app_protocol("http2") + // .verify_upstream_tls(false) .label("edge", "edghts_") .metadata("example tunnel metadata from rust") .listen() diff --git a/ngrok/examples/tls.rs b/ngrok/examples/tls.rs index 098f279..583bbf6 100644 --- a/ngrok/examples/tls.rs +++ b/ngrok/examples/tls.rs @@ -47,6 +47,7 @@ async fn start_tunnel() -> anyhow::Result { .tls_endpoint() // .allow_cidr("0.0.0.0/0") // .deny_cidr("10.1.1.1/32") + // .verify_upstream_tls(false) // .domain(".ngrok.io") // .forwards_to("example rust"), // .mutual_tlsca(CA_CERT.into()) diff --git a/ngrok/src/config/common.rs b/ngrok/src/config/common.rs index 30b42ad..89cc03b 100644 --- a/ngrok/src/config/common.rs +++ b/ngrok/src/config/common.rs @@ -130,6 +130,8 @@ pub(crate) trait TunnelConfig { fn forwards_to(&self) -> String; /// The L7 protocol the upstream service expects fn forwards_proto(&self) -> String; + /// Whether to disable certificate verification for this tunnel. + fn verify_upstream_tls(&self) -> bool; /// Internal-only, extra data sent when binding a tunnel. fn extra(&self) -> BindExtra; /// The protocol for this tunnel. @@ -152,6 +154,9 @@ where fn forwards_proto(&self) -> String { (**self).forwards_proto() } + fn verify_upstream_tls(&self) -> bool { + (**self).verify_upstream_tls() + } fn extra(&self) -> BindExtra { (**self).extra() } @@ -199,6 +204,8 @@ pub(crate) struct CommonOpts { pub(crate) forwards_to: Option, // Tunnel L7 app protocol pub(crate) forwards_proto: Option, + // Whether to disable certificate verification for this tunnel. + verify_upstream_tls: Option, // Policy that defines rules that should be applied to incoming or outgoing // connections to the edge. pub(crate) policy: Option, @@ -215,6 +222,14 @@ impl CommonOpts { self.forwards_to = Some(to_url.as_str().into()); self } + + pub(crate) fn set_verify_upstream_tls(&mut self, verify_upstream_tls: bool) { + self.verify_upstream_tls = Some(verify_upstream_tls) + } + + pub(crate) fn verify_upstream_tls(&self) -> bool { + self.verify_upstream_tls.unwrap_or(true) + } } // transform into the wire protocol format diff --git a/ngrok/src/config/http.rs b/ngrok/src/config/http.rs index 81372cc..4f54c6a 100644 --- a/ngrok/src/config/http.rs +++ b/ngrok/src/config/http.rs @@ -142,6 +142,10 @@ impl TunnelConfig for HttpOptions { self.common_opts.forwards_proto.clone().unwrap_or_default() } + fn verify_upstream_tls(&self) -> bool { + self.common_opts.verify_upstream_tls() + } + fn extra(&self) -> BindExtra { BindExtra { token: Default::default(), @@ -266,6 +270,14 @@ impl HttpTunnelBuilder { self } + /// Disables backend TLS certificate verification for forwards from this tunnel. + pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut Self { + self.options + .common_opts + .set_verify_upstream_tls(verify_upstream_tls); + self + } + /// Sets the scheme for this edge. pub fn scheme(&mut self, scheme: Scheme) -> &mut Self { self.options.scheme = scheme; diff --git a/ngrok/src/config/labeled.rs b/ngrok/src/config/labeled.rs index 868ce2f..5e6ee14 100644 --- a/ngrok/src/config/labeled.rs +++ b/ngrok/src/config/labeled.rs @@ -41,6 +41,10 @@ impl TunnelConfig for LabeledOptions { self.common_opts.forwards_proto.clone().unwrap_or_default() } + fn verify_upstream_tls(&self) -> bool { + self.common_opts.verify_upstream_tls() + } + fn extra(&self) -> BindExtra { BindExtra { token: Default::default(), @@ -101,6 +105,14 @@ impl LabeledTunnelBuilder { self } + /// Disables backend TLS certificate verification for forwards from this tunnel. + pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut Self { + self.options + .common_opts + .set_verify_upstream_tls(verify_upstream_tls); + self + } + pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self { self.options.common_opts.for_forwarding_to(to_url); self diff --git a/ngrok/src/config/tcp.rs b/ngrok/src/config/tcp.rs index a1edcd3..33f23ac 100644 --- a/ngrok/src/config/tcp.rs +++ b/ngrok/src/config/tcp.rs @@ -57,6 +57,10 @@ impl TunnelConfig for TcpOptions { String::new() } + fn verify_upstream_tls(&self) -> bool { + self.common_opts.verify_upstream_tls() + } + fn opts(&self) -> Option { // fill out all the options, translating to proto here let mut tcp_endpoint = proto::TcpEndpoint::default(); @@ -124,6 +128,15 @@ impl TcpTunnelBuilder { self.options.common_opts.forwards_to = Some(forwards_to.into()); self } + + /// Disables backend TLS certificate verification for forwards from this tunnel. + pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut Self { + self.options + .common_opts + .set_verify_upstream_tls(verify_upstream_tls); + self + } + /// Sets the TCP address to request for this edge. /// /// https://ngrok.com/docs/network-edge/domains-and-tcp-addresses/#tcp-addresses diff --git a/ngrok/src/config/tls.rs b/ngrok/src/config/tls.rs index 2bf6477..69fd037 100644 --- a/ngrok/src/config/tls.rs +++ b/ngrok/src/config/tls.rs @@ -52,6 +52,10 @@ impl TunnelConfig for TlsOptions { String::new() } + fn verify_upstream_tls(&self) -> bool { + self.common_opts.verify_upstream_tls() + } + fn extra(&self) -> BindExtra { BindExtra { token: Default::default(), @@ -143,6 +147,15 @@ impl TlsTunnelBuilder { self.options.common_opts.forwards_to = Some(forwards_to.into()); self } + + /// Disables backend TLS certificate verification for forwards from this tunnel. + pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut Self { + self.options + .common_opts + .set_verify_upstream_tls(verify_upstream_tls); + self + } + /// Sets the domain to request for this edge. /// /// https://ngrok.com/docs/network-edge/domains-and-tcp-addresses/#domains diff --git a/ngrok/src/conn.rs b/ngrok/src/conn.rs index 7c11399..ec2e830 100644 --- a/ngrok/src/conn.rs +++ b/ngrok/src/conn.rs @@ -38,6 +38,7 @@ pub(crate) struct Info { pub(crate) remote_addr: SocketAddr, pub(crate) proxy_proto: ProxyProto, pub(crate) app_protocol: Option, + pub(crate) verify_upstream_tls: bool, } impl ConnInfo for Info { diff --git a/ngrok/src/online_tests.rs b/ngrok/src/online_tests.rs index e3fcbf4..c3db209 100644 --- a/ngrok/src/online_tests.rs +++ b/ngrok/src/online_tests.rs @@ -34,6 +34,7 @@ use futures_rustls::rustls::{ ClientConfig, RootCertStore, }; +// use native_tls; use hyper::{ header, HeaderMap, @@ -688,6 +689,48 @@ async fn tls() -> Result<(), Error> { Ok(()) } +#[test] +#[cfg_attr(not(feature = "authenticated-tests"), ignore)] +async fn app_protocol() -> Result<(), Error> { + let tun = Session::builder() + .authtoken_from_env() + .connect() + .await? + .http_endpoint() + .app_protocol("http2") + .listen_and_forward("https://ngrok.com".parse()?) + .await?; + + // smoke test + let client = reqwest::Client::new(); + let resp = client.get(tun.url()).send().await; + + assert!(resp.is_ok()); + + Ok(()) +} + +#[test] +#[cfg_attr(not(feature = "authenticated-tests"), ignore)] +async fn verify_upstream_tls() -> Result<(), Error> { + let tun = Session::builder() + .authtoken_from_env() + .connect() + .await? + .http_endpoint() + .verify_upstream_tls(false) + .listen_and_forward("https://ngrok.com".parse()?) + .await?; + + // smoke test + let client = reqwest::Client::new(); + let resp = client.get(tun.url()).send().await; + + assert!(resp.is_ok()); + + Ok(()) +} + #[cfg_attr(not(feature = "online-tests"), ignore)] #[test] async fn session_ca_cert() -> Result<(), Error> { diff --git a/ngrok/src/session.rs b/ngrok/src/session.rs index db1fb91..cbecf74 100644 --- a/ngrok/src/session.rs +++ b/ngrok/src/session.rs @@ -133,6 +133,7 @@ struct BoundTunnel { labels: HashMap, forwards_to: String, forwards_proto: String, + verify_upstream_tls: bool, tx: Sender>, } @@ -887,6 +888,7 @@ impl Session { let labels = tunnel_cfg.labels(); let forwards_to = tunnel_cfg.forwards_to(); let forwards_proto = tunnel_cfg.forwards_proto(); + let verify_upstream_tls = tunnel_cfg.verify_upstream_tls(); // non-labeled tunnel let (tunnel, bound) = if tunnel_cfg.proto() != "" { @@ -924,6 +926,7 @@ impl Session { labels, forwards_to, forwards_proto, + verify_upstream_tls, tx, }, ) @@ -959,6 +962,7 @@ impl Session { opts: Default::default(), forwards_to, forwards_proto, + verify_upstream_tls, labels, tx, }, @@ -1034,6 +1038,7 @@ async fn accept_one( let res = if let Some(tun) = guard.get(&id) { let mut header = conn.header; let app_protocol = Some(tun.forwards_proto.to_string()).filter(|s| !s.is_empty()); + let verify_upstream_tls = tun.verify_upstream_tls; // Note: this is a bit of a hack. Normally, passthrough_tls is only // a thing on edge connections, but we're making sure it's set for // endpoint connections as well. In their case, we have to look at the @@ -1055,6 +1060,7 @@ async fn accept_one( .send(Ok(ConnInner { info: crate::conn::Info { app_protocol, + verify_upstream_tls, remote_addr, header, proxy_proto, diff --git a/ngrok/src/tunnel_ext.rs b/ngrok/src/tunnel_ext.rs index 035dcec..9899804 100644 --- a/ngrok/src/tunnel_ext.rs +++ b/ngrok/src/tunnel_ext.rs @@ -14,8 +14,10 @@ use std::{ }; use async_trait::async_trait; +use bitflags::bitflags; use futures::stream::TryStreamExt; use futures_rustls::rustls::{ + self, pki_types, ClientConfig, RootCertStore, @@ -30,6 +32,7 @@ use hyper::{ }; use once_cell::sync::Lazy; use proxy_protocol::ProxyHeader; +use rustls::crypto::ring as provider; #[cfg(feature = "hyper")] use tokio::io::{ AsyncRead, @@ -134,6 +137,7 @@ impl ConnExt for EdgeConn { tokio::spawn(async move { let mut upstream = match connect( self.edge_type() == EdgeType::Tls && self.passthrough_tls(), + self.inner.info.verify_upstream_tls, self.inner.info.app_protocol.clone(), None, // Edges don't support proxyproto (afaik) &url, @@ -167,6 +171,7 @@ impl ConnExt for EndpointConn { let proto_http = matches!(self.proto(), "http" | "https"); let passthrough_tls = self.inner.info.passthrough_tls(); let app_protocol = self.inner.info.app_protocol.clone(); + let verify_upstream_tls = self.inner.info.verify_upstream_tls; let (mut stream, proxy_header) = match proxy_proto { ProxyProto::None => (crate::proxy_proto::Stream::disabled(self), None), @@ -188,6 +193,7 @@ impl ConnExt for EndpointConn { let mut upstream = match connect( proto_tls && passthrough_tls, + verify_upstream_tls, app_protocol, proxy_header, &url, @@ -211,7 +217,10 @@ impl ConnExt for EndpointConn { } } -fn tls_config(app_protocol: Option) -> Result, &'static io::Error> { +fn tls_config( + app_protocol: Option, + verify_upstream_tls: bool, +) -> Result, &'static io::Error> { // The root certificate store, lazily loaded once. static ROOT_STORE: Lazy> = Lazy::new(|| { let der_certs = rustls_native_certs::load_native_certs()? @@ -228,40 +237,68 @@ fn tls_config(app_protocol: Option) -> Result, &'stati // Disabling the lint because this is a local static that doesn't escape the // enclosing context. It fine. #[allow(clippy::type_complexity)] - static CONFIGS: Lazy, Arc>, &'static io::Error>> = + static CONFIGS: Lazy>, &'static io::Error>> = Lazy::new(|| { let root_store = ROOT_STORE.as_ref()?; - Ok([None, Some("http2".to_string())] - .into_iter() - .map(|p| { - let mut config = ClientConfig::builder() - .with_root_certificates(root_store.clone()) - .with_no_client_auth(); - if let Some("http2") = p.as_deref() { - config - .alpn_protocols - .extend(["h2", "http/1.1"].iter().map(|s| s.as_bytes().to_vec())); - } - (p, Arc::new(config)) - }) - .collect()) + Ok(std::ops::Range { + start: 0, + end: TlsFlags::FLAG_MAX.bits() + 1, + } + .map(|p| { + let http2 = (p & TlsFlags::FLAG_HTTP2.bits()) != 0; + let verify_upstream_tls = (p & TlsFlags::FLAG_verify_upstream_tls.bits()) != 0; + + let mut config = ClientConfig::builder() + .with_root_certificates(root_store.clone()) + .with_no_client_auth(); + if !verify_upstream_tls { + config.dangerous().set_certificate_verifier(Arc::new( + danger::NoCertificateVerification::new(provider::default_provider()), + )); + } + + if http2 { + config + .alpn_protocols + .extend(["h2", "http/1.1"].iter().map(|s| s.as_bytes().to_vec())); + } + (p, Arc::new(config)) + }) + .collect()) }); - let configs: &HashMap, Arc> = CONFIGS.as_ref().map_err(|e| *e)?; + let configs: &HashMap> = CONFIGS.as_ref().map_err(|e| *e)?; + let mut key = 0; + if Some("http2").eq(&app_protocol.as_deref()) { + key |= TlsFlags::FLAG_HTTP2.bits(); + } + if verify_upstream_tls { + key |= TlsFlags::FLAG_verify_upstream_tls.bits(); + } Ok(configs - .get(&app_protocol) - .or_else(|| configs.get(&None)) + .get(&key) + .or_else(|| configs.get(&0)) .unwrap() .clone()) } +bitflags! { + struct TlsFlags: u8 { + const FLAG_HTTP2 = 0b01; + const FLAG_verify_upstream_tls = 0b10; + const FLAG_MAX = Self::FLAG_HTTP2.bits() + | Self::FLAG_verify_upstream_tls.bits(); + } +} + // Establish the connection to forward the tunnel stream to. // Takes the tunnel and connection to make additional decisions on how to wrap // the forwarded connection, i.e. reordering tls termination and proxyproto. // Note: this additional wrapping logic currently unimplemented. async fn connect( tunnel_tls: bool, + verify_upstream_tls: bool, app_protocol: Option, proxy_proto_header: Option, url: &Url, @@ -359,10 +396,12 @@ async fn connect( .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))? .to_owned(); conn = Box::new( - futures_rustls::TlsConnector::from(tls_config(app_protocol).map_err(|e| e.kind())?) - .connect(domain, conn.compat()) - .await? - .compat(), + futures_rustls::TlsConnector::from( + tls_config(app_protocol, verify_upstream_tls).map_err(|e| e.kind())?, + ) + .connect(domain, conn.compat()) + .await? + .compat(), ) } @@ -405,3 +444,77 @@ fn serve_gateway_error( .in_current_span(), ) } + +// https://github.com/rustls/rustls/blob/main/examples/src/bin/tlsclient-mio.rs#L334 +mod danger { + use futures_rustls::rustls; + use rustls::{ + client::danger::HandshakeSignatureValid, + crypto::{ + verify_tls12_signature, + verify_tls13_signature, + CryptoProvider, + }, + DigitallySignedStruct, + }; + + use super::pki_types::{ + CertificateDer, + ServerName, + UnixTime, + }; + + #[derive(Debug)] + pub struct NoCertificateVerification(CryptoProvider); + + impl NoCertificateVerification { + pub fn new(provider: CryptoProvider) -> Self { + Self(provider) + } + } + + impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp: &[u8], + _now: UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls12_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls13_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.0.signature_verification_algorithms.supported_schemes() + } + } +}