libc::recv在docker上没有返回值

3

我在运行在 macOS 上的 Docker 中使用 Rust 编写了一个简单的数据包捕获程序。

然而,libc::recv 函数没有返回任何结果。

代码文件为 src/main.rs。

use std::io;

fn main() -> io::Result<()> {
    let sock = unsafe {
        match libc::socket(
            libc::AF_PACKET,
            libc::SOCK_RAW,
            libc::ETH_P_ALL.to_be() as _,
        ) {
            -1 => Err(io::Error::last_os_error()),
            fd => Ok(fd),
        }
    }?;
    println!("sock: {}", sock);

    let mut buf = [0u8; 1024];
    loop {
        let len = unsafe {
            libc::recv(
                sock,
                (&mut buf[..]).as_mut_ptr() as *mut libc::c_void,
                buf.len(),
                0,
            )
        };
        println!("len: {}", len);
    }
}

Dockerfile


RUN apt-get update && apt-get install -y tcpdump

WORKDIR /app

COPY . .

ENTRYPOINT ["cargo", "run"]

运行命令

$ docker build . -t rust_cap && docker run -p 127.0.0.1:15006:15006/udp -it --rm --name="rust_cap" rust_cap

我尝试使用tcpdump检查是否有任何数据包发送到容器中,并使用strace检查系统调用,它们似乎是正确的。

其次,我使用C语言编写了与上述代码相同的代码,如下所示:

main.c

#include <stdio.h>
#include <sys/socket.h>
#include <net/ethernet.h>

int main(int argc, char **argv) {
    int sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (sock < 0) {
        perror("socket");
        exit(1);
    }
    printf("sock: %d\n", sock);

    u_char buf[1024];
    while (1) {
        int len = recv(sock, buf, sizeof(buf), 0);
        printf("len: %d\n", len);
    }
}

Dockerfile

FROM ubuntu:20.04

RUN apt-get update && apt-get install -y build-essential

COPY . .
RUN cc -o main main.c

ENTRYPOINT ["./main"]

运行命令

$ docker build . -t c_cap && docker run -p 127.0.0.1:15007:15007/udp -it --rm --name="c_cap" c_cap

这段 C 代码可以正确运行。(我能在标准输出中看到我发送的每条消息的长度len: xxx

为什么 Rust 代码中的 recv 没有返回呢?我漏了什么吗?


参考 strace 输出

Rust 代码中:

socket(AF_PACKET, SOCK_RAW, htons(0 /* ETH_P_??? */)) = 3
write(1, "sock: 3\n", 8sock: 3
)                = 8
recvfrom(3,

C语言中(接收缓冲区的部分被省略)

socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) = 3
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0
brk(NULL)                               = 0x5616f5ab4000
brk(0x5616f5ad5000)                     = 0x5616f5ad5000
write(1, "sock: 3\n", 8sock: 3
)                = 8
recvfrom(3,

我通过以下方式运行了strace。

$ docker run -it --cap-add sys_ptrace --entrypoint="/bin/bash" $IMAGE
$ cargo build && strace /app/target/debug/xxx
# or strace /main 

1
检查使用strace调用系统调用,它们似乎是正确的。你能详细说明一下吗?特别是关于strace的输出?如果它是“正确的”,那么你应该看到在Rust程序中发出并完成了recv。如果不是这样的话,你看到了什么?Rust和C程序的strace输出应该几乎相同。如果它们不同,你的问题就在于这种差异。如有疑问,请编辑问题以包括两个程序的相关部分strace输出。 - user4815162342
@user4815162342 感谢您的评论。我已经更新了它。在我的看来,它似乎是在recv处阻塞了两个。 - thara
1
你截取了strace输出的时间太早(或者附加得太晚),所以我们看不到套接字是如何创建的。(请对两个程序都保留这个输出。) - user4815162342
哦,我从另一个容器中运行了strace,结果缺少了它们。我已经更新了它。看起来我向socket(2)传递了非法协议... - thara
2
ETH_P_ALL 是一个类型为 c_int 的值 3,即 3i32。你需要将它转换为大端序,这样你就会得到类似于 0x03000000 的东西。htons 代表主机到网络的 _short_(即 u16),所以你最终应该得到 0x0300,这是一个完全不同的数字。要模拟 htons(ETH_P_ALL),你应该做类似于 (ETH_P_ALL as u16).to_be() as _ 的操作。 - user4815162342
@user4815162342 我自己解决了这个问题并且我自己发布了一个答案。非常感谢。 - thara
1个回答

1
我自己解决了这个问题。
传递给libc::socket的一种libc::ETH_P_ALL类型是 c_int别名为i32
将libc :: ETH_P_ALL作为i32转换为大端是不正确的,作为libc::socket参数。
相反,必须将libc :: ETH_P_ALL作为u16转换为大端。
use std::io;

fn main() -> io::Result<()> {
    let sock = unsafe {
        match libc::socket(
            libc::AF_PACKET,
            libc::SOCK_RAW,
-            libc::ETH_P_ALL.to_be() as _,
+            (libc::ETH_P_ALL as u16).to_be() as _,
        ) {
            -1 => Err(io::Error::last_os_error()),
            fd => Ok(fd),
        }
    }?;
    println!("sock: {}", sock);

    let mut buf = [0u8; 1024];
    loop {
        let len = unsafe {
            libc::recv(
                sock,
                (&mut buf[..]).as_mut_ptr() as *mut libc::c_void,
                buf.len(),
                0,
            )
        };
        println!("len: {}", len);
    }
}

FYI,(供您参考)
use libc;

fn main() {
    assert_eq!(0x0003, libc::ETH_P_ALL);
    assert_eq!(0x3000000, libc::ETH_P_ALL.to_be());  // <- incorrect in the question
    assert_eq!(0x0003, libc::ETH_P_ALL as u16);
    assert_eq!(0x300, (libc::ETH_P_ALL as u16).to_be());  // <- correct
}

你为什么要首先将它转换或强制转换呢? - Shepmaster
1
@Shepmaster 因为BSD网络API要求使用"网络序"(大端序),所以建议在这种情况下使用nix。但是,nix目前还不支持 ETH_P_ALL(https://github.com/nix-rust/nix/issues/854)。 - user4815162342
@user4815162342 那为什么 libc::AF_PACKET 或者 libc::SOCK_RAW 没有被同样转换呢? - Shepmaster
1
@Shepmaster libc 提供的常量只是镜像 C 标头文件中定义的常量,因此它们不能被预转换,因为它们可能被用于不希望以网络顺序进行转换的地方,或者在不同整数宽度上进行转换(也有 htonl)。无论好坏,C 中需要完全相同的转换,并且如果调用错误的转换函数,则可能出现相同的错误。如果 OP 在他们的 C 代码片段中调用了 htonl(ETH_P_ALL) 而不是 htons(ETH_P_ALL),他们将面临相同的问题。 - user4815162342
1
@thara 你可以使用from_raw_fd()将套接字转换为File,然后在所有安全的 Rust 中继续使用它,例如调用read()等。 - user4815162342

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接