use std::{ str::FromStr, }; use fltk::{ app, button::Button, enums::CallbackTrigger, frame::Frame, group::Flex, input::*, prelude::*, window::Window }; use rand::{ Rng, SeedableRng, }; use structopt::StructOpt; use tokio::runtime::Runtime; use quic_demo::{ client_proxy::*, prelude::*, protocol::PeerId, }; #[derive (Debug, StructOpt)] struct Opt { #[structopt (long)] window_title: Option , #[structopt (long)] relay_addr: Option , #[structopt (long)] client_id: Option , #[structopt (long)] cert_url: Option , } #[derive (Clone, Copy)] enum Message { OpenPort (usize), ClosePort (usize), AddPort, } struct GuiClient <'a> { rt: &'a Runtime, frame_status: Frame, ports: Vec , but_add_port: Button, } struct Port { gui: GuiPort, forwarding_instance: Option , } impl Port { pub fn open_port ( &mut self, rt: &Runtime, connection_p2_p3: quinn::Connection, ) -> anyhow::Result <()> { let params = self.gui.get_params ()?; let _guard = rt.enter (); let forwarding_instance = rt.block_on (ForwardingInstance::new ( connection_p2_p3, params, ))?; self.gui.input_client_port.set_value (&forwarding_instance.local_port ().to_string ()); self.forwarding_instance.replace (forwarding_instance); self.gui.set_forwarding (true); Ok (()) } } struct GuiPort { row: fltk::group::Flex, input_client_port: Input, input_server_id: Input, input_server_port: Input, but_open: Button, but_close: Button, } impl GuiClient <'_> { pub fn open_port ( &mut self, connection_p2_p3: quinn::Connection, port_idx: usize, ) -> anyhow::Result <()> { self.ports [port_idx].open_port (self.rt, connection_p2_p3)?; self.sync_status (); Ok (()) } pub fn close_port (&mut self, port_idx: usize) -> anyhow::Result <()> { if let Some (old_instance) = self.ports [port_idx].forwarding_instance.take () { self.rt.block_on (async { old_instance.close () .await .context ("closing ForwardingInstance")?; Ok::<_, anyhow::Error> (()) })?; } self.ports [port_idx].gui.set_forwarding (false); self.sync_status (); Ok (()) } fn open_ports (&self) -> usize { self.ports.iter () .map (|x| if x.forwarding_instance.is_some () { 1 } else { 0 }) .sum () } pub fn sync_status (&mut self) { let open_ports = self.open_ports (); self.frame_status.set_label (&format! ("Forwarding {} ports", open_ports)); } pub fn add_port ( &mut self, ports_col: &mut Flex, fltk_tx: fltk::app::Sender ) { const MAX_PORTS: usize = 15; if self.ports.len () >= MAX_PORTS { return; } let gui = GuiPort::new (fltk_tx, self.ports.len ()); ports_col.add (&gui.row); ports_col.set_size (&gui.row, 30); let port = Port { gui, forwarding_instance: None, }; self.ports.push (port); if self.ports.len () >= MAX_PORTS { self.but_add_port.deactivate (); } } } fn main () -> anyhow::Result <()> { tracing_subscriber::fmt::init (); let rt = Runtime::new ()?; let opt = Opt::from_args (); let (fltk_tx, fltk_rx) = app::channel:: (); let app = app::App::default (); let window_title = opt.window_title.clone ().unwrap_or_else (|| "PTTH client proxy".to_string ()); let mut wind = Window::new (100, 100, 800, 600, None) .with_label (&window_title); wind.make_resizable (true); let mut col = Flex::default ().column ().size_of_parent (); let frame_status = Frame::default (); col.set_size (&frame_status, 30); { let mut row = Flex::default ().row (); let l = Frame::default ().with_label ("Server ID"); row.set_size (&l, 120); let l = Frame::default ().with_label ("Server port"); row.set_size (&l, 80); let l = Frame::default ().with_label ("Local port"); row.set_size (&l, 80); row.end (); col.set_size (&row, 30); } let mut ports_col = Flex::default ().column (); ports_col.end (); let mut but_add_port = Button::default ().with_label ("+"); but_add_port.set_trigger (CallbackTrigger::Release); but_add_port.emit (fltk_tx, Message::AddPort); col.set_size (&but_add_port, 30); col.end (); let relay_addr = opt.relay_addr.as_ref () .map (|s| &s[..]) .unwrap_or ("127.0.0.1:30380") .parse () .context ("relay_addr should be like 127.0.0.1:30380")?; let mut gui_client = GuiClient { rt: &rt, frame_status, ports: Default::default (), but_add_port, }; gui_client.add_port (&mut ports_col, fltk_tx); ports_col.recalc (); gui_client.sync_status (); wind.end (); wind.show (); let connection_p2_p3 = rt.block_on (async move { let server_cert = match opt.cert_url.as_ref () { Some (url) => reqwest::get (url).await?.bytes ().await?, None => tokio::fs::read ("ptth_quic_output/quic_server.crt").await.context ("can't read quic_server.crt from disk")?.into (), }; let endpoint = make_client_endpoint ("0.0.0.0:0".parse ()?, &[&server_cert])?; trace! ("Connecting to relay server"); let client_id = opt.client_id.unwrap_or_else (|| "bogus_client".to_string ()); let quinn::NewConnection { connection, .. } = protocol::p2_connect_to_p3 (&endpoint, &relay_addr, &client_id).await .context ("P2 can't connect to P3")?; Ok::<_, anyhow::Error> (connection) })?; while app.wait () { match fltk_rx.recv () { Some (Message::OpenPort (port_idx)) => { if let Err (e) = gui_client.open_port (connection_p2_p3.clone (), port_idx) { error! ("{:?}", e); } }, Some (Message::ClosePort (port_idx)) => { gui_client.close_port (port_idx)?; }, Some (Message::AddPort) => { gui_client.add_port (&mut ports_col, fltk_tx); ports_col.recalc (); ports_col.redraw (); }, None => (), } } Ok (()) } fn set_active (w: &mut W, b: bool) { if b { w.activate (); } else { w.deactivate (); } } impl GuiPort { fn new (fltk_tx: fltk::app::Sender , port_idx: usize) -> Self { let mut row = Flex::default ().row (); let mut input_server_id = Input::default (); let mut input_server_port = Input::default (); let mut input_client_port = Input::default (); let mut but_open = Button::default ().with_label ("Open"); let mut but_close = Button::default ().with_label ("Close"); row.set_size (&input_server_id, 120); row.set_size (&input_server_port, 80); row.set_size (&input_client_port, 80); row.set_size (&but_open, 80); row.set_size (&but_close, 80); input_client_port.set_value (""); input_client_port.set_readonly (true); input_server_id.set_value ("bogus_server"); input_server_port.set_value ("5900"); but_open.set_trigger (CallbackTrigger::Release); but_open.emit (fltk_tx, Message::OpenPort (port_idx)); but_close.set_trigger (CallbackTrigger::Release); but_close.emit (fltk_tx, Message::ClosePort (port_idx)); row.end (); let mut output = Self { row, input_client_port, input_server_id, input_server_port, but_open, but_close, }; output.set_forwarding (false); output } fn get_params (&self) -> anyhow::Result { let server_tcp_port = u16::from_str (&self.input_server_port.value ())?; let server_id = self.input_server_id.value (); let client_tcp_port = PortInfo { server_id: &server_id, server_tcp_port, }.random_eph_port (); Ok (ForwardingParams { client_tcp_port, server_id, server_tcp_port, }) } fn set_forwarding (&mut self, x: bool) { set_active (&mut self.input_client_port, x); set_active (&mut self.input_server_id, !x); set_active (&mut self.input_server_port, !x); set_active (&mut self.but_open, !x); set_active (&mut self.but_close, x); self.but_open.set (x); self.but_close.set (!x); } } // This can collide, but who cares // It's not secure or anything - It's just supposed to pick a port somewhat // deterministically based on the server and relay info. #[derive (serde::Serialize)] struct PortInfo <'a> { // relay_addr: SocketAddr, server_id: &'a str, server_tcp_port: u16 } impl PortInfo <'_> { // https://en.wikipedia.org/wiki/TCP_ports#Dynamic,_private_or_ephemeral_ports fn random_eph_port (&self) -> u16 { let seed = blake3::hash (&rmp_serde::to_vec (self).expect ("Can't hash PortInfo - impossible error")); let mut rng = rand_chacha::ChaCha20Rng::from_seed (*seed.as_bytes ()); let tcp_eph_range = 49152..=65535; rng.gen_range (tcp_eph_range) } } #[cfg (test)] mod test { use blake3::Hasher; use super::*; #[test] fn prng () { let hasher = Hasher::default (); let seed = hasher.finalize (); let mut rng = rand_chacha::ChaCha20Rng::from_seed (*seed.as_bytes ()); let tcp_eph_range = 49152..=65535; let port = rng.gen_range (tcp_eph_range); assert_eq! (port, 49408); for (input, expected) in [ (("127.0.0.1:4000", "bogus_server", 22), 51168), // The relay address is explicitly excluded from the eph port // computation in case I want to support connecting to a server // across multiple relays (("127.0.0.1:30380", "bogus_server", 22), 51168), (("127.0.0.1:4000", "real_server", 22), 53873), (("127.0.0.1:4000", "bogus_server", 5900), 53844), ] { let (_relay_addr, server_id, server_tcp_port) = input; let input = PortInfo { server_id, server_tcp_port, }; let actual = input.random_eph_port (); assert_eq! (expected, actual); } } }