Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,37 @@ jobs:
shell: pwsh
run: sccache --show-stats

agent-tunnel-e2e:
name: agent tunnel end-to-end test
needs: [preflight]
runs-on: ubuntu-24.04
timeout-minutes: 30

steps:
- name: Checkout ${{ github.repository }}
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.ref }}

- name: Setup Rust cache
uses: ./.github/actions/setup-rust-cache
with:
sccache-enabled: ${{ needs.preflight.outputs.sccache }}

- name: Configure runner
run: docker info

- name: Test agent tunnel
shell: pwsh
run: cargo test --locked -p testsuite --test integration_tests cli::agent::tunnel::docker_isolates_real_agent_dns_and_ip_routes -- --ignored --exact --nocapture
env:
AWS_LC_SYS_NO_ASM: true

- name: Show sccache stats
if: ${{ needs.preflight.outputs.sccache == 'true' && !cancelled() }}
shell: pwsh
run: sccache --show-stats

check-dependencies:
name: Check no ${{ matrix.banned }} in ${{ matrix.package }} [${{ matrix.target }}]
needs: [preflight]
Expand Down Expand Up @@ -1267,7 +1298,7 @@ jobs:
success:
name: Success
if: ${{ always() }}
needs: [tests, lints, check-dependencies, jetsocat-lipo, devolutions-gateway-powershell, devolutions-gateway, devolutions-gateway-merge, devolutions-pedm-desktop, devolutions-agent, devolutions-agent-merge, devolutions-pedm-client, dotnet-utils-tests, winapi-sanitizer-tests, winapi-miri, pedm-simulator, secure-memory-verifier]
needs: [tests, agent-tunnel-e2e, lints, check-dependencies, jetsocat-lipo, devolutions-gateway-powershell, devolutions-gateway, devolutions-gateway-merge, devolutions-pedm-desktop, devolutions-agent, devolutions-agent-merge, devolutions-pedm-client, dotnet-utils-tests, winapi-sanitizer-tests, winapi-miri, pedm-simulator, secure-memory-verifier]
runs-on: ubuntu-latest

steps:
Expand Down
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 17 additions & 1 deletion testsuite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,34 @@ typed-builder = "0.21"
tokio-tungstenite = { version = "0.29", features = ["rustls-tls-native-roots"] }

[dev-dependencies]
agent-tunnel = { path = "../crates/agent-tunnel", features = ["test-utils"] }
agent-tunnel-proto = { path = "../crates/agent-tunnel-proto", features = ["serde"] }
base64 = "0.23"
proxy-socks = { path = "../crates/proxy-socks" }
camino = "1"
devolutions-gateway-task = { path = "../crates/devolutions-gateway-task" }
devolutions-gateway = { path = "../devolutions-gateway" }
futures-util = "0.3"
ipnetwork = "0.20"
libsql = { version = "0.9", default-features = false, features = ["core"] }
mcp-proxy.path = "../crates/mcp-proxy"
network-scanner = { path = "../crates/network-scanner", features = ["test-utils"] }
network-scanner-proto = { path = "../crates/network-scanner-proto" }
nonempty = "0.12"
picky = { version = "7.0.0-rc.25", default-features = false, features = ["jose"] }
proxy-socks = { path = "../crates/proxy-socks" }
quinn = "0.11"
rcgen = { version = "0.13", features = ["pem", "x509-parser"] }
reqwest = { version = "0.12", default-features = false, features = ["json"] }
rstest = "0.25"
rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
rustls-pemfile = "2"
rustls-pki-types = "1"
serde_json = "1"
sysevent.path = "../crates/sysevent"
tempfile = "3"
test-utils.path = "../crates/test-utils"
tokio-rustls = { version = "0.26", features = ["ring"] }
uuid = { version = "1", features = ["v4"] }

[target.'cfg(unix)'.dev-dependencies]
sysevent-syslog.path = "../crates/sysevent-syslog"
Expand Down
6 changes: 6 additions & 0 deletions testsuite/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ pub fn agent_assert_cmd() -> assert_cmd::Command {
cmd
}

pub fn agent_tokio_cmd() -> tokio::process::Command {
let mut cmd = tokio::process::Command::new(&*AGENT_BIN_PATH);
cmd.env("RUST_BACKTRACE", "0");
cmd
}

