openmoat-proxy 0.4.0

Default-deny egress proxy for OpenMoat: CONNECT with an SNI check, plain HTTP, its own DNS and an audit record per connection.
Documentation
use openmoat_core::{EvalContext, Policy};

use super::*;

fn ctx() -> EvalContext {
    EvalContext {
        home: "/home/u".to_owned(),
        project: None,
        real_home: None,
        real_project: None,
        moved_dirs: Vec::new(),
        cwd: "/home/u".to_owned(),
        case_insensitive_paths: false,
    }
}

fn verdicts(yaml: &str, cases: &[(&str, Verdict, &str)]) {
    let policy = Policy::parse(yaml).unwrap();
    let compiled = CompiledPolicy::compile(&policy, &ctx()).unwrap();
    for (h, verdict, rule) in cases {
        let d = host(&compiled, h);
        assert_eq!(
            (d.verdict, d.rules[0].as_str()),
            (*verdict, *rule),
            "{h}: {d:?}"
        );
    }
}

#[test]
fn the_default_policy_allows_only_listed_hosts() {
    verdicts(
        openmoat_core::DEFAULT_POLICY,
        &[
            ("api.github.com", Verdict::Allow, "registries"),
            ("crates.io", Verdict::Allow, "registries"),
            // unlisted: the fetch default (ask) is refused, never asked
            ("evil.example", Verdict::Deny, "default.fetch"),
            // local-net is an ask group: refused too
            ("localhost", Verdict::Deny, "local-net"),
            ("169.254.169.254", Verdict::Deny, "cloud-metadata"),
            ("metadata.google.internal", Verdict::Deny, RULE_ADDRESS),
        ],
    );
}

#[test]
fn net_and_fetch_lists_both_allow_and_deny_wins() {
    let yaml = r#"
version: 1
defaults: { "*": deny }
deny:
  - { id: no-evil, net: ["*.evil.example"] }
allow:
  - { id: docs, fetch: ["docs.rs"] }
  - { id: wide, net: ["*.example", "*.evil.example"] }
"#;
    verdicts(
        yaml,
        &[
            ("docs.rs", Verdict::Allow, "docs"),
            ("a.example", Verdict::Allow, "wide"),
            ("x.evil.example", Verdict::Deny, "no-evil"),
            ("other.org", Verdict::Deny, "default"),
        ],
    );
}

#[test]
fn metadata_names_are_refused_even_when_allowed() {
    let yaml = r#"
version: 1
allow:
  - { id: all, net: ["*"] }
"#;
    verdicts(
        yaml,
        &[
            ("metadata.google.internal", Verdict::Deny, RULE_ADDRESS),
            ("x.metadata.goog", Verdict::Deny, RULE_ADDRESS),
            ("notmetadata.goog", Verdict::Allow, "all"),
        ],
    );
}

#[test]
fn local_and_metadata_addresses_are_forbidden() {
    for (ip, why) in [
        ("127.0.0.1", Forbidden::Loopback),
        ("127.8.9.10", Forbidden::Loopback),
        ("::1", Forbidden::Loopback),
        ("::ffff:127.0.0.1", Forbidden::Loopback),
        ("169.254.169.254", Forbidden::LinkLocal),
        ("::ffff:169.254.169.254", Forbidden::LinkLocal),
        ("64:ff9b::a9fe:a9fe", Forbidden::LinkLocal),
        ("::a9fe:a9fe", Forbidden::LinkLocal),
        ("fe80::1", Forbidden::LinkLocal),
        ("febf::1", Forbidden::LinkLocal),
        ("fd00:ec2::254", Forbidden::Metadata),
        ("100.100.100.200", Forbidden::Metadata),
        ("0.0.0.0", Forbidden::NotUnicast),
        ("0.1.2.3", Forbidden::NotUnicast),
        ("::", Forbidden::NotUnicast),
        ("255.255.255.255", Forbidden::NotUnicast),
        ("224.0.0.1", Forbidden::NotUnicast),
        ("ff02::1", Forbidden::NotUnicast),
    ] {
        assert_eq!(forbidden(ip.parse().unwrap()), Some(why), "{ip}");
    }
}

