@push.rocks/smartnftables 4.1.0 → 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 +9 -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 +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.forwardhooks.types.d.ts +35 -0
- package/dist_ts/managed.forwardhooks.types.js +2 -0
- package/package.json +1 -1
- package/readme.md +69 -16
- package/rust/src/docker.frontend.rs +2 -2
- package/rust/src/docker.graph.rs +36 -11
- package/rust/src/docker.policy.rs +90 -31
- package/rust/src/docker.rs +37 -13
- package/rust/src/docker_tests.rs +161 -2
- package/rust/src/egress.host.rs +44 -45
- package/rust/src/egress.rs +56 -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/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.forwardhooks.types.ts +26 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
//! Read-only view of every packet-filter owner on the IPv4/IPv6 forward hook of
|
|
2
|
+
//! this network namespace. It claims, adopts and changes nothing.
|
|
3
|
+
use crate::wire::{self, Attr, Socket};
|
|
4
|
+
use crate::{decode, encode, Error, Request, Result};
|
|
5
|
+
use serde::Serialize;
|
|
6
|
+
|
|
7
|
+
#[cfg(test)]
|
|
8
|
+
#[path = "forwardhooks_tests.rs"]
|
|
9
|
+
mod tests;
|
|
10
|
+
|
|
11
|
+
/// One nf_tables base chain on the forward hook.
|
|
12
|
+
#[derive(Clone, Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
|
13
|
+
#[serde(rename_all = "camelCase")]
|
|
14
|
+
pub struct Chain {
|
|
15
|
+
pub family: &'static str,
|
|
16
|
+
pub table: String,
|
|
17
|
+
pub table_handle: String,
|
|
18
|
+
pub chain: String,
|
|
19
|
+
#[serde(rename = "type")]
|
|
20
|
+
pub chain_type: String,
|
|
21
|
+
pub priority: i32,
|
|
22
|
+
pub policy: &'static str,
|
|
23
|
+
}
|
|
24
|
+
/// Docker's iptables-nft dispatch chains in table `ip filter`.
|
|
25
|
+
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
|
|
26
|
+
#[serde(rename_all = "camelCase")]
|
|
27
|
+
pub struct Docker {
|
|
28
|
+
pub docker_user: bool,
|
|
29
|
+
pub docker_forward: bool,
|
|
30
|
+
}
|
|
31
|
+
/// Registered legacy xtables tables. Their hooks, including FORWARD, are not
|
|
32
|
+
/// nf_tables objects and never appear in `chains`.
|
|
33
|
+
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
|
|
34
|
+
#[serde(rename_all = "camelCase")]
|
|
35
|
+
pub struct Legacy {
|
|
36
|
+
pub ipv4: Vec<String>,
|
|
37
|
+
pub ipv6: Vec<String>,
|
|
38
|
+
}
|
|
39
|
+
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
|
|
40
|
+
#[serde(rename_all = "camelCase")]
|
|
41
|
+
pub struct Inspection {
|
|
42
|
+
pub chains: Vec<Chain>,
|
|
43
|
+
pub docker: Docker,
|
|
44
|
+
pub legacy_tables: Legacy,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// The families whose forward hook sees routed IPv4 or IPv6 packets.
|
|
48
|
+
fn family(value: u8) -> Option<&'static str> {
|
|
49
|
+
match value {
|
|
50
|
+
1 => Some("inet"),
|
|
51
|
+
2 => Some("ip"),
|
|
52
|
+
10 => Some("ip6"),
|
|
53
|
+
_ => None,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const NF_INET_FORWARD: u32 = 2;
|
|
57
|
+
|
|
58
|
+
/// Classify one consistent table and chain dump.
|
|
59
|
+
pub(crate) fn classify(
|
|
60
|
+
tables: &[(u8, Vec<Attr>)],
|
|
61
|
+
chains: &[(u8, Vec<Attr>)],
|
|
62
|
+
) -> Result<(Vec<Chain>, Docker)> {
|
|
63
|
+
let mut result = Vec::new();
|
|
64
|
+
let mut docker = Docker {
|
|
65
|
+
docker_user: false,
|
|
66
|
+
docker_forward: false,
|
|
67
|
+
};
|
|
68
|
+
for (number, attributes) in chains {
|
|
69
|
+
let table = wire::text(attributes, 1)?;
|
|
70
|
+
let name = wire::text(attributes, 3)?;
|
|
71
|
+
if *number == 2 && table == "filter" {
|
|
72
|
+
match name.as_str() {
|
|
73
|
+
"DOCKER-USER" => docker.docker_user = true,
|
|
74
|
+
"DOCKER-FORWARD" => docker.docker_forward = true,
|
|
75
|
+
_ => {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
let Some(family) = family(*number) else {
|
|
79
|
+
continue;
|
|
80
|
+
};
|
|
81
|
+
// Only base chains carry a hook.
|
|
82
|
+
if !attributes.iter().any(|attribute| attribute.id() == 4) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
let hook = wire::attrs(&wire::one(attributes, 4)?.value)?;
|
|
86
|
+
if wire::number(&hook, 1)? != NF_INET_FORWARD {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
let priority = wire::number(&hook, 2)? as i32;
|
|
90
|
+
let policy = match wire::number(attributes, 5)? {
|
|
91
|
+
0 => "drop",
|
|
92
|
+
1 => "accept",
|
|
93
|
+
_ => return Err(Error::Protocol),
|
|
94
|
+
};
|
|
95
|
+
let handle = tables
|
|
96
|
+
.iter()
|
|
97
|
+
.filter(|(owner, table_attributes)| {
|
|
98
|
+
owner == number
|
|
99
|
+
&& wire::text(table_attributes, 1).ok().as_deref() == Some(table.as_str())
|
|
100
|
+
})
|
|
101
|
+
.map(|(_, table_attributes)| wire::handle(table_attributes, 4))
|
|
102
|
+
.collect::<Result<Vec<_>>>()?;
|
|
103
|
+
let [handle] = handle.as_slice() else {
|
|
104
|
+
return Err(Error::Conflict);
|
|
105
|
+
};
|
|
106
|
+
result.push(Chain {
|
|
107
|
+
family,
|
|
108
|
+
table,
|
|
109
|
+
table_handle: handle.to_string(),
|
|
110
|
+
chain: name,
|
|
111
|
+
chain_type: wire::text(attributes, 7)?,
|
|
112
|
+
priority,
|
|
113
|
+
policy,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
result.sort();
|
|
117
|
+
Ok((result, docker))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// One registered table name per line; an absent file means none is registered.
|
|
121
|
+
fn legacy(path: &str) -> Result<Vec<String>> {
|
|
122
|
+
use std::io::Read;
|
|
123
|
+
let file = match std::fs::File::open(path) {
|
|
124
|
+
Ok(file) => file,
|
|
125
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
|
|
126
|
+
Err(_) => return Err(Error::Unavailable),
|
|
127
|
+
};
|
|
128
|
+
let mut text = String::new();
|
|
129
|
+
file.take(4097)
|
|
130
|
+
.read_to_string(&mut text)
|
|
131
|
+
.map_err(|_| Error::Protocol)?;
|
|
132
|
+
if text.len() > 4096 {
|
|
133
|
+
return Err(Error::Protocol);
|
|
134
|
+
}
|
|
135
|
+
let mut names = Vec::new();
|
|
136
|
+
for line in text.lines() {
|
|
137
|
+
if !crate::policy::identifier(line) {
|
|
138
|
+
return Err(Error::Protocol);
|
|
139
|
+
}
|
|
140
|
+
names.push(line.to_owned());
|
|
141
|
+
}
|
|
142
|
+
names.sort();
|
|
143
|
+
Ok(names)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
pub fn inspect() -> Result<Inspection> {
|
|
147
|
+
let mut socket = Socket::open()?;
|
|
148
|
+
let generation = socket.generation()?;
|
|
149
|
+
let tables = socket.dump_all_families(1)?;
|
|
150
|
+
let chains = socket.dump_all_families(4)?;
|
|
151
|
+
// Both dumps describe one ruleset generation, or the view is inconclusive.
|
|
152
|
+
if socket.generation()? != generation {
|
|
153
|
+
return Err(Error::Conflict);
|
|
154
|
+
}
|
|
155
|
+
let (chains, docker) = classify(&tables, &chains)?;
|
|
156
|
+
Ok(Inspection {
|
|
157
|
+
chains,
|
|
158
|
+
docker,
|
|
159
|
+
legacy_tables: Legacy {
|
|
160
|
+
ipv4: legacy("/proc/self/net/ip_tables_names")?,
|
|
161
|
+
ipv6: legacy("/proc/self/net/ip6_tables_names")?,
|
|
162
|
+
},
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
pub fn dispatch(request: Request) -> Result<serde_json::Value> {
|
|
167
|
+
match request.method.as_str() {
|
|
168
|
+
"inspectForwardHooks" => {
|
|
169
|
+
if decode::<serde_json::Value>(request.params)? != serde_json::json!({}) {
|
|
170
|
+
return Err(Error::Invalid);
|
|
171
|
+
}
|
|
172
|
+
encode(inspect()?)
|
|
173
|
+
}
|
|
174
|
+
_ => Err(Error::Invalid),
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
fn table(family: u8, name: &str, handle: u64) -> (u8, Vec<Attr>) {
|
|
4
|
+
(family, vec![Attr::string(1, name), Attr::u64(4, handle)])
|
|
5
|
+
}
|
|
6
|
+
fn base(
|
|
7
|
+
family: u8,
|
|
8
|
+
table: &str,
|
|
9
|
+
name: &str,
|
|
10
|
+
hook: u32,
|
|
11
|
+
priority: i32,
|
|
12
|
+
policy: u32,
|
|
13
|
+
) -> (u8, Vec<Attr>) {
|
|
14
|
+
let hook = Attr::nested(
|
|
15
|
+
4,
|
|
16
|
+
vec![
|
|
17
|
+
Attr::bytes(1, hook.to_be_bytes().to_vec()),
|
|
18
|
+
Attr::bytes(2, priority.to_be_bytes().to_vec()),
|
|
19
|
+
],
|
|
20
|
+
);
|
|
21
|
+
(
|
|
22
|
+
family,
|
|
23
|
+
vec![
|
|
24
|
+
Attr::string(1, table),
|
|
25
|
+
Attr::string(3, name),
|
|
26
|
+
hook,
|
|
27
|
+
Attr::bytes(5, policy.to_be_bytes().to_vec()),
|
|
28
|
+
Attr::string(7, "filter"),
|
|
29
|
+
],
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
fn regular(family: u8, table: &str, name: &str) -> (u8, Vec<Attr>) {
|
|
33
|
+
(family, vec![Attr::string(1, table), Attr::string(3, name)])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[test]
|
|
37
|
+
fn forward_hooks_report_every_routed_forward_base_chain_with_its_table() {
|
|
38
|
+
let tables = [
|
|
39
|
+
table(1, "snft_host", 7),
|
|
40
|
+
table(2, "filter", 3),
|
|
41
|
+
table(10, "filter", 4),
|
|
42
|
+
table(7, "filter", 5),
|
|
43
|
+
table(5, "edge", 6),
|
|
44
|
+
table(1, "other", 8),
|
|
45
|
+
];
|
|
46
|
+
let chains = [
|
|
47
|
+
base(1, "snft_host", "forward", 2, 0, 1),
|
|
48
|
+
base(1, "snft_host", "input", 1, 0, 1),
|
|
49
|
+
base(2, "filter", "FORWARD", 2, 0, 0),
|
|
50
|
+
regular(2, "filter", "DOCKER-USER"),
|
|
51
|
+
regular(2, "filter", "DOCKER-FORWARD"),
|
|
52
|
+
base(10, "filter", "FORWARD", 2, 0, 1),
|
|
53
|
+
// Bridge and netdev hooks do not see routed packets.
|
|
54
|
+
base(7, "filter", "FORWARD", 2, -200, 0),
|
|
55
|
+
base(5, "edge", "ingress", 0, -500, 1),
|
|
56
|
+
base(1, "other", "early", 2, -150, 0),
|
|
57
|
+
];
|
|
58
|
+
let (chains, docker) = classify(&tables, &chains).unwrap();
|
|
59
|
+
let summary: Vec<_> = chains
|
|
60
|
+
.iter()
|
|
61
|
+
.map(|c| {
|
|
62
|
+
(
|
|
63
|
+
c.family,
|
|
64
|
+
c.table.as_str(),
|
|
65
|
+
c.table_handle.as_str(),
|
|
66
|
+
c.chain.as_str(),
|
|
67
|
+
c.priority,
|
|
68
|
+
c.policy,
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
.collect();
|
|
72
|
+
assert_eq!(
|
|
73
|
+
summary,
|
|
74
|
+
vec![
|
|
75
|
+
("inet", "other", "8", "early", -150, "drop"),
|
|
76
|
+
("inet", "snft_host", "7", "forward", 0, "accept"),
|
|
77
|
+
("ip", "filter", "3", "FORWARD", 0, "drop"),
|
|
78
|
+
("ip6", "filter", "4", "FORWARD", 0, "accept"),
|
|
79
|
+
]
|
|
80
|
+
);
|
|
81
|
+
assert_eq!(
|
|
82
|
+
docker,
|
|
83
|
+
Docker {
|
|
84
|
+
docker_user: true,
|
|
85
|
+
docker_forward: true
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
let json = serde_json::to_value(&chains[0]).unwrap();
|
|
89
|
+
assert_eq!(
|
|
90
|
+
json,
|
|
91
|
+
serde_json::json!({"family":"inet","table":"other","tableHandle":"8","chain":"early",
|
|
92
|
+
"type":"filter","priority":-150,"policy":"drop"})
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[test]
|
|
97
|
+
fn forward_hooks_report_absent_docker_chains_and_reject_inconsistent_dumps() {
|
|
98
|
+
let (chains, docker) = classify(
|
|
99
|
+
&[],
|
|
100
|
+
&[
|
|
101
|
+
regular(10, "filter", "DOCKER-USER"),
|
|
102
|
+
regular(2, "nat", "DOCKER-FORWARD"),
|
|
103
|
+
],
|
|
104
|
+
)
|
|
105
|
+
.unwrap();
|
|
106
|
+
assert!(chains.is_empty());
|
|
107
|
+
assert_eq!(
|
|
108
|
+
docker,
|
|
109
|
+
Docker {
|
|
110
|
+
docker_user: false,
|
|
111
|
+
docker_forward: false
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
// A base chain whose table the table dump does not hold exactly once.
|
|
115
|
+
let chain = [base(2, "filter", "FORWARD", 2, 0, 0)];
|
|
116
|
+
assert_eq!(classify(&[], &chain), Err(Error::Conflict));
|
|
117
|
+
assert_eq!(
|
|
118
|
+
classify(&[table(2, "filter", 1), table(2, "filter", 2)], &chain),
|
|
119
|
+
Err(Error::Conflict)
|
|
120
|
+
);
|
|
121
|
+
assert_eq!(
|
|
122
|
+
classify(
|
|
123
|
+
&[table(2, "filter", 1)],
|
|
124
|
+
&[base(2, "filter", "FORWARD", 2, 0, 3)]
|
|
125
|
+
),
|
|
126
|
+
Err(Error::Protocol)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[test]
|
|
131
|
+
fn forward_hooks_read_absent_legacy_tables_as_none() {
|
|
132
|
+
assert_eq!(
|
|
133
|
+
legacy("/proc/self/net/smartnftables-absent-fixture").unwrap(),
|
|
134
|
+
Vec::<String>::new()
|
|
135
|
+
);
|
|
136
|
+
}
|
package/rust/src/main.rs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
mod egress;
|
|
2
2
|
mod docker;
|
|
3
|
+
mod forwardhooks;
|
|
3
4
|
mod managed;
|
|
4
5
|
mod namespace;
|
|
5
6
|
mod owner;
|
|
@@ -172,14 +173,12 @@ fn failure(id: &str, error: Error) -> serde_json::Value {
|
|
|
172
173
|
|
|
173
174
|
fn main() {
|
|
174
175
|
let arguments = std::env::args().skip(1).collect::<Vec<_>>();
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
[
|
|
178
|
-
[
|
|
179
|
-
if (management == "--management" || management == "--docker-forwarding") && option == "--network-namespace-fd" =>
|
|
180
|
-
{
|
|
176
|
+
const MODES: [&str; 3] = ["--management", "--docker-forwarding", "--forward-hooks"];
|
|
177
|
+
let (mode, descriptor) = match arguments.as_slice() {
|
|
178
|
+
[mode] if MODES.contains(&mode.as_str()) => (mode.clone(), None),
|
|
179
|
+
[mode, option, fd] if MODES.contains(&mode.as_str()) && option == "--network-namespace-fd" => {
|
|
181
180
|
match fd.parse::<i32>() {
|
|
182
|
-
Ok(value) => (
|
|
181
|
+
Ok(value) => (mode.clone(), Some(value)),
|
|
183
182
|
Err(_) => std::process::exit(2),
|
|
184
183
|
}
|
|
185
184
|
}
|
|
@@ -217,7 +216,12 @@ fn main() {
|
|
|
217
216
|
std::process::exit(2);
|
|
218
217
|
}
|
|
219
218
|
let id = request.id.clone();
|
|
220
|
-
let
|
|
219
|
+
let result = match mode.as_str() {
|
|
220
|
+
"--docker-forwarding" => docker::dispatch(&mut docker, request),
|
|
221
|
+
"--forward-hooks" => forwardhooks::dispatch(request),
|
|
222
|
+
_ => dispatch(&mut owner, request),
|
|
223
|
+
};
|
|
224
|
+
let response = match result {
|
|
221
225
|
Ok(result) => serde_json::json!({"id":id,"success":true,"result":result}),
|
|
222
226
|
Err(error) => failure(&id, error),
|
|
223
227
|
};
|
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;
|
|
@@ -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
|
+
};
|