使用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, ...)
而是将套接字绑定到接口上的地址。所以该路线不强制使用工作组1 (SO_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一起使用slirp4netns但outbound_addr6
未经测试。无需特权访问。