runewarp 0.1.0

Runewarp is an ingress tunneling tool for exposing local services without moving TLS termination to the edge. Clients connect out over QUIC, so you can publish services without putting your backend directly on the Internet or leaking your public IP.
Documentation
use std::error::Error;
use std::fs;

use runewarp::{
    CLIENT_CERT_FILENAME, CLIENT_IDENTITY_FILENAME, CLIENT_KEY_FILENAME, ClientPublicCertConfig,
    ClientRuntimeArgs, ClientSettingsResolutionDefaults, ClientSettingsResolutionError, LogLevel,
    SelectedClientConfig, resolve_selected_client_settings,
};
use tempfile::tempdir;

#[test]
fn cli_only_resolution_uses_the_runtime_owned_client_retry_defaults() -> Result<(), Box<dyn Error>>
{
    let tempdir = tempdir()?;
    let identity_directory = tempdir.path().join("client-identity");
    write_identity_material(&identity_directory)?;

    let settings = resolve_selected_client_settings(
        SelectedClientConfig::None,
        &ClientRuntimeArgs {
            server_address: Some("Tunnel.Example.Test.".to_owned()),
            backend_address: Some("localhost:8443".to_owned()),
        },
        &ClientSettingsResolutionDefaults {
            identity_directory: identity_directory.clone(),
            public_cert_directory: tempdir.path().join("unused-public-cert"),
        },
    )?;

    assert_eq!(settings.server_hostname, "tunnel.example.test");
    assert_eq!(settings.server_port, 443);
    assert_eq!(settings.log_level, LogLevel::Info);
    assert_eq!(settings.server_ca_file, None);
    assert_eq!(settings.identity_directory, identity_directory);
    assert_eq!(settings.services.len(), 1);
    assert_eq!(settings.services[0].public_hostnames, None);
    assert_eq!(settings.services[0].backend_address, "localhost:8443");
    Ok(())
}

#[test]
fn cli_only_resolution_requires_backend_address_without_a_selected_config()
-> Result<(), Box<dyn Error>> {
    let tempdir = tempdir()?;
    let identity_directory = tempdir.path().join("client-identity");
    write_identity_material(&identity_directory)?;

    let error = resolve_selected_client_settings(
        SelectedClientConfig::None,
        &ClientRuntimeArgs {
            server_address: Some("tunnel.example.test".to_owned()),
            backend_address: None,
        },
        &ClientSettingsResolutionDefaults {
            identity_directory,
            public_cert_directory: tempdir.path().join("unused-public-cert"),
        },
    )
    .expect_err("expected backend-address validation error");

    match error {
        ClientSettingsResolutionError::Validation {
            path: None,
            messages,
        } => {
            assert_eq!(messages.len(), 1);
            assert_eq!(
                messages[0],
                "--backend-address is required when no selected client config is available"
            );
        }
        other => panic!("expected a CLI-only validation error, got {other}"),
    }

    Ok(())
}

#[test]
fn selected_config_without_a_client_section_can_still_resolve_from_runtime_flags()
-> Result<(), Box<dyn Error>> {
    let tempdir = tempdir()?;
    let identity_directory = tempdir.path().join("client-identity");
    write_identity_material(&identity_directory)?;
    fs::write(
        tempdir.path().join("config.toml"),
        r#"
[server]
hostname = "tunnel.example.test"
"#,
    )?;

    let settings = resolve_selected_client_settings(
        SelectedClientConfig::Explicit(tempdir.path().join("config.toml")),
        &ClientRuntimeArgs {
            server_address: Some("tunnel.example.test:9443".to_owned()),
            backend_address: Some("backend.internal:443".to_owned()),
        },
        &ClientSettingsResolutionDefaults {
            identity_directory,
            public_cert_directory: tempdir.path().join("unused-public-cert"),
        },
    )?;

    assert_eq!(settings.server_hostname, "tunnel.example.test");
    assert_eq!(settings.server_port, 9443);
    assert_eq!(settings.log_level, LogLevel::Info);
    assert_eq!(settings.services.len(), 1);
    assert_eq!(settings.services[0].public_hostnames, None);
    assert_eq!(settings.services[0].backend_address, "backend.internal:443");
    Ok(())
}

