Skip to content

Commit 99c82ed

Browse files
authored
Merge pull request #181 from dfinity/shiling/engine
feat: add support of new type of subnet (cloud engine) in domain canister matcher
2 parents 379d19f + 44a6063 commit 99c82ed

File tree

12 files changed

+803
-73
lines changed

12 files changed

+803
-73
lines changed

Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ default = []
1010

1111
sev-snp = ["ic-bn-lib/sev-snp"]
1212
acme = ["ic-bn-lib/acme-dns", "ic-bn-lib/acme-alpn"]
13-
bench = ["dep:ic-http-certification", "dep:rand_regex", "dep:serde_cbor"]
13+
bench = ["dep:ic-http-certification", "dep:rand_regex"]
1414
tokio_console = ["dep:console-subscriber"]
1515
debug = []
1616

@@ -76,7 +76,7 @@ rustls = { version = "0.23.18", default-features = false, features = [
7676
"brotli",
7777
] }
7878
serde = "1.0.214"
79-
serde_cbor = { version = "0.11.2", optional = true }
79+
serde_cbor = "0.11.2"
8080
serde_json = "1.0.132"
8181
sha2 = "0.10.8"
8282
strum = { version = "0.27.1", features = ["derive"] }
@@ -155,3 +155,7 @@ path = "tools/create_acme_account.rs"
155155
[[bin]]
156156
name = "cloudflare-check"
157157
path = "tools/cloudflare_check.rs"
158+
159+
[[bin]]
160+
name = "gen-testdata"
161+
path = "tools/gen_testdata.rs"

src/cli.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,14 @@ pub struct Domain {
239239
#[clap(env, long, requires = "domain_system", value_delimiter = ',')]
240240
pub domain_app: Vec<FQDN>,
241241

242+
/// List of domains that serve cloud engines only
243+
#[clap(env, long, requires = "domain_app", value_delimiter = ',')]
244+
pub domain_engine: Vec<FQDN>,
245+
246+
/// How frequently to poll the NNS for subnet routing table and type information
247+
#[clap(env, long, default_value = "5m", value_parser = parse_duration)]
248+
pub subnets_info_poll_interval: Duration,
249+
242250
/// List of canister aliases in format '<alias>:<canister_id>'
243251
#[clap(env, long, value_delimiter = ',')]
244252
pub domain_canister_alias: Vec<CanisterAlias>,

src/core.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use ic_bn_lib::{
1414
vector::client::Vector,
1515
};
1616
use ic_bn_lib_common::{
17+
principal,
1718
traits::{custom_domains::ProvidesCustomDomains, tls::ProvidesCertificates},
1819
types::{
1920
dns::Options as DnsOptions,
@@ -29,10 +30,13 @@ use tracing_subscriber::{EnvFilter, reload::Handle};
2930
use crate::{
3031
cli::Cli,
3132
metrics,
33+
routing::ic::subnets_info::SubnetsInfoFetcher,
3234
routing::{
3335
self,
3436
ic::{
3537
create_agent,
38+
http_service::AgentHttpService,
39+
nodes_fetcher::MAINNET_ROOT_SUBNET_ID,
3640
route_provider::{RouteProviderWrapper, setup_route_provider},
3741
},
3842
},
@@ -238,6 +242,29 @@ pub async fn main(
238242
);
239243
}
240244

245+
// Subnet info: periodically fetch the full NNS routing table and subnet
246+
// types. Required for both system-subnet and engine-subnet routing
247+
// decisions in DomainCanisterMatcher.
248+
let http_service = Arc::new(AgentHttpService::new(
249+
http_client_hyper.clone(),
250+
cli.ic.ic_request_retry_interval,
251+
));
252+
let agent = create_agent(cli, http_service, route_provider.clone())
253+
.await
254+
.context("unable to create agent for subnets info fetcher")?;
255+
256+
let root_subnet_id = principal!(MAINNET_ROOT_SUBNET_ID);
257+
258+
let fetcher = Arc::new(SubnetsInfoFetcher::new(Arc::new(agent), root_subnet_id));
259+
let subnets_info = fetcher.info.clone();
260+
261+
health_manager.add(fetcher.clone());
262+
tasks.add_interval(
263+
"subnets_info_fetcher",
264+
fetcher,
265+
cli.domain.subnets_info_poll_interval,
266+
);
267+
241268
// Setup WAF
242269
let waf_layer = if cli.waf.waf_enable {
243270
let v = WafLayer::new_from_cli(&cli.waf, Some(http_client.clone()))
@@ -265,6 +292,7 @@ pub async fn main(
265292
vector.clone(),
266293
waf_layer,
267294
custom_domains_router,
295+
subnets_info,
268296
)
269297
.await
270298
.context("unable to setup Axum router")?;

src/policy/domain_canister.rs

Lines changed: 140 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,44 @@
1+
use std::sync::Arc;
2+
13
use ahash::AHashSet;
4+
use arc_swap::ArcSwapOption;
25
use candid::Principal;
36
use fqdn::{FQDN, Fqdn};
47

5-
// System subnets routing table
6-
pub const SYSTEM_SUBNETS: [(Principal, Principal); 5] = [
7-
(
8-
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01]),
9-
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 0x01]),
10-
),
11-
(
12-
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x01, 0x01]),
13-
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x01, 0x01]),
14-
),
15-
(
16-
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x01, 0x01]),
17-
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0x01, 0x01]),
18-
),
19-
(
20-
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x01, 0xa0, 0x00, 0x00, 0x01, 0x01]),
21-
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x01, 0xaf, 0xff, 0xff, 0x01, 0x01]),
22-
),
23-
(
24-
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x02, 0x10, 0x00, 0x00, 0x01, 0x01]),
25-
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x02, 0x1f, 0xff, 0xff, 0x01, 0x01]),
26-
),
27-
];
28-
29-
/// Checks if given canister id belongs to a system subnet
30-
pub fn is_system_subnet(canister_id: Principal) -> bool {
31-
SYSTEM_SUBNETS
32-
.iter()
33-
.any(|x| canister_id >= x.0 && canister_id <= x.1)
34-
}
8+
use crate::routing::ic::subnets_info::{SubnetType, SubnetsInfo};
359

