ngrok/config/
common.rs

1use std::{
2    collections::HashMap,
3    env,
4    process,
5};
6
7use async_trait::async_trait;
8use once_cell::sync::OnceCell;
9use url::Url;
10
11pub use crate::internals::proto::ProxyProto;
12use crate::{
13    config::policies::Policy,
14    forwarder::Forwarder,
15    internals::proto::{
16        BindExtra,
17        BindOpts,
18        IpRestriction,
19        MutualTls,
20    },
21    session::RpcError,
22    Session,
23    Tunnel,
24};
25
26/// Represents the ingress configuration for an ngrok endpoint.
27///
28/// Bindings determine where and how your endpoint is exposed.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum Binding {
31    /// Publicly accessible endpoint (default for most configurations).
32    Public,
33    /// Internal-only endpoint, not accessible from the public internet.
34    Internal,
35    /// Kubernetes cluster binding for service mesh integration.
36    Kubernetes,
37}
38
39impl Binding {
40    /// Returns the string representation of this binding.
41    pub fn as_str(&self) -> &'static str {
42        match self {
43            Binding::Public => "public",
44            Binding::Internal => "internal",
45            Binding::Kubernetes => "kubernetes",
46        }
47    }
48
49    /// Validates if a string is a recognized binding value.
50    pub(crate) fn validate(s: &str) -> Result<(), String> {
51        match s.to_lowercase().as_str() {
52            "public" | "internal" | "kubernetes" => Ok(()),
53            _ => Err(format!(
54                "Invalid binding value '{}'. Expected 'public', 'internal', or 'kubernetes'",
55                s
56            )),
57        }
58    }
59}
60
61impl From<Binding> for String {
62    fn from(binding: Binding) -> String {
63        binding.as_str().to_string()
64    }
65}
66
67impl std::str::FromStr for Binding {
68    type Err = String;
69
70    fn from_str(s: &str) -> Result<Self, Self::Err> {
71        match s.to_lowercase().as_str() {
72            "public" => Ok(Binding::Public),
73            "internal" => Ok(Binding::Internal),
74            "kubernetes" => Ok(Binding::Kubernetes),
75            _ => Err(format!(
76                "Invalid binding value '{}'. Expected 'public', 'internal', or 'kubernetes'",
77                s
78            )),
79        }
80    }
81}
82
83impl std::fmt::Display for Binding {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        write!(f, "{}", self.as_str())
86    }
87}
88
89pub(crate) fn default_forwards_to() -> &'static str {
90    static FORWARDS_TO: OnceCell<String> = OnceCell::new();
91
92    FORWARDS_TO
93        .get_or_init(|| {
94            let hostname = hostname::get()
95                .unwrap_or("<unknown>".into())
96                .to_string_lossy()
97                .into_owned();
98            let exe = env::current_exe()
99                .unwrap_or("<unknown>".into())
100                .to_string_lossy()
101                .into_owned();
102            let pid = process::id();
103            format!("app://{hostname}/{exe}?pid={pid}")
104        })
105        .as_str()
106}
107
108/// Trait representing things that can be built into an ngrok tunnel.
109#[async_trait]
110pub trait TunnelBuilder: From<Session> {
111    /// The ngrok tunnel type that this builder produces.
112    type Tunnel: Tunnel;
113
114    /// Begin listening for new connections on this tunnel.
115    async fn listen(&self) -> Result<Self::Tunnel, RpcError>;
116}
117
118/// Trait representing things that can be built into an ngrok tunnel and then
119/// forwarded to a provided URL.
120#[async_trait]
121pub trait ForwarderBuilder: TunnelBuilder {
122    /// Start listening for new connections on this tunnel and forward all
123    /// connections to the provided URL.
124    ///
125    /// This will also set the `forwards_to` metadata for the tunnel.
126    async fn listen_and_forward(&self, to_url: Url) -> Result<Forwarder<Self::Tunnel>, RpcError>;
127}
128
129macro_rules! impl_builder {
130    ($(#[$m:meta])* $name:ident, $opts:ty, $tun:ident, $edgepoint:tt) => {
131        $(#[$m])*
132        #[derive(Clone)]
133        pub struct $name {
134            options: $opts,
135            // Note: This is only optional for testing purposes.
136            session: Option<Session>,
137        }
138
139        mod __builder_impl {
140            use $crate::forwarder::Forwarder;
141            use $crate::config::common::ForwarderBuilder;
142            use $crate::config::common::TunnelBuilder;
143            use $crate::session::RpcError;
144            use async_trait::async_trait;
145            use url::Url;
146
147            use super::*;
148
149            impl From<Session> for $name {
150                fn from(session: Session) -> Self {
151                    $name {
152                        options: Default::default(),
153                        session: session.into(),
154                    }
155                }
156            }
157
158            #[async_trait]
159            impl TunnelBuilder for $name {
160                type Tunnel = $tun;
161
162                async fn listen(&self) -> Result<$tun, RpcError> {
163                    Ok($tun {
164                        inner: self
165                            .session
166                            .as_ref()
167                            .unwrap()
168                            .start_tunnel(&self.options)
169                            .await?,
170                    })
171                }
172            }
173
174            #[async_trait]
175            impl ForwarderBuilder for $name {
176                async fn listen_and_forward(&self, to_url: Url) -> Result<Forwarder<$tun>, RpcError> {
177                    let mut cfg = self.clone();
178                    cfg.for_forwarding_to(&to_url).await;
179                    let tunnel = cfg.listen().await?;
180                    let info = tunnel.make_info();
181                    $crate::forwarder::forward(tunnel, info, to_url)
182                }
183            }
184        }
185    };
186}
187
188/// Tunnel configuration trait, implemented by our top-level config objects.
189pub(crate) trait TunnelConfig {
190    /// The "forwards to" metadata.
191    ///
192    /// Only for display/informational purposes.
193    fn forwards_to(&self) -> String;
194    /// The L7 protocol the upstream service expects
195    fn forwards_proto(&self) -> String;
196    /// Whether to disable certificate verification for this tunnel.
197    fn verify_upstream_tls(&self) -> bool;
198    /// Internal-only, extra data sent when binding a tunnel.
199    fn extra(&self) -> BindExtra;
200    /// The protocol for this tunnel.
201    fn proto(&self) -> String;
202    /// The middleware and other configuration options for this tunnel.
203    fn opts(&self) -> Option<BindOpts>;
204    /// The labels for this tunnel.
205    fn labels(&self) -> HashMap<String, String>;
206}
207
208// delegate references
209impl<T> TunnelConfig for &T
210where
211    T: TunnelConfig,
212{
213    fn forwards_to(&self) -> String {
214        (**self).forwards_to()
215    }
216
217    fn forwards_proto(&self) -> String {
218        (**self).forwards_proto()
219    }
220    fn verify_upstream_tls(&self) -> bool {
221        (**self).verify_upstream_tls()
222    }
223    fn extra(&self) -> BindExtra {
224        (**self).extra()
225    }
226    fn proto(&self) -> String {
227        (**self).proto()
228    }
229    fn opts(&self) -> Option<BindOpts> {
230        (**self).opts()
231    }
232    fn labels(&self) -> HashMap<String, String> {
233        (**self).labels()
234    }
235}
236
237/// Restrictions placed on the origin of incoming connections to the edge.
238#[derive(Clone, Default)]
239pub(crate) struct CidrRestrictions {
240    /// Rejects connections that do not match the given CIDRs
241    pub(crate) allowed: Vec<String>,
242    /// Rejects connections that match the given CIDRs and allows all other CIDRs.
243    pub(crate) denied: Vec<String>,
244}
245
246impl CidrRestrictions {
247    pub(crate) fn allow(&mut self, cidr: impl Into<String>) {
248        self.allowed.push(cidr.into());
249    }
250    pub(crate) fn deny(&mut self, cidr: impl Into<String>) {
251        self.denied.push(cidr.into());
252    }
253}
254
255// Common
256#[derive(Default, Clone)]
257pub(crate) struct CommonOpts {
258    // Restrictions placed on the origin of incoming connections to the edge.
259    pub(crate) cidr_restrictions: CidrRestrictions,
260    // The version of PROXY protocol to use with this tunnel, zero if not
261    // using.
262    pub(crate) proxy_proto: ProxyProto,
263    // Tunnel-specific opaque metadata. Viewable via the API.
264    pub(crate) metadata: Option<String>,
265    // Tunnel backend metadata. Viewable via the dashboard and API, but has no
266    // bearing on tunnel behavior.
267    pub(crate) forwards_to: Option<String>,
268    // Tunnel L7 app protocol
269    pub(crate) forwards_proto: Option<String>,
270    // Whether to disable certificate verification for this tunnel.
271    verify_upstream_tls: Option<bool>,
272    // DEPRECATED: use traffic_policy instead.
273    pub(crate) policy: Option<Policy>,
274    // Policy that defines rules that should be applied to incoming or outgoing
275    // connections to the edge.
276    pub(crate) traffic_policy: Option<String>,
277    // Allows the endpoint to pool with other endpoints with the same host/port/binding
278    pub(crate) pooling_enabled: Option<bool>,
279}
280
281impl CommonOpts {
282    // Get the proto version of cidr restrictions
283    pub(crate) fn ip_restriction(&self) -> Option<IpRestriction> {
284        (!self.cidr_restrictions.allowed.is_empty() || !self.cidr_restrictions.denied.is_empty())
285            .then_some(self.cidr_restrictions.clone().into())
286    }
287
288    pub(crate) fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self {
289        self.forwards_to = Some(to_url.as_str().into());
290        self
291    }
292
293    pub(crate) fn set_verify_upstream_tls(&mut self, verify_upstream_tls: bool) {
294        self.verify_upstream_tls = Some(verify_upstream_tls)
295    }
296
297    pub(crate) fn verify_upstream_tls(&self) -> bool {
298        self.verify_upstream_tls.unwrap_or(true)
299    }
300}
301
302// transform into the wire protocol format
303impl From<CidrRestrictions> for IpRestriction {
304    fn from(cr: CidrRestrictions) -> Self {
305        IpRestriction {
306            allow_cidrs: cr.allowed,
307            deny_cidrs: cr.denied,
308        }
309    }
310}
311
312impl From<&[bytes::Bytes]> for MutualTls {
313    fn from(b: &[bytes::Bytes]) -> Self {
314        let mut aggregated = Vec::new();
315        b.iter().for_each(|c| aggregated.extend(c));
316        MutualTls {
317            mutual_tls_ca: aggregated,
318        }
319    }
320}