diff --git a/Cargo.lock b/Cargo.lock index ecfb94a..457def6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,17 @@ dependencies = [ "libc", ] +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "atomicwrites" version = "0.4.4" @@ -241,6 +252,7 @@ dependencies = [ "duct", "duct_sh", "env_logger", + "hickory-resolver", "humantime", "k8s-openapi", "lazy_static", @@ -391,6 +403,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + [[package]] name = "deranged" version = "0.4.0" @@ -527,6 +545,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -743,6 +773,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -764,6 +800,51 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-proto" +version = "0.24.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.8.5", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.24.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot", + "rand 0.8.5", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1044,6 +1125,18 @@ dependencies = [ "libc", ] +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1116,6 +1209,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1150,6 +1249,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "memchr" version = "2.7.5" @@ -1437,6 +1545,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.101" @@ -1484,6 +1601,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_core" version = "0.3.1" @@ -1499,6 +1637,15 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + [[package]] name = "rc2" version = "0.8.1" @@ -1639,6 +1786,12 @@ dependencies = [ "winreg", ] +[[package]] +name = "resolv-conf" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" + [[package]] name = "ring" version = "0.17.14" @@ -2065,7 +2218,7 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", @@ -2144,7 +2297,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" dependencies = [ - "rand", + "rand 0.4.6", "remove_dir_all", ] @@ -2237,6 +2390,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.47.1" @@ -2314,9 +2482,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "tracing-core" version = "0.1.34" @@ -2514,6 +2694,12 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + [[package]] name = "winapi" version = "0.3.9" @@ -2881,6 +3067,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index ecd5472..8ce1ecc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ serde_with = "^3.0" serde_yaml = "^0.9" strfmt = "^0.2" reqwest = { version = "0.11", features = ["blocking", "json", "default-tls", "rustls-tls", "native-tls"] } +hickory-resolver = "0.24" tempdir = "^0.3" tokio = { version = "1", features = ["full"] } url = "^2.2" diff --git a/src/config/kube.rs b/src/config/kube.rs index 7dfc407..267df30 100644 --- a/src/config/kube.rs +++ b/src/config/kube.rs @@ -17,12 +17,14 @@ use base64::engine::{general_purpose::STANDARD, Engine}; +use hickory_resolver::{config::*, Resolver}; use std::collections::BTreeMap; use std::collections::HashMap; use std::convert::From; use std::env; use std::fs::File; use std::io::{BufReader, Read}; +use std::net::IpAddr; //use crate::certs::{get_cert, get_cert_from_pem, get_key_from_str, get_private_key}; use super::kubefile::{AuthProvider, ExecProvider}; @@ -36,28 +38,36 @@ pub struct ClusterConf { pub server: String, pub tls_server_name: Option, pub insecure_skip_tls_verify: bool, + pub custom_dns_mapping: Option<(String, IpAddr)>, // (hostname, resolved_ip) } impl ClusterConf { - fn new(cert: Option, server: String, tls_server_name: Option) -> ClusterConf { + fn new_insecure( + cert: Option, + server: String, + tls_server_name: Option, + ) -> ClusterConf { ClusterConf { cert, server, tls_server_name, - insecure_skip_tls_verify: false, + insecure_skip_tls_verify: true, + custom_dns_mapping: None, } } - fn new_insecure( + fn new_with_custom_dns( cert: Option, server: String, tls_server_name: Option, + custom_dns_mapping: Option<(String, IpAddr)>, ) -> ClusterConf { ClusterConf { cert, server, tls_server_name, - insecure_skip_tls_verify: true, + insecure_skip_tls_verify: false, + custom_dns_mapping, } } } @@ -123,6 +133,19 @@ pub struct Config { pub users: HashMap, } +// Helper function to create custom DNS mapping from server URL and TLS server name +fn create_custom_dns_mapping(server_url: &str, tls_server_name: &str) -> Option<(String, IpAddr)> { + let url = reqwest::Url::parse(server_url).ok()?; + let proxy_host = url.host_str()?; + + // Resolve the proxy host to its IP address + let resolver = Resolver::new(ResolverConfig::default(), ResolverOpts::default()).ok()?; + let response = resolver.lookup_ip(proxy_host).ok()?; + let proxy_ip = response.iter().next()?; + + Some((tls_server_name.to_string(), proxy_ip)) +} + // some utility functions fn get_full_path(path: String) -> Result { if path.is_empty() { @@ -186,12 +209,19 @@ impl Config { let mut br = BufReader::new(f); let mut s = String::new(); br.read_to_string(&mut s).expect("Couldn't read cert"); + + let custom_dns = + cluster.conf.tls_server_name.as_ref().and_then(|tls_name| { + create_custom_dns_mapping(&cluster.conf.server, tls_name) + }); + cluster_map.insert( cluster.name.clone(), - ClusterConf::new( + ClusterConf::new_with_custom_dns( Some(s), cluster.conf.server.clone(), cluster.conf.tls_server_name.clone(), + custom_dns, ), ); } @@ -212,12 +242,19 @@ impl Config { "Invalid utf8 data in certificate: {e}" )) })?; + + let custom_dns = + cluster.conf.tls_server_name.as_ref().and_then(|tls_name| { + create_custom_dns_mapping(&cluster.conf.server, tls_name) + }); + cluster_map.insert( cluster.name.clone(), - ClusterConf::new( + ClusterConf::new_with_custom_dns( Some(cert_pem), cluster.conf.server.clone(), cluster.conf.tls_server_name.clone(), + custom_dns, ), ); } @@ -226,19 +263,31 @@ impl Config { } }, (None, None) => { - let conf = if cluster.conf.skip_tls { + let custom_dns = + cluster.conf.tls_server_name.as_ref().and_then(|tls_name| { + create_custom_dns_mapping(&cluster.conf.server, tls_name) + }); + + let mut conf = if cluster.conf.skip_tls { ClusterConf::new_insecure( None, cluster.conf.server.clone(), cluster.conf.tls_server_name.clone(), ) } else { - ClusterConf::new( + ClusterConf::new_with_custom_dns( None, cluster.conf.server.clone(), cluster.conf.tls_server_name.clone(), + custom_dns.clone(), ) }; + + // For insecure connections, we still want the custom DNS mapping + if cluster.conf.skip_tls && custom_dns.is_some() { + conf.custom_dns_mapping = custom_dns; + } + cluster_map.insert(cluster.name.clone(), conf); } } @@ -288,8 +337,12 @@ impl Config { .ok_or(ClickError::Kube(ClickErrNo::InvalidUser))?; let mut endpoint = reqwest::Url::parse(&cluster.server)?; - if cluster.tls_server_name.is_some() { - endpoint.set_host(cluster.tls_server_name.as_deref())?; + + // When using custom DNS mapping, we keep the original hostname in the URL + // The DNS resolver will handle mapping it to the correct IP + // We also need to ensure we're using the TLS server name for proper hostname + if let Some((tls_hostname, _)) = &cluster.custom_dns_mapping { + endpoint.set_host(Some(tls_hostname))?; } let ca_certs = match &cluster.cert { @@ -347,6 +400,7 @@ impl Config { user.impersonate_user.clone(), click_conf.connect_timeout_secs, click_conf.read_timeout_secs, + cluster.custom_dns_mapping.clone(), ) }) } @@ -469,9 +523,17 @@ bW1EDp3zdHSo1TRJ6V6e6bR64eVaH4QwnNOfpSXY fn ensure_tls_server_name() { let conf = get_config_from_kubefile_test_conf(); let click_conf = crate::config::click::tests::get_parsed_test_click_config(); + + // Test that the TLS server name is correctly parsed and stored + let cluster = conf.clusters.get("tls-cluster").unwrap(); + assert!(cluster.tls_server_name == Some("api.example.com".to_string())); + + // Test that the context can be created (even if DNS resolution fails in test) let ctx = conf.get_context("tls-context", &click_conf); assert!(ctx.is_ok()); let ctx = ctx.unwrap(); - assert!(ctx.endpoint.host_str() == Some("tls.foo")); + + // The original server hostname should be preserved when DNS resolution fails + assert!(ctx.endpoint.host_str() == Some("proxy.example.com")); } } diff --git a/src/config/kubefile.rs b/src/config/kubefile.rs index c7aac88..f724bcc 100644 --- a/src/config/kubefile.rs +++ b/src/config/kubefile.rs @@ -627,8 +627,8 @@ clusters: name: data - cluster: insecure-skip-tls-verify: true - server: http://nos.foo:80 - tls-server-name: tls.foo + server: https://proxy.example.com:443 + tls-server-name: api.example.com name: tls-cluster contexts: - context: diff --git a/src/k8s.rs b/src/k8s.rs index 0fdc91d..4f1ecf0 100644 --- a/src/k8s.rs +++ b/src/k8s.rs @@ -18,6 +18,7 @@ use k8s_openapi::{http, List, ListableResource}; use reqwest::blocking::Client; use reqwest::{Certificate, Identity, Url}; use serde::Deserialize; +use std::net::{IpAddr, SocketAddr}; use url::Host; use yasna::models::ObjectIdentifier; @@ -175,9 +176,11 @@ pub struct Context { impersonate_user: Option, connect_timeout_secs: u32, read_timeout_secs: u32, + custom_dns_mapping: Option<(String, IpAddr)>, } impl Context { + #[allow(clippy::too_many_arguments)] pub fn new>( name: S, endpoint: Url, @@ -186,6 +189,7 @@ impl Context { impersonate_user: Option, connect_timeout_secs: u32, read_timeout_secs: u32, + custom_dns_mapping: Option<(String, IpAddr)>, ) -> Context { let (client, client_auth) = Context::get_client( &endpoint, @@ -194,12 +198,20 @@ impl Context { None, connect_timeout_secs, read_timeout_secs, + custom_dns_mapping.clone(), ); // have to create a special client for logs until // https://github.com/seanmonstar/reqwest/issues/1380 // is resolved - let (log_client, _) = - Context::get_client(&endpoint, root_cas.clone(), auth, None, u32::MAX, u32::MAX); + let (log_client, _) = Context::get_client( + &endpoint, + root_cas.clone(), + auth, + None, + u32::MAX, + u32::MAX, + custom_dns_mapping.clone(), + ); let client = RefCell::new(client); let log_client = RefCell::new(log_client); let client_auth = RefCell::new(client_auth); @@ -213,6 +225,7 @@ impl Context { impersonate_user, connect_timeout_secs, read_timeout_secs, + custom_dns_mapping, } } @@ -223,12 +236,19 @@ impl Context { id: Option, connect_timeout_secs: u32, read_timeout_secs: u32, + custom_dns_mapping: Option<(String, IpAddr)>, ) -> (Client, Option) { let host = endpoint.host().unwrap(); - let client = match host { + let mut client = match host { Host::Domain(_) => Client::builder().use_rustls_tls(), _ => Client::builder().use_native_tls(), }; + + // Use custom DNS mapping if we have one + if let Some((hostname, ip)) = custom_dns_mapping { + // reqwest's resolve method allows mapping specific hostnames to IP addresses + client = client.resolve(&hostname, SocketAddr::new(ip, 443)); + } let client = match root_cas { Some(cas) => { let mut client = client; @@ -286,6 +306,7 @@ impl Context { Some(id.clone()), self.connect_timeout_secs, self.read_timeout_secs, + self.custom_dns_mapping.clone(), ); let (new_log_client, _) = Context::get_client( &self.endpoint, @@ -294,6 +315,7 @@ impl Context { Some(id), u32::MAX, u32::MAX, + self.custom_dns_mapping.clone(), ); *self.client.borrow_mut() = new_client; *self.log_client.borrow_mut() = new_log_client;