Configurable TLS ciphers

This commit is contained in:
Ara Sadoyan
2025-07-27 11:13:39 +02:00
parent ece4fa20af
commit a44979ec82
8 changed files with 219 additions and 86 deletions

View File

@@ -61,27 +61,3 @@ pub fn calc_metrics(metric_types: &MetricTypes) {
REQUESTS_BY_METHOD.with_label_values(&[&metric_types.method]).inc();
RESPONSE_LATENCY.observe(metric_types.latency.as_secs_f64());
}
/*
pub fn calc_metrics(method: String, code: u16, latency: Duration) {
REQUEST_COUNT.inc();
let timer = REQUEST_LATENCY.start_timer();
timer.observe_duration();
RESPONSE_CODES.with_label_values(&[&code.to_string()]).inc();
REQUESTS_BY_METHOD.with_label_values(&[&method]).inc();
RESPONSE_LATENCY.observe(latency.as_secs_f64());
}
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(5));
loop {
interval.tick().await;
// read Pingora stats
let stats = pingora.get_stats();
// update Prometheus metrics accordingly
REQUEST_COUNT.set(stats.requests_total);
// ... etc
}
});
*/

View File

@@ -2,8 +2,8 @@ use crate::utils::structs::*;
use dashmap::DashMap;
use log::{error, info, warn};
use std::collections::HashMap;
use std::fs;
use std::sync::atomic::AtomicUsize;
use std::{env, fs};
pub fn load_configuration(d: &str, kind: &str) -> Option<Configuration> {
let yaml_data = match kind {
@@ -116,14 +116,6 @@ fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
header_list.insert(path.clone(), hl);
for server in &path_config.servers {
// let mut rate: Option<isize> = None;
// let size: isize = path_config.servers.len() as isize;
// if let Some(limit) = &path_config.rate_limit {
// if size > 0 {
// rate = Some(limit / size);
// }
// }
if let Some((ip, port_str)) = server.split_once(':') {
if let Ok(port) = port_str.parse::<u16>() {
server_list.push(InnerMap {
@@ -138,10 +130,8 @@ fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
}
}
}
path_map.insert(path.clone(), (server_list, AtomicUsize::new(0)));
}
config.headers.insert(hostname.clone(), header_list);
config.upstreams.insert(hostname.clone(), path_map);
}
@@ -149,11 +139,11 @@ fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
}
pub fn parce_main_config(path: &str) -> AppConfig {
info!("Parsing configuration");
let data = fs::read_to_string(path).unwrap();
let reply = DashMap::new();
let cfg: HashMap<String, String> = serde_yaml::from_str(&*data).expect("Failed to parse main config file");
let mut cfo: AppConfig = serde_yaml::from_str(&*data).expect("Failed to parse main config file");
log_builder(&cfo);
cfo.hc_method = cfo.hc_method.to_uppercase();
for (k, v) in cfg {
reply.insert(k.to_string(), v.to_string());
@@ -170,5 +160,60 @@ pub fn parce_main_config(path: &str) -> AppConfig {
}
}
};
cfo.proxy_tls_grade = parce_tls_grades(cfo.proxy_tls_grade.clone());
cfo
}
fn parce_tls_grades(what: Option<String>) -> Option<String> {
match what {
Some(g) => match g.to_ascii_lowercase().as_str() {
"a+" => {
info!("TLS grade set to: [ A+ ]");
Some("a+".to_string())
}
"a" => {
info!("TLS grade set to: [ A ]");
Some("a".to_string())
}
"b" => {
info!("TLS grade set to: [ B ]");
Some("b".to_string())
}
"c" => {
info!("TLS grade set to: [ C ]");
Some("c".to_string())
}
"unsafe" => {
info!("TLS grade set to: [ UNSAFE ]");
Some("unsafe".to_string())
}
_ => {
warn!("Error parsing TLS grade, defaulting to: `B`");
Some("b".to_string())
}
},
None => {
warn!("TLS grade not set, defaulting to: medium");
Some("b".to_string())
}
}
}
fn log_builder(conf: &AppConfig) {
let log_level = conf.log_level.clone();
unsafe {
match log_level.as_str() {
"info" => env::set_var("RUST_LOG", "info"),
"error" => env::set_var("RUST_LOG", "error"),
"warn" => env::set_var("RUST_LOG", "warn"),
"debug" => env::set_var("RUST_LOG", "debug"),
"trace" => env::set_var("RUST_LOG", "trace"),
"off" => env::set_var("RUST_LOG", "off"),
_ => {
println!("Error reading log level, defaulting to: INFO");
env::set_var("RUST_LOG", "info")
}
}
}
env_logger::builder().init();
}

View File

