@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/docker.graph.rs
CHANGED
|
@@ -9,17 +9,36 @@ fn payload(base: u32, offset: u32, length: u32) -> Attr {
|
|
|
9
9
|
expr("payload",vec![Attr::u32(1,1),Attr::u32(2,base),Attr::u32(3,offset),Attr::u32(4,length)])
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
fn span(value: &str) -> Result<(u16, u16)> {
|
|
13
|
+
let (first,last)=value.split_once(':').unwrap_or((value,value));
|
|
14
|
+
Ok((first.parse::<u16>().map_err(|_|Error::Invalid)?,last.parse::<u16>().map_err(|_|Error::Invalid)?))
|
|
15
|
+
}
|
|
16
|
+
/// The one key of a rule among alternatives, with its value.
|
|
17
|
+
fn either<'a>(rule: &'a Rule, keys: [&'static str; 2]) -> Result<(usize, &'a str)> {
|
|
18
|
+
let mut found=keys.iter().enumerate().filter_map(|(index,key)|value(rule,key).ok().map(|value|(index,value)));
|
|
19
|
+
let result=found.next().ok_or(Error::Invalid)?;
|
|
20
|
+
if found.next().is_some() {return Err(Error::Invalid);}
|
|
21
|
+
Ok(result)
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
/// Fixed Linux UAPI / xtables 1.8.10 and 1.8.11 constructors, not a raw rule API.
|
|
13
25
|
pub(super) fn expected(rule: &Rule) -> Result<Vec<Attr>> {
|
|
14
26
|
let reply=value(rule,"--ctdir")?=="REPLY";
|
|
15
27
|
let new=value(rule,"--ctstate")?=="NEW";
|
|
16
28
|
let protocol=match value(rule,"-p")? {"tcp"=>6_u16,"udp"=>17,_=>return Err(Error::Invalid)};
|
|
17
|
-
|
|
18
|
-
let
|
|
19
|
-
let (
|
|
20
|
-
let
|
|
21
|
-
|
|
22
|
-
let
|
|
29
|
+
// The packet's own source (0) or destination (1) address and port.
|
|
30
|
+
let (side,live)=either(rule,["-s","-d"])?;
|
|
31
|
+
let live=live.strip_suffix("/32").ok_or(Error::Invalid)?.parse::<std::net::Ipv4Addr>().map_err(|_|Error::Invalid)?.octets();
|
|
32
|
+
let (port_side,ports)=either(rule,["--sport","--dport"])?;
|
|
33
|
+
if port_side!=side {return Err(Error::Invalid);}
|
|
34
|
+
let (first,last)=span(ports)?;
|
|
35
|
+
// The originally tracked source (0) or destination (1) address and port.
|
|
36
|
+
let (tracked_side,tracked)=either(rule,["--ctorigsrc","--ctorigdst"])?;
|
|
37
|
+
let tracked=tracked.parse::<std::net::Ipv4Addr>().map_err(|_|Error::Invalid)?.octets();
|
|
38
|
+
let (tracked_port_side,tracked_ports)=either(rule,["--ctorigsrcport","--ctorigdstport"])?;
|
|
39
|
+
if tracked_port_side!=tracked_side {return Err(Error::Invalid);}
|
|
40
|
+
let (tracked_first,tracked_last)=span(tracked_ports)?;
|
|
41
|
+
let mut result=vec![payload(1,if side==0 {12} else {16},4),compare(live.to_vec())];
|
|
23
42
|
for (key,argument) in [(6,"-i"),(7,"-o")] {
|
|
24
43
|
result.extend(meta(key,[value(rule,argument)?.as_bytes(),&[0]].concat()));
|
|
25
44
|
}
|
|
@@ -30,7 +49,7 @@ pub(super) fn expected(rule: &Rule) -> Result<Vec<Attr>> {
|
|
|
30
49
|
Attr::nested(4,vec![Attr::bytes(1,vec![0x17])]),Attr::nested(5,vec![Attr::bytes(1,vec![0])]),
|
|
31
50
|
]),compare(vec![2])]);
|
|
32
51
|
}
|
|
33
|
-
result.push(payload(2,if
|
|
52
|
+
result.push(payload(2,if side==0 {0} else {2},2));
|
|
34
53
|
result.push(if first==last {compare(first.to_be_bytes().to_vec())} else {
|
|
35
54
|
expr("range",vec![Attr::u32(1,1),Attr::u32(2,0),
|
|
36
55
|
Attr::nested(3,vec![Attr::bytes(1,first.to_be_bytes().to_vec())]),
|
|
@@ -38,11 +57,17 @@ pub(super) fn expected(rule: &Rule) -> Result<Vec<Attr>> {
|
|
|
38
57
|
});
|
|
39
58
|
// xt_conntrack_mtinfo3 is 164 bytes, XT_ALIGN'ed to 168 on both shipped
|
|
40
59
|
// 64-bit Linux targets. All fields and padding are initialized explicitly.
|
|
60
|
+
// The original source address/mask are at 0/16, its ports at 138/154; the
|
|
61
|
+
// original destination's at 32/48 and 140/156.
|
|
41
62
|
let mut conntrack=vec![0;168];
|
|
42
|
-
|
|
43
|
-
conntrack[
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
let address=32*tracked_side;
|
|
64
|
+
conntrack[address..address+4].copy_from_slice(&tracked);
|
|
65
|
+
conntrack[address+16..address+32].fill(0xff); // canonical bare-host nf_inet_addr mask
|
|
66
|
+
// STATE|PROTO|DIRECTION with ORIGSRC|ORIGSRC_PORT or ORIGDST|ORIGDST_PORT.
|
|
67
|
+
let flags=0x1003|if tracked_side==0 {0x0104} else {0x0208};
|
|
68
|
+
let port=2*tracked_side;
|
|
69
|
+
for (offset,number) in [(136,protocol),(138+port,tracked_first),(146,flags),
|
|
70
|
+
(148,if reply {0x1000} else {0}),(150,if new {8} else {2}),(154+port,tracked_last)] {
|
|
46
71
|
conntrack[offset..offset+2].copy_from_slice(&number.to_ne_bytes());
|
|
47
72
|
}
|
|
48
73
|
result.push(expr("match",vec![Attr::string(1,"conntrack"),Attr::u32(2,3),Attr::bytes(3,conntrack)]));
|
|
@@ -63,7 +63,11 @@ impl Policy {
|
|
|
63
63
|
return Err(Error::Invalid);
|
|
64
64
|
}
|
|
65
65
|
self.barrier.prepared.validate()?;
|
|
66
|
-
|
|
66
|
+
// The contribution admits handoff flows through Docker's FORWARD path; an
|
|
67
|
+
// exclusive barrier would drop Docker's own container forwarding.
|
|
68
|
+
if self.scope()?.exclusive_forwarding {
|
|
69
|
+
return Err(Error::Invalid);
|
|
70
|
+
}
|
|
67
71
|
let receipt = &self.barrier.receipt;
|
|
68
72
|
let handle = receipt.table_handle.parse::<u64>().map_err(|_| Error::Invalid)?;
|
|
69
73
|
if handle == 0 || handle.to_string() != receipt.table_handle
|
|
@@ -95,46 +99,105 @@ pub struct Rule {
|
|
|
95
99
|
fn port_range(first: u16, last: u16) -> String {
|
|
96
100
|
if first == last { first.to_string() } else { format!("{first}:{last}") }
|
|
97
101
|
}
|
|
102
|
+
/// One side of a packet or of its originally tracked tuple: an exact address
|
|
103
|
+
/// and an inclusive port span.
|
|
104
|
+
#[derive(Clone, Copy)]
|
|
105
|
+
struct Tuple<'a> {
|
|
106
|
+
address: &'a str,
|
|
107
|
+
first: u16,
|
|
108
|
+
last: u16,
|
|
109
|
+
}
|
|
110
|
+
/// One flow class of the barrier's forward chain, in its original direction:
|
|
111
|
+
/// it enters on `input`, leaves on `output`, carries `live` as its source
|
|
112
|
+
/// (`source`) or destination, and conntrack tracked it with `tracked` as its
|
|
113
|
+
/// original source (`source`) or destination. A leased or symmetric flow is
|
|
114
|
+
/// keyed on its own source, before outer source NAT; an inbound publication on
|
|
115
|
+
/// its translated inside destination and its published outside destination.
|
|
116
|
+
struct Flow<'a> {
|
|
117
|
+
input: &'a str,
|
|
118
|
+
output: &'a str,
|
|
119
|
+
protocol: &'a str,
|
|
120
|
+
source: bool,
|
|
121
|
+
live: Tuple<'a>,
|
|
122
|
+
tracked: Tuple<'a>,
|
|
123
|
+
}
|
|
124
|
+
impl Flow<'_> {
|
|
125
|
+
/// The ESTABLISHED original direction, a NEW opening (TCP only with SYN and
|
|
126
|
+
/// FIN/RST/ACK clear) and the ESTABLISHED reply, as the barrier admits them.
|
|
127
|
+
fn rules(&self, mut push: impl FnMut(Vec<String>) -> Result<()>) -> Result<()> {
|
|
128
|
+
for state in ["NEW", "ESTABLISHED", "REPLY"] {
|
|
129
|
+
let reply = state == "REPLY";
|
|
130
|
+
// The reply swaps the interfaces and the packet's tuple sides; the
|
|
131
|
+
// originally tracked tuple stays the same.
|
|
132
|
+
let source = self.source != reply;
|
|
133
|
+
let mut args: Vec<String> = vec![
|
|
134
|
+
"-i".into(), if reply { self.output } else { self.input }.into(),
|
|
135
|
+
"-o".into(), if reply { self.input } else { self.output }.into(),
|
|
136
|
+
if source { "-s" } else { "-d" }.into(), format!("{}/32", self.live.address),
|
|
137
|
+
"-p".into(), self.protocol.into(), "-m".into(), self.protocol.into(),
|
|
138
|
+
if source { "--sport" } else { "--dport" }.into(), port_range(self.live.first, self.live.last),
|
|
139
|
+
];
|
|
140
|
+
if state == "NEW" && self.protocol == "tcp" {
|
|
141
|
+
args.extend(["--tcp-flags","FIN,SYN,RST,ACK","SYN"].map(String::from));
|
|
142
|
+
}
|
|
143
|
+
let (address, port) = if self.source { ("--ctorigsrc", "--ctorigsrcport") } else { ("--ctorigdst", "--ctorigdstport") };
|
|
144
|
+
args.extend(["-m","conntrack","--ctstate",if reply {"ESTABLISHED"} else {state},"--ctproto",if self.protocol == "tcp" {"6"} else {"17"},address].map(String::from));
|
|
145
|
+
// Canonical xtables host form: explicit /32 has different unused
|
|
146
|
+
// conntrack mask bytes and does not survive a save/restore round trip.
|
|
147
|
+
args.push(self.tracked.address.into());
|
|
148
|
+
args.push(port.into());args.push(port_range(self.tracked.first,self.tracked.last));
|
|
149
|
+
args.extend(["--ctdir",if reply {"REPLY"} else {"ORIGINAL"}].map(String::from));
|
|
150
|
+
push(args)?;
|
|
151
|
+
}
|
|
152
|
+
Ok(())
|
|
153
|
+
}
|
|
154
|
+
}
|
|
98
155
|
impl Prepared {
|
|
99
156
|
pub fn validate(&self) -> Result<()> {
|
|
100
157
|
if self.policy.clone().prepare(self.backend.clone())? != *self { return Err(Error::Invalid); }
|
|
101
158
|
Ok(())
|
|
102
159
|
}
|
|
160
|
+
/// Every forwarded flow the barrier admits, from the scope's one forwarding
|
|
161
|
+
/// enumeration: leased ranges first, then each publication's inbound flows
|
|
162
|
+
/// and, when symmetric, the flows it opens from its target ports.
|
|
163
|
+
fn flows(&self) -> Result<Vec<Flow<'_>>> {
|
|
164
|
+
let scope = self.policy.scope()?;
|
|
165
|
+
let forwarding = scope.forwarding()?;
|
|
166
|
+
let uplink = scope.uplink.interface_name.as_str();
|
|
167
|
+
let mut flows = Vec::new();
|
|
168
|
+
for leased in &forwarding.leased {
|
|
169
|
+
let tuple = Tuple { address:&leased.allocation.transit_source_address, first:leased.range.first, last:leased.range.last };
|
|
170
|
+
flows.push(Flow { input:&leased.handoff.link.interface_name, output:uplink, protocol:&leased.range.protocol,
|
|
171
|
+
source:true, live:tuple, tracked:tuple });
|
|
172
|
+
}
|
|
173
|
+
for published in &forwarding.published {
|
|
174
|
+
let port = published.port;
|
|
175
|
+
let inside = Tuple { address:&port.target_address, first:port.target_port, last:port.target_last() };
|
|
176
|
+
let outside = Tuple { address:&port.host_ip, first:port.host_port, last:port.host_last() };
|
|
177
|
+
let link = published.link.interface_name.as_str();
|
|
178
|
+
flows.push(Flow { input:uplink, output:link, protocol:&port.protocol, source:false, live:inside, tracked:outside });
|
|
179
|
+
if port.symmetric {
|
|
180
|
+
flows.push(Flow { input:link, output:uplink, protocol:&port.protocol, source:true, live:inside, tracked:inside });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
Ok(flows)
|
|
184
|
+
}
|
|
103
185
|
pub fn rules(&self, options: &Options) -> Result<Vec<Rule>> {
|
|
104
186
|
options.validate()?;
|
|
105
|
-
let
|
|
187
|
+
let digest = self.digest.strip_prefix("sha256:").ok_or(Error::Invalid)?;
|
|
106
188
|
let mut result = Vec::new();
|
|
107
189
|
let mut bytes = 0;
|
|
108
|
-
for
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
];
|
|
120
|
-
if state == "NEW" && range.protocol == "tcp" {
|
|
121
|
-
args.extend(["--tcp-flags","FIN,SYN,RST,ACK","SYN"].map(String::from));
|
|
122
|
-
}
|
|
123
|
-
args.extend(["-m","conntrack","--ctstate",if reply {"ESTABLISHED"} else {state},"--ctproto",if range.protocol == "tcp" {"6"} else {"17"},"--ctorigsrc"].map(String::from));
|
|
124
|
-
// Canonical xtables host form: explicit /32 has different unused
|
|
125
|
-
// conntrack mask bytes and does not survive a save/restore round trip.
|
|
126
|
-
args.push(allocation.transit_source_address.clone());
|
|
127
|
-
args.push("--ctorigsrcport".into());args.push(port_range(range.first,range.last));
|
|
128
|
-
args.extend(["--ctdir",if reply {"REPLY"} else {"ORIGINAL"},"-m","comment","--comment"].map(String::from));
|
|
129
|
-
let comment = format!("snftd1:{}:{}:{}:{}",options.owner_id,options.instance_id,self.digest.strip_prefix("sha256:").ok_or(Error::Invalid)?,result.len());
|
|
130
|
-
args.push(comment.clone());args.extend(["-j","ACCEPT"].map(String::from));
|
|
131
|
-
bytes += args.iter().map(|arg|arg.len()+1).sum::<usize>()+32;
|
|
132
|
-
crate::capacity("restoreRules", 192, result.len() + 1)?;
|
|
133
|
-
crate::capacity("restoreBytes", 100_000, bytes)?;
|
|
134
|
-
result.push(Rule {comment,args});
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
190
|
+
for flow in self.flows()? {
|
|
191
|
+
flow.rules(|mut args| {
|
|
192
|
+
let comment = format!("snftd1:{}:{}:{}:{}",options.owner_id,options.instance_id,digest,result.len());
|
|
193
|
+
args.extend(["-m","comment","--comment"].map(String::from));
|
|
194
|
+
args.push(comment.clone());args.extend(["-j","ACCEPT"].map(String::from));
|
|
195
|
+
bytes += args.iter().map(|arg|arg.len()+1).sum::<usize>()+32;
|
|
196
|
+
crate::capacity("restoreRules", 192, result.len() + 1)?;
|
|
197
|
+
crate::capacity("restoreBytes", 100_000, bytes)?;
|
|
198
|
+
result.push(Rule {comment,args});
|
|
199
|
+
Ok(())
|
|
200
|
+
})?;
|
|
138
201
|
}
|
|
139
202
|
Ok(result)
|
|
140
203
|
}
|
package/rust/src/docker.rs
CHANGED
|
@@ -71,13 +71,38 @@ impl Owner {
|
|
|
71
71
|
|| expected.receipt.backend!=expected.prepared.backend {return Err(Error::Conflict);}
|
|
72
72
|
Ok(())
|
|
73
73
|
}
|
|
74
|
-
fn
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
fn handoffs(prepared: &Prepared) -> Result<Vec<&crate::egress::LocalLink>> {
|
|
75
|
+
Ok(prepared.policy.scope()?.handoffs.iter().map(|handoff|&handoff.link).collect())
|
|
76
|
+
}
|
|
77
|
+
fn bound(link: &crate::egress::LocalLink) -> wire::BoundInterface<'_> {
|
|
78
|
+
wire::BoundInterface {index:link.interface_index,name:&link.interface_name,kind:&link.interface_kind,
|
|
79
|
+
mac:link.mac_address.as_deref(),link_index:link.interface_link_index,addresses:&link.required_ipv4_addresses}
|
|
80
|
+
}
|
|
81
|
+
/// The handoff links of a transition: those both generations bind exactly,
|
|
82
|
+
/// and those it adds or drops.
|
|
83
|
+
fn fenced<'a>(previous: Option<&'a Prepared>, target: &'a Prepared) -> Result<(Vec<&'a crate::egress::LocalLink>, Vec<&'a crate::egress::LocalLink>)> {
|
|
84
|
+
let previous=previous.map(Self::handoffs).transpose()?.unwrap_or_default();
|
|
85
|
+
let target=Self::handoffs(target)?;
|
|
86
|
+
let kept=target.iter().copied().filter(|link|previous.contains(link)).collect();
|
|
87
|
+
let changed=previous.iter().copied().filter(|link|!target.contains(link))
|
|
88
|
+
.chain(target.iter().copied().filter(|link|!previous.contains(link))).collect();
|
|
89
|
+
Ok((kept,changed))
|
|
90
|
+
}
|
|
91
|
+
/// Reconcile fence, before and after the single restore transaction. A handoff
|
|
92
|
+
/// link both generations bind exactly may carry packets: the transaction swaps
|
|
93
|
+
/// the owned block atomically, so a flow both contributions admit passes
|
|
94
|
+
/// throughout, and the referenced barrier already is the target. Every link
|
|
95
|
+
/// the transition adds or drops must be down.
|
|
96
|
+
fn fence(previous: Option<&Prepared>, target: &Prepared) -> Result<()> {
|
|
97
|
+
let (kept,changed)=Self::fenced(previous,target)?;
|
|
98
|
+
wire::verify_bound_interfaces(kept.into_iter().map(Self::bound))?;
|
|
99
|
+
wire::verify_bound_interfaces_down(changed.into_iter().map(Self::bound))
|
|
100
|
+
}
|
|
101
|
+
/// Cleanup fence: deletion only withdraws admission. A handoff link that no
|
|
102
|
+
/// longer exists, such as a veth that left with its router namespace, counts
|
|
103
|
+
/// as down; one that exists must still be the exact bound link, and down.
|
|
104
|
+
fn cleanup_fence(prepared: &Prepared) -> Result<()> {
|
|
105
|
+
wire::verify_bound_interfaces_down_or_absent(Self::handoffs(prepared)?.into_iter().map(Self::bound))
|
|
81
106
|
}
|
|
82
107
|
fn applied(&self, prepared: Prepared, backend: String) -> Applied {
|
|
83
108
|
Applied {receipt:Receipt {identity:self.identity.clone(),backend,revision:prepared.policy.revision,digest:prepared.digest.clone()},prepared}
|
|
@@ -102,15 +127,14 @@ impl Owner {
|
|
|
102
127
|
return Ok(self.applied(transition.target.clone(),snapshot.backend));
|
|
103
128
|
}
|
|
104
129
|
if !snapshot.verified_matches(&self.options,&previous_rules,deadline)? {return Err(Error::Conflict);}
|
|
105
|
-
|
|
106
|
-
Self::fence(
|
|
130
|
+
let previous=transition.previous.as_ref().map(|v|&v.prepared);
|
|
131
|
+
Self::fence(previous,&transition.target)?;
|
|
107
132
|
frontend::replace(&previous_rules,&target_rules,deadline)?;
|
|
108
133
|
self.context()?;
|
|
109
134
|
let current=frontend::Snapshot::read(deadline)?;
|
|
110
135
|
if current.backend!=snapshot.backend || !current.verified_matches(&self.options,&target_rules,deadline)? {return Err(Error::Conflict);}
|
|
111
136
|
owner::Owner::verify_external(&transition.target.policy.barrier)?;
|
|
112
|
-
|
|
113
|
-
Self::fence(&transition.target)?;
|
|
137
|
+
Self::fence(previous,&transition.target)?;
|
|
114
138
|
Ok(self.applied(transition.target.clone(),current.backend))
|
|
115
139
|
})();
|
|
116
140
|
match result {
|
|
@@ -141,12 +165,12 @@ impl Owner {
|
|
|
141
165
|
if current.verified_matches(&self.options,&[],deadline)? {return Ok(());}
|
|
142
166
|
let rules=expected.prepared.rules(&self.options)?;
|
|
143
167
|
if !current.verified_matches(&self.options,&rules,deadline)? {return Err(Error::Conflict);}
|
|
144
|
-
Self::
|
|
168
|
+
Self::cleanup_fence(&expected.prepared)?;
|
|
145
169
|
frontend::replace(&rules,&[],deadline)?;
|
|
146
170
|
self.context()?;
|
|
147
171
|
let after=frontend::Snapshot::read(deadline)?;
|
|
148
172
|
if after.backend!=current.backend || !after.verified_matches(&self.options,&[],deadline)? {return Err(Error::Conflict);}
|
|
149
|
-
Self::
|
|
173
|
+
Self::cleanup_fence(&expected.prepared)
|
|
150
174
|
})();
|
|
151
175
|
match result {Ok(())=>{self.applied=Some(expected);self.error=None;self.released=true;Ok(())},Err(error)=>{self.error=Some(error);Err(error)}}
|
|
152
176
|
}
|
package/rust/src/docker_tests.rs
CHANGED
|
@@ -2,8 +2,9 @@ use super::*;
|
|
|
2
2
|
use serde_json::json;
|
|
3
3
|
|
|
4
4
|
fn options() -> Options {Options {owner_id:"dockerfixture".into(),instance_id:"qualification_20260913".into()}}
|
|
5
|
-
fn policy() -> policy::Policy {
|
|
6
|
-
|
|
5
|
+
fn policy() -> policy::Policy {policy_from(crate::egress::tests::host())}
|
|
6
|
+
fn policy_from(host: serde_json::Value) -> policy::Policy {
|
|
7
|
+
let prepared:crate::managed::Prepared=serde_json::from_value::<crate::egress::Policy>(host).unwrap().prepare().unwrap().into();
|
|
7
8
|
policy::Policy {schema_version:1,revision:1,barrier:owner::Applied {
|
|
8
9
|
receipt:owner::Receipt {identity:owner::Identity {owner_id:"barrier".into(),instance_id:"qualification_20260913".into(),table_name:"snft_fixture".into(),boot_id:"00000000-0000-4000-8000-000000000000".into(),namespace_device:"4".into(),namespace_inode:"1".into()},table_handle:"1".into(),revision:1,digest:prepared.digest.clone()},prepared}}
|
|
9
10
|
}
|
|
@@ -30,6 +31,9 @@ fn docker_prepare_rejects_router_receipts_bad_hashes_and_unbounded_restore() {
|
|
|
30
31
|
let mut value=policy();value.schema_version=2;assert!(value.prepare("v1.8.11 (nf_tables)".into()).is_err());
|
|
31
32
|
let mut value=policy();value.barrier.receipt.digest="sha256:wrong".into();assert!(value.prepare("v1.8.11 (nf_tables)".into()).is_err());
|
|
32
33
|
let mut value=policy();value.barrier.prepared.policy=crate::managed::Policy::Egress(serde_json::from_value(crate::egress::tests::router()).unwrap());assert!(value.prepare("v1.8.11 (nf_tables)".into()).is_err());
|
|
34
|
+
let mut exclusive=crate::egress::tests::host();exclusive["scope"]["exclusiveForwarding"]=json!(true);
|
|
35
|
+
let prepared:crate::managed::Prepared=serde_json::from_value::<crate::egress::Policy>(exclusive).unwrap().prepare().unwrap().into();
|
|
36
|
+
let mut value=policy();value.barrier.receipt.digest=prepared.digest.clone();value.barrier.prepared=prepared;assert!(value.prepare("v1.8.11 (nf_tables)".into()).is_err());
|
|
33
37
|
let value=serde_json::to_value(policy()).unwrap();
|
|
34
38
|
let mut extra=value.clone();extra["rules"]=json!([]);assert!(serde_json::from_value::<policy::Policy>(extra).is_err());
|
|
35
39
|
let mut extra=value;extra["barrier"]["receipt"]["tableHandle"]=json!("01");assert!(serde_json::from_value::<policy::Policy>(extra).unwrap().prepare("v1.8.11 (nf_tables)".into()).is_err());
|
|
@@ -146,3 +150,161 @@ fn docker_forward_jumps_require_exact_unconditional_order() {
|
|
|
146
150
|
assert!(!graph::matches_forward(&row(expected),if target=="DOCKER-USER" {"DOCKER-FORWARD"} else {"DOCKER-USER"}).unwrap());
|
|
147
151
|
}
|
|
148
152
|
}
|
|
153
|
+
|
|
154
|
+
/// The exact 4.1.0 contribution of a scope without publications: arguments,
|
|
155
|
+
/// order, comments and native graph bytes are unchanged.
|
|
156
|
+
#[test]
|
|
157
|
+
fn docker_leased_contribution_keeps_its_exact_previous_rules_and_graphs() {
|
|
158
|
+
let rules=policy().prepare("v1.8.11 (nf_tables)".into()).unwrap().rules(&options()).unwrap();
|
|
159
|
+
let comment="snftd1:dockerfixture:qualification_20260913:2023fff251e6e68c6ec275e6b5e76f609b8bc0210b18ae3f3c88bfac3a54f5ee";
|
|
160
|
+
let expected=[
|
|
161
|
+
"-i handoff -o ens18 -s 10.240.0.2/32 -p tcp -m tcp --sport 10000:10999 --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW --ctproto 6 --ctorigsrc 10.240.0.2 --ctorigsrcport 10000:10999 --ctdir ORIGINAL",
|
|
162
|
+
"-i handoff -o ens18 -s 10.240.0.2/32 -p tcp -m tcp --sport 10000:10999 -m conntrack --ctstate ESTABLISHED --ctproto 6 --ctorigsrc 10.240.0.2 --ctorigsrcport 10000:10999 --ctdir ORIGINAL",
|
|
163
|
+
"-i ens18 -o handoff -d 10.240.0.2/32 -p tcp -m tcp --dport 10000:10999 -m conntrack --ctstate ESTABLISHED --ctproto 6 --ctorigsrc 10.240.0.2 --ctorigsrcport 10000:10999 --ctdir REPLY",
|
|
164
|
+
"-i handoff -o ens18 -s 10.240.0.2/32 -p udp -m udp --sport 10000:10999 -m conntrack --ctstate NEW --ctproto 17 --ctorigsrc 10.240.0.2 --ctorigsrcport 10000:10999 --ctdir ORIGINAL",
|
|
165
|
+
"-i handoff -o ens18 -s 10.240.0.2/32 -p udp -m udp --sport 10000:10999 -m conntrack --ctstate ESTABLISHED --ctproto 17 --ctorigsrc 10.240.0.2 --ctorigsrcport 10000:10999 --ctdir ORIGINAL",
|
|
166
|
+
"-i ens18 -o handoff -d 10.240.0.2/32 -p udp -m udp --dport 10000:10999 -m conntrack --ctstate ESTABLISHED --ctproto 17 --ctorigsrc 10.240.0.2 --ctorigsrcport 10000:10999 --ctdir REPLY",
|
|
167
|
+
];
|
|
168
|
+
assert_eq!(rules.len(),expected.len());
|
|
169
|
+
for (index,(rule,args)) in rules.iter().zip(expected).enumerate() {
|
|
170
|
+
assert_eq!(rule.args.join(" "),format!("{args} -m comment --comment {comment}:{index} -j ACCEPT"));
|
|
171
|
+
}
|
|
172
|
+
let mut digest=<sha2::Sha256 as sha2::Digest>::new();
|
|
173
|
+
for rule in &rules {sha2::Digest::update(&mut digest,crate::wire::encode_attrs(&graph::expected(rule).unwrap()));}
|
|
174
|
+
assert_eq!(format!("{:x}",sha2::Digest::finalize(digest)),"dc46825f57c6f0001c1a517760e9e5560e4a5ee90f00322075c4b96e41e24c79");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn published() -> policy::Policy {
|
|
178
|
+
let mut range=crate::egress::tests::published_port("udp",20000,20000);
|
|
179
|
+
range["hostPortEnd"]=json!(20200);range["symmetric"]=json!(true);
|
|
180
|
+
policy_from(crate::egress::tests::published_host(json!([range,crate::egress::tests::published_port("tcp",443,8443)])))
|
|
181
|
+
}
|
|
182
|
+
fn conntrack(expressions: &[crate::wire::Attr]) -> Vec<u8> {
|
|
183
|
+
for expression in expressions {
|
|
184
|
+
let fields=crate::wire::attrs(&expression.value).unwrap();
|
|
185
|
+
if crate::wire::text(&fields,1).unwrap()!="match" {continue;}
|
|
186
|
+
let data=crate::wire::attrs(&crate::wire::one(&fields,2).unwrap().value).unwrap();
|
|
187
|
+
if crate::wire::text(&data,1).unwrap()=="conntrack" {return crate::wire::one(&data,3).unwrap().value.clone();}
|
|
188
|
+
}
|
|
189
|
+
panic!("no conntrack match")
|
|
190
|
+
}
|
|
191
|
+
fn word(bytes: &[u8], offset: usize) -> u16 {u16::from_ne_bytes([bytes[offset],bytes[offset+1]])}
|
|
192
|
+
|
|
193
|
+
/// Inbound publications and their replies, and the flows a symmetric
|
|
194
|
+
/// publication opens from its target ports, as the barrier's forward chain
|
|
195
|
+
/// admits them: leased ranges first, then publications in canonical order.
|
|
196
|
+
#[test]
|
|
197
|
+
fn docker_contribution_admits_every_published_and_symmetric_flow_of_the_barrier() {
|
|
198
|
+
let prepared=published().prepare("v1.8.11 (nf_tables)".into()).unwrap();
|
|
199
|
+
let rules=prepared.rules(&options()).unwrap();
|
|
200
|
+
// 2 leased ranges, 2 inbound publications and 1 symmetric publication, each
|
|
201
|
+
// NEW, ESTABLISHED and the ESTABLISHED reply.
|
|
202
|
+
assert_eq!(rules.len(),15);
|
|
203
|
+
let args=|index: usize|rules[index].args[..rules[index].args.len()-6].join(" ");
|
|
204
|
+
assert_eq!(args(6),"-i ens18 -o handoff -d 10.240.0.2/32 -p tcp -m tcp --dport 8443 --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW --ctproto 6 --ctorigdst 192.0.2.2 --ctorigdstport 443 --ctdir ORIGINAL");
|
|
205
|
+
assert_eq!(args(7),"-i ens18 -o handoff -d 10.240.0.2/32 -p tcp -m tcp --dport 8443 -m conntrack --ctstate ESTABLISHED --ctproto 6 --ctorigdst 192.0.2.2 --ctorigdstport 443 --ctdir ORIGINAL");
|
|
206
|
+
assert_eq!(args(8),"-i handoff -o ens18 -s 10.240.0.2/32 -p tcp -m tcp --sport 8443 -m conntrack --ctstate ESTABLISHED --ctproto 6 --ctorigdst 192.0.2.2 --ctorigdstport 443 --ctdir REPLY");
|
|
207
|
+
assert_eq!(args(9),"-i ens18 -o handoff -d 10.240.0.2/32 -p udp -m udp --dport 20000:20200 -m conntrack --ctstate NEW --ctproto 17 --ctorigdst 192.0.2.2 --ctorigdstport 20000:20200 --ctdir ORIGINAL");
|
|
208
|
+
assert_eq!(args(11),"-i handoff -o ens18 -s 10.240.0.2/32 -p udp -m udp --sport 20000:20200 -m conntrack --ctstate ESTABLISHED --ctproto 17 --ctorigdst 192.0.2.2 --ctorigdstport 20000:20200 --ctdir REPLY");
|
|
209
|
+
assert_eq!(args(12),"-i handoff -o ens18 -s 10.240.0.2/32 -p udp -m udp --sport 20000:20200 -m conntrack --ctstate NEW --ctproto 17 --ctorigsrc 10.240.0.2 --ctorigsrcport 20000:20200 --ctdir ORIGINAL");
|
|
210
|
+
assert_eq!(args(13),"-i handoff -o ens18 -s 10.240.0.2/32 -p udp -m udp --sport 20000:20200 -m conntrack --ctstate ESTABLISHED --ctproto 17 --ctorigsrc 10.240.0.2 --ctorigsrcport 20000:20200 --ctdir ORIGINAL");
|
|
211
|
+
assert_eq!(args(14),"-i ens18 -o handoff -d 10.240.0.2/32 -p udp -m udp --dport 20000:20200 -m conntrack --ctstate ESTABLISHED --ctproto 17 --ctorigsrc 10.240.0.2 --ctorigsrcport 20000:20200 --ctdir REPLY");
|
|
212
|
+
// Without publications only the leased rules remain; a non-symmetric
|
|
213
|
+
// publication opens nothing from its target ports.
|
|
214
|
+
let mut plain=crate::egress::tests::published_port("udp",20000,20000);plain["hostPortEnd"]=json!(20200);
|
|
215
|
+
let plain=policy_from(crate::egress::tests::published_host(json!([plain]))).prepare("v1.8.11 (nf_tables)".into()).unwrap().rules(&options()).unwrap();
|
|
216
|
+
assert_eq!(plain.len(),9);
|
|
217
|
+
assert!(plain.iter().all(|rule|!rule.args.windows(2).any(|w|w==["--ctorigsrcport","20000:20200"])));
|
|
218
|
+
assert!(snapshot(&text(&rules)).matches(&options(),&rules).unwrap());
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#[test]
|
|
222
|
+
fn docker_inbound_graph_binds_the_original_destination_tuple() {
|
|
223
|
+
let rules=published().prepare("v1.8.11 (nf_tables)".into()).unwrap().rules(&options()).unwrap();
|
|
224
|
+
for (index,reply) in [(6,false),(8,true)] {
|
|
225
|
+
let expressions=graph::expected(&rules[index]).unwrap();
|
|
226
|
+
let data=conntrack(&expressions);
|
|
227
|
+
assert_eq!(data.len(),168);
|
|
228
|
+
assert!(data[..32].iter().all(|byte|*byte==0));
|
|
229
|
+
assert_eq!(&data[32..36],&[192,0,2,2]);
|
|
230
|
+
assert!(data[36..48].iter().all(|byte|*byte==0) && data[48..64].iter().all(|byte|*byte==0xff));
|
|
231
|
+
assert_eq!((word(&data,136),word(&data,138),word(&data,140),word(&data,146),word(&data,148),word(&data,150)),
|
|
232
|
+
(6,0,443,0x120b,if reply {0x1000} else {0},if reply {2} else {8}));
|
|
233
|
+
assert_eq!((word(&data,154),word(&data,156)),(0,443));
|
|
234
|
+
// The live tuple: the translated inside destination, or its reply source.
|
|
235
|
+
let payload=crate::wire::attrs(&expressions[0].value).unwrap();
|
|
236
|
+
let fields=crate::wire::attrs(&crate::wire::one(&payload,2).unwrap().value).unwrap();
|
|
237
|
+
assert_eq!(crate::wire::one(&fields,3).unwrap().value,(if reply {12_u32} else {16}).to_be_bytes());
|
|
238
|
+
let row=|expressions|vec![crate::wire::Attr::string(1,"filter"),crate::wire::Attr::string(2,"DOCKER-USER"),
|
|
239
|
+
crate::wire::Attr::u64(3,42),crate::wire::Attr::nested(4,expressions)];
|
|
240
|
+
assert!(graph::matches_rule(&row(expressions.clone()),&rules[index]).unwrap());
|
|
241
|
+
let mut hidden=expressions.clone();
|
|
242
|
+
for expression in &mut hidden {
|
|
243
|
+
let mut fields=crate::wire::attrs(&expression.value).unwrap();
|
|
244
|
+
if crate::wire::text(&fields,1).unwrap()!="match" {continue;}
|
|
245
|
+
let mut data=crate::wire::attrs(&crate::wire::one(&fields,2).unwrap().value).unwrap();
|
|
246
|
+
if crate::wire::text(&data,1).unwrap()!="conntrack" {continue;}
|
|
247
|
+
data.iter_mut().find(|a|a.id()==3).unwrap().value[52..64].fill(0);
|
|
248
|
+
*fields.iter_mut().find(|a|a.id()==2).unwrap()=crate::wire::Attr::nested(2,data);
|
|
249
|
+
expression.value=crate::wire::encode_attrs(&fields);
|
|
250
|
+
}
|
|
251
|
+
assert!(!graph::matches_rule(&row(hidden),&rules[index]).unwrap());
|
|
252
|
+
}
|
|
253
|
+
// A ranged inbound publication binds its whole outside span.
|
|
254
|
+
let data=conntrack(&graph::expected(&rules[9]).unwrap());
|
|
255
|
+
assert_eq!((word(&data,140),word(&data,156)),(20000,20200));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#[test]
|
|
259
|
+
fn docker_snapshot_accepts_rendered_destination_tuples_and_rejects_changed_ones() {
|
|
260
|
+
let rules=published().prepare("v1.8.11 (nf_tables)".into()).unwrap().rules(&options()).unwrap();
|
|
261
|
+
let original=text(&rules);
|
|
262
|
+
assert!(snapshot(&original.replace("--ctorigdst 192.0.2.2 ","--ctorigdst 192.0.2.2/32 ")).matches(&options(),&rules).unwrap());
|
|
263
|
+
for (before,after) in [("--ctorigdstport 443","--ctorigdstport 444"),("--ctorigdst 192.0.2.2","--ctorigsrc 192.0.2.2"),
|
|
264
|
+
("--dport 8443","--dport 8444"),("--ctorigdst 192.0.2.2","--ctorigdst 192.0.2.3")] {
|
|
265
|
+
assert!(!snapshot(&original.replacen(before,after,1)).matches(&options(),&rules).unwrap());
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#[test]
|
|
270
|
+
fn docker_contribution_keeps_its_restore_bound() {
|
|
271
|
+
let ports:Vec<_>=(0..64_u16).map(|index|crate::egress::tests::published_port("tcp",443+index,443+index)).collect();
|
|
272
|
+
let error=policy_from(crate::egress::tests::published_host(json!(ports))).prepare("v1.8.11 (nf_tables)".into()).unwrap_err();
|
|
273
|
+
assert_eq!(error.code(),"EXHAUSTED");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fn absent(mut host: serde_json::Value) -> serde_json::Value {
|
|
277
|
+
// No link holds this index on a test host.
|
|
278
|
+
host["scope"]["handoffs"][0]["link"]["interfaceIndex"]=json!(2_147_483_000_u32);host
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#[test]
|
|
282
|
+
fn docker_reconcile_fence_lets_only_links_both_generations_bind_carry_packets() {
|
|
283
|
+
let target=policy().prepare("v1.8.11 (nf_tables)".into()).unwrap();
|
|
284
|
+
let amended=published().prepare("v1.8.11 (nf_tables)".into()).unwrap();
|
|
285
|
+
let moved=policy_from(absent(crate::egress::tests::host())).prepare("v1.8.11 (nf_tables)".into()).unwrap();
|
|
286
|
+
let names=|links: Vec<&crate::egress::LocalLink>|links.iter().map(|link|link.interface_index).collect::<Vec<_>>();
|
|
287
|
+
let (kept,changed)=Owner::fenced(None,&target).unwrap();
|
|
288
|
+
assert_eq!((names(kept),names(changed)),(vec![],vec![4]));
|
|
289
|
+
let (kept,changed)=Owner::fenced(Some(&target),&amended).unwrap();
|
|
290
|
+
assert_eq!((names(kept),names(changed)),(vec![4],vec![]));
|
|
291
|
+
let (kept,changed)=Owner::fenced(Some(&target),&moved).unwrap();
|
|
292
|
+
assert_eq!((names(kept),names(changed)),(vec![],vec![4,2_147_483_000]));
|
|
293
|
+
// A link the transition keeps must still exist exactly.
|
|
294
|
+
assert!(Owner::fence(Some(&moved),&moved).is_err());
|
|
295
|
+
assert!(Owner::fence(None,&moved).is_err());
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#[test]
|
|
299
|
+
fn docker_cleanup_fence_counts_a_vanished_handoff_as_down() {
|
|
300
|
+
let gone=policy_from(absent(crate::egress::tests::host())).prepare("v1.8.11 (nf_tables)".into()).unwrap();
|
|
301
|
+
Owner::cleanup_fence(&gone).unwrap();
|
|
302
|
+
// The mutation fence still requires the exact link.
|
|
303
|
+
let link=&gone.policy.scope().unwrap().handoffs[0].link;
|
|
304
|
+
assert!(crate::wire::verify_bound_interfaces_down(std::iter::once(Owner::bound(link))).is_err());
|
|
305
|
+
// A different link at a bound index is not the bound link.
|
|
306
|
+
let mut loopback=crate::egress::tests::host();
|
|
307
|
+
loopback["scope"]["handoffs"][0]["link"]["interfaceIndex"]=json!(1);
|
|
308
|
+
let loopback=policy_from(loopback).prepare("v1.8.11 (nf_tables)".into()).unwrap();
|
|
309
|
+
assert_eq!(Owner::cleanup_fence(&loopback),Err(Error::Conflict));
|
|
310
|
+
}
|
package/rust/src/egress.host.rs
CHANGED
|
@@ -84,34 +84,31 @@ fn leased(
|
|
|
84
84
|
expressions.extend(original_range(range.first, range.last));
|
|
85
85
|
Ok(expressions)
|
|
86
86
|
}
|
|
87
|
-
/// The publications of this hop
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
Ok(published::Hop {
|
|
87
|
+
/// The publications of this hop, from the scope's forwarding enumeration.
|
|
88
|
+
fn hop<'a>(scope: &'a HostScope, forwarding: &Forwarding<'a>) -> published::Hop<'a> {
|
|
89
|
+
let publications = forwarding
|
|
90
|
+
.published
|
|
91
|
+
.iter()
|
|
92
|
+
.map(|item| published::Publication {
|
|
93
|
+
protocol: &item.port.protocol,
|
|
94
|
+
outside: (
|
|
95
|
+
&item.port.host_ip,
|
|
96
|
+
item.port.host_port,
|
|
97
|
+
item.port.host_last(),
|
|
98
|
+
),
|
|
99
|
+
inside: (
|
|
100
|
+
&item.port.target_address,
|
|
101
|
+
item.port.target_port,
|
|
102
|
+
item.port.target_last(),
|
|
103
|
+
),
|
|
104
|
+
link: item.link,
|
|
105
|
+
symmetric: item.port.symmetric,
|
|
106
|
+
})
|
|
107
|
+
.collect();
|
|
108
|
+
published::Hop {
|
|
112
109
|
publications,
|
|
113
110
|
fixed: &scope.uplink,
|
|
114
|
-
}
|
|
111
|
+
}
|
|
115
112
|
}
|
|
116
113
|
/// The exact platform endpoints served on host addresses. Every packet is
|
|
117
114
|
/// checked against the live and the originally tracked destination tuple, so a
|
|
@@ -219,7 +216,8 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &HostScope) -> Result<()
|
|
|
219
216
|
.ok_or(crate::Error::Invalid)
|
|
220
217
|
})?;
|
|
221
218
|
}
|
|
222
|
-
let
|
|
219
|
+
let forwarding = scope.forwarding()?;
|
|
220
|
+
let hop = hop(scope, &forwarding);
|
|
223
221
|
published::sets(program, &hop, false)?;
|
|
224
222
|
protection(program, scope, false)?;
|
|
225
223
|
protection(program, scope, true)?;
|
|
@@ -241,26 +239,27 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &HostScope) -> Result<()
|
|
|
241
239
|
program.jump("forward", link(&handoff.link, true), "protected_out")?;
|
|
242
240
|
program.jump("forward", link(&handoff.link, false), "protected_back")?;
|
|
243
241
|
}
|
|
244
|
-
for
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
let mut reply = envelope(scope, handoff, allocation, range, true)?;
|
|
257
|
-
reply.extend(state(2));
|
|
258
|
-
program.end("forward", reply, 1)?;
|
|
259
|
-
let mut nat = original;
|
|
260
|
-
nat.extend(snat(&scope.snat_address, None)?);
|
|
261
|
-
program.rule("post", nat)?;
|
|
242
|
+
for LeasedForward {
|
|
243
|
+
handoff,
|
|
244
|
+
allocation,
|
|
245
|
+
range,
|
|
246
|
+
} in &forwarding.leased
|
|
247
|
+
{
|
|
248
|
+
let original = envelope(scope, handoff, allocation, range, false)?;
|
|
249
|
+
for connection_state in [2, 8] {
|
|
250
|
+
let mut expressions = original.clone();
|
|
251
|
+
expressions.extend(state(connection_state));
|
|
252
|
+
if connection_state == 8 && range.protocol == "tcp" {
|
|
253
|
+
expressions.extend(opening_tcp());
|
|
262
254
|
}
|
|
255
|
+
program.end("forward", expressions, 1)?;
|
|
263
256
|
}
|
|
257
|
+
let mut reply = envelope(scope, handoff, allocation, range, true)?;
|
|
258
|
+
reply.extend(state(2));
|
|
259
|
+
program.end("forward", reply, 1)?;
|
|
260
|
+
let mut nat = original;
|
|
261
|
+
nat.extend(snat(&scope.snat_address, None)?);
|
|
262
|
+
program.rule("post", nat)?;
|
|
264
263
|
}
|
|
265
264
|
// Symmetric flows leave from ports outside every leased range of their
|
|
266
265
|
// address, after the capture barrier and before the terminal handoff denials.
|
|
@@ -272,5 +271,14 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &HostScope) -> Result<()
|
|
|
272
271
|
program.deny_link("forward", &handoff.link, true)?;
|
|
273
272
|
program.deny_link("forward", &handoff.link, false)?;
|
|
274
273
|
}
|
|
274
|
+
// Exclusive forwarding: everything the forward chain has not accepted by now
|
|
275
|
+
// drops, of every family and on every pair of interfaces, so the host
|
|
276
|
+
// forwards exactly this scope's leased, published and symmetric handoff
|
|
277
|
+
// flows. nftables runs every table's forward base chain, and a drop in any
|
|
278
|
+
// one is final: this denies forwarding that other owners would accept, and
|
|
279
|
+
// accepting here still cannot override their drops.
|
|
280
|
+
if scope.exclusive_forwarding {
|
|
281
|
+
program.end("forward", vec![], 0)?;
|
|
282
|
+
}
|
|
275
283
|
Ok(())
|
|
276
284
|
}
|