diff --git a/lightway-client/src/config.rs b/lightway-client/src/config.rs index 48b18007..87b7109f 100644 --- a/lightway-client/src/config.rs +++ b/lightway-client/src/config.rs @@ -16,9 +16,6 @@ use std::time::Duration as StdDuration; use std::{net::Ipv4Addr, path::PathBuf}; use struct_patch::Patch; -#[cfg(feature = "mobile")] -use std::net::IpAddr; - // NOTE // The cli argument and options all set to ConfigPatch, which is generated by // Patch derive, do NOT set defualts in the clap macros, and let all default @@ -34,11 +31,13 @@ pub struct Config { doc = "Generate configure for single server in the format to path from `-c, --config-file`" ))] #[serde(skip_serializing)] + #[schemars(skip)] pub generate: ConfigFormat, #[patch(attribute(clap(short, long)))] #[patch(attribute(doc = "Config File to load"))] #[serde(skip_serializing)] + #[schemars(skip)] pub config_file: PathBuf, /// Servers to attempt to connect to. Configuration is only supported in @@ -46,11 +45,13 @@ pub struct Config { #[patch(attribute(clap(skip)))] #[serde(default)] #[serde(skip_serializing)] + #[schemars(skip)] pub servers: Vec, #[patch(attribute(clap(short, long)))] #[patch(attribute(doc = r#"Server to connect to in `:` format Only used if `servers` is empty"#))] + /// socket address, ex: 127.0.0.1:27690 pub server: String, #[patch(attribute(clap(short, long)))] @@ -72,6 +73,7 @@ pub struct Config { #[patch(attribute(doc = r#"Auth token If both token and user/pass are provided, token auth will be used. user/pass will be ignored in this case"#))] + #[schemars(extend("format" = "password"))] pub token: Option, #[patch(attribute(clap(short, long, hide = true)))] @@ -80,12 +82,14 @@ pub struct Config { #[patch(attribute(clap(short, long, hide = true)))] #[patch(attribute(doc = "Password for auth"))] + #[schemars(extend("format" = "password"))] pub password: Option, #[patch(attribute(clap(long)))] #[patch(attribute(doc = r#"CA certificate This can either be a path to the file, or a string starting with "-----BEGIN CERTIFICATE-----""#))] + #[schemars(extend("format" = "textarea"))] pub ca_cert: String, #[patch(attribute(clap(long)))] @@ -99,6 +103,7 @@ pub struct Config { #[cfg(windows)] #[patch(attribute(clap(long)))] #[patch(attribute(doc = "Path to wintun.dll file (Windows only)"))] + #[schemars(extend("x-cfg" = "windows"))] pub wintun_file: Option, #[cfg(windows)] @@ -115,6 +120,7 @@ pub struct Config { #[patch(attribute(clap(long)))] #[patch(attribute(doc = "Peer IP to use in Tun device"))] + #[schemars(extend("x-cfg" = "desktop"))] pub tun_peer_ip: Ipv4Addr, #[patch(attribute(clap(long)))] @@ -123,16 +129,20 @@ pub struct Config { #[cfg(feature = "postquantum")] #[patch(attribute(clap(long, value_enum)))] + #[patch(attribute(doc = "Enable Post Quantum Crypto"))] + #[schemars(extend("x-cfg" = "desktop"))] pub keyshare: KeyShare, #[patch(attribute(clap(long)))] #[patch(attribute(doc = "Interval between keepalives"))] #[schemars(schema_with = "lightway_app_utils::args::nonzero_duration_schema")] + /// ex: 10s pub keepalive_interval: NonZeroDuration, #[patch(attribute(clap(long)))] #[patch(attribute(doc = "Keepalive timeout"))] #[schemars(schema_with = "lightway_app_utils::args::nonzero_duration_schema")] + /// ex: 60s pub keepalive_timeout: NonZeroDuration, #[patch(attribute(clap(long)))] @@ -144,7 +154,9 @@ pub struct Config { #[patch(attribute(clap(long)))] #[patch(attribute(doc = r#"Time it takes to trigger a tracer packet when we haven't received an outside packet"#))] + #[schemars(extend("x-cfg" = "desktop"))] #[schemars(schema_with = "lightway_app_utils::args::nonzero_duration_schema")] + /// ex: 10s pub tracer_packet_timeout: NonZeroDuration, // NOTE: also "Defer timeout" in mobile device @@ -154,16 +166,21 @@ pub struct Config { If the preferred connection connects before the timeout, it will be used immediately."#) )] #[schemars(schema_with = "lightway_app_utils::args::duration_schema")] + /// ex: 2000ms pub preferred_connection_wait_interval: Duration, #[patch(attribute(clap(long)))] #[patch(attribute(doc = "Socket send buffer size"))] + #[schemars(extend("x-cfg" = "desktop"))] #[schemars(schema_with = "byte_size_schema")] + /// ex: 1.5 MiB pub sndbuf: ByteSize, #[patch(attribute(clap(long)))] #[patch(attribute(doc = "Socket receive buffer size"))] + #[schemars(extend("x-cfg" = "desktop"))] #[schemars(schema_with = "byte_size_schema")] + /// ex: 1.5 MiB pub rcvbuf: ByteSize, #[cfg(batch_receive)] @@ -173,6 +190,7 @@ pub struct Config { #[patch(attribute( doc = "Enable batch receive (`recvmsg_x` on macOS, `recvmmsg` on Linux/Android))" ))] + #[schemars(extend("x-cfg" = "batch_receive"))] pub enable_batch_receive: bool, #[cfg(desktop)] @@ -182,6 +200,7 @@ pub struct Config { default: Sets up routes as specified in server, tun_local_ip, tun_peer_ip, tun_dns_ip noexec : Does not setup any routes lan : Sets up default + additional lan routes"#))] + #[schemars(extend("x-cfg" = "desktop"))] pub route_mode: RouteMode, #[cfg(desktop)] @@ -190,6 +209,7 @@ pub struct Config { Modes: default: Sets up DNS Configuration based on target platform noexec : Skips DNS Configuration setup"#))] + #[schemars(extend("x-cfg" = "desktop"))] pub dns_config_mode: DnsConfigMode, #[patch(attribute(clap(long, value_enum)))] @@ -211,29 +231,35 @@ pub struct Config { #[patch(empty_value = false)] #[patch(attribute(serde(default)))] #[patch(attribute(doc = "Enable PMTU discovery for [`ConnectionType::Udp`] connections"))] + #[schemars(extend("x-cfg" = "desktop"))] pub enable_pmtud: bool, #[patch(attribute(clap(long)))] #[patch(attribute(doc = "Base MTU to use for PMTU discovery"))] + #[schemars(extend("x-cfg" = "desktop"))] pub pmtud_base_mtu: Option, #[patch(attribute(clap(long)))] #[patch(empty_value = false)] #[patch(attribute(serde(default)))] #[patch(attribute(doc = "Enable IO-uring interface for Tunnel"))] + #[schemars(extend("x-cfg" = "linux"))] pub enable_tun_iouring: bool, // Any value more than 1024 negatively impact the throughput #[patch(attribute(clap(long)))] #[patch(attribute(doc = r#"IO-uring submission queue count. Only applicable when `enable_tun_iouring` is `true`"#))] + #[schemars(extend("x-cfg" = "linux"))] pub iouring_entry_count: usize, #[patch(attribute(clap(long)))] #[patch(attribute(doc = r#"IO-uring sqpoll idle time. If non-zero use a kernel thread to perform submission queue polling. After the given idle time the thread will go to sleep."#))] + #[schemars(extend("x-cfg" = "linux"))] #[schemars(schema_with = "lightway_app_utils::args::duration_schema")] + /// ex: 100ms pub iouring_sqpoll_idle_time: Duration, #[patch(attribute(clap(short, long)))] @@ -244,11 +270,13 @@ pub struct Config { Only used if a codec is set"#) )] #[serde(alias = "enable_inside_pkt_encoding_at_connect")] + #[schemars(extend("x-cfg" = "desktop"))] pub enable_inside_pkt_encoding: bool, #[cfg(feature = "debug")] #[patch(attribute(clap(long)))] #[patch(attribute(doc = "File path to save wireshark keylog"))] + #[schemars(extend("x-cfg" = "desktop"))] pub keylog: Option, #[cfg(feature = "debug")] @@ -256,6 +284,7 @@ pub struct Config { #[patch(empty_value = false)] #[patch(attribute(serde(default)))] #[patch(attribute(doc = "Enable TLS debug logging"))] + #[schemars(extend("x-cfg" = "desktop"))] pub tls_debug: bool, #[cfg(windows)] @@ -274,11 +303,13 @@ pub struct Config { #[patch(attribute(serde(default)))] #[patch(attribute(doc = r#"Enable DPAPI encryption/decryption for config file Only for Windows platform"#))] + #[schemars(extend("x-cfg" = "windows"))] pub enable_dpapi: bool, /// SNI header for TLS connections #[cfg(feature = "mobile")] #[patch(attribute(clap(skip)))] + #[schemars(extend("x-cfg" = "mobile"))] pub sni_header: String, } @@ -308,45 +339,6 @@ impl Config { .expect("the path already initialized if it is none"), ) } - - #[cfg(feature = "mobile")] - pub fn apply_mobile_config(&mut self, config: MobileConfig) { - match config.dns_ip.parse::() { - Ok(ip) => self.tun_dns_ip = ip, - Err(e) => tracing::warn!("dns ip is invalid: {e}. Using default."), - } - - match config.local_ip.parse::() { - Ok(ip) => self.tun_local_ip = ip, - Err(e) => tracing::warn!("local ip is invalid: {e}. Using default."), - } - - if let Ok(defer_interval) = u64::try_from(config.defer_timeout) { - self.preferred_connection_wait_interval = - Duration::from_std_duration(StdDuration::from_millis(defer_interval)); - } else { - tracing::warn!( - "Defer timeout value {} is invalid. Using default value.", - config.defer_timeout - ); - } - - let MobileConfig { - enable_heart_beat, - enable_expresslane, - sni_header, - .. - } = config; - - self.keepalive_continuous = enable_heart_beat; - self.enable_expresslane = enable_expresslane; - self.sni_header = sni_header - } - - #[cfg(feature = "mobile")] - pub fn apply_mobile_connect_configs(&mut self, configs: Vec) { - self.servers = configs.into_iter().map(|c| c.into()).collect() - } } impl Default for Config { @@ -437,7 +429,7 @@ pub struct ConnectionConfig { /// Username for User/Pass Auth #[serde(default)] - pub username: Option, + pub user: Option, /// Passwordfor User/Pass Auth #[serde(default)] @@ -460,11 +452,7 @@ impl ConnectionConfig { /// Try build auth from config #[cfg(feature = "mobile")] pub fn take_auth(&mut self) -> Result { - take_auth( - self.token.take(), - self.username.take(), - self.password.take(), - ) + take_auth(self.token.take(), self.user.take(), self.password.take()) } /// Try build CA from ca_crt @@ -510,135 +498,6 @@ pub enum Error { InsufficientAuth, } -#[cfg(any(feature = "mobile", feature = "mobile-test"))] -#[derive(Debug)] -#[cfg_attr( - all(feature = "mobile", not(feature = "mobile-test")), - derive(uniffi::Record) -)] -/// User settings from the mobile app, it is a small set config and easier to use in mobile -pub struct MobileConfig { - /// Enable continuous/NAT keep-alive - pub enable_heart_beat: bool, - /// SNI header for TLS connections - pub sni_header: String, - /// Defer timeout in milliseconds - defer_timeout: i64, - /// Enable Expresslane for UDP connections - pub enable_expresslane: bool, - /// The local ip for TUN interface in ipv4 format - pub local_ip: String, - /// The dns ip in ipv4 format - pub dns_ip: String, -} - -/// Endpoint settings from the mobile app, it is a small set and easier to use in mobile -#[cfg(feature = "mobile")] -#[derive(Clone)] -#[cfg_attr( - all(feature = "mobile", not(feature = "mobile-test")), - derive(uniffi::Record) -)] -pub struct MobileConnectionConfig { - pub server_ip: IpAddress, - pub port: u16, - pub server_dn: String, - pub username: Option, - pub password: Option, - pub auth_token: Option, - pub ca_cert: String, - pub use_tcp: bool, - pub use_cha_cha_20: bool, - pub outside_mtu: u32, -} - -#[cfg(feature = "mobile")] -impl From for ConnectionConfig { - fn from( - MobileConnectionConfig { - server_ip, - port, - server_dn, - username, - password, - auth_token, - ca_cert, - use_tcp, - use_cha_cha_20, - outside_mtu, - }: MobileConnectionConfig, - ) -> ConnectionConfig { - ConnectionConfig { - server: format!("{}:{}", server_ip.ip, port), - mode: if use_tcp { - ConnectionType::Tcp - } else { - ConnectionType::Udp - }, - server_dn: if server_dn.trim().is_empty() { - None - } else { - Some(server_dn) - }, - cipher: if use_cha_cha_20 { - Cipher::Chacha20 - } else { - Cipher::default() - }, - username, - password, - token: auth_token, - outside_mtu: outside_mtu as usize, - ca_cert: if ca_cert.trim().is_empty() { - None - } else { - Some(ca_cert) - }, - } - } -} - -// Ref: https://mozilla.github.io/uniffi-rs/0.27/proc_macro/index.html#the-unifficustom_type-and-unifficustom_newtype-macros -#[cfg(all(feature = "mobile", not(feature = "mobile-test")))] -uniffi::custom_type!(IpAddress, String); - -#[cfg(feature = "mobile")] -#[derive(Debug, Eq, PartialEq, Clone)] -/// Custom type with `String` as the `Builtin` bridge -pub struct IpAddress { - pub ip: IpAddr, -} - -#[cfg(feature = "mobile")] -impl std::fmt::Display for IpAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.ip) - } -} - -#[cfg(all(feature = "mobile", not(feature = "mobile-test")))] -impl crate::UniffiCustomTypeConverter for IpAddress { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - use std::str::FromStr; - Ok(IpAddress { - ip: IpAddr::from_str(val.as_str())?, - }) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.ip.to_string() - } -} - -#[cfg(feature = "mobile")] -impl From for IpAddr { - fn from(val: IpAddress) -> Self { - val.ip - } -} - fn take_auth( token: Option, user: Option, diff --git a/lightway-client/src/lib.rs b/lightway-client/src/lib.rs index 12d72db9..ac3403dd 100644 --- a/lightway-client/src/lib.rs +++ b/lightway-client/src/lib.rs @@ -114,6 +114,8 @@ pub enum LightwayError { Unauthorized, #[error("Config Error: `{0}`")] ConfigError(#[from] crate::config::Error), + #[error("Config Format Error: `{0}`")] + ConfigFormatError(#[from] serde_saphyr::Error), #[cfg(feature = "mobile")] #[error("Logging bridge initialization error: `{0}`")] diff --git a/lightway-client/src/mobile.rs b/lightway-client/src/mobile.rs index 87a9f648..33c78e97 100644 --- a/lightway-client/src/mobile.rs +++ b/lightway-client/src/mobile.rs @@ -3,6 +3,7 @@ pub(crate) mod lightway; pub(crate) mod tracing_utils; use std::sync::{Arc, OnceLock}; +use struct_patch::Patch; use tracing::info; #[derive(Debug, PartialEq, Eq)] @@ -35,7 +36,7 @@ impl TryFrom for ExpresslaneState { #[cfg_attr(not(feature = "mobile-test"), uniffi::export(with_foreign))] #[cfg_attr(test, mockall::automock)] -pub trait RustEventHandlers: Send + Sync { +pub trait EventHandlers: Send + Sync { /// Handles VPN connection status changes from the native Lightway client. /// State values: 2=Connecting, 6=LinkUp, 5=Authenticating, 7=Online, 4=Disconnecting, 1=Disconnected (from lightway-core) /// @@ -144,33 +145,32 @@ impl RustVpnConnection { /// `LightwayError` for proper error handling. fn parallel_connect( &self, - endpoints: Vec, - event_handler: Arc, + event_handler: Arc, raw_tun_fd: i32, - mobile_config: crate::config::MobileConfig, + config_content: String, ) -> Result { info!("start parallel Lightway connections"); let mut config = crate::config::Config::default(); - config.apply_mobile_config(mobile_config); - info!("Received {} endpoints", endpoints.len()); - if endpoints.is_empty() { + config.apply(serde_saphyr::from_str(&config_content)?); + config.servers.push(crate::config::ConnectionConfig { + server: config.server.clone(), + mode: config.mode, + server_dn: config.server_dn.take(), + cipher: config.cipher, + outside_mtu: config.outside_mtu, + user: config.user.take(), + password: config.password.take(), + token: config.token.take(), + ca_cert: Some(config.ca_cert.clone()), + }); + + info!("Received {} endpoints", config.servers.len()); + if config.servers.is_empty() { return Err(LightwayError::EmptyEndpointsError); } - for endpoint in &endpoints { - info!( - "Endpoint {}:{} with {}", - endpoint.server_ip, - endpoint.port, - if endpoint.use_tcp { - "lightway_tcp" - } else { - "lightway_udp" - }, - ); - } - config.apply_mobile_connect_configs(endpoints); + tracing::debug!("Config:\n{config:#?}"); let mut builder = tokio::runtime::Builder::new_current_thread(); builder diff --git a/lightway-client/src/mobile/lightway.rs b/lightway-client/src/mobile/lightway.rs index 1904e193..3caf987c 100644 --- a/lightway-client/src/mobile/lightway.rs +++ b/lightway-client/src/mobile/lightway.rs @@ -1,7 +1,7 @@ use crate::config::{Config, ConnectionConfig}; use crate::io::outside::OutsideIO; use crate::keepalive::{Keepalive, KeepaliveResult}; -use crate::mobile::RustEventHandlers; +use crate::mobile::EventHandlers; use crate::mobile::{DeviceNetworkState, ExpresslaneState}; use crate::{ ClientIpConfigCb, ClientResult, ConnectionState, inside_io_task, io, @@ -45,10 +45,7 @@ enum OutsideSocket { } impl OutsideSocket { - fn new( - use_tcp: bool, - event_handler: Option>, - ) -> uniffi::Result { + fn new(use_tcp: bool, event_handler: Option>) -> uniffi::Result { if use_tcp { let socket = TcpSocket::new_v4()?; let fd = socket.as_raw_fd(); @@ -177,7 +174,7 @@ async fn setup_tunnel_interface( pub(crate) async fn async_lightway_start( tun_fd: RawFd, - external_event_handler: Arc, + external_event_handler: Arc, config: Config, connected_index: Arc>, ) -> uniffi::Result { @@ -445,7 +442,7 @@ async fn restartable_outside_io_task( keepalive: Keepalive, notify_keepalive_reply: Arc, mut new_outside_io_receiver: MpscReceiver<()>, - external_event_handler: Arc, + external_event_handler: Arc, ) -> uniffi::Result<()> { let mut current_outside_io = outside_io_config.outside_io; let mut first_run = true; @@ -563,7 +560,7 @@ struct LightwayClientConnectArgs { expresslane_keys_rotation_interval: std::time::Duration, online_signal_sender: tokio::sync::mpsc::Sender, event_stream_handler: EventStreamCallback, - external_event_handler: Arc, + external_event_handler: Arc, } /// Individual connection to a lightway server @@ -728,7 +725,7 @@ async fn lightway_client_connect( async fn handle_global_events( mut stream: EventStream, connection_start_time: Instant, - event_handler: Arc, + event_handler: Arc, ) { let mut current_state = State::Connecting; let mut is_first_packet_received = false; @@ -890,7 +887,7 @@ async fn handle_network_change( #[cfg(test)] mod test { use super::*; - use crate::mobile::MockRustEventHandlers; + use crate::mobile::MockEventHandlers; use mockall::Sequence; use mockall::predicate::eq; @@ -907,7 +904,7 @@ mod test { // Make sure we don't advertise Online state in this function let mut seq = Sequence::new(); - let mut mock_event_handler = MockRustEventHandlers::new(); + let mut mock_event_handler = MockEventHandlers::new(); mock_event_handler .expect_handle_status_change() .times(1) @@ -941,7 +938,7 @@ mod test { sender.event(Event::StateChanged(State::Disconnected)); }); - let mut mock_event_handler = MockRustEventHandlers::new(); + let mut mock_event_handler = MockEventHandlers::new(); mock_event_handler .expect_handle_status_change() .times(0) @@ -961,7 +958,7 @@ mod test { sender.event(Event::FirstPacketReceived); }); - let mut mock_event_handler = MockRustEventHandlers::new(); + let mut mock_event_handler = MockEventHandlers::new(); mock_event_handler .expect_received_first_packet() .with(eq(174u64)) @@ -973,7 +970,7 @@ mod test { #[tokio::test] async fn test_outside_socket_new_calls_created_outside_fd() { // Test TCP socket creation - let mut mock_event_handler = MockRustEventHandlers::new(); + let mut mock_event_handler = MockEventHandlers::new(); mock_event_handler .expect_created_outside_fd() @@ -984,7 +981,7 @@ mod test { assert!(tcp_result.is_ok()); // Test UDP socket creation - let mut mock_event_handler = MockRustEventHandlers::new(); + let mut mock_event_handler = MockEventHandlers::new(); mock_event_handler .expect_created_outside_fd() diff --git a/lightway-client/src/platform/windows/crypto.rs b/lightway-client/src/platform/windows/crypto.rs index 704f4444..82d56b23 100644 --- a/lightway-client/src/platform/windows/crypto.rs +++ b/lightway-client/src/platform/windows/crypto.rs @@ -19,14 +19,14 @@ mod tests { #[derive(Debug, Serialize, PartialEq)] struct TestConfig { server: String, - username: String, + user: String, password: String, } fn generate_mock_config() -> String { let config = TestConfig { server: "vpn.example.com".to_string(), - username: "user1".to_string(), + user: "user1".to_string(), password: "securepassword".to_string(), }; serde_saphyr::to_string(&config).unwrap() diff --git a/nix/modules/devshells.nix b/nix/modules/devshells.nix index 54b83ada..3448e77b 100644 --- a/nix/modules/devshells.nix +++ b/nix/modules/devshells.nix @@ -39,9 +39,11 @@ }; androidSdk = androidComposition.androidsdk; buildScript = pkgs.writeShellScriptBin "build" '' + cd "$(git rev-parse --show-toplevel 2>/dev/null)" cargo make build-android ''; cleanScript = pkgs.writeShellScriptBin "clean" '' + cd "$(git rev-parse --show-toplevel 2>/dev/null)" cargo make clean-android ''; pinned-cargo-ndk = pkgs.callPackage ../pkgs/cargo-ndk.nix { };