【问题标题】:libc::recv does not return on dockerlibc::recv 不会在 docker 上返回
【发布时间】:2021-06-01 14:59:03
【问题描述】:

我在 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

为什么recv 不会在 Rust 代码中返回? 我错过了什么?


参考。 strace 输出

在锈中

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 

【问题讨论】:

  • 检查是否通过 strace 调用系统调用,它们似乎是正确的。 - 您能否详细说明这一点,尤其是 strace 的输出?如果它是“正确的”,那么您应该会看到 recv 在 Rust 程序中发出并完成。是这样吗?如果不是,你看到了什么do? Rust 和 C 程序的strace 输出应该几乎相同。如果不是,那么您的问题就在于差异。如有疑问,请编辑问题以包含两个程序的strace 输出的相关部分。
  • @user4815162342 感谢您的评论。我更新了它。在我看来,他们俩都被recv 屏蔽了。
  • 您过早地切断了 strace 输出(或连接得太晚),所以我们看不到套接字是如何创建的。 (请为这两个程序保留它。)
  • 哦,我从另一个容器运行了 strace,但我错过了它们。我更新了它。看来我将非法协议传递给套接字(2)......
  • ETH_P_ALLc_int 类型的值3,即3i32。你将它转换为大端,这样你就得到了 0x03000000 之类的东西。 htons 代表主机到网络 short(即 u16),因此您最终应该得到 0x0300,一个完全不同的数字。要模拟htons(ETH_P_ALL),您可能应该执行类似(ETH_P_ALL as u16).to_be() as _ 的操作。

标签: docker rust libc


【解决方案1】:

我自己解决了这个问题。

传递给libc::socketlibc::ETH_P_ALL 类型是c_int aliasing to i32

将 libc::ETH_P_ALL as i32 转换为 big endian 作为 libc::socket 参数是不正确的。

相反,它必须将libc::ETH_P_ALL as 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);
    }
}

仅供参考,

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 因为 BSD 网络 API 需要“网络顺序”(大端)的常量。在其他情况下,我建议使用 nix,但 nix doesn't support ETH_P_ALL yet.
  • @user4815162342 那么为什么libc::AF_PACKETlibc::SOCK_RAW 没有进行类似的转换呢?
  • @Shepmaster libc 提供的常量只是反映了 C 头文件中定义的那些常量,因此它们不能被预先转换,因为它们可能用于不需要按网络顺序转换的地方,或者转换是在不同的整数宽度上完成的(还有htonl)。无论好坏,在 C 中都需要完全相同的转换,如果调用错误的转换函数,可能会出现相同的错误。如果 OP 在他们的 C sn-p 中调用 htonl(ETH_P_ALL) 而不是 htons(ETH_P_ALL),他们将面临同样的问题。
  • @thara 您可以使用from_raw_fd() 将套接字转换为File,然后在初始套接字创建后继续在安全的Rust 中使用它,例如拨打read(),等等。
猜你喜欢
  • 1970-01-01
  • 2021-11-19
  • 1970-01-01
  • 1970-01-01
  • 2018-11-19
  • 2015-04-16
  • 2011-02-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多