rightkit_browser/
proxy.rs1use 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#[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}