#[test]
fn server_address_runtime_flag_overrides_a_selected_config_before_validation()
-> Result<(), Box<dyn Error>> {
    let tempdir = tempdir()?;
    let identity_directory = tempdir.path().join("client-identity");
    write_identity_material(&identity_directory)?;
    fs::write(
        tempdir.path().join("config.toml"),
        r#"
log-level = "off"

[client]
"#,
    )?;

    let settings = resolve_selected_client_settings(
        SelectedClientConfig::Explicit(tempdir.path().join("config.toml")),
        &ClientRuntimeArgs {
            server_address: Some("Tunnel.Example.Test.".to_owned()),
            backend_address: Some("backend.internal:443".to_owned()),
        },
        &ClientSettingsResolutionDefaults {
            identity_directory,
            public_cert_directory: tempdir.path().join("unused-public-cert"),
        },
    )?;

    assert_eq!(settings.server_hostname, "tunnel.example.test");
    assert_eq!(settings.server_port, 443);
    assert_eq!(settings.log_level, LogLevel::Off);
    assert_eq!(settings.services.len(), 1);
    assert_eq!(settings.services[0].public_hostnames, None);
    assert_eq!(settings.services[0].backend_address, "backend.internal:443");
    Ok(())
}

#[test]
fn server_address_runtime_flag_rescues_an_invalid_selected_config_value()
-> Result<(), Box<dyn Error>> {
    let tempdir = tempdir()?;
    let identity_directory = tempdir.path().join("client-identity");
    write_identity_material(&identity_directory)?;
    fs::write(
        tempdir.path().join("config.toml"),
        r#"
log-level = "off"

[client]
server-address = "127.0.0.1:443"
"#,
    )?;

    let settings = resolve_selected_client_settings(
        SelectedClientConfig::Explicit(tempdir.path().join("config.toml")),
        &ClientRuntimeArgs {
            server_address: Some("Tunnel.Example.Test.".to_owned()),
            backend_address: Some("backend.internal:443".to_owned()),
        },
        &ClientSettingsResolutionDefaults {
            identity_directory,
            public_cert_directory: tempdir.path().join("unused-public-cert"),
        },
    )?;

    assert_eq!(settings.server_hostname, "tunnel.example.test");
    assert_eq!(settings.server_port, 443);
    assert_eq!(settings.log_level, LogLevel::Off);
    assert_eq!(settings.services.len(), 1);
    assert_eq!(settings.services[0].public_hostnames, None);
    assert_eq!(settings.services[0].backend_address, "backend.internal:443");
    Ok(())
}

#[test]
fn backend_address_runtime_flag_is_rejected_when_selected_config_already_has_services()
-> Result<(), Box<dyn Error>> {
    let tempdir = tempdir()?;
    let identity_directory = tempdir.path().join("client-identity");
    write_identity_material(&identity_directory)?;
    fs::write(
        tempdir.path().join("config.toml"),
        r#"
[client]
server-address = "tunnel.example.test"

[[client.services]]
backend-address = "backend.internal:443"
"#,
    )?;

    let error = resolve_selected_client_settings(
        SelectedClientConfig::Explicit(tempdir.path().join("config.toml")),
        &ClientRuntimeArgs {
            server_address: None,
            backend_address: Some("override.internal:8443".to_owned()),
        },
        &ClientSettingsResolutionDefaults {
            identity_directory,
            public_cert_directory: tempdir.path().join("unused-public-cert"),
        },
    )
    .expect_err("expected backend-address conflict");

    let messages = error
        .validation_messages()
        .expect("selected config validation messages");
    assert_eq!(messages.len(), 1);
    assert_eq!(
        messages[0],
        "--backend-address may be used only when the selected config contributes no [[client.services]] blocks"
    );
    Ok(())
}

