@push.rocks/smartnftables 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +18 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +4 -4
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +4 -4
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.manageddockerforwarding.d.ts +17 -0
- package/dist_ts/classes.manageddockerforwarding.js +128 -0
- package/dist_ts/classes.managednftables.d.ts +2 -0
- package/dist_ts/classes.managednftables.js +2 -2
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/managed.docker.types.d.ts +78 -0
- package/dist_ts/managed.docker.types.js +2 -0
- package/dist_ts/managed.egress.types.d.ts +9 -1
- package/package.json +1 -1
- package/readme.md +100 -6
- package/rust/src/docker.frontend.rs +203 -0
- package/rust/src/docker.graph.rs +129 -0
- package/rust/src/docker.policy.rs +140 -0
- package/rust/src/docker.rs +191 -0
- package/rust/src/docker_process_tests.rs +29 -0
- package/rust/src/docker_tests.rs +148 -0
- package/rust/src/egress.compile.rs +3 -0
- package/rust/src/egress.poolguard.rs +30 -0
- package/rust/src/egress.rs +27 -0
- package/rust/src/egress_tests.rs +85 -0
- package/rust/src/main.rs +10 -5
- package/rust/src/managed.rs +4 -0
- package/rust/src/owner.rs +19 -1
- package/rust/src/owner_egress_tests.rs +1 -1
- package/rust/src/owner_egress_traffic_tests.rs +21 -10
- package/rust/src/owner_host_traffic_tests.rs +4 -10
- package/rust/src/owner_poolguard_tests.rs +265 -0
- package/rust/src/owner_tests.rs +3 -0
- package/rust/src/wire.links.rs +11 -0
- package/rust/src/wire.rs +14 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.manageddockerforwarding.ts +113 -0
- package/ts/classes.managednftables.ts +1 -1
- package/ts/index.ts +2 -0
- package/ts/managed.docker.types.ts +49 -0
- package/ts/managed.egress.types.ts +10 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
#[test]
|
|
4
|
+
fn frontend_subprocess_drains_large_input_and_output_with_a_clean_environment() {
|
|
5
|
+
let input="bounded-input\n".repeat(12_000);
|
|
6
|
+
assert_eq!(command("/usr/bin/cat",&[],input.as_bytes(),deadline()).unwrap(),input);
|
|
7
|
+
let environment=command("/usr/bin/env",&[],&[],deadline()).unwrap();
|
|
8
|
+
let values:BTreeSet<_>=environment.lines().collect();
|
|
9
|
+
assert_eq!(values,BTreeSet::from(["PATH=/usr/sbin:/usr/bin:/sbin:/bin","LC_ALL=C"]));
|
|
10
|
+
}
|
|
11
|
+
fn child_failure(kind: &str, source: &str, expected: Error) {
|
|
12
|
+
let directory=std::env::temp_dir().join(format!("smartnftables-command-{}-{kind}",std::process::id()));
|
|
13
|
+
std::fs::create_dir(&directory).unwrap();
|
|
14
|
+
let pidfile=directory.join("pid");
|
|
15
|
+
let source=format!("import os,pathlib,time;pathlib.Path({:?}).write_text(str(os.getpid()));{source}",pidfile.to_str().unwrap());
|
|
16
|
+
assert_eq!(command("/usr/bin/python3",&["-c",&source],&[],deadline()),Err(expected));
|
|
17
|
+
let pid=std::fs::read_to_string(&pidfile).unwrap().parse::<i32>().unwrap();
|
|
18
|
+
assert_eq!(unsafe{libc::kill(pid,0)},-1);
|
|
19
|
+
assert_eq!(std::io::Error::last_os_error().raw_os_error(),Some(libc::ESRCH));
|
|
20
|
+
std::fs::remove_file(pidfile).unwrap();std::fs::remove_dir(directory).unwrap();
|
|
21
|
+
}
|
|
22
|
+
#[test]
|
|
23
|
+
fn frontend_subprocess_output_overflow_stops_and_joins_the_exact_child() {
|
|
24
|
+
child_failure("output","os.write(1,b'x'*2097153);time.sleep(60)",Error::Protocol);
|
|
25
|
+
}
|
|
26
|
+
#[test]
|
|
27
|
+
fn frontend_subprocess_timeout_stops_and_joins_the_exact_child() {
|
|
28
|
+
child_failure("timeout","time.sleep(60)",Error::Unavailable);
|
|
29
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
use serde_json::json;
|
|
3
|
+
|
|
4
|
+
fn options() -> Options {Options {owner_id:"dockerfixture".into(),instance_id:"qualification_20260913".into()}}
|
|
5
|
+
fn policy() -> policy::Policy {
|
|
6
|
+
let prepared:crate::managed::Prepared=serde_json::from_value::<crate::egress::Policy>(crate::egress::tests::host()).unwrap().prepare().unwrap().into();
|
|
7
|
+
policy::Policy {schema_version:1,revision:1,barrier:owner::Applied {
|
|
8
|
+
receipt:owner::Receipt {identity:owner::Identity {owner_id:"barrier".into(),instance_id:"qualification_20260913".into(),table_name:"snft_fixture".into(),boot_id:"00000000-0000-4000-8000-000000000000".into(),namespace_device:"4".into(),namespace_inode:"1".into()},table_handle:"1".into(),revision:1,digest:prepared.digest.clone()},prepared}}
|
|
9
|
+
}
|
|
10
|
+
fn text(rules: &[policy::Rule]) -> String {
|
|
11
|
+
let mut text="*filter\n:INPUT ACCEPT [0:0]\n:FORWARD DROP [0:0]\n:OUTPUT ACCEPT [0:0]\n:DOCKER-USER - [0:0]\n:DOCKER-FORWARD - [0:0]\n-A FORWARD -j DOCKER-USER\n-A FORWARD -j DOCKER-FORWARD\n".to_owned();
|
|
12
|
+
for rule in rules {text.push_str(&format!("-A DOCKER-USER {}\n",rule.args.join(" ")));}
|
|
13
|
+
text.push_str("-A DOCKER-USER -m comment --comment \"foreign unrelated rule\" -j RETURN\nCOMMIT\n");text
|
|
14
|
+
}
|
|
15
|
+
fn snapshot(text: &str) -> frontend::Snapshot {frontend::Snapshot::parse("v1.8.11 (nf_tables)".into(),text).unwrap()}
|
|
16
|
+
|
|
17
|
+
#[test]
|
|
18
|
+
fn docker_compiler_derives_only_scoped_directed_ranges_from_host_barrier() {
|
|
19
|
+
let prepared=policy().prepare("v1.8.11 (nf_tables)".into()).unwrap();let rules=prepared.rules(&options()).unwrap();
|
|
20
|
+
assert_eq!(rules.len(),6);
|
|
21
|
+
assert!(rules.iter().all(|r|r.args.contains(&"--ctorigsrcport".into()) && r.args.contains(&"--ctdir".into()) && !r.args.iter().any(|s|s.contains("RELATED"))));
|
|
22
|
+
let new_tcp=&rules[0].args;
|
|
23
|
+
assert!(new_tcp.windows(3).any(|w|w==["--tcp-flags","FIN,SYN,RST,ACK","SYN"]));
|
|
24
|
+
assert!(rules.iter().all(|r|r.comment.len()<256));
|
|
25
|
+
assert!(rules.iter().all(|r|r.args.windows(2).any(|w|w[0]=="--ctorigsrc" && !w[1].contains('/'))));
|
|
26
|
+
assert!(snapshot(&text(&rules)).matches(&options(),&rules).unwrap());
|
|
27
|
+
}
|
|
28
|
+
#[test]
|
|
29
|
+
fn docker_prepare_rejects_router_receipts_bad_hashes_and_unbounded_restore() {
|
|
30
|
+
let mut value=policy();value.schema_version=2;assert!(value.prepare("v1.8.11 (nf_tables)".into()).is_err());
|
|
31
|
+
let mut value=policy();value.barrier.receipt.digest="sha256:wrong".into();assert!(value.prepare("v1.8.11 (nf_tables)".into()).is_err());
|
|
32
|
+
let mut value=policy();value.barrier.prepared.policy=crate::managed::Policy::Egress(serde_json::from_value(crate::egress::tests::router()).unwrap());assert!(value.prepare("v1.8.11 (nf_tables)".into()).is_err());
|
|
33
|
+
let value=serde_json::to_value(policy()).unwrap();
|
|
34
|
+
let mut extra=value.clone();extra["rules"]=json!([]);assert!(serde_json::from_value::<policy::Policy>(extra).is_err());
|
|
35
|
+
let mut extra=value;extra["barrier"]["receipt"]["tableHandle"]=json!("01");assert!(serde_json::from_value::<policy::Policy>(extra).unwrap().prepare("v1.8.11 (nf_tables)".into()).is_err());
|
|
36
|
+
}
|
|
37
|
+
#[test]
|
|
38
|
+
fn docker_policy_digest_binds_barrier_handle_and_revision() {
|
|
39
|
+
let expected=policy().prepare("v1.8.11 (nf_tables)".into()).unwrap();
|
|
40
|
+
assert_ne!(policy().prepare("v1.8.10 (nf_tables)".into()).unwrap().digest,expected.digest);
|
|
41
|
+
for backend in ["v1.8. (nf_tables)","v1.8.9 (nf_tables)","v1.8.11 (legacy)","v1.8.011 (nf_tables)"] {
|
|
42
|
+
assert!(policy().prepare(backend.into()).is_err());
|
|
43
|
+
}
|
|
44
|
+
let mut value=policy();value.barrier.receipt.table_handle="2".into();assert_ne!(value.prepare("v1.8.11 (nf_tables)".into()).unwrap().digest,expected.digest);
|
|
45
|
+
let mut value=policy();value.revision=2;assert_ne!(value.prepare("v1.8.11 (nf_tables)".into()).unwrap().digest,expected.digest);
|
|
46
|
+
let mut forged=expected;forged.policy.revision=2;assert!(forged.validate().is_err());
|
|
47
|
+
}
|
|
48
|
+
#[test]
|
|
49
|
+
fn docker_snapshot_accepts_only_equivalent_frontend_rendering() {
|
|
50
|
+
let rules=policy().prepare("v1.8.11 (nf_tables)".into()).unwrap().rules(&options()).unwrap();
|
|
51
|
+
let original=text(&rules);
|
|
52
|
+
let rendered=original.replace("--ctorigsrc 10.240.0.2/32","--ctorigsrc 10.240.0.2");
|
|
53
|
+
assert!(snapshot(&rendered).matches(&options(),&rules).unwrap());
|
|
54
|
+
for (before,after) in [("--ctdir ORIGINAL","--ctdir REPLY"),("--ctstate NEW","--ctstate RELATED"),("--sport 10000:10999","--sport 1:65535"),("-o ens18","-o foreign"),("-j ACCEPT","-j RETURN"),("FIN,SYN,RST,ACK SYN","FIN,SYN,RST,ACK ACK")] {
|
|
55
|
+
assert!(!snapshot(&original.replacen(before,after,1)).matches(&options(),&rules).unwrap());
|
|
56
|
+
}
|
|
57
|
+
assert!(snapshot(&original.replacen("-j ACCEPT","-m mark --mark 1 -j ACCEPT",1)).matches(&options(),&rules).is_err());
|
|
58
|
+
}
|
|
59
|
+
#[test]
|
|
60
|
+
fn docker_snapshot_rejects_duplicates_moved_owner_and_foreign_instance() {
|
|
61
|
+
let rules=policy().prepare("v1.8.11 (nf_tables)".into()).unwrap().rules(&options()).unwrap();
|
|
62
|
+
let original=text(&rules);
|
|
63
|
+
let duplicate=original.replace("COMMIT",&format!("-A DOCKER-USER {}\nCOMMIT",rules[0].args.join(" ")));
|
|
64
|
+
assert!(!snapshot(&duplicate).matches(&options(),&rules).unwrap());
|
|
65
|
+
let moved=original.replacen("-A DOCKER-USER", "-A INPUT",1);
|
|
66
|
+
assert!(!snapshot(&moved).matches(&options(),&rules).unwrap());
|
|
67
|
+
assert!(!snapshot(&original.replace("qualification_20260913","foreign_instance_2026")).matches(&options(),&rules).unwrap());
|
|
68
|
+
assert_eq!(snapshot(&original.replace("snftd1:dockerfixture:","snftd1:anotherowner:")).matches(&options(),&[]),Err(Error::ForeignOwner));
|
|
69
|
+
let prepended=original.replacen("-A DOCKER-USER","-A DOCKER-USER -j RETURN\n-A DOCKER-USER",1);
|
|
70
|
+
assert!(!snapshot(&prepended).matches(&options(),&rules).unwrap());
|
|
71
|
+
assert!(!snapshot(&original).matches(&options(),&[]).unwrap());
|
|
72
|
+
}
|
|
73
|
+
#[test]
|
|
74
|
+
fn docker_snapshot_rejects_wrong_backend_path_policy_chain_order_and_table_framing() {
|
|
75
|
+
let original=text(&[]);
|
|
76
|
+
for changed in [original.replace(":FORWARD DROP",":FORWARD ACCEPT"),original.replace("-A FORWARD -j DOCKER-USER","-A FORWARD -j ACCEPT"),original.replace("*filter","*nat"),original.replace("COMMIT","COMMIT\n*filter"),original.replace(":DOCKER-USER - [0:0]\n","")] {
|
|
77
|
+
assert!(frontend::Snapshot::parse("v1.8.11 (nf_tables)".into(),&changed).is_err());
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[test]
|
|
82
|
+
fn docker_raw_graph_rejects_early_verdicts_reordered_matches_and_hidden_bytes() {
|
|
83
|
+
let rule=policy().prepare("v1.8.11 (nf_tables)".into()).unwrap().rules(&options()).unwrap().remove(0);
|
|
84
|
+
let expressions=graph::expected(&rule).unwrap();
|
|
85
|
+
let row=|expressions|vec![crate::wire::Attr::string(1,"filter"),crate::wire::Attr::string(2,"DOCKER-USER"),
|
|
86
|
+
crate::wire::Attr::u64(3,42),crate::wire::Attr::nested(4,expressions)];
|
|
87
|
+
assert!(graph::matches_rule(&row(expressions.clone()),&rule).unwrap());
|
|
88
|
+
let mut early=expressions.clone();let verdict=early.pop().unwrap();early.insert(0,verdict);
|
|
89
|
+
assert!(!graph::matches_rule(&row(early),&rule).unwrap());
|
|
90
|
+
let mut reordered=expressions.clone();reordered.swap(0,1);
|
|
91
|
+
assert!(!graph::matches_rule(&row(reordered),&rule).unwrap());
|
|
92
|
+
let mut moved_counter=expressions.clone();let length=moved_counter.len();moved_counter.swap(length-1,length-2);
|
|
93
|
+
assert!(!graph::matches_rule(&row(moved_counter),&rule).unwrap());
|
|
94
|
+
let mut hidden=expressions.clone();
|
|
95
|
+
for expression in &mut hidden {
|
|
96
|
+
let mut fields=crate::wire::attrs(&expression.value).unwrap();
|
|
97
|
+
if crate::wire::text(&fields,1).unwrap()!="match" {continue;}
|
|
98
|
+
let mut data=crate::wire::attrs(&crate::wire::one(&fields,2).unwrap().value).unwrap();
|
|
99
|
+
if crate::wire::text(&data,1).unwrap()!="conntrack" {continue;}
|
|
100
|
+
data.iter_mut().find(|a|a.id()==3).unwrap().value[20..32].fill(0);
|
|
101
|
+
*fields.iter_mut().find(|a|a.id()==2).unwrap()=crate::wire::Attr::nested(2,data);
|
|
102
|
+
expression.value=crate::wire::encode_attrs(&fields);
|
|
103
|
+
}
|
|
104
|
+
assert!(!graph::matches_rule(&row(hidden),&rule).unwrap());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[test]
|
|
108
|
+
fn docker_raw_graph_accepts_kernel_field_order_flags_and_changing_counters_only() {
|
|
109
|
+
let rule=policy().prepare("v1.8.11 (nf_tables)".into()).unwrap().rules(&options()).unwrap().remove(0);
|
|
110
|
+
let mut expressions=graph::expected(&rule).unwrap();
|
|
111
|
+
for expression in &mut expressions {
|
|
112
|
+
let mut fields=crate::wire::attrs(&expression.value).unwrap();
|
|
113
|
+
if crate::wire::text(&fields,1).unwrap()=="counter" {
|
|
114
|
+
*fields.iter_mut().find(|a|a.id()==2).unwrap()=crate::wire::Attr::nested(2,
|
|
115
|
+
vec![crate::wire::Attr::u64(2,123),crate::wire::Attr::u64(1,456)]);
|
|
116
|
+
}
|
|
117
|
+
fields.reverse();expression.value=crate::wire::encode_attrs(&fields);
|
|
118
|
+
}
|
|
119
|
+
fn kernel(attributes: &mut [crate::wire::Attr]) {
|
|
120
|
+
for attribute in attributes {
|
|
121
|
+
if attribute.kind&0x8000!=0 {
|
|
122
|
+
let mut children=crate::wire::attrs(&attribute.value).unwrap();kernel(&mut children);
|
|
123
|
+
attribute.value=crate::wire::encode_attrs(&children);
|
|
124
|
+
}
|
|
125
|
+
attribute.kind &= 0x3fff;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
kernel(&mut expressions);
|
|
129
|
+
let row=vec![crate::wire::Attr::string(1,"filter"),crate::wire::Attr::string(2,"DOCKER-USER"),
|
|
130
|
+
crate::wire::Attr::u64(3,42),crate::wire::Attr::nested(4,expressions)];
|
|
131
|
+
assert!(graph::matches_rule(&row,&rule).unwrap());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[test]
|
|
135
|
+
fn docker_forward_jumps_require_exact_unconditional_order() {
|
|
136
|
+
use crate::{policy::{expr,verdict},wire::Attr};
|
|
137
|
+
let row=|expressions|vec![Attr::string(1,"filter"),Attr::string(2,"FORWARD"),
|
|
138
|
+
Attr::u64(3,42),Attr::nested(4,expressions)];
|
|
139
|
+
for target in ["DOCKER-USER","DOCKER-FORWARD"] {
|
|
140
|
+
let expected=vec![expr("counter",vec![Attr::u64(1,7),Attr::u64(2,11)]),verdict(-3,Some(target))];
|
|
141
|
+
assert!(graph::matches_forward(&row(expected.clone()),target).unwrap());
|
|
142
|
+
let mut early=expected.clone();early.insert(0,verdict(1,None));
|
|
143
|
+
assert!(!graph::matches_forward(&row(early),target).unwrap());
|
|
144
|
+
let mut reordered=expected.clone();reordered.reverse();
|
|
145
|
+
assert!(!graph::matches_forward(&row(reordered),target).unwrap());
|
|
146
|
+
assert!(!graph::matches_forward(&row(expected),if target=="DOCKER-USER" {"DOCKER-FORWARD"} else {"DOCKER-USER"}).unwrap());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -4,6 +4,8 @@ use crate::wire::{self, Attr};
|
|
|
4
4
|
|
|
5
5
|
#[path = "egress.host.rs"]
|
|
6
6
|
mod host;
|
|
7
|
+
#[path = "egress.poolguard.rs"]
|
|
8
|
+
mod poolguard;
|
|
7
9
|
#[path = "egress.router.rs"]
|
|
8
10
|
mod router;
|
|
9
11
|
|
|
@@ -283,6 +285,7 @@ pub(super) fn compile(prepared: &Prepared, table: &str) -> Result<Vec<(u16, Vec<
|
|
|
283
285
|
router::compile(&mut program, scope, prepared.policy.revision)?
|
|
284
286
|
}
|
|
285
287
|
Scope::HostTransit(scope) => host::compile(&mut program, scope)?,
|
|
288
|
+
Scope::AllocationPoolGuard(scope) => poolguard::compile(&mut program, scope)?,
|
|
286
289
|
}
|
|
287
290
|
Ok(program.operations)
|
|
288
291
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
pub(super) fn compile(program: &mut Program<'_>, scope: &AllocationPoolGuardScope) -> Result<()> {
|
|
4
|
+
for (name, hook) in [("input", 1), ("forward", 2), ("output", 3)] {
|
|
5
|
+
program.chain(name, Some((hook, 0, "filter")))?;
|
|
6
|
+
}
|
|
7
|
+
for name in ["pool_guard", "pool_reply"] {
|
|
8
|
+
program.chain(name, None)?;
|
|
9
|
+
}
|
|
10
|
+
for name in ["input", "forward", "output"] {
|
|
11
|
+
program.jump(name, ipv4(), "pool_guard")?;
|
|
12
|
+
}
|
|
13
|
+
// An earlier owner's DNAT must not conceal the original protected target.
|
|
14
|
+
for prefix in &scope.prefixes {
|
|
15
|
+
program.end("pool_guard", original_address(prefix, false)?, 0)?;
|
|
16
|
+
}
|
|
17
|
+
// GOTO, not JUMP: RETURN from pool_reply must return to the base chain,
|
|
18
|
+
// skipping current-destination denial for legitimate reverse-SNAT replies.
|
|
19
|
+
let mut reply = ct(1, None, vec![1]);
|
|
20
|
+
reply.push(verdict(-4, Some("pool_reply")));
|
|
21
|
+
program.rule("pool_guard", reply)?;
|
|
22
|
+
// A missing conntrack tuple/direction breaks only its rule. Untracked or
|
|
23
|
+
// invalid packets still reach this unconditional current-destination guard.
|
|
24
|
+
for prefix in &scope.prefixes {
|
|
25
|
+
program.end("pool_guard", current_address(prefix, false)?, 0)?;
|
|
26
|
+
program.end("pool_reply", current_address(prefix, true)?, 0)?;
|
|
27
|
+
}
|
|
28
|
+
program.end("pool_guard", vec![], -5)?;
|
|
29
|
+
program.end("pool_reply", vec![], -5)
|
|
30
|
+
}
|
package/rust/src/egress.rs
CHANGED
|
@@ -119,10 +119,17 @@ pub struct HostScope {
|
|
|
119
119
|
pub snat_address: String,
|
|
120
120
|
}
|
|
121
121
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
122
|
+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
123
|
+
pub struct AllocationPoolGuardScope {
|
|
124
|
+
pub authority_digest: String,
|
|
125
|
+
pub prefixes: Vec<String>,
|
|
126
|
+
}
|
|
127
|
+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
122
128
|
#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
|
|
123
129
|
pub enum Scope {
|
|
124
130
|
RouterEgress(RouterScope),
|
|
125
131
|
HostTransit(HostScope),
|
|
132
|
+
AllocationPoolGuard(AllocationPoolGuardScope),
|
|
126
133
|
}
|
|
127
134
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
128
135
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
@@ -532,6 +539,23 @@ fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
|
532
539
|
)
|
|
533
540
|
}))
|
|
534
541
|
}
|
|
542
|
+
fn normalize_pool_guard(value: &mut AllocationPoolGuardScope) -> Result<()> {
|
|
543
|
+
require(
|
|
544
|
+
digest(&value.authority_digest) && !value.prefixes.is_empty() && value.prefixes.len() <= 64,
|
|
545
|
+
)?;
|
|
546
|
+
value.prefixes.sort();
|
|
547
|
+
for (index, item) in value.prefixes.iter().enumerate() {
|
|
548
|
+
require(
|
|
549
|
+
covers("10.0.0.0/8", item)?
|
|
550
|
+
|| covers("172.16.0.0/12", item)?
|
|
551
|
+
|| covers("192.168.0.0/16", item)?,
|
|
552
|
+
)?;
|
|
553
|
+
for previous in &value.prefixes[..index] {
|
|
554
|
+
require(!overlaps(item, previous)?)?;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
Ok(())
|
|
558
|
+
}
|
|
535
559
|
fn normalize_host(value: &mut HostScope) -> Result<()> {
|
|
536
560
|
normalize_protection(&mut value.protection)?;
|
|
537
561
|
normalize_link(&mut value.uplink)?;
|
|
@@ -642,6 +666,7 @@ impl Policy {
|
|
|
642
666
|
match &mut self.scope {
|
|
643
667
|
Scope::RouterEgress(scope) => normalize_router(scope, self.revision)?,
|
|
644
668
|
Scope::HostTransit(scope) => normalize_host(scope)?,
|
|
669
|
+
Scope::AllocationPoolGuard(scope) => normalize_pool_guard(scope)?,
|
|
645
670
|
}
|
|
646
671
|
Ok(self)
|
|
647
672
|
}
|
|
@@ -667,6 +692,8 @@ impl Prepared {
|
|
|
667
692
|
.map(|item| &item.link)
|
|
668
693
|
.chain(std::iter::once(&scope.uplink))
|
|
669
694
|
.collect(),
|
|
695
|
+
// No link/route dependency: this policy must apply before allocation.
|
|
696
|
+
Scope::AllocationPoolGuard(_) => return Ok(()),
|
|
670
697
|
};
|
|
671
698
|
crate::wire::verify_bound_interfaces(links.into_iter().map(|link| {
|
|
672
699
|
crate::wire::BoundInterface {
|
package/rust/src/egress_tests.rs
CHANGED
|
@@ -45,6 +45,11 @@ pub(crate) fn host() -> Value {
|
|
|
45
45
|
"handoffs":[{"link":link(4,"handoff","veth","10.240.0.1"),"allocations":[allocation()]}],
|
|
46
46
|
"uplink":link(2,"ens18","ethernet","192.0.2.2"),"snatAddress":"192.0.2.2"}})
|
|
47
47
|
}
|
|
48
|
+
pub(crate) fn pool_guard() -> Value {
|
|
49
|
+
json!({"schemaVersion":2,"revision":1,"scope":{"kind":"allocationPoolGuard",
|
|
50
|
+
"authorityDigest":format!("sha256:{}", "b".repeat(64)),
|
|
51
|
+
"prefixes":["10.241.0.0/16", "10.240.0.0/16"]}})
|
|
52
|
+
}
|
|
48
53
|
fn normalized(value: Value) -> super::Result<Policy> {
|
|
49
54
|
serde_json::from_value::<Policy>(value)
|
|
50
55
|
.map_err(|_| crate::Error::Invalid)?
|
|
@@ -330,3 +335,83 @@ fn managed_union_keeps_strict_duplicate_and_unknown_field_rejection() {
|
|
|
330
335
|
)
|
|
331
336
|
.is_err());
|
|
332
337
|
}
|
|
338
|
+
|
|
339
|
+
#[test]
|
|
340
|
+
fn allocation_pool_guard_is_canonical_authority_bound_and_private_only() {
|
|
341
|
+
let prepared = normalized(pool_guard()).unwrap().prepare().unwrap();
|
|
342
|
+
let mut reordered = pool_guard();
|
|
343
|
+
reordered["scope"]["prefixes"]
|
|
344
|
+
.as_array_mut()
|
|
345
|
+
.unwrap()
|
|
346
|
+
.reverse();
|
|
347
|
+
assert_eq!(normalized(reordered).unwrap().prepare().unwrap(), prepared);
|
|
348
|
+
let mut changed_authority = pool_guard();
|
|
349
|
+
changed_authority["scope"]["authorityDigest"] = json!(format!("sha256:{}", "c".repeat(64)));
|
|
350
|
+
assert_ne!(
|
|
351
|
+
normalized(changed_authority)
|
|
352
|
+
.unwrap()
|
|
353
|
+
.prepare()
|
|
354
|
+
.unwrap()
|
|
355
|
+
.digest,
|
|
356
|
+
prepared.digest
|
|
357
|
+
);
|
|
358
|
+
for prefixes in [
|
|
359
|
+
json!([]),
|
|
360
|
+
json!(["10.0.0.1/8"]),
|
|
361
|
+
json!(["10.0.0.0/8", "10.1.0.0/16"]),
|
|
362
|
+
json!(["10.0.0.0/8", "10.0.0.0/8"]),
|
|
363
|
+
json!(["0.0.0.0/0"]),
|
|
364
|
+
json!(["192.0.2.0/24"]),
|
|
365
|
+
json!(["127.0.0.0/8"]),
|
|
366
|
+
json!(["172.0.0.0/8"]),
|
|
367
|
+
json!(["192.168.0.0/15"]),
|
|
368
|
+
json!(["10.0.0.0/08"]),
|
|
369
|
+
json!(["::/0"]),
|
|
370
|
+
] {
|
|
371
|
+
assert!(normalized(changed(pool_guard(), "/scope/prefixes", prefixes)).is_err());
|
|
372
|
+
}
|
|
373
|
+
let mut extra = pool_guard();
|
|
374
|
+
extra["scope"]["handoffs"] = json!([]);
|
|
375
|
+
assert!(normalized(extra).is_err());
|
|
376
|
+
assert!(normalized(changed(
|
|
377
|
+
pool_guard(),
|
|
378
|
+
"/scope/authorityDigest",
|
|
379
|
+
json!("sha256:bad")
|
|
380
|
+
))
|
|
381
|
+
.is_err());
|
|
382
|
+
assert!(normalized(changed(
|
|
383
|
+
pool_guard(),
|
|
384
|
+
"/scope/prefixes",
|
|
385
|
+
json!(["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"])
|
|
386
|
+
))
|
|
387
|
+
.is_ok());
|
|
388
|
+
let duplicate = serde_json::to_string(&pool_guard())
|
|
389
|
+
.unwrap()
|
|
390
|
+
.replace("\"prefixes\":", "\"prefixes\":[],\"prefixes\":");
|
|
391
|
+
assert!(serde_json::from_str::<crate::managed::Policy>(&duplicate).is_err());
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#[test]
|
|
395
|
+
fn allocation_pool_guard_maximum_fits_the_atomic_graph_without_link_bindings() {
|
|
396
|
+
let prefixes: Vec<_> = (0..64).map(|part| format!("10.{part}.0.0/16")).collect();
|
|
397
|
+
let prepared = normalized(changed(pool_guard(), "/scope/prefixes", json!(prefixes)))
|
|
398
|
+
.unwrap()
|
|
399
|
+
.prepare()
|
|
400
|
+
.unwrap();
|
|
401
|
+
prepared.validate().unwrap();
|
|
402
|
+
prepared.validate_interfaces().unwrap();
|
|
403
|
+
let program = prepared
|
|
404
|
+
.program(&format!("snft_{}", "x".repeat(59)))
|
|
405
|
+
.unwrap();
|
|
406
|
+
let bytes: usize = program
|
|
407
|
+
.iter()
|
|
408
|
+
.map(|(_, attrs)| crate::wire::encode_attrs(attrs).len() + 20)
|
|
409
|
+
.sum();
|
|
410
|
+
assert!(program.len() <= 768 && bytes <= 100_000);
|
|
411
|
+
println!(
|
|
412
|
+
"POOL_GUARD_MAX prefixes=64 operations={} bytes={bytes}",
|
|
413
|
+
program.len()
|
|
414
|
+
);
|
|
415
|
+
let excessive: Vec<_> = (0..65).map(|part| format!("10.{part}.0.0/16")).collect();
|
|
416
|
+
assert!(normalized(changed(pool_guard(), "/scope/prefixes", json!(excessive))).is_err());
|
|
417
|
+
}
|
package/rust/src/main.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
mod egress;
|
|
2
|
+
mod docker;
|
|
2
3
|
mod managed;
|
|
3
4
|
mod namespace;
|
|
4
5
|
mod owner;
|
|
@@ -14,6 +15,7 @@ use std::io::{BufRead, Read, Write};
|
|
|
14
15
|
pub enum Error {
|
|
15
16
|
Invalid,
|
|
16
17
|
Unsupported,
|
|
18
|
+
UnsupportedBackend,
|
|
17
19
|
Permission,
|
|
18
20
|
ForeignOwner,
|
|
19
21
|
Conflict,
|
|
@@ -27,6 +29,7 @@ impl Error {
|
|
|
27
29
|
match self {
|
|
28
30
|
Self::Invalid => "INVALID",
|
|
29
31
|
Self::Unsupported => "UNSUPPORTED_KERNEL",
|
|
32
|
+
Self::UnsupportedBackend => "UNSUPPORTED_BACKEND",
|
|
30
33
|
Self::Permission => "PERMISSION",
|
|
31
34
|
Self::ForeignOwner => "FOREIGN_OWNER",
|
|
32
35
|
Self::Conflict => "CONFLICT",
|
|
@@ -101,13 +104,14 @@ fn dispatch(owner: &mut Option<owner::Owner>, request: Request) -> Result<serde_
|
|
|
101
104
|
|
|
102
105
|
fn main() {
|
|
103
106
|
let arguments = std::env::args().skip(1).collect::<Vec<_>>();
|
|
104
|
-
let descriptor = match arguments.as_slice() {
|
|
105
|
-
[management] if management == "--management" => None,
|
|
107
|
+
let (docker_mode, descriptor) = match arguments.as_slice() {
|
|
108
|
+
[management] if management == "--management" => (false, None),
|
|
109
|
+
[management] if management == "--docker-forwarding" => (true, None),
|
|
106
110
|
[management, option, fd]
|
|
107
|
-
if management == "--management" && option == "--network-namespace-fd" =>
|
|
111
|
+
if (management == "--management" || management == "--docker-forwarding") && option == "--network-namespace-fd" =>
|
|
108
112
|
{
|
|
109
113
|
match fd.parse::<i32>() {
|
|
110
|
-
Ok(value) => Some(value),
|
|
114
|
+
Ok(value) => (management == "--docker-forwarding", Some(value)),
|
|
111
115
|
Err(_) => std::process::exit(2),
|
|
112
116
|
}
|
|
113
117
|
}
|
|
@@ -125,6 +129,7 @@ fn main() {
|
|
|
125
129
|
output.flush().unwrap();
|
|
126
130
|
let mut input = std::io::stdin().lock();
|
|
127
131
|
let mut owner = None;
|
|
132
|
+
let mut docker = None;
|
|
128
133
|
loop {
|
|
129
134
|
let mut bytes = Vec::new();
|
|
130
135
|
let read = std::io::Read::by_ref(&mut input)
|
|
@@ -144,7 +149,7 @@ fn main() {
|
|
|
144
149
|
std::process::exit(2);
|
|
145
150
|
}
|
|
146
151
|
let id = request.id.clone();
|
|
147
|
-
let response = match dispatch(&mut owner, request) {
|
|
152
|
+
let response = match if docker_mode { docker::dispatch(&mut docker, request) } else { dispatch(&mut owner, request) } {
|
|
148
153
|
Ok(result) => serde_json::json!({"id":id,"success":true,"result":result}),
|
|
149
154
|
Err(error) => {
|
|
150
155
|
serde_json::json!({"id":id,"success":false,"error":"Managed policy operation failed.","errorCode":error.code()})
|
package/rust/src/managed.rs
CHANGED
|
@@ -34,6 +34,10 @@ impl Policy {
|
|
|
34
34
|
scope: egress::Scope::HostTransit(_),
|
|
35
35
|
..
|
|
36
36
|
}) => "hostTransitV2",
|
|
37
|
+
Self::Egress(egress::Policy {
|
|
38
|
+
scope: egress::Scope::AllocationPoolGuard(_),
|
|
39
|
+
..
|
|
40
|
+
}) => "allocationPoolGuardV2",
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
pub fn prepare(self) -> Result<Prepared> {
|
package/rust/src/owner.rs
CHANGED
|
@@ -70,7 +70,7 @@ pub struct Owner {
|
|
|
70
70
|
released: bool,
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
fn context() -> Result<(String, String, String)> {
|
|
73
|
+
pub(crate) fn context() -> Result<(String, String, String)> {
|
|
74
74
|
let boot = std::fs::read_to_string("/proc/sys/kernel/random/boot_id")
|
|
75
75
|
.map_err(|_| Error::Unavailable)?
|
|
76
76
|
.trim()
|
|
@@ -112,6 +112,24 @@ fn kernel_floor() -> Result<()> {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
impl Owner {
|
|
115
|
+
/// Read another dedicated owner's exact receipt without claiming its table.
|
|
116
|
+
/// PERSIST may retain enforcement after that owner's socket closes.
|
|
117
|
+
pub(crate) fn verify_external(expected: &Applied) -> Result<()> {
|
|
118
|
+
let identity = &expected.receipt.identity;
|
|
119
|
+
let mut reader = Self::new(Options {
|
|
120
|
+
owner_id: identity.owner_id.clone(),
|
|
121
|
+
instance_id: identity.instance_id.clone(),
|
|
122
|
+
table_name: identity.table_name.clone(),
|
|
123
|
+
})?;
|
|
124
|
+
reader.validate_receipt(expected)?;
|
|
125
|
+
let graph = reader.graph()?.ok_or(Error::Conflict)?;
|
|
126
|
+
reader.binding(&graph, Some(expected))?;
|
|
127
|
+
if !reader.matches(&graph, &expected.prepared)? {
|
|
128
|
+
return Err(Error::Conflict);
|
|
129
|
+
}
|
|
130
|
+
expected.prepared.validate_interfaces()
|
|
131
|
+
}
|
|
132
|
+
|
|
115
133
|
pub fn new(options: Options) -> Result<Self> {
|
|
116
134
|
if !identifier(&options.owner_id)
|
|
117
135
|
|| !identifier(&options.instance_id)
|
|
@@ -62,7 +62,7 @@ fn host() -> egress::Policy {
|
|
|
62
62
|
value["scope"]["uplink"] = binding("uplink", "192.0.2.2", "veth");
|
|
63
63
|
serde_json::from_value(value).unwrap()
|
|
64
64
|
}
|
|
65
|
-
fn recover(name: &str, policy: egress::Policy) {
|
|
65
|
+
pub(super) fn recover(name: &str, policy: egress::Policy) {
|
|
66
66
|
let prepared: Prepared = policy.clone().prepare().unwrap().into();
|
|
67
67
|
prepared.validate_interfaces().expect("fixture bindings");
|
|
68
68
|
let mut owner = Owner::new(options(name)).unwrap();
|
|
@@ -205,6 +205,18 @@ fn v2_four_namespace_private_tun_local_dns_egress_and_revocation() {
|
|
|
205
205
|
ns_ip(ROUTER, &["address", "add", "10.242.0.1/24", "dev", "vpn"]);
|
|
206
206
|
ns_ip(ROUTER, &["link", "set", "vpn", "up"]);
|
|
207
207
|
let mut device = in_namespace(ROUTER, tun);
|
|
208
|
+
let (pool_owner, pool_applied) = in_namespace(HOST, || {
|
|
209
|
+
let mut owner = Owner::new(options("pool_coexistence")).unwrap();
|
|
210
|
+
let policy: egress::Policy = serde_json::from_value(egress::tests::pool_guard()).unwrap();
|
|
211
|
+
let applied = owner
|
|
212
|
+
.reconcile(Transition {
|
|
213
|
+
previous: None,
|
|
214
|
+
target: policy.prepare().unwrap().into(),
|
|
215
|
+
})
|
|
216
|
+
.unwrap();
|
|
217
|
+
assert!(owner.inspect().enforced);
|
|
218
|
+
(owner, applied)
|
|
219
|
+
});
|
|
208
220
|
let (host_owner, host_applied) = in_namespace(HOST, || {
|
|
209
221
|
let mut owner = Owner::new(options("host_packets")).unwrap();
|
|
210
222
|
let applied = owner
|
|
@@ -260,11 +272,8 @@ fn v2_four_namespace_private_tun_local_dns_egress_and_revocation() {
|
|
|
260
272
|
});
|
|
261
273
|
let listener = in_namespace(PEER, || TcpListener::bind("192.0.2.3:3000").unwrap());
|
|
262
274
|
let mut client = in_namespace(WORK, || {
|
|
263
|
-
TcpStream::connect_timeout(
|
|
264
|
-
|
|
265
|
-
Duration::from_secs(1),
|
|
266
|
-
)
|
|
267
|
-
.unwrap()
|
|
275
|
+
TcpStream::connect_timeout(&"192.0.2.3:3000".parse().unwrap(), Duration::from_secs(1))
|
|
276
|
+
.unwrap()
|
|
268
277
|
});
|
|
269
278
|
let (mut server, peer) = listener.accept().unwrap();
|
|
270
279
|
assert_eq!(peer.ip().to_string(), "192.0.2.2");
|
|
@@ -290,11 +299,8 @@ fn v2_four_namespace_private_tun_local_dns_egress_and_revocation() {
|
|
|
290
299
|
assert_eq!(syn[33] & 0xc0, 0xc0);
|
|
291
300
|
assert!((10016..=10031).contains(&u16::from_be_bytes(syn[20..22].try_into().unwrap())));
|
|
292
301
|
let mut local_tcp = in_namespace(ROUTER, || {
|
|
293
|
-
TcpStream::connect_timeout(
|
|
294
|
-
|
|
295
|
-
Duration::from_secs(1),
|
|
296
|
-
)
|
|
297
|
-
.unwrap()
|
|
302
|
+
TcpStream::connect_timeout(&"192.0.2.3:3000".parse().unwrap(), Duration::from_secs(1))
|
|
303
|
+
.unwrap()
|
|
298
304
|
});
|
|
299
305
|
let (mut local_peer, local_outer) = listener.accept().unwrap();
|
|
300
306
|
assert!((10016..=10031).contains(&local_outer.port()));
|
|
@@ -466,5 +472,10 @@ fn v2_four_namespace_private_tun_local_dns_egress_and_revocation() {
|
|
|
466
472
|
let mut owner = host_owner;
|
|
467
473
|
owner.release(host_applied).unwrap();
|
|
468
474
|
});
|
|
475
|
+
in_namespace(HOST, move || {
|
|
476
|
+
let mut owner = pool_owner;
|
|
477
|
+
assert!(owner.inspect().enforced);
|
|
478
|
+
owner.release(pool_applied).unwrap();
|
|
479
|
+
});
|
|
469
480
|
println!("V2_PACKET_PROOF private_dns=true tun_private=true tun_public_denied=true workload_public=true local_dns_and_tcp=true handoff_udp_tcp_ranges=true ecn_syn=true opening_ack_rst_denied=true fragments_checked=true range_exhaustion_denied=true protected_grant_denied=true ungranted_local_denied=true retired_replies_denied=true revoked_egress_denied=true disjoint_generation_restored=true flows_drained=false reusable=false");
|
|
470
481
|
}
|
|
@@ -7,7 +7,7 @@ const SOURCE: &str = "hs_source";
|
|
|
7
7
|
const HOST: &str = "hs_host";
|
|
8
8
|
const PEER: &str = "hs_peer";
|
|
9
9
|
|
|
10
|
-
fn payload(offset: u32, bytes: Vec<u8>, transport: bool) -> Vec<Attr> {
|
|
10
|
+
pub(super) fn payload(offset: u32, bytes: Vec<u8>, transport: bool) -> Vec<Attr> {
|
|
11
11
|
vec![
|
|
12
12
|
expr(
|
|
13
13
|
"payload",
|
|
@@ -21,7 +21,7 @@ fn payload(offset: u32, bytes: Vec<u8>, transport: bool) -> Vec<Attr> {
|
|
|
21
21
|
compare(bytes),
|
|
22
22
|
]
|
|
23
23
|
}
|
|
24
|
-
fn immediate(value: Vec<u8>) -> Attr {
|
|
24
|
+
pub(super) fn immediate(value: Vec<u8>) -> Attr {
|
|
25
25
|
expr(
|
|
26
26
|
"immediate",
|
|
27
27
|
vec![
|
|
@@ -196,10 +196,7 @@ fn v2_host_rejects_foreign_dnat_zone_local_diversion_and_ipv6() {
|
|
|
196
196
|
HOST,
|
|
197
197
|
&["route", "add", "203.0.113.2/32", "via", "192.0.2.3"],
|
|
198
198
|
);
|
|
199
|
-
ns_ip(
|
|
200
|
-
HOST,
|
|
201
|
-
&["route", "add", "10.250.0.2/32", "via", "192.0.2.3"],
|
|
202
|
-
);
|
|
199
|
+
ns_ip(HOST, &["route", "add", "10.250.0.2/32", "via", "192.0.2.3"]);
|
|
203
200
|
for address in ["203.0.113.2/32", "10.250.0.2/32"] {
|
|
204
201
|
ns_ip(PEER, &["address", "add", address, "dev", "lo"]);
|
|
205
202
|
}
|
|
@@ -285,10 +282,7 @@ fn v2_host_rejects_foreign_dnat_zone_local_diversion_and_ipv6() {
|
|
|
285
282
|
control(source, destination, target, false);
|
|
286
283
|
}
|
|
287
284
|
let allowed = socket(SOURCE, "10.240.0.2:10000");
|
|
288
|
-
assert_eq!(
|
|
289
|
-
roundtrip(&allowed, &public).ip().to_string(),
|
|
290
|
-
"192.0.2.2"
|
|
291
|
-
);
|
|
285
|
+
assert_eq!(roundtrip(&allowed, &public).ip().to_string(), "192.0.2.2");
|
|
292
286
|
let platform = socket(PEER, "192.0.2.3:5300");
|
|
293
287
|
roundtrip(&allowed, &platform);
|
|
294
288
|
let unleased = socket(SOURCE, "10.240.0.2:9999");
|