Skip to main content

rightkit_browser/
proxy.rs

1//! In-process pinning proxy. Chrome is started with `--proxy-server` pointing
2//! here (loopback included, no bypass), so Chrome never resolves names itself:
3//! this proxy resolves once through [`NetworkPolicy::pin`], checks the resolved
4//! addresses against the policy, and connects to the vetted address. A name
5//! that flips from public to private between check and connect (DNS
6//! rebinding), or a request that bypassed page interception (workers), can
7//! therefore never reach a blocked address.
8use crate::policy::Guard;
9use std::net::{IpAddr, SocketAddr};
10use std::sync::{Arc, Mutex};
11use std::time::Duration;
12use tokio::io::{AsyncReadExt, AsyncWriteExt};
13use tokio::net::{TcpListener, TcpStream};
14use tokio::task::JoinHandle;
15
16/// One connection the proxy made, with the address it pinned.
17#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct PinnedConnection {
19    pub host: String,
20    pub port: u16,
21    pub ip: IpAddr,
22}
23
24pub(crate) struct PinProxy {
25    pub addr: SocketAddr,
26    pub log: Arc<Mutex<Vec<PinnedConnection>>>,
27    task: JoinHandle<()>,
28}
29
30impl Drop for PinProxy {
31    fn drop(&mut self) {
32        self.task.abort();
33    }
34}
35
36impl PinProxy {
37    pub async fn start(guard: Arc<Guard>) -> std::io::Result<PinProxy> {
38        let listener = TcpListener::bind("127.0.0.1:0").await?;
39        let addr = listener.local_addr()?;
40        let log: Arc<Mutex<Vec<PinnedConnection>>> = Arc::default();
41        let l = log.clone();
42        let task = tokio::spawn(async move {
43            loop {
44                let Ok((conn, _)) = listener.accept().await else {
45                    break;
46                };
47                let (guard, log) = (guard.clone(), l.clone());
48                tokio::spawn(async move {
49                    let _ = serve(conn, guard, log).await;
50                });
51            }
52        });
53        Ok(PinProxy { addr, log, task })
54    }
55}
56
57async fn refuse(conn: &mut TcpStream, code: &str, why: &str) {
58    let body = format!("blocked by rightkit-browser network policy: {why}");
59    let _ = conn
60        .write_all(
61            format!(
62                "HTTP/1.1 {code}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
63                body.len()
64            )
65            .as_bytes(),
66        )
67        .await;
68}
69
70async fn serve(
71    mut conn: TcpStream,
72    guard: Arc<Guard>,
73    log: Arc<Mutex<Vec<PinnedConnection>>>,
74) -> std::io::Result<()> {
75    let mut buf = Vec::new();
76    let mut chunk = [0u8; 4096];
77    let head_end = loop {
78        if let Some(i) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
79            break i + 4;
80        }
81        if buf.len() > 64 * 1024 {
82            return Ok(());
83        }
84        let n = conn.read(&mut chunk).await?;
85        if n == 0 {
86            return Ok(());
87        }
88        buf.extend_from_slice(&chunk[..n]);
89    };
90    let head = String::from_utf8_lossy(&buf[..head_end]).to_string();
91    let rest = buf[head_end..].to_vec();
92    let mut lines = head.split("\r\n");
93    let first = lines.next().unwrap_or("");
94    let mut parts = first.split_whitespace();
95    let (method, target, version) = (
96        parts.next().unwrap_or(""),
97        parts.next().unwrap_or(""),
98        parts.next().unwrap_or("HTTP/1.1"),
99    );
100
101    let is_connect = method.eq_ignore_ascii_case("CONNECT");
102    let (host, port, forward_head) = if is_connect {
103        let (h, p) = target.rsplit_once(':').unwrap_or((target, "443"));
104        (h.to_string(), p.parse::<u16>().unwrap_or(443), None)
105    } else {
106        let Ok(u) = url::Url::parse(target) else {
107            refuse(
108                &mut conn,
109                "400 Bad Request",
110                "proxy requests must use absolute URLs",
111            )
112            .await;
113            return Ok(());
114        };
115        let Some(h) = u.host_str() else {
116            refuse(&mut conn, "400 Bad Request", "no host").await;
117            return Ok(());
118        };
119        let path = match u.query() {
120            Some(q) => format!("{}?{q}", u.path()),
121            None => u.path().to_string(),
122        };
123        let mut out = format!("{method} {path} {version}\r\n");
124        for l in lines.filter(|l| !l.is_empty()) {
125            let name = l.split(':').next().unwrap_or("").to_ascii_lowercase();
126            if matches!(
127                name.as_str(),
128                "connection" | "proxy-connection" | "keep-alive" | "proxy-authorization"
129            ) {
130                continue;
131            }
132            out.push_str(l);
133            out.push_str("\r\n");
134        }
135        out.push_str("Connection: close\r\n\r\n");
136        (
137            h.to_string(),
138            u.port_or_known_default().unwrap_or(80),
139            Some(out),
140        )
141    };
142
143    let ips = match guard.network.pin(&host).await {
144        Ok(ips) => ips,
145        Err(reason) => {
146            guard.denied_network(None, "Connect", &reason);
147            refuse(&mut conn, "403 Forbidden", &reason.to_string()).await;
148            return Ok(());
149        }
150    };
151    let mut upstream = None;
152    for ip in ips {
153        if let Ok(Ok(s)) = tokio::time::timeout(
154            Duration::from_secs(3),
155            TcpStream::connect(SocketAddr::new(ip, port)),
156        )
157        .await
158        {
159            log.lock().unwrap().push(PinnedConnection {
160                host: host.clone(),
161                port,
162                ip,
163            });
164            upstream = Some(s);
165            break;
166        }
167        log.lock().unwrap().push(PinnedConnection {
168            host: host.clone(),
169            port,
170            ip,
171        });
172    }
173    let Some(mut upstream) = upstream else {
174        refuse(&mut conn, "502 Bad Gateway", "connect failed").await;
175        return Ok(());
176    };
177    match forward_head {
178        None => {
179            conn.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
180                .await?;
181            upstream.write_all(&rest).await?;
182        }
183        Some(h) => {
184            upstream.write_all(h.as_bytes()).await?;
185            upstream.write_all(&rest).await?;
186        }
187    }
188    let _ = tokio::io::copy_bidirectional(&mut conn, &mut upstream).await;
189    Ok(())
190}