Pull request 78: Add command to generate client's config

Squashed commit of the following:

commit d7c1d780d9b58a9108330fb37f7278f01397dc2f
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Tue Jul 22 22:40:56 2025 +0400

    Fix docs for certificate

commit 045c8d3170f825335cca084b9f0b46e2b0e99553
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Tue Jul 22 22:39:59 2025 +0400

    Fulfill the generated config with all remaing fields from [endpoint] config section

commit 1ab3271c0785a75fc89ec4f7f9bd214516d3d16d
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Tue Jul 22 22:30:10 2025 +0400

    Fix has_ipv6 description

commit 21f138edca65d7aa1606881d58789a51790f601c
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Tue Jul 22 19:08:34 2025 +0400

    Place has_ipv6 after addresses

commit 0b520c398cede67557fbb2669d0d46e8daaf5823
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Tue Jul 22 19:01:58 2025 +0400

    Add has_ipv6 field to client's config

commit 03c692e63f2d6a91f4ccd332627f44eb00f6066a
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Mon Jun 16 14:38:40 2025 +0400

    Do not create authenticator if there are no clients

commit e16d2de1063dfadc244c0605bddecbcd56e55514
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Jun 5 18:45:10 2025 +0400

    Introduce client's config generator

commit bb83e046c1ae71fca63033c515152780be4412ba
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Jun 5 17:08:01 2025 +0400

    Move ToTomlComment trait to utils

commit a170584d76684c8960c218146e7a10672a373863
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Jun 5 17:07:01 2025 +0400

    Move authenticator out of settings
This commit is contained in:
Andrey Yakushin
2025-07-28 11:36:53 +03:00
parent 8e018cd56a
commit b7a95f5a71
12 changed files with 209 additions and 53 deletions

View File

