You shouldn't need any custom NAT rules.
Traffic exiting the containers will hit the MASQUERADE
rule that Docker installs for the network (e.g., -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
), so your virtual machines will see traffic originating from the libvirt bridge (192.168.122.1
).
Configuration
I have a Docker container named example-container
attached to docker0
:
$ docker ps
[...]
0ddd3554466b alpine "/bin/sh" 5 seconds ago Up 4 seconds example-container
That container has the following network configuration:
/ # ip addr show eth0
2642: eth0@if2643: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fdc9:87ad:f15:5bea:0:242:ac11:2/64 scope global flags 02
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever
I have virtual machine named example-vm
attached to the libvirt default
network:
$ virsh list
Id Name State
----------------------------
2 example-vm running
The virtual machine has the following network configuration:
[root@localhost ~]# ip addr show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:30:7b:a6 brd ff:ff:ff:ff:ff:ff
altname enp1s0
inet 192.168.122.70/24 brd 192.168.122.255 scope global dynamic noprefixroute eth0
valid_lft 3548sec preferred_lft 3548sec
inet6 fe80::2ccd:72ed:1fea:ccda/64 scope link noprefixroute
valid_lft forever preferred_lft forever
From the container to the vm
From inside the container, I am able to ping
the virtual machine:
/ # ping -c2 192.168.122.70
PING 192.168.122.70 (192.168.122.70): 56 data bytes
64 bytes from 192.168.122.70: seq=0 ttl=63 time=0.262 ms
64 bytes from 192.168.122.70: seq=1 ttl=63 time=0.286 ms
--- 192.168.122.70 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.262/0.274/0.286 ms
If I run tcpdump
inside the virtual machine while doing this, I see:
[root@localhost ~]# tcpdump -i eth0 -n icmp
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
07:23:38.661246 IP 192.168.122.1 > 192.168.122.70: ICMP echo request, id 9, seq 0, length 64
07:23:38.661277 IP 192.168.122.70 > 192.168.122.1: ICMP echo reply, id 9, seq 0, length 64
07:23:39.661247 IP 192.168.122.1 > 192.168.122.70: ICMP echo request, id 9, seq 1, length 64
07:23:39.661261 IP 192.168.122.70 > 192.168.122.1: ICMP echo reply, id 9, seq 1, length 64
From the vm to the container
Similarly, I can ping the container from the virtual machine:
[root@localhost ~]# ping -c2 172.17.0.2
PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
64 bytes from 172.17.0.2: icmp_seq=1 ttl=63 time=0.149 ms
64 bytes from 172.17.0.2: icmp_seq=2 ttl=63 time=0.127 ms
--- 172.17.0.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1035ms
rtt min/avg/max/mdev = 0.127/0.138/0.149/0.011 ms
And tcpdump
in the container shows:
/ # tcpdump -i eth0 -n
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
12:24:48.690361 IP 172.17.0.1 > 172.17.0.2: ICMP echo request, id 2, seq 1, length 64
12:24:48.690373 IP 172.17.0.2 > 172.17.0.1: ICMP echo reply, id 2, seq 1, length 64
12:24:49.723189 IP 172.17.0.1 > 172.17.0.2: ICMP echo request, id 2, seq 2, length 64
12:24:49.723204 IP 172.17.0.2 > 172.17.0.1: ICMP echo reply, id 2, seq 2, length 64
Netfilter rules
This works because Docker installs the following rule:
host# iptables -t nat -S POSTROUTING
-P POSTROUTING ACCEPT
[...]
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
[...]
When traffic exits the container, it hits the MASQUERADE
rule. Since the libvirt virbr0
interface is the egress interface for that packet, it gets rewritten to the address of the bridge, which is why in the tcpdump
output in the vm we see the ping
requests originating from 192.168.122.1.
Similarly, libvirt
installs the following rules:
host# iptables -t nat -S LIBVIRT_PRT
[...]
-A LIBVIRT_PRT -s 192.168.122.0/24 -d 224.0.0.0/24 -j RETURN
-A LIBVIRT_PRT -s 192.168.122.0/24 -d 255.255.255.255/32 -j RETURN
-A LIBVIRT_PRT -s 192.168.122.0/24 ! -d 192.168.122.0/24 -p tcp -j MASQUERADE --to-ports 1024-65535
-A LIBVIRT_PRT -s 192.168.122.0/24 ! -d 192.168.122.0/24 -p udp -j MASQUERADE --to-ports 1024-65535
-A LIBVIRT_PRT -s 192.168.122.0/24 ! -d 192.168.122.0/24 -j MASQUERADE
[...]
These accomplish the same thing; requests from the virtual machine to the container get rewritten to the address of the docker0
bridge.