Score:1

Forward port to a VM via iptables from same bridge

ms flag

I have running a proxmox server with two VMs which are connected to the bridge interface vmbr0:

  • Host: 10.10.10.1 (extern IP: 123.123.123.123)
  • VM1: 10.10.10.2
  • VM2: 10.10.10.3

On VM2 a load balancer is running on port 443 (and yes I want to have it there and not on the host machine). I added a prerouting iptables rule to forward all incoming traffic on port 443 to VM2. This rule is working and allows to connect from "the internet" on port 443 (on VM2). I also added an output rule on the public IP to forward to VM2 so the host can connect using the external IP to the load balancer of VM2.

An open problem is now connecting from VM1 to VM2 via the extern IP (123.123.123.123) but it cannot connect:

# from VM1
$ curl -v https://123.123.123.123
*   Trying 123.123.123.123:443...
^C

If I use the internal IP directly (https://10.10.10.3) it is working. I mean I could change the /etc/hosts but this is not the ideal way. So do you have any suggestions how I can fix that?

Outputs from the host machine (I removed unnecessary parts to minimize the output):

$ ip a
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
2: enp0s31f6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 01:01:01:01:01:01 brd ff:ff:ff:ff:ff:ff
    inet 123.123.123.123/26 scope global enp0s31f6
       valid_lft forever preferred_lft forever
3: vmbr0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 01:01:01:01:01:01 brd ff:ff:ff:ff:ff:ff
    inet 10.10.10.1/24 scope global vmbr0
       valid_lft forever preferred_lft forever


$ iptables -t nat -L -v -n
Chain PREROUTING (policy ACCEPT 1697 packets, 96837 bytes)
 pkts bytes target     prot opt in     out     source               destination
  149  8924 DNAT       tcp  --  *      *       0.0.0.0/0            123.123.123.123      tcp dpt:443 to:10.10.10.3:443

Chain INPUT (policy ACCEPT 784 packets, 36637 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 10 packets, 723 bytes)
 pkts bytes target     prot opt in     out     source               destination
    3   180 DNAT       tcp  --  *      *       0.0.0.0/0            123.123.123.123      tcp dpt:443 to:10.10.10.3:443

Chain POSTROUTING (policy ACCEPT 534 packets, 39072 bytes)
 pkts bytes target     prot opt in     out     source               destination
1908K  138M MASQUERADE  all  --  *      enp0s31f6  10.10.10.0/24        0.0.0.0/0


$ iptables -L -v -n             
Chain INPUT (policy ACCEPT 8078 packets, 834K bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain FORWARD (policy ACCEPT 70853 packets, 12M bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 7079 packets, 926K bytes)
 pkts bytes target     prot opt in     out     source               destination 


$ cat /proc/sys/net/ipv4/ip_forward
1
Score:1
cl flag
A.B

It's a typical case of NAT hairpinning. When the client (VM1 or even VM2) is on the same LAN as the server (VM2), the flow becomes asymmetrical: the reply will bypass the part doing NAT, so replies are not un-DNAT-ed and the client receives a reply from an address (VM2) it didn't ask anything to (instead of public Host) and is rejected.

The easiest way to overcome this is to do SNAT in addition to DNAT when this case happens. So in addition to:

1908K 138M MASQUERADE all -- * enp0s31f6 10.10.10.0/24 0.0.0.0/0

Note that for this case the outgoing interface is not the public-facing interface but the interface toward VM2 (it's vmbr0).

One could add (here with a specific filter to port 443, this filter can be removed if the outcome is intended whatever the service):

iptables -t nat -A POSTROUTING -s 10.10.10.0/24 -d 123.123.123.123 -p tcp --dport 443 -j MASQUERADE

UPDATE: above rule doesn't work because the (initial) packet's destination 123.123.123.123 got changed to 10.10.10.3 at nat/PREROUTING. The one below would work instead (it queries Netfilter about the initial destination before DNAT happened):

iptables -t nat -A POSTROUTING -s 10.10.10.0/24 -m conntrack --ctorigdst 123.123.123.123 -p tcp --dport 443 -j MASQUERADE

but considering this is the least useful example and needlessly complex, let's just forget it.

Instead of above, especially if the host were to have multiple IP public IP addresses and/or multiple internal LANs (each with a client and a server in the same place), it's more useful to query Netfilter to know when this has to be done: when DNAT already happened first (and then it doesn't even require to specify a port to handle all cases):

iptables -t nat -A POSTROUTING -m conntrack --ctstate DNAT -j MASQUERADE

Instead of MASQUERADE, one can choose any arbitrary source NAT to a network prefix (iptables' NETMAP) that matches the server's default route, so replies are correctly going through the host and correctly un-DNAT-ed. This allows to have logs on the server that can also be used to know which specific internal client did the query. As it's useful per-LAN, the LAN should be stated again and the rule used multiple times for multiple LANs (which is not OP's current case, but this answer aims to consider more cases).

For example if, not being used anywhere else, the chosen fictive network is 10.11.10.0/24 (which will be part of the default route of VM2), instead of above use this:

iptables -t nat -A POSTROUTING -s 10.10.10.0/24 -m conntrack --ctstate DNAT -j NETMAP --to 10.11.10.0/24

VM2's logs can now show 3 cases:

  • query from public IP address: actual client address, including the host if it chose to query to 123.123.123.123 (with the socket initially choosing from 123.123.123.123 so this is the source) before being redirected by nat/OUTPUT.

  • query from an address in 10.10.10.0/24: direct query from LAN (including a query from the host) to VM2 without using 123.123.123.123 as destination.

  • query from an address in 10.11.10.0/24: query from LAN to 123.123.123.123 which was redirected. VM1 will always appear as 10.11.10.2 source because NETMAP keeps the host part intact.

    This last part will also allow to distinguish if VM1 did the query, or say a new VM VM3 initially at 10.10.10.4 seen as 10.11.10.4.

ChristophLSA avatar
ms flag
Thanks for the detailed explanation. The first command didn't work for me but the last two are working fine. :)
A.B avatar
cl flag
A.B
I'm a bit surprised the first command didn't work, it should have. Anyway, good it's working. Also, as a remark, use `iptables-save -c` to give your ruleset in future questions, it's way easier to read back (and to reproduce when needed).
A.B avatar
cl flag
A.B
Oh I found why it's not working, my bad. it's because the destination is now 10.10.10.3 and no more 123.123.123.123. This indeed makes this 1st answer useless (I could use `-m conntrack --ctorigdst ...` to artificially make it work, but it's better to just delete it).
I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.