// sandhole: Expose HTTP/SSH/TCP services through SSH port forwarding // Copyright (C) 2024-2026 Eric Rodrigues Pires // // This program is free software: you can redistribute it and/or modify it under // the terms of the GNU Affero General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for // more details. // // You should have received a copy of the GNU Affero General Public License along // with this program. If not, see . use std::{sync::Arc, time::Duration}; use clap::Parser; use rustls::{ClientConfig, client::danger::ServerCertVerifier}; use rustls_acme::acme::ACME_TLS_ALPN_NAME; use sandhole::{ApplicationConfig, entrypoint}; use tokio::{ net::TcpStream, time::{sleep, timeout}, }; use tokio_rustls::TlsConnector; use crate::common::SandholeHandle; /// This test ensures that invalid options result in errors when running /// Sandhole. #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn config_invalid_options() { // 1. Fail to initialize Sandhole if HTTP, TCP, UDP, and aliasing are all disabled let config = ApplicationConfig::parse_from([ "sandhole", "--domain=foobar.tld", "--user-keys-directory", &(format!( "{}/tests/data/user_keys", std::env::var("CARGO_MANIFEST_DIR").unwrap() )), "--admin-keys-directory", &(format!( "{}/tests/data/admin_keys", std::env::var("CARGO_MANIFEST_DIR").unwrap() )), "--certificates-directory", &(format!( "{}/tests/data/certificates", std::env::var("CARGO_MANIFEST_DIR").unwrap() )), "--private-key-file", &(format!( "{}/tests/data/server_keys/ssh", std::env::var("CARGO_MANIFEST_DIR").unwrap() )), "--acme-cache-directory", &(format!( "{}/tests/data/acme_cache", std::env::var("CARGO_MANIFEST_DIR").unwrap() )), "--listen-address=127.0.0.1", "--ssh-port=18022", "--disable-http", "--disable-tcp", "--disable-udp", "--disable-aliasing", "--acme-use-staging", ]); if timeout(Duration::from_secs(5), async { assert!(entrypoint(config).await.is_err()); }) .await .is_err() { panic!("Timeout waiting for Sandhole to start.") }; // 2. Fail to initialize with --bind-hostnames=cname if --no-domain is set let config = ApplicationConfig::parse_from([ "sandhole", "--no-domain", "--user-keys-directory", &(format!( "{}/tests/data/user_keys", std::env::var("CARGO_MANIFEST_DIR").unwrap() )), "--admin-keys-directory", &(format!( "{}/tests/data/admin_keys", std::env::var("CARGO_MANIFEST_DIR").unwrap() )), "--certificates-directory", &(format!( "{}/tests/data/certificates", std::env::var("CARGO_MANIFEST_DIR").unwrap() )), "--private-key-file", &(format!( "{}/tests/data/server_keys/ssh", std::env::var("CARGO_MANIFEST_DIR").unwrap() )), "--acme-cache-directory", &(format!( "{}/tests/data/acme_cache", std::env::var("CARGO_MANIFEST_DIR").unwrap() )), "--listen-address=127.0.0.1", "--ssh-port=18022", "--http-port=18080", "--https-port=18443", "--bind-hostnames=cname", ]); if timeout(Duration::from_secs(5), async { assert!(entrypoint(config).await.is_err()); }) .await .is_err() { panic!("Timeout waiting for Sandhole to start.") }; // 3a. Fail to initialize ACME ALPN resolver if HTTPS port is not 443 let config = ApplicationConfig::parse_from([ "sandhole", "--domain=foobar.tld", "--user-keys-directory", &(format!( "{}/tests/data/user_keys", std::env::var("CARGO_MANIFEST_DIR").unwrap() )), "--admin-keys-directory", &(format!( "{}/tests/data/admin_keys", std::env::var("CARGO_MANIFEST_DIR").unwrap() )), "--certificates-directory", &(format!( "{}/tests/data/certificates", std::env::var("CARGO_MANIFEST_DIR").unwrap() )), "--private-key-file", &(format!( "{}/tests/data/server_keys/ssh", std::env::var("CARGO_MANIFEST_DIR").unwrap() )), "--acme-cache-directory", &(format!( "{}/tests/data/acme_cache", std::env::var("CARGO_MANIFEST_DIR").unwrap() )), "--listen-address=127.0.0.1", "--ssh-port=18022", "--http-port=18080", "--https-port=18443", "--acme-use-staging", "--acme-contact-email=someone@github.com", "--bind-hostnames=all", ]); let _sandhole_handle = SandholeHandle(tokio::spawn(async move { entrypoint(config).await })); if timeout(Duration::from_secs(5), async { while TcpStream::connect("127.0.0.1:18022").await.is_err() { sleep(Duration::from_millis(100)).await; } }) .await .is_err() { panic!("Timeout waiting for Sandhole to start.") }; // 3b. Fail to connect with fake TLS-ALPN-01 challenge verifier let mut tls_config = ClientConfig::builder() .dangerous() .with_custom_certificate_verifier(Arc::new(SkipServerVerification)) .with_no_client_auth(); tls_config.alpn_protocols.push(ACME_TLS_ALPN_NAME.to_vec()); let tls_config = Arc::new(tls_config); let connector = TlsConnector::from(tls_config); let tcp_stream = TcpStream::connect("127.0.0.1:18443") .await .expect("TCP connection failed"); assert!( connector .connect("test.foobar.tld".try_into().unwrap(), tcp_stream) .await .is_err() ); } #[derive(Debug)] struct SkipServerVerification; impl ServerCertVerifier for SkipServerVerification { fn verify_server_cert( &self, _end_entity: &rustls_pki_types::CertificateDer<'_>, _intermediates: &[rustls_pki_types::CertificateDer<'_>], _server_name: &rustls_pki_types::ServerName<'_>, _ocsp_response: &[u8], _now: rustls_pki_types::UnixTime, ) -> Result { Ok(rustls::client::danger::ServerCertVerified::assertion()) } fn verify_tls12_signature( &self, _message: &[u8], _cert: &rustls_pki_types::CertificateDer<'_>, _dss: &rustls::DigitallySignedStruct, ) -> Result { Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) } fn verify_tls13_signature( &self, _message: &[u8], _cert: &rustls_pki_types::CertificateDer<'_>, _dss: &rustls::DigitallySignedStruct, ) -> Result { Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) } fn supported_verify_schemes(&self) -> Vec { vec![ rustls::SignatureScheme::RSA_PKCS1_SHA1, rustls::SignatureScheme::ECDSA_SHA1_Legacy, rustls::SignatureScheme::RSA_PKCS1_SHA256, rustls::SignatureScheme::ECDSA_NISTP256_SHA256, rustls::SignatureScheme::RSA_PKCS1_SHA384, rustls::SignatureScheme::ECDSA_NISTP384_SHA384, rustls::SignatureScheme::RSA_PKCS1_SHA512, rustls::SignatureScheme::ECDSA_NISTP521_SHA512, rustls::SignatureScheme::RSA_PSS_SHA256, rustls::SignatureScheme::RSA_PSS_SHA384, rustls::SignatureScheme::RSA_PSS_SHA512, rustls::SignatureScheme::ED25519, rustls::SignatureScheme::ED448, ] } }