pub fn assert_stderr_eq(output: &assert_cmd::assert::Assert, expected: expect_test::Expect) {
let stderr = std::str::from_utf8(&output.get_output().stderr).unwrap();
expected.assert_eq(stderr);
Expand Down
33 changes: 27 additions & 6 deletions testsuite/src/dgw_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ pub struct AgentTunnelConfig {

#[derive(TypedBuilder)]
pub struct DgwConfig {
#[builder(default, setter(into))]
hostname: Option<String>,
#[builder(default = "127.0.0.1".to_owned(), setter(into))]
listener_host: String,
#[builder(default, setter(into))]
provisioner_public_key_data: Option<String>,
#[builder(default, setter(into))]
tcp_port: Option<u16>,
#[builder(default, setter(into))]
Expand Down Expand Up @@ -79,6 +85,9 @@ pub struct DgwConfigHandle {
impl DgwConfigHandle {
pub fn init(config: DgwConfig) -> anyhow::Result<Self> {
let DgwConfig {
hostname,
listener_host,
provisioner_public_key_data,
tcp_port,
http_port,
disable_token_validation,
Expand Down Expand Up @@ -119,19 +128,31 @@ impl DgwConfigHandle {
String::new()
};

let hostname_json = hostname
.map(|hostname| {
format!(
r#" "Hostname": "{hostname}",
"#
)
})
.unwrap_or_default();
let provisioner_public_key_data = provisioner_public_key_data.unwrap_or_else(|| {
"mMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4vuqLOkl1pWobt6su1XO9VskgCAwevEGs6kkNjJQBwkGnPKYLmNF1E/af1yCocfVn/OnPf9e4x+lXVyZ6LMDJxFxu+axdgOq3Ld392J1iAEbfvwlyRFnEXFOJNyylqg3bY6LvnWHL/XZczVdMD9xYfq2sO9bg3xjRW4s7r9EEYOFjqVT3VFznH9iWJVtcSEKukmS/3uKoO6lGhacvu0HgjXXdgq0R8zvR4XRJ9Fcnf0f9Ypoc+i6L80NVjrRCeVOH+Ld/2fA9bocpfLarcVqG3RjS+qgOtpyCc0jWVFF4zaGQ7LUDFkEIYILkICeMMn2ll29hmZNzsJzZJ9s6NocgQIDAQAB".to_owned()
});

let config = format!(
r#"{{
"ProvisionerPublicKeyData": {{
"Value": "mMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4vuqLOkl1pWobt6su1XO9VskgCAwevEGs6kkNjJQBwkGnPKYLmNF1E/af1yCocfVn/OnPf9e4x+lXVyZ6LMDJxFxu+axdgOq3Ld392J1iAEbfvwlyRFnEXFOJNyylqg3bY6LvnWHL/XZczVdMD9xYfq2sO9bg3xjRW4s7r9EEYOFjqVT3VFznH9iWJVtcSEKukmS/3uKoO6lGhacvu0HgjXXdgq0R8zvR4XRJ9Fcnf0f9Ypoc+i6L80NVjrRCeVOH+Ld/2fA9bocpfLarcVqG3RjS+qgOtpyCc0jWVFF4zaGQ7LUDFkEIYILkICeMMn2ll29hmZNzsJzZJ9s6NocgQIDAQAB"
{hostname_json} "ProvisionerPublicKeyData": {{
"Value": "{provisioner_public_key_data}"
}},
"Listeners": [
{{
"InternalUrl": "tcp://127.0.0.1:{tcp_port}",
"ExternalUrl": "tcp://127.0.0.1:{tcp_port}"
"InternalUrl": "tcp://{listener_host}:{tcp_port}",
"ExternalUrl": "tcp://{listener_host}:{tcp_port}"
}},
{{
"InternalUrl": "http://127.0.0.1:{http_port}",
"ExternalUrl": "http://127.0.0.1:{http_port}"
"InternalUrl": "http://{listener_host}:{http_port}",
"ExternalUrl": "http://{listener_host}:{http_port}"
}}
],
"VerbosityProfile": "{verbosity_profile}",
Expand Down
58 changes: 58 additions & 0 deletions testsuite/tests/agent_tunnel/cert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use agent_tunnel::cert::{CaManager, extract_agent_id_from_pem};
use camino::Utf8PathBuf;
use tempfile::TempDir;
use uuid::Uuid;

use super::common::generate_csr_with_cn;

fn fresh_ca() -> (TempDir, std::sync::Arc<CaManager>) {
let temp_dir = tempfile::tempdir().expect("create temporary directory");
let data_dir = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).expect("use utf-8 temporary path");
let manager = CaManager::load_or_generate(&data_dir).expect("generate test ca");
(temp_dir, manager)
}

#[test]
fn sign_agent_csr_ignores_csr_subject_uses_passed_identity() {
let (_temp_dir, ca_manager) = fresh_ca();

let real_agent_id = Uuid::new_v4();
let (_evil_key, evil_csr_pem) = generate_csr_with_cn("evil-impersonator");

let signed = ca_manager
.sign_agent_csr(real_agent_id, "legit-name", &evil_csr_pem, None)
.expect("sign agent csr");

let recovered = extract_agent_id_from_pem(&signed.client_cert_pem).expect("issued certificate has urn:uuid san");
assert_eq!(
recovered, real_agent_id,
"issued cert must encode the agent_id passed by the caller, not the CSR subject"
);
}

#[test]
fn extract_agent_id_from_pem_round_trips() {
let (_temp_dir, ca_manager) = fresh_ca();

let known_id = Uuid::new_v4();
let (_key, csr_pem) = generate_csr_with_cn("round-trip-agent");

let signed = ca_manager
.sign_agent_csr(known_id, "round-trip-agent", &csr_pem, None)
.expect("sign agent csr");

let recovered = extract_agent_id_from_pem(&signed.client_cert_pem).expect("urn:uuid san present");
assert_eq!(recovered, known_id);
}

#[test]
fn extract_agent_id_from_pem_rejects_cert_without_san() {
let (_temp_dir, ca_manager) = fresh_ca();
let error = extract_agent_id_from_pem(ca_manager.ca_cert_pem()).expect_err("ca certificate has no urn:uuid san");

let msg = format!("{error:#}");
assert!(
msg.contains("urn:uuid"),
"error should reference the missing urn:uuid SAN, got: {msg}"
);
}
Loading
Loading