#[test]
fn malformed_selected_services_are_not_masked_by_backend_address_runtime_flags()
-> Result<(), Box<dyn Error>> {
    let tempdir = tempdir()?;
    let identity_directory = tempdir.path().join("client-identity");
    write_identity_material(&identity_directory)?;
    fs::write(
        tempdir.path().join("config.toml"),
        r#"
[client]
server-address = "tunnel.example.test"

[[client.services]]
public-hostnames = ["app.example.test"]
"#,
    )?;

    let error = resolve_selected_client_settings(
        SelectedClientConfig::Explicit(tempdir.path().join("config.toml")),
        &ClientRuntimeArgs {
            server_address: None,
            backend_address: Some("override.internal:8443".to_owned()),
        },
        &ClientSettingsResolutionDefaults {
            identity_directory,
            public_cert_directory: tempdir.path().join("unused-public-cert"),
        },
    )
    .expect_err("expected malformed service validation");

    let messages = error
        .validation_messages()
        .expect("selected config validation messages");
    assert!(messages.contains(
        &"--backend-address may be used only when the selected config contributes no [[client.services]] blocks"
            .to_owned()
    ));
    assert!(messages.contains(&"client.services[].backend-address is required".to_owned()));
    Ok(())
}

#[test]
fn cli_only_resolution_rejects_ip_literal_server_addresses() -> Result<(), Box<dyn Error>> {
    let tempdir = tempdir()?;
    let identity_directory = tempdir.path().join("client-identity");
    write_identity_material(&identity_directory)?;

    let error = resolve_selected_client_settings(
        SelectedClientConfig::None,
        &ClientRuntimeArgs {
            server_address: Some("127.0.0.1:443".to_owned()),
            backend_address: Some("backend.internal:443".to_owned()),
        },
        &ClientSettingsResolutionDefaults {
            identity_directory,
            public_cert_directory: tempdir.path().join("unused-public-cert"),
        },
    )
    .expect_err("expected IP literal validation");

    let messages = error
        .validation_messages()
        .expect("CLI-only validation messages");
    assert!(
        messages.contains(
            &"client.server-address is invalid: IP literals are not supported".to_owned()
        )
    );
    Ok(())
}

#[test]
fn selected_config_uses_the_injected_default_public_cert_dir_for_terminate_mode()
-> Result<(), Box<dyn Error>> {
    let tempdir = tempdir()?;
    let identity_directory = tempdir.path().join("client-identity");
    let public_cert_directory = tempdir.path().join("xdg-data/client/public-cert");
    write_identity_material(&identity_directory)?;
    fs::create_dir_all(&public_cert_directory)?;
    fs::write(
        tempdir.path().join("config.toml"),
        r#"
[client]
server-address = "tunnel.example.test"

[[client.services]]
public-hostnames = ["app.example.test"]
backend-address = "backend.internal:443"
tls-mode = "terminate"
"#,
    )?;

    let settings = resolve_selected_client_settings(
        SelectedClientConfig::Explicit(tempdir.path().join("config.toml")),
        &ClientRuntimeArgs::default(),
        &ClientSettingsResolutionDefaults {
            identity_directory,
            public_cert_directory: public_cert_directory.clone(),
        },
    )?;

    assert!(matches!(
        settings.public_cert_config,
        Some(ClientPublicCertConfig::Manual { directory }) if directory == public_cert_directory
    ));
    Ok(())
}

fn write_identity_material(path: &std::path::Path) -> Result<(), Box<dyn Error>> {
    fs::create_dir_all(path)?;
    fs::write(path.join(CLIENT_CERT_FILENAME), "placeholder certificate")?;
    fs::write(path.join(CLIENT_KEY_FILENAME), "placeholder key")?;
    fs::write(
        path.join(CLIENT_IDENTITY_FILENAME),
        "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
    )?;
    Ok(())
}