1+ use std:: sync:: Arc ;
2+
13use ahash:: AHashSet ;
4+ use arc_swap:: ArcSwapOption ;
25use candid:: Principal ;
36use 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) ]
3812pub 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
4420impl 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) ]
6349mod 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}
0 commit comments