Skip to content

Commit 49646df

Browse files
author
Erich Heine
committed
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 cdd3812 commit 49646df

File tree

1 file changed

+201
-0
lines changed

1 file changed

+201
-0
lines changed

examples/subnet_nat.rs

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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+
.unwrap();
79+
80+
if let Err(res) = msg.execute().await {
81+
eprintln!("{}", res);
82+
return Err(());
83+
}
84+
85+
// Then add the ingress 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+
.unwrap();
110+
111+
if let Err(res) = msg.execute().await {
112+
eprintln!("{}", res);
113+
return Err(());
114+
}
115+
116+
Ok(())
117+
}
118+
119+
// TODO: There is no code in netlink-packet-route for egress qisc types yet.
120+
// This shells out to the `tc` command instead, and should be replaced when
121+
// the appropriate message types are available in netlink-packet-route.
122+
async fn create_egress_qdisc(devname: &str) -> Result<(), ()> {
123+
match std::process::Command::new("tc")
124+
.args(&[
125+
"qdisc", "add", "dev", devname, "root", "handle", "10:", "htb",
126+
])
127+
.output()
128+
{
129+
Err(e) => {
130+
eprintln!("Error creating egress qdisc: {}", e);
131+
Err(())
132+
}
133+
Ok(output) if output.status.success() => Ok(()),
134+
Ok(_) => {
135+
eprintln!("Error creating egress qdisc:");
136+
Err(())
137+
}
138+
}
139+
}
140+
141+
async fn create_ingress_qdisc(handle: Handle, index: u32) -> Result<(), ()> {
142+
if let Err(e) = handle
143+
.qdisc()
144+
.add(index as i32)
145+
.handle(0xffff, 0)
146+
.ingress()
147+
.execute()
148+
.await
149+
{
150+
eprintln!("Error creating ingress qdisc: {e}");
151+
return Err(());
152+
}
153+
154+
Ok(())
155+
}
156+
157+
async fn get_link_index_by_name(
158+
handle: Handle,
159+
name: String,
160+
) -> Result<u32, Error> {
161+
let mut links = handle.link().get().match_name(name).execute();
162+
let link = (links.try_next().await?).expect("Link not found");
163+
Ok(link.header.index)
164+
}
165+
166+
fn split_cidr(cidr_text: &str) -> Result<(Ipv4Addr, usize), String> {
167+
let (prefix, len) = cidr_text
168+
.split_once('/')
169+
.ok_or(format!("'{}' is not a valid CIDR", cidr_text))?;
170+
let address: Ipv4Addr = prefix.parse().map_err(|e| {
171+
format!("'{}' cannot be parsed to an IP address: {}", prefix, e)
172+
})?;
173+
let prefix_len: usize = len
174+
.parse()
175+
.map_err(|_| format!("'{}' is not a valid prefix length", len))?;
176+
177+
Ok((address, prefix_len))
178+
}
179+
180+
fn usage() {
181+
eprintln!(
182+
"usage:
183+
cargo run --example subnet_nat -- <devname> <old_subnet> <new_subnet>
184+
185+
This is will have the same effect as:
186+
tc qdisc add dev $devname root handle 10: htb
187+
tc qdisc add dev $devname ingress handle ffff
188+
189+
tc filter add dev $devname parent 10: protocol ip prio 10 matchall action nat egress $old_subnet $new_subnet
190+
tc filter add dev $devname parent ffff: protocol ip prio 10 matchall action nat ingress $new_subnet $old_subnet
191+
192+
Note that you need to run this program as root. Instead of running cargo as root,
193+
build the example normally:
194+
195+
cd rtnetlink ; cargo build --example add_tc_qdisc_ingress
196+
197+
Then find the binary in the target directory:
198+
199+
cd ../target/debug/example ; sudo ./add_tc_qdisc_ingress <index>"
200+
);
201+
}

0 commit comments

Comments
 (0)