Skip to content

Commit 6a53bf9

Browse files
authored
GH-675: Find Alternative to route -n in Linux (#251)
1 parent c612681 commit 6a53bf9

File tree

4 files changed

+183
-81
lines changed

4 files changed

+183
-81
lines changed

automap/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

automap/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ port_scanner = "0.1.5"
2020
pretty-hex = "0.1.0"
2121
rand = {version = "0.7.0", features = ["getrandom", "small_rng"]}
2222

23+
24+
[dev-dependencies]
25+
regex = "1.5.4"
26+
2327
[[bin]]
2428
name = "automap"
2529
path = "src/main.rs"

automap/src/comm_layer/pcp_pmp_common/linux_specific.rs

Lines changed: 177 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,38 @@ pub fn linux_find_routers(command: &dyn FindRoutersCommand) -> Result<Vec<IpAddr
1111
Ok(stdout) => stdout,
1212
Err(stderr) => return Err(AutomapError::ProtocolError(stderr)),
1313
};
14-
let addresses = output
14+
let init: Result<Vec<IpAddr>, AutomapError> = Ok(vec![]);
15+
output
1516
.split('\n')
16-
.map(|line| {
17-
line.split(' ')
18-
.filter(|piece| !piece.is_empty())
19-
.collect::<Vec<&str>>()
17+
.take_while(|line| line.trim_start().starts_with("default "))
18+
.fold(init, |acc, line| match acc {
19+
Ok(mut ip_addr_vec) => {
20+
let ip_str: String = line
21+
.chars()
22+
.skip_while(|char| !char.is_numeric())
23+
.take_while(|char| !char.is_whitespace())
24+
.collect();
25+
26+
match IpAddr::from_str(&ip_str) {
27+
Ok(ip_addr) => {
28+
ip_addr_vec.push(ip_addr);
29+
Ok(ip_addr_vec)
30+
}
31+
Err(e) => Err(AutomapError::FindRouterError(format!(
32+
"Failed to parse an IP from \"ip route\": {:?} Line: {}",
33+
e, line
34+
))),
35+
}
36+
}
37+
Err(e) => Err(e),
2038
})
21-
.filter(|line_vec| (line_vec.len() >= 4) && (line_vec[3] == "UG"))
22-
.map(|line_vec| IpAddr::from_str(line_vec[1]).expect("Bad syntax from route -n"))
23-
.collect::<Vec<IpAddr>>();
24-
Ok(addresses)
2539
}
2640

2741
pub struct LinuxFindRoutersCommand {}
2842

2943
impl FindRoutersCommand for LinuxFindRoutersCommand {
3044
fn execute(&self) -> Result<String, String> {
31-
self.execute_command("route -n")
45+
self.execute_command("ip route")
3246
}
3347
}
3448

@@ -48,19 +62,18 @@ impl LinuxFindRoutersCommand {
4862
mod tests {
4963
use super::*;
5064
use crate::mocks::FindRoutersCommandMock;
65+
use regex::Regex;
5166
use std::str::FromStr;
5267

5368
#[test]
5469
fn find_routers_works_when_there_is_a_router_to_find() {
55-
let route_n_output = "Kernel IP routing table
56-
Destination Gateway Genmask Flags Metric Ref Use Iface
57-
0.0.0.0 192.168.0.1 0.0.0.0 UG 100 0 0 enp4s0
58-
169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 enp4s0
59-
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
60-
172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-2c4b4b668d71
61-
192.168.0.0 0.0.0.0 255.255.255.0 U 100 0 0 enp4s0
62-
";
63-
let find_routers_command = FindRoutersCommandMock::new(Ok(&route_n_output));
70+
let ip_route_output = "\
71+
default via 192.168.0.1 dev enp4s0 proto dhcp src 192.168.0.100 metric 100\n\
72+
169.254.0.0/16 dev enp4s0 scope link metric 1000\n\
73+
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown\n\
74+
172.18.0.0/16 dev br-85f38f356a58 proto kernel scope link src 172.18.0.1 linkdown\n\
75+
192.168.0.0/24 dev enp4s0 proto kernel scope link src 192.168.0.100 metric 100";
76+
let find_routers_command = FindRoutersCommandMock::new(Ok(&ip_route_output));
6477

6578
let result = linux_find_routers(&find_routers_command).unwrap();
6679

@@ -69,16 +82,14 @@ Destination Gateway Genmask Flags Metric Ref Use Iface
6982

7083
#[test]
7184
fn find_routers_works_when_there_are_multiple_routers_to_find() {
72-
let route_n_output = "Kernel IP routing table
73-
Destination Gateway Genmask Flags Metric Ref Use Iface
74-
0.0.0.0 192.168.0.1 0.0.0.0 UG 100 0 0 enp4s0
75-
0.0.0.0 192.168.0.2 0.0.0.0 UG 100 0 0 enp4s0
76-
169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 enp4s0
77-
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
78-
172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-2c4b4b668d71
79-
192.168.0.0 0.0.0.0 255.255.255.0 U 100 0 0 enp4s0
80-
";
81-
let find_routers_command = FindRoutersCommandMock::new(Ok(&route_n_output));
85+
let ip_route_output = "\
86+
default via 192.168.0.1 dev enp0s8 proto dhcp metric 101\n\
87+
default via 192.168.0.2 dev enp0s3 proto dhcp metric 102\n\
88+
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15 metric 102\n\
89+
169.254.0.0/16 dev enp0s3 scope link metric 1000\n\
90+
192.168.1.0/24 dev enp0s8 proto kernel scope link src 192.168.1.64 metric 101\n\
91+
192.168.1.1 via 10.0.2.15 dev enp0s3";
92+
let find_routers_command = FindRoutersCommandMock::new(Ok(&ip_route_output));
8293

8394
let result = linux_find_routers(&find_routers_command).unwrap();
8495

@@ -91,16 +102,30 @@ Destination Gateway Genmask Flags Metric Ref Use Iface
91102
)
92103
}
93104

105+
#[test]
106+
fn find_routers_supports_ip_address_of_ipv6() {
107+
let route_n_output = "\
108+
default via 2001:1:2:3:4:5:6:7 dev enX0 proto kernel metric 256 pref medium\n\
109+
fe80::/64 dev docker0 proto kernel metric 256 pref medium";
110+
111+
let find_routers_command = FindRoutersCommandMock::new(Ok(&route_n_output));
112+
113+
let result = linux_find_routers(&find_routers_command);
114+
115+
assert_eq!(
116+
result,
117+
Ok(vec![IpAddr::from_str("2001:1:2:3:4:5:6:7").unwrap()])
118+
)
119+
}
120+
94121
#[test]
95122
fn find_routers_works_when_there_is_no_router_to_find() {
96-
let route_n_output = "Kernel IP routing table
97-
Destination Gateway Genmask Flags Metric Ref Use Iface
98-
0.0.0.0 192.168.0.1 0.0.0.0 U 100 0 0 enp4s0
99-
169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 enp4s0
100-
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
101-
172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-2c4b4b668d71
102-
192.168.0.0 0.0.0.0 255.255.255.0 U 100 0 0 enp4s0
103-
";
123+
let route_n_output = "\
124+
10.1.0.0/16 dev eth0 proto kernel scope link src 10.1.0.84 metric 100\n\
125+
0.1.0.1 dev eth0 proto dhcp scope link src 10.1.0.84 metric 100\n\
126+
168.63.129.16 via 10.1.0.1 dev eth0 proto dhcp src 10.1.0.84 metric 100\n\
127+
169.254.169.254 via 10.1.0.1 dev eth0 proto dhcp src 10.1.0.84 metric 100\n\
128+
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown";
104129
let find_routers_command = FindRoutersCommandMock::new(Ok(&route_n_output));
105130

106131
let result = linux_find_routers(&find_routers_command).unwrap();
@@ -120,55 +145,127 @@ Destination Gateway Genmask Flags Metric Ref Use Iface
120145
)
121146
}
122147

148+
#[test]
149+
fn find_routers_returns_error_if_ip_addresses_can_not_be_parsed() {
150+
let route_n_output = "\
151+
default via 192.168.0.1 dev enp4s0 proto dhcp src 192.168.0.100 metric 100\n\
152+
default via 192.168.0 dev enp0s3 proto dhcp metric 102\n\
153+
169.254.0.0/16 dev enp4s0 scope link metric 1000";
154+
let find_routers_command = FindRoutersCommandMock::new(Ok(&route_n_output));
155+
156+
let result = linux_find_routers(&find_routers_command);
157+
158+
eprintln!("{:?}", result);
159+
160+
assert_eq!(
161+
result,
162+
Err(AutomapError::FindRouterError(
163+
"Failed to parse an IP from \"ip route\": AddrParseError(Ip) Line: default via 192.168.0 dev enp0s3 proto dhcp metric 102".to_string()
164+
))
165+
)
166+
}
167+
123168
#[test]
124169
fn find_routers_command_produces_output_that_looks_right() {
125170
let subject = LinuxFindRoutersCommand::new();
126171

127172
let result = subject.execute().unwrap();
128173

129-
let lines = result.split('\n').collect::<Vec<&str>>();
130-
assert_eq!("Kernel IP routing table", lines[0]);
131-
let headings = lines[1]
132-
.split(' ')
133-
.filter(|s| s.len() > 0)
134-
.collect::<Vec<&str>>();
135-
assert_eq!(
136-
headings,
137-
vec![
138-
"Destination",
139-
"Gateway",
140-
"Genmask",
141-
"Flags",
142-
"Metric",
143-
"Ref",
144-
"Use",
145-
"Iface",
146-
]
147-
);
148-
for line in &lines[3..] {
149-
if line.len() == 0 {
150-
continue;
151-
}
152-
let columns = line
153-
.split(' ')
154-
.filter(|s| s.len() > 0)
155-
.collect::<Vec<&str>>();
156-
for idx in 0..3 {
157-
if IpAddr::from_str(columns[idx]).is_err() {
158-
panic!(
159-
"Column {} should have been an IP address but wasn't: {}",
160-
idx, columns[idx]
161-
)
162-
}
163-
}
164-
for idx in 4..7 {
165-
if columns[idx].parse::<u64>().is_err() {
166-
panic!(
167-
"Column {} should have been numeric but wasn't: {}",
168-
idx, columns[idx]
169-
)
170-
}
171-
}
174+
let mut lines = result.split('\n').collect::<Vec<&str>>();
175+
let len = lines.len();
176+
if lines[len - 1].is_empty() {
177+
lines.remove(len - 1);
172178
}
179+
let reg = ip_route_regex();
180+
lines.iter().for_each(|line| {
181+
assert!(reg.is_match(line), "Lines: {:?} line: {}", lines, line);
182+
});
183+
}
184+
185+
fn ip_route_regex() -> Regex {
186+
let reg_for_ip = r"((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}";
187+
Regex::new(&format!(
188+
r#"^(default via )?{}(/\d+)?\s(dev|via)(\s.+){{3,}}"#,
189+
reg_for_ip
190+
))
191+
.unwrap()
192+
}
193+
194+
#[test]
195+
fn reg_for_ip_route_command_output_good_and_bad_ip() {
196+
let route_n_output = vec![
197+
(
198+
"default via 0.1.0.1 dev eth0 proto dhcp scope link src 10.1.0.84 metric 100",
199+
true,
200+
"Example of good IPv4",
201+
),
202+
(
203+
"10.1.0.0/16 dev eth0 proto kernel scope link src 10.1.0.84 metric 100",
204+
true,
205+
"Example of good IPv4",
206+
),
207+
(
208+
"168.63.129.16 via 10.1.0.1 dev eth0 proto dhcp src 10.1.0.84 metric 100",
209+
true,
210+
"Example of good IPv4",
211+
),
212+
(
213+
"169.254.169.254 via 10.1.0.1 dev eth0 proto dhcp src 10.1.0.84 metric 100",
214+
true,
215+
"Example of good IPv4",
216+
),
217+
(
218+
"172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown",
219+
true,
220+
"Example of good IPv4",
221+
),
222+
(
223+
"10.1.0.0/16 dev eth0 proto kernel scope link src 10.1.0.84 metric 100",
224+
true,
225+
"Example of good IPv4",
226+
),
227+
(
228+
"0.1.255.1 dev eth0 proto dhcp",
229+
true,
230+
"Example of good IPv4",
231+
),
232+
(
233+
"0.1.0 dev eth0 proto dhcp scope link src 10.1.0.84 metric 100",
234+
false,
235+
"IPv4 address has only three elements",
236+
),
237+
(
238+
"0.1.256.1 dev eth0 proto dhcp",
239+
false,
240+
"IPv4 address has an element greater than 255",
241+
),
242+
(
243+
"0.1.b.1 dev eth0 proto dhcp",
244+
false,
245+
"IPv4 address contains a letter",
246+
),
247+
(
248+
"0.1.0.1/ dev eth0 proto dhcp",
249+
false,
250+
"IPv4 Subnet is missing a netmask",
251+
),
252+
(
253+
"2001:0db8:0000:0000:0000:ff00:0042:8329 dev eth0 proto dhcp",
254+
false,
255+
"Regex does not support IPV6",
256+
),
257+
];
258+
259+
let regex = ip_route_regex();
260+
261+
route_n_output.iter().for_each(|line| {
262+
assert_eq!(
263+
regex.is_match(line.0),
264+
line.1,
265+
"{}: Line: {}",
266+
line.2,
267+
line.0
268+
);
269+
});
173270
}
174271
}

automap/src/comm_layer/pmp.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1442,7 +1442,7 @@ mod tests {
14421442
next_lifetime: Duration::from_secs(600),
14431443
remap_interval: Duration::from_secs(0),
14441444
};
1445-
let mut last_remapped = Instant::now().sub(Duration::from_secs(3600));
1445+
let mut last_remapped = Instant::now().sub(Duration::from_secs(60));
14461446
let logger = Logger::new("maybe_remap_handles_remapping_error");
14471447
let transactor = PmpTransactor::new();
14481448
let mut subject =

0 commit comments

Comments
 (0)