So I have a work-around, that uses namespaces. Basically I create a namespace that only has an IPv4 address and NAT that to my LAN. So anything running in that namespace will think it only has an IPv4 address.
In my case I have br-lan as a bridge for my LAN access so the setup for this namespace would be
ip netns del ip4only
ip netns add ip4only
ip link add ip4only-root type veth peer name ip4only-ns
ip link set ip4only-ns netns ip4only
ip addr add 192.168.200.1/24 dev ip4only-root
ip link set ip4only-root up
ip netns exec ip4only ip addr add 192.168.200.2/24 dev ip4only-ns
ip netns exec ip4only ip link set ip4only-ns up
ip netns exec ip4only ip link set lo up
ip netns exec ip4only ip route add default via 192.168.200.1
echo 1 > /proc/sys/net/ipv4/ip_forward
# Flush forward rules, policy DROP by default.
iptables -P FORWARD DROP
iptables -F FORWARD
# Flush nat rules.
iptables -t nat -F
# Enable masquerading of 192.168.200.0
iptables -t nat -A POSTROUTING -s 192.168.200.0/255.255.255.0 -o br-lan -j MASQUERADE
# Allow forwarding between br-lan and ip4only-root
iptables -A FORWARD -i br-lan -o ip4only-root -j ACCEPT
iptables -A FORWARD -o br-lan -i ip4only-root -j ACCEPT
So now we can see
% sudo ip netns exec ip4only ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
21: ip4only-ns@if22: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 86:58:d8:c4:b7:47 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 192.168.200.2/24 scope global ip4only-ns
valid_lft forever preferred_lft forever
inet6 fe80::8458:d8ff:fec4:b747/64 scope link
valid_lft forever preferred_lft forever
Yes it does look like there are inet6 addresses but these are link-local so aren’t used.
Now I don’t want to use sudo each time, so I created a wrapper that enters the name space, and uses capabilities for the permissions.
% cat ip4only.c
// ip4only
// gcc -o ip4only ip4only.c
// sudo setcap cap_sys_admin+ep ./ip4only
//
// Now we can do "ip4only command" (eg "ip4only ip addr")
// and it will run in the ip4only network namespace
// We need this for setns()
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#define errExit(msg) { perror(msg); exit(EXIT_FAILURE); }
int main(int argc, char *argv[])
{
int fd;
if (argc < 2)
{
fprintf(stderr, "%s cmd args...\n", argv[0]);
exit(EXIT_FAILURE);
}
// To set a namespace we need to have an open file handle.
// Network namespaces live in /var/run/ns so that's easy
fd = open("/var/run/netns/ip4only", O_RDONLY);
if (fd == -1)
errExit("open");
// Join the namespace
if (setns(fd, 0) == -1)
errExit("setns");
// Run the specified command
execvp(argv[1], &argv[1]);
// If we got here, there's an error!
errExit("execvp");
}
So now I can do
% ip4only speedtest
Speedtest by Ookla
Server: i3D.net - Newark, NJ (id: 43263)
ISP: Verizon Fios
And we can see it’s picking up my ISP’s IPv4 address and not HE Tunnel.
This wrapper might be useful for other purposes as well!