Skip to main content

scion_http3/
tunnel.rs

1// Copyright 2026 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Byte streams through `CONNECT` tunnels.
16
17use std::{
18    fmt, io,
19    pin::Pin,
20    sync::Arc,
21    task::{Context, Poll},
22};
23
24use scion_quic::h3::client::{H3DuplexStream, Http3Client};
25use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
26
27use crate::authority::Authority;
28
29/// Builds the authority-form `CONNECT` request for `authority`.
30pub(crate) fn connect_request(authority: &Authority) -> http::Request<()> {
31    http::Request::builder()
32        .method(http::Method::CONNECT)
33        .uri(format!("https://{authority}"))
34        .body(())
35        .expect("a valid authority forms a valid URI")
36}
37
38/// A byte stream through an accepted `CONNECT` tunnel.
39///
40/// The two directions are independent. `shutdown` closes the write half and leaves the read half
41/// open. A read returns `Ok(0)` at the end of the stream. A stream the peer reset or stopped fails
42/// with [`io::ErrorKind::ConnectionReset`], and a closed connection with
43/// [`io::ErrorKind::NotConnected`]. Dropping the tunnel resets the stream.
44pub struct Tunnel {
45    stream: H3DuplexStream,
46    /// Keeps the connection alive while the pool may evict its origin.
47    _connection: Arc<Http3Client>,
48}
49
50impl Tunnel {
51    pub(crate) fn new(stream: H3DuplexStream, connection: Arc<Http3Client>) -> Self {
52        Tunnel {
53            stream,
54            _connection: connection,
55        }
56    }
57}
58
59impl AsyncRead for Tunnel {
60    fn poll_read(
61        self: Pin<&mut Self>,
62        cx: &mut Context<'_>,
63        buf: &mut ReadBuf<'_>,
64    ) -> Poll<io::Result<()>> {
65        Pin::new(&mut self.get_mut().stream).poll_read(cx, buf)
66    }
67}
68
69impl AsyncWrite for Tunnel {
70    fn poll_write(
71        self: Pin<&mut Self>,
72        cx: &mut Context<'_>,
73        buf: &[u8],
74    ) -> Poll<io::Result<usize>> {
75        Pin::new(&mut self.get_mut().stream).poll_write(cx, buf)
76    }
77
78    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
79        Pin::new(&mut self.get_mut().stream).poll_flush(cx)
80    }
81
82    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
83        Pin::new(&mut self.get_mut().stream).poll_shutdown(cx)
84    }
85}
86
87impl fmt::Debug for Tunnel {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        f.debug_struct("Tunnel").finish_non_exhaustive()
90    }
91}