// 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 russh::keys::{key::PrivateKeyWithHashAlg, load_secret_key};
use sandhole::{ApplicationConfig, entrypoint};
use tokio::{
net::TcpStream,
time::{sleep, timeout},
};
use crate::common::SandholeHandle;
/// This test ensures that the Prometheus alias can be fully disabled by the
/// `--disable-prometheus` option.
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn config_disable_prometheus() {
// 1. Initialize Sandhole
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()
)),
"--disable-directory-creation",
"--listen-address=127.0.0.1",
"--ssh-port=18022",
"--http-port=18080",
"--https-port=18443",
"--acme-use-staging",
"--bind-hostnames=none",
"--disable-prometheus",
"--idle-connection-timeout=800ms",
"--authentication-request-timeout=5s",
"--http-request-timeout=5s",
]);
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.")
};
// 2. Start SSH admin client that will fail to local forward the Prometheus service
let key = load_secret_key(
std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
.join("tests/data/private_keys/admin"),
None,
)
.expect("Missing file admin");
let ssh_client = SshClient;
let mut session = russh::client::connect(Default::default(), "127.0.0.1:18022", ssh_client)
.await
.expect("Failed to connect to SSH server");
assert!(
session
.authenticate_publickey(
"admin",
PrivateKeyWithHashAlg::new(
Arc::new(key),
session.best_supported_rsa_hash().await.unwrap().flatten()
)
)
.await
.expect("SSH authentication failed")
.success(),
"authentication didn't succeed"
);
assert!(
session
.channel_open_direct_tcpip("prometheus.sandhole", 10, "127.0.0.1", 12345)
.await
.is_err(),
"alias should've been disabled"
);
}
struct SshClient;
impl russh::client::Handler for SshClient {
type Error = color_eyre::eyre::Error;
async fn check_server_key(
&mut self,
_key: &russh::keys::PublicKey,
) -> Result {
Ok(true)
}
}