Skip to content

Commit f1b37db

Browse files
author
Erich Heine
committed
Re #12 - add an example of the matchall filter
Create a tc subnet nat on a specified interface using the CIDRs provided on the command line.
1 parent b2eb333 commit f1b37db

File tree

1 file changed

+200
-0
lines changed

1 file changed

+200
-0
lines changed

examples/subnet_nat.rs

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// SPDX-License-Identifier: MIT
2+
use std::env;
3+
use std::net::Ipv4Addr;
4+
5+
use futures::stream::TryStreamExt;
6+
use netlink_packet_route::tc::{self, nlas::matchall, nlas::nat, Action};
7+
use rtnetlink::{new_connection, Error, Handle};
8+
9+
#[tokio::main]
10+
async fn main() -> Result<(), ()> {
11+
env_logger::init();
12+
13+
// Parse the command line
14+
let args: Vec<String> = env::args().collect();
15+
if args.len() != 4 {
16+
usage();
17+
return Ok(());
18+
}
19+
20+
let (old_subnet, prefix_len) = match split_cidr(&args[2]) {
21+
Ok(addrs) => addrs,
22+
Err(s) => {
23+
eprintln!("{}", s);
24+
return Err(());
25+
}
26+
};
27+
28+
let (new_subnet, _) = match split_cidr(&args[3]) {
29+
Ok(addrs) => addrs,
30+
Err(s) => {
31+
eprintln!("{}", s);
32+
return Err(());
33+
}
34+
};
35+
36+
let (connection, handle, _) = new_connection().unwrap();
37+
tokio::spawn(connection);
38+
let link_index =
39+
match get_link_index_by_name(handle.clone(), args[1].clone()).await {
40+
Ok(i) => i,
41+
Err(_) => {
42+
eprintln!("Link: {} not found", args[1]);
43+
return Err(());
44+
}
45+
};
46+
47+
// Create qdiscs on the interface.
48+
create_ingress_qdisc(handle.clone(), link_index).await?;
49+
create_egress_qdisc(&args[1]).await?;
50+
51+
// Add tc nat action filters
52+
53+
// first add the egress filter. This is equivalent to the following command:
54+
// tc filter add dev $devname \
55+
// parent 10: protocol ip prio 10 \
56+
// matchall action nat egress $old_subnet, $new_subnet
57+
let nat_params = nat::Nla::Parms(
58+
nat::TcNat::default()
59+
.set_new_addr(new_subnet)
60+
.set_old_addr(old_subnet)
61+
.set_prefix(prefix_len)
62+
.egress(),
63+
);
64+
65+
let mut nat_act = Action::default();
66+
nat_act.nlas.push(tc::ActNla::Kind(nat::KIND.to_string()));
67+
nat_act
68+
.nlas
69+
.push(tc::ActNla::Options(vec![tc::ActOpt::Nat(nat_params)]));
70+
71+
let msg = handle
72+
.traffic_filter(link_index as i32)
73+
.add()
74+
.parent(0x10 << 16)
75+
.priority(10)
76+
.protocol(0x0008)
77+
.matchall(vec![matchall::Nla::Act(vec![nat_act])]);
78+
79+
if let Err(res) = msg.execute().await {
80+
eprintln!("{}", res);
81+
return Err(());
82+
}
83+
84+
// Then add the ingress filter, This is equivalent to the command:
85+
// first add the egress filter. This is equivalent to the following command:
86+
// tc filter add dev $devname \
87+
// parent 10: protocol ip prio 10 \
88+
// matchall action nat ingress $new_subnet, $old_subnet
89+
let nat_params = nat::Nla::Parms(
90+
nat::TcNat::default()
91+
.set_new_addr(old_subnet)
92+
.set_old_addr(new_subnet)
93+
.set_prefix(prefix_len),
94+
);
95+
96+
let mut nat_act = Action::default();
97+
nat_act.nlas.push(tc::ActNla::Kind(nat::KIND.to_string()));
98+
nat_act
99+
.nlas
100+
.push(tc::ActNla::Options(vec![tc::ActOpt::Nat(nat_params)]));
101+
102+
let msg = handle
103+
.traffic_filter(link_index as i32)
104+
.add()
105+
.parent(0xffff << 16)
106+
.priority(10)
107+
.protocol(0x0008)
108+
.matchall(vec![matchall::Nla::Act(vec![nat_act])]);
109+
110+
if let Err(res) = msg.execute().await {
111+
eprintln!("{}", res);
112+
return Err(());
113+
}
114+
115+
Ok(())
116+
}
117+
118+
// TODO: There is no code in netlink-packet-route for egress qisc types yet.
119+
// This shells out to the `tc` command instead, and should be replaced when
120+
// the appropriate message types are available in netlink-packet-route.
121+
async fn create_egress_qdisc(devname: &str) -> Result<(), ()> {
122+
match std::process::Command::new("tc")
123+
.args(&[
124+
"qdisc", "add", "dev", devname, "root", "handle", "10:", "htb",
125+
])
126+
.output()
127+
{
128+
Err(e) => {
129+
eprintln!("Error creating egress qdisc: {}", e);
130+
Err(())
131+
}
132+
Ok(output) if output.status.success() => Ok(()),
133+
Ok(_) => {
134+
eprintln!("Error creating egress qdisc:");
135+
Err(())
136+
}
137+
}
138+
}
139+
140+
async fn create_ingress_qdisc(handle: Handle, index: u32) -> Result<(), ()> {
141+
if let Err(e) = handle
142+
.qdisc()
143+
.add(index as i32)
144+
.handle(0xffff, 0)
145+
.ingress()
146+
.execute()
147+
.await
148+
{
149+
eprintln!("Error creating ingress qdisc: {e}");
150+
return Err(());
151+
}
152+
153+
Ok(())
154+
}
155+
156+
async fn get_link_index_by_name(
157+
handle: Handle,
158+
name: String,
159+
) -> Result<u32, Error> {
160+
let mut links = handle.link().get().match_name(name).execute();
161+
let link = (links.try_next().await?).expect("Link not found");
162+
Ok(link.header.index)
163+
}
164+
165+
fn split_cidr(cidr_text: &str) -> Result<(Ipv4Addr, usize), String> {
166+
let (prefix, len) = cidr_text
167+
.split_once('/')
168+
.ok_or(format!("'{}' is not a valid CIDR", cidr_text))?;
169+
let address: Ipv4Addr = prefix.parse().map_err(|e| {
170+
format!("'{}' cannot be parsed to an IP address: {}", prefix, e)
171+
})?;
172+
let prefix_len: usize = len
173+
.parse()
174+
.map_err(|_| format!("'{}' is not a valid prefix length", len))?;
175+
176+
Ok((address, prefix_len))
177+
}
178+
179+
fn usage() {
180+
eprintln!(
181+
"usage:
182+
cargo run --example subnet_nat -- <devname> <old_subnet> <new_subnet>
183+
184+
This is will have the same effect as:
185+
tc qdisc add dev $devname root handle 10: htb
186+
tc qdisc add dev $devname ingress handle ffff
187+
188+
tc filter add dev $devname parent 10: protocol ip prio 10 matchall action nat egress $old_subnet $new_subnet
189+
tc filter add dev $devname parent ffff: protocol ip prio 10 matchall action nat ingress $new_subnet $old_subnet
190+
191+
Note that you need to run this program as root. Instead of running cargo as root,
192+
build the example normally:
193+
194+
cd rtnetlink ; cargo build --example add_tc_qdisc_ingress
195+
196+
Then find the binary in the target directory:
197+
198+
cd ../target/debug/example ; sudo ./add_tc_qdisc_ingress <index>"
199+
);
200+
}

0 commit comments

Comments
 (0)