Skip to content

Commit 4bea4c4

Browse files
committed
test(nat): Add tests for static NAT + masquerade
Add unit tests to validate the use in a pipeline of static NAT + masquerade, or static NAT + port forwarding, on opposite sides of a peering. We set up a pipeline, create a packet, process it, and check for its output; then we create a fake reply, process it, check as well; at last, we re-process the original packet, to make sure that the connection still works after the flow table entry has been created. Signed-off-by: Quentin Monnet <qmo@qmon.net>
1 parent ba0fdf6 commit 4bea4c4

3 files changed

Lines changed: 347 additions & 0 deletions

File tree

nat/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pub mod portfw;
2828
mod ranges;
2929
pub mod stateful;
3030
pub mod stateless;
31+
mod test;
3132

3233
pub use icmp_handler::nf::IcmpErrorHandler;
3334
pub use port::NatPort;

nat/src/portfw/portfwtable/access.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
//! Port forwarding table objects for concurrent accesses
55
//! from datapath and configuration / management
66
7+
use super::super::build_port_forwarding_configuration;
78
use super::PortFwTableError;
89
use super::objects::{PortFwEntry, PortFwTable};
10+
use config::external::overlay::vpc::ValidatedVpcTable;
911
use left_right::{Absorb, ReadGuard, ReadHandle, ReadHandleFactory, WriteHandle};
1012

1113
#[allow(unused)]
@@ -57,6 +59,14 @@ impl PortFwTableWriter {
5759
self.0.publish(); // intended
5860
Ok(())
5961
}
62+
pub fn update_from_vpc_table(
63+
&mut self,
64+
vpc_table: &ValidatedVpcTable,
65+
) -> Result<(), PortFwTableError> {
66+
let ruleset = build_port_forwarding_configuration(vpc_table)
67+
.map_err(|e| PortFwTableError::Unsupported(e.to_string()))?;
68+
self.update_table(&ruleset)
69+
}
6070
}
6171

6272
#[derive(Debug)]

