ngrok/config/
tcp.rs

1use std::{
2    collections::HashMap,
3    convert::From,
4};
5
6use url::Url;
7
8use super::{
9    common::ProxyProto,
10    Policy,
11};
12// These are used for doc comment links.
13#[allow(unused_imports)]
14use crate::config::{
15    ForwarderBuilder,
16    TunnelBuilder,
17};
18use crate::{
19    config::common::{
20        default_forwards_to,
21        Binding,
22        CommonOpts,
23        TunnelConfig,
24    },
25    internals::proto::{
26        self,
27        BindExtra,
28        BindOpts,
29    },
30    tunnel::TcpTunnel,
31    Session,
32};
33
34/// The options for a TCP edge.
35#[derive(Default, Clone)]
36struct TcpOptions {
37    pub(crate) common_opts: CommonOpts,
38    pub(crate) remote_addr: Option<String>,
39    pub(crate) bindings: Vec<String>,
40}
41
42impl TunnelConfig for TcpOptions {
43    fn forwards_to(&self) -> String {
44        self.common_opts
45            .forwards_to
46            .clone()
47            .unwrap_or(default_forwards_to().into())
48    }
49    fn extra(&self) -> BindExtra {
50        BindExtra {
51            token: Default::default(),
52            ip_policy_ref: Default::default(),
53            metadata: self.common_opts.metadata.clone().unwrap_or_default(),
54            bindings: self.bindings.clone(),
55            pooling_enabled: self.common_opts.pooling_enabled.unwrap_or(false),
56        }
57    }
58    fn proto(&self) -> String {
59        "tcp".into()
60    }
61
62    fn forwards_proto(&self) -> String {
63        // not supported
64        String::new()
65    }
66
67    fn verify_upstream_tls(&self) -> bool {
68        self.common_opts.verify_upstream_tls()
69    }
70
71    fn opts(&self) -> Option<BindOpts> {
72        // fill out all the options, translating to proto here
73        let mut tcp_endpoint = proto::TcpEndpoint::default();
74
75        if let Some(remote_addr) = self.remote_addr.as_ref() {
76            tcp_endpoint.addr = remote_addr.clone();
77        }
78        tcp_endpoint.proxy_proto = self.common_opts.proxy_proto;
79
80        tcp_endpoint.ip_restriction = self.common_opts.ip_restriction();
81
82        tcp_endpoint.traffic_policy = if self.common_opts.traffic_policy.is_some() {
83            self.common_opts.traffic_policy.clone().map(From::from)
84        } else if self.common_opts.policy.is_some() {
85            self.common_opts.policy.clone().map(From::from)
86        } else {
87            None
88        };
89        Some(BindOpts::Tcp(tcp_endpoint))
90    }
91    fn labels(&self) -> HashMap<String, String> {
92        HashMap::new()
93    }
94}
95
96impl_builder! {
97    /// A builder for a tunnel backing a TCP endpoint.
98    ///
99    /// https://ngrok.com/docs/tcp/
100    TcpTunnelBuilder, TcpOptions, TcpTunnel, endpoint
101}
102
103/// The options for a TCP edge.
104impl TcpTunnelBuilder {
105    /// Add the provided CIDR to the allowlist.
106    ///
107    /// https://ngrok.com/docs/tcp/ip-restrictions/
108    pub fn allow_cidr(&mut self, cidr: impl Into<String>) -> &mut Self {
109        self.options.common_opts.cidr_restrictions.allow(cidr);
110        self
111    }
112    /// Add the provided CIDR to the denylist.
113    ///
114    /// https://ngrok.com/docs/tcp/ip-restrictions/
115    pub fn deny_cidr(&mut self, cidr: impl Into<String>) -> &mut Self {
116        self.options.common_opts.cidr_restrictions.deny(cidr);
117        self
118    }
119    /// Sets the PROXY protocol version for connections over this tunnel.
120    pub fn proxy_proto(&mut self, proxy_proto: ProxyProto) -> &mut Self {
121        self.options.common_opts.proxy_proto = proxy_proto;
122        self
123    }
124    /// Sets the opaque metadata string for this tunnel.
125    ///
126    /// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
127    pub fn metadata(&mut self, metadata: impl Into<String>) -> &mut Self {
128        self.options.common_opts.metadata = Some(metadata.into());
129        self
130    }
131
132    /// Sets the ingress configuration for this endpoint.
133    ///
134    /// Valid binding values are:
135    /// - `"public"` - Publicly accessible endpoint
136    /// - `"internal"` - Internal-only endpoint
137    /// - `"kubernetes"` - Kubernetes cluster binding
138    ///
139    /// If not specified, the ngrok service will use its default binding configuration.
140    ///
141    /// # Panics
142    ///
143    /// Panics if called more than once or if an invalid binding value is provided.
144    ///
145    /// # Examples
146    ///
147    /// ```no_run
148    /// # use ngrok::Session;
149    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
150    /// let session = Session::builder().authtoken_from_env().connect().await?;
151    ///
152    /// // Using string
153    /// let tunnel = session.tcp_endpoint().binding("internal").listen().await?;
154    ///
155    /// // Using typed enum
156    /// use ngrok::config::Binding;
157    /// let tunnel = session.tcp_endpoint().binding(Binding::Public).listen().await?;
158    /// # Ok(())
159    /// # }
160    /// ```
161    pub fn binding(&mut self, binding: impl Into<String>) -> &mut Self {
162        if !self.options.bindings.is_empty() {
163            panic!("binding() can only be called once");
164        }
165        let binding_str = binding.into();
166        if let Err(e) = Binding::validate(&binding_str) {
167            panic!("{}", e);
168        }
169        self.options.bindings.push(binding_str);
170        self
171    }
172    /// Sets the ForwardsTo string for this tunnel. This can be viewed via the
173    /// API or dashboard.
174    ///
175    /// This overrides the default process info if using
176    /// [TunnelBuilder::listen], and is in turn overridden by the url provided
177    /// to [ForwarderBuilder::listen_and_forward].
178    ///
179    /// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
180    pub fn forwards_to(&mut self, forwards_to: impl Into<String>) -> &mut Self {
181        self.options.common_opts.forwards_to = Some(forwards_to.into());
182        self
183    }
184
185    /// Disables backend TLS certificate verification for forwards from this tunnel.
186    pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut Self {
187        self.options
188            .common_opts
189            .set_verify_upstream_tls(verify_upstream_tls);
190        self
191    }
192
193    /// Sets the TCP address to request for this edge.
194    ///
195    /// https://ngrok.com/docs/network-edge/domains-and-tcp-addresses/#tcp-addresses
196    pub fn remote_addr(&mut self, remote_addr: impl Into<String>) -> &mut Self {
197        self.options.remote_addr = Some(remote_addr.into());
198        self
199    }
200
201    /// DEPRECATED: use traffic_policy instead.
202    pub fn policy<S>(&mut self, s: S) -> Result<&mut Self, S::Error>
203    where
204        S: TryInto<Policy>,
205    {
206        self.options.common_opts.policy = Some(s.try_into()?);
207        Ok(self)
208    }
209
210    /// Set policy for this edge.
211    pub fn traffic_policy(&mut self, policy_str: impl Into<String>) -> &mut Self {
212        self.options.common_opts.traffic_policy = Some(policy_str.into());
213        self
214    }
215
216    pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self {
217        self.options.common_opts.for_forwarding_to(to_url);
218        self
219    }
220
221    /// Allows the endpoint to pool with other endpoints with the same host/port/binding
222    pub fn pooling_enabled(&mut self, pooling_enabled: impl Into<bool>) -> &mut Self {
223        self.options.common_opts.pooling_enabled = Some(pooling_enabled.into());
224        self
225    }
226}
227
228#[cfg(test)]
229mod test {
230    use super::*;
231    use crate::config::policies::test::POLICY_JSON;
232    const METADATA: &str = "testmeta";
233    const TEST_FORWARD: &str = "testforward";
234    const REMOTE_ADDR: &str = "4.tcp.ngrok.io:1337";
235    const ALLOW_CIDR: &str = "0.0.0.0/0";
236    const DENY_CIDR: &str = "10.1.1.1/32";
237
238    #[test]
239    fn test_interface_to_proto() {
240        // pass to a function accepting the trait to avoid
241        // "creates a temporary which is freed while still in use"
242        tunnel_test(
243            &TcpTunnelBuilder {
244                session: None,
245                options: Default::default(),
246            }
247            .allow_cidr(ALLOW_CIDR)
248            .deny_cidr(DENY_CIDR)
249            .proxy_proto(ProxyProto::V2)
250            .metadata(METADATA)
251            .remote_addr(REMOTE_ADDR)
252            .forwards_to(TEST_FORWARD)
253            .policy(POLICY_JSON)
254            .unwrap()
255            .options,
256        );
257    }
258
259    fn tunnel_test<C>(tunnel_cfg: &C)
260    where
261        C: TunnelConfig,
262    {
263        assert_eq!(TEST_FORWARD, tunnel_cfg.forwards_to());
264
265        let extra = tunnel_cfg.extra();
266        assert_eq!(String::default(), *extra.token);
267        assert_eq!(METADATA, extra.metadata);
268        assert_eq!(Vec::<String>::new(), extra.bindings);
269        assert_eq!(String::default(), extra.ip_policy_ref);
270
271        assert_eq!("tcp", tunnel_cfg.proto());
272
273        let opts = tunnel_cfg.opts().unwrap();
274        assert!(matches!(opts, BindOpts::Tcp { .. }));
275        if let BindOpts::Tcp(endpoint) = opts {
276            assert_eq!(REMOTE_ADDR, endpoint.addr);
277            assert!(matches!(endpoint.proxy_proto, ProxyProto::V2));
278
279            let ip_restriction = endpoint.ip_restriction.unwrap();
280            assert_eq!(Vec::from([ALLOW_CIDR]), ip_restriction.allow_cidrs);
281            assert_eq!(Vec::from([DENY_CIDR]), ip_restriction.deny_cidrs);
282        }
283
284        assert_eq!(HashMap::new(), tunnel_cfg.labels());
285    }
286
287    #[test]
288    fn test_binding_valid_values() {
289        let mut builder = TcpTunnelBuilder {
290            session: None,
291            options: Default::default(),
292        };
293
294        // Test "public"
295        builder.binding("public");
296        assert_eq!(vec!["public"], builder.options.bindings);
297
298        // Test "internal"
299        let mut builder = TcpTunnelBuilder {
300            session: None,
301            options: Default::default(),
302        };
303        builder.binding("internal");
304        assert_eq!(vec!["internal"], builder.options.bindings);
305
306        // Test "kubernetes"
307        let mut builder = TcpTunnelBuilder {
308            session: None,
309            options: Default::default(),
310        };
311        builder.binding("kubernetes");
312        assert_eq!(vec!["kubernetes"], builder.options.bindings);
313
314        // Test with Binding enum
315        let mut builder = TcpTunnelBuilder {
316            session: None,
317            options: Default::default(),
318        };
319        builder.binding(Binding::Public);
320        assert_eq!(vec!["public"], builder.options.bindings);
321    }
322
323    #[test]
324    #[should_panic(expected = "Invalid binding value")]
325    fn test_binding_invalid_value() {
326        let mut builder = TcpTunnelBuilder {
327            session: None,
328            options: Default::default(),
329        };
330        builder.binding("invalid");
331    }
332
333    #[test]
334    #[should_panic(expected = "binding() can only be called once")]
335    fn test_binding_called_twice() {
336        let mut builder = TcpTunnelBuilder {
337            session: None,
338            options: Default::default(),
339        };
340        builder.binding("public");
341        builder.binding("internal");
342    }
343}