@push.rocks/smartnftables 1.2.1 → 1.4.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 +37 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +14 -0
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +14 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.managednftables.d.ts +21 -0
- package/dist_ts/classes.managednftables.js +182 -0
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/managed.types.d.ts +103 -0
- package/dist_ts/managed.types.js +2 -0
- package/dist_ts/plugins.d.ts +5 -2
- package/dist_ts/plugins.js +6 -3
- package/native-notices/crates/block-buffer-0.10.4/license-apache +201 -0
- package/native-notices/crates/block-buffer-0.10.4/license-mit +25 -0
- package/native-notices/crates/cfg-if-1.0.4/license-apache +201 -0
- package/native-notices/crates/cfg-if-1.0.4/license-mit +25 -0
- package/native-notices/crates/cpufeatures-0.2.17/license-apache +201 -0
- package/native-notices/crates/cpufeatures-0.2.17/license-mit +25 -0
- package/native-notices/crates/crypto-common-0.1.7/license-apache +201 -0
- package/native-notices/crates/crypto-common-0.1.7/license-mit +25 -0
- package/native-notices/crates/digest-0.10.7/license-apache +201 -0
- package/native-notices/crates/digest-0.10.7/license-mit +25 -0
- package/native-notices/crates/generic-array-0.14.7/license +21 -0
- package/native-notices/crates/itoa-1.0.18/license-apache +176 -0
- package/native-notices/crates/itoa-1.0.18/license-mit +23 -0
- package/native-notices/crates/libc-0.2.189/license-apache +176 -0
- package/native-notices/crates/libc-0.2.189/license-mit +25 -0
- package/native-notices/crates/memchr-2.8.3/copying +3 -0
- package/native-notices/crates/memchr-2.8.3/license-mit +21 -0
- package/native-notices/crates/proc-macro2-1.0.107/license-apache +176 -0
- package/native-notices/crates/proc-macro2-1.0.107/license-mit +23 -0
- package/native-notices/crates/quote-1.0.47/license-apache +176 -0
- package/native-notices/crates/quote-1.0.47/license-mit +23 -0
- package/native-notices/crates/serde-1.0.229/license-apache +176 -0
- package/native-notices/crates/serde-1.0.229/license-mit +23 -0
- package/native-notices/crates/serde_core-1.0.229/license-apache +176 -0
- package/native-notices/crates/serde_core-1.0.229/license-mit +23 -0
- package/native-notices/crates/serde_derive-1.0.229/license-apache +176 -0
- package/native-notices/crates/serde_derive-1.0.229/license-mit +23 -0
- package/native-notices/crates/serde_json-1.0.151/license-apache +176 -0
- package/native-notices/crates/serde_json-1.0.151/license-mit +23 -0
- package/native-notices/crates/sha2-0.10.9/license-apache +201 -0
- package/native-notices/crates/sha2-0.10.9/license-mit +27 -0
- package/native-notices/crates/syn-3.0.5/license-apache +176 -0
- package/native-notices/crates/syn-3.0.5/license-mit +23 -0
- package/native-notices/crates/typenum-1.20.1/license +1 -0
- package/native-notices/crates/typenum-1.20.1/license-apache +201 -0
- package/native-notices/crates/typenum-1.20.1/license-mit +21 -0
- package/native-notices/crates/unicode-ident-1.0.24/license-apache +176 -0
- package/native-notices/crates/unicode-ident-1.0.24/license-mit +23 -0
- package/native-notices/crates/unicode-ident-1.0.24/license-unicode +39 -0
- package/native-notices/crates/version_check-0.9.5/license-apache +201 -0
- package/native-notices/crates/version_check-0.9.5/license-mit +19 -0
- package/native-notices/crates/zmij-1.0.23/license-mit +23 -0
- package/native-notices/manifest.json +520 -0
- package/native-notices/readme.md +36 -0
- package/native-notices/rust-standard-library/addr2line-0.25.1-license-apache +201 -0
- package/native-notices/rust-standard-library/addr2line-0.25.1-license-mit +25 -0
- package/native-notices/rust-standard-library/cargo.lock +522 -0
- package/native-notices/rust-standard-library/compiler-builtins-license.txt +275 -0
- package/native-notices/rust-standard-library/dependency-notices.html +9072 -0
- package/native-notices/rust-standard-library/license-apache +176 -0
- package/native-notices/rust-standard-library/license-mit +25 -0
- package/native-notices/rust-standard-library/llvm-libunwind-license.txt +311 -0
- package/native-notices/rust-standard-library/musl-1.2.5-copyright.txt +193 -0
- package/native-notices/rust-standard-library/unicode-license.txt +39 -0
- package/package.json +23 -8
- package/readme.md +160 -2
- package/rust/.cargo/config.toml +2 -0
- package/rust/Cargo.lock +192 -0
- package/rust/Cargo.toml +13 -0
- package/rust/src/main.rs +159 -0
- package/rust/src/namespace.rs +51 -0
- package/rust/src/owner.rs +604 -0
- package/rust/src/owner_namespace_tests.rs +242 -0
- package/rust/src/owner_tests.rs +798 -0
- package/rust/src/policy.rs +427 -0
- package/rust/src/tests.rs +97 -0
- package/rust/src/wire.rs +437 -0
- package/scripts/check-native-notices.mjs +27 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.managednftables.ts +153 -0
- package/ts/index.ts +2 -0
- package/ts/managed.types.ts +91 -0
- package/ts/plugins.ts +6 -0
- /package/{license → license.md} +0 -0
package/rust/src/wire.rs
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
//! Small private codec for the Linux nftables UAPI. There is no public raw message API.
|
|
2
|
+
use crate::{Error, Result};
|
|
3
|
+
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
|
4
|
+
use std::time::{Duration, Instant};
|
|
5
|
+
|
|
6
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
7
|
+
pub struct Attr {
|
|
8
|
+
pub kind: u16,
|
|
9
|
+
pub value: Vec<u8>,
|
|
10
|
+
}
|
|
11
|
+
impl Attr {
|
|
12
|
+
pub fn bytes(kind: u16, value: Vec<u8>) -> Self {
|
|
13
|
+
Self { kind, value }
|
|
14
|
+
}
|
|
15
|
+
pub fn string(kind: u16, value: &str) -> Self {
|
|
16
|
+
Self::bytes(kind, [value.as_bytes(), &[0]].concat())
|
|
17
|
+
}
|
|
18
|
+
pub fn u32(kind: u16, value: u32) -> Self {
|
|
19
|
+
Self::bytes(kind, value.to_be_bytes().to_vec())
|
|
20
|
+
}
|
|
21
|
+
pub fn u64(kind: u16, value: u64) -> Self {
|
|
22
|
+
Self::bytes(kind, value.to_be_bytes().to_vec())
|
|
23
|
+
}
|
|
24
|
+
pub fn nested(kind: u16, value: Vec<Attr>) -> Self {
|
|
25
|
+
Self::bytes(kind | 0x8000, encode_attrs(&value))
|
|
26
|
+
}
|
|
27
|
+
pub fn id(&self) -> u16 {
|
|
28
|
+
self.kind & 0x3fff
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
pub fn encode_attrs(attrs: &[Attr]) -> Vec<u8> {
|
|
32
|
+
let mut result = Vec::new();
|
|
33
|
+
for attr in attrs {
|
|
34
|
+
let len = 4 + attr.value.len();
|
|
35
|
+
result.extend((len as u16).to_ne_bytes());
|
|
36
|
+
result.extend(attr.kind.to_ne_bytes());
|
|
37
|
+
result.extend(&attr.value);
|
|
38
|
+
result.resize((result.len() + 3) & !3, 0);
|
|
39
|
+
}
|
|
40
|
+
result
|
|
41
|
+
}
|
|
42
|
+
pub fn attrs(mut bytes: &[u8]) -> Result<Vec<Attr>> {
|
|
43
|
+
let mut result = Vec::new();
|
|
44
|
+
while !bytes.is_empty() {
|
|
45
|
+
if bytes.len() < 4 || result.len() > 4096 {
|
|
46
|
+
return Err(Error::Protocol);
|
|
47
|
+
}
|
|
48
|
+
let len = u16::from_ne_bytes(bytes[..2].try_into().unwrap()) as usize;
|
|
49
|
+
let size = (len + 3) & !3;
|
|
50
|
+
if len < 4 || size > bytes.len() {
|
|
51
|
+
return Err(Error::Protocol);
|
|
52
|
+
}
|
|
53
|
+
result.push(Attr::bytes(
|
|
54
|
+
u16::from_ne_bytes(bytes[2..4].try_into().unwrap()),
|
|
55
|
+
bytes[4..len].to_vec(),
|
|
56
|
+
));
|
|
57
|
+
bytes = &bytes[size..];
|
|
58
|
+
}
|
|
59
|
+
Ok(result)
|
|
60
|
+
}
|
|
61
|
+
pub fn one(attrs: &[Attr], id: u16) -> Result<&Attr> {
|
|
62
|
+
let mut matches = attrs.iter().filter(|v| v.id() == id);
|
|
63
|
+
let value = matches.next().ok_or(Error::Protocol)?;
|
|
64
|
+
if matches.next().is_some() {
|
|
65
|
+
return Err(Error::Protocol);
|
|
66
|
+
}
|
|
67
|
+
Ok(value)
|
|
68
|
+
}
|
|
69
|
+
pub fn text(attrs: &[Attr], id: u16) -> Result<String> {
|
|
70
|
+
let bytes = &one(attrs, id)?.value;
|
|
71
|
+
if bytes.last() != Some(&0) || bytes[..bytes.len() - 1].contains(&0) {
|
|
72
|
+
return Err(Error::Protocol);
|
|
73
|
+
}
|
|
74
|
+
String::from_utf8(bytes[..bytes.len() - 1].to_vec()).map_err(|_| Error::Protocol)
|
|
75
|
+
}
|
|
76
|
+
pub fn number(attrs: &[Attr], id: u16) -> Result<u32> {
|
|
77
|
+
Ok(u32::from_be_bytes(
|
|
78
|
+
one(attrs, id)?
|
|
79
|
+
.value
|
|
80
|
+
.as_slice()
|
|
81
|
+
.try_into()
|
|
82
|
+
.map_err(|_| Error::Protocol)?,
|
|
83
|
+
))
|
|
84
|
+
}
|
|
85
|
+
pub fn handle(attrs: &[Attr], id: u16) -> Result<u64> {
|
|
86
|
+
Ok(u64::from_be_bytes(
|
|
87
|
+
one(attrs, id)?
|
|
88
|
+
.value
|
|
89
|
+
.as_slice()
|
|
90
|
+
.try_into()
|
|
91
|
+
.map_err(|_| Error::Protocol)?,
|
|
92
|
+
))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Attribute order/byte-order flags are not identity; ordered nested expression lists are.
|
|
96
|
+
pub fn canonical(attributes: &[Attr], ordered: bool) -> Result<Vec<(u16, Vec<u8>)>> {
|
|
97
|
+
let mut result = Vec::new();
|
|
98
|
+
for attr in attributes {
|
|
99
|
+
let value = if attr.kind & 0x8000 != 0 {
|
|
100
|
+
let nested = attrs(&attr.value)?;
|
|
101
|
+
let ordered = nested.iter().all(|item| item.id() == 1);
|
|
102
|
+
serde_json::to_vec(&canonical(&nested, ordered)?).map_err(|_| Error::Protocol)?
|
|
103
|
+
} else {
|
|
104
|
+
attr.value.clone()
|
|
105
|
+
};
|
|
106
|
+
result.push((attr.id(), value));
|
|
107
|
+
}
|
|
108
|
+
if !ordered {
|
|
109
|
+
result.sort_by_key(|(id, _)| *id);
|
|
110
|
+
}
|
|
111
|
+
Ok(result)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
struct Message {
|
|
115
|
+
kind: u16,
|
|
116
|
+
flags: u16,
|
|
117
|
+
sequence: u32,
|
|
118
|
+
payload: Vec<u8>,
|
|
119
|
+
}
|
|
120
|
+
pub struct Socket {
|
|
121
|
+
fd: OwnedFd,
|
|
122
|
+
pub port: u32,
|
|
123
|
+
sequence: u32,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// Read-only, namespace-local RTM_GETLINK qualification. This is not a public
|
|
127
|
+
/// routing or link-creation escape; only the strict native compiler calls it.
|
|
128
|
+
pub fn verify_interfaces<'a>(
|
|
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
|
+
}
|
|
190
|
+
impl Socket {
|
|
191
|
+
pub fn open() -> Result<Self> {
|
|
192
|
+
Self::open_protocol(libc::NETLINK_NETFILTER)
|
|
193
|
+
}
|
|
194
|
+
fn open_protocol(protocol: i32) -> Result<Self> {
|
|
195
|
+
// SAFETY: valid AF_NETLINK sockaddr and owned descriptor, never shared with callers.
|
|
196
|
+
let raw = unsafe {
|
|
197
|
+
libc::socket(
|
|
198
|
+
libc::AF_NETLINK,
|
|
199
|
+
libc::SOCK_RAW | libc::SOCK_CLOEXEC,
|
|
200
|
+
protocol,
|
|
201
|
+
)
|
|
202
|
+
};
|
|
203
|
+
if raw < 0 {
|
|
204
|
+
return Err(Error::Unavailable);
|
|
205
|
+
}
|
|
206
|
+
let fd = unsafe { OwnedFd::from_raw_fd(raw) };
|
|
207
|
+
let mut address: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
|
|
208
|
+
address.nl_family = libc::AF_NETLINK as u16;
|
|
209
|
+
if unsafe {
|
|
210
|
+
libc::bind(
|
|
211
|
+
raw,
|
|
212
|
+
(&address as *const libc::sockaddr_nl).cast(),
|
|
213
|
+
std::mem::size_of_val(&address) as _,
|
|
214
|
+
)
|
|
215
|
+
} < 0
|
|
216
|
+
{
|
|
217
|
+
return Err(Error::Unavailable);
|
|
218
|
+
}
|
|
219
|
+
let mut len = std::mem::size_of_val(&address) as libc::socklen_t;
|
|
220
|
+
if unsafe {
|
|
221
|
+
libc::getsockname(
|
|
222
|
+
raw,
|
|
223
|
+
(&mut address as *mut libc::sockaddr_nl).cast(),
|
|
224
|
+
&mut len,
|
|
225
|
+
)
|
|
226
|
+
} < 0
|
|
227
|
+
{
|
|
228
|
+
return Err(Error::Unavailable);
|
|
229
|
+
}
|
|
230
|
+
let capacity: libc::c_int = 1_048_576;
|
|
231
|
+
for option in [libc::SO_SNDBUF, libc::SO_RCVBUF] {
|
|
232
|
+
if unsafe {
|
|
233
|
+
libc::setsockopt(
|
|
234
|
+
raw,
|
|
235
|
+
libc::SOL_SOCKET,
|
|
236
|
+
option,
|
|
237
|
+
(&capacity as *const libc::c_int).cast(),
|
|
238
|
+
std::mem::size_of_val(&capacity) as _,
|
|
239
|
+
)
|
|
240
|
+
} < 0
|
|
241
|
+
{
|
|
242
|
+
return Err(Error::Unavailable);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
Ok(Self {
|
|
246
|
+
fd,
|
|
247
|
+
port: address.nl_pid,
|
|
248
|
+
sequence: 0,
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
fn sequence(&mut self) -> Result<u32> {
|
|
252
|
+
self.sequence = self.sequence.checked_add(1).ok_or(Error::Unavailable)?;
|
|
253
|
+
Ok(self.sequence)
|
|
254
|
+
}
|
|
255
|
+
fn message(&mut self, kind: u16, flags: u16, attributes: &[Attr]) -> Result<(u32, Vec<u8>)> {
|
|
256
|
+
let sequence = self.sequence()?;
|
|
257
|
+
let batch = kind == 16 || kind == 17;
|
|
258
|
+
let payload = encode_attrs(attributes);
|
|
259
|
+
if payload.len() > 200_000 {
|
|
260
|
+
return Err(Error::Invalid);
|
|
261
|
+
}
|
|
262
|
+
let mut bytes = Vec::new();
|
|
263
|
+
bytes.extend(((payload.len() + 20) as u32).to_ne_bytes());
|
|
264
|
+
bytes.extend(kind.to_ne_bytes());
|
|
265
|
+
bytes.extend(flags.to_ne_bytes());
|
|
266
|
+
bytes.extend(sequence.to_ne_bytes());
|
|
267
|
+
bytes.extend(self.port.to_ne_bytes());
|
|
268
|
+
bytes.extend([if batch { 0 } else { 1 }, 0]);
|
|
269
|
+
bytes.extend(if batch { 10_u16 } else { 0 }.to_be_bytes());
|
|
270
|
+
bytes.extend(payload);
|
|
271
|
+
Ok((sequence, bytes))
|
|
272
|
+
}
|
|
273
|
+
fn send(&self, bytes: &[u8]) -> Result<()> {
|
|
274
|
+
if bytes.len() > 240_000 {
|
|
275
|
+
return Err(Error::Invalid);
|
|
276
|
+
}
|
|
277
|
+
let mut address: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
|
|
278
|
+
address.nl_family = libc::AF_NETLINK as u16;
|
|
279
|
+
let written = unsafe {
|
|
280
|
+
libc::sendto(
|
|
281
|
+
self.fd.as_raw_fd(),
|
|
282
|
+
bytes.as_ptr().cast(),
|
|
283
|
+
bytes.len(),
|
|
284
|
+
0,
|
|
285
|
+
(&address as *const libc::sockaddr_nl).cast(),
|
|
286
|
+
std::mem::size_of_val(&address) as _,
|
|
287
|
+
)
|
|
288
|
+
};
|
|
289
|
+
if written < 0 || written as usize != bytes.len() {
|
|
290
|
+
return Err(Error::Unavailable);
|
|
291
|
+
}
|
|
292
|
+
Ok(())
|
|
293
|
+
}
|
|
294
|
+
fn receive(&self, until: Instant) -> Result<Vec<Message>> {
|
|
295
|
+
let remaining = until.saturating_duration_since(Instant::now()).as_millis();
|
|
296
|
+
if remaining == 0 {
|
|
297
|
+
return Err(Error::Unavailable);
|
|
298
|
+
}
|
|
299
|
+
let mut poll = libc::pollfd {
|
|
300
|
+
fd: self.fd.as_raw_fd(),
|
|
301
|
+
events: libc::POLLIN,
|
|
302
|
+
revents: 0,
|
|
303
|
+
};
|
|
304
|
+
if unsafe { libc::poll(&mut poll, 1, remaining.min(3000) as i32) } <= 0 {
|
|
305
|
+
return Err(Error::Unavailable);
|
|
306
|
+
}
|
|
307
|
+
let mut bytes = vec![0_u8; 262_144];
|
|
308
|
+
let mut address: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
|
|
309
|
+
let mut length = std::mem::size_of_val(&address) as libc::socklen_t;
|
|
310
|
+
let received = unsafe {
|
|
311
|
+
libc::recvfrom(
|
|
312
|
+
self.fd.as_raw_fd(),
|
|
313
|
+
bytes.as_mut_ptr().cast(),
|
|
314
|
+
bytes.len(),
|
|
315
|
+
libc::MSG_TRUNC,
|
|
316
|
+
(&mut address as *mut libc::sockaddr_nl).cast(),
|
|
317
|
+
&mut length,
|
|
318
|
+
)
|
|
319
|
+
};
|
|
320
|
+
if received < 0 || received as usize > bytes.len() || address.nl_pid != 0 {
|
|
321
|
+
return Err(Error::Protocol);
|
|
322
|
+
}
|
|
323
|
+
bytes.truncate(received as usize);
|
|
324
|
+
let mut offset = 0;
|
|
325
|
+
let mut result = Vec::new();
|
|
326
|
+
while offset < bytes.len() {
|
|
327
|
+
if bytes.len() - offset < 16 {
|
|
328
|
+
return Err(Error::Protocol);
|
|
329
|
+
}
|
|
330
|
+
let header = &bytes[offset..offset + 16];
|
|
331
|
+
let size = u32::from_ne_bytes(header[..4].try_into().unwrap()) as usize;
|
|
332
|
+
if size < 16 || offset + size > bytes.len() {
|
|
333
|
+
return Err(Error::Protocol);
|
|
334
|
+
}
|
|
335
|
+
result.push(Message {
|
|
336
|
+
kind: u16::from_ne_bytes(header[4..6].try_into().unwrap()),
|
|
337
|
+
flags: u16::from_ne_bytes(header[6..8].try_into().unwrap()),
|
|
338
|
+
sequence: u32::from_ne_bytes(header[8..12].try_into().unwrap()),
|
|
339
|
+
payload: bytes[offset + 16..offset + size].to_vec(),
|
|
340
|
+
});
|
|
341
|
+
offset += (size + 3) & !3;
|
|
342
|
+
}
|
|
343
|
+
Ok(result)
|
|
344
|
+
}
|
|
345
|
+
pub fn query(
|
|
346
|
+
&mut self,
|
|
347
|
+
operation: u16,
|
|
348
|
+
input: Vec<Attr>,
|
|
349
|
+
dump: bool,
|
|
350
|
+
) -> Result<Vec<Vec<Attr>>> {
|
|
351
|
+
let (sequence, bytes) =
|
|
352
|
+
self.message(0x0a00 | operation, 1 | if dump { 0x300 } else { 0 }, &input)?;
|
|
353
|
+
self.send(&bytes)?;
|
|
354
|
+
let until = Instant::now() + Duration::from_secs(3);
|
|
355
|
+
let mut result = Vec::new();
|
|
356
|
+
let mut total = 0;
|
|
357
|
+
loop {
|
|
358
|
+
for message in self.receive(until)? {
|
|
359
|
+
if message.sequence != sequence {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if message.flags & 0x10 != 0 {
|
|
363
|
+
return Err(Error::Conflict);
|
|
364
|
+
}
|
|
365
|
+
if message.kind == 2 {
|
|
366
|
+
let code = i32::from_ne_bytes(
|
|
367
|
+
message
|
|
368
|
+
.payload
|
|
369
|
+
.get(..4)
|
|
370
|
+
.ok_or(Error::Protocol)?
|
|
371
|
+
.try_into()
|
|
372
|
+
.unwrap(),
|
|
373
|
+
);
|
|
374
|
+
if code != 0 {
|
|
375
|
+
return Err(Error::kernel(code));
|
|
376
|
+
}
|
|
377
|
+
} else if message.kind == 3 {
|
|
378
|
+
if message.payload.len() >= 4
|
|
379
|
+
&& i32::from_ne_bytes(message.payload[..4].try_into().unwrap()) != 0
|
|
380
|
+
{
|
|
381
|
+
return Err(Error::Conflict);
|
|
382
|
+
}
|
|
383
|
+
return Ok(result);
|
|
384
|
+
} else if message.kind & 0xff00 == 0x0a00 {
|
|
385
|
+
total += message.payload.len();
|
|
386
|
+
if total > 2_097_152 || result.len() >= 4096 || message.payload.len() < 4 {
|
|
387
|
+
return Err(Error::Protocol);
|
|
388
|
+
}
|
|
389
|
+
result.push(attrs(&message.payload[4..])?);
|
|
390
|
+
if !dump {
|
|
391
|
+
return Ok(result);
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
return Err(Error::Protocol);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
pub fn generation(&mut self) -> Result<u32> {
|
|
400
|
+
let values = self.query(16, vec![], false)?;
|
|
401
|
+
let generation = number(values.first().ok_or(Error::Protocol)?, 1)?;
|
|
402
|
+
if generation == 0 {
|
|
403
|
+
return Err(Error::Protocol);
|
|
404
|
+
}
|
|
405
|
+
Ok(generation)
|
|
406
|
+
}
|
|
407
|
+
pub fn batch(&mut self, generation: u32, operations: Vec<(u16, u16, Vec<Attr>)>) -> Result<()> {
|
|
408
|
+
if generation == 0 || operations.len() > 2048 {
|
|
409
|
+
return Err(Error::Invalid);
|
|
410
|
+
}
|
|
411
|
+
let (first, mut bytes) = self.message(16, 1 | 4, &[Attr::u32(1, generation)])?;
|
|
412
|
+
for (kind, flags, attributes) in operations {
|
|
413
|
+
bytes.extend(self.message(0x0a00 | kind, flags | 1 | 4, &attributes)?.1);
|
|
414
|
+
}
|
|
415
|
+
let (last, end) = self.message(17, 1 | 4, &[])?;
|
|
416
|
+
bytes.extend(end);
|
|
417
|
+
self.send(&bytes)?;
|
|
418
|
+
let until = Instant::now() + Duration::from_secs(3);
|
|
419
|
+
loop {
|
|
420
|
+
for message in self.receive(until)? {
|
|
421
|
+
if message.sequence < first || message.sequence > last {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if message.kind != 2 || message.payload.len() < 4 {
|
|
425
|
+
return Err(Error::Protocol);
|
|
426
|
+
}
|
|
427
|
+
let code = i32::from_ne_bytes(message.payload[..4].try_into().unwrap());
|
|
428
|
+
if code != 0 {
|
|
429
|
+
return Err(Error::kernel(code));
|
|
430
|
+
}
|
|
431
|
+
if message.sequence == last {
|
|
432
|
+
return Ok(());
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { resolve, relative } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const root = resolve(import.meta.dirname, '..');
|
|
7
|
+
const notices = resolve(root, 'native-notices');
|
|
8
|
+
const manifest = JSON.parse(readFileSync(resolve(notices, 'manifest.json'), 'utf8'));
|
|
9
|
+
const digest = (path) => createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
10
|
+
const requireEqual = (actual, expected, label) => {
|
|
11
|
+
if (actual !== expected) throw new Error(`Native notice inventory mismatch: ${label}; refresh and review notices before distribution.`);
|
|
12
|
+
};
|
|
13
|
+
requireEqual(digest(resolve(root, 'rust/Cargo.lock')), manifest.cargoLockSha256, 'Cargo.lock');
|
|
14
|
+
requireEqual(execFileSync('rustc', ['-vV'], { encoding: 'utf8' }).trim(), manifest.rustc, 'Rust compiler');
|
|
15
|
+
const sysroot = execFileSync('rustc', ['--print', 'sysroot'], { encoding: 'utf8' }).trim();
|
|
16
|
+
requireEqual(digest(resolve(sysroot, 'share/doc/rust/COPYRIGHT.html')), manifest.toolchainCopyrightSha256, 'toolchain copyright');
|
|
17
|
+
for (const [name, hash] of Object.entries(manifest.noticeHashes)) {
|
|
18
|
+
const path = resolve(notices, name);
|
|
19
|
+
if (relative(notices, path).startsWith('..')) throw new Error('Invalid notice inventory path.');
|
|
20
|
+
requireEqual(digest(path), hash, name);
|
|
21
|
+
}
|
|
22
|
+
for (const [name, hash] of Object.entries(manifest.runtimeArtifactHashes)) {
|
|
23
|
+
const path = resolve(sysroot, name);
|
|
24
|
+
if (relative(sysroot, path).startsWith('..')) throw new Error('Invalid runtime inventory path.');
|
|
25
|
+
requireEqual(digest(path), hash, name);
|
|
26
|
+
}
|
|
27
|
+
console.log('Native notices match the locked crates and qualified Rust runtime artifacts.');
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { IManagedNftOptions, IManagedNftPolicy, IManagedNftTransition, IAppliedManagedNftPolicy,
|
|
3
|
+
IPreparedManagedNftPolicy, IManagedNftStatus, TManagedNftCommands } from './managed.types.js';
|
|
4
|
+
|
|
5
|
+
export class ManagedNftablesError extends Error {
|
|
6
|
+
constructor(public readonly code: string) { super(`Managed nftables ${code}.`); }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Capture inert input without evaluating getters, proxies, coercions or toJSON. */
|
|
10
|
+
function snapshot<T>(input: T): T {
|
|
11
|
+
let count = 0;
|
|
12
|
+
let stringBytes = 0;
|
|
13
|
+
const string = (value: string): string => {
|
|
14
|
+
if (value.length > 240_000 || (stringBytes += Buffer.byteLength(value)) > 240_000) throw new ManagedNftablesError('INVALID');
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
const copy = (value: unknown, depth: number): unknown => {
|
|
18
|
+
if (++count > 16_384 || depth > 20) throw new ManagedNftablesError('INVALID');
|
|
19
|
+
if (typeof value === 'string') return string(value);
|
|
20
|
+
if (value === null || typeof value === 'boolean') return value;
|
|
21
|
+
if (typeof value === 'number' && Number.isSafeInteger(value)) return value;
|
|
22
|
+
if (typeof value !== 'object' || plugins.util.types.isProxy(value)) throw new ManagedNftablesError('INVALID');
|
|
23
|
+
const array = Array.isArray(value);
|
|
24
|
+
if (Object.getPrototypeOf(value) !== (array ? Array.prototype : Object.prototype)) throw new ManagedNftablesError('INVALID');
|
|
25
|
+
const keys = Reflect.ownKeys(value);
|
|
26
|
+
if (keys.length > 16_384 - count) throw new ManagedNftablesError('INVALID');
|
|
27
|
+
for (const key of keys) if (typeof key === 'string') string(key);
|
|
28
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
29
|
+
const result: Record<string, unknown> | unknown[] = array ? [] : {};
|
|
30
|
+
if (array && keys.length !== (value as unknown[]).length + 1) throw new ManagedNftablesError('INVALID');
|
|
31
|
+
for (const key of keys) {
|
|
32
|
+
if (array && key === 'length') continue;
|
|
33
|
+
const descriptor = descriptors[key as string];
|
|
34
|
+
if (typeof key !== 'string' || !descriptor.enumerable || !('value' in descriptor)
|
|
35
|
+
|| array && !/^(0|[1-9][0-9]*)$/.test(key)) throw new ManagedNftablesError('INVALID');
|
|
36
|
+
Object.defineProperty(result, key, { value: copy(descriptor.value, depth + 1), enumerable: true, configurable: true, writable: true });
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
const result = copy(input, 0) as T;
|
|
41
|
+
if (Buffer.byteLength(JSON.stringify(result)) > 240_000) throw new ManagedNftablesError('INVALID');
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One private native/netlink owner. Persistence and workload link lifetimes belong to its caller. */
|
|
46
|
+
export class ManagedNftables {
|
|
47
|
+
readonly #options: Readonly<IManagedNftOptions>;
|
|
48
|
+
#bridge: plugins.smartrust.RustBridge<TManagedNftCommands> | null = null;
|
|
49
|
+
#state: 'new' | 'starting' | 'ready' | 'closing' | 'failed-owned' | 'stopped' = 'new';
|
|
50
|
+
#work: Promise<unknown> | null = null;
|
|
51
|
+
#close: Promise<void> | null = null;
|
|
52
|
+
#nativeMayOwn = false;
|
|
53
|
+
|
|
54
|
+
constructor(options: IManagedNftOptions) {
|
|
55
|
+
this.#options = snapshot(options);
|
|
56
|
+
if (Object.keys(this.#options).some((key) => !['ownerId', 'instanceId', 'tableName', 'binaryPath', 'networkNamespaceFd'].includes(key))
|
|
57
|
+
|| ![this.#options.ownerId, this.#options.instanceId, this.#options.tableName].every((value) => typeof value === 'string' && /^[A-Za-z0-9_-]{1,64}$/.test(value))
|
|
58
|
+
|| this.#options.instanceId.length < 16 || !this.#options.tableName.startsWith('snft_')
|
|
59
|
+
|| this.#options.binaryPath !== undefined && (!plugins.path.isAbsolute(this.#options.binaryPath) || this.#options.binaryPath.includes('\0'))) {
|
|
60
|
+
throw new ManagedNftablesError('INVALID');
|
|
61
|
+
}
|
|
62
|
+
if (this.#options.networkNamespaceFd !== undefined && (process.platform !== 'linux'
|
|
63
|
+
|| !Number.isInteger(this.#options.networkNamespaceFd) || this.#options.networkNamespaceFd < 0
|
|
64
|
+
|| this.#options.networkNamespaceFd > 0x7fff_ffff)) throw new ManagedNftablesError('INVALID');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public get state(): string { return this.#state; }
|
|
68
|
+
|
|
69
|
+
/** Starts only the compiler/IPC owner. The returned idle status is not enforcement. */
|
|
70
|
+
public start(): Promise<IManagedNftStatus> {
|
|
71
|
+
if (this.#state !== 'new') return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
|
|
72
|
+
this.#state = 'starting';
|
|
73
|
+
const work = (async () => {
|
|
74
|
+
if (process.platform !== 'linux' || !['x64', 'arm64'].includes(process.arch)) throw new ManagedNftablesError('UNSUPPORTED_PLATFORM');
|
|
75
|
+
const root = plugins.url.fileURLToPath(new URL('../', import.meta.url));
|
|
76
|
+
this.#bridge = new plugins.smartrust.RustBridge<TManagedNftCommands>({
|
|
77
|
+
binaryName: 'smartnftables', binaryPath: this.#options.binaryPath
|
|
78
|
+
?? plugins.path.join(root, 'dist_rust', `smartnftables_linux_${process.arch === 'x64' ? 'amd64' : 'arm64'}_musl`),
|
|
79
|
+
localPaths: [], searchSystemPath: false,
|
|
80
|
+
cliArgs: ['--management', ...(this.#options.networkNamespaceFd === undefined ? [] : ['--network-namespace-fd', '3'])],
|
|
81
|
+
inheritedFileDescriptors: this.#options.networkNamespaceFd === undefined ? undefined : [this.#options.networkNamespaceFd],
|
|
82
|
+
readyTimeoutMs: 3000, requestTimeoutMs: 30_000, maxPayloadSize: 262_144,
|
|
83
|
+
maxPendingRequestsByMethod: { preparePolicy: 1, openOwner: 1, reconcilePolicy: 1, inspectPolicy: 1, releasePolicy: 1, closeOwner: 1 },
|
|
84
|
+
});
|
|
85
|
+
this.#bridge.on('exit', () => {
|
|
86
|
+
if (!['closing', 'stopped'].includes(this.#state)) this.#state = 'failed-owned';
|
|
87
|
+
});
|
|
88
|
+
if (!await this.#bridge.spawn()) throw new ManagedNftablesError('UNAVAILABLE');
|
|
89
|
+
const { ownerId, instanceId, tableName } = this.#options;
|
|
90
|
+
const status = snapshot(await this.#bridge.sendCommand('openOwner', { ownerId, instanceId, tableName }));
|
|
91
|
+
if (this.#state !== 'starting') throw new ManagedNftablesError('UNAVAILABLE');
|
|
92
|
+
this.#state = 'ready'; return status;
|
|
93
|
+
})();
|
|
94
|
+
return this.#track(work);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public prepare(policy: IManagedNftPolicy): Promise<IPreparedManagedNftPolicy> {
|
|
98
|
+
return this.#invoke('preparePolicy', policy);
|
|
99
|
+
}
|
|
100
|
+
public reconcile(transition: IManagedNftTransition): Promise<IAppliedManagedNftPolicy> {
|
|
101
|
+
return this.#invoke('reconcilePolicy', transition);
|
|
102
|
+
}
|
|
103
|
+
public inspect(): Promise<IManagedNftStatus> { return this.#invoke('inspectPolicy', {}); }
|
|
104
|
+
public release(expected: IAppliedManagedNftPolicy): Promise<{ released: true }> {
|
|
105
|
+
return this.#invoke('releasePolicy', expected);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Join policy operations and owned-table deletion, then native termination; packet/allocation fencing belongs to the caller. */
|
|
109
|
+
public close(): Promise<void> {
|
|
110
|
+
if (this.#close) return this.#close;
|
|
111
|
+
if (this.#state === 'stopped') return Promise.resolve();
|
|
112
|
+
this.#state = 'closing';
|
|
113
|
+
this.#close = (async () => {
|
|
114
|
+
await this.#work?.catch(() => undefined);
|
|
115
|
+
if (this.#bridge) {
|
|
116
|
+
if (this.#nativeMayOwn) {
|
|
117
|
+
const result = await this.#bridge.sendCommand('closeOwner', {});
|
|
118
|
+
if (result.released !== true) throw new ManagedNftablesError('PROTOCOL');
|
|
119
|
+
}
|
|
120
|
+
await this.#bridge.terminate({ gracePeriodMs: 1000 });
|
|
121
|
+
}
|
|
122
|
+
this.#bridge = null; this.#state = 'stopped';
|
|
123
|
+
})().catch(() => {
|
|
124
|
+
this.#state = 'failed-owned'; this.#close = null;
|
|
125
|
+
throw new ManagedNftablesError('CLEANUP_UNCONFIRMED');
|
|
126
|
+
});
|
|
127
|
+
return this.#close;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#invoke<K extends keyof TManagedNftCommands>(method: K, params: TManagedNftCommands[K]['params']): Promise<TManagedNftCommands[K]['result']> {
|
|
131
|
+
if (!['ready', 'failed-owned'].includes(this.#state) || !this.#bridge || this.#work) return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
|
|
132
|
+
let captured: TManagedNftCommands[K]['params'];
|
|
133
|
+
try { captured = snapshot(params); } catch { return Promise.reject(new ManagedNftablesError('INVALID')); }
|
|
134
|
+
if (method === 'reconcilePolicy' || method === 'releasePolicy') this.#nativeMayOwn = true;
|
|
135
|
+
return this.#track(this.#bridge.sendCommand(method, captured).then((result) => {
|
|
136
|
+
const capturedResult = snapshot(result);
|
|
137
|
+
if (method === 'releasePolicy') this.#nativeMayOwn = false;
|
|
138
|
+
if (this.#state !== 'closing' && (method === 'reconcilePolicy' || method === 'releasePolicy'
|
|
139
|
+
|| method === 'inspectPolicy' && (capturedResult as IManagedNftStatus).error === null)) this.#state = 'ready';
|
|
140
|
+
return capturedResult;
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
#track<T>(work: Promise<T>): Promise<T> {
|
|
144
|
+
const owned = work.catch((error) => {
|
|
145
|
+
if (this.#state !== 'closing') this.#state = 'failed-owned';
|
|
146
|
+
const code = error instanceof plugins.smartrust.RustBridgeRequestError ? error.responseErrorCode : undefined;
|
|
147
|
+
throw new ManagedNftablesError(typeof code === 'string' && /^[A-Z_]{1,64}$/.test(code) ? code : 'UNAVAILABLE');
|
|
148
|
+
});
|
|
149
|
+
this.#work = owned;
|
|
150
|
+
void owned.then(() => { this.#work = null; }, () => { this.#work = null; });
|
|
151
|
+
return owned;
|
|
152
|
+
}
|
|
153
|
+
}
|
package/ts/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Main facade
|
|
2
2
|
export { SmartNftables } from './nft.manager.js';
|
|
3
|
+
export { ManagedNftables, ManagedNftablesError } from './classes.managednftables.js';
|
|
4
|
+
export type * from './managed.types.js';
|
|
3
5
|
|
|
4
6
|
// Sub-managers
|
|
5
7
|
export { NatManager } from './nft.manager.nat.js';
|