@push.rocks/smartnftables 1.4.0 → 1.5.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.
Files changed (41) hide show
  1. package/changelog.md +14 -0
  2. package/dist_rust/smartnftables_linux_amd64_musl +0 -0
  3. package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +4 -4
  4. package/dist_rust/smartnftables_linux_arm64_musl +0 -0
  5. package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +4 -4
  6. package/dist_ts/00_commitinfo_data.js +1 -1
  7. package/dist_ts/classes.managednftables.d.ts +7 -7
  8. package/dist_ts/classes.managednftables.js +1 -1
  9. package/dist_ts/index.d.ts +1 -0
  10. package/dist_ts/index.js +1 -1
  11. package/dist_ts/managed.egress.types.d.ts +91 -0
  12. package/dist_ts/managed.egress.types.js +2 -0
  13. package/dist_ts/managed.types.d.ts +20 -18
  14. package/package.json +2 -2
  15. package/readme.md +75 -4
  16. package/rust/src/egress.compile.rs +288 -0
  17. package/rust/src/egress.host.rs +103 -0
  18. package/rust/src/egress.router.rs +231 -0
  19. package/rust/src/egress.rs +682 -0
  20. package/rust/src/egress_tests.rs +332 -0
  21. package/rust/src/main.rs +3 -1
  22. package/rust/src/managed.rs +105 -0
  23. package/rust/src/owner.rs +96 -6
  24. package/rust/src/owner_coexistence_tests.rs +77 -0
  25. package/rust/src/owner_egress_identity_tests.rs +155 -0
  26. package/rust/src/owner_egress_tests.rs +153 -0
  27. package/rust/src/owner_egress_traffic_tests.rs +470 -0
  28. package/rust/src/owner_host_traffic_tests.rs +305 -0
  29. package/rust/src/owner_identity_tests.rs +169 -0
  30. package/rust/src/owner_link_tests.rs +119 -0
  31. package/rust/src/owner_packet_fixture.rs +200 -0
  32. package/rust/src/owner_tests.rs +33 -7
  33. package/rust/src/policy.rs +120 -81
  34. package/rust/src/tests.rs +22 -0
  35. package/rust/src/wire.links.rs +268 -0
  36. package/rust/src/wire.rs +3 -64
  37. package/ts/00_commitinfo_data.ts +1 -1
  38. package/ts/classes.managednftables.ts +12 -12
  39. package/ts/index.ts +1 -0
  40. package/ts/managed.egress.types.ts +85 -0
  41. package/ts/managed.types.ts +20 -16
@@ -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
+ }