Skip to content
Draft
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
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ default = []

sev-snp = ["ic-bn-lib/sev-snp"]
acme = ["ic-bn-lib/acme-dns", "ic-bn-lib/acme-alpn"]
bench = ["dep:ic-http-certification", "dep:rand_regex", "dep:serde_cbor"]
bench = ["dep:ic-http-certification", "dep:rand_regex"]
tokio_console = ["dep:console-subscriber"]
debug = []

Expand Down Expand Up @@ -76,7 +76,7 @@ rustls = { version = "0.23.18", default-features = false, features = [
"brotli",
] }
serde = "1.0.214"
serde_cbor = { version = "0.11.2", optional = true }
serde_cbor = "0.11.2"
serde_json = "1.0.132"
sha2 = "0.10.8"
strum = { version = "0.27.1", features = ["derive"] }
Expand Down
10 changes: 10 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,16 @@ pub struct Domain {
#[clap(env, long, requires = "domain_system", value_delimiter = ',')]
pub domain_app: Vec<FQDN>,

/// List of domains that we serve engine (verified_application) subnets from.
/// When set, canisters on cloud-engine subnets are routed here instead of
/// the app domain. Requires --domain-app.
#[clap(env, long, requires = "domain_app", value_delimiter = ',')]
pub domain_engine: Vec<FQDN>,

/// How frequently to poll the NNS for subnet routing table and type information
#[clap(env, long, default_value = "5m", value_parser = parse_duration)]
pub subnets_info_poll_interval: Duration,

/// List of canister aliases in format '<alias>:<canister_id>'
#[clap(env, long, value_delimiter = ',')]
pub domain_canister_alias: Vec<CanisterAlias>,
Expand Down
27 changes: 27 additions & 0 deletions src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@ use tracing_subscriber::{EnvFilter, reload::Handle};
use crate::{
cli::Cli,
metrics,
routing::ic::subnets_info::SubnetsInfoFetcher,
routing::{
self,
ic::{
create_agent,
http_service::AgentHttpService,
nodes_fetcher::MAINNET_ROOT_SUBNET_ID,
route_provider::{RouteProviderWrapper, setup_route_provider},
},
},
Expand Down Expand Up @@ -238,6 +241,29 @@ pub async fn main(
);
}

// Subnet info: periodically fetch the full NNS routing table and subnet
// types. Required for both system-subnet and engine-subnet routing
// decisions in DomainCanisterMatcher.
let http_service = Arc::new(AgentHttpService::new(
http_client_hyper.clone(),
cli.ic.ic_request_retry_interval,
));
let agent = create_agent(cli, http_service, route_provider.clone())
.await
.context("unable to create agent for subnets info fetcher")?;

let root_subnet_id = candid::Principal::from_text(MAINNET_ROOT_SUBNET_ID)
.expect("MAINNET_ROOT_SUBNET_ID is valid");

let fetcher = Arc::new(SubnetsInfoFetcher::new(Arc::new(agent), root_subnet_id));
let subnets_info = fetcher.info.clone();

tasks.add_interval(
"subnets_info_fetcher",
fetcher,
cli.domain.subnets_info_poll_interval,
);

// Setup WAF
let waf_layer = if cli.waf.waf_enable {
let v = WafLayer::new_from_cli(&cli.waf, Some(http_client.clone()))
Expand Down Expand Up @@ -265,6 +291,7 @@ pub async fn main(
vector.clone(),
waf_layer,
custom_domains_router,
subnets_info,
)
.await
.context("unable to setup Axum router")?;
Expand Down
153 changes: 93 additions & 60 deletions src/policy/domain_canister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,30 @@ use ahash::AHashSet;
use candid::Principal;
use fqdn::{FQDN, Fqdn};

// System subnets routing table
pub const SYSTEM_SUBNETS: [(Principal, Principal); 5] = [
(
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01]),
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 0x01]),
),
(
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x01, 0x01]),
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x01, 0x01]),
),
(
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x01, 0x01]),
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0x01, 0x01]),
),
(
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x01, 0xa0, 0x00, 0x00, 0x01, 0x01]),
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x01, 0xaf, 0xff, 0xff, 0x01, 0x01]),
),
(
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x02, 0x10, 0x00, 0x00, 0x01, 0x01]),
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x02, 0x1f, 0xff, 0xff, 0x01, 0x01]),
),
];

