@push.rocks/smartnftables 4.0.1 → 4.2.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 +19 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +5 -5
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +5 -5
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.manageddockerforwarding.d.ts +2 -1
- package/dist_ts/classes.manageddockerforwarding.js +3 -2
- package/dist_ts/forwardhooks.d.ts +8 -0
- package/dist_ts/forwardhooks.js +42 -0
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/managed.docker.types.d.ts +2 -1
- package/dist_ts/managed.egress.types.d.ts +5 -0
- package/dist_ts/managed.forwardhooks.types.d.ts +35 -0
- package/dist_ts/managed.forwardhooks.types.js +2 -0
- package/package.json +5 -4
- package/readme.md +139 -19
- package/rust/src/docker.frontend.rs +2 -2
- package/rust/src/docker.graph.rs +36 -11
- package/rust/src/docker.policy.rs +95 -32
- package/rust/src/docker.rs +37 -13
- package/rust/src/docker_tests.rs +164 -2
- package/rust/src/egress.host.rs +53 -45
- package/rust/src/egress.rs +64 -0
- package/rust/src/egress_exclusiveforwarding_tests.rs +256 -0
- package/rust/src/forwardhooks.rs +176 -0
- package/rust/src/forwardhooks_tests.rs +136 -0
- package/rust/src/main.rs +12 -8
- package/rust/src/owner_exclusiveforwarding_tests.rs +200 -0
- package/rust/src/owner_host_traffic_tests.rs +1 -1
- package/rust/src/owner_tests.rs +3 -0
- package/rust/src/wire.links.rs +28 -6
- package/rust/src/wire.rs +22 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.manageddockerforwarding.ts +2 -1
- package/ts/forwardhooks.ts +39 -0
- package/ts/index.ts +2 -0
- package/ts/managed.docker.types.ts +2 -1
- package/ts/managed.egress.types.ts +5 -0
- package/ts/managed.forwardhooks.types.ts +26 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
//! Exclusive forwarding qualified on a host namespace that forwards IPv4 and
|
|
2
|
+
//! IPv6 between a handoff, an uplink and a LAN link no scope binds.
|
|
3
|
+
use super::egress_traffic_tests::{connect, host_policy, ns_ip, roundtrip, socket};
|
|
4
|
+
use super::host_traffic_tests::{control, ipv6_pair};
|
|
5
|
+
use super::*;
|
|
6
|
+
use crate::egress;
|
|
7
|
+
use std::net::UdpSocket;
|
|
8
|
+
use std::time::Duration;
|
|
9
|
+
|
|
10
|
+
const SOURCE: &str = "xf_source";
|
|
11
|
+
const HOST: &str = "xf_host";
|
|
12
|
+
const PEER: &str = "xf_peer";
|
|
13
|
+
const LAN: &str = "xf_lan";
|
|
14
|
+
|
|
15
|
+
/// The fixture host policy at `revision`, with the member and the handoffs
|
|
16
|
+
/// chosen. It reads the bound links, so it runs in the host namespace.
|
|
17
|
+
fn policy(revision: u64, exclusive: bool, handoffs: bool) -> Prepared {
|
|
18
|
+
let mut policy = host_policy();
|
|
19
|
+
policy.revision = revision;
|
|
20
|
+
let egress::Scope::HostTransit(scope) = &mut policy.scope else {
|
|
21
|
+
panic!()
|
|
22
|
+
};
|
|
23
|
+
scope.exclusive_forwarding = exclusive;
|
|
24
|
+
if !handoffs {
|
|
25
|
+
scope.handoffs.clear();
|
|
26
|
+
}
|
|
27
|
+
let target: Prepared = policy.prepare().unwrap().into();
|
|
28
|
+
target
|
|
29
|
+
.validate_interfaces()
|
|
30
|
+
.expect("exclusive forwarding fixture binding");
|
|
31
|
+
target
|
|
32
|
+
}
|
|
33
|
+
/// A UDP socket in `ns` on an address built at run time.
|
|
34
|
+
fn bound(ns: &str, address: String) -> UdpSocket {
|
|
35
|
+
let socket = in_namespace(ns, move || UdpSocket::bind(address).unwrap());
|
|
36
|
+
socket
|
|
37
|
+
.set_read_timeout(Some(Duration::from_millis(250)))
|
|
38
|
+
.unwrap();
|
|
39
|
+
socket
|
|
40
|
+
}
|
|
41
|
+
/// Every forward this host performs for the LAN link: the host as the LAN's
|
|
42
|
+
/// gateway to the uplink peer, the peer's way back into the LAN, and the same
|
|
43
|
+
/// gateway for IPv6. Fresh ports per call give each probe its own conntrack entry.
|
|
44
|
+
fn lan_probes(port: u16, allowed: bool) {
|
|
45
|
+
for (from, source, to, destination) in [
|
|
46
|
+
(
|
|
47
|
+
LAN,
|
|
48
|
+
format!("172.16.0.2:{port}"),
|
|
49
|
+
PEER,
|
|
50
|
+
format!("192.0.2.3:{port}"),
|
|
51
|
+
),
|
|
52
|
+
(
|
|
53
|
+
PEER,
|
|
54
|
+
format!("192.0.2.3:{}", port + 1),
|
|
55
|
+
LAN,
|
|
56
|
+
format!("172.16.0.2:{}", port + 1),
|
|
57
|
+
),
|
|
58
|
+
(
|
|
59
|
+
LAN,
|
|
60
|
+
format!("[fd00:16::2]:{port}"),
|
|
61
|
+
PEER,
|
|
62
|
+
format!("[2001:db8:190::3]:{port}"),
|
|
63
|
+
),
|
|
64
|
+
] {
|
|
65
|
+
let target = bound(to, destination.clone());
|
|
66
|
+
control(&bound(from, source), &destination, &target, allowed);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/// Replaces the applied policy with `policy(revision, exclusive, handoffs)`,
|
|
70
|
+
/// bound in the host namespace that holds the links.
|
|
71
|
+
fn reconcile(
|
|
72
|
+
owner: Owner,
|
|
73
|
+
previous: Applied,
|
|
74
|
+
(revision, exclusive, handoffs): (u64, bool, bool),
|
|
75
|
+
) -> (Owner, Applied) {
|
|
76
|
+
in_namespace(HOST, move || {
|
|
77
|
+
let mut owner = owner;
|
|
78
|
+
let applied = owner
|
|
79
|
+
.reconcile(Transition {
|
|
80
|
+
previous: Some(previous),
|
|
81
|
+
target: policy(revision, exclusive, handoffs),
|
|
82
|
+
})
|
|
83
|
+
.unwrap();
|
|
84
|
+
assert!(owner.inspect().enforced);
|
|
85
|
+
(owner, applied)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
#[ignore = "requires disposable isolated native qualification guest"]
|
|
91
|
+
fn v2_host_exclusive_forwarding_forwards_only_the_scope_and_withdraws_cleanly() {
|
|
92
|
+
isolated();
|
|
93
|
+
for name in [SOURCE, HOST, PEER, LAN] {
|
|
94
|
+
ip(&["netns", "add", name]);
|
|
95
|
+
ns_ip(name, &["link", "set", "lo", "up"]);
|
|
96
|
+
in_namespace(name, || {
|
|
97
|
+
std::fs::write("/proc/sys/net/ipv4/ip_forward", "1").unwrap();
|
|
98
|
+
std::fs::write("/proc/sys/net/ipv6/conf/all/forwarding", "1").unwrap();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
connect(
|
|
102
|
+
SOURCE,
|
|
103
|
+
"source",
|
|
104
|
+
"10.240.0.2/30",
|
|
105
|
+
HOST,
|
|
106
|
+
"router",
|
|
107
|
+
"10.240.0.1/30",
|
|
108
|
+
);
|
|
109
|
+
connect(
|
|
110
|
+
HOST,
|
|
111
|
+
"uplink",
|
|
112
|
+
"192.0.2.2/24",
|
|
113
|
+
PEER,
|
|
114
|
+
"underlay",
|
|
115
|
+
"192.0.2.3/24",
|
|
116
|
+
);
|
|
117
|
+
connect(
|
|
118
|
+
HOST,
|
|
119
|
+
"lanlink",
|
|
120
|
+
"172.16.0.1/24",
|
|
121
|
+
LAN,
|
|
122
|
+
"lanpeer",
|
|
123
|
+
"172.16.0.2/24",
|
|
124
|
+
);
|
|
125
|
+
ns_ip(SOURCE, &["route", "add", "default", "via", "10.240.0.1"]);
|
|
126
|
+
ns_ip(PEER, &["route", "add", "default", "via", "192.0.2.2"]);
|
|
127
|
+
ns_ip(LAN, &["route", "add", "default", "via", "172.16.0.1"]);
|
|
128
|
+
ipv6_pair(
|
|
129
|
+
HOST,
|
|
130
|
+
"uplink",
|
|
131
|
+
"2001:db8:190::108",
|
|
132
|
+
PEER,
|
|
133
|
+
"underlay",
|
|
134
|
+
"2001:db8:190::3",
|
|
135
|
+
);
|
|
136
|
+
ipv6_pair(HOST, "lanlink", "fd00:16::1", LAN, "lanpeer", "fd00:16::2");
|
|
137
|
+
ns_ip(
|
|
138
|
+
PEER,
|
|
139
|
+
&["-6", "route", "add", "default", "via", "2001:db8:190::108"],
|
|
140
|
+
);
|
|
141
|
+
ns_ip(LAN, &["-6", "route", "add", "default", "via", "fd00:16::1"]);
|
|
142
|
+
// The leased egress path: the transit source and a leased port to the
|
|
143
|
+
// uplink peer's platform endpoint, answered through outer SNAT.
|
|
144
|
+
let leased = socket(SOURCE, "10.240.0.2:10000");
|
|
145
|
+
let platform = socket(PEER, "192.0.2.3:5300");
|
|
146
|
+
let unleased = socket(SOURCE, "10.240.0.2:9999");
|
|
147
|
+
// Positive controls: without any policy the host forwards for the LAN in
|
|
148
|
+
// both directions and both families.
|
|
149
|
+
lan_probes(41000, true);
|
|
150
|
+
// The scope without the member leaves forwarding outside its handoffs to
|
|
151
|
+
// other owners, so every later denial is caused by the member.
|
|
152
|
+
let (owner, applied) = in_namespace(HOST, || {
|
|
153
|
+
let mut owner = Owner::new(options("host_exclusive")).unwrap();
|
|
154
|
+
let applied = owner
|
|
155
|
+
.reconcile(Transition {
|
|
156
|
+
previous: None,
|
|
157
|
+
target: policy(1, false, true),
|
|
158
|
+
})
|
|
159
|
+
.unwrap();
|
|
160
|
+
assert!(owner.inspect().enforced);
|
|
161
|
+
(owner, applied)
|
|
162
|
+
});
|
|
163
|
+
lan_probes(41010, true);
|
|
164
|
+
assert_eq!(roundtrip(&leased, &platform).ip().to_string(), "192.0.2.2");
|
|
165
|
+
// A LAN flow already established through the host before the member.
|
|
166
|
+
let established = socket(LAN, "172.16.0.2:41100");
|
|
167
|
+
let established_target = socket(PEER, "192.0.2.3:41100");
|
|
168
|
+
assert_eq!(
|
|
169
|
+
roundtrip(&established, &established_target)
|
|
170
|
+
.ip()
|
|
171
|
+
.to_string(),
|
|
172
|
+
"172.16.0.2"
|
|
173
|
+
);
|
|
174
|
+
// Exclusive: only the scope's leased handoff flows still cross the host.
|
|
175
|
+
let (owner, applied) = reconcile(owner, applied, (2, true, true));
|
|
176
|
+
lan_probes(41020, false);
|
|
177
|
+
control(&established, "192.0.2.3:41100", &established_target, false);
|
|
178
|
+
assert_eq!(roundtrip(&leased, &platform).ip().to_string(), "192.0.2.2");
|
|
179
|
+
control(&unleased, "192.0.2.3:5300", &platform, false);
|
|
180
|
+
// Without handoffs the exclusive scope forwards nothing at all.
|
|
181
|
+
let (owner, applied) = reconcile(owner, applied, (3, true, false));
|
|
182
|
+
lan_probes(41030, false);
|
|
183
|
+
control(&leased, "192.0.2.3:5300", &platform, false);
|
|
184
|
+
// Replacing it with a scope without the member reopens the LAN path.
|
|
185
|
+
let (owner, applied) = reconcile(owner, applied, (4, false, true));
|
|
186
|
+
lan_probes(41040, true);
|
|
187
|
+
assert_eq!(roundtrip(&leased, &platform).ip().to_string(), "192.0.2.2");
|
|
188
|
+
// Release of an exclusive scope reopens it as well.
|
|
189
|
+
let (owner, applied) = reconcile(owner, applied, (5, true, true));
|
|
190
|
+
lan_probes(41050, false);
|
|
191
|
+
in_namespace(HOST, move || {
|
|
192
|
+
let mut owner = owner;
|
|
193
|
+
assert!(owner.inspect().enforced);
|
|
194
|
+
owner.release(applied).unwrap();
|
|
195
|
+
});
|
|
196
|
+
lan_probes(41060, true);
|
|
197
|
+
let reopened = socket(SOURCE, "10.240.0.2:10001");
|
|
198
|
+
control(&reopened, "192.0.2.3:5300", &platform, true);
|
|
199
|
+
println!("V2_HOST_EXCLUSIVE_FORWARDING_PROOF positive_controls=true member_absent_lan_forwarded=true lan_ipv4_denied=true lan_reverse_denied=true lan_ipv6_denied=true established_foreign_flow_stopped=true leased_egress_kept=true unleased_port_denied=true empty_scope_forwards_nothing=true replacement_reopens=true release_reopens=true");
|
|
200
|
+
}
|
|
@@ -121,7 +121,7 @@ pub(super) fn control(source: &UdpSocket, destination: &str, target: &UdpSocket,
|
|
|
121
121
|
assert!(target.recv(&mut [0; 64]).is_err());
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
-
fn ipv6_pair(a: &str, aname: &str, aip: &str, b: &str, bname: &str, bip: &str) {
|
|
124
|
+
pub(super) fn ipv6_pair(a: &str, aname: &str, aip: &str, b: &str, bname: &str, bip: &str) {
|
|
125
125
|
let aname_owned = aname.to_owned();
|
|
126
126
|
let bname_owned = bname.to_owned();
|
|
127
127
|
let amac = in_namespace(a, move || packet_fixture::mac(&aname_owned));
|
package/rust/src/owner_tests.rs
CHANGED
package/rust/src/wire.links.rs
CHANGED
|
@@ -22,6 +22,11 @@ pub struct BoundInterface<'a> {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
fn read_link(socket: &mut Socket, index: u32) -> Result<Link> {
|
|
25
|
+
query_link(socket, index)?.map_err(Error::kernel)
|
|
26
|
+
}
|
|
27
|
+
/// The link at `index`, or the kernel's refusal code, such as `-ENODEV` for an
|
|
28
|
+
/// index no link holds.
|
|
29
|
+
fn query_link(socket: &mut Socket, index: u32) -> Result<std::result::Result<Link, i32>> {
|
|
25
30
|
let sequence = socket.sequence()?;
|
|
26
31
|
let mut request = Vec::new();
|
|
27
32
|
request.extend(32_u32.to_ne_bytes());
|
|
@@ -49,17 +54,17 @@ fn read_link(socket: &mut Socket, index: u32) -> Result<Link> {
|
|
|
49
54
|
.unwrap(),
|
|
50
55
|
);
|
|
51
56
|
if error != 0 {
|
|
52
|
-
return Err(
|
|
57
|
+
return Ok(Err(error));
|
|
53
58
|
}
|
|
54
59
|
} else if message.kind == 16 && message.payload.len() >= 16 {
|
|
55
60
|
if u32::from_ne_bytes(message.payload[4..8].try_into().unwrap()) != index {
|
|
56
61
|
return Err(Error::Conflict);
|
|
57
62
|
}
|
|
58
|
-
return Ok(Link {
|
|
63
|
+
return Ok(Ok(Link {
|
|
59
64
|
hardware: u16::from_ne_bytes(message.payload[2..4].try_into().unwrap()),
|
|
60
65
|
flags: u32::from_ne_bytes(message.payload[8..12].try_into().unwrap()),
|
|
61
66
|
attributes: attrs(&message.payload[16..])?,
|
|
62
|
-
});
|
|
67
|
+
}));
|
|
63
68
|
} else {
|
|
64
69
|
return Err(Error::Protocol);
|
|
65
70
|
}
|
|
@@ -271,9 +276,26 @@ pub fn verify_bound_interfaces<'a>(
|
|
|
271
276
|
pub fn verify_bound_interfaces_down<'a>(interfaces: impl Iterator<Item = BoundInterface<'a>>) -> Result<()> {
|
|
272
277
|
let mut socket = Socket::open_protocol(libc::NETLINK_ROUTE)?;
|
|
273
278
|
for binding in interfaces {
|
|
274
|
-
|
|
275
|
-
matches(&link, &binding)?;
|
|
276
|
-
if link.flags & libc::IFF_UP as u32 != 0 {return Err(Error::Conflict);}
|
|
279
|
+
down(&read_link(&mut socket, binding.index)?, &binding)?;
|
|
277
280
|
}
|
|
278
281
|
Ok(())
|
|
279
282
|
}
|
|
283
|
+
/// Cleanup fence: a bound link whose index no link holds any more carries no
|
|
284
|
+
/// packets and counts as down. A link at that index must still be the exact
|
|
285
|
+
/// bound link, and down.
|
|
286
|
+
pub fn verify_bound_interfaces_down_or_absent<'a>(interfaces: impl Iterator<Item = BoundInterface<'a>>) -> Result<()> {
|
|
287
|
+
let mut socket = Socket::open_protocol(libc::NETLINK_ROUTE)?;
|
|
288
|
+
for binding in interfaces {
|
|
289
|
+
match query_link(&mut socket, binding.index)? {
|
|
290
|
+
Ok(link) => down(&link, &binding)?,
|
|
291
|
+
Err(code) if code == -libc::ENODEV => {}
|
|
292
|
+
Err(code) => return Err(Error::kernel(code)),
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
Ok(())
|
|
296
|
+
}
|
|
297
|
+
fn down(link: &Link, binding: &BoundInterface<'_>) -> Result<()> {
|
|
298
|
+
matches(link, binding)?;
|
|
299
|
+
if link.flags & libc::IFF_UP as u32 != 0 {return Err(Error::Conflict);}
|
|
300
|
+
Ok(())
|
|
301
|
+
}
|
package/rust/src/wire.rs
CHANGED
|
@@ -139,7 +139,10 @@ pub struct Socket {
|
|
|
139
139
|
|
|
140
140
|
#[path = "wire.links.rs"]
|
|
141
141
|
mod links;
|
|
142
|
-
pub use links::{
|
|
142
|
+
pub use links::{
|
|
143
|
+
verify_bound_interfaces, verify_bound_interfaces_down, verify_bound_interfaces_down_or_absent,
|
|
144
|
+
verify_interfaces, BoundInterface,
|
|
145
|
+
};
|
|
143
146
|
impl Socket {
|
|
144
147
|
pub fn open() -> Result<Self> {
|
|
145
148
|
Self::open_protocol(libc::NETLINK_NETFILTER)
|
|
@@ -311,6 +314,23 @@ impl Socket {
|
|
|
311
314
|
self.query_family(operation, input, dump, 2)
|
|
312
315
|
}
|
|
313
316
|
fn query_family(&mut self, operation: u16, input: Vec<Attr>, dump: bool, family: u8) -> Result<Vec<Vec<Attr>>> {
|
|
317
|
+
Ok(self
|
|
318
|
+
.query_families(operation, input, dump, family)?
|
|
319
|
+
.into_iter()
|
|
320
|
+
.map(|(_, attributes)| attributes)
|
|
321
|
+
.collect())
|
|
322
|
+
}
|
|
323
|
+
/// Dump every object of one kind across all families, each with its family.
|
|
324
|
+
pub fn dump_all_families(&mut self, operation: u16) -> Result<Vec<(u8, Vec<Attr>)>> {
|
|
325
|
+
self.query_families(operation, vec![], true, 0)
|
|
326
|
+
}
|
|
327
|
+
fn query_families(
|
|
328
|
+
&mut self,
|
|
329
|
+
operation: u16,
|
|
330
|
+
input: Vec<Attr>,
|
|
331
|
+
dump: bool,
|
|
332
|
+
family: u8,
|
|
333
|
+
) -> Result<Vec<(u8, Vec<Attr>)>> {
|
|
314
334
|
let (sequence, bytes) =
|
|
315
335
|
self.message_family(0x0a00 | operation, 1 | if dump { 0x300 } else { 0 }, &input, family)?;
|
|
316
336
|
self.send(&bytes)?;
|
|
@@ -350,7 +370,7 @@ impl Socket {
|
|
|
350
370
|
return Err(Error::Protocol);
|
|
351
371
|
}
|
|
352
372
|
if family == 2 && message.payload[0] != family {return Err(Error::Protocol);}
|
|
353
|
-
result.push(attrs(&message.payload[4..])?);
|
|
373
|
+
result.push((message.payload[0], attrs(&message.payload[4..])?));
|
|
354
374
|
if !dump {
|
|
355
375
|
return Ok(result);
|
|
356
376
|
}
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -65,7 +65,8 @@ export class ManagedDockerForwarding {
|
|
|
65
65
|
return this.#invoke('releaseDockerForwarding', expected);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/** Join operations and exact contributed-rule deletion, then native termination. Keep affected handoff links down
|
|
68
|
+
/** Join operations and exact contributed-rule deletion, then native termination. Keep affected handoff links down
|
|
69
|
+
* through cleanup; a handoff link that no longer exists counts as down. */
|
|
69
70
|
public close(): Promise<void> {
|
|
70
71
|
if (this.#close) return this.#close;
|
|
71
72
|
if (this.#state === 'stopped') return Promise.resolve();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { snapshot, managedIpcBytes, ManagedNftablesError, nativeError } from './classes.managednftables.js';
|
|
3
|
+
import type { IForwardHookInspection, IForwardHookInspectionOptions, TForwardHookCommands } from './managed.forwardhooks.types.js';
|
|
4
|
+
|
|
5
|
+
/** Reads every packet-filter owner on the IPv4/IPv6 forward hook of one network
|
|
6
|
+
* namespace: nf_tables forward base chains of the `ip`, `ip6` and `inet`
|
|
7
|
+
* families, Docker's DOCKER-USER/DOCKER-FORWARD chains and registered legacy
|
|
8
|
+
* xtables tables. One native process takes one generation-consistent read and
|
|
9
|
+
* exits; it claims, adopts and changes nothing. The result is a point-in-time
|
|
10
|
+
* view: another owner can appear after it. */
|
|
11
|
+
export async function inspectForwardHooks(options: IForwardHookInspectionOptions = {}): Promise<IForwardHookInspection> {
|
|
12
|
+
const captured = snapshot(options);
|
|
13
|
+
if (Object.keys(captured).some((key) => !['binaryPath', 'networkNamespaceFd'].includes(key))
|
|
14
|
+
|| captured.binaryPath !== undefined && (typeof captured.binaryPath !== 'string'
|
|
15
|
+
|| !plugins.path.isAbsolute(captured.binaryPath) || captured.binaryPath.includes('\0'))
|
|
16
|
+
|| captured.networkNamespaceFd !== undefined && (!Number.isInteger(captured.networkNamespaceFd)
|
|
17
|
+
|| captured.networkNamespaceFd < 0 || captured.networkNamespaceFd > 0x7fff_ffff)) {
|
|
18
|
+
throw new ManagedNftablesError('INVALID');
|
|
19
|
+
}
|
|
20
|
+
if (process.platform !== 'linux' || !['x64', 'arm64'].includes(process.arch)) throw new ManagedNftablesError('UNSUPPORTED_PLATFORM');
|
|
21
|
+
const root = plugins.url.fileURLToPath(new URL('../', import.meta.url));
|
|
22
|
+
const bridge = new plugins.smartrust.RustBridge<TForwardHookCommands>({
|
|
23
|
+
binaryName: 'smartnftables', binaryPath: captured.binaryPath
|
|
24
|
+
?? plugins.path.join(root, 'dist_rust', `smartnftables_linux_${process.arch === 'x64' ? 'amd64' : 'arm64'}_musl`),
|
|
25
|
+
localPaths: [], searchSystemPath: false,
|
|
26
|
+
cliArgs: ['--forward-hooks', ...(captured.networkNamespaceFd === undefined ? [] : ['--network-namespace-fd', '3'])],
|
|
27
|
+
inheritedFileDescriptors: captured.networkNamespaceFd === undefined ? undefined : [captured.networkNamespaceFd],
|
|
28
|
+
readyTimeoutMs: 3000, requestTimeoutMs: 30_000, maxPayloadSize: managedIpcBytes,
|
|
29
|
+
maxPendingRequestsByMethod: { inspectForwardHooks: 1 },
|
|
30
|
+
});
|
|
31
|
+
try {
|
|
32
|
+
if (!await bridge.spawn()) throw new ManagedNftablesError('UNAVAILABLE');
|
|
33
|
+
return snapshot(await bridge.sendCommand('inspectForwardHooks', {}));
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw error instanceof ManagedNftablesError ? error : nativeError(error);
|
|
36
|
+
} finally {
|
|
37
|
+
await bridge.terminate({ gracePeriodMs: 1000 });
|
|
38
|
+
}
|
|
39
|
+
}
|
package/ts/index.ts
CHANGED
|
@@ -25,7 +25,8 @@ export interface IAppliedManagedDockerForwardingPolicy {
|
|
|
25
25
|
receipt: { identity: IManagedDockerForwardingIdentity; backend: string; revision: number; digest: string };
|
|
26
26
|
prepared: IPreparedManagedDockerForwardingPolicy;
|
|
27
27
|
}
|
|
28
|
-
/** Persist before mutation.
|
|
28
|
+
/** Persist before mutation. A handoff link both generations bind exactly may stay up: one atomic transaction swaps
|
|
29
|
+
* the contribution. Keep every link the transition adds or drops down until reconciliation is confirmed. */
|
|
29
30
|
export interface IManagedDockerForwardingTransition {
|
|
30
31
|
previous: IAppliedManagedDockerForwardingPolicy | null;
|
|
31
32
|
target: IPreparedManagedDockerForwardingPolicy;
|
|
@@ -193,6 +193,11 @@ export interface IManagedNftHostTransitScopeV2 {
|
|
|
193
193
|
* leased source address and a source port of the allocation's range for the endpoint's protocol; its
|
|
194
194
|
* ESTABLISHED replies return. Absent and empty are the same canonical policy, digest and compiled graph. */
|
|
195
195
|
localPlatformEndpoints?: string[];
|
|
196
|
+
/** Optional: this table is the host's only forwarding owner. Every forwarded packet the scope does not
|
|
197
|
+
* admit (its leased, published and symmetric handoff flows) drops, IPv4 and IPv6, on every pair of
|
|
198
|
+
* interfaces, including forwarding that another table would accept. With no `handoffs` the host forwards
|
|
199
|
+
* nothing. It sets no sysctl. Absent and `false` are the same canonical policy, digest and compiled graph. */
|
|
200
|
+
exclusiveForwarding?: boolean;
|
|
196
201
|
}
|
|
197
202
|
|
|
198
203
|
/** One loopback TCP service that only one local user may dial. Every packet the host sends to the
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { IManagedNftOptions } from './managed.types.js';
|
|
2
|
+
|
|
3
|
+
export interface IForwardHookInspectionOptions extends Pick<IManagedNftOptions, 'binaryPath' | 'networkNamespaceFd'> {}
|
|
4
|
+
/** One nf_tables base chain on the forward hook of the `ip`, `ip6` or `inet` family. */
|
|
5
|
+
export interface IForwardHookChain {
|
|
6
|
+
family: 'ip' | 'ip6' | 'inet';
|
|
7
|
+
table: string;
|
|
8
|
+
/** Kernel table handle, as a managed receipt's `tableHandle`. */
|
|
9
|
+
tableHandle: string;
|
|
10
|
+
chain: string;
|
|
11
|
+
type: string;
|
|
12
|
+
priority: number;
|
|
13
|
+
policy: 'accept' | 'drop';
|
|
14
|
+
}
|
|
15
|
+
/** One consistent read of every packet-filter owner on the routed forward path of a network namespace. */
|
|
16
|
+
export interface IForwardHookInspection {
|
|
17
|
+
/** Every forward base chain, sorted by family, table and chain; managed tables included. */
|
|
18
|
+
chains: IForwardHookChain[];
|
|
19
|
+
/** Docker's iptables-nft dispatch chains in table `ip filter`. */
|
|
20
|
+
docker: { dockerUser: boolean; dockerForward: boolean };
|
|
21
|
+
/** Registered legacy xtables tables, whose hooks are not nf_tables chains. */
|
|
22
|
+
legacyTables: { ipv4: string[]; ipv6: string[] };
|
|
23
|
+
}
|
|
24
|
+
export type TForwardHookCommands = {
|
|
25
|
+
inspectForwardHooks: { params: Record<string, never>; result: IForwardHookInspection };
|
|
26
|
+
};
|