Skip to content

Commit fc00f5f

Browse files
authored
Merge pull request #5 from jgallag88/netlinkSocketsBackport
[Backport to 6.0] Fix handling of netlink sockets
2 parents 3f6451d + d10a79a commit fc00f5f

File tree

8 files changed

+225
-57
lines changed

8 files changed

+225
-57
lines changed

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1-
.idea/
21
/target/
32
**/*.rs.bk
3+
4+
# Common editor/IDE files
5+
.idea/
6+
*.iml
7+
*.swp
8+
*.swo

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.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ path = "src/bin/ptree.rs"
2323
[[bin]]
2424
name = "epoll_example"
2525
path = "src/bin/testing/epoll.rs"
26+
[[bin]]
27+
name = "netlink_example"
28+
path = "src/bin/testing/netlink.rs"
2629

2730
[profile.release]
2831
lto = true
@@ -34,6 +37,7 @@ panic = "abort" # Save size. We don't need unwinding anyway.
3437
[dependencies]
3538
getopts = "0.2.15"
3639
nix = "0.12.0"
40+
libc = "0.2.48"
3741

3842
[package.metadata.deb]
3943
assets = [

src/bin/testing/netlink.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// Copyright 2019 Delphix
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
use nix::sys::socket::{socket, AddressFamily, SockAddr, SockType, SockFlag, bind};
18+
use nix::errno::Errno;
19+
20+
use std::fs::File;
21+
use std::os::raw::c_int;
22+
23+
extern crate nix;
24+
25+
const NETLINK_ROUTE: c_int = 0;
26+
27+
fn main() {
28+
// Use libc crate directly instead of nix because nix's 'socket' method doesn't have ability to
29+
// specify netlink socket protocol.
30+
let fd = unsafe {
31+
libc::socket(AddressFamily::Netlink as c_int,
32+
SockType::Datagram as c_int,
33+
NETLINK_ROUTE)
34+
};
35+
36+
let fd = Errno::result(fd).unwrap();
37+
38+
bind(fd, &SockAddr::new_netlink(0, 0));
39+
40+
// Signal parent process (the test process) that this process is ready to be observed by the
41+
// ptool being tested.
42+
File::create("/tmp/ptools-test-ready").unwrap();
43+
44+
// Wait for the parent finish running the ptool and then kill us.
45+
loop {}
46+
}
47+

src/ptools.rs

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -501,12 +501,18 @@ fn print_file(pid: u64, fd: u64, sockets: &HashMap<u64, SockInfo>) {
501501
// TODO we can print more specific information for epoll fds by looking at /proc/[pid]/fdinfo/[fd]
502502
match file_type {
503503
FileType::Posix(PosixFileType::Socket) => {
504-
// TODO what to do if there is no entry in /proc/net/tcp corresponding to the inode?
505-
// TODO use sshd as example
504+
// TODO We should read the 'system.sockprotoname' xattr for /proc/[pid]/fd/[fd] for
505+
// sockets. That way we can at least print the protocol even if we weren't able to find
506+
// any info for the socket in procfs.
506507
// TODO make sure we are displaying information that is for the correct namespace
507-
let sock_info = sockets.get(&stat_info.st_ino).unwrap(); // TODO add error msg saying not found (until we implement logic for handling IPv6)
508-
print_sock_type(sock_info.sock_type);
509-
print_sock_address(&sock_info);
508+
// TODO handle IPv6
509+
if let Some(sock_info) = sockets.get(&stat_info.st_ino) {
510+
print_sock_type(sock_info.sock_type);
511+
print_sock_address(&sock_info);
512+
} else {
513+
print!(" ERROR: failed to find info for socket with inode num {}\n",
514+
stat_info.st_ino);
515+
}
510516
},
511517
_ => {
512518
let path = fs::read_link(link_path).unwrap();
@@ -676,9 +682,6 @@ fn parse_ipv4_sock_addr(s: &str) -> Result<SocketAddr, ParseError> {
676682
Ok(SocketAddr::new(IpAddr::V4(addr), port))
677683
}
678684

679-
// TODO it isn't ideal to have to go through all of the info in
680-
// /proc/net/{tcp,tcp6,udp,udp6,raw,...} every time we want to the get the info for
681-
// a single socket. Is there a faster way to do this for a single socket?
682685
fn fetch_sock_info(pid: u64) -> Result<HashMap<u64, SockInfo>, Box<Error>> {
683686
let file = File::open(format!("/proc/{}/net/unix", pid)).unwrap();
684687
let mut sockets = BufReader::new(file)
@@ -699,6 +702,27 @@ fn fetch_sock_info(pid: u64) -> Result<HashMap<u64, SockInfo>, Box<Error>> {
699702
(inode, sock_info)
700703
}).collect::<HashMap<_,_>>();
701704

705+
let file = File::open(format!("/proc/{}/net/netlink", pid)).unwrap();
706+
let netlink_sockets = BufReader::new(file)
707+
.lines()
708+
.skip(1) // Header
709+
.map(|line| {
710+
let line = line.unwrap();
711+
let fields = line.split_whitespace().collect::<Vec<&str>>();
712+
let inode = fields[9].parse().unwrap();
713+
let sock_info = SockInfo {
714+
family: AddressFamily::Netlink,
715+
sock_type: SockType::Datagram,
716+
inode: inode,
717+
local_addr: None,
718+
peer_addr: None,
719+
peer_pid: None,
720+
};
721+
(inode, sock_info)
722+
}).collect::<HashMap<_,_>>();
723+
sockets.extend(netlink_sockets);
724+
725+
// procfs entries for tcp, udp, and raw sockets all use same format
702726
let parse_file = |file, s_type| {
703727
BufReader::new(file)
704728
.lines()

tests/common/mod.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//
2+
// Copyright 2018, 2019 Delphix
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
use std::path::{Path, PathBuf};
18+
use std::fs;
19+
use std::io;
20+
use std::process::{Command, Stdio};
21+
22+
// Find an executable produced by the Cargo build
23+
fn find_exec(name: &str) -> PathBuf {
24+
25+
// Find the path where Cargo has placed the executables by looking at this test process's
26+
// executable, which was also built by Cargo.
27+
let this_exec = std::env::current_exe().unwrap();
28+
let exec_dir = this_exec.parent().unwrap().parent().unwrap();
29+
30+
exec_dir.join(name)
31+
}
32+
33+
// Run a ptool against a sample process and return the stdout of the ptool
34+
pub fn run_ptool(tool: &str, test_proc: &str) -> String {
35+
36+
let signal_file = Path::new("/tmp/ptools-test-ready");
37+
if let Err(e) = fs::remove_file(signal_file) {
38+
if e.kind() != io::ErrorKind::NotFound {
39+
panic!("Failed to remove {:?}: {:?}", signal_file, e.kind())
40+
}
41+
}
42+
43+
let mut examined_proc = Command::new(find_exec(test_proc))
44+
.stdin(Stdio::null())
45+
.stderr(Stdio::inherit())
46+
.stdout(Stdio::inherit())
47+
.spawn()
48+
.unwrap();
49+
50+
// Wait for process-to-be-examined to be ready
51+
while !signal_file.exists() {
52+
if let Some(status) = examined_proc.try_wait().unwrap() {
53+
panic!("Child exited too soon with status {}", status)
54+
}
55+
}
56+
57+
let pfiles_output = Command::new(find_exec(tool))
58+
.arg(examined_proc.id().to_string())
59+
.stdin(Stdio::null())
60+
.output()
61+
.unwrap();
62+
let stderr = String::from_utf8_lossy(&pfiles_output.stderr);
63+
let stdout = String::from_utf8_lossy(&pfiles_output.stdout);
64+
assert_eq!(stderr, "");
65+
66+
examined_proc.kill().unwrap();
67+
68+
stdout.into_owned()
69+
}
70+

tests/epoll_test.rs

Lines changed: 19 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,27 @@
1-
use std::path::{Path, PathBuf};
2-
use std::fs;
3-
use std::io;
4-
use std::process::{Command, Stdio};
5-
6-
// Find an executable produced by the Cargo build
7-
fn find_exec(name: &str) -> PathBuf {
8-
9-
// Find the path where Cargo has placed the executables by looking at this test process's
10-
// executable, which was also built by Cargo.
11-
let this_exec = std::env::current_exe().unwrap();
12-
let exec_dir = this_exec.parent().unwrap().parent().unwrap();
13-
14-
exec_dir.join(name)
15-
}
1+
//
2+
// Copyright 2018, 2019 Delphix
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
mod common;
1618

1719
#[test]
18-
fn test_epoll() {
19-
20-
let signal_file = Path::new("/tmp/ptools-test-ready");
21-
if let Err(e) = fs::remove_file(signal_file) {
22-
if e.kind() != io::ErrorKind::NotFound {
23-
panic!("Failed to remove {:?}: {:?}", signal_file, e.kind())
24-
}
25-
}
26-
27-
let mut example_proc = Command::new(find_exec("epoll_example"))
28-
.stdin(Stdio::null())
29-
.stderr(Stdio::inherit())
30-
.stdout(Stdio::inherit())
31-
.spawn()
32-
.unwrap();
33-
34-
// Wait for example process to be ready
35-
while !signal_file.exists() {
36-
if let Some(status) = example_proc.try_wait().unwrap() {
37-
panic!("Child exited too soon with status {}", status)
38-
}
39-
}
40-
41-
let pfiles_output = Command::new(find_exec("pfiles"))
42-
.arg(example_proc.id().to_string())
43-
.stdin(Stdio::null())
44-
.output()
45-
.unwrap();
46-
let stderr = String::from_utf8_lossy(&pfiles_output.stderr);
47-
let stdout = String::from_utf8_lossy(&pfiles_output.stdout);
48-
assert_eq!(stderr, "");
20+
fn epoll_basic() {
21+
let stdout = common::run_ptool("pfiles", "epoll_example");
4922

5023
let pattern = "5: anon_inode(epoll)";
5124
if !stdout.contains(pattern) {
5225
panic!("String '{}' not found in command output:\n\n{}\n\n", pattern, stdout);
5326
}
54-
55-
example_proc.kill().unwrap();
5627
}

tests/netlink_test.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// Copyright 2019 Delphix
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
mod common;
18+
19+
#[test]
20+
fn netlink_basic() {
21+
let stdout = common::run_ptool("pfiles", "netlink_example");
22+
let lines = stdout.lines().collect::<Vec<&str>>();
23+
24+
//
25+
// We expect something along the lines of
26+
// ...
27+
// 3: S_IFSOCK mode:777 dev:0,9 ino:725134 uid:65433 gid:50 size:0
28+
// O_RDWR
29+
// SOCK_DGRAM
30+
// sockname: AF_NETLINK
31+
//
32+
let pattern = "3: S_IFSOCK";
33+
let split_lines = lines
34+
.split(|l| l.trim().starts_with(pattern))
35+
.collect::<Vec<_>>();
36+
37+
if split_lines.len() != 2 {
38+
panic!("String '{}' not found in command output:\n\n{}\n\n", pattern, stdout);
39+
}
40+
let fd_info = split_lines[1];
41+
42+
let pattern = "sockname: AF_NETLINK";
43+
if fd_info[2].trim() != pattern {
44+
panic!("String '{}' not found in command output:\n\n{}\n\n", pattern, fd_info.join("\n"));
45+
}
46+
}

0 commit comments

Comments
 (0)