/// Checks if given canister id belongs to a system subnet
pub fn is_system_subnet(canister_id: Principal) -> bool {
SYSTEM_SUBNETS
.iter()
.any(|x| canister_id >= x.0 && canister_id <= x.1)
}
use crate::routing::ic::subnets_info::{SubnetType, SubnetsInfo};

/// Things needed to verify domain-canister match
#[derive(derive_new::new)]
pub struct DomainCanisterMatcher {
pre_isolation_canisters: AHashSet<Principal>,
domains_app: Vec<FQDN>,
domains_system: Vec<FQDN>,
domains_engine: Vec<FQDN>,
}

impl DomainCanisterMatcher {
/// Check if given canister id and host match from policy perspective
pub fn check(&self, canister_id: Principal, host: &Fqdn) -> bool {
/// Check if given canister id and host match from policy perspective.
/// `subnets_info` is the current NNS snapshot, loaded by the caller.
pub fn check(&self, canister_id: Principal, host: &Fqdn, subnets_info: &SubnetsInfo) -> bool {
// These are always allowed
if self.pre_isolation_canisters.contains(&canister_id) {
return true;
}

let domains = if is_system_subnet(canister_id) {
&self.domains_system
} else {
&self.domains_app
let domains = match subnets_info.subnet_type(canister_id) {
Some(SubnetType::System) => &self.domains_system,
Some(SubnetType::CloudEngine) => &self.domains_engine,
_ => &self.domains_app,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good practice to explicitly match (instead of _), so if somebody adds a new SubnetType, they get a compile-time error and won't forget to update this code.
You can use Some(SubnetType::...) | None to match two things at the same time.

};

domains.iter().any(|x| host.is_subdomain_of(x))
Expand All @@ -61,43 +34,103 @@ impl DomainCanisterMatcher {

#[cfg(test)]
mod tests {
use ahash::AHashMap;
use fqdn::fqdn;
use ic_bn_lib_common::principal;

use super::*;
use crate::routing::ic::subnets_info::SubnetType;

// Principals used as subnet IDs in the test snapshot
const SUBNET_SYSTEM: &str = "tdb26-jop6k-aogll-7ltgs-eruif-6kk7m-qpktf-gdiqx-mxtrf-vb5e6-eqe";
const SUBNET_ENGINE: &str = "nl6hn-ja4yw-wvmpy-3z2jx-ymc34-pisx3-3cp5z-3oj4a-qzzny-jbsv3-4qe";

// Canisters that fall inside the ranges defined below
const CANISTER_SYSTEM: &str = "qoctq-giaaa-aaaaa-aaaea-cai"; // NNS
const CANISTER_ENGINE: &str = "s6hwe-laaaa-aaaab-qaeba-cai";
const CANISTER_APP: &str = "oydqf-haaaa-aaaao-afpsa-cai";
const CANISTER_PIC: &str = "2dcn6-oqaaa-aaaai-abvoq-cai"; // pre-isolation

fn test_snapshot() -> SubnetsInfo {
let subnet_system = principal!(SUBNET_SYSTEM);
let subnet_engine = principal!(SUBNET_ENGINE);

let system_canister = principal!(CANISTER_SYSTEM);
let engine_canister = principal!(CANISTER_ENGINE);

let ranges = vec![
(system_canister, system_canister, subnet_system),
(engine_canister, engine_canister, subnet_engine),
];

let mut types = AHashMap::new();
types.insert(subnet_system, SubnetType::System);
types.insert(subnet_engine, SubnetType::CloudEngine);

SubnetsInfo::new(ranges, types)
}

fn matcher() -> DomainCanisterMatcher {
let mut pic = AHashSet::new();
pic.insert(principal!(CANISTER_PIC));

DomainCanisterMatcher::new(
pic,
vec![fqdn!("icp0.io")], // app
vec![fqdn!("ic0.app")], // system
vec![fqdn!("engine.io")], // engine
)
}

#[test]
fn test_is_system_subnet() {
assert!(is_system_subnet(principal!("qoctq-giaaa-aaaaa-aaaea-cai"),)); // nns
assert!(is_system_subnet(principal!("rdmx6-jaaaa-aaaaa-aaadq-cai"))); // identity
assert!(!is_system_subnet(principal!("oydqf-haaaa-aaaao-afpsa-cai"))); // something else
fn system_canister_allowed_on_system_domain() {
let info = test_snapshot();
assert!(matcher().check(principal!(CANISTER_SYSTEM), &fqdn!("ic0.app"), &info));
}

#[test]
fn test_domain_canister_match() {
let mut pic = AHashSet::new();
pic.insert(principal!("2dcn6-oqaaa-aaaai-abvoq-cai"));
fn system_canister_rejected_on_app_domain() {
let info = test_snapshot();
assert!(!matcher().check(principal!(CANISTER_SYSTEM), &fqdn!("icp0.io"), &info));
}

let dcm = DomainCanisterMatcher::new(pic, vec![fqdn!("icp0.io")], vec![fqdn!("ic0.app")]);
#[test]
fn engine_canister_allowed_on_engine_domain() {
let info = test_snapshot();
assert!(matcher().check(principal!(CANISTER_ENGINE), &fqdn!("engine.io"), &info));
}

assert!(dcm.check(
principal!("qoctq-giaaa-aaaaa-aaaea-cai"), // nns on system domain
&fqdn!("ic0.app"),
));
#[test]
fn engine_canister_rejected_on_app_domain() {
let info = test_snapshot();
assert!(!matcher().check(principal!(CANISTER_ENGINE), &fqdn!("icp0.io"), &info));
}

assert!(!dcm.check(
principal!("s6hwe-laaaa-aaaab-qaeba-cai"), // something else on system domain
&fqdn!("ic0.app"),
));
#[test]
fn app_canister_allowed_on_app_domain() {
let info = test_snapshot();
assert!(matcher().check(principal!(CANISTER_APP), &fqdn!("icp0.io"), &info));
}

assert!(dcm.check(
principal!("s6hwe-laaaa-aaaab-qaeba-cai"), // something else on app domain
&fqdn!("icp0.io"),
));
#[test]
fn app_canister_rejected_on_system_domain() {
let info = test_snapshot();
assert!(!matcher().check(principal!(CANISTER_APP), &fqdn!("ic0.app"), &info));
}

#[test]
fn pre_isolation_canister_allowed_on_any_domain() {
let info = test_snapshot();
assert!(matcher().check(principal!(CANISTER_PIC), &fqdn!("ic0.app"), &info));
assert!(matcher().check(principal!(CANISTER_PIC), &fqdn!("icp0.io"), &info));
assert!(matcher().check(principal!(CANISTER_PIC), &fqdn!("engine.io"), &info));
}

assert!(dcm.check(
principal!("2dcn6-oqaaa-aaaai-abvoq-cai"), // pre-isolation canister on system domain
&fqdn!("ic0.app"),
));
#[test]
fn empty_snapshot_falls_through_to_app_domain() {
let info = SubnetsInfo::default();
// With no snapshot, subnet type is unknown → app domain for everything
assert!(matcher().check(principal!(CANISTER_APP), &fqdn!("icp0.io"), &info));
assert!(!matcher().check(principal!(CANISTER_APP), &fqdn!("ic0.app"), &info));
}
}
1 change: 1 addition & 0 deletions src/routing/ic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod health_check;
pub mod http_service;
pub mod nodes_fetcher;
pub mod route_provider;
pub mod subnets_info;

use std::{fs, sync::Arc};

Expand Down
Loading