如何配置 podman 容器以仅使用我的wireguard 接口?

如何配置 podman 容器以仅使用我的wireguard 接口?

使用wg-quick该设置,我成功地在 Linux bookworm 机器上Table=off设置了一个非默认界面,该界面按我的预期工作:wg1

  • curl ifconfig.co返回我的真实IP。
  • curl --interface wg1 ifconfig.co返回我的wireguard 服务器的IP。

我想知道我现在可以运行一个无根 podman 容器来wg1与外界通信。默认情况下,podman容器使用我的eth0接口;podman run -it docker.io/curlimages/curl:latest ifconfig.co返回我的真实IP,这就是我想要的。但我找不到如何在容器内做与主机上相同的事情。

我以为我可以逃脱podman run --network=slirp4netns:outbound_addr=wg1 -it docker.io/curlimages/curl:latest ifconfig.co但这又回来了curl: (6) Could not resolve host: ifconfig.co。这似乎不是(只是?) DNS 问题,因为使用 ifconfig.co 的 IP 并不能更好地工作:curl: (7) Failed to connect to 172.64.110.32 port 80 after 75840 ms: Couldn't connect to server

我想我必须自己设置一个 podman 网络,或者一个 pod,或者类似的东西,但是在我找到的许多教程中迷失了方向,并且从未设法获得我想要的结果:curl从内部通过我的wireguard 服务器无根容器。

在此先感谢您的帮助。

答案1

背景

slirp4netns是一个使用套接字而不是路由的网络模拟器:从容器的角度来看,路由是由其网关完成的,但实际上主机并不进行路由:slirp4netns只是为检测到的来自容器的每个流动态打开 TCP 和 UDP 套接字来处理传出流量。

使用 straceslirp4netns,可以看到目前它并没有做curl正在做的事情:它没有使用绑定到接口setsockopt(..., SO_BINDTODEVICE, ...)而是将套接字绑定到接口上的地址。所以该路线不强制使用工作组1SO_BINDTODEVICE当没有剩余路由时,也会强制使用该接口,就像在其上添加了默认路由一样。对于 WireGuard 等第 3 层接口来说,没有问题,对于没有默认路由且打开了正确网关的第 2 层接口来说,问题有点多他们)这会失败,因为没有路线可以告诉使用工作组1

包装纸

一个LD_PRELOAD包装器可用于解决以下功能的缺乏库斯利普。改变库斯利普slirp_bind_outbound()行为本来是更好的选择,但我选择改变bind(2):较脆弱但更容易实现。

包装器检查是否对slirp4netns作为环境变量提供的 IP 地址(必须是目标接口上的地址)进行了绑定尝试(由 进行),在这种情况下,此外还使用 将套接字绑定到接口SO_BINDTODEVICE,即OP 的预期行为,就像主机的行为一样卷曲命令正在做。当然,如果这种绑定由于slirp_bind_outbound()调用之外的任何其他原因发生,那么预计会发生中断。

指示

需要最低限度的开发环境海湾合作委员会。使用此命令进行编译:

gcc -o bindtodevicewrapper.so bindtodevicewrapper.c -shared -fPIC -O2 -ldl

下面的文件bindtodevicewrapper.c

#define _GNU_SOURCE

#include <dlfcn.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <net/if.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#include <string.h>
#include <stdlib.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
    static int init=0;
    static int (*orig_bind)(int, const struct sockaddr *, socklen_t);
    static struct in_addr ipv4bindtodevice;
    static struct in6_addr ipv6bindtodevice;
    static char devname[IFNAMSIZ];

    if (!init) {
        if ((orig_bind=dlsym(RTLD_NEXT,"bind")) == NULL) {
            return -1;
        }
        memset(&ipv4bindtodevice, 0xff, sizeof ipv4bindtodevice);   /* invalid for binding */
        memset(&ipv6bindtodevice, 0xff, sizeof ipv6bindtodevice);   /* invalid for binding */
        if (getenv("WRAPPER_BINDTODEVICE")!=NULL) {
            strncpy(devname, getenv("WRAPPER_BINDTODEVICE"), sizeof devname);
            if(getenv("WRAPPER_INET")!=NULL)
                inet_pton(AF_INET, getenv("WRAPPER_INET"),&ipv4bindtodevice);
            if(getenv("WRAPPER_INET6")!=NULL)
                inet_pton(AF_INET6, getenv("WRAPPER_INET6"),&ipv6bindtodevice);
        }
        init=1;
    }

    if (devname != NULL) {
        if (addr->sa_family == AF_INET) {
            if (!memcmp(&((struct sockaddr_in *)addr)->sin_addr, &ipv4bindtodevice, sizeof ipv4bindtodevice))
                setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, devname, strlen(devname)+1);
        }
        else if (addr->sa_family == AF_INET6) {
            if (!memcmp(&((struct sockaddr_in6 *)addr)->sin6_addr, &ipv6bindtodevice, sizeof ipv6bindtodevice))
                setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, devname, strlen(devname)+1);
        }
    }

    return orig_bind(sockfd, addr, addrlen);
}

需要jq避免猜测接口上的 IP 地址,否则可以手动执行此操作,例如WRAPPER_INET=10.5.0.2如果 10.5.0.2 是接口上的地址工作组1。这是一行(仅包含单个命令的导出变量),\为了便于阅读,用以下命令分隔:

WRAPPER_BINDTODEVICE=wg1 \
WRAPPER_INET=$(ip -4 -json addr show dev wg1 | jq -r '.[].addr_info[0].local') \
LD_PRELOAD=./bindtodevicewrapper.so \
podman run --network=slirp4netns:outbound_addr=wg1 -it docker.io/curlimages/curl:latest ifconfig.co

已测试在没有 DNS 的情况下使用 IPv4(未使用 DNS 进行测试)。使用时还应该与WRAPPER_INET6=...IPv6一起使用slirp4netnsoutbound_addr6未经测试。无需特权访问。

相关内容