Background
slirp4netns is a network emulator that uses sockets instead of routing: from the container's point of view, routing is done by its gateway, but actually the host isn't routing: slirp4netns just opens TCP and UDP sockets on the fly for each flow detected coming from the container to handle outgoing traffic.
Using strace on slirp4netns, one can see that currently it doesn't do what curl is doing: it doesn't bind to the interface using setsockopt(..., SO_BINDTODEVICE, ...) but instead binds the socket to the address on the interface. So the route isn't forced to use wg1 (SO_BINDTODEVICE will also force the use of the interface when there's no route left as if it had added a default route on it. Works without issue for layer 3 interfaces like WireGuard, a bit more problematic for layer 2 interfaces without default route with proper gateway on them) and this fails because there's no route to tell to use wg1.
Wrapper
An LD_PRELOAD wrapper can be used to work around the lack of the functionality in libslirp. Altering libslirp's slirp_bind_outbound() behavior would have been the better choice, but I chose instead to alter bind(2): frailer but easier to achieve.
The wrapper checks if a bind attempt (made by slirp4netns) to the IP address (which must be the address on the target interface) provided as environment variable was made, and in such case in addition also binds the socket to the interface using SO_BINDTODEVICE, which is OP's intended behavior, just like the host's curl command is doing. Of course should such bind happen for any other reason than slirp_bind_outbound()'s invocation then expect disruption.
Instructions
Requires a minimal development environment with gcc. Compile with this command:
gcc -o bindtodevicewrapper.so bindtodevicewrapper.c -shared -fPIC -O2 -ldl
the file bindtodevicewrapper.c below:
#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);
}
Requires jq to avoid having to guess the IP address on the interface, else one can do it manually for example with WRAPPER_INET=10.5.0.2 if 10.5.0.2 is the address on wg1. This is a single line (with exported variables only for the single command), split with \ for readability:
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
Tested working with IPv4 without DNS (not tested with DNS). Should also work with WRAPPER_INET6=... for IPv6 when using slirp4netns's outbound_addr6 but not tested. No privileged access required.