3610
/// Things needed to verify domain-canister match
3711
#[derive(derive_new::new)]
3812
pub struct DomainCanisterMatcher {
3913
pre_isolation_canisters: AHashSet<Principal>,
4014
domains_app: Vec<FQDN>,
4115
domains_system: Vec<FQDN>,
16+
domains_engine: Vec<FQDN>,
17+
subnets_info: Arc<ArcSwapOption<SubnetsInfo>>,
4218
}
4319

4420
impl DomainCanisterMatcher {
45-
/// Check if given canister id and host match from policy perspective
21+
/// Check if given canister id and host match from policy perspective.
4622
pub fn check(&self, canister_id: Principal, host: &Fqdn) -> bool {
47-
// These are always allowed
48-
if self.pre_isolation_canisters.contains(&canister_id) {
23+
let guard = self.subnets_info.load();
24+
// Compute subnet type once; `None` when no snapshot has been stored yet.
25+
let subnet_type = guard.as_deref().and_then(|si| si.subnet_type(canister_id));
26+
27+
// Pre-isolation canisters are exempt from domain checks, unless they are
28+
// on a CloudEngine subnet, where the normal domain policy still applies.
29+
if self.pre_isolation_canisters.contains(&canister_id)
30+
&& subnet_type != Some(SubnetType::CloudEngine)
31+
{
4932
return true;
5033
}
5134

52-
let domains = if is_system_subnet(canister_id) {
53-
&self.domains_system
54-
} else {
55-
&self.domains_app
35+
let domains = match subnet_type {
36+
Some(SubnetType::System) => &self.domains_system,
37+
Some(SubnetType::CloudEngine) => &self.domains_engine,
38+
Some(SubnetType::Application)
39+
| Some(SubnetType::VerifiedApplication)
40+
| Some(SubnetType::Unknown)
41+
| None => &self.domains_app,
5642
};
5743

5844
domains.iter().any(|x| host.is_subdomain_of(x))
@@ -61,43 +47,136 @@ impl DomainCanisterMatcher {
6147

6248
#[cfg(test)]
6349
mod tests {
50+
use ahash::AHashMap;
51+
use arc_swap::ArcSwapOption;
6452
use fqdn::fqdn;
6553
use ic_bn_lib_common::principal;
6654

6755
use super::*;
56+
use crate::routing::ic::subnets_info::SubnetType;
57+
58+
use crate::test::TEST_ROOT_SUBNET_ID;
59+
60+
// Principals used as subnet IDs in the test snapshot
61+
const SUBNET_SYSTEM: &str = TEST_ROOT_SUBNET_ID;
62+
const SUBNET_ENGINE: &str = "nl6hn-ja4yw-wvmpy-3z2jx-ymc34-pisx3-3cp5z-3oj4a-qzzny-jbsv3-4qe";
63+
64+
// Canisters that fall inside the ranges defined below
65+
const CANISTER_SYSTEM: &str = "qoctq-giaaa-aaaaa-aaaea-cai"; // NNS
66+
const CANISTER_ENGINE: &str = "s6hwe-laaaa-aaaab-qaeba-cai";
67+
const CANISTER_APP: &str = "oydqf-haaaa-aaaao-afpsa-cai";
68+
const CANISTER_PIC: &str = "2dcn6-oqaaa-aaaai-abvoq-cai"; // pre-isolation
69+
70+
fn test_snapshot() -> Arc<ArcSwapOption<SubnetsInfo>> {
71+
let subnet_system = principal!(SUBNET_SYSTEM);
72+
let subnet_engine = principal!(SUBNET_ENGINE);
73+
74+
let ranges = vec![
75+
(
76+
principal!(CANISTER_SYSTEM),
77+
principal!(CANISTER_SYSTEM),
78+
subnet_system,
79+
),
80+
(
81+
principal!(CANISTER_ENGINE),
82+
principal!(CANISTER_ENGINE),
83+
subnet_engine,
84+
),
85+
];
86+
87+
let mut types = AHashMap::new();
88+
types.insert(subnet_system, SubnetType::System);
89+
types.insert(subnet_engine, SubnetType::CloudEngine);
90+
91+
Arc::new(ArcSwapOption::new(Some(Arc::new(SubnetsInfo::new(
92+
ranges, types,
93+
)))))
94+
}
95+
96+
fn matcher() -> DomainCanisterMatcher {
97+
let mut pic = AHashSet::new();
98+
pic.insert(principal!(CANISTER_PIC));
99+
100+
DomainCanisterMatcher::new(
101+
pic,
102+
vec![fqdn!("icp0.io")], // app
103+
vec![fqdn!("ic0.app")], // system
104+
vec![fqdn!("engine.io")], // engine
105+
test_snapshot(),
106+
)
107+
}
68108

69109
#[test]
70-
fn test_is_system_subnet() {
71-
assert!(is_system_subnet(principal!("qoctq-giaaa-aaaaa-aaaea-cai"),)); // nns
72-
assert!(is_system_subnet(principal!("rdmx6-jaaaa-aaaaa-aaadq-cai"))); // identity
73-
assert!(!is_system_subnet(principal!("oydqf-haaaa-aaaao-afpsa-cai"))); // something else
110+
fn system_canister_allowed_on_system_domain() {
111+
assert!(matcher().check(principal!(CANISTER_SYSTEM), &fqdn!("ic0.app")));
74112
}
75113

76114
#[test]
77-
fn test_domain_canister_match() {
78-
let mut pic = AHashSet::new();
79-
pic.insert(principal!("2dcn6-oqaaa-aaaai-abvoq-cai"));
115+
fn system_canister_rejected_on_app_domain() {
116+
assert!(!matcher().check(principal!(CANISTER_SYSTEM), &fqdn!("icp0.io")));
117+
}
80118

81-
let dcm = DomainCanisterMatcher::new(pic, vec![fqdn!("icp0.io")], vec![fqdn!("ic0.app")]);
119+
#[test]
120+
fn engine_canister_allowed_on_engine_domain() {
121+
assert!(matcher().check(principal!(CANISTER_ENGINE), &fqdn!("engine.io")));
122+
}
82123

83-
assert!(dcm.check(
84-
principal!("qoctq-giaaa-aaaaa-aaaea-cai"), // nns on system domain
85-
&fqdn!("ic0.app"),
86-
));
124+
#[test]
125+
fn engine_canister_rejected_on_app_domain() {
126+
assert!(!matcher().check(principal!(CANISTER_ENGINE), &fqdn!("icp0.io")));
127+
}
87128

88-
assert!(!dcm.check(
89-
principal!("s6hwe-laaaa-aaaab-qaeba-cai"), // something else on system domain
90-
&fqdn!("ic0.app"),
91-
));
129+
#[test]
130+
fn app_canister_allowed_on_app_domain() {
131+
assert!(matcher().check(principal!(CANISTER_APP), &fqdn!("icp0.io")));
132+
}
92133

93-
assert!(dcm.check(
94-
principal!("s6hwe-laaaa-aaaab-qaeba-cai"), // something else on app domain
95-
&fqdn!("icp0.io"),
96-
));
134+
#[test]
135+
fn app_canister_rejected_on_system_domain() {
136+
assert!(!matcher().check(principal!(CANISTER_APP), &fqdn!("ic0.app")));
137+
}
97138

98-
assert!(dcm.check(
99-
principal!("2dcn6-oqaaa-aaaai-abvoq-cai"), // pre-isolation canister on system domain
100-
&fqdn!("ic0.app"),
101-
));
139+
#[test]
140+
fn pre_isolation_canister_allowed_on_non_engine_subnet_domains() {
141+
// CANISTER_PIC is not on a CloudEngine subnet, so it bypasses domain checks
142+
assert!(matcher().check(principal!(CANISTER_PIC), &fqdn!("ic0.app")));
143+
assert!(matcher().check(principal!(CANISTER_PIC), &fqdn!("icp0.io")));
144+
assert!(matcher().check(principal!(CANISTER_PIC), &fqdn!("engine.io")));
145+
}
146+
147+
#[test]
148+
fn pre_isolation_canister_on_engine_subnet_subject_to_domain_policy() {
149+
// Even if CANISTER_ENGINE is in the pre-isolation set, CloudEngine subnet
150+
// canisters must still use the engine domain.
151+
let mut pic = AHashSet::new();
152+
pic.insert(principal!(CANISTER_ENGINE));
153+
// Reuse test_snapshot() — CANISTER_ENGINE already maps to CloudEngine there.
154+
let m = DomainCanisterMatcher::new(
155+
pic,
156+
vec![fqdn!("icp0.io")],
157+
vec![fqdn!("ic0.app")],
158+
vec![fqdn!("engine.io")],
159+
test_snapshot(),
160+
);
161+
assert!(m.check(principal!(CANISTER_ENGINE), &fqdn!("engine.io")));
162+
assert!(!m.check(principal!(CANISTER_ENGINE), &fqdn!("icp0.io")));
163+
assert!(!m.check(principal!(CANISTER_ENGINE), &fqdn!("ic0.app")));
164+
}
165+
166+
#[test]
167+
fn empty_snapshot_falls_through_to_app_domain() {
168+
let empty = Arc::new(ArcSwapOption::<SubnetsInfo>::empty());
169+
let mut pic = AHashSet::new();
170+
pic.insert(principal!(CANISTER_PIC));
171+
let m = DomainCanisterMatcher::new(
172+
pic,
173+
vec![fqdn!("icp0.io")],
174+
vec![fqdn!("ic0.app")],
175+
vec![fqdn!("engine.io")],
176+
empty,
177+
);
178+
// With no snapshot, subnet type is unknown → app domain for everything
179+
assert!(m.check(principal!(CANISTER_APP), &fqdn!("icp0.io")));
180+
assert!(!m.check(principal!(CANISTER_APP), &fqdn!("ic0.app")));
102181
}
103182
}

src/routing/ic/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod health_check;
66
pub mod http_service;
77
pub mod nodes_fetcher;
88
pub mod route_provider;
9+
pub mod subnets_info;
910

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

0 commit comments

Comments
 (0)