@@ -3,25 +3,8 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
// pub type InnerMap = BackendConfig;
pub type UpstreamsDashMap = DashMap<String, DashMap<String, (Vec<InnerMap>, AtomicUsize)>>;
// #[derive(Debug, Default)]
// pub struct UpstreamsMap {
// pub upstreams: DashMap<String, DashMap<String, (Vec<InnerMap>, AtomicUsize)>>,
// pub ratelimit: DashMap<String, Option<isize>>,
// }
// impl UpstreamsMap {
// pub fn new() -> Self {
// Self {
// upstreams: Default::default(),
// ratelimit: Default::default(),
// }
// }
// }
//
// pub type XUpstreamsDashMap = DashMap<String, UpstreamsMap>;
pub type UpstreamsIdMap = DashMap<String, InnerMap>;
pub type Headers = DashMap<String, DashMap<String, Vec<(String, String)>>>;
@@ -103,6 +86,7 @@ pub struct AppConfig {
pub proxy_port_tls: Option<u16>,
pub local_server: Option<(String, u16)>,
pub proxy_certificates: Option<String>,
pub proxy_tls_grade: Option<String>,
pub file_server_address: Option<String>,
pub file_server_folder: Option<String>,
}

View File

@@ -1,5 +1,5 @@
use dashmap::DashMap;
use log::error;
use log::{error, warn};
use pingora::tls::ssl::{select_next_proto, AlpnError, NameType, SniError, SslAlert, SslContext, SslFiletype, SslMethod, SslRef};
use rustls_pemfile::{read_one, Item};
use serde::Deserialize;
@@ -37,12 +37,12 @@ pub struct Certificates {
}
impl Certificates {
pub fn new(configs: &Vec<CertificateConfig>) -> Option<Self> {
pub fn new(configs: &Vec<CertificateConfig>, _grade: &str) -> Option<Self> {
let default_cert = configs.first().expect("At least one TLS certificate required");
let mut cert_infos = Vec::new();
let name_map: DashMap<String, SslContext> = DashMap::new();
for config in configs {
let cert_info = load_cert_info(&config.cert_path, &config.key_path);
let cert_info = load_cert_info(&config.cert_path, &config.key_path, _grade);
match cert_info {
Some(cert) => {
for name in &cert.common_names {
@@ -106,7 +106,7 @@ impl Certificates {
}
}
fn load_cert_info(cert_path: &str, key_path: &str) -> Option<CertificateInfo> {
fn load_cert_info(cert_path: &str, key_path: &str, _grade: &str) -> Option<CertificateInfo> {
let mut common_names = HashSet::new();
let mut alt_names = HashSet::new();
@@ -162,7 +162,7 @@ fn load_cert_info(cert_path: &str, key_path: &str) -> Option<CertificateInfo> {
}
}
if let Ok(ssl_context) = create_ssl_context(cert_path, key_path) {
if let Ok(ssl_context) = create_ssl_context(cert_path, key_path, _grade) {
Some(CertificateInfo {
cert_path: cert_path.to_string(),
key_path: key_path.to_string(),
@@ -176,13 +176,137 @@ fn load_cert_info(cert_path: &str, key_path: &str) -> Option<CertificateInfo> {
}
}
fn create_ssl_context(cert_path: &str, key_path: &str) -> Result<SslContext, Box<dyn std::error::Error>> {
// fn create_ssl_context(cert_path: &str, key_path: &str) -> Result<SslContext, Box<dyn std::error::Error>> {
// let mut ctx = SslContext::builder(SslMethod::tls())?;
// ctx.set_certificate_chain_file(cert_path)?;
// ctx.set_private_key_file(key_path, SslFiletype::PEM)?;
// ctx.set_alpn_select_callback(prefer_h2);
// let built = ctx.build();
// Ok(built)
// }
struct TlsConfig {
options: pingora::tls::ssl::SslOptions,
ciphers: &'static str,
}
enum TlsGrade {
APlus,
A,
B,
C,
Unsafe,
}
impl TlsGrade {
fn to_config(&self) -> TlsConfig {
match self {
// A+ (A+ on Qualys SSL Labs)
TlsGrade::APlus => TlsConfig {
options: pingora::tls::ssl::SslOptions::NO_SSL_MASK
| pingora::tls::ssl::SslOptions::NO_TLSV1
| pingora::tls::ssl::SslOptions::NO_TLSV1_1
| pingora::tls::ssl::SslOptions::NO_TLSV1_2,
ciphers: concat!(
// TLS 1.3 ciphers (in order of preference)
"TLS_AES_256_GCM_SHA384:",
"TLS_CHACHA20_POLY1305_SHA256:",
"TLS_AES_128_GCM_SHA256:",
// TLS 1.2 ciphers with PFS and AEAD
"ECDHE-ECDSA-AES256-GCM-SHA384:",
"ECDHE-RSA-AES256-GCM-SHA384:",
"ECDHE-ECDSA-CHACHA20-POLY1305:",
"ECDHE-RSA-CHACHA20-POLY1305:",
"ECDHE-ECDSA-AES128-GCM-SHA256:",
"ECDHE-RSA-AES128-GCM-SHA256"
),
},
// A (A on Qualys SSL Labs)
TlsGrade::A => TlsConfig {
options: pingora::tls::ssl::SslOptions::NO_SSL_MASK | pingora::tls::ssl::SslOptions::NO_TLSV1 | pingora::tls::ssl::SslOptions::NO_TLSV1_1,
ciphers: concat!(
// TLS 1.3 ciphers
"TLS_AES_256_GCM_SHA384:",
"TLS_CHACHA20_POLY1305_SHA256:",
"TLS_AES_128_GCM_SHA256:",
// TLS 1.2 ciphers
"ECDHE-ECDSA-AES256-GCM-SHA384:",
"ECDHE-RSA-AES256-GCM-SHA384:",
"ECDHE-ECDSA-CHACHA20-POLY1305:",
"ECDHE-RSA-CHACHA20-POLY1305:",
"ECDHE-ECDSA-AES128-GCM-SHA256:",
"ECDHE-RSA-AES128-GCM-SHA256:",
"DHE-RSA-AES256-GCM-SHA384:",
"DHE-RSA-AES128-GCM-SHA256"
),
},
// B (B on Qualys SSL Labs)
TlsGrade::B => TlsConfig {
options: pingora::tls::ssl::SslOptions::NO_SSL_MASK | pingora::tls::ssl::SslOptions::NO_TLSV1,
ciphers: concat!(
"ECDHE-ECDSA-AES256-GCM-SHA384:",
"ECDHE-RSA-AES256-GCM-SHA384:",
"ECDHE-ECDSA-AES128-GCM-SHA256:",
"ECDHE-RSA-AES128-GCM-SHA256:",
"DHE-RSA-AES256-GCM-SHA384:",
"DHE-RSA-AES128-GCM-SHA256:",
"ECDHE-ECDSA-AES256-SHA384:",
"ECDHE-RSA-AES256-SHA384:",
"ECDHE-ECDSA-AES128-SHA256:",
"ECDHE-RSA-AES128-SHA256"
),
},
// C (C on Qualys SSL Labs)
TlsGrade::C => TlsConfig {
options: pingora::tls::ssl::SslOptions::NO_SSL_MASK,
ciphers: concat!(
"ECDHE-ECDSA-AES256-GCM-SHA384:",
"ECDHE-RSA-AES256-GCM-SHA384:",
"ECDHE-ECDSA-AES128-GCM-SHA256:",
"ECDHE-RSA-AES128-GCM-SHA256:",
"DHE-RSA-AES256-GCM-SHA384:",
"DHE-RSA-AES128-GCM-SHA256:",
"ECDHE-ECDSA-AES256-SHA384:",
"ECDHE-RSA-AES256-SHA384:",
"ECDHE-ECDSA-AES128-SHA256:",
"ECDHE-RSA-AES128-SHA256:",
"AES256-GCM-SHA384:",
"AES128-GCM-SHA256:",
"AES256-SHA256:",
"AES128-SHA256"
),
},
// Unsafe (F on Qualys SSL Labs)
TlsGrade::Unsafe => TlsConfig {
options: pingora::tls::ssl::SslOptions::empty(),
ciphers: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
},
}
}
fn from_str(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"a+" => Some(TlsGrade::APlus),
"a" => Some(TlsGrade::A),
"b" => Some(TlsGrade::B),
"c" => Some(TlsGrade::C),
"unsafe" => Some(TlsGrade::Unsafe),
_ => None,
}
}
}
fn create_ssl_context(cert_path: &str, key_path: &str, grade: &str) -> Result<SslContext, Box<dyn std::error::Error>> {
let mut ctx = SslContext::builder(SslMethod::tls())?;
let config = TlsGrade::from_str(grade).map(|g| g.to_config()).unwrap_or_else(|| {
warn!("Invalid TLS grade '{}', defaulting to UNSAFE", grade);
TlsGrade::Unsafe.to_config()
});
ctx.set_options(config.options);
ctx.set_certificate_chain_file(cert_path)?;
ctx.set_private_key_file(key_path, SslFiletype::PEM)?;
ctx.set_cipher_list(config.ciphers)?;
ctx.set_alpn_select_callback(prefer_h2);
let built = ctx.build();
Ok(built)
Ok(ctx.build())
}
pub fn prefer_h2<'a>(_ssl: &mut SslRef, alpn_in: &'a [u8]) -> Result<&'a [u8], AlpnError> {