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    /// # use ngrok::config::TunnelBuilder;
150    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
151    /// let session = Session::builder().authtoken_from_env().connect().await?;
152    ///
153    /// // Using string
154    /// let tunnel = session.tcp_endpoint().binding("internal").listen().await?;
155    ///
156    /// // Using typed enum
157    /// use ngrok::config::Binding;
158    /// let tunnel = session.tcp_endpoint().binding(Binding::Public).listen().await?;
159    /// # Ok(())
160    /// # }
161    /// ```
162    pub fn binding(&mut self, binding: impl Into<String>) -> &mut Self {
163        if !self.options.bindings.is_empty() {
164            panic!("binding() can only be called once");
165        }
166        let binding_str = binding.into();
167        if let Err(e) = Binding::validate(&binding_str) {
168            panic!("{}", e);
169        }
170        self.options.bindings.push(binding_str);
171        self
172    }
173    /// Sets the ForwardsTo string for this tunnel. This can be viewed via the
174    /// API or dashboard.
175    ///
176    /// This overrides the default process info if using
177    /// [TunnelBuilder::listen], and is in turn overridden by the url provided
178    /// to [ForwarderBuilder::listen_and_forward].
179    ///
180    /// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
181    pub fn forwards_to(&mut self, forwards_to: impl Into<String>) -> &mut Self {
182        self.options.common_opts.forwards_to = Some(forwards_to.into());
183        self
184    }
185
186    /// Disables backend TLS certificate verification for forwards from this tunnel.
187    pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut Self {
188        self.options
189            .common_opts
190            .set_verify_upstream_tls(verify_upstream_tls);
191        self
192    }
193
194    /// Sets the TCP address to request for this edge.
195    ///
196    /// https://ngrok.com/docs/network-edge/domains-and-tcp-addresses/#tcp-addresses
197    pub fn remote_addr(&mut self, remote_addr: impl Into<String>) -> &mut Self {
198        self.options.remote_addr = Some(remote_addr.into());
199        self
200    }
201
202    /// DEPRECATED: use traffic_policy instead.
203    pub fn policy<S>(&mut self, s: S) -> Result<&mut Self, S::Error>
204    where
205        S: TryInto<Policy>,
206    {
207        self.options.common_opts.policy = Some(s.try_into()?);
208        Ok(self)
209    }
210
211    /// Set policy for this edge.
212    pub fn traffic_policy(&mut self, policy_str: impl Into<String>) -> &mut Self {
213        self.options.common_opts.traffic_policy = Some(policy_str.into());
214        self
215    }
216
217    pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self {
218        self.options.common_opts.for_forwarding_to(to_url);
219        self
220    }
221
222    /// Allows the endpoint to pool with other endpoints with the same host/port/binding
223    pub fn pooling_enabled(&mut self, pooling_enabled: impl Into<bool>) -> &mut Self {
224        self.options.common_opts.pooling_enabled = Some(pooling_enabled.into());
225        self
226    }
227}
228
229#[cfg(test)]
230mod test {
231    use super::*;
232    use crate::config::policies::test::POLICY_JSON;
233    const METADATA: &str = "testmeta";
234    const TEST_FORWARD: &str = "testforward";
235    const REMOTE_ADDR: &str = "4.tcp.ngrok.io:1337";
236    const ALLOW_CIDR: &str = "0.0.0.0/0";
237    const DENY_CIDR: &str = "10.1.1.1/32";
238
239    #[test]
240    fn test_interface_to_proto() {
241        // pass to a function accepting the trait to avoid
242        // "creates a temporary which is freed while still in use"
243        tunnel_test(
244            &TcpTunnelBuilder {
245                session: None,
246                options: Default::default(),
247            }
248            .allow_cidr(ALLOW_CIDR)
249            .deny_cidr(DENY_CIDR)
250            .proxy_proto(ProxyProto::V2)
251            .metadata(METADATA)
252            .remote_addr(REMOTE_ADDR)
253            .forwards_to(TEST_FORWARD)
254            .policy(POLICY_JSON)
255            .unwrap()
256            .options,
257        );
258    }
259
260    fn tunnel_test<C>(tunnel_cfg: &C)
261    where
262        C: TunnelConfig,
263    {
264        assert_eq!(TEST_FORWARD, tunnel_cfg.forwards_to());
265
266        let extra = tunnel_cfg.extra();
267        assert_eq!(String::default(), *extra.token);
268        assert_eq!(METADATA, extra.metadata);
269        assert_eq!(Vec::<String>::new(), extra.bindings);
270        assert_eq!(String::default(), extra.ip_policy_ref);
271
272        assert_eq!("tcp", tunnel_cfg.proto());
273
274        let opts = tunnel_cfg.opts().unwrap();
275        assert!(matches!(opts, BindOpts::Tcp { .. }));
276        if let BindOpts::Tcp(endpoint) = opts {
277            assert_eq!(REMOTE_ADDR, endpoint.addr);
278            assert!(matches!(endpoint.proxy_proto, ProxyProto::V2));
279
280            let ip_restriction = endpoint.ip_restriction.unwrap();
281            assert_eq!(Vec::from([ALLOW_CIDR]), ip_restriction.allow_cidrs);
282            assert_eq!(Vec::from([DENY_CIDR]), ip_restriction.deny_cidrs);
283        }
284
285        assert_eq!(HashMap::new(), tunnel_cfg.labels());
286    }
287
288    #[test]
289    fn test_binding_valid_values() {
290        let mut builder = TcpTunnelBuilder {
291            session: None,
292            options: Default::default(),
293        };
294
295        // Test "public"
296        builder.binding("public");
297        assert_eq!(vec!["public"], builder.options.bindings);
298
299        // Test "internal"
300        let mut builder = TcpTunnelBuilder {
301            session: None,
302            options: Default::default(),
303        };
304        builder.binding("internal");
305        assert_eq!(vec!["internal"], builder.options.bindings);
306
307        // Test "kubernetes"
308        let mut builder = TcpTunnelBuilder {
309            session: None,
310            options: Default::default(),
311        };
312        builder.binding("kubernetes");
313        assert_eq!(vec!["kubernetes"], builder.options.bindings);
314
315        // Test with Binding enum
316        let mut builder = TcpTunnelBuilder {
317            session: None,
318            options: Default::default(),
319        };
320        builder.binding(Binding::Public);
321        assert_eq!(vec!["public"], builder.options.bindings);
322    }
323
324    #[test]
325    #[should_panic(expected = "Invalid binding value")]
326    fn test_binding_invalid_value() {
327        let mut builder = TcpTunnelBuilder {
328            session: None,
329            options: Default::default(),
330        };
331        builder.binding("invalid");
332    }
333
334    #[test]
335    #[should_panic(expected = "binding() can only be called once")]
336    fn test_binding_called_twice() {
337        let mut builder = TcpTunnelBuilder {
338            session: None,
339            options: Default::default(),
340        };
341        builder.binding("public");
342        builder.binding("internal");
343    }
344}