Skip to content

Commit 8a890d6

Browse files
authored
feat(route): Migrate ip route command to rtnetlink (#46)
* deps(anyhow): remove it * refactor(route): refactor it * feat(route): Migrate `ip route` command to rtnetlink
1 parent 55c20fb commit 8a890d6

5 files changed

Lines changed: 186 additions & 69 deletions

File tree

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ mimalloc = { version = "0.1.39", default-features = false, optional = true }
4949

5050
[target.'cfg(target_os = "linux")'.dependencies]
5151
sysctl = "0.5.5"
52+
rtnetlink = "0.14"
53+
netlink-packet-route = "0.19"
54+
futures = "0.3.30"
5255

5356
[target.'cfg(target_family = "unix")'.dependencies]
5457
daemonize = "0.5.0"

src/main.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ mod daemon;
44
pub mod error;
55
mod proxy;
66
mod update;
7-
mod util;
87

98
use clap::{Args, Parser, Subcommand};
109
use std::net::SocketAddr;

src/proxy/mod.rs

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ mod auth;
22
mod connect;
33
mod http;
44
mod murmur;
5+
#[cfg(target_os = "linux")]
6+
mod route;
57
mod socks5;
68

79
use crate::{AuthMode, BootArgs, Proxy};
@@ -43,33 +45,22 @@ pub async fn run(args: BootArgs) -> crate::Result<()> {
4345
tracing::info!("Arch: {}", std::env::consts::ARCH);
4446
tracing::info!("Version: {}", env!("CARGO_PKG_VERSION"));
4547

46-
// Auto set sysctl
4748
#[cfg(target_os = "linux")]
48-
args.cidr.map(|v6| {
49-
crate::util::sysctl_ipv6_no_local_bind();
50-
crate::util::sysctl_route_add_cidr(&v6);
51-
});
49+
if let Some(cidr) = &args.cidr {
50+
route::sysctl_ipv6_no_local_bind();
51+
route::sysctl_route_add_cidr(&cidr).await;
52+
}
53+
54+
let ctx = move |auth: AuthMode| ProxyContext {
55+
bind: args.bind,
56+
concurrent: args.concurrent,
57+
auth,
58+
whitelist: args.whitelist,
59+
connector: connect::Connector::new(args.cidr, args.fallback),
60+
};
5261

5362
match args.proxy {
54-
Proxy::Http { auth } => {
55-
http::proxy(ProxyContext {
56-
bind: args.bind,
57-
concurrent: args.concurrent,
58-
auth,
59-
whitelist: args.whitelist,
60-
connector: connect::Connector::new(args.cidr, args.fallback),
61-
})
62-
.await
63-
}
64-
Proxy::Socks5 { auth } => {
65-
socks5::proxy(ProxyContext {
66-
bind: args.bind,
67-
concurrent: args.concurrent,
68-
auth,
69-
whitelist: args.whitelist,
70-
connector: connect::Connector::new(args.cidr, args.fallback),
71-
})
72-
.await
73-
}
63+
Proxy::Http { auth } => http::proxy(ctx(auth)).await,
64+
Proxy::Socks5 { auth } => socks5::proxy(ctx(auth)).await,
7465
}
7566
}

src/proxy/route.rs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
use futures::TryStreamExt;
2+
use netlink_packet_route::{
3+
route::{RouteAddress, RouteAttribute, RouteProtocol, RouteScope, RouteType},
4+
AddressFamily,
5+
};
6+
use rtnetlink::{new_connection, Error, Handle, IpVersion};
7+
8+
/// Attempts to add a route to the given subnet on the loopback interface.
9+
///
10+
/// This function uses the `ip` command to add a route to the loopback
11+
/// interface. It checks if the current user has root privileges before
12+
/// attempting to add the route. If the user does not have root privileges, the
13+
/// function returns immediately. If the `ip` command fails, it prints an error
14+
/// message to the console.
15+
///
16+
/// # Arguments
17+
///
18+
/// * `subnet` - The subnet for which to add a route.
19+
///
20+
/// # Example
21+
///
22+
/// ```
23+
/// let subnet = cidr::IpCidr::from_str("192.168.1.0/24").unwrap();
24+
/// sysctl_route_add_cidr(&subnet);
25+
/// ```
26+
pub async fn sysctl_route_add_cidr(subnet: &cidr::IpCidr) {
27+
if !nix::unistd::Uid::effective().is_root() {
28+
return;
29+
}
30+
31+
let (connection, handle, _) = new_connection().unwrap();
32+
33+
tokio::spawn(connection);
34+
35+
if let Err(e) = add_route(handle.clone(), subnet).await {
36+
eprintln!("{e}");
37+
}
38+
}
39+
40+
async fn add_route(handle: Handle, cidr: &cidr::IpCidr) -> Result<(), Error> {
41+
let route = handle.route();
42+
let iface_idx = handle
43+
.link()
44+
.get()
45+
.match_name("lo".to_owned())
46+
.execute()
47+
.try_next()
48+
.await?
49+
.unwrap()
50+
.header
51+
.index;
52+
53+
// Check if the route already exists
54+
let route_check = |ip_version: IpVersion,
55+
address_family: AddressFamily,
56+
destination_prefix_length: u8,
57+
route_address: RouteAddress| async move {
58+
let mut routes = handle.route().get(ip_version).execute();
59+
while let Some(route) = routes.try_next().await? {
60+
let header = route.header;
61+
if header.address_family == address_family
62+
&& header.destination_prefix_length == destination_prefix_length
63+
{
64+
for attr in route.attributes.iter() {
65+
if let RouteAttribute::Destination(dest) = attr {
66+
if dest == &route_address {
67+
return Ok(true);
68+
}
69+
}
70+
}
71+
}
72+
}
73+
Ok(false)
74+
};
75+
76+
// Add a route to the loopback interface.
77+
match cidr {
78+
cidr::IpCidr::V4(v4) => {
79+
if route_check(
80+
IpVersion::V4,
81+
AddressFamily::Inet,
82+
v4.network_length(),
83+
RouteAddress::Inet(v4.first_address()),
84+
)
85+
.await?
86+
{
87+
return Ok(());
88+
}
89+
route
90+
.add()
91+
.v4()
92+
.destination_prefix(v4.first_address(), v4.network_length())
93+
.kind(RouteType::Local)
94+
.protocol(RouteProtocol::Boot)
95+
.scope(RouteScope::Universe)
96+
.output_interface(iface_idx)
97+
.priority(1024)
98+
.execute()
99+
.await?
100+
}
101+
cidr::IpCidr::V6(v6) => {
102+
if route_check(
103+
IpVersion::V6,
104+
AddressFamily::Inet6,
105+
v6.network_length(),
106+
RouteAddress::Inet6(v6.first_address()),
107+
)
108+
.await?
109+
{
110+
return Ok(());
111+
}
112+
route
113+
.add()
114+
.v6()
115+
.destination_prefix(v6.first_address(), v6.network_length())
116+
.kind(RouteType::Local)
117+
.protocol(RouteProtocol::Boot)
118+
.scope(RouteScope::Universe)
119+
.output_interface(iface_idx)
120+
.priority(1024)
121+
.execute()
122+
.await?
123+
}
124+
}
125+
126+
Ok(())
127+
}
128+
129+
/// Tries to disable local binding for IPv6.
130+
///
131+
/// This function uses the `sysctl` command to disable local binding for IPv6.
132+
/// It checks if the current user has root privileges before attempting to
133+
/// change the setting. If the user does not have root privileges, the function
134+
/// returns immediately. If the `sysctl` command fails, it prints an error
135+
/// message to the console.
136+
///
137+
/// # Example
138+
///
139+
/// ```
140+
/// sysctl_ipv6_no_local_bind();
141+
/// ```
142+
pub fn sysctl_ipv6_no_local_bind() {
143+
if !nix::unistd::Uid::effective().is_root() {
144+
return;
145+
}
146+
147+
use sysctl::Sysctl;
148+
const CTLNAME: &str = "net.ipv6.ip_nonlocal_bind";
149+
150+
let ctl = <sysctl::Ctl as Sysctl>::new(CTLNAME)
151+
.expect(&format!("could not get sysctl '{}'", CTLNAME));
152+
let _ = ctl.name().expect("could not get sysctl name");
153+
154+
let old_value = ctl.value_string().expect("could not get sysctl value");
155+
156+
let target_value = match old_value.as_ref() {
157+
"0" => "1",
158+
"1" | _ => &old_value,
159+
};
160+
161+
ctl.set_value_string(target_value).unwrap_or_else(|e| {
162+
panic!(
163+
"could not set sysctl '{}' to '{}': {}",
164+
CTLNAME, target_value, e
165+
)
166+
});
167+
}

src/util.rs

Lines changed: 0 additions & 43 deletions
This file was deleted.

0 commit comments

Comments
 (0)