Skip to main content

ngrok/
tunnel_ext.rs

1use std::{
2    collections::HashMap,
3    io,
4    sync::Arc,
5};
6#[cfg(feature = "hyper")]
7use std::{
8    convert::Infallible,
9    fmt,
10};
11
12use async_trait::async_trait;
13use bitflags::bitflags;
14use futures::stream::TryStreamExt;
15use futures_rustls::rustls::{
16    self,
17    ClientConfig,
18    pki_types,
19};
20#[cfg(feature = "hyper")]
21use hyper::{
22    Response,
23    StatusCode,
24    server::conn::http1,
25    service::service_fn,
26};
27use once_cell::sync::Lazy;
28use proxy_protocol::ProxyHeader;
29use tokio::{
30    io::copy_bidirectional,
31    net::TcpStream,
32    task::JoinHandle,
33};
34use tokio_util::compat::{
35    FuturesAsyncReadCompatExt,
36    TokioAsyncReadCompatExt,
37};
38#[cfg(feature = "hyper")]
39use tracing::debug;
40use tracing::{
41    Instrument,
42    Span,
43    field,
44    warn,
45};
46use url::Url;
47
48use crate::{
49    EdgeConn,
50    EndpointConn,
51    prelude::*,
52    proxy_proto,
53    session::IoStream,
54};
55
56#[allow(deprecated)]
57#[async_trait]
58impl<T> TunnelExt for T
59where
60    T: Tunnel + Send,
61    <T as Tunnel>::Conn: ConnExt,
62{
63    async fn forward(&mut self, url: Url) -> Result<(), io::Error> {
64        forward_tunnel(self, url).await
65    }
66}
67
68/// Extension methods auto-implemented for all tunnel types
69#[async_trait]
70#[deprecated = "superceded by the `listen_and_forward` builder method"]
71pub trait TunnelExt: Tunnel + Send {
72    /// Forward incoming tunnel connections to the provided url based on its
73    /// scheme.
74    /// This currently supports http, https, tls, and tcp on all platforms, unix
75    /// sockets on unix platforms, and named pipes on Windows via the "pipe"
76    /// scheme.
77    ///
78    /// Unix socket URLs can be formatted as `unix://path/to/socket` or
79    /// `unix:path/to/socket` for relative paths or as `unix:///path/to/socket` or
80    /// `unix:/path/to/socket` for absolute paths.
81    ///
82    /// Windows named pipe URLs can be formatted as `pipe:mypipename` or
83    /// `pipe://host/mypipename`. If no host is provided, as with
84    /// `pipe:///mypipename` or `pipe:/mypipename`, the leading slash will be
85    /// preserved.
86    async fn forward(&mut self, url: Url) -> Result<(), io::Error>;
87}
88
89pub(crate) trait ConnExt {
90    fn forward_to(self, url: &Url) -> JoinHandle<io::Result<()>>;
91}
92
93#[tracing::instrument(skip_all, fields(tunnel_id = tun.id(), url = %url))]
94pub(crate) async fn forward_tunnel<T>(tun: &mut T, url: Url) -> Result<(), io::Error>
95where
96    T: Tunnel + 'static + ?Sized,
97    <T as Tunnel>::Conn: ConnExt,
98{
99    loop {
100        let tunnel_conn = match tun
101            .try_next()
102            .await
103            .map_err(|err| io::Error::new(io::ErrorKind::NotConnected, err))?
104        {
105            Some(conn) => conn,
106            _ => {
107                return Ok(());
108            }
109        };
110
111        tunnel_conn.forward_to(&url);
112    }
113}
114
115impl ConnExt for EdgeConn {
116    fn forward_to(mut self, url: &Url) -> JoinHandle<io::Result<()>> {
117        let url = url.clone();
118        tokio::spawn(async move {
119            let mut upstream = match connect(
120                self.edge_type() == EdgeType::Tls && self.passthrough_tls(),
121                self.inner.info.verify_upstream_tls,
122                self.inner.info.app_protocol.clone(),
123                None, // Edges don't support proxyproto (afaik)
124                &url,
125            )
126            .await
127            {
128                Ok(conn) => conn,
129                Err(error) => {
130                    #[cfg(feature = "hyper")]
131                    if self.edge_type() == EdgeType::Https {
132                        serve_gateway_error(format!("{error}"), self);
133                    }
134                    warn!(%error, "error connecting to upstream");
135                    return Err(error);
136                }
137            };
138
139            copy_bidirectional(&mut self, &mut upstream).await?;
140            Ok(())
141        })
142    }
143}
144
145impl ConnExt for EndpointConn {
146    fn forward_to(self, url: &Url) -> JoinHandle<Result<(), io::Error>> {
147        let url = url.clone();
148        tokio::spawn(async move {
149            let proxy_proto = self.inner.info.proxy_proto;
150            let proto_tls = self.proto() == "tls";
151            #[cfg(feature = "hyper")]
152            let proto_http = matches!(self.proto(), "http" | "https");
153            let passthrough_tls = self.inner.info.passthrough_tls();
154            let app_protocol = self.inner.info.app_protocol.clone();
155            let verify_upstream_tls = self.inner.info.verify_upstream_tls;
156
157            let (mut stream, proxy_header) = match proxy_proto {
158                ProxyProto::None => (crate::proxy_proto::Stream::disabled(self), None),
159                _ => {
160                    let mut stream = crate::proxy_proto::Stream::incoming(self);
161                    let header = stream
162                        .proxy_header()
163                        .await?
164                        .map_err(|e| {
165                            io::Error::new(
166                                io::ErrorKind::InvalidData,
167                                format!("invalid proxy-protocol header: {}", e),
168                            )
169                        })?
170                        .cloned();
171                    (stream, header)
172                }
173            };
174
175            let mut upstream = match connect(
176                proto_tls && passthrough_tls,
177                verify_upstream_tls,
178                app_protocol,
179                proxy_header,
180                &url,
181            )
182            .await
183            {
184                Ok(conn) => conn,
185                Err(error) => {
186                    #[cfg(feature = "hyper")]
187                    if proto_http {
188                        serve_gateway_error(format!("{error}"), stream);
189                    }
190                    warn!(%error, "error connecting to upstream");
191                    return Err(error);
192                }
193            };
194
195            copy_bidirectional(&mut stream, &mut upstream).await?;
196            Ok(())
197        })
198    }
199}
200
201bitflags! {
202    struct TlsFlags: u8 {
203        const FLAG_HTTP2       = 0b01;
204        const FLAG_verify_upstream_tls       = 0b10;
205        const FLAG_MAX     = Self::FLAG_HTTP2.bits()
206                           | Self::FLAG_verify_upstream_tls.bits();
207    }
208}
209
210static NO_CRYPTO_PROVIDER_ERROR: Lazy<io::Error> = Lazy::new(|| {
211    io::Error::new(
212        io::ErrorKind::NotFound,
213        "no default CryptoProvider installed",
214    )
215});
216
217fn tls_config(
218    app_protocol: Option<String>,
219    verify_upstream_tls: bool,
220) -> Result<Arc<ClientConfig>, &'static io::Error> {
221    // A hashmap of tls client configs for different configurations.
222    // There won't need to be a lot of variation among these, and we'll want to
223    // reuse them as much as we can, which is why we initialize them all once
224    // and then pull out the one we need.
225    // Disabling the lint because this is a local static that doesn't escape the
226    // enclosing context. It fine.
227    #[allow(clippy::type_complexity)]
228    static CONFIGS: Lazy<Result<HashMap<u8, Arc<ClientConfig>>, &'static io::Error>> =
229        Lazy::new(|| {
230            std::ops::Range {
231                start: 0,
232                end: TlsFlags::FLAG_MAX.bits() + 1,
233            }
234            .map(|p| {
235                let http2 = (p & TlsFlags::FLAG_HTTP2.bits()) != 0;
236                let verify_upstream_tls = (p & TlsFlags::FLAG_verify_upstream_tls.bits()) != 0;
237                let mut config = crate::session::host_certs_tls_config()?;
238                if !verify_upstream_tls {
239                    let provider = rustls::crypto::CryptoProvider::get_default()
240                        .ok_or(&*NO_CRYPTO_PROVIDER_ERROR)?
241                        .as_ref()
242                        .clone();
243                    config.dangerous().set_certificate_verifier(Arc::new(
244                        danger::NoCertificateVerification::new(provider),
245                    ));
246                }
247
248                if http2 {
249                    config
250                        .alpn_protocols
251                        .extend(["h2", "http/1.1"].iter().map(|s| s.as_bytes().to_vec()));
252                }
253                Ok((p, Arc::new(config)))
254            })
255            .collect()
256        });
257
258    let configs: &HashMap<u8, Arc<ClientConfig>> = CONFIGS.as_ref().map_err(|e| *e)?;
259    let mut key = 0;
260    if Some("http2").eq(&app_protocol.as_deref()) {
261        key |= TlsFlags::FLAG_HTTP2.bits();
262    }
263    if verify_upstream_tls {
264        key |= TlsFlags::FLAG_verify_upstream_tls.bits();
265    }
266
267    Ok(configs
268        .get(&key)
269        .or_else(|| configs.get(&0))
270        .unwrap()
271        .clone())
272}
273
274// Establish the connection to forward the tunnel stream to.
275// Takes the tunnel and connection to make additional decisions on how to wrap
276// the forwarded connection, i.e. reordering tls termination and proxyproto.
277// Note: this additional wrapping logic currently unimplemented.
278async fn connect(
279    tunnel_tls: bool,
280    verify_upstream_tls: bool,
281    app_protocol: Option<String>,
282    proxy_proto_header: Option<ProxyHeader>,
283    url: &Url,
284) -> Result<Box<dyn IoStream>, io::Error> {
285    let host = url.host_str().unwrap_or("localhost");
286    let mut backend_tls: bool = false;
287    let mut conn: Box<dyn IoStream> = match url.scheme() {
288        "tcp" => {
289            let port = url.port().ok_or_else(|| {
290                io::Error::new(
291                    io::ErrorKind::InvalidInput,
292                    format!("missing port for tcp forwarding url {url}"),
293                )
294            })?;
295            let conn = connect_tcp(host, port).in_current_span().await?;
296            Box::new(conn)
297        }
298
299        "http" => {
300            let port = url.port().unwrap_or(80);
301            let conn = connect_tcp(host, port).in_current_span().await?;
302            Box::new(conn)
303        }
304
305        "https" | "tls" => {
306            let port = url.port().unwrap_or(443);
307            let conn = connect_tcp(host, port).in_current_span().await?;
308
309            backend_tls = true;
310            Box::new(conn)
311        }
312
313        #[cfg(not(target_os = "windows"))]
314        "unix" => {
315            use std::borrow::Cow;
316
317            use tokio::net::UnixStream;
318
319            let mut addr = Cow::Borrowed(url.path());
320            if let Some(host) = url.host_str() {
321                // note: if host exists, there should always be a leading / in
322                // the path, but we should consider it a relative path.
323                addr = Cow::Owned(format!("{host}{addr}"));
324            }
325            Box::new(UnixStream::connect(&*addr).await?)
326        }
327
328        #[cfg(target_os = "windows")]
329        "pipe" => {
330            use std::time::Duration;
331
332            use tokio::net::windows::named_pipe::ClientOptions;
333            use windows_sys::Win32::Foundation::ERROR_PIPE_BUSY;
334
335            let mut pipe_name = url.path();
336            if url.host_str().is_some() {
337                pipe_name = pipe_name.strip_prefix('/').unwrap_or(pipe_name);
338            }
339            if pipe_name.is_empty() {
340                return Err(io::Error::new(
341                    io::ErrorKind::InvalidInput,
342                    format!("missing pipe name in forwarding url {url}"),
343                ));
344            }
345            let host = url
346                .host_str()
347                // Consider localhost to mean "." for the pipe name
348                .map(|h| if h == "localhost" { "." } else { h })
349                .unwrap_or(".");
350            // Finally, assemble the full name.
351            let addr = format!("\\\\{host}\\pipe\\{pipe_name}");
352            // loop behavior copied from docs
353            // https://docs.rs/tokio/latest/tokio/net/windows/named_pipe/struct.NamedPipeClient.html
354            let local_conn = loop {
355                match ClientOptions::new().open(&addr) {
356                    Ok(client) => break client,
357                    Err(error) if error.raw_os_error() == Some(ERROR_PIPE_BUSY as i32) => (),
358                    Err(error) => return Err(error),
359                }
360
361                tokio::time::sleep(Duration::from_millis(50)).await;
362            };
363            Box::new(local_conn)
364        }
365        _ => {
366            return Err(io::Error::new(
367                io::ErrorKind::InvalidInput,
368                format!("unrecognized scheme in forwarding url: {url}"),
369            ));
370        }
371    };
372
373    // We have to write the proxy header _before_ tls termination
374    if let Some(header) = proxy_proto_header {
375        conn = Box::new(
376            proxy_proto::Stream::outgoing(conn, header)
377                .expect("re-serializing proxy header should always succeed"),
378        )
379    };
380
381    if backend_tls && !tunnel_tls {
382        let domain = pki_types::ServerName::try_from(host)
383            .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?
384            .to_owned();
385        conn = Box::new(
386            futures_rustls::TlsConnector::from(
387                tls_config(app_protocol, verify_upstream_tls).map_err(|e| e.kind())?,
388            )
389            .connect(domain, conn.compat())
390            .await?
391            .compat(),
392        )
393    }
394
395    // TODO: header rewrites?
396
397    Ok(conn)
398}
399
400async fn connect_tcp(host: &str, port: u16) -> Result<TcpStream, io::Error> {
401    let conn = TcpStream::connect(&format!("{}:{}", host, port)).await?;
402    if let Ok(addr) = conn.peer_addr() {
403        Span::current().record("forward_addr", field::display(addr));
404    }
405    Ok(conn)
406}
407
408#[cfg(feature = "hyper")]
409fn serve_gateway_error(
410    err: impl fmt::Display + Send + 'static,
411    conn: impl hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static,
412) -> JoinHandle<()> {
413    tokio::spawn(
414        async move {
415            let service = service_fn(move |_req| {
416                debug!("serving bad gateway error");
417                let mut resp = Response::new(format!("failed to dial backend: {err}"));
418                *resp.status_mut() = StatusCode::BAD_GATEWAY;
419                futures::future::ok::<_, Infallible>(resp)
420            });
421
422            let res = http1::Builder::new()
423                .keep_alive(false)
424                .serve_connection(conn, service)
425                .await;
426            debug!(?res, "connection closed");
427        }
428        .in_current_span(),
429    )
430}
431
432// https://github.com/rustls/rustls/blob/main/examples/src/bin/tlsclient-mio.rs#L334
433mod danger {
434    use futures_rustls::rustls;
435    use rustls::{
436        DigitallySignedStruct,
437        client::danger::HandshakeSignatureValid,
438        crypto::{
439            CryptoProvider,
440            verify_tls12_signature,
441            verify_tls13_signature,
442        },
443    };
444
445    use super::pki_types::{
446        CertificateDer,
447        ServerName,
448        UnixTime,
449    };
450
451    #[derive(Debug)]
452    pub struct NoCertificateVerification(CryptoProvider);
453
454    impl NoCertificateVerification {
455        pub fn new(provider: CryptoProvider) -> Self {
456            Self(provider)
457        }
458    }
459
460    impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
461        fn verify_server_cert(
462            &self,
463            _end_entity: &CertificateDer<'_>,
464            _intermediates: &[CertificateDer<'_>],
465            _server_name: &ServerName<'_>,
466            _ocsp: &[u8],
467            _now: UnixTime,
468        ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
469            Ok(rustls::client::danger::ServerCertVerified::assertion())
470        }
471
472        fn verify_tls12_signature(
473            &self,
474            message: &[u8],
475            cert: &CertificateDer<'_>,
476            dss: &DigitallySignedStruct,
477        ) -> Result<HandshakeSignatureValid, rustls::Error> {
478            verify_tls12_signature(
479                message,
480                cert,
481                dss,
482                &self.0.signature_verification_algorithms,
483            )
484        }
485
486        fn verify_tls13_signature(
487            &self,
488            message: &[u8],
489            cert: &CertificateDer<'_>,
490            dss: &DigitallySignedStruct,
491        ) -> Result<HandshakeSignatureValid, rustls::Error> {
492            verify_tls13_signature(
493                message,
494                cert,
495                dss,
496                &self.0.signature_verification_algorithms,
497            )
498        }
499
500        fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
501            self.0.signature_verification_algorithms.supported_schemes()
502        }
503    }
504}