1use std::net::{Ipv4Addr, Ipv6Addr};
7use std::sync::Arc;
8use std::time::Duration;
9
10use tokio::io::{AsyncReadExt, AsyncWriteExt};
11use tokio::net::{TcpListener, TcpStream};
12use tokio::sync::mpsc;
13use tokio_stream::wrappers::ReceiverStream;
14
15use crate::error::SailError;
16use crate::pb::workerproxy::v1 as pb;
17use crate::worker::WorkerProxy;
18
19const TUNNEL_CHANNEL_CAP: usize = 32;
22const TUNNEL_READ_CHUNK: usize = 32 * 1024;
24
25#[must_use = "dropping a PortForward stops accepting new connections"]
28pub struct PortForward {
29 local_port: u16,
30 accept: tokio::task::JoinHandle<()>,
31}
32
33impl Drop for PortForward {
34 fn drop(&mut self) {
35 self.accept.abort();
36 }
37}
38
39impl PortForward {
40 pub fn local_port(&self) -> u16 {
44 self.local_port
45 }
46}
47
48pub async fn forward_port(
53 worker: Arc<WorkerProxy>,
54 endpoint: String,
55 sailbox_id: String,
56 local_port: u16,
57 remote_port: u16,
58) -> Result<PortForward, SailError> {
59 let v4 = TcpListener::bind((Ipv4Addr::LOCALHOST, local_port))
60 .await
61 .map_err(|err| SailError::Internal {
62 message: format!("bind 127.0.0.1:{local_port}: {err}"),
63 })?;
64 let bound = v4.local_addr().map_or(local_port, |addr| addr.port());
65 let v6 = match TcpListener::bind((Ipv6Addr::LOCALHOST, bound)).await {
69 Ok(v6) => Some(v6),
70 Err(err) if err.kind() == std::io::ErrorKind::AddrInUse => {
75 return Err(SailError::Internal {
76 message: format!("bind [::1]:{bound}: {err}"),
77 });
78 }
79 Err(_) => None,
82 };
83 let accept = tokio::spawn(async move {
84 loop {
85 let accepted = match &v6 {
86 Some(v6) => tokio::select! {
87 conn = v4.accept() => conn,
88 conn = v6.accept() => conn,
89 },
90 None => v4.accept().await,
91 };
92 match accepted {
93 Ok((conn, _)) => {
94 let worker = Arc::clone(&worker);
95 let endpoint = endpoint.clone();
96 let sailbox_id = sailbox_id.clone();
97 tokio::spawn(async move {
98 let _ =
99 tunnel_connection(worker, &endpoint, &sailbox_id, remote_port, conn)
100 .await;
101 });
102 }
103 Err(_) => tokio::time::sleep(Duration::from_millis(50)).await,
106 }
107 }
108 });
109 Ok(PortForward {
110 local_port: bound,
111 accept,
112 })
113}
114
115async fn tunnel_connection(
119 worker: Arc<WorkerProxy>,
120 endpoint: &str,
121 sailbox_id: &str,
122 remote_port: u16,
123 conn: TcpStream,
124) -> Result<(), SailError> {
125 let (tx, rx) = mpsc::channel::<pb::TunnelSailboxPortRequest>(TUNNEL_CHANNEL_CAP);
126 let handshake = pb::TunnelSailboxPortRequest {
128 sailbox_id: sailbox_id.to_string(),
129 port: u32::from(remote_port),
130 data: Vec::new(),
131 eof: false,
132 };
133 if tx.send(handshake).await.is_err() {
134 return Ok(());
135 }
136 let (mut read_half, mut write_half) = conn.into_split();
137
138 let uplink = tokio::spawn(async move {
145 let mut buf = vec![0u8; TUNNEL_READ_CHUNK];
146 loop {
147 match read_half.read(&mut buf).await {
148 Ok(n) if n > 0 => {
149 if tx.send(data_frame(buf[..n].to_vec())).await.is_err() {
150 break;
151 }
152 }
153 _ => {
160 let _ = tx.send(eof_frame()).await;
161 break;
162 }
163 }
164 }
165 });
166
167 let response = match open_tunnel_stream(&worker, endpoint, rx).await {
168 Ok(response) => response,
169 Err(err) => {
170 uplink.abort();
171 return Err(err);
172 }
173 };
174 let mut server = response.into_inner();
175
176 while let Ok(Some(frame)) = server.message().await {
178 if !frame.data.is_empty() && write_half.write_all(&frame.data).await.is_err() {
179 break;
180 }
181 if frame.eof {
182 let _ = write_half.shutdown().await;
183 }
184 }
185 uplink.abort();
186 Ok(())
187}
188
189pub(crate) fn forwardable_local_port(url: &str) -> Option<u16> {
194 let parsed = url::Url::parse(url).ok()?;
195 if is_forwardable_local_host(&parsed) {
196 return parsed.port_or_known_default();
197 }
198 None
199}
200
201fn is_forwardable_local_host(url: &url::Url) -> bool {
207 match url.host() {
208 Some(url::Host::Domain(domain)) => domain == "localhost",
209 Some(url::Host::Ipv4(ip)) => ip == Ipv4Addr::LOCALHOST || ip.is_unspecified(),
210 Some(url::Host::Ipv6(ip)) => ip.is_unspecified(),
211 _ => false,
212 }
213}
214
215pub(crate) fn is_unforwardable_loopback_url(url: &str) -> bool {
221 let Ok(parsed) = url::Url::parse(url) else {
222 return false;
223 };
224 match parsed.host() {
225 Some(url::Host::Ipv4(ip)) => ip.is_loopback() && ip != Ipv4Addr::LOCALHOST,
226 Some(url::Host::Ipv6(ip)) => match ip.to_ipv4_mapped() {
230 Some(v4) => v4.is_loopback(),
231 None => ip.is_loopback(),
232 },
233 _ => false,
234 }
235}
236
237pub(crate) fn rewrite_loopback_url(
246 url: &str,
247 local_port_for: impl Fn(u16) -> Option<u16>,
248) -> String {
249 let Ok(mut parsed) = url::Url::parse(url) else {
250 return url.to_string();
251 };
252 if !is_forwardable_local_host(&parsed) {
253 return url.to_string();
254 }
255 let Some(remote) = parsed.port_or_known_default() else {
256 return url.to_string();
257 };
258 let Some(local) = local_port_for(remote) else {
259 return url.to_string();
260 };
261 let wildcard = matches!(parsed.host(), Some(url::Host::Ipv4(ip)) if ip.is_unspecified())
262 || matches!(parsed.host(), Some(url::Host::Ipv6(ip)) if ip.is_unspecified());
263 if !wildcard {
264 return url.to_string();
265 }
266 if parsed.set_host(Some("localhost")).is_ok() && parsed.set_port(Some(local)).is_ok() {
267 parsed.to_string()
268 } else {
269 url.to_string()
270 }
271}
272
273pub(crate) enum RedirectCallback {
276 Forwardable(u16),
279 Unreachable,
283}
284
285pub(crate) fn redirect_callback(url: &str) -> Option<RedirectCallback> {
289 let parsed = url::Url::parse(url).ok()?;
290 for (key, value) in parsed.query_pairs() {
291 if key == "redirect_uri" || key == "redirect_url" {
292 if let Some(port) = forwardable_local_port(&value) {
293 return Some(RedirectCallback::Forwardable(port));
294 }
295 if is_unforwardable_loopback_url(&value) {
296 return Some(RedirectCallback::Unreachable);
297 }
298 }
299 }
300 None
301}
302
303type TunnelResponse = tonic::Response<tonic::Streaming<pb::TunnelSailboxPortResponse>>;
305
306async fn open_tunnel_stream(
307 worker: &Arc<WorkerProxy>,
308 endpoint: &str,
309 rx: mpsc::Receiver<pb::TunnelSailboxPortRequest>,
310) -> Result<TunnelResponse, SailError> {
311 let request = worker.request_for(ReceiverStream::new(rx), &[], None)?;
312 worker
313 .client_for(endpoint)?
314 .tunnel_sailbox_port(request)
315 .await
316 .map_err(|status| SailError::from_rpc_status(&status))
317}
318
319fn data_frame(data: Vec<u8>) -> pb::TunnelSailboxPortRequest {
320 pb::TunnelSailboxPortRequest {
321 sailbox_id: String::new(),
322 port: 0,
323 data,
324 eof: false,
325 }
326}
327
328fn eof_frame() -> pb::TunnelSailboxPortRequest {
329 pb::TunnelSailboxPortRequest {
330 sailbox_id: String::new(),
331 port: 0,
332 data: Vec::new(),
333 eof: true,
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use std::collections::HashMap;
341
342 #[test]
343 fn forwardable_local_port_covers_loopback_wildcard_and_defaults() {
344 assert_eq!(forwardable_local_port("http://localhost:5173/"), Some(5173));
345 assert_eq!(
346 forwardable_local_port("http://127.0.0.1:9000/x"),
347 Some(9000)
348 );
349 assert_eq!(forwardable_local_port("http://0.0.0.0:3000/"), Some(3000));
352 assert_eq!(forwardable_local_port("http://[::]:3000/"), Some(3000));
353 assert_eq!(forwardable_local_port("http://localhost/cb"), Some(80));
355 assert_eq!(forwardable_local_port("https://localhost/cb"), Some(443));
356 assert_eq!(forwardable_local_port("https://example.com:8080/"), None);
359 assert_eq!(forwardable_local_port("http://[::1]:5173/"), None);
360 assert_eq!(
361 forwardable_local_port("http://[::ffff:127.0.0.1]:5173/"),
362 None
363 );
364 assert_eq!(forwardable_local_port("http://127.0.0.2:3000/"), None);
365 }
366
367 #[test]
368 fn is_unforwardable_loopback_url_matches_undialed_loopbacks() {
369 assert!(is_unforwardable_loopback_url("http://[::1]:5173/"));
372 assert!(is_unforwardable_loopback_url("http://127.0.0.2:3000/"));
373 assert!(is_unforwardable_loopback_url(
374 "http://[::ffff:127.0.0.1]:3000/"
375 ));
376 assert!(!is_unforwardable_loopback_url("http://localhost:8080/"));
378 assert!(!is_unforwardable_loopback_url("http://127.0.0.1:8080/"));
379 assert!(!is_unforwardable_loopback_url("http://0.0.0.0:8080/"));
380 assert!(!is_unforwardable_loopback_url("http://[::]:8080/"));
381 assert!(!is_unforwardable_loopback_url("https://example.com/"));
382 }
383
384 #[test]
385 fn rewrite_loopback_url_only_rewrites_wildcard_hosts() {
386 let mut forwards = HashMap::new();
387 forwards.insert(3000u16, 3000u16); forwards.insert(8080u16, 8080u16);
389 let lookup = |remote: u16| forwards.get(&remote).copied();
390
391 assert_eq!(
394 rewrite_loopback_url("https://localhost:8080/app", lookup),
395 "https://localhost:8080/app"
396 );
397 assert_eq!(
398 rewrite_loopback_url("http://127.0.0.1:3000/x", lookup),
399 "http://127.0.0.1:3000/x"
400 );
401 assert_eq!(
403 rewrite_loopback_url("http://0.0.0.0:3000/x", lookup),
404 "http://localhost:3000/x"
405 );
406 assert_eq!(
407 rewrite_loopback_url("http://[::]:3000/x", lookup),
408 "http://localhost:3000/x"
409 );
410 assert_eq!(
412 rewrite_loopback_url("http://localhost:9999/x", lookup),
413 "http://localhost:9999/x"
414 );
415 assert_eq!(
416 rewrite_loopback_url("https://example.com:8080/x", lookup),
417 "https://example.com:8080/x"
418 );
419 }
420
421 #[test]
422 fn redirect_callback_classifies_the_redirect_uri() {
423 use RedirectCallback::{Forwardable, Unreachable};
424 assert!(matches!(
427 redirect_callback(
428 "https://example.com/oauth?client_id=x&redirect_uri=http%3A%2F%2Flocalhost%3A43222%2Fcallback"
429 ),
430 Some(Forwardable(43222))
431 ));
432 assert!(matches!(
433 redirect_callback("https://id.example/a?redirect_url=http://127.0.0.1:5000/cb"),
434 Some(Forwardable(5000))
435 ));
436 assert!(matches!(
438 redirect_callback("https://example.com/oauth?redirect_uri=http://[::1]:5173/cb"),
439 Some(Unreachable)
440 ));
441 assert!(matches!(
442 redirect_callback("https://example.com/oauth?redirect_uri=http://127.0.0.2:5173/cb"),
443 Some(Unreachable)
444 ));
445 assert!(
447 redirect_callback("https://example.com/oauth?redirect_uri=https://example.com/cb")
448 .is_none()
449 );
450 assert!(redirect_callback("https://example.com/login").is_none());
451 }
452}