@push.rocks/smartnftables 1.4.0 → 1.6.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 +26 -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 +9 -7
- package/dist_ts/classes.managednftables.js +2 -2
- package/dist_ts/index.d.ts +3 -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 +91 -0
- package/dist_ts/managed.egress.types.js +2 -0
- package/dist_ts/managed.types.d.ts +20 -18
- package/package.json +2 -2
- package/readme.md +143 -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 +288 -0
- package/rust/src/egress.host.rs +103 -0
- package/rust/src/egress.router.rs +231 -0
- package/rust/src/egress.rs +682 -0
- package/rust/src/egress_tests.rs +332 -0
- package/rust/src/main.rs +13 -6
- package/rust/src/managed.rs +105 -0
- package/rust/src/owner.rs +115 -7
- package/rust/src/owner_coexistence_tests.rs +77 -0
- package/rust/src/owner_egress_identity_tests.rs +155 -0
- package/rust/src/owner_egress_tests.rs +153 -0
- package/rust/src/owner_egress_traffic_tests.rs +470 -0
- package/rust/src/owner_host_traffic_tests.rs +305 -0
- package/rust/src/owner_identity_tests.rs +169 -0
- package/rust/src/owner_link_tests.rs +119 -0
- package/rust/src/owner_packet_fixture.rs +200 -0
- package/rust/src/owner_tests.rs +33 -7
- package/rust/src/policy.rs +120 -81
- package/rust/src/tests.rs +22 -0
- package/rust/src/wire.links.rs +279 -0
- package/rust/src/wire.rs +16 -66
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.manageddockerforwarding.ts +113 -0
- package/ts/classes.managednftables.ts +13 -13
- package/ts/index.ts +3 -0
- package/ts/managed.docker.types.ts +49 -0
- package/ts/managed.egress.types.ts +85 -0
- package/ts/managed.types.ts +20 -16
package/rust/src/owner.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
use crate::
|
|
1
|
+
use crate::managed::Prepared;
|
|
2
|
+
use crate::policy::identifier;
|
|
2
3
|
use crate::wire::{self, Attr, Socket};
|
|
3
4
|
use crate::{Error, Result};
|
|
4
5
|
use serde::{Deserialize, Serialize};
|
|
@@ -69,7 +70,7 @@ pub struct Owner {
|
|
|
69
70
|
released: bool,
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
fn context() -> Result<(String, String, String)> {
|
|
73
|
+
pub(crate) fn context() -> Result<(String, String, String)> {
|
|
73
74
|
let boot = std::fs::read_to_string("/proc/sys/kernel/random/boot_id")
|
|
74
75
|
.map_err(|_| Error::Unavailable)?
|
|
75
76
|
.trim()
|
|
@@ -111,6 +112,24 @@ fn kernel_floor() -> Result<()> {
|
|
|
111
112
|
}
|
|
112
113
|
|
|
113
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
|
+
|
|
114
133
|
pub fn new(options: Options) -> Result<Self> {
|
|
115
134
|
if !identifier(&options.owner_id)
|
|
116
135
|
|| !identifier(&options.instance_id)
|
|
@@ -171,7 +190,15 @@ impl Owner {
|
|
|
171
190
|
Err(error) => return Err(error),
|
|
172
191
|
_ => return Err(Error::Protocol),
|
|
173
192
|
};
|
|
174
|
-
|
|
193
|
+
// Linux's NFT_MSG_GETCHAIN dump is family-wide: it ignores
|
|
194
|
+
// NFTA_CHAIN_TABLE. Select exact table identity before examining
|
|
195
|
+
// chain contents; another owner's chains are not our graph.
|
|
196
|
+
let mut chains = Vec::new();
|
|
197
|
+
for chain in socket.query(4, vec![Attr::string(1, &table_name)], true)? {
|
|
198
|
+
if wire::text(&chain, 1)? == table_name {
|
|
199
|
+
chains.push(chain);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
175
202
|
let rules = socket.query(7, vec![Attr::string(1, &table_name)], true)?;
|
|
176
203
|
// The managed compiler never creates any of these. Foreign objects reject.
|
|
177
204
|
for operation in [10, 19, 23] {
|
|
@@ -222,7 +249,7 @@ impl Owner {
|
|
|
222
249
|
if handle == 0
|
|
223
250
|
|| handle.to_string() != expected.receipt.table_handle
|
|
224
251
|
|| expected.receipt.identity != self.identity
|
|
225
|
-
|| expected.receipt.revision != expected.prepared.policy.revision
|
|
252
|
+
|| expected.receipt.revision != expected.prepared.policy.revision()
|
|
226
253
|
|| expected.receipt.digest != expected.prepared.digest
|
|
227
254
|
{
|
|
228
255
|
return Err(Error::Conflict);
|
|
@@ -316,7 +343,7 @@ impl Owner {
|
|
|
316
343
|
receipt: Receipt {
|
|
317
344
|
identity: self.identity.clone(),
|
|
318
345
|
table_handle: wire::handle(&graph.table, 4)?.to_string(),
|
|
319
|
-
revision: prepared.policy.revision,
|
|
346
|
+
revision: prepared.policy.revision(),
|
|
320
347
|
digest: prepared.digest.clone(),
|
|
321
348
|
},
|
|
322
349
|
prepared,
|
|
@@ -334,7 +361,9 @@ impl Owner {
|
|
|
334
361
|
}
|
|
335
362
|
if let Some(previous) = &transition.previous {
|
|
336
363
|
self.validate_receipt(previous)?;
|
|
337
|
-
if transition.target.policy.
|
|
364
|
+
if transition.target.policy.kind() != previous.prepared.policy.kind()
|
|
365
|
+
|| transition.target.policy.revision() <= previous.prepared.policy.revision()
|
|
366
|
+
{
|
|
338
367
|
return Err(Error::Conflict);
|
|
339
368
|
}
|
|
340
369
|
}
|
|
@@ -568,11 +597,13 @@ fn rule_identity(attributes: &[Attr]) -> Result<(String, Vec<u8>)> {
|
|
|
568
597
|
}
|
|
569
598
|
let name = wire::text(&fields, 1)?;
|
|
570
599
|
let mut data = wire::attrs(&wire::one(&fields, 2)?.value)?;
|
|
600
|
+
validate_egress_expression(&name, &data)?;
|
|
571
601
|
let nested: &[u16] = match name.as_str() {
|
|
572
602
|
"immediate" => &[2],
|
|
573
603
|
"cmp" => &[3],
|
|
574
604
|
"bitwise" => &[4, 5],
|
|
575
|
-
"
|
|
605
|
+
"range" => &[3, 4],
|
|
606
|
+
"meta" | "payload" | "ct" | "nat" => &[],
|
|
576
607
|
_ => return Err(Error::Conflict),
|
|
577
608
|
};
|
|
578
609
|
for attribute in &mut data {
|
|
@@ -599,6 +630,83 @@ fn rule_identity(attributes: &[Attr]) -> Result<(String, Vec<u8>)> {
|
|
|
599
630
|
Ok((chain, encoded))
|
|
600
631
|
}
|
|
601
632
|
|
|
633
|
+
/// These expressions are compiler-owned. Preserve all operands in graph identity;
|
|
634
|
+
/// neither unknown fields nor default repair may hide a changed kernel rule.
|
|
635
|
+
fn validate_egress_expression(name: &str, data: &[Attr]) -> Result<()> {
|
|
636
|
+
let allowed: &[u16] = match name {
|
|
637
|
+
"ct" | "range" => &[1, 2, 3, 4],
|
|
638
|
+
"nat" => &[1, 2, 3, 4, 5, 6, 7],
|
|
639
|
+
_ => return Ok(()),
|
|
640
|
+
};
|
|
641
|
+
for attribute in data {
|
|
642
|
+
if !allowed.contains(&attribute.id()) {
|
|
643
|
+
return Err(Error::Conflict);
|
|
644
|
+
}
|
|
645
|
+
wire::one(data, attribute.id())?; // Duplicate fields are never canonical identity.
|
|
646
|
+
}
|
|
647
|
+
match name {
|
|
648
|
+
"ct" => {
|
|
649
|
+
let read = data.iter().any(|item| item.id() == 1);
|
|
650
|
+
let write = data.iter().any(|item| item.id() == 4);
|
|
651
|
+
if read == write {
|
|
652
|
+
return Err(Error::Conflict);
|
|
653
|
+
}
|
|
654
|
+
wire::number(data, if read { 1 } else { 4 })?;
|
|
655
|
+
wire::number(data, 2)?;
|
|
656
|
+
if data.iter().any(|item| item.id() == 3)
|
|
657
|
+
&& ![vec![0], vec![1]].contains(&wire::one(data, 3)?.value)
|
|
658
|
+
{
|
|
659
|
+
return Err(Error::Conflict);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
"nat" => {
|
|
663
|
+
// Emit min/max registers and derived flags explicitly, just as Linux
|
|
664
|
+
// dumps them. This surface only compiles address-bearing IPv4 NAT.
|
|
665
|
+
for id in [1, 2, 3, 4, 7] {
|
|
666
|
+
wire::number(data, id)?;
|
|
667
|
+
}
|
|
668
|
+
if wire::number(data, 1)? > 1 || wire::number(data, 2)? != 2 {
|
|
669
|
+
return Err(Error::Conflict);
|
|
670
|
+
}
|
|
671
|
+
let first_port = data.iter().any(|item| item.id() == 5);
|
|
672
|
+
let last_port = data.iter().any(|item| item.id() == 6);
|
|
673
|
+
if first_port != last_port {
|
|
674
|
+
return Err(Error::Conflict);
|
|
675
|
+
}
|
|
676
|
+
if first_port {
|
|
677
|
+
wire::number(data, 5)?;
|
|
678
|
+
wire::number(data, 6)?;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
"range" => {
|
|
682
|
+
wire::number(data, 1)?;
|
|
683
|
+
if wire::number(data, 2)? > 1 || data.len() != 4 {
|
|
684
|
+
return Err(Error::Conflict);
|
|
685
|
+
}
|
|
686
|
+
let mut length = None;
|
|
687
|
+
for id in [3, 4] {
|
|
688
|
+
let value = wire::attrs(&wire::one(data, id)?.value)?;
|
|
689
|
+
if value.len() != 1 {
|
|
690
|
+
return Err(Error::Conflict);
|
|
691
|
+
}
|
|
692
|
+
let bytes = &wire::one(&value, 1)?.value;
|
|
693
|
+
if !(1..=16).contains(&bytes.len())
|
|
694
|
+
|| length.is_some_and(|size| size != bytes.len())
|
|
695
|
+
{
|
|
696
|
+
return Err(Error::Conflict);
|
|
697
|
+
}
|
|
698
|
+
length = Some(bytes.len());
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
_ => unreachable!(),
|
|
702
|
+
}
|
|
703
|
+
Ok(())
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
#[cfg(test)]
|
|
707
|
+
#[path = "owner_identity_tests.rs"]
|
|
708
|
+
mod identity_tests;
|
|
709
|
+
|
|
602
710
|
#[cfg(test)]
|
|
603
711
|
#[path = "owner_tests.rs"]
|
|
604
712
|
mod native_tests;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
#[test]
|
|
4
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
5
|
+
fn owner_scopes_family_wide_chain_dump_and_retains_foreign_table() {
|
|
6
|
+
isolated();
|
|
7
|
+
let mut foreign = Socket::open().unwrap();
|
|
8
|
+
let generation = foreign.generation().unwrap();
|
|
9
|
+
foreign
|
|
10
|
+
.batch(
|
|
11
|
+
generation,
|
|
12
|
+
vec![
|
|
13
|
+
(
|
|
14
|
+
0,
|
|
15
|
+
0x600,
|
|
16
|
+
vec![Attr::string(1, "foreign_table"), Attr::u32(2, 6)],
|
|
17
|
+
),
|
|
18
|
+
(
|
|
19
|
+
3,
|
|
20
|
+
0x600,
|
|
21
|
+
vec![Attr::string(1, "foreign_table"), Attr::string(3, "input")],
|
|
22
|
+
),
|
|
23
|
+
(
|
|
24
|
+
3,
|
|
25
|
+
0x600,
|
|
26
|
+
vec![
|
|
27
|
+
Attr::string(1, "foreign_table"),
|
|
28
|
+
Attr::string(3, "foreign_chain"),
|
|
29
|
+
],
|
|
30
|
+
),
|
|
31
|
+
],
|
|
32
|
+
)
|
|
33
|
+
.unwrap();
|
|
34
|
+
let before = foreign
|
|
35
|
+
.query(4, vec![Attr::string(1, "foreign_table")], true)
|
|
36
|
+
.unwrap();
|
|
37
|
+
assert_eq!(before.len(), 2);
|
|
38
|
+
let mut owner = Owner::new(options("coexistence")).unwrap();
|
|
39
|
+
let intent = Transition {
|
|
40
|
+
previous: None,
|
|
41
|
+
target: empty(1),
|
|
42
|
+
};
|
|
43
|
+
let applied = owner.reconcile(intent.clone()).unwrap();
|
|
44
|
+
assert!(owner.inspect().enforced);
|
|
45
|
+
// Prove the kernel ignores the requested chain table filter.
|
|
46
|
+
assert_eq!(
|
|
47
|
+
foreign
|
|
48
|
+
.query(4, vec![Attr::string(1, "foreign_table")], true)
|
|
49
|
+
.unwrap()
|
|
50
|
+
.len(),
|
|
51
|
+
5
|
|
52
|
+
);
|
|
53
|
+
assert_eq!(owner.graph().unwrap().unwrap().chains.len(), 3);
|
|
54
|
+
drop(owner);
|
|
55
|
+
let mut owner = Owner::new(options("coexistence")).unwrap();
|
|
56
|
+
assert_eq!(owner.reconcile(intent).unwrap(), applied);
|
|
57
|
+
let applied = owner
|
|
58
|
+
.reconcile(Transition {
|
|
59
|
+
previous: Some(applied),
|
|
60
|
+
target: empty(2),
|
|
61
|
+
})
|
|
62
|
+
.unwrap();
|
|
63
|
+
owner.release(applied).unwrap();
|
|
64
|
+
assert_eq!(
|
|
65
|
+
foreign
|
|
66
|
+
.query(4, vec![Attr::string(1, "foreign_table")], true)
|
|
67
|
+
.unwrap(),
|
|
68
|
+
before
|
|
69
|
+
);
|
|
70
|
+
let generation = foreign.generation().unwrap();
|
|
71
|
+
foreign
|
|
72
|
+
.batch(
|
|
73
|
+
generation,
|
|
74
|
+
vec![(2, 0, vec![Attr::string(1, "foreign_table")])],
|
|
75
|
+
)
|
|
76
|
+
.unwrap();
|
|
77
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
use super::isolated;
|
|
2
|
+
use crate::owner::{chain_identity, rule_identity};
|
|
3
|
+
use crate::wire::{self, Attr, Socket};
|
|
4
|
+
|
|
5
|
+
fn expression(name: &str, data: Vec<Attr>) -> Attr {
|
|
6
|
+
Attr::nested(1, vec![Attr::string(1, name), Attr::nested(2, data)])
|
|
7
|
+
}
|
|
8
|
+
fn immediate(register: u32, value: Vec<u8>) -> Attr {
|
|
9
|
+
expression(
|
|
10
|
+
"immediate",
|
|
11
|
+
vec![
|
|
12
|
+
Attr::u32(1, register),
|
|
13
|
+
Attr::nested(2, vec![Attr::bytes(1, value)]),
|
|
14
|
+
],
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
fn chain(name: &str, hook: u32, priority: i32, kind: &str) -> Vec<Attr> {
|
|
18
|
+
vec![
|
|
19
|
+
Attr::string(1, "snft_egress_identity"),
|
|
20
|
+
Attr::string(3, name),
|
|
21
|
+
Attr::nested(4, vec![Attr::u32(1, hook), Attr::u32(2, priority as u32)]),
|
|
22
|
+
Attr::u32(5, 1),
|
|
23
|
+
Attr::string(7, kind),
|
|
24
|
+
Attr::u32(10, 1),
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
fn rule(chain: &str, ordinal: u8, expressions: Vec<Attr>) -> Vec<Attr> {
|
|
28
|
+
vec![
|
|
29
|
+
Attr::string(1, "snft_egress_identity"),
|
|
30
|
+
Attr::string(2, chain),
|
|
31
|
+
Attr::nested(4, expressions),
|
|
32
|
+
Attr::bytes(7, vec![ordinal]),
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[test]
|
|
37
|
+
#[ignore = "requires the explicitly isolated QEMU qualification guest"]
|
|
38
|
+
fn native_ct_nat_range_graph_identity_matches_exact_linux_dump() {
|
|
39
|
+
isolated();
|
|
40
|
+
let chains = [
|
|
41
|
+
chain("raw", 0, -300, "filter"),
|
|
42
|
+
chain("forward", 2, 0, "filter"),
|
|
43
|
+
chain("post", 4, 100, "nat"),
|
|
44
|
+
];
|
|
45
|
+
let rules = [
|
|
46
|
+
rule(
|
|
47
|
+
"raw",
|
|
48
|
+
1,
|
|
49
|
+
vec![
|
|
50
|
+
immediate(1, 17_u16.to_ne_bytes().to_vec()),
|
|
51
|
+
expression("ct", vec![Attr::u32(2, 17), Attr::u32(4, 1)]),
|
|
52
|
+
],
|
|
53
|
+
),
|
|
54
|
+
rule(
|
|
55
|
+
"forward",
|
|
56
|
+
2,
|
|
57
|
+
vec![
|
|
58
|
+
expression(
|
|
59
|
+
"ct",
|
|
60
|
+
vec![Attr::u32(1, 1), Attr::u32(2, 20), Attr::bytes(3, vec![0])],
|
|
61
|
+
),
|
|
62
|
+
expression("ct", vec![Attr::u32(1, 1), Attr::u32(2, 17)]),
|
|
63
|
+
immediate(1, vec![0x7a; 16]),
|
|
64
|
+
expression("ct", vec![Attr::u32(2, 13), Attr::u32(4, 1)]),
|
|
65
|
+
expression("ct", vec![Attr::u32(1, 1), Attr::u32(2, 13)]),
|
|
66
|
+
expression(
|
|
67
|
+
"payload",
|
|
68
|
+
vec![
|
|
69
|
+
Attr::u32(1, 1),
|
|
70
|
+
Attr::u32(2, 2),
|
|
71
|
+
Attr::u32(3, 0),
|
|
72
|
+
Attr::u32(4, 2),
|
|
73
|
+
],
|
|
74
|
+
),
|
|
75
|
+
expression(
|
|
76
|
+
"range",
|
|
77
|
+
vec![
|
|
78
|
+
Attr::u32(1, 1),
|
|
79
|
+
Attr::u32(2, 0),
|
|
80
|
+
Attr::nested(3, vec![Attr::bytes(1, 10000_u16.to_be_bytes().to_vec())]),
|
|
81
|
+
Attr::nested(4, vec![Attr::bytes(1, 10999_u16.to_be_bytes().to_vec())]),
|
|
82
|
+
],
|
|
83
|
+
),
|
|
84
|
+
],
|
|
85
|
+
),
|
|
86
|
+
rule(
|
|
87
|
+
"post",
|
|
88
|
+
3,
|
|
89
|
+
vec![
|
|
90
|
+
immediate(1, vec![10, 240, 0, 2]),
|
|
91
|
+
immediate(2, 10000_u16.to_be_bytes().to_vec()),
|
|
92
|
+
immediate(3, 10999_u16.to_be_bytes().to_vec()),
|
|
93
|
+
expression(
|
|
94
|
+
"nat",
|
|
95
|
+
vec![
|
|
96
|
+
Attr::u32(1, 0),
|
|
97
|
+
Attr::u32(2, 2),
|
|
98
|
+
Attr::u32(3, 1),
|
|
99
|
+
Attr::u32(4, 1),
|
|
100
|
+
Attr::u32(5, 2),
|
|
101
|
+
Attr::u32(6, 3),
|
|
102
|
+
Attr::u32(7, 3),
|
|
103
|
+
],
|
|
104
|
+
),
|
|
105
|
+
],
|
|
106
|
+
),
|
|
107
|
+
];
|
|
108
|
+
let mut socket = Socket::open().unwrap();
|
|
109
|
+
let mut operations = vec![(
|
|
110
|
+
0,
|
|
111
|
+
0x600,
|
|
112
|
+
vec![Attr::string(1, "snft_egress_identity"), Attr::u32(2, 6)],
|
|
113
|
+
)];
|
|
114
|
+
operations.extend(chains.iter().cloned().map(|value| (3, 0x600, value)));
|
|
115
|
+
operations.extend(rules.iter().cloned().map(|value| (6, 0xc00, value)));
|
|
116
|
+
let generation = socket.generation().unwrap();
|
|
117
|
+
socket.batch(generation, operations).unwrap();
|
|
118
|
+
let dumped_chains = socket
|
|
119
|
+
.query(4, vec![Attr::string(1, "snft_egress_identity")], true)
|
|
120
|
+
.unwrap();
|
|
121
|
+
assert_eq!(dumped_chains.len(), chains.len());
|
|
122
|
+
for expected in &chains {
|
|
123
|
+
let name = wire::text(expected, 3).unwrap();
|
|
124
|
+
let actual = dumped_chains
|
|
125
|
+
.iter()
|
|
126
|
+
.find(|item| wire::text(item, 3).unwrap() == name)
|
|
127
|
+
.unwrap();
|
|
128
|
+
assert_eq!(
|
|
129
|
+
chain_identity(actual).unwrap(),
|
|
130
|
+
chain_identity(expected).unwrap()
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
let dumped_rules = socket
|
|
134
|
+
.query(7, vec![Attr::string(1, "snft_egress_identity")], true)
|
|
135
|
+
.unwrap();
|
|
136
|
+
assert_eq!(dumped_rules.len(), rules.len());
|
|
137
|
+
for expected in &rules {
|
|
138
|
+
let actual = dumped_rules
|
|
139
|
+
.iter()
|
|
140
|
+
.find(|item| wire::one(item, 7).unwrap().value == wire::one(expected, 7).unwrap().value)
|
|
141
|
+
.unwrap();
|
|
142
|
+
assert_eq!(
|
|
143
|
+
rule_identity(actual).unwrap(),
|
|
144
|
+
rule_identity(expected).unwrap()
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
let generation = socket.generation().unwrap();
|
|
148
|
+
socket
|
|
149
|
+
.batch(
|
|
150
|
+
generation,
|
|
151
|
+
vec![(2, 0, vec![Attr::string(1, "snft_egress_identity")])],
|
|
152
|
+
)
|
|
153
|
+
.unwrap();
|
|
154
|
+
println!("EGRESS_EXPRESSION_IDENTITY ct_zone_label_original_ipv4=true nat_explicit_min_max=true range_inclusive=true traffic_qualified=false");
|
|
155
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
use crate::egress;
|
|
3
|
+
use serde_json::{json, Value};
|
|
4
|
+
|
|
5
|
+
fn binding(name: &str, address: &str, kind: &str) -> Value {
|
|
6
|
+
let output = std::process::Command::new("/bin/ip")
|
|
7
|
+
.args(["-j", "-d", "link", "show", "dev", name])
|
|
8
|
+
.output()
|
|
9
|
+
.unwrap();
|
|
10
|
+
assert!(output.status.success());
|
|
11
|
+
let observed = serde_json::from_slice::<Value>(&output.stdout).unwrap()[0].clone();
|
|
12
|
+
let index = observed["ifindex"].as_u64().unwrap();
|
|
13
|
+
let peer_index = if kind == "veth" {
|
|
14
|
+
let peer = std::ffi::CString::new(format!("{name}peer")).unwrap();
|
|
15
|
+
let index = unsafe { libc::if_nametoindex(peer.as_ptr()) };
|
|
16
|
+
assert_ne!(index, 0);
|
|
17
|
+
u64::from(index)
|
|
18
|
+
} else {
|
|
19
|
+
index
|
|
20
|
+
};
|
|
21
|
+
json!({"interfaceIndex":index,"interfaceName":name,"interfaceKind":kind,
|
|
22
|
+
"macAddress":if kind=="tun" {Value::Null} else {observed["address"].clone()},
|
|
23
|
+
"interfaceLinkIndex":peer_index,
|
|
24
|
+
"requiredIpv4Addresses":[address]})
|
|
25
|
+
}
|
|
26
|
+
fn veth(name: &str, address: &str) {
|
|
27
|
+
ip(&[
|
|
28
|
+
"link",
|
|
29
|
+
"add",
|
|
30
|
+
name,
|
|
31
|
+
"type",
|
|
32
|
+
"veth",
|
|
33
|
+
"peer",
|
|
34
|
+
"name",
|
|
35
|
+
&format!("{name}peer"),
|
|
36
|
+
]);
|
|
37
|
+
ip(&["address", "add", address, "dev", name]);
|
|
38
|
+
ip(&["link", "set", name, "up"]);
|
|
39
|
+
}
|
|
40
|
+
fn router() -> egress::Policy {
|
|
41
|
+
veth("work", "10.241.0.1/24");
|
|
42
|
+
veth("handoff", "10.240.0.2/30");
|
|
43
|
+
ip(&["tuntap", "add", "dev", "vpn", "mode", "tun"]);
|
|
44
|
+
ip(&["address", "add", "10.242.0.1/24", "dev", "vpn"]);
|
|
45
|
+
let mut value = egress::tests::router();
|
|
46
|
+
value["scope"]["links"] = json!([
|
|
47
|
+
binding("work", "10.241.0.1", "veth"),
|
|
48
|
+
binding("vpn", "10.242.0.1", "tun")
|
|
49
|
+
]);
|
|
50
|
+
value["scope"]["handoff"] = binding("handoff", "10.240.0.2", "veth");
|
|
51
|
+
for index in 0..2 {
|
|
52
|
+
value["scope"]["endpoints"][index]["interfaceIndex"] =
|
|
53
|
+
value["scope"]["links"][index]["interfaceIndex"].clone();
|
|
54
|
+
}
|
|
55
|
+
serde_json::from_value(value).unwrap()
|
|
56
|
+
}
|
|
57
|
+
fn host() -> egress::Policy {
|
|
58
|
+
veth("handoff", "10.240.0.1/30");
|
|
59
|
+
veth("uplink", "192.0.2.2/24");
|
|
60
|
+
let mut value = egress::tests::host();
|
|
61
|
+
value["scope"]["handoffs"][0]["link"] = binding("handoff", "10.240.0.1", "veth");
|
|
62
|
+
value["scope"]["uplink"] = binding("uplink", "192.0.2.2", "veth");
|
|
63
|
+
serde_json::from_value(value).unwrap()
|
|
64
|
+
}
|
|
65
|
+
fn recover(name: &str, policy: egress::Policy) {
|
|
66
|
+
let prepared: Prepared = policy.clone().prepare().unwrap().into();
|
|
67
|
+
prepared.validate_interfaces().expect("fixture bindings");
|
|
68
|
+
let mut owner = Owner::new(options(name)).unwrap();
|
|
69
|
+
let result = owner.reconcile(Transition {
|
|
70
|
+
previous: None,
|
|
71
|
+
target: prepared.clone(),
|
|
72
|
+
});
|
|
73
|
+
if result.is_err() {
|
|
74
|
+
if let Some(graph) = owner.graph().unwrap() {
|
|
75
|
+
for (kind, expected) in prepared.program(&format!("snft_{name}")).unwrap() {
|
|
76
|
+
if kind == 3 {
|
|
77
|
+
let name = wire::text(&expected, 3).unwrap();
|
|
78
|
+
let actual = graph
|
|
79
|
+
.chains
|
|
80
|
+
.iter()
|
|
81
|
+
.find(|item| wire::text(item, 3).unwrap() == name)
|
|
82
|
+
.unwrap();
|
|
83
|
+
assert_eq!(
|
|
84
|
+
chain_identity(&expected),
|
|
85
|
+
chain_identity(actual),
|
|
86
|
+
"chain {name}"
|
|
87
|
+
);
|
|
88
|
+
} else {
|
|
89
|
+
let stamp = &wire::one(&expected, 7).unwrap().value;
|
|
90
|
+
let actual = graph
|
|
91
|
+
.rules
|
|
92
|
+
.iter()
|
|
93
|
+
.find(|item| &wire::one(item, 7).unwrap().value == stamp)
|
|
94
|
+
.unwrap();
|
|
95
|
+
if rule_identity(&expected) != rule_identity(actual) {
|
|
96
|
+
println!("expected_rule={expected:?}\nactual_rule={actual:?}");
|
|
97
|
+
panic!("full graph identity differs");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
let applied = result.unwrap();
|
|
104
|
+
assert!(owner.inspect().enforced);
|
|
105
|
+
let handle = applied.receipt.table_handle.clone();
|
|
106
|
+
// Drop only the process/socket owner. PERSIST retains the complete graph.
|
|
107
|
+
drop(owner);
|
|
108
|
+
let mut owner = Owner::new(options(name)).unwrap();
|
|
109
|
+
let recovered = owner
|
|
110
|
+
.reconcile(Transition {
|
|
111
|
+
previous: None,
|
|
112
|
+
target: prepared,
|
|
113
|
+
})
|
|
114
|
+
.unwrap();
|
|
115
|
+
assert_eq!(recovered.receipt.table_handle, handle);
|
|
116
|
+
assert!(owner.inspect().enforced);
|
|
117
|
+
let mut next = policy;
|
|
118
|
+
next.revision += 1;
|
|
119
|
+
let transition = Transition {
|
|
120
|
+
previous: Some(recovered.clone()),
|
|
121
|
+
target: next.prepare().unwrap().into(),
|
|
122
|
+
};
|
|
123
|
+
let next_applied = owner.reconcile(transition.clone()).unwrap();
|
|
124
|
+
assert!(owner.inspect().enforced);
|
|
125
|
+
// Exact persisted command replay after the target ACK was lost.
|
|
126
|
+
drop(owner);
|
|
127
|
+
let mut owner = Owner::new(options(name)).unwrap();
|
|
128
|
+
assert_eq!(owner.reconcile(transition).unwrap(), next_applied);
|
|
129
|
+
assert!(owner.inspect().enforced);
|
|
130
|
+
// A valid private-v1 policy cannot replace this table's v2 role.
|
|
131
|
+
assert_eq!(
|
|
132
|
+
owner.reconcile(Transition {
|
|
133
|
+
previous: Some(next_applied.clone()),
|
|
134
|
+
target: empty(3)
|
|
135
|
+
}),
|
|
136
|
+
Err(Error::Conflict)
|
|
137
|
+
);
|
|
138
|
+
assert!(owner.inspect().enforced);
|
|
139
|
+
owner.release(next_applied).unwrap();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#[test]
|
|
143
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
144
|
+
fn v2_router_complete_graph_persist_and_lost_ack_recovery() {
|
|
145
|
+
isolated();
|
|
146
|
+
recover("router_v2", router());
|
|
147
|
+
}
|
|
148
|
+
#[test]
|
|
149
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
150
|
+
fn v2_host_complete_graph_persist_and_lost_ack_recovery() {
|
|
151
|
+
isolated();
|
|
152
|
+
recover("host_v2", host());
|
|
153
|
+
}
|