Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/connection_setting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Connection Setting

Multiple VPN connections are supported via the `servers` field (`Vec<ConnectionConfig>`). At the same time, Config is designed to work equally well with a single server connection in a straightforward and energy-efficient way — as is the case with the current android client implementation.
It is also common to share the same auth across multiple connections within a group of servers. The top-level fields in Config outside of servers are sufficient to set up a single connection, but this introduces complexity when the code needs to handle both single and multiple connection cases.
To address this, `Config` provides three helper methods that abstract over the `servers` field:

- `.len()` and `.is_empty()` — simple helpers to inspect how many servers are configured.
- `.take_servers()` — extracts the servers from the config, normalizing by promoting the top-level connection config into a single entry in servers if needed, and filling in any missing auth or certificate information in the process.

This makes it possible to specify a single auth or certificate at the top level and have it applied across all connections automatically.
The network initialization flow remains clean and consistent across clients, as illustrated in the following pseudo code. Note that the actual implementation differs due to asynchronous execution.
```rust
#[cfg(platform)]
fn client(mut config: Config) {
let servers = config.take_servers();

for server in servers.into_iter() {
connect(server);
}
}
```
2 changes: 2 additions & 0 deletions docs/design_overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ desktop or an iPhone.

lightway-client is a Linux implementation for a fully working Lightway client with both TCP and UDP support.

All clients possible work with mulitple server connections with different auth settings, and it still possible to set one auth for all connection, please refer [Connection Setting.](/docs/connection_setting.md)

## lightway-server