@@ -1,10 +1,14 @@
use std::sync::Arc;
use log::{error, info, LevelFilter};
use tokio::signal;
use vpn_libs_endpoint::authentication::registry_based::RegistryBasedAuthenticator;
use vpn_libs_endpoint::authentication::Authenticator;
use vpn_libs_endpoint::core::Core;
use vpn_libs_endpoint::{log_utils, settings};
use vpn_libs_endpoint::settings::Settings;
use vpn_libs_endpoint::shutdown::Shutdown;
use vpn_libs_endpoint::client_config;
const VERSION_STRING: &str = env!("CARGO_PKG_VERSION");
@@ -13,6 +17,8 @@ const LOG_LEVEL_PARAM_NAME: &str = "log_level";
const LOG_FILE_PARAM_NAME: &str = "log_file";
const SETTINGS_PARAM_NAME: &str = "settings";
const TLS_HOSTS_SETTINGS_PARAM_NAME: &str = "tls_hosts_settings";
const CLIENT_CONFIG_PARAM_NAME: &str = "client_config";
const ADDRESS_PARAM_NAME: &str = "address";
const SENTRY_DSN_PARAM_NAME: &str = "sentry_dsn";
const THREADS_NUM_PARAM_NAME: &str = "threads_num";
@@ -56,6 +62,19 @@ fn main() {
.action(clap::ArgAction::Set)
.required_unless_present(VERSION_PARAM_NAME)
.help("Path to a file containing TLS hosts settings. Sending SIGHUP to the process causes reloading the settings."),
clap::Arg::new(CLIENT_CONFIG_PARAM_NAME)
.action(clap::ArgAction::Set)
.requires(ADDRESS_PARAM_NAME)
.short('c')
.long("client_config")
.value_names(["client_name"])
.help("Print the endpoint config for specified client and exit."),
clap::Arg::new(ADDRESS_PARAM_NAME)
.action(clap::ArgAction::Append)
.requires(CLIENT_CONFIG_PARAM_NAME)
.short('a')
.long("address")
.help("Endpoint address to be added to client's config.")
])
.disable_version_flag(true)
.get_matches();
@@ -105,6 +124,18 @@ fn main() {
.expect("Couldn't read the TLS hosts settings file")
).expect("Couldn't parse the TLS hosts settings file");
if args.contains_id(CLIENT_CONFIG_PARAM_NAME) {
let username = args.get_one::<String>(CLIENT_CONFIG_PARAM_NAME).unwrap();
let addresses: Vec<String> = args.get_many::<String>(ADDRESS_PARAM_NAME)
.map(Iterator::cloned)
.map(Iterator::collect)
.expect("Addresses should be specified");
let client_config = client_config::build(&username, &addresses, settings.get_clients(), &tls_hosts_settings);
println!("{}", client_config.compose_toml());
return;
}
let rt = {
let mut builder = tokio::runtime::Builder::new_multi_thread();
builder.enable_io();
@@ -119,8 +150,13 @@ fn main() {
};
let shutdown = Shutdown::new();
let authenticator: Option<Arc<dyn Authenticator>> = if !settings.get_clients().is_empty() {
Some(Arc::new(RegistryBasedAuthenticator::new(settings.get_clients())))
} else {
None
};
let core = Arc::new(Core::new(
settings, tls_hosts_settings, shutdown.clone(),
settings, authenticator, tls_hosts_settings, shutdown.clone(),
).expect("Couldn't create core instance"));
let listen_task = {

View File

@@ -23,11 +23,10 @@ pub struct RegistryBasedAuthenticator {
}
impl RegistryBasedAuthenticator {
pub fn new<I>(clients: I) -> Self
where I: Iterator<Item=Client>
pub fn new(clients: &Vec<Client>) -> Self
{
Self {
clients: clients
clients: clients.iter()
.map(|x| BASE64_ENGINE.encode(format!("{}:{}", x.username, x.password)))
.map(Cow::Owned)
.collect(),

118
lib/src/client_config.rs Normal file
View File

@@ -0,0 +1,118 @@
#[cfg(feature = "rt_doc")]
use macros::{Getter, RuntimeDoc};
use once_cell::sync::Lazy;
use toml_edit::{value, Document};
use crate::{authentication::registry_based, settings::TlsHostsSettings, utils::ToTomlComment};
pub fn build(client: &String, addresses: &Vec<String>, username: &Vec<registry_based::Client>, hostsettings: &TlsHostsSettings) -> ClientConfig {
let user = username.iter().find(|x| {
x.username == *client
}) .expect("There is no user config for specified username");
let host = hostsettings.main_hosts.first().expect("Can't find main host inside hosts config");
ClientConfig {
hostname: host.hostname.clone(),
addresses: addresses.clone(),
has_ipv6: true, // Hardcoded to true, client could change this himself
username: user.username.clone(),
password: user.password.clone(),
skip_verification: false,
certificate: std::fs::read_to_string(&host.cert_chain_path)
.expect("Failed to load certificate"),
upstream_protocol: "http2".into(),
upstream_fallback_protocol: "".into(),
anti_dpi: false,
}
}
#[cfg_attr(feature = "rt_doc", derive(Getter, RuntimeDoc))]
pub struct ClientConfig {
/// Endpoint host name, used for TLS session establishment
hostname: String,
/// Endpoint addresses.
addresses: Vec<String>,
/// Whether IPv6 traffic can be routed through the endpoint
has_ipv6: bool,
/// Username for authorization
username: String,
/// Password for authorization
password: String,
/// Skip the endpoint certificate verification?
/// That is, any certificate is accepted with this one set to true.
skip_verification: bool,
/// Endpoint certificate in PEM format.
/// If not specified, the endpoint certificate is verified using the system storage.
certificate: String,
/// Protocol to be used to communicate with the endpoint [http2, http3]
upstream_protocol: String,
/// Fallback protocol to be used in case the main one fails [<none>, http2, http3]
upstream_fallback_protocol: String,
/// Is anti-DPI measures should be enabled
anti_dpi: bool
}
impl ClientConfig {
pub fn compose_toml(&self) -> String {
let mut doc: Document = TEMPLATE.parse().unwrap();
doc["hostname"] = value(&self.hostname);
let vec = toml_edit::Array::from_iter(self.addresses.iter());
doc["addresses"] = value(vec);
doc["has_ipv6"] = value(self.has_ipv6);
doc["username"] = value(&self.username);
doc["password"] = value(&self.password);
doc["skip_verification"] = value(self.skip_verification);
doc["certificate"] = value(&self.certificate);
doc["upstream_protocol"] = value(&self.upstream_protocol);
doc["upstream_fallback_protocol"] = value(&self.upstream_fallback_protocol);
doc["anti_dpi"] = value(self.anti_dpi);
doc.to_string()
}
}
static TEMPLATE: Lazy<String> = Lazy::new(|| format!(
r#"
# This file was automatically generated by endpoint and could be used in vpn client.
{}
hostname = ""
{}
addresses = []
{}
has_ipv6 = true
{}
username = ""
{}
password = ""
{}
skip_verification = false
{}
certificate = ""
{}
upstream_protocol = ""
{}
upstream_fallback_protocol = ""
{}
anti_dpi = false
"#,
ClientConfig::doc_hostname().to_toml_comment(),
ClientConfig::doc_addresses().to_toml_comment(),
ClientConfig::doc_has_ipv6().to_toml_comment(),
ClientConfig::doc_username().to_toml_comment(),
ClientConfig::doc_password().to_toml_comment(),
ClientConfig::doc_skip_verification().to_toml_comment(),
ClientConfig::doc_certificate().to_toml_comment(),
ClientConfig::doc_upstream_protocol().to_toml_comment(),
ClientConfig::doc_upstream_fallback_protocol().to_toml_comment(),
ClientConfig::doc_anti_dpi().to_toml_comment(),
));

View File

@@ -40,6 +40,7 @@ pub struct Core {
pub(crate) struct Context {
pub settings: Arc<Settings>,
pub authenticator: Option<Arc<dyn authentication::Authenticator>>,
tls_demux: Arc<RwLock<TlsDemux>>,
pub icmp_forwarder: Option<Arc<IcmpForwarder>>,
pub shutdown: Arc<Mutex<Shutdown>>,
@@ -51,6 +52,7 @@ pub(crate) struct Context {
impl Core {
pub fn new(
settings: Settings,
authenticator: Option<Arc<dyn authentication::Authenticator>>,
tls_hosts_settings: settings::TlsHostsSettings,
shutdown: Arc<Mutex<Shutdown>>,
) -> Result<Self, Error> {
@@ -66,6 +68,7 @@ impl Core {
Ok(Self {
context: Arc::new(Context {
settings: settings.clone(),
authenticator,
tls_demux: Arc::new(RwLock::new(
TlsDemux::new(&settings, &tls_hosts_settings)
.map_err(|e| Error::TlsDemultiplexer(e.to_string()))?
@@ -390,7 +393,7 @@ impl Core {
) {
let _metrics_guard = Metrics::client_sessions_counter(context.metrics.clone(), protocol);
let authentication_policy = match context.settings.authenticator.as_ref().zip(sni_auth_creds) {
let authentication_policy = match context.authenticator.as_ref().zip(sni_auth_creds) {
None => tunnel::AuthenticationPolicy::Default,
Some((authenticator, credentials)) => {
let auth = authentication::Source::Sni(credentials.into());
@@ -458,6 +461,7 @@ impl Default for Context {
let settings = Arc::new(Settings::default());
Self {
settings: settings.clone(),
authenticator: None,
tls_demux: Arc::new(RwLock::new(
TlsDemux::new(&settings, &settings::TlsHostsSettings::default()).unwrap()
)),

View File

@@ -10,6 +10,7 @@ pub mod log_utils;
pub mod shutdown;
pub mod net_utils;
pub mod utils;
pub mod client_config;
mod direct_forwarder;
mod downstream;

View File

@@ -4,15 +4,13 @@ use std::io;
use std::io::ErrorKind;
use std::net::{Ipv4Addr, SocketAddr, ToSocketAddrs};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
#[cfg(feature = "rt_doc")]
use macros::{Getter, RuntimeDoc};
use serde::{Deserialize, Serialize};
use toml_edit::{Document, Item};
use authentication::registry_based::RegistryBasedAuthenticator;
use crate::authentication::Authenticator;
use authentication::registry_based::Client;
use crate::{authentication, utils};
pub type Socks5BuilderResult<T> = Result<T, Socks5Error>;
@@ -101,6 +99,7 @@ pub struct Settings {
pub(crate) forward_protocol: ForwardProtocolSettings,
/// The set of enabled client listener codecs
pub(crate) listen_protocols: ListenProtocolSettings,
// TODO (ayakushin): fix docs
/// The client authenticator.
///
/// If [forward_protocol](Settings.forward_protocol) is set to
@@ -125,8 +124,8 @@ pub struct Settings {
#[serde(default)]
#[serde(skip_serializing)]
#[serde(rename(deserialize = "credentials_file"))]
#[serde(deserialize_with = "deserialize_authenticator")]
pub(crate) authenticator: Option<Arc<dyn Authenticator>>,
#[serde(deserialize_with = "deserialize_clients")]
pub(crate) clients: Vec<Client>,
/// The reverse proxy settings.
/// With this one set up the endpoint does TLS termination on such connections and
/// translates HTTP/x traffic into HTTP/1.1 protocol towards the server and back
@@ -488,12 +487,12 @@ impl Default for Settings {
tcp_connections_timeout: Settings::default_tcp_connections_timeout(),
udp_connections_timeout: Settings::default_udp_connections_timeout(),
forward_protocol: Default::default(),
clients: Default::default(),
listen_protocols: ListenProtocolSettings {
http1: Some(Http1Settings::builder().build()),
http2: Some(Http2Settings::builder().build()),
quic: Some(QuicSettings::builder().build()),
},
authenticator: None,
reverse_proxy: None,
icmp: None,
metrics: Default::default(),
@@ -729,7 +728,7 @@ impl SettingsBuilder {
udp_connections_timeout: Settings::default_udp_connections_timeout(),
forward_protocol: Default::default(),
listen_protocols: Default::default(),
authenticator: None,
clients: Default::default(),
reverse_proxy: None,
icmp: None,
metrics: Default::default(),
@@ -824,8 +823,8 @@ impl SettingsBuilder {
}
/// Set the client authenticator
pub fn authenticator(mut self, x: Box<dyn Authenticator>) -> Self {
self.settings.authenticator = Some(Arc::from(x));
pub fn clients(mut self, x: Vec<Client>) -> Self {
self.settings.clients = x;
self
}
@@ -1279,7 +1278,7 @@ fn deserialize_file_path<'de, D>(deserializer: D) -> Result<String, D::Error>
deserializer.deserialize_str(Visitor)
}
fn deserialize_authenticator<'de, D>(deserializer: D) -> Result<Option<Arc<dyn Authenticator>>, D::Error>
fn deserialize_clients<'de, D>(deserializer: D) -> Result<Vec<Client>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
@@ -1297,26 +1296,19 @@ fn deserialize_authenticator<'de, D>(deserializer: D) -> Result<Option<Arc<dyn A
&"A TOML-formatted file",
))?;
let mut clients = clients.get("client")
let res = clients.get("client")
.and_then(Item::as_array_of_tables)
.ok_or(serde::de::Error::invalid_value(
serde::de::Unexpected::Other("Not an array of clients"),
&"An array of clients",
))?
.iter()
.map(|x| (authentication::registry_based::Client {
.map(|x| (Client {
username: demangle_toml_string(x["username"].to_string()),
password: demangle_toml_string(x["password"].to_string()),
}))
.peekable();
if clients.peek().is_none() {
return Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Other("Empty client list"),
&"Non-empty client list",
));
}
})).collect();
Ok(Some(Arc::new(RegistryBasedAuthenticator::new(clients))))
Ok(res)
}
fn demangle_toml_string(x: String) -> String {

View File

@@ -120,7 +120,7 @@ impl Tunnel {
let forwarder_auth = match (
auth_info,
authentication_policy,
context.settings.authenticator.clone(),
context.authenticator.clone(),
) {
(Ok(Some(source)), _, Some(authenticator)) =>
match authenticator.authenticate(&source, &log_id) {

View File

@@ -112,6 +112,27 @@ impl<I, T> IterJoin for I
}
}
pub trait ToTomlComment {
/// Prepend each line of string with "# " turning
/// the whole string it into TOML comment.
fn to_toml_comment(&self) -> String;
}
impl ToTomlComment for &str {
fn to_toml_comment(&self) -> String {
self.lines()
.map(|x| format!("# {x}"))
.join("\n")
}
}
impl ToTomlComment for String {
fn to_toml_comment(&self) -> String {
self.as_str().to_toml_comment()
}
}
#[cfg(test)]
mod tests {
use crate::utils::IterJoin;

View File

@@ -120,12 +120,12 @@ async fn run_endpoint(listen_address: &SocketAddr, with_auth: bool, socks_proxy:
.allow_private_network_connections(true);
if with_auth {
builder = builder.authenticator(Box::new(RegistryBasedAuthenticator::new(std::iter::once(
builder = builder.clients(Vec::from_iter(std::iter::once(
authentication::registry_based::Client {
username: "a".into(),
password: "b".into(),
}
))));
)));
}
if let Some(address) = socks_proxy {

View File

@@ -20,6 +20,7 @@ use rustls::client::ServerCertVerified;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::{TcpStream, UdpSocket};
use tokio_rustls::TlsConnector;
use vpn_libs_endpoint::authentication::{Authenticator, registry_based::RegistryBasedAuthenticator};
use vpn_libs_endpoint::core::Core;
use vpn_libs_endpoint::log_utils;
use vpn_libs_endpoint::settings::{Http1Settings, Http2Settings, ListenProtocolSettings, QuicSettings, Settings, TlsHostInfo, TlsHostsSettings};
@@ -219,8 +220,13 @@ pub async fn run_endpoint(listen_address: &SocketAddr) {
pub async fn run_endpoint_with_settings(settings: Settings, hosts_settings: TlsHostsSettings) {
let shutdown = Shutdown::new();
let authenticator: Option<Arc<dyn Authenticator>> = if !settings.get_clients().is_empty() {
Some(Arc::new(RegistryBasedAuthenticator::new(settings.get_clients())))
} else {
None
};
let endpoint = Core::new(settings, hosts_settings, shutdown).unwrap();
let endpoint = Core::new(settings, authenticator, hosts_settings, shutdown).unwrap();
endpoint.listen().await.unwrap();
}

View File

@@ -1,9 +1,8 @@
use std::iter::once;
use toml_edit::{Document, value};
use vpn_libs_endpoint::settings::{ForwardProtocolSettings, Http1Settings, Http2Settings, IcmpSettings, ListenProtocolSettings, MetricsSettings, QuicSettings, Settings};
use vpn_libs_endpoint::utils::IterJoin;
use vpn_libs_endpoint::utils::{IterJoin, ToTomlComment};
use crate::template_settings;
use crate::template_settings::ToTomlComment;
pub fn compose_document(settings: &Settings, credentials_path: &str) -> String {
once(compose_main_table(settings, credentials_path))

View File

@@ -1,26 +1,6 @@
use once_cell::sync::Lazy;
use vpn_libs_endpoint::settings::{ForwardProtocolSettings, Http1Settings, Http2Settings, IcmpSettings, ListenProtocolSettings, MetricsSettings, QuicSettings, Settings, Socks5ForwarderSettings};
use vpn_libs_endpoint::utils::IterJoin;
pub trait ToTomlComment {
/// Prepend each line of string with "# " turning
/// the whole string it into TOML comment.
fn to_toml_comment(&self) -> String;
}
impl ToTomlComment for &str {
fn to_toml_comment(&self) -> String {
self.lines()
.map(|x| format!("# {x}"))
.join("\n")
}
}
impl ToTomlComment for String {
fn to_toml_comment(&self) -> String {
self.as_str().to_toml_comment()
}
}
use vpn_libs_endpoint::utils::ToTomlComment;
pub static MAIN_TABLE: Lazy<String> = Lazy::new(|| format!(
r#"{}