#[test]
fn ordinary_addresses_are_not_forbidden() {
    for ip in [
        "93.184.215.14",
        "9.255.255.255",
        "11.0.0.0",
        "172.15.255.255",
        "172.32.0.0",
        "192.167.255.255",
        "192.169.0.0",
        "100.63.255.255",
        "100.128.0.0",
        "198.17.255.255",
        "198.20.0.0",
        "2606:4700::1111",
        "fec0::1",
        "fe00::1",
        "::ffff:8.8.8.8",
    ] {
        assert_eq!(forbidden(ip.parse().unwrap()), None, "{ip}");
    }
}

#[test]
fn private_ranges_are_forbidden_in_every_spelling() {
    let private = |range| Some(Forbidden::Private(range));
    let ten = "a private address (10.0.0.0/8)";
    let cgnat = "a shared CGNAT address (100.64.0.0/10)";
    for (ip, why) in [
        ("10.0.0.0", private(ten)),
        ("10.255.255.255", private(ten)),
        ("::ffff:10.0.0.5", private(ten)),
        ("::a00:5", private(ten)),
        ("64:ff9b::a00:5", private(ten)),
        ("172.16.0.0", private("a private address (172.16.0.0/12)")),
        (
            "172.31.255.255",
            private("a private address (172.16.0.0/12)"),
        ),
        ("192.168.0.0", private("a private address (192.168.0.0/16)")),
        (
            "::ffff:192.168.255.255",
            private("a private address (192.168.0.0/16)"),
        ),
        ("100.64.0.0", private(cgnat)),
        ("100.127.255.255", private(cgnat)),
        ("64:ff9b::6440:1", private(cgnat)),
        (
            "198.18.0.0",
            private("a benchmarking address (198.18.0.0/15)"),
        ),
        (
            "198.19.255.255",
            private("a benchmarking address (198.18.0.0/15)"),
        ),
        ("fc00::", private("a unique local address (fc00::/7)")),
        ("fdff:ffff::1", private("a unique local address (fc00::/7)")),
        // metadata inside a private range stays metadata: never opened
        ("100.100.100.200", Some(Forbidden::Metadata)),
        ("fd00:ec2::254", Some(Forbidden::Metadata)),
    ] {
        assert_eq!(forbidden(ip.parse().unwrap()), why, "{ip}");
    }
}

#[test]
fn only_allow_rules_naming_an_address_open_a_private_one() {
    let yaml = r#"
version: 1
deny:
  - { id: no-lab, net: ["10.9.*"] }
allow:
  - { id: wide, net: ["*", "*.example", "?0.0.0.1"] }
  - { id: lan, net: ["10.0.0.5", "192.168.1.*", "10.9.0.1", "fd12::*"] }
  - { id: docs, fetch: ["172.16.0.9"] }
ask:
  - { id: lab, net: ["100.64.0.1"] }
"#;
    let policy = Policy::parse(yaml).unwrap();
    let named_policy = address_policy(&policy);
    let addresses = CompiledPolicy::compile(&named_policy, &ctx()).unwrap();
    for (ip, open) in [
        ("10.0.0.5", true),
        ("::ffff:10.0.0.5", true),
        ("192.168.1.77", true),
        ("172.16.0.9", true),
        ("fd12::1", true),
        ("10.0.0.6", false),
        ("192.168.2.1", false),
        ("20.0.0.1", false),
        ("10.9.0.1", false),
        ("100.64.0.1", false),
    ] {
        assert_eq!(named(&addresses, ip.parse().unwrap()), open, "{ip}");
    }
}

#[test]
fn a_name_with_a_port_or_a_leading_digit_does_not_name_an_address() {
    let yaml = r#"
version: 1
allow:
  - { id: names, net: ["db.internal:5432", "1password.com", "10.0.0.5", "fd12::*"] }
"#;
    let named_policy = address_policy(&Policy::parse(yaml).unwrap());
    assert_eq!(
        named_policy.allow[0].patterns(Kind::Net),
        ["10.0.0.5", "fd12::*"]
    );
}