lightway-server is a Linux implementation for a fully working Lightway server with both TCP and UDP support.
Expand Down
104 changes: 75 additions & 29 deletions lightway-client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use lightway_app_utils::args::KeyShare;
use lightway_app_utils::args::{
Cipher, ConfigFormat, ConnectionType, Duration, LogLevel, NonZeroDuration,
};
use lightway_core::{AuthMethod, MAX_OUTSIDE_MTU, RootCertificate};
use lightway_core::{AuthMethod, MAX_OUTSIDE_MTU};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::time::Duration as StdDuration;
Expand Down Expand Up @@ -46,7 +46,7 @@ pub struct Config {
#[serde(default)]
#[serde(skip_serializing)]
#[schemars(skip)]
pub servers: Vec<ConnectionConfig>,
servers: Vec<ConnectionConfig>,

#[patch(attribute(clap(short, long)))]
#[patch(attribute(doc = r#"Server to connect to in `<hostname>:<port>` format
Expand Down Expand Up @@ -314,30 +314,69 @@ pub struct Config {
}

impl Config {
/// Try build auth from config
pub fn take_auth(&mut self) -> Result<AuthMethod, Error> {
take_auth(self.token.take(), self.user.take(), self.password.take())
/// The number of servers
pub fn len(&self) -> usize {
if self.server.is_empty() {
self.servers.len()
} else {
self.servers.len() + 1
}
}

/// Try build CA from ca_crt
pub fn load_ca(&self) -> Result<lightway_core::tls::RootCertificate<'_>, Error> {
load_ca(&self.ca_cert)
/// Check if there is any server setting
pub fn is_empty(&self) -> bool {
self.servers.is_empty() && self.server.is_empty()
}

/// Try build CA from ca_crt file
/// If input ca_path is none, will take ca_cert field as path and pass to SSL
pub fn load_ca_file<'a>(
&self,
ca_path: &'a mut Option<PathBuf>,
) -> lightway_core::tls::RootCertificate<'a> {
if ca_path.is_none() {
*ca_path = Some(PathBuf::from(&self.ca_cert));
/// Take out configures for servers
/// and normalize the auth and ca of ConnectionConfig of servers
pub fn take_servers(&mut self) -> Result<Vec<ConnectionConfig>, Error> {
if self.servers.is_empty() {
self.servers = vec![ConnectionConfig {
server: self.server.clone(),
mode: self.mode,
server_dn: self.server_dn.take(),
cipher: self.cipher,
outside_mtu: self.outside_mtu,
..Default::default()
}];
}
let ca_content = if check_cert_header(&self.ca_cert) {
Some(self.ca_cert.clone())
} else {
// NOTE: we support a path input on desktop, we keep None if global cert is not set, and
// it is okay to use certs for each server, thees will be checked in following loop.
if cfg!(desktop) {
std::fs::read_to_string(&self.ca_cert).ok()
} else {
None
}
};
Comment on lines +344 to +354

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is repeated in a different way below. I think we can keep the load_ca(cert_or_path) -> Result<cert> and put the logic in there

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a little different between these, so we can not make a load_ca for the Config and also for the ConnectionConfig.

We accept the global config without CA, so the std::fs::read_to_string(&self.ca_cert).ok() and for the connection level the error should raise *ca_cert = std::fs::read_to_string(&mut *ca_cert).map_err(|_| Error::CaFileNotFound)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More details note added to point out the difference.

for server in self.servers.iter_mut() {
if server.user.is_none() && server.password.is_none() && server.token.is_none() {
server.user = self.user.clone();
server.password = self.password.clone();
server.token = self.token.clone();
}

if let Some(ref mut ca_cert) = server.ca_cert {
if !check_cert_header(&ca_cert) {
// NOTE: we support a path input on desktop, but raise error if not found
if cfg!(desktop) {
*ca_cert = std::fs::read_to_string(&mut *ca_cert)
.map_err(|_| Error::CaFileNotFound)?;
} else {
return Err(Error::InvalidCertificate);
}
}
} else {
if ca_content.is_none() {
return Err(Error::CaFileNotFound);
}
server.ca_cert = ca_content.clone();
}
}
RootCertificate::PemFileOrDirectory(
ca_path
.as_ref()
.expect("the path already initialized if it is none"),
)
Ok(std::mem::take::<Vec<ConnectionConfig>>(&mut self.servers))
}
}

Expand Down Expand Up @@ -450,7 +489,6 @@ pub struct ConnectionConfig {

impl ConnectionConfig {
/// Try build auth from config
#[cfg(feature = "mobile")]
pub fn take_auth(&mut self) -> Result<AuthMethod, Error> {
take_auth(self.token.take(), self.user.take(), self.password.take())
}
Expand All @@ -460,7 +498,15 @@ impl ConnectionConfig {
pub fn load_ca(&self) -> Result<lightway_core::tls::RootCertificate<'_>, Error> {
self.ca_cert
.as_ref()
.map(load_ca)
.map(|ca| {
if check_cert_header(ca) {
Ok(lightway_core::tls::RootCertificate::PemBuffer(
ca.as_bytes(),
))
} else {
Err(Error::InvalidCertificate)
}
})
.ok_or(Error::InvalidCertificate)?
}

Expand Down Expand Up @@ -496,6 +542,10 @@ pub enum Error {
/// Lack information to Authentication
#[error("Insufficient information for Authentication")]
InsufficientAuth,

/// No such Ca file from user's input
#[error("Ca file is absent")]
CaFileNotFound,
}

fn take_auth(
Expand All @@ -510,12 +560,8 @@ fn take_auth(
}
}

fn load_ca(ca: &String) -> Result<lightway_core::tls::RootCertificate<'_>, Error> {
if ca.starts_with("-----BEGIN CERTIFICATE-----") {
Ok(RootCertificate::PemBuffer(ca.as_bytes()))
} else {
Err(Error::InvalidCertificate)
}
fn check_cert_header<T: AsRef<str>>(content: T) -> bool {
content.as_ref().starts_with("-----BEGIN CERTIFICATE-----")
}

fn byte_size_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
Expand Down
82 changes: 45 additions & 37 deletions lightway-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,7 @@ pub struct BestConnectionInfo {

#[derive(educe::Educe)]
#[educe(Debug)]
pub struct ClientConfig<'cert, ExtAppState: Send + Sync> {
/// Auth parameters to use for connection
#[educe(Debug(ignore))]
pub auth: AuthMethod,

/// CA certificate
#[educe(Debug(ignore))]
pub root_ca_cert: RootCertificate<'cert>,

pub struct ClientConfig<ExtAppState: Send + Sync> {
/// Outside (wire) MTU
pub outside_mtu: usize,

Expand Down Expand Up @@ -290,6 +282,12 @@ pub struct ClientConnectionConfig<EventHandler: 'static + Send + EventCallback>
/// Server IP address and port
pub server: SocketAddr,

/// Auth parameters to use for connection
pub auth: AuthMethod,

/// Content of CA certificate
pub cert_content: String,

/// Inside plugins to use
#[educe(Debug(method(debug_fmt_plugin_list)))]
pub inside_plugins: PluginFactoryList,
Expand Down Expand Up @@ -804,17 +802,29 @@ pub async fn connect<
EventHandler: 'static + Send + EventCallback,
ExtAppState: 'static + Default + Send + Sync,
>(
config: &ClientConfig<'_, ExtAppState>,
mut server_config: ClientConnectionConfig<EventHandler>,
config: &ClientConfig<ExtAppState>,
server_config: ClientConnectionConfig<EventHandler>,
inside_io: Arc<dyn io::inside::InsideIO<ExtAppState>>,
) -> Result<ClientConnection<ExtAppState>> {
let mut join_set = JoinSet::new();
let ClientConnectionConfig {
mode,
cipher,
server,
server_dn,
auth,
cert_content,
inside_pkt_codec,
inside_plugins,
outside_plugins,
event_handler,
} = server_config;

let (connection_type, outside_io): (ConnectionType, Arc<dyn io::outside::OutsideIO>) =
match server_config.mode {
match mode {
ClientConnectionMode::Datagram(maybe_sock) => {
#[cfg_attr(not(batch_receive), allow(unused_mut))]
let mut sock = io::outside::Udp::new(server_config.server, maybe_sock)
let mut sock = io::outside::Udp::new(server, maybe_sock)
.await
.inspect_err(|e| tracing::error!("Failed to create outside IO UDP socket: {e}"))
.context("Outside IO UDP")?;
Expand All @@ -827,7 +837,7 @@ pub async fn connect<
(ConnectionType::Datagram, Arc::new(sock))
}
ClientConnectionMode::Stream(maybe_sock) => {
let sock = io::outside::Tcp::new(server_config.server, maybe_sock)
let sock = io::outside::Tcp::new(server, maybe_sock)
.await
.inspect_err(|e| tracing::error!("Failed to create outside IO TCP socket: {e}"))
.context("Outside IO TCP")?;
Expand All @@ -853,29 +863,28 @@ pub async fn connect<
set_logging_callback(|m: &str| tracing::debug!(target: "ssl_debug", m));
}

let (inside_io_codec, encoded_pkt_receiver, decoded_pkt_receiver) =
match &server_config.inside_pkt_codec {
Some(codec_factory) => {
let codec = codec_factory.build();
(
Some((codec.encoder, codec.decoder)),
Some(codec.encoded_pkt_receiver),
Some(codec.decoded_pkt_receiver),
)
}
None => (None, None, None),
};
let (inside_io_codec, encoded_pkt_receiver, decoded_pkt_receiver) = match &inside_pkt_codec {
Some(codec_factory) => {
let codec = codec_factory.build();
(
Some((codec.encoder, codec.decoder)),
Some(codec.encoded_pkt_receiver),
Some(codec.decoded_pkt_receiver),
)
}
None => (None, None, None),
};

let conn_builder = ClientContextBuilder::new(
connection_type,
config.root_ca_cert,
RootCertificate::PemBuffer(cert_content.as_bytes()),
None,
Arc::new(ClientIpConfigCb),
connection_ticker_cb,
)?
.with_cipher(server_config.cipher.into())?
.with_inside_plugins(server_config.inside_plugins)
.with_outside_plugins(server_config.outside_plugins)
.with_cipher(cipher.into())?
.with_inside_plugins(inside_plugins)
.with_outside_plugins(outside_plugins)
.when(config.enable_expresslane, |b| {
b.with_expresslane(config.expresslane_keys_rotation_interval)
})
Expand All @@ -890,11 +899,11 @@ pub async fn connect<
outside_io.clone().into_io_send_callback(),
config.outside_mtu,
)?
.with_auth(config.auth.clone())
.with_auth(auth)
.with_event_cb(Box::new(event_cb))
.with_inside_pkt_codec(inside_io_codec)
.when_some(config.pmtud_base_mtu, |b, mtu| b.with_pmtud_base_mtu(mtu))
.when_some(server_config.server_dn, |b, sdn| {
.when_some(server_dn, |b, sdn| {
b.with_server_domain_name_validation(&sdn)
})
.when(connection_type.is_datagram() && config.enable_pmtud, |b| {
Expand Down Expand Up @@ -923,7 +932,6 @@ pub async fn connect<
let (connected_tx, connected_rx) = oneshot::channel();
let (disconnected_tx, disconnected_rx) = oneshot::channel();

let event_handler = server_config.event_handler.take();
join_set.spawn(handle_events(
event_stream,
keepalive.clone(),
Expand Down Expand Up @@ -994,8 +1002,8 @@ pub async fn connect<
Some(_) = keepalive_task => Err(anyhow!("Keepalive timeout")),
io = &mut outside_io_loop => Err(anyhow!("Outside IO loop exited: {io:?}")),
io = &mut inside_io_loop => Err(anyhow!("Inside IO loop exited: {io:?}")),
io = &mut encoded_pkt_send_task, if server_config.inside_pkt_codec.is_some() => Err(anyhow!("Inside IO (Encoded packet send task) exited: {io:?}")),
io = &mut decoded_pkt_send_task, if server_config.inside_pkt_codec.is_some() => Err(anyhow!("Inside IO (Decoded packet send task) exited: {io:?}")),
io = &mut encoded_pkt_send_task, if inside_pkt_codec.is_some() => Err(anyhow!("Inside IO (Encoded packet send task) exited: {io:?}")),
io = &mut decoded_pkt_send_task, if inside_pkt_codec.is_some() => Err(anyhow!("Inside IO (Decoded packet send task) exited: {io:?}")),
_ = &mut ticker_task => Err(anyhow!("Ticker task stopped")),
result = &mut network_change_task => {
match result {
Expand Down Expand Up @@ -1172,7 +1180,7 @@ fn validate_client_config<
EventHandler: 'static + Send + EventCallback,
ExtAppState: Send + Sync,
>(
config: &ClientConfig<'_, ExtAppState>,
config: &ClientConfig<ExtAppState>,
servers: &[ClientConnectionConfig<EventHandler>],
) -> Result<()> {
if config.network_change_signal.is_some() && config.keepalive_interval.is_zero() {
Expand Down Expand Up @@ -1208,7 +1216,7 @@ pub async fn client<
EventHandler: 'static + Send + EventCallback,
ExtAppState: 'static + Default + Send + Sync,
>(
mut config: ClientConfig<'_, ExtAppState>,
mut config: ClientConfig<ExtAppState>,
mut stop_signal: oneshot::Receiver<()>,
conn_confs: Vec<ClientConnectionConfig<EventHandler>>,
) -> Result<ClientResult> {
Expand Down
Loading
Loading