Skip to main content

geph5_client/
client.rs

1use anyctx::AnyCtx;
2
3use anyhow::Context;
4use bytes::Bytes;
5use futures_concurrency::future::Race as _;
6use futures_util::{FutureExt, TryFutureExt, future::Shared, task::noop_waker};
7use geph5_broker_protocol::UserInfo;
8use geph5_misc_rpc::client_control::{ControlClient, ControlService};
9use geph5_rt::Immortal;
10use nanorpc::DynRpcTransport;
11use sillad::Pipe;
12use std::sync::Arc;
13#[cfg(unix)]
14use tokio::io::{AsyncReadExt, AsyncWriteExt};
15
16use crate::{
17    auth::{auth_loop, get_auth_token},
18    broker::broker_client,
19    bw_token::bw_token_refresh_loop,
20    control_prot::{ControlProtocolImpl, DummyControlProtocolTransport},
21    http_proxy::http_proxy_serve,
22    logging,
23    pac::pac_serve,
24    port_forward::port_forward,
25    session::{open_conn, run_session},
26    socks5::socks5_loop,
27    vpn::{recv_vpn_packet, send_vpn_packet, vpn_loop},
28};
29
30// `Config` and its broker-source descriptors live in `geph5-misc-rpc` so that
31// tools which only configure or drive an engine (the `geph` daemon/CLI) can
32// depend on that lightweight crate instead of this whole engine. Re-exported
33// here so `geph5_client::Config` etc. keep working. The behavior over
34// `BrokerSource` lives in `broker.rs` as extension traits.
35pub use geph5_misc_rpc::client_config::{BrokerKeys, Config};
36
37#[derive(Clone)]
38pub struct Client {
39    task: Shared<geph5_rt::Task<Result<(), Arc<anyhow::Error>>>>,
40    ctx: AnyCtx<Config>,
41}
42
43impl Client {
44    /// Starts the client logic in the loop, returning the handle.
45    pub fn start(cfg: Config) -> Self {
46        Self::start_with_vpn_fd(cfg, None)
47    }
48
49    /// Starts the client logic in the loop with an optional platform-VPN file
50    /// descriptor wired into the VPN channels. The fd is consumed by this call.
51    #[cfg(unix)]
52    pub fn start_with_vpn_fd(cfg: Config, vpn_fd: Option<i32>) -> Self {
53        let ctx = AnyCtx::new(cfg.clone());
54        // Initialize logging once we have context so JSON logs go to SQLite
55        let _ = logging::init_logging(&ctx);
56        let ((fd_limit, _), _) = binary_search::binary_search((1, ()), (65536, ()), |lim| {
57            if rlimit::increase_nofile_limit(lim).unwrap_or_default() >= lim {
58                binary_search::Direction::Low(())
59            } else {
60                binary_search::Direction::High(())
61            }
62        });
63        tracing::info!("raised file descriptor limit to {}", fd_limit);
64
65        if let Some(fd) = vpn_fd {
66            let ctx_clone = ctx.clone();
67            geph5_rt::spawn(async move {
68                // Create an async file descriptor from the raw fd
69                let file: std::fs::File = unsafe { std::os::fd::FromRawFd::from_raw_fd(fd) };
70                let async_fd = geph5_rt::asyncfd::AsyncFdStream::new(file)
71                    .expect("could not wrap VPN fd in AsyncFdStream");
72
73                // Split the file descriptor for reading and writingz
74                let (mut reader, mut writer) = tokio::io::split(async_fd);
75
76                // Spawn a task for reading from fd and sending to VPN
77                let read_task = async {
78                    let mut buf = vec![0u8; 65535]; // Buffer for reading packets
79                    loop {
80                        match reader.read(&mut buf).await {
81                            Ok(n) if n > 0 => {
82                                // macOS utun prepends a 4-byte address-family
83                                // header to every packet; strip it to recover the
84                                // raw IP packet the IP stack expects.
85                                #[cfg(target_os = "macos")]
86                                let pkt = {
87                                    if n <= 4 {
88                                        continue;
89                                    }
90                                    bytes::Bytes::copy_from_slice(&buf[4..n])
91                                };
92                                #[cfg(not(target_os = "macos"))]
93                                let pkt = bytes::Bytes::copy_from_slice(&buf[..n]);
94                                // Send the packet to the VPN
95                                send_vpn_packet(&ctx_clone, pkt).await;
96                            }
97                            Ok(0) => {
98                                // EOF
99                                tracing::warn!("VPN fd reached EOF");
100                                break;
101                            }
102                            Err(e) => {
103                                tracing::error!("Error reading from VPN fd: {}", e);
104                                break;
105                            }
106                            _ => break,
107                        }
108                    }
109                    anyhow::Ok(())
110                };
111
112                // Spawn a task for receiving from VPN and writing to fd
113                let write_task = async {
114                    loop {
115                        // Receive a packet from the VPN
116                        let packet = recv_vpn_packet(&ctx_clone).await;
117
118                        // macOS utun expects a 4-byte address-family header before
119                        // the IP packet, written as a single datagram. Pick AF from
120                        // the IP version nibble (AF_INET=2, AF_INET6=30).
121                        #[cfg(target_os = "macos")]
122                        let packet = {
123                            let af: u32 = if packet.first().map(|b| b >> 4) == Some(6) {
124                                30
125                            } else {
126                                2
127                            };
128                            let mut framed = Vec::with_capacity(4 + packet.len());
129                            framed.extend_from_slice(&af.to_be_bytes());
130                            framed.extend_from_slice(&packet);
131                            bytes::Bytes::from(framed)
132                        };
133
134                        // Write the packet to the file descriptor
135                        if let Err(e) = writer.write_all(&packet).await {
136                            tracing::error!("Error writing to VPN fd: {}", e);
137                            break;
138                        }
139
140                        if let Err(e) = writer.flush().await {
141                            tracing::error!("Error flushing VPN fd: {}", e);
142                            break;
143                        }
144                    }
145                    anyhow::Ok(())
146                };
147
148                // Wait for either task to complete (or fail)
149                let _ = (read_task, write_task).race().await;
150                tracing::warn!("VPN fd handler exited");
151            })
152            .detach();
153        }
154        let task = geph5_rt::spawn(client_main(ctx.clone()).map_err(Arc::new));
155        Client {
156            task: task.shared(),
157            ctx,
158        }
159    }
160
161    /// Starts the client logic in the loop. Non-Unix targets never get a VPN fd.
162    #[cfg(not(unix))]
163    pub fn start_with_vpn_fd(cfg: Config, _vpn_fd: Option<i32>) -> Self {
164        let ctx = AnyCtx::new(cfg.clone());
165        let _ = logging::init_logging(&ctx);
166        let ((fd_limit, _), _) = binary_search::binary_search((1, ()), (65536, ()), |lim| {
167            if rlimit::increase_nofile_limit(lim).unwrap_or_default() >= lim {
168                binary_search::Direction::Low(())
169            } else {
170                binary_search::Direction::High(())
171            }
172        });
173        tracing::info!("raised file descriptor limit to {}", fd_limit);
174
175        let task = geph5_rt::spawn(client_main(ctx.clone()).map_err(Arc::new));
176        Client {
177            task: task.shared(),
178            ctx,
179        }
180    }
181
182    /// Opens a connection through the tunnel.
183    pub async fn open_conn(&self, remote: &str) -> anyhow::Result<Box<dyn Pipe>> {
184        open_conn(&self.ctx, "tcp", remote).await
185    }
186
187    /// Wait until there's an error.
188    pub async fn wait_until_dead(self) -> anyhow::Result<()> {
189        self.task.await.map_err(|e| anyhow::anyhow!(e))
190    }
191
192    /// Check for an error.
193    pub fn check_dead(&self) -> anyhow::Result<()> {
194        match self
195            .task
196            .clone()
197            .poll_unpin(&mut std::task::Context::from_waker(&noop_waker()))
198        {
199            std::task::Poll::Ready(val) => val.map_err(|e| anyhow::anyhow!(e))?,
200            std::task::Poll::Pending => {}
201        }
202
203        Ok(())
204    }
205
206    /// Get the control protocol client.
207    pub fn control_client(&self) -> ControlClient {
208        ControlClient(DynRpcTransport::new(DummyControlProtocolTransport(
209            ControlService(ControlProtocolImpl {
210                ctx: self.ctx.clone(),
211            }),
212        )))
213    }
214
215    /// Gets the user info.
216    pub async fn user_info(&self) -> anyhow::Result<UserInfo> {
217        let auth_token = get_auth_token(&self.ctx).await?;
218        let user_info = broker_client(&self.ctx)?
219            .get_user_info(auth_token)
220            .await??
221            .context("no such user")?;
222        Ok(user_info)
223    }
224
225    /// Force a particular packet to be sent through VPN mode, regardless of whether VPN mode is on.
226    pub async fn send_vpn_packet(&self, bts: Bytes) -> anyhow::Result<()> {
227        send_vpn_packet(&self.ctx, bts).await;
228        Ok(())
229    }
230
231    /// Receive a packet from VPN mode, regardless of whether VPN mode is on.
232    pub async fn recv_vpn_packet(&self) -> anyhow::Result<Bytes> {
233        let packet = recv_vpn_packet(&self.ctx).await;
234        Ok(packet)
235    }
236}
237
238pub type CtxField<T> = fn(&AnyCtx<Config>) -> T;
239
240async fn client_main(ctx: AnyCtx<Config>) -> anyhow::Result<()> {
241    let tcp_rpc_serve = async {
242        if let Some(control_listen) = ctx.init().control_listen {
243            nanorpc_sillad::rpc_serve(
244                sillad::tcp::TcpListener::bind(control_listen).await?,
245                ControlService(ControlProtocolImpl { ctx: ctx.clone() }),
246            )
247            .await?;
248            anyhow::Ok(())
249        } else {
250            std::future::pending().await
251        }
252    };
253    let unix_rpc_serve = async {
254        #[cfg(unix)]
255        if let Some(path) = ctx.init().control_listen_unix.as_ref() {
256            nanorpc_sillad::rpc_serve(
257                sillad::unix::UnixListener::bind(path).await?,
258                ControlService(ControlProtocolImpl { ctx: ctx.clone() }),
259            )
260            .await?;
261            return anyhow::Ok(());
262        }
263        std::future::pending().await
264    };
265    let pipe_rpc_serve = async {
266        #[cfg(windows)]
267        if let Some(name) = ctx.init().control_listen_pipe.as_ref() {
268            nanorpc_sillad::rpc_serve(
269                sillad::windows_pipe::NamedPipeListener::bind(name, None)?,
270                ControlService(ControlProtocolImpl { ctx: ctx.clone() }),
271            )
272            .await?;
273            return anyhow::Ok(());
274        }
275        std::future::pending().await
276    };
277    let rpc_serve = (tcp_rpc_serve, unix_rpc_serve, pipe_rpc_serve).race();
278    if ctx.init().dry_run {
279        rpc_serve.await
280    } else {
281        let vpn_loop = vpn_loop(&ctx);
282
283        let _client_loop = Immortal::spawn(run_session(ctx.clone()));
284
285        (
286            socks5_loop(&ctx)
287                .inspect_err(|e| tracing::error!(err = debug(e), "socks5 loop stopped")),
288            vpn_loop.inspect_err(|e| tracing::error!(err = debug(e), "vpn loop stopped")),
289            http_proxy_serve(&ctx)
290                .inspect_err(|e| tracing::error!(err = debug(e), "http proxy stopped")),
291            auth_loop(&ctx).inspect_err(|e| tracing::error!(err = debug(e), "auth loop stopped")),
292            bw_token_refresh_loop(&ctx)
293                .inspect_err(|e| tracing::error!(err = debug(e), "bw token loop stopped")),
294            rpc_serve,
295            pac_serve(&ctx),
296            port_forward(&ctx),
297        )
298            .race()
299            .await
300    }
301}