@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
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
use super::{attrs, one, text, Attr, Socket};
|
|
2
|
+
use crate::{Error, Result};
|
|
3
|
+
use std::collections::BTreeSet;
|
|
4
|
+
use std::net::Ipv4Addr;
|
|
5
|
+
use std::time::{Duration, Instant};
|
|
6
|
+
|
|
7
|
+
struct Link {
|
|
8
|
+
hardware: u16,
|
|
9
|
+
flags: u32,
|
|
10
|
+
attributes: Vec<Attr>,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/// Current local facts only. Peer namespaces and link-generation lifetimes remain caller-owned.
|
|
14
|
+
#[derive(Clone, Copy)]
|
|
15
|
+
pub struct BoundInterface<'a> {
|
|
16
|
+
pub index: u32,
|
|
17
|
+
pub name: &'a str,
|
|
18
|
+
pub kind: &'a str,
|
|
19
|
+
pub mac: Option<&'a str>,
|
|
20
|
+
pub link_index: u32,
|
|
21
|
+
pub addresses: &'a [String],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn read_link(socket: &mut Socket, index: u32) -> Result<Link> {
|
|
25
|
+
let sequence = socket.sequence()?;
|
|
26
|
+
let mut request = Vec::new();
|
|
27
|
+
request.extend(32_u32.to_ne_bytes());
|
|
28
|
+
request.extend(18_u16.to_ne_bytes());
|
|
29
|
+
request.extend(1_u16.to_ne_bytes());
|
|
30
|
+
request.extend(sequence.to_ne_bytes());
|
|
31
|
+
request.extend(socket.port.to_ne_bytes());
|
|
32
|
+
request.extend([0; 4]);
|
|
33
|
+
request.extend(index.to_ne_bytes());
|
|
34
|
+
request.extend([0; 8]);
|
|
35
|
+
socket.send(&request)?;
|
|
36
|
+
let until = Instant::now() + Duration::from_secs(3);
|
|
37
|
+
loop {
|
|
38
|
+
for message in socket.receive(until)? {
|
|
39
|
+
if message.sequence != sequence {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if message.kind == 2 {
|
|
43
|
+
let error = i32::from_ne_bytes(
|
|
44
|
+
message
|
|
45
|
+
.payload
|
|
46
|
+
.get(..4)
|
|
47
|
+
.ok_or(Error::Protocol)?
|
|
48
|
+
.try_into()
|
|
49
|
+
.unwrap(),
|
|
50
|
+
);
|
|
51
|
+
if error != 0 {
|
|
52
|
+
return Err(Error::kernel(error));
|
|
53
|
+
}
|
|
54
|
+
} else if message.kind == 16 && message.payload.len() >= 16 {
|
|
55
|
+
if u32::from_ne_bytes(message.payload[4..8].try_into().unwrap()) != index {
|
|
56
|
+
return Err(Error::Conflict);
|
|
57
|
+
}
|
|
58
|
+
return Ok(Link {
|
|
59
|
+
hardware: u16::from_ne_bytes(message.payload[2..4].try_into().unwrap()),
|
|
60
|
+
flags: u32::from_ne_bytes(message.payload[8..12].try_into().unwrap()),
|
|
61
|
+
attributes: attrs(&message.payload[16..])?,
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
return Err(Error::Protocol);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
fn unbridged(attributes: &[Attr]) -> Result<()> {
|
|
70
|
+
if attributes.iter().any(|attr| attr.id() == 10)
|
|
71
|
+
&& one(attributes, 10)?.value != 0_u32.to_ne_bytes()
|
|
72
|
+
{
|
|
73
|
+
return Err(Error::Conflict);
|
|
74
|
+
}
|
|
75
|
+
Ok(())
|
|
76
|
+
}
|
|
77
|
+
fn virtual_kind(attributes: &[Attr], kind: &str) -> Result<()> {
|
|
78
|
+
let info = attrs(&one(attributes, 18)?.value)?;
|
|
79
|
+
if text(&info, 1)? != kind {
|
|
80
|
+
return Err(Error::Conflict);
|
|
81
|
+
}
|
|
82
|
+
if kind == "tun" && one(&attrs(&one(&info, 2)?.value)?, 3)?.value != [1] {
|
|
83
|
+
return Err(Error::Conflict);
|
|
84
|
+
}
|
|
85
|
+
Ok(())
|
|
86
|
+
}
|
|
87
|
+
/// Preserve schema-v1's local name/index/kind/master checks exactly.
|
|
88
|
+
pub fn verify_interfaces<'a>(
|
|
89
|
+
interfaces: impl Iterator<Item = (u32, &'a str, &'a str)>,
|
|
90
|
+
) -> Result<()> {
|
|
91
|
+
let mut socket = Socket::open_protocol(libc::NETLINK_ROUTE)?;
|
|
92
|
+
for (index, name, kind) in interfaces {
|
|
93
|
+
let link = read_link(&mut socket, index)?;
|
|
94
|
+
if text(&link.attributes, 3)? != name {
|
|
95
|
+
return Err(Error::Conflict);
|
|
96
|
+
}
|
|
97
|
+
unbridged(&link.attributes)?;
|
|
98
|
+
virtual_kind(&link.attributes, kind)?;
|
|
99
|
+
}
|
|
100
|
+
Ok(())
|
|
101
|
+
}
|
|
102
|
+
fn matches(link: &Link, binding: &BoundInterface<'_>) -> Result<()> {
|
|
103
|
+
if text(&link.attributes, 3)? != binding.name || link.flags & libc::IFF_LOOPBACK as u32 != 0 {
|
|
104
|
+
return Err(Error::Conflict);
|
|
105
|
+
}
|
|
106
|
+
unbridged(&link.attributes)?;
|
|
107
|
+
match binding.kind {
|
|
108
|
+
"veth" | "tun" => virtual_kind(&link.attributes, binding.kind)?,
|
|
109
|
+
"ethernet" => {
|
|
110
|
+
// Physical/VirtIO Ethernet has no IFLA_LINKINFO; bridge, VLAN, bond,
|
|
111
|
+
// dummy and other virtual kinds are not an implicit uplink role.
|
|
112
|
+
if link.attributes.iter().any(|item| item.id() == 18) {
|
|
113
|
+
return Err(Error::Conflict);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
_ => return Err(Error::Invalid),
|
|
117
|
+
}
|
|
118
|
+
if binding.kind == "tun" {
|
|
119
|
+
if link.hardware != 65534 || binding.mac.is_some() {
|
|
120
|
+
return Err(Error::Conflict);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
let mac = &one(&link.attributes, 1)?.value;
|
|
124
|
+
if link.hardware != 1
|
|
125
|
+
|| mac.len() != 6
|
|
126
|
+
|| mac[0] & 1 != 0
|
|
127
|
+
|| mac.iter().all(|item| *item == 0)
|
|
128
|
+
{
|
|
129
|
+
return Err(Error::Conflict);
|
|
130
|
+
}
|
|
131
|
+
let actual = mac
|
|
132
|
+
.iter()
|
|
133
|
+
.map(|item| format!("{item:02x}"))
|
|
134
|
+
.collect::<Vec<_>>()
|
|
135
|
+
.join(":");
|
|
136
|
+
if binding.mac != Some(actual.as_str()) {
|
|
137
|
+
return Err(Error::Conflict);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
let peer = if link.attributes.iter().any(|item| item.id() == 5) {
|
|
141
|
+
u32::from_ne_bytes(
|
|
142
|
+
one(&link.attributes, 5)?
|
|
143
|
+
.value
|
|
144
|
+
.as_slice()
|
|
145
|
+
.try_into()
|
|
146
|
+
.map_err(|_| Error::Protocol)?,
|
|
147
|
+
)
|
|
148
|
+
} else {
|
|
149
|
+
binding.index
|
|
150
|
+
};
|
|
151
|
+
if peer != binding.link_index {
|
|
152
|
+
return Err(Error::Conflict);
|
|
153
|
+
}
|
|
154
|
+
Ok(())
|
|
155
|
+
}
|
|
156
|
+
fn read_addresses(socket: &mut Socket, index: u32) -> Result<BTreeSet<String>> {
|
|
157
|
+
let sequence = socket.sequence()?;
|
|
158
|
+
let mut request = Vec::new();
|
|
159
|
+
request.extend(24_u32.to_ne_bytes());
|
|
160
|
+
request.extend(22_u16.to_ne_bytes());
|
|
161
|
+
request.extend(0x301_u16.to_ne_bytes());
|
|
162
|
+
request.extend(sequence.to_ne_bytes());
|
|
163
|
+
request.extend(socket.port.to_ne_bytes());
|
|
164
|
+
request.extend([libc::AF_INET as u8, 0, 0, 0]);
|
|
165
|
+
request.extend(index.to_ne_bytes());
|
|
166
|
+
socket.send(&request)?;
|
|
167
|
+
let until = Instant::now() + Duration::from_secs(3);
|
|
168
|
+
let mut result = BTreeSet::new();
|
|
169
|
+
let mut total = 0;
|
|
170
|
+
let mut count = 0;
|
|
171
|
+
loop {
|
|
172
|
+
for message in socket.receive(until)? {
|
|
173
|
+
if message.sequence != sequence {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if message.flags & 0x10 != 0 {
|
|
177
|
+
return Err(Error::Conflict);
|
|
178
|
+
}
|
|
179
|
+
if message.kind == 2 || message.kind == 3 {
|
|
180
|
+
if message.kind == 2 || !message.payload.is_empty() {
|
|
181
|
+
let error = i32::from_ne_bytes(
|
|
182
|
+
message
|
|
183
|
+
.payload
|
|
184
|
+
.get(..4)
|
|
185
|
+
.ok_or(Error::Protocol)?
|
|
186
|
+
.try_into()
|
|
187
|
+
.unwrap(),
|
|
188
|
+
);
|
|
189
|
+
if error != 0 {
|
|
190
|
+
return Err(Error::kernel(error));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if message.kind == 3 {
|
|
194
|
+
return Ok(result);
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
count += 1;
|
|
199
|
+
total += message.payload.len();
|
|
200
|
+
if message.kind != 20 || message.payload.len() < 8 || count > 4096 || total > 2_097_152
|
|
201
|
+
{
|
|
202
|
+
return Err(Error::Protocol);
|
|
203
|
+
}
|
|
204
|
+
if message.payload[0] != libc::AF_INET as u8 {
|
|
205
|
+
return Err(Error::Protocol);
|
|
206
|
+
}
|
|
207
|
+
if u32::from_ne_bytes(message.payload[4..8].try_into().unwrap()) != index {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
let data = attrs(&message.payload[8..])?;
|
|
211
|
+
let local = if data.iter().any(|item| item.id() == 2) {
|
|
212
|
+
one(&data, 2)?
|
|
213
|
+
} else {
|
|
214
|
+
one(&data, 1)?
|
|
215
|
+
};
|
|
216
|
+
let bytes: [u8; 4] = local
|
|
217
|
+
.value
|
|
218
|
+
.as_slice()
|
|
219
|
+
.try_into()
|
|
220
|
+
.map_err(|_| Error::Protocol)?;
|
|
221
|
+
let flags = if data.iter().any(|item| item.id() == 8) {
|
|
222
|
+
u32::from_ne_bytes(
|
|
223
|
+
one(&data, 8)?
|
|
224
|
+
.value
|
|
225
|
+
.as_slice()
|
|
226
|
+
.try_into()
|
|
227
|
+
.map_err(|_| Error::Protocol)?,
|
|
228
|
+
)
|
|
229
|
+
} else {
|
|
230
|
+
u32::from(message.payload[2])
|
|
231
|
+
};
|
|
232
|
+
// Tentative, failed or deprecated source addresses do not qualify.
|
|
233
|
+
if flags & (0x40 | 0x08 | 0x20) != 0 {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if data.iter().any(|item| item.id() == 6) {
|
|
237
|
+
let cache = &one(&data, 6)?.value;
|
|
238
|
+
if cache.len() != 16 {
|
|
239
|
+
return Err(Error::Protocol);
|
|
240
|
+
}
|
|
241
|
+
if u32::from_ne_bytes(cache[..4].try_into().unwrap()) == 0
|
|
242
|
+
|| u32::from_ne_bytes(cache[4..8].try_into().unwrap()) == 0
|
|
243
|
+
{
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
result.insert(Ipv4Addr::from(bytes).to_string());
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
pub fn verify_bound_interfaces<'a>(
|
|
252
|
+
interfaces: impl Iterator<Item = BoundInterface<'a>>,
|
|
253
|
+
) -> Result<()> {
|
|
254
|
+
let mut socket = Socket::open_protocol(libc::NETLINK_ROUTE)?;
|
|
255
|
+
for binding in interfaces {
|
|
256
|
+
matches(&read_link(&mut socket, binding.index)?, &binding)?;
|
|
257
|
+
let addresses = read_addresses(&mut socket, binding.index)?;
|
|
258
|
+
if binding
|
|
259
|
+
.addresses
|
|
260
|
+
.iter()
|
|
261
|
+
.any(|address| !addresses.contains(address))
|
|
262
|
+
{
|
|
263
|
+
return Err(Error::Conflict);
|
|
264
|
+
}
|
|
265
|
+
matches(&read_link(&mut socket, binding.index)?, &binding)?;
|
|
266
|
+
}
|
|
267
|
+
Ok(())
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Mutation fence: the caller retains these exact links until cleanup completes.
|
|
271
|
+
pub fn verify_bound_interfaces_down<'a>(interfaces: impl Iterator<Item = BoundInterface<'a>>) -> Result<()> {
|
|
272
|
+
let mut socket = Socket::open_protocol(libc::NETLINK_ROUTE)?;
|
|
273
|
+
for binding in interfaces {
|
|
274
|
+
let link = read_link(&mut socket, binding.index)?;
|
|
275
|
+
matches(&link, &binding)?;
|
|
276
|
+
if link.flags & libc::IFF_UP as u32 != 0 {return Err(Error::Conflict);}
|
|
277
|
+
}
|
|
278
|
+
Ok(())
|
|
279
|
+
}
|
package/rust/src/wire.rs
CHANGED
|
@@ -123,70 +123,9 @@ pub struct Socket {
|
|
|
123
123
|
sequence: u32,
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
pub
|
|
129
|
-
interfaces: impl Iterator<Item = (u32, &'a str, &'a str)>,
|
|
130
|
-
) -> Result<()> {
|
|
131
|
-
let mut socket = Socket::open_protocol(libc::NETLINK_ROUTE)?;
|
|
132
|
-
for (index, name, kind) in interfaces {
|
|
133
|
-
let sequence = socket.sequence()?;
|
|
134
|
-
let mut request = Vec::new();
|
|
135
|
-
request.extend(32_u32.to_ne_bytes());
|
|
136
|
-
request.extend(18_u16.to_ne_bytes());
|
|
137
|
-
request.extend(1_u16.to_ne_bytes());
|
|
138
|
-
request.extend(sequence.to_ne_bytes());
|
|
139
|
-
request.extend(socket.port.to_ne_bytes());
|
|
140
|
-
request.extend([0; 4]);
|
|
141
|
-
request.extend(index.to_ne_bytes());
|
|
142
|
-
request.extend([0; 8]);
|
|
143
|
-
socket.send(&request)?;
|
|
144
|
-
let until = Instant::now() + Duration::from_secs(3);
|
|
145
|
-
let response = 'read: loop {
|
|
146
|
-
for message in socket.receive(until)? {
|
|
147
|
-
if message.sequence != sequence {
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
if message.kind == 2 {
|
|
151
|
-
let error = i32::from_ne_bytes(
|
|
152
|
-
message
|
|
153
|
-
.payload
|
|
154
|
-
.get(..4)
|
|
155
|
-
.ok_or(Error::Protocol)?
|
|
156
|
-
.try_into()
|
|
157
|
-
.unwrap(),
|
|
158
|
-
);
|
|
159
|
-
if error != 0 {
|
|
160
|
-
return Err(Error::kernel(error));
|
|
161
|
-
}
|
|
162
|
-
} else if message.kind == 16 && message.payload.len() >= 16 {
|
|
163
|
-
if u32::from_ne_bytes(message.payload[4..8].try_into().unwrap()) != index {
|
|
164
|
-
return Err(Error::Conflict);
|
|
165
|
-
}
|
|
166
|
-
break 'read attrs(&message.payload[16..])?;
|
|
167
|
-
} else {
|
|
168
|
-
return Err(Error::Protocol);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
if text(&response, 3)? != name {
|
|
173
|
-
return Err(Error::Conflict);
|
|
174
|
-
}
|
|
175
|
-
if response.iter().any(|attr| attr.id() == 10)
|
|
176
|
-
&& one(&response, 10)?.value != 0_u32.to_ne_bytes()
|
|
177
|
-
{
|
|
178
|
-
return Err(Error::Conflict);
|
|
179
|
-
}
|
|
180
|
-
let info = attrs(&one(&response, 18)?.value)?;
|
|
181
|
-
if text(&info, 1)? != kind {
|
|
182
|
-
return Err(Error::Conflict);
|
|
183
|
-
}
|
|
184
|
-
if kind == "tun" && one(&attrs(&one(&info, 2)?.value)?, 3)?.value != [1] {
|
|
185
|
-
return Err(Error::Conflict);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
Ok(())
|
|
189
|
-
}
|
|
126
|
+
#[path = "wire.links.rs"]
|
|
127
|
+
mod links;
|
|
128
|
+
pub use links::{verify_bound_interfaces, verify_bound_interfaces_down, verify_interfaces, BoundInterface};
|
|
190
129
|
impl Socket {
|
|
191
130
|
pub fn open() -> Result<Self> {
|
|
192
131
|
Self::open_protocol(libc::NETLINK_NETFILTER)
|
|
@@ -253,6 +192,9 @@ impl Socket {
|
|
|
253
192
|
Ok(self.sequence)
|
|
254
193
|
}
|
|
255
194
|
fn message(&mut self, kind: u16, flags: u16, attributes: &[Attr]) -> Result<(u32, Vec<u8>)> {
|
|
195
|
+
self.message_family(kind, flags, attributes, 1)
|
|
196
|
+
}
|
|
197
|
+
fn message_family(&mut self, kind: u16, flags: u16, attributes: &[Attr], family: u8) -> Result<(u32, Vec<u8>)> {
|
|
256
198
|
let sequence = self.sequence()?;
|
|
257
199
|
let batch = kind == 16 || kind == 17;
|
|
258
200
|
let payload = encode_attrs(attributes);
|
|
@@ -265,7 +207,7 @@ impl Socket {
|
|
|
265
207
|
bytes.extend(flags.to_ne_bytes());
|
|
266
208
|
bytes.extend(sequence.to_ne_bytes());
|
|
267
209
|
bytes.extend(self.port.to_ne_bytes());
|
|
268
|
-
bytes.extend([if batch { 0 } else {
|
|
210
|
+
bytes.extend([if batch { 0 } else { family }, 0]);
|
|
269
211
|
bytes.extend(if batch { 10_u16 } else { 0 }.to_be_bytes());
|
|
270
212
|
bytes.extend(payload);
|
|
271
213
|
Ok((sequence, bytes))
|
|
@@ -348,8 +290,15 @@ impl Socket {
|
|
|
348
290
|
input: Vec<Attr>,
|
|
349
291
|
dump: bool,
|
|
350
292
|
) -> Result<Vec<Vec<Attr>>> {
|
|
293
|
+
self.query_family(operation, input, dump, 1)
|
|
294
|
+
}
|
|
295
|
+
/// Read Docker's IPv4 nft graph without claiming or mutating its table.
|
|
296
|
+
pub fn query_ipv4(&mut self, operation: u16, input: Vec<Attr>, dump: bool) -> Result<Vec<Vec<Attr>>> {
|
|
297
|
+
self.query_family(operation, input, dump, 2)
|
|
298
|
+
}
|
|
299
|
+
fn query_family(&mut self, operation: u16, input: Vec<Attr>, dump: bool, family: u8) -> Result<Vec<Vec<Attr>>> {
|
|
351
300
|
let (sequence, bytes) =
|
|
352
|
-
self.
|
|
301
|
+
self.message_family(0x0a00 | operation, 1 | if dump { 0x300 } else { 0 }, &input, family)?;
|
|
353
302
|
self.send(&bytes)?;
|
|
354
303
|
let until = Instant::now() + Duration::from_secs(3);
|
|
355
304
|
let mut result = Vec::new();
|
|
@@ -386,6 +335,7 @@ impl Socket {
|
|
|
386
335
|
if total > 2_097_152 || result.len() >= 4096 || message.payload.len() < 4 {
|
|
387
336
|
return Err(Error::Protocol);
|
|
388
337
|
}
|
|
338
|
+
if family == 2 && message.payload[0] != family {return Err(Error::Protocol);}
|
|
389
339
|
result.push(attrs(&message.payload[4..])?);
|
|
390
340
|
if !dump {
|
|
391
341
|
return Ok(result);
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { snapshot, ManagedNftablesError } from './classes.managednftables.js';
|
|
3
|
+
import type { IManagedDockerForwardingOptions, IManagedDockerForwardingStatus, IManagedDockerForwardingPolicy, IPreparedManagedDockerForwardingPolicy, IManagedDockerForwardingTransition, IAppliedManagedDockerForwardingPolicy, TManagedDockerForwardingCommands } from './managed.docker.types.js';
|
|
4
|
+
|
|
5
|
+
/** One node-level Docker DOCKER-USER contribution. Caller owns durable intents and handoff lifetimes. */
|
|
6
|
+
export class ManagedDockerForwarding {
|
|
7
|
+
readonly #options: Readonly<IManagedDockerForwardingOptions>;
|
|
8
|
+
#bridge: plugins.smartrust.RustBridge<TManagedDockerForwardingCommands> | null = null;
|
|
9
|
+
#state: 'new' | 'starting' | 'ready' | 'closing' | 'failed-owned' | 'stopped' = 'new';
|
|
10
|
+
#work: Promise<unknown> | null = null;
|
|
11
|
+
#close: Promise<void> | null = null;
|
|
12
|
+
#nativeMayOwn = false;
|
|
13
|
+
|
|
14
|
+
constructor(options: IManagedDockerForwardingOptions) {
|
|
15
|
+
this.#options = snapshot(options);
|
|
16
|
+
if (Object.keys(this.#options).some((key) => !['ownerId', 'instanceId', 'binaryPath', 'networkNamespaceFd'].includes(key))
|
|
17
|
+
|| ![this.#options.ownerId, this.#options.instanceId].every((value) => typeof value === 'string' && /^[A-Za-z0-9_-]{1,64}$/.test(value))
|
|
18
|
+
|| this.#options.instanceId.length < 16
|
|
19
|
+
|| this.#options.binaryPath !== undefined && (!plugins.path.isAbsolute(this.#options.binaryPath) || this.#options.binaryPath.includes('\0'))) {
|
|
20
|
+
throw new ManagedNftablesError('INVALID');
|
|
21
|
+
}
|
|
22
|
+
if (this.#options.networkNamespaceFd !== undefined && (process.platform !== 'linux'
|
|
23
|
+
|| !Number.isInteger(this.#options.networkNamespaceFd) || this.#options.networkNamespaceFd < 0
|
|
24
|
+
|| this.#options.networkNamespaceFd > 0x7fff_ffff)) throw new ManagedNftablesError('INVALID');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public get state(): string { return this.#state; }
|
|
28
|
+
|
|
29
|
+
/** Starts only the compiler/IPC owner. The returned idle status is not enforcement. */
|
|
30
|
+
public start(): Promise<IManagedDockerForwardingStatus> {
|
|
31
|
+
if (this.#state !== 'new') return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
|
|
32
|
+
this.#state = 'starting';
|
|
33
|
+
const work = (async () => {
|
|
34
|
+
if (process.platform !== 'linux' || !['x64', 'arm64'].includes(process.arch)) throw new ManagedNftablesError('UNSUPPORTED_PLATFORM');
|
|
35
|
+
const root = plugins.url.fileURLToPath(new URL('../', import.meta.url));
|
|
36
|
+
this.#bridge = new plugins.smartrust.RustBridge<TManagedDockerForwardingCommands>({
|
|
37
|
+
binaryName: 'smartnftables', binaryPath: this.#options.binaryPath
|
|
38
|
+
?? plugins.path.join(root, 'dist_rust', `smartnftables_linux_${process.arch === 'x64' ? 'amd64' : 'arm64'}_musl`),
|
|
39
|
+
localPaths: [], searchSystemPath: false,
|
|
40
|
+
cliArgs: ['--docker-forwarding', ...(this.#options.networkNamespaceFd === undefined ? [] : ['--network-namespace-fd', '3'])],
|
|
41
|
+
inheritedFileDescriptors: this.#options.networkNamespaceFd === undefined ? undefined : [this.#options.networkNamespaceFd],
|
|
42
|
+
readyTimeoutMs: 3000, requestTimeoutMs: 30_000, maxPayloadSize: 262_144,
|
|
43
|
+
maxPendingRequestsByMethod: { prepareDockerForwarding: 1, openDockerForwarding: 1, reconcileDockerForwarding: 1, inspectDockerForwarding: 1, releaseDockerForwarding: 1, closeDockerForwarding: 1 },
|
|
44
|
+
});
|
|
45
|
+
this.#bridge.on('exit', () => {
|
|
46
|
+
if (!['closing', 'stopped'].includes(this.#state)) this.#state = 'failed-owned';
|
|
47
|
+
});
|
|
48
|
+
if (!await this.#bridge.spawn()) throw new ManagedNftablesError('UNAVAILABLE');
|
|
49
|
+
const { ownerId, instanceId } = this.#options;
|
|
50
|
+
const status = snapshot(await this.#bridge.sendCommand('openDockerForwarding', { ownerId, instanceId }));
|
|
51
|
+
if (this.#state !== 'starting') throw new ManagedNftablesError('UNAVAILABLE');
|
|
52
|
+
this.#state = 'ready'; return status;
|
|
53
|
+
})();
|
|
54
|
+
return this.#track(work);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public prepare(policy: IManagedDockerForwardingPolicy): Promise<IPreparedManagedDockerForwardingPolicy> {
|
|
58
|
+
return this.#invoke('prepareDockerForwarding', policy);
|
|
59
|
+
}
|
|
60
|
+
public reconcile(transition: IManagedDockerForwardingTransition): Promise<IAppliedManagedDockerForwardingPolicy> {
|
|
61
|
+
return this.#invoke('reconcileDockerForwarding', transition);
|
|
62
|
+
}
|
|
63
|
+
public inspect(): Promise<IManagedDockerForwardingStatus> { return this.#invoke('inspectDockerForwarding', {}); }
|
|
64
|
+
public release(expected: IAppliedManagedDockerForwardingPolicy): Promise<{ released: true }> {
|
|
65
|
+
return this.#invoke('releaseDockerForwarding', expected);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Join operations and exact contributed-rule deletion, then native termination. Keep affected handoff links down through cleanup. */
|
|
69
|
+
public close(): Promise<void> {
|
|
70
|
+
if (this.#close) return this.#close;
|
|
71
|
+
if (this.#state === 'stopped') return Promise.resolve();
|
|
72
|
+
this.#state = 'closing';
|
|
73
|
+
this.#close = (async () => {
|
|
74
|
+
await this.#work?.catch(() => undefined);
|
|
75
|
+
if (this.#bridge) {
|
|
76
|
+
if (this.#nativeMayOwn) {
|
|
77
|
+
const result = await this.#bridge.sendCommand('closeDockerForwarding', {});
|
|
78
|
+
if (result.released !== true) throw new ManagedNftablesError('PROTOCOL');
|
|
79
|
+
}
|
|
80
|
+
await this.#bridge.terminate({ gracePeriodMs: 1000 });
|
|
81
|
+
}
|
|
82
|
+
this.#bridge = null; this.#state = 'stopped';
|
|
83
|
+
})().catch(() => {
|
|
84
|
+
this.#state = 'failed-owned'; this.#close = null;
|
|
85
|
+
throw new ManagedNftablesError('CLEANUP_UNCONFIRMED');
|
|
86
|
+
});
|
|
87
|
+
return this.#close;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#invoke<K extends keyof TManagedDockerForwardingCommands>(method: K, params: TManagedDockerForwardingCommands[K]['params']): Promise<TManagedDockerForwardingCommands[K]['result']> {
|
|
91
|
+
if (!['ready', 'failed-owned'].includes(this.#state) || !this.#bridge || this.#work) return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
|
|
92
|
+
let captured: TManagedDockerForwardingCommands[K]['params'];
|
|
93
|
+
try { captured = snapshot(params); } catch { return Promise.reject(new ManagedNftablesError('INVALID')); }
|
|
94
|
+
if (method === 'reconcileDockerForwarding' || method === 'releaseDockerForwarding') this.#nativeMayOwn = true;
|
|
95
|
+
return this.#track(this.#bridge.sendCommand(method, captured).then((result) => {
|
|
96
|
+
const capturedResult = snapshot(result);
|
|
97
|
+
if (method === 'releaseDockerForwarding') this.#nativeMayOwn = false;
|
|
98
|
+
if (this.#state !== 'closing' && (method === 'reconcileDockerForwarding' || method === 'releaseDockerForwarding'
|
|
99
|
+
|| method === 'inspectDockerForwarding' && (capturedResult as IManagedDockerForwardingStatus).error === null)) this.#state = 'ready';
|
|
100
|
+
return capturedResult;
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
#track<T>(work: Promise<T>): Promise<T> {
|
|
104
|
+
const owned = work.catch((error) => {
|
|
105
|
+
if (this.#state !== 'closing') this.#state = 'failed-owned';
|
|
106
|
+
const code = error instanceof plugins.smartrust.RustBridgeRequestError ? error.responseErrorCode : undefined;
|
|
107
|
+
throw new ManagedNftablesError(typeof code === 'string' && /^[A-Z_]{1,64}$/.test(code) ? code : 'UNAVAILABLE');
|
|
108
|
+
});
|
|
109
|
+
this.#work = owned;
|
|
110
|
+
void owned.then(() => { this.#work = null; }, () => { this.#work = null; });
|
|
111
|
+
return owned;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import type { IManagedNftOptions, IManagedNftPolicy, IManagedNftTransition, IAppliedManagedNftPolicy,
|
|
3
|
-
IPreparedManagedNftPolicy, IManagedNftStatus, TManagedNftCommands } from './managed.types.js';
|
|
3
|
+
IPreparedManagedNftPolicy, IManagedNftStatus, TManagedNftCommands, TManagedNftPolicy } from './managed.types.js';
|
|
4
4
|
|
|
5
5
|
export class ManagedNftablesError extends Error {
|
|
6
6
|
constructor(public readonly code: string) { super(`Managed nftables ${code}.`); }
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/** Capture inert input without evaluating getters, proxies, coercions or toJSON. */
|
|
10
|
-
function snapshot<T>(input: T): T {
|
|
10
|
+
export function snapshot<T>(input: T): T {
|
|
11
11
|
let count = 0;
|
|
12
12
|
let stringBytes = 0;
|
|
13
13
|
const string = (value: string): string => {
|
|
@@ -43,9 +43,9 @@ function snapshot<T>(input: T): T {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** One private native/netlink owner. Persistence and workload link lifetimes belong to its caller. */
|
|
46
|
-
export class ManagedNftables {
|
|
46
|
+
export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> {
|
|
47
47
|
readonly #options: Readonly<IManagedNftOptions>;
|
|
48
|
-
#bridge: plugins.smartrust.RustBridge<TManagedNftCommands
|
|
48
|
+
#bridge: plugins.smartrust.RustBridge<TManagedNftCommands<TPolicy>> | null = null;
|
|
49
49
|
#state: 'new' | 'starting' | 'ready' | 'closing' | 'failed-owned' | 'stopped' = 'new';
|
|
50
50
|
#work: Promise<unknown> | null = null;
|
|
51
51
|
#close: Promise<void> | null = null;
|
|
@@ -67,13 +67,13 @@ export class ManagedNftables {
|
|
|
67
67
|
public get state(): string { return this.#state; }
|
|
68
68
|
|
|
69
69
|
/** Starts only the compiler/IPC owner. The returned idle status is not enforcement. */
|
|
70
|
-
public start(): Promise<IManagedNftStatus
|
|
70
|
+
public start(): Promise<IManagedNftStatus<TPolicy>> {
|
|
71
71
|
if (this.#state !== 'new') return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
|
|
72
72
|
this.#state = 'starting';
|
|
73
73
|
const work = (async () => {
|
|
74
74
|
if (process.platform !== 'linux' || !['x64', 'arm64'].includes(process.arch)) throw new ManagedNftablesError('UNSUPPORTED_PLATFORM');
|
|
75
75
|
const root = plugins.url.fileURLToPath(new URL('../', import.meta.url));
|
|
76
|
-
this.#bridge = new plugins.smartrust.RustBridge<TManagedNftCommands
|
|
76
|
+
this.#bridge = new plugins.smartrust.RustBridge<TManagedNftCommands<TPolicy>>({
|
|
77
77
|
binaryName: 'smartnftables', binaryPath: this.#options.binaryPath
|
|
78
78
|
?? plugins.path.join(root, 'dist_rust', `smartnftables_linux_${process.arch === 'x64' ? 'amd64' : 'arm64'}_musl`),
|
|
79
79
|
localPaths: [], searchSystemPath: false,
|
|
@@ -94,14 +94,14 @@ export class ManagedNftables {
|
|
|
94
94
|
return this.#track(work);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
public prepare(policy:
|
|
97
|
+
public prepare(policy: TPolicy): Promise<IPreparedManagedNftPolicy<TPolicy>> {
|
|
98
98
|
return this.#invoke('preparePolicy', policy);
|
|
99
99
|
}
|
|
100
|
-
public reconcile(transition: IManagedNftTransition): Promise<IAppliedManagedNftPolicy
|
|
100
|
+
public reconcile(transition: IManagedNftTransition<TPolicy>): Promise<IAppliedManagedNftPolicy<TPolicy>> {
|
|
101
101
|
return this.#invoke('reconcilePolicy', transition);
|
|
102
102
|
}
|
|
103
|
-
public inspect(): Promise<IManagedNftStatus
|
|
104
|
-
public release(expected: IAppliedManagedNftPolicy): Promise<{ released: true }> {
|
|
103
|
+
public inspect(): Promise<IManagedNftStatus<TPolicy>> { return this.#invoke('inspectPolicy', {}); }
|
|
104
|
+
public release(expected: IAppliedManagedNftPolicy<TPolicy>): Promise<{ released: true }> {
|
|
105
105
|
return this.#invoke('releasePolicy', expected);
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -127,16 +127,16 @@ export class ManagedNftables {
|
|
|
127
127
|
return this.#close;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
#invoke<K extends keyof TManagedNftCommands
|
|
130
|
+
#invoke<K extends keyof TManagedNftCommands<TPolicy>>(method: K, params: TManagedNftCommands<TPolicy>[K]['params']): Promise<TManagedNftCommands<TPolicy>[K]['result']> {
|
|
131
131
|
if (!['ready', 'failed-owned'].includes(this.#state) || !this.#bridge || this.#work) return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
|
|
132
|
-
let captured: TManagedNftCommands[K]['params'];
|
|
132
|
+
let captured: TManagedNftCommands<TPolicy>[K]['params'];
|
|
133
133
|
try { captured = snapshot(params); } catch { return Promise.reject(new ManagedNftablesError('INVALID')); }
|
|
134
134
|
if (method === 'reconcilePolicy' || method === 'releasePolicy') this.#nativeMayOwn = true;
|
|
135
135
|
return this.#track(this.#bridge.sendCommand(method, captured).then((result) => {
|
|
136
136
|
const capturedResult = snapshot(result);
|
|
137
137
|
if (method === 'releasePolicy') this.#nativeMayOwn = false;
|
|
138
138
|
if (this.#state !== 'closing' && (method === 'reconcilePolicy' || method === 'releasePolicy'
|
|
139
|
-
|| method === 'inspectPolicy' && (capturedResult as IManagedNftStatus).error === null)) this.#state = 'ready';
|
|
139
|
+
|| method === 'inspectPolicy' && (capturedResult as IManagedNftStatus<TPolicy>).error === null)) this.#state = 'ready';
|
|
140
140
|
return capturedResult;
|
|
141
141
|
}));
|
|
142
142
|
}
|
package/ts/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export { SmartNftables } from './nft.manager.js';
|
|
3
3
|
export { ManagedNftables, ManagedNftablesError } from './classes.managednftables.js';
|
|
4
4
|
export type * from './managed.types.js';
|
|
5
|
+
export type * from './managed.egress.types.js';
|
|
5
6
|
|
|
6
7
|
// Sub-managers
|
|
7
8
|
export { NatManager } from './nft.manager.nat.js';
|
|
@@ -61,3 +62,5 @@ export type {
|
|
|
61
62
|
ISmartNftablesOptions,
|
|
62
63
|
INftStatus,
|
|
63
64
|
} from './nft.types.js';
|
|
65
|
+
export * from './managed.docker.types.js';
|
|
66
|
+
export * from './classes.manageddockerforwarding.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { IAppliedManagedNftPolicy, IManagedNftOptions } from './managed.types.js';
|
|
2
|
+
import type { IManagedNftPolicyV2, IManagedNftHostTransitScopeV2 } from './managed.egress.types.js';
|
|
3
|
+
|
|
4
|
+
export interface IManagedDockerForwardingOptions extends Omit<IManagedNftOptions, 'tableName'> {}
|
|
5
|
+
export interface IManagedDockerForwardingPolicy {
|
|
6
|
+
schemaVersion: 1;
|
|
7
|
+
revision: number;
|
|
8
|
+
/** Exact dedicated host barrier. This contribution never adopts or modifies its table. */
|
|
9
|
+
barrier: IAppliedManagedNftPolicy<IManagedNftPolicyV2 & { scope: IManagedNftHostTransitScopeV2 }>;
|
|
10
|
+
}
|
|
11
|
+
export interface IPreparedManagedDockerForwardingPolicy {
|
|
12
|
+
policy: IManagedDockerForwardingPolicy;
|
|
13
|
+
/** Exact nft frontend version captured during prepare and checked through recovery. */
|
|
14
|
+
backend: string;
|
|
15
|
+
digest: string;
|
|
16
|
+
}
|
|
17
|
+
export interface IManagedDockerForwardingIdentity {
|
|
18
|
+
ownerId: string;
|
|
19
|
+
instanceId: string;
|
|
20
|
+
bootId: string;
|
|
21
|
+
namespaceDevice: string;
|
|
22
|
+
namespaceInode: string;
|
|
23
|
+
}
|
|
24
|
+
export interface IAppliedManagedDockerForwardingPolicy {
|
|
25
|
+
receipt: { identity: IManagedDockerForwardingIdentity; backend: string; revision: number; digest: string };
|
|
26
|
+
prepared: IPreparedManagedDockerForwardingPolicy;
|
|
27
|
+
}
|
|
28
|
+
/** Persist before mutation. Keep both generations' exact handoff links down until reconciliation is confirmed. */
|
|
29
|
+
export interface IManagedDockerForwardingTransition {
|
|
30
|
+
previous: IAppliedManagedDockerForwardingPolicy | null;
|
|
31
|
+
target: IPreparedManagedDockerForwardingPolicy;
|
|
32
|
+
}
|
|
33
|
+
export interface IManagedDockerForwardingStatus {
|
|
34
|
+
identity: IManagedDockerForwardingIdentity;
|
|
35
|
+
state: 'idle' | 'applied' | 'failed-owned' | 'released';
|
|
36
|
+
/** Exact contribution and barrier inspected; does not attest the whole host path or durable fencing. */
|
|
37
|
+
present: boolean;
|
|
38
|
+
applied: IAppliedManagedDockerForwardingPolicy | null;
|
|
39
|
+
pending: IManagedDockerForwardingTransition | null;
|
|
40
|
+
error: string | null;
|
|
41
|
+
}
|
|
42
|
+
export type TManagedDockerForwardingCommands = {
|
|
43
|
+
prepareDockerForwarding: { params: IManagedDockerForwardingPolicy; result: IPreparedManagedDockerForwardingPolicy };
|
|
44
|
+
openDockerForwarding: { params: Pick<IManagedDockerForwardingOptions, 'ownerId' | 'instanceId'>; result: IManagedDockerForwardingStatus };
|
|
45
|
+
reconcileDockerForwarding: { params: IManagedDockerForwardingTransition; result: IAppliedManagedDockerForwardingPolicy };
|
|
46
|
+
inspectDockerForwarding: { params: Record<string, never>; result: IManagedDockerForwardingStatus };
|
|
47
|
+
releaseDockerForwarding: { params: IAppliedManagedDockerForwardingPolicy; result: { released: true } };
|
|
48
|
+
closeDockerForwarding: { params: Record<string, never>; result: { released: true } };
|
|
49
|
+
};
|