@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
package/rust/src/egress.rs
CHANGED
|
@@ -210,6 +210,67 @@ pub struct HostScope {
|
|
|
210
210
|
/// bound links. Absent and empty are the same canonical policy.
|
|
211
211
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
212
212
|
pub local_platform_endpoints: Vec<String>,
|
|
213
|
+
/// This table is the host's only forwarding owner: every forwarded packet,
|
|
214
|
+
/// IPv4 or IPv6, that the scope does not admit drops, instead of passing to
|
|
215
|
+
/// other owners. Absent and false are the same canonical policy.
|
|
216
|
+
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
|
217
|
+
pub exclusive_forwarding: bool,
|
|
218
|
+
}
|
|
219
|
+
/// One leased source port range of one allocation, on the handoff that holds it.
|
|
220
|
+
pub struct LeasedForward<'a> {
|
|
221
|
+
pub handoff: &'a HostHandoff,
|
|
222
|
+
pub allocation: &'a Allocation,
|
|
223
|
+
pub range: &'a PortRange,
|
|
224
|
+
}
|
|
225
|
+
/// One publication, on the handoff link that holds its leased target address.
|
|
226
|
+
pub struct PublishedForward<'a> {
|
|
227
|
+
pub port: &'a PublishedPort,
|
|
228
|
+
pub link: &'a LocalLink,
|
|
229
|
+
}
|
|
230
|
+
/// The forwarded flows a host-transit scope admits, in canonical scope order:
|
|
231
|
+
/// leased flows from a handoff to the uplink, and per publication its inbound
|
|
232
|
+
/// flows from the uplink to the handoff and, when symmetric, the flows it opens
|
|
233
|
+
/// from its target ports to the uplink; each with its replies. The scope's nft
|
|
234
|
+
/// forward chain and the Docker contribution both derive from this one list.
|
|
235
|
+
pub struct Forwarding<'a> {
|
|
236
|
+
pub leased: Vec<LeasedForward<'a>>,
|
|
237
|
+
pub published: Vec<PublishedForward<'a>>,
|
|
238
|
+
}
|
|
239
|
+
impl HostScope {
|
|
240
|
+
pub fn forwarding(&self) -> Result<Forwarding<'_>> {
|
|
241
|
+
let mut leased = Vec::new();
|
|
242
|
+
for handoff in &self.handoffs {
|
|
243
|
+
for allocation in &handoff.allocations {
|
|
244
|
+
for range in &allocation.source_port_ranges {
|
|
245
|
+
leased.push(LeasedForward {
|
|
246
|
+
handoff,
|
|
247
|
+
allocation,
|
|
248
|
+
range,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Validation admits only leased transit source addresses as targets, and
|
|
254
|
+
// one address never spans two active handoff links, so each publication
|
|
255
|
+
// has exactly one handoff.
|
|
256
|
+
let mut published = Vec::new();
|
|
257
|
+
for port in &self.published_ports {
|
|
258
|
+
let handoff =
|
|
259
|
+
self.handoffs
|
|
260
|
+
.iter()
|
|
261
|
+
.find(|handoff| {
|
|
262
|
+
handoff.allocations.iter().any(|allocation| {
|
|
263
|
+
allocation.transit_source_address == port.target_address
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
.ok_or(Error::Invalid)?;
|
|
267
|
+
published.push(PublishedForward {
|
|
268
|
+
port,
|
|
269
|
+
link: &handoff.link,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
Ok(Forwarding { leased, published })
|
|
273
|
+
}
|
|
213
274
|
}
|
|
214
275
|
/// One loopback TCP service that only one local user may dial: packets the host
|
|
215
276
|
/// sends to the exact address and port from a socket of any other user reject,
|
|
@@ -1168,6 +1229,9 @@ fn normalize_host(value: &mut HostScope) -> Result<()> {
|
|
|
1168
1229
|
)
|
|
1169
1230
|
}
|
|
1170
1231
|
|
|
1232
|
+
#[cfg(test)]
|
|
1233
|
+
#[path = "egress_exclusiveforwarding_tests.rs"]
|
|
1234
|
+
mod exclusiveforwarding_tests;
|
|
1171
1235
|
#[cfg(test)]
|
|
1172
1236
|
#[path = "egress_hostgrant_tests.rs"]
|
|
1173
1237
|
pub(crate) mod hostgrant_tests;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
//! Exclusive forwarding in host transit: the host forwards exactly the scope's
|
|
2
|
+
//! leased, published and symmetric handoff flows, and every other forwarded
|
|
3
|
+
//! packet drops instead of passing to other owners.
|
|
4
|
+
use super::hostgrant_tests::{
|
|
5
|
+
bytes, chains, decide, normalized, opening, prepared, Flow, ACK, ESTABLISHED,
|
|
6
|
+
};
|
|
7
|
+
use super::tests::{host, pool_guard, published_host, published_port, router};
|
|
8
|
+
use super::Prepared;
|
|
9
|
+
use crate::policy::verdict;
|
|
10
|
+
use serde_json::{json, Value};
|
|
11
|
+
|
|
12
|
+
const TRANSIT: [u8; 4] = [10, 240, 0, 2];
|
|
13
|
+
const UPLINK: [u8; 4] = [192, 0, 2, 2];
|
|
14
|
+
const PUBLIC: [u8; 4] = [198, 51, 100, 200];
|
|
15
|
+
const CLIENT: [u8; 4] = [198, 51, 100, 9];
|
|
16
|
+
const LAN_PEER: [u8; 4] = [172, 16, 0, 9];
|
|
17
|
+
const HANDOFF: Option<(u32, &str)> = Some((4, "handoff"));
|
|
18
|
+
const UPLINK_LINK: Option<(u32, &str)> = Some((2, "ens18"));
|
|
19
|
+
/// Another host link that no scope binds: a management LAN, a second uplink.
|
|
20
|
+
const LAN: Option<(u32, &str)> = Some((7, "lan0"));
|
|
21
|
+
|
|
22
|
+
fn exclusive(mut value: Value) -> Value {
|
|
23
|
+
value["scope"]["exclusiveForwarding"] = json!(true);
|
|
24
|
+
value
|
|
25
|
+
}
|
|
26
|
+
/// The fixture with one symmetric UDP publication of the transit address.
|
|
27
|
+
fn published() -> Value {
|
|
28
|
+
let mut port = published_port("udp", 5060, 5060);
|
|
29
|
+
port["symmetric"] = json!(true);
|
|
30
|
+
published_host(json!([published_port("tcp", 443, 8443), port]))
|
|
31
|
+
}
|
|
32
|
+
/// One forwarded packet; the originally tracked tuple equals the live one.
|
|
33
|
+
fn forwarded(
|
|
34
|
+
input: Option<(u32, &'static str)>,
|
|
35
|
+
output: Option<(u32, &'static str)>,
|
|
36
|
+
(source, source_port): ([u8; 4], u16),
|
|
37
|
+
(destination, destination_port): ([u8; 4], u16),
|
|
38
|
+
) -> Flow {
|
|
39
|
+
Flow {
|
|
40
|
+
input,
|
|
41
|
+
output,
|
|
42
|
+
source,
|
|
43
|
+
destination,
|
|
44
|
+
source_port,
|
|
45
|
+
destination_port,
|
|
46
|
+
original: (source, destination, source_port, destination_port),
|
|
47
|
+
..opening(input, output)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/// The answer to `flow` on the reverse links.
|
|
51
|
+
fn answer(flow: Flow) -> Flow {
|
|
52
|
+
Flow {
|
|
53
|
+
input: flow.output,
|
|
54
|
+
output: flow.input,
|
|
55
|
+
source: flow.destination,
|
|
56
|
+
destination: flow.source,
|
|
57
|
+
source_port: flow.destination_port,
|
|
58
|
+
destination_port: flow.source_port,
|
|
59
|
+
tcp_flags: ACK,
|
|
60
|
+
state: ESTABLISHED,
|
|
61
|
+
reply: true,
|
|
62
|
+
..flow
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/// Forwarded traffic no scope admits: the host as a gateway between its other
|
|
66
|
+
/// links and the uplink, a hairpin back out of the uplink, and traffic between
|
|
67
|
+
/// two links the scope does not bind, in both directions and both protocols.
|
|
68
|
+
fn unadmitted() -> Vec<Flow> {
|
|
69
|
+
let mut result = Vec::new();
|
|
70
|
+
for flow in [
|
|
71
|
+
forwarded(LAN, UPLINK_LINK, (LAN_PEER, 40000), (PUBLIC, 443)),
|
|
72
|
+
forwarded(UPLINK_LINK, LAN, (CLIENT, 40000), (LAN_PEER, 22)),
|
|
73
|
+
forwarded(UPLINK_LINK, UPLINK_LINK, (CLIENT, 40000), (PUBLIC, 443)),
|
|
74
|
+
forwarded(
|
|
75
|
+
LAN,
|
|
76
|
+
Some((8, "lan1")),
|
|
77
|
+
(LAN_PEER, 40000),
|
|
78
|
+
([172, 17, 0, 9], 80),
|
|
79
|
+
),
|
|
80
|
+
] {
|
|
81
|
+
result.extend([
|
|
82
|
+
flow,
|
|
83
|
+
answer(flow),
|
|
84
|
+
Flow {
|
|
85
|
+
protocol: 17,
|
|
86
|
+
..flow
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
result
|
|
91
|
+
}
|
|
92
|
+
/// Every flow the scope forwards: leased TCP and UDP egress with replies,
|
|
93
|
+
/// published inbound TCP and UDP with replies, and symmetric UDP outbound.
|
|
94
|
+
fn admitted() -> Vec<Flow> {
|
|
95
|
+
let leased = forwarded(HANDOFF, UPLINK_LINK, (TRANSIT, 10500), (PUBLIC, 443));
|
|
96
|
+
let inbound = Flow {
|
|
97
|
+
original: (CLIENT, UPLINK, 40000, 443),
|
|
98
|
+
..forwarded(UPLINK_LINK, HANDOFF, (CLIENT, 40000), (TRANSIT, 8443))
|
|
99
|
+
};
|
|
100
|
+
let signalling = Flow {
|
|
101
|
+
protocol: 17,
|
|
102
|
+
original: (CLIENT, UPLINK, 40001, 5060),
|
|
103
|
+
..forwarded(UPLINK_LINK, HANDOFF, (CLIENT, 40001), (TRANSIT, 5060))
|
|
104
|
+
};
|
|
105
|
+
let symmetric = Flow {
|
|
106
|
+
protocol: 17,
|
|
107
|
+
..forwarded(HANDOFF, UPLINK_LINK, (TRANSIT, 5060), (CLIENT, 5060))
|
|
108
|
+
};
|
|
109
|
+
let mut result = Vec::new();
|
|
110
|
+
for flow in [
|
|
111
|
+
leased,
|
|
112
|
+
Flow {
|
|
113
|
+
protocol: 17,
|
|
114
|
+
..leased
|
|
115
|
+
},
|
|
116
|
+
inbound,
|
|
117
|
+
signalling,
|
|
118
|
+
symmetric,
|
|
119
|
+
] {
|
|
120
|
+
result.extend([
|
|
121
|
+
flow,
|
|
122
|
+
Flow {
|
|
123
|
+
state: ESTABLISHED,
|
|
124
|
+
tcp_flags: ACK,
|
|
125
|
+
..flow
|
|
126
|
+
},
|
|
127
|
+
answer(flow),
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
result
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[test]
|
|
134
|
+
fn exclusive_forwarding_absent_and_false_keep_the_host_digest_and_compiled_bytes() {
|
|
135
|
+
let baseline = prepared(host());
|
|
136
|
+
assert!(!serde_json::to_string(&baseline.policy)
|
|
137
|
+
.unwrap()
|
|
138
|
+
.contains("exclusiveForwarding"));
|
|
139
|
+
let mut value = host();
|
|
140
|
+
value["scope"]["exclusiveForwarding"] = json!(false);
|
|
141
|
+
let explicit = prepared(value);
|
|
142
|
+
assert_eq!(explicit, baseline);
|
|
143
|
+
assert_eq!(bytes(&explicit), bytes(&baseline));
|
|
144
|
+
let with = prepared(exclusive(host()));
|
|
145
|
+
assert_ne!(with.digest, baseline.digest);
|
|
146
|
+
assert!(serde_json::to_string(&with.policy)
|
|
147
|
+
.unwrap()
|
|
148
|
+
.contains("\"exclusiveForwarding\":true"));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[test]
|
|
152
|
+
fn exclusive_forwarding_appends_one_unconditional_forward_drop_and_nothing_else() {
|
|
153
|
+
for fixture in [host(), published()] {
|
|
154
|
+
let baseline = chains(&prepared(fixture.clone()));
|
|
155
|
+
let with = chains(&prepared(exclusive(fixture)));
|
|
156
|
+
assert_eq!(
|
|
157
|
+
with.keys().collect::<Vec<_>>(),
|
|
158
|
+
baseline.keys().collect::<Vec<_>>()
|
|
159
|
+
);
|
|
160
|
+
for (name, rules) in &baseline {
|
|
161
|
+
if name != "forward" {
|
|
162
|
+
assert_eq!(&with[name], rules, "{name}");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// The previous forward rules keep their order and the drop follows them.
|
|
166
|
+
// It has no family or interface match: forwarded IPv6 and every link
|
|
167
|
+
// pair drop like forwarded IPv4.
|
|
168
|
+
let forward = &with["forward"];
|
|
169
|
+
assert_eq!(&forward[..forward.len() - 1], &baseline["forward"][..]);
|
|
170
|
+
assert_eq!(forward.last().unwrap(), &vec![verdict(0, None)]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#[test]
|
|
175
|
+
fn exclusive_forwarding_denies_every_unadmitted_forward_and_keeps_every_admitted_one() {
|
|
176
|
+
for fixture in [host(), published()] {
|
|
177
|
+
let baseline = prepared(fixture.clone());
|
|
178
|
+
let with = prepared(exclusive(fixture));
|
|
179
|
+
// Without the member another owner's forwarding passes this table.
|
|
180
|
+
for (index, flow) in unadmitted().iter().enumerate() {
|
|
181
|
+
let decision = decide(&baseline, "forward", flow);
|
|
182
|
+
assert_eq!(
|
|
183
|
+
(decision.verdict, decision.chain.as_str()),
|
|
184
|
+
(1, "policy"),
|
|
185
|
+
"baseline {index}"
|
|
186
|
+
);
|
|
187
|
+
let decision = decide(&with, "forward", flow);
|
|
188
|
+
assert_eq!(
|
|
189
|
+
(decision.verdict, decision.chain.as_str()),
|
|
190
|
+
(0, "forward"),
|
|
191
|
+
"exclusive {index}"
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
// Every flow the scope forwards keeps its exact decision, and so does
|
|
195
|
+
// every handoff flow it already denies.
|
|
196
|
+
let handoff = [
|
|
197
|
+
forwarded(HANDOFF, LAN, (TRANSIT, 10500), (LAN_PEER, 443)),
|
|
198
|
+
forwarded(LAN, HANDOFF, (LAN_PEER, 40000), (TRANSIT, 8443)),
|
|
199
|
+
forwarded(HANDOFF, UPLINK_LINK, (TRANSIT, 9999), (PUBLIC, 443)),
|
|
200
|
+
forwarded(
|
|
201
|
+
HANDOFF,
|
|
202
|
+
UPLINK_LINK,
|
|
203
|
+
(TRANSIT, 10500),
|
|
204
|
+
([10, 250, 0, 2], 443),
|
|
205
|
+
),
|
|
206
|
+
];
|
|
207
|
+
for (index, flow) in admitted().iter().chain(&handoff).enumerate() {
|
|
208
|
+
assert_eq!(
|
|
209
|
+
decide(&with, "forward", flow),
|
|
210
|
+
decide(&baseline, "forward", flow),
|
|
211
|
+
"flow {index}"
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// The positive controls are real admissions of the published fixture.
|
|
216
|
+
let published = prepared(exclusive(published()));
|
|
217
|
+
for (index, flow) in admitted().iter().enumerate() {
|
|
218
|
+
assert_eq!(
|
|
219
|
+
decide(&published, "forward", flow).verdict,
|
|
220
|
+
1,
|
|
221
|
+
"admitted {index}"
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#[test]
|
|
227
|
+
fn exclusive_forwarding_without_handoffs_forwards_nothing() {
|
|
228
|
+
let mut value = exclusive(host());
|
|
229
|
+
value["scope"]["handoffs"] = json!([]);
|
|
230
|
+
let empty = prepared(value);
|
|
231
|
+
assert_eq!(chains(&empty)["forward"], vec![vec![verdict(0, None)]]);
|
|
232
|
+
let leased = forwarded(HANDOFF, UPLINK_LINK, (TRANSIT, 10500), (PUBLIC, 443));
|
|
233
|
+
for flow in unadmitted().into_iter().chain([leased, answer(leased)]) {
|
|
234
|
+
assert_eq!(decide(&empty, "forward", &flow).verdict, 0);
|
|
235
|
+
}
|
|
236
|
+
// Without the member an empty scope forwards everything, as before.
|
|
237
|
+
let mut value = host();
|
|
238
|
+
value["scope"]["handoffs"] = json!([]);
|
|
239
|
+
let open = prepared(value);
|
|
240
|
+
assert!(chains(&open)["forward"].is_empty());
|
|
241
|
+
assert_eq!(decide(&open, "forward", &leased).verdict, 1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#[test]
|
|
245
|
+
fn exclusive_forwarding_is_a_host_transit_boolean_only() {
|
|
246
|
+
for fixture in [router(), pool_guard()] {
|
|
247
|
+
assert!(normalized(exclusive(fixture)).is_err());
|
|
248
|
+
}
|
|
249
|
+
for invalid in [json!(1), json!("true"), json!(null)] {
|
|
250
|
+
let mut value = host();
|
|
251
|
+
value["scope"]["exclusiveForwarding"] = invalid;
|
|
252
|
+
assert!(normalized(value).is_err());
|
|
253
|
+
}
|
|
254
|
+
let restored: Prepared = prepared(exclusive(host()));
|
|
255
|
+
assert_eq!(restored.clone().policy.prepare().unwrap(), restored);
|
|
256
|
+
}
|
|
@@ -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
|
};
|