nat/src/test.rs

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Open Network Fabric Authors
3+
4+
//! NAT tests
5+
6+
#![cfg(test)]
7+
8+
use super::*;
9+
use crate::portfw::{PortForwarder, PortFwTableWriter};
10+
use crate::stateful::{NatAllocatorWriter, StatefulNatConfig};
11+
use crate::stateless::NatTablesWriter;
12+
use crate::stateless::setup::build_nat_configuration;
13+
use concurrency::sync::Arc;
14+
use config::external::overlay::vpc::{Vpc, VpcTable};
15+
use config::external::overlay::vpcpeering::{VpcExpose, VpcManifest, VpcPeering, VpcPeeringTable};
16+
use config::external::overlay::{Overlay, ValidatedOverlay};
17+
use flow_entry::flow_table::FlowLookup;
18+
use flow_entry::flow_table::FlowTable;
19+
use flow_filter::{FlowFilter, FlowFilterTable, FlowFilterTableWriter};
20+
use lpm::prefix::{PortRange, PrefixWithOptionalPorts};
21+
use net::buffer::TestBuffer;
22+
use net::eth::mac::Mac;
23+
use net::packet::Packet;
24+
use net::packet::test_utils::{addr_v4, build_test_udp_ipv4_frame};
25+
use net::vxlan::Vni;
26+
use pipeline::{DynPipeline, NetworkFunction};
27+
use tracectl::get_trace_ctl;
28+
29+
fn addr(s: &str) -> IpAddr {
30+
addr_v4(s).into()
31+
}
32+
33+
fn pwp(s: &str, start: u16, end: u16) -> PrefixWithOptionalPorts {
34+
PrefixWithOptionalPorts::new(s.into(), Some(PortRange::new(start, end).unwrap()))
35+
}
36+
37+
fn vni(vni: u32) -> Vni {
38+
Vni::new_checked(vni).expect("Failed to create VNI")
39+
}
40+
41+
fn build_packet(
42+
src_ip: &str,
43+
dst_ip: &str,
44+
src_port: u16,
45+
dst_port: u16,
46+
src_vni: Vni,
47+
) -> Packet<TestBuffer> {
48+
let mut packet: Packet<TestBuffer> = build_test_udp_ipv4_frame(
49+
Mac([0x2, 0, 0, 0, 0, 1]),
50+
Mac([0x2, 0, 0, 0, 0, 2]),
51+
src_ip,
52+
dst_ip,
53+
src_port,
54+
dst_port,
55+
);
56+
packet.meta_mut().set_overlay(true);
57+
packet.meta_mut().src_vpcd = Some(src_vni.into());
58+
println!("built packet:\n{packet}");
59+
packet
60+
}
61+
62+
fn setup_masq_pipeline(
63+
overlay: &ValidatedOverlay,
64+
) -> (
65+
DynPipeline<TestBuffer>,
66+
Arc<FlowTable>,
67+
FlowFilterTableWriter,
68+
NatTablesWriter,
69+
PortFwTableWriter,
70+
NatAllocatorWriter,
71+
) {
72+
let mut pipeline: DynPipeline<TestBuffer> = DynPipeline::new();
73+
74+
// Flow table
75+
let flow_table = Arc::new(FlowTable::default());
76+
let flow_lookup = FlowLookup::new("flow-lookup", flow_table.clone());
77+
pipeline = pipeline.add_stage(flow_lookup);
78+
79+
// Flow-filter
80+
let flow_filter_table = FlowFilterTable::build_from_overlay(overlay).unwrap();
81+
println!("{flow_filter_table}");
82+
let mut flow_filter_writer = FlowFilterTableWriter::new();
83+
flow_filter_writer.update_flow_filter_table(flow_filter_table);
84+
let flow_filter = FlowFilter::new("flow-filter", flow_filter_writer.get_reader());
85+
pipeline = pipeline.add_stage(flow_filter);
86+
87+
// Static NAT
88+
let static_nat_tables = build_nat_configuration(overlay.vpc_table()).unwrap();
89+
println!("{static_nat_tables}");
90+
let mut static_nat_writer = NatTablesWriter::new();
91+
static_nat_writer.update_nat_tables(static_nat_tables);
92+
let static_nat_tabler_factory = static_nat_writer.get_reader_factory();
93+
let static_nat1 = StatelessNat::with_reader("static-NAT-1", static_nat_tabler_factory.handle());
94+
pipeline = pipeline.add_stage(static_nat1);
95+
96+
// Port forwarding
97+
let mut portfw_writer = PortFwTableWriter::new();
98+
portfw_writer
99+
.update_from_vpc_table(overlay.vpc_table())
100+
.unwrap();
101+
let portfw = PortForwarder::new("port-forwarder", portfw_writer.reader(), flow_table.clone());
102+
if let Some(table) = portfw_writer.enter() {
103+
println!("{}", table.as_ref());
104+
}
105+
pipeline = pipeline.add_stage(portfw);
106+
107+
// Masquerade
108+
let mut allocator = NatAllocatorWriter::new();
109+
let masquerade = StatefulNat::new("masquerade", flow_table.clone(), allocator.get_reader());
110+
let masquerade_config = StatefulNatConfig::new(overlay.vpc_table(), 1);
111+
allocator.update_nat_allocator(masquerade_config, &flow_table);
112+
if let Some(state) = allocator.get_reader().get() {
113+
println!("{state}");
114+
}
115+
pipeline = pipeline.add_stage(masquerade);
116+
117+
(
118+
pipeline,
119+
flow_table,
120+
flow_filter_writer,
121+
static_nat_writer,
122+
portfw_writer,
123+
allocator,
124+
)
125+
}
126+
127+
#[tokio::test]
128+
async fn test_nat_combination_static_masquerade() {
129+
let _tctl = get_trace_ctl().setup_from_string("pipeline=debug");
130+
131+
let vni1 = vni(100);
132+
let vni2 = vni(200);
133+
let orig_src_ip = "1.2.3.4";
134+
let orig_dst_ip = "5.6.7.8";
135+
let orig_src_port = 1234;
136+
let orig_dst_port = 5678;
137+
let target_src_ip = "5.5.5.5";
138+
let target_dst_ip = "192.168.0.8";
139+
140+
// Build overlay
141+
let mut vpc_table = VpcTable::new();
142+
vpc_table
143+
.add(Vpc::new("vpc1", "VPC01", vni1.as_u32()).unwrap())
144+
.unwrap();
145+
vpc_table
146+
.add(Vpc::new("vpc2", "VPC02", vni2.as_u32()).unwrap())
147+
.unwrap();
148+
let mut peering_table = VpcPeeringTable::new();
149+
peering_table
150+
.add(VpcPeering::new(
151+
"vpc1-to-vpc2",
152+
VpcManifest::with_exposes(
153+
"vpc1",
154+
vec![
155+
VpcExpose::empty()
156+
.make_stateful_nat(None) // Masquerade
157+
.unwrap()
158+
.ip("1.2.3.0/24".into())
159+
.as_range("5.5.5.5/32".into())
160+
.unwrap(),
161+
],
162+
),
163+
VpcManifest::with_exposes(
164+
"vpc2",
165+
vec![
166+
VpcExpose::empty()
167+
.make_stateless_nat() // Static NAT (no ports)
168+
.unwrap()
169+
.ip("192.168.0.0/24".into())
170+
.as_range("5.6.7.0/24".into())
171+
.unwrap(),
172+
],
173+
),
174+
None,
175+
))
176+
.unwrap();
177+
let overlay = Overlay::new(vpc_table, peering_table).validate().unwrap();
178+
179+
// Build pipeline
180+
//
181+
// Retrieve the writer handles even if we don't use them, to avoid them being dropped when we
182+
// exit setup_masq_pipeline().
183+
let (
184+
mut pipeline,
185+
flow_table,
186+
_flow_filter_writer,
187+
_static_nat_writer,
188+
_portfw_writer,
189+
_masquerade_writer,
190+
) = setup_masq_pipeline(&overlay);
191+
192+
// Test packet: | 1.2.3.4:1234 > 5.6.7.8:5678 | becomes | 5.5.5.5:XXXX > 192.168.0.8:5678 |
193+
let packet = build_packet(orig_src_ip, orig_dst_ip, orig_src_port, orig_dst_port, vni1);
194+
let output: Vec<_> = pipeline.process(std::iter::once(packet)).collect();
195+
assert_eq!(output.len(), 1);
196+
let packet_out = output.first().unwrap();
197+
println!("packet_out:\n{packet_out}\n{flow_table}");
198+
assert_eq!(packet_out.get_done(), None);
199+
assert_eq!(packet_out.ip_source(), Some(addr(target_src_ip)));
200+
assert_eq!(packet_out.ip_destination(), Some(addr(target_dst_ip)));
201+
202+
let target_src_port = packet_out.transport_src_port().unwrap().get();
203+
let target_dst_port = orig_dst_port;
204+
205+
// Reply packet: | 192.168.0.8:5678 > 5.5.5.5:XXXX | becomes | 5.6.7.8:5678 > 1.2.3.4:1234 |
206+
let packet_reply = build_packet(
207+
target_dst_ip,
208+
target_src_ip,
209+
target_dst_port,
210+
target_src_port,
211+
vni2,
212+
);
213+
let output_reply: Vec<_> = pipeline.process(std::iter::once(packet_reply)).collect();
214+
assert_eq!(output_reply.len(), 1);
215+
let packet_out_reply = output_reply.first().unwrap();
216+
println!("packet_out_reply:\n{packet_out_reply}\n{flow_table}");
217+
assert_eq!(packet_out_reply.get_done(), None);
218+
assert_eq!(packet_out_reply.ip_source(), Some(addr(orig_dst_ip)));
219+
assert_eq!(packet_out_reply.ip_destination(), Some(addr(orig_src_ip)));
220+
221+
// Test initial packet again now that the session is established
222+
let packet = build_packet(orig_src_ip, orig_dst_ip, orig_src_port, orig_dst_port, vni1);
223+
let output: Vec<_> = pipeline.process(std::iter::once(packet)).collect();
224+
assert_eq!(output.len(), 1);
225+
let packet_out = output.first().unwrap();
226+
println!("packet_out:\n{packet_out}\n{flow_table}");
227+
assert_eq!(packet_out.get_done(), None);
228+
assert_eq!(packet_out.ip_source(), Some(addr(target_src_ip)));
229+
assert_eq!(packet_out.ip_destination(), Some(addr(target_dst_ip)));
230+
assert_eq!(flow_table.len(), Some(2));
231+
}
232+
233+
#[tokio::test]
234+
async fn test_nat_combination_static_portfw() {
235+
let _tctl = get_trace_ctl().setup_from_string("pipeline=debug");
236+
237+
let vni1 = vni(100);
238+
let vni2 = vni(200);
239+
let orig_src_ip = "1.2.3.4";
240+
let orig_dst_ip = "5.6.7.8";
241+
let orig_src_port = 1234;
242+
let orig_dst_port = 5678;
243+
let target_src_ip = "5.5.5.4";
244+
let target_dst_ip = "192.168.0.8";
245+
let target_src_port = 1734;
246+
let target_dst_port = 7678;
247+
248+
// Build overlay
249+
let mut vpc_table = VpcTable::new();
250+
vpc_table
251+
.add(Vpc::new("vpc1", "VPC01", vni1.as_u32()).unwrap())
252+
.unwrap();
253+
vpc_table
254+
.add(Vpc::new("vpc2", "VPC02", vni2.as_u32()).unwrap())
255+
.unwrap();
256+
let mut peering_table = VpcPeeringTable::new();
257+
peering_table
258+
.add(VpcPeering::new(
259+
"vpc1-to-vpc2",
260+
VpcManifest::with_exposes(
261+
"vpc1",
262+
vec![
263+
VpcExpose::empty()
264+
.make_stateless_nat() // Static NAT (with ports)
265+
.unwrap()
266+
.ip(pwp("1.2.3.0/24", 1201, 1300))
267+
.as_range(pwp("5.5.5.0/24", 1701, 1800))
268+
.unwrap(),
269+
],
270+
),
271+
VpcManifest::with_exposes(
272+
"vpc2",
273+
vec![
274+
VpcExpose::empty()
275+
.make_port_forwarding(None, None) // Port forwarding
276+
.unwrap()
277+
.ip(pwp("192.168.0.0/24", 7001, 8000))
278+
.as_range(pwp("5.6.7.0/24", 5001, 6000))
279+
.unwrap(),
280+
],
281+
),
282+
None,
283+
))
284+
.unwrap();
285+
let overlay = Overlay::new(vpc_table, peering_table).validate().unwrap();
286+
287+
// Build pipeline
288+
//
289+
// Retrieve the writer handles even if we don't use them, to avoid them being dropped when we
290+
// exit setup_masq_pipeline().
291+
let (
292+
mut pipeline,
293+
flow_table,
294+
_flow_filter_writer,
295+
_static_nat_writer,
296+
_portfw_writer,
297+
_masquerade_writer,
298+
) = setup_masq_pipeline(&overlay);
299+
300+
// Test packet: | 1.2.3.4:1234 > 5.6.7.8:5678 | becomes | 5.5.5.4:1734 > 192.168.0.8:7678 |
301+
let packet = build_packet(orig_src_ip, orig_dst_ip, orig_src_port, orig_dst_port, vni1);
302+
let output: Vec<_> = pipeline.process(std::iter::once(packet)).collect();
303+
assert_eq!(output.len(), 1);
304+
let packet_out = output.first().unwrap();
305+
println!("packet_out:\n{packet_out}\n{flow_table}");
306+
assert_eq!(packet_out.get_done(), None);
307+
assert_eq!(packet_out.ip_source(), Some(addr(target_src_ip)));
308+
assert_eq!(packet_out.ip_destination(), Some(addr(target_dst_ip)));
309+
310+
// Reply packet: | 192.168.0.8:7678 > 5.5.5.4:1734 | becomes | 5.6.7.8:5678 > 1.2.3.4:1234 |
311+
let packet_reply = build_packet(
312+
target_dst_ip,
313+
target_src_ip,
314+
target_dst_port,
315+
target_src_port,
316+
vni2,
317+
);
318+
let output_reply: Vec<_> = pipeline.process(std::iter::once(packet_reply)).collect();
319+
assert_eq!(output_reply.len(), 1);
320+
let packet_out_reply = output_reply.first().unwrap();
321+
println!("packet_out_reply:\n{packet_out_reply}\n{flow_table}");
322+
assert_eq!(packet_out_reply.get_done(), None);
323+
assert_eq!(packet_out_reply.ip_source(), Some(addr(orig_dst_ip)));
324+
assert_eq!(packet_out_reply.ip_destination(), Some(addr(orig_src_ip)));
325+
326+
// Test initial packet again now that the session is established
327+
let packet = build_packet(orig_src_ip, orig_dst_ip, orig_src_port, orig_dst_port, vni1);
328+
let output: Vec<_> = pipeline.process(std::iter::once(packet)).collect();
329+
assert_eq!(output.len(), 1);
330+
let packet_out = output.first().unwrap();
331+
println!("packet_out:\n{packet_out}\n{flow_table}");
332+
assert_eq!(packet_out.get_done(), None);
333+
assert_eq!(packet_out.ip_source(), Some(addr(target_src_ip)));
334+
assert_eq!(packet_out.ip_destination(), Some(addr(target_dst_ip)));
335+
assert_eq!(flow_table.len(), Some(2));
336+
}

0 commit comments

Comments
 (0)