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