增加ssh
增加deploy下细分的tcp、tls、ssh
This commit is contained in:
80
tests/test_rtr_debug_client_ssh_cli.rs
Normal file
80
tests/test_rtr_debug_client_ssh_cli.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn run_client(args: &[&str]) -> std::process::Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_rtr_debug_client"))
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("failed to run rtr_debug_client")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_requires_user() {
|
||||
let output = run_client(&[
|
||||
"--ssh",
|
||||
"--ssh-key",
|
||||
"tests/fixtures/ssh/client.key",
|
||||
"--ssh-server-key",
|
||||
"tests/fixtures/ssh/server.pub",
|
||||
]);
|
||||
assert!(!output.status.success());
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("SSH mode requires --ssh-user <name>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_requires_key() {
|
||||
let output = run_client(&[
|
||||
"--ssh",
|
||||
"--ssh-user",
|
||||
"rpki-rtr",
|
||||
"--ssh-server-key",
|
||||
"tests/fixtures/ssh/server.pub",
|
||||
]);
|
||||
assert!(!output.status.success());
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("SSH mode requires --ssh-key <path>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_requires_host_key_verification() {
|
||||
let output = run_client(&[
|
||||
"--ssh",
|
||||
"--ssh-user",
|
||||
"rpki-rtr",
|
||||
"--ssh-key",
|
||||
"tests/fixtures/ssh/client.key",
|
||||
]);
|
||||
assert!(!output.status.success());
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains("SSH mode requires host key verification")
|
||||
&& stderr.contains("--ssh-known-hosts")
|
||||
&& stderr.contains("--ssh-server-key")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_rejects_multiple_host_key_verification_sources() {
|
||||
let output = run_client(&[
|
||||
"--ssh",
|
||||
"--ssh-user",
|
||||
"rpki-rtr",
|
||||
"--ssh-key",
|
||||
"tests/fixtures/ssh/client.key",
|
||||
"--ssh-known-hosts",
|
||||
"tests/fixtures/ssh/known_hosts",
|
||||
"--ssh-server-key",
|
||||
"tests/fixtures/ssh/server.pub",
|
||||
]);
|
||||
assert!(!output.status.success());
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("must choose one"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_conflicts_with_tls() {
|
||||
let output = run_client(&["--ssh", "--tls"]);
|
||||
assert!(!output.status.success());
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("--tls cannot be used together with --ssh"));
|
||||
}
|
||||
344
tests/test_server_transports.rs
Normal file
344
tests/test_server_transports.rs
Normal file
@@ -0,0 +1,344 @@
|
||||
use std::fs;
|
||||
use std::io::BufReader;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use rustls::{ClientConfig, RootCertStore};
|
||||
use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName};
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{Instant, sleep};
|
||||
use tokio_rustls::TlsConnector;
|
||||
|
||||
use rpki::rtr::cache::{RtrCacheBuilder, SessionIds, SharedRtrCache};
|
||||
use rpki::rtr::payload::Timing;
|
||||
use rpki::rtr::pdu::{CacheResponse, EndOfDataV1, ResetQuery};
|
||||
use rpki::rtr::server::RtrService;
|
||||
use russh::client;
|
||||
use russh::keys;
|
||||
use russh::keys::ssh_key::LineEnding;
|
||||
|
||||
fn fixture_path(name: &str) -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests")
|
||||
.join("fixtures")
|
||||
.join("tls")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
fn load_pem_certs(path: &Path) -> Vec<CertificateDer<'static>> {
|
||||
let file = fs::File::open(path).expect("open cert file");
|
||||
let mut reader = BufReader::new(file);
|
||||
rustls_pemfile::certs(&mut reader)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.expect("parse certs")
|
||||
}
|
||||
|
||||
fn load_pem_key(path: &Path) -> PrivateKeyDer<'static> {
|
||||
let file = fs::File::open(path).expect("open key file");
|
||||
let mut reader = BufReader::new(file);
|
||||
rustls_pemfile::private_key(&mut reader)
|
||||
.expect("read private key")
|
||||
.expect("missing private key")
|
||||
}
|
||||
|
||||
fn test_cache() -> SharedRtrCache {
|
||||
Arc::new(RwLock::new(
|
||||
RtrCacheBuilder::new()
|
||||
.session_ids(SessionIds::from_array([42, 42, 42]))
|
||||
.serials([100, 100, 100])
|
||||
.timing(Timing::new(600, 600, 7200))
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn reserve_local_addr() -> SocketAddr {
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind temp listener");
|
||||
listener.local_addr().expect("local addr")
|
||||
}
|
||||
|
||||
async fn wait_for_port(addr: SocketAddr) {
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
loop {
|
||||
if TcpStream::connect(addr).await.is_ok() {
|
||||
return;
|
||||
}
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"port {} did not open in time",
|
||||
addr
|
||||
);
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_tls_client(addr: SocketAddr) -> tokio_rustls::client::TlsStream<TcpStream> {
|
||||
let mut roots = RootCertStore::empty();
|
||||
for cert in load_pem_certs(&fixture_path("client-ca.crt")) {
|
||||
roots.add(cert).expect("add root cert");
|
||||
}
|
||||
|
||||
let certs = load_pem_certs(&fixture_path("client-good.crt"));
|
||||
let key = load_pem_key(&fixture_path("client-good.key"));
|
||||
let cfg = ClientConfig::builder()
|
||||
.with_root_certificates(roots)
|
||||
.with_client_auth_cert(certs, key)
|
||||
.expect("build tls client auth");
|
||||
let connector = TlsConnector::from(Arc::new(cfg));
|
||||
|
||||
let tcp = TcpStream::connect(addr).await.expect("connect tls tcp");
|
||||
connector
|
||||
.connect(ServerName::IpAddress(addr.ip().into()), tcp)
|
||||
.await
|
||||
.expect("tls connect")
|
||||
}
|
||||
|
||||
struct TestSshClientHandler;
|
||||
|
||||
impl client::Handler for TestSshClientHandler {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
async fn check_server_key(
|
||||
&mut self,
|
||||
_server_public_key: &russh::keys::ssh_key::PublicKey,
|
||||
) -> Result<bool, Self::Error> {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unified_server_tcp_handles_reset_query() {
|
||||
let service = RtrService::new(test_cache());
|
||||
let tcp_addr = reserve_local_addr();
|
||||
|
||||
let running = service.spawn_tcp_only(tcp_addr);
|
||||
wait_for_port(tcp_addr).await;
|
||||
|
||||
let mut client = TcpStream::connect(tcp_addr).await.expect("connect tcp");
|
||||
ResetQuery::new(1)
|
||||
.write(&mut client)
|
||||
.await
|
||||
.expect("send reset");
|
||||
|
||||
let response = CacheResponse::read(&mut client)
|
||||
.await
|
||||
.expect("read cache response");
|
||||
assert_eq!(response.version(), 1);
|
||||
assert_eq!(response.session_id(), 42);
|
||||
|
||||
let eod = EndOfDataV1::read(&mut client).await.expect("read eod");
|
||||
assert_eq!(eod.version(), 1);
|
||||
assert_eq!(eod.session_id(), 42);
|
||||
assert_eq!(eod.serial_number(), 100);
|
||||
|
||||
running.shutdown();
|
||||
running.wait().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unified_server_tls_handles_reset_query() {
|
||||
let service = RtrService::new(test_cache());
|
||||
let tcp_addr = reserve_local_addr();
|
||||
let tls_addr = reserve_local_addr();
|
||||
|
||||
let running = service.spawn_tcp_and_tls_from_pem(
|
||||
tcp_addr,
|
||||
tls_addr,
|
||||
fixture_path("server.crt"),
|
||||
fixture_path("server.key"),
|
||||
fixture_path("client-ca.crt"),
|
||||
);
|
||||
wait_for_port(tls_addr).await;
|
||||
|
||||
let mut client = connect_tls_client(tls_addr).await;
|
||||
ResetQuery::new(1)
|
||||
.write(&mut client)
|
||||
.await
|
||||
.expect("send reset tls");
|
||||
|
||||
let response = CacheResponse::read(&mut client)
|
||||
.await
|
||||
.expect("read tls cache response");
|
||||
assert_eq!(response.version(), 1);
|
||||
assert_eq!(response.session_id(), 42);
|
||||
|
||||
let eod = EndOfDataV1::read(&mut client).await.expect("read tls eod");
|
||||
assert_eq!(eod.version(), 1);
|
||||
assert_eq!(eod.session_id(), 42);
|
||||
assert_eq!(eod.serial_number(), 100);
|
||||
|
||||
running.shutdown();
|
||||
running.wait().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unified_server_ssh_opens_listener_and_emits_banner() {
|
||||
let service = RtrService::new(test_cache());
|
||||
let tcp_addr = reserve_local_addr();
|
||||
let ssh_addr = reserve_local_addr();
|
||||
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let host_key_path = tmp.path().join("ssh_host_ed25519_key");
|
||||
let authorized_keys_path = tmp.path().join("authorized_keys");
|
||||
|
||||
let host_key =
|
||||
keys::PrivateKey::random(&mut rand::rng(), keys::Algorithm::Ed25519).expect("gen host key");
|
||||
let host_key_pem = host_key
|
||||
.to_openssh(LineEnding::LF)
|
||||
.expect("encode host key");
|
||||
fs::write(&host_key_path, host_key_pem).expect("write host key");
|
||||
|
||||
let pubkey_line = host_key.public_key().to_openssh().expect("encode pubkey");
|
||||
fs::write(&authorized_keys_path, format!("{pubkey_line}\n")).expect("write authorized_keys");
|
||||
|
||||
let running = service.spawn_tcp_and_ssh_from_openssh(
|
||||
tcp_addr,
|
||||
ssh_addr,
|
||||
&host_key_path,
|
||||
&authorized_keys_path,
|
||||
"rpki-rtr",
|
||||
"rpki-rtr",
|
||||
None,
|
||||
);
|
||||
wait_for_port(ssh_addr).await;
|
||||
|
||||
let stream = TcpStream::connect(ssh_addr).await.expect("connect ssh");
|
||||
let mut reader = tokio::io::BufReader::new(stream);
|
||||
let mut banner = String::new();
|
||||
reader
|
||||
.read_line(&mut banner)
|
||||
.await
|
||||
.expect("read ssh banner");
|
||||
assert!(
|
||||
banner.starts_with("SSH-2.0-"),
|
||||
"unexpected ssh banner: {}",
|
||||
banner
|
||||
);
|
||||
|
||||
running.shutdown();
|
||||
running.wait().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unified_server_ssh_accepts_password_when_configured() {
|
||||
let service = RtrService::new(test_cache());
|
||||
let tcp_addr = reserve_local_addr();
|
||||
let ssh_addr = reserve_local_addr();
|
||||
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let host_key_path = tmp.path().join("ssh_host_ed25519_key");
|
||||
let authorized_keys_path = tmp.path().join("authorized_keys");
|
||||
|
||||
let host_key =
|
||||
keys::PrivateKey::random(&mut rand::rng(), keys::Algorithm::Ed25519).expect("gen host key");
|
||||
let host_key_pem = host_key
|
||||
.to_openssh(LineEnding::LF)
|
||||
.expect("encode host key");
|
||||
fs::write(&host_key_path, host_key_pem).expect("write host key");
|
||||
|
||||
let pubkey_line = host_key.public_key().to_openssh().expect("encode pubkey");
|
||||
fs::write(&authorized_keys_path, format!("{pubkey_line}\n")).expect("write authorized_keys");
|
||||
|
||||
let running = service.spawn_tcp_and_ssh_from_openssh(
|
||||
tcp_addr,
|
||||
ssh_addr,
|
||||
&host_key_path,
|
||||
&authorized_keys_path,
|
||||
"rpki-rtr",
|
||||
"rpki-rtr",
|
||||
Some("test-password"),
|
||||
);
|
||||
wait_for_port(ssh_addr).await;
|
||||
|
||||
let mut session = client::connect(
|
||||
Arc::new(client::Config::default()),
|
||||
ssh_addr,
|
||||
TestSshClientHandler,
|
||||
)
|
||||
.await
|
||||
.expect("connect ssh client");
|
||||
let auth_result = session
|
||||
.authenticate_password("rpki-rtr", "test-password")
|
||||
.await
|
||||
.expect("password auth result");
|
||||
assert!(auth_result.success(), "password auth should succeed");
|
||||
|
||||
let channel = session
|
||||
.channel_open_session()
|
||||
.await
|
||||
.expect("open session channel");
|
||||
channel
|
||||
.request_subsystem(true, "rpki-rtr")
|
||||
.await
|
||||
.expect("request subsystem");
|
||||
let mut stream = channel.into_stream();
|
||||
|
||||
ResetQuery::new(1)
|
||||
.write(&mut stream)
|
||||
.await
|
||||
.expect("send reset over ssh subsystem");
|
||||
let response = CacheResponse::read(&mut stream)
|
||||
.await
|
||||
.expect("read cache response over ssh subsystem");
|
||||
assert_eq!(response.version(), 1);
|
||||
assert_eq!(response.session_id(), 42);
|
||||
let eod = EndOfDataV1::read(&mut stream)
|
||||
.await
|
||||
.expect("read eod over ssh subsystem");
|
||||
assert_eq!(eod.version(), 1);
|
||||
assert_eq!(eod.session_id(), 42);
|
||||
assert_eq!(eod.serial_number(), 100);
|
||||
|
||||
running.shutdown();
|
||||
running.wait().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unified_server_ssh_rejects_password_when_not_configured() {
|
||||
let service = RtrService::new(test_cache());
|
||||
let tcp_addr = reserve_local_addr();
|
||||
let ssh_addr = reserve_local_addr();
|
||||
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let host_key_path = tmp.path().join("ssh_host_ed25519_key");
|
||||
let authorized_keys_path = tmp.path().join("authorized_keys");
|
||||
|
||||
let host_key =
|
||||
keys::PrivateKey::random(&mut rand::rng(), keys::Algorithm::Ed25519).expect("gen host key");
|
||||
let host_key_pem = host_key
|
||||
.to_openssh(LineEnding::LF)
|
||||
.expect("encode host key");
|
||||
fs::write(&host_key_path, host_key_pem).expect("write host key");
|
||||
|
||||
let pubkey_line = host_key.public_key().to_openssh().expect("encode pubkey");
|
||||
fs::write(&authorized_keys_path, format!("{pubkey_line}\n")).expect("write authorized_keys");
|
||||
|
||||
let running = service.spawn_tcp_and_ssh_from_openssh(
|
||||
tcp_addr,
|
||||
ssh_addr,
|
||||
&host_key_path,
|
||||
&authorized_keys_path,
|
||||
"rpki-rtr",
|
||||
"rpki-rtr",
|
||||
None,
|
||||
);
|
||||
wait_for_port(ssh_addr).await;
|
||||
|
||||
let mut session = client::connect(
|
||||
Arc::new(client::Config::default()),
|
||||
ssh_addr,
|
||||
TestSshClientHandler,
|
||||
)
|
||||
.await
|
||||
.expect("connect ssh client");
|
||||
let auth_result = session
|
||||
.authenticate_password("rpki-rtr", "test-password")
|
||||
.await
|
||||
.expect("password auth result");
|
||||
assert!(!auth_result.success(), "password auth should be rejected");
|
||||
|
||||
running.shutdown();
|
||||
running.wait().await;
|
||||
}
|
||||
Reference in New Issue
Block a user