Skip to content

Using Source NAT option on Private Gateway does not work #2680

Closed

Description

@Slair1
ISSUE TYPE
  • Bug Report
COMPONENT NAME
VPC (Private Gateway Source NAT)
CLOUDSTACK VERSION
At least: 4.9, 4.10, and 4.11
CONFIGURATION

When you use the Source NAT feature of Private Gateways on a VPC. This should Source NAT all traffic from CloudStack VMs going towards IPs reachable through Private Gateways

OS / ENVIRONMENT

I happen to be running KVM, but shouldn't be hypervisor specific. it is an issue in the SystemVM / VPC Router

SUMMARY

There is a bug in the Private Gateway functionality, when Source NAT is enabled for the Private Gateway. When the SNAT is added to iptables, it has the source CIDR of the private gateway subnet. Since no VMs live in that private gateway subnet, the SNAT doesn’t work.

STEPS TO REPRODUCE

Below is an example:

  • VMs have IP addresses in the 10.0.0.0/24 subnet.
  • The Private Gateway address is 10.101.141.2/30

In the outputs below, the SOURCE field for the new SNAT (eth3) only matches if the source is 10.101.141.0/30. Since the VM has an IP address in 10.0.0.0/24, the VMs don’t get SNAT’d as they should when talking across the private gateway. The SOURCE should be set to ANYWHERE.

BEFORE ADDING PRIVATE GATEWAY
Chain POSTROUTING (policy ACCEPT 1 packets, 52 bytes)
pkts bytes target     prot opt in     out     source               destination
    2   736 SNAT       all  --  any    eth2    10.0.0.0/24          anywhere             to:10.0.0.1
   16  1039 SNAT       all  --  any    eth1    anywhere             anywhere             to:46.99.52.18
EXPECTED RESULTS
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target     prot opt in     out     source               destination
    0     0 SNAT       all  --  any    eth3    anywhere             anywhere             to:10.101.141.2
    2   736 SNAT       all  --  any    eth2    anywhere             anywhere             to:10.0.0.1
   23  1515 SNAT       all  --  any    eth1    anywhere             anywhere             to:46.99.52.18
ACTUAL RESULTS
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target     prot opt in     out     source               destination
    0     0 SNAT       all  --  any    eth3    10.101.141.0/30      anywhere             to:10.101.141.2
    2   736 SNAT       all  --  any    eth2    10.0.0.0/24          anywhere             to:10.0.0.1
   23  1515 SNAT       all  --  any    eth1    anywhere             anywhere             to:46.99.52.18
SUGGESTED FIX

It looks like CsAddress.py treats the creation of the Private Gateway SNAT as if it is a GUEST network, which works fine, except for the SNAT problem shown above. Here is the code from MASTER (line 479 is SNAT rule):

if self.get_type() in ["guest"]:
...
...
    self.fw.append(["nat", "front",
        "-A POSTROUTING -s %s -o %s -j SNAT --to-source %s" %
        (guestNetworkCidr, self.dev, self.address['public_ip'])])

I am thinking we just change that to the following. I can’t think of any reason we need the source/guest CIDR specified:

if self.get_type() in ["guest"]:
...
...
    self.fw.append(["nat", "front",
        "-A POSTROUTING -o %s -j SNAT --to-source %s" %
        (self.dev, self.address['public_ip'])])
More Information

Here is a link I found that further documents the Private Gateway Source NAT feature:

https://cwiki.apache.org/confluence/display/CLOUDSTACK/Source+NAT+on+private+gateway

Notice, how in the SNAT shown it does not specify a source guest network CIDR - my above fix matches the example in the link above.

Activity

  1. ustcweizhou commented on May 30, 2018

    @ustcweizhou
    Contributor

    @Slair1 Thanks for you reply on jira.
    You are correct. VPC connects the entire network in DC via private gateway, not entire internet. The routes (default and to static route) are good.

    Looking at the code, I think the iptables rules should be added by

            if self.get_type() == "public" and self.config.is_vpc() and method == "add":
                if self.address["source_nat"]:
                    vpccidr = cmdline.get_vpccidr()
                    self.fw.append(
                        ["filter", 3, "-A FORWARD -s %s ! -d %s -j ACCEPT" % (vpccidr, vpccidr)])
                    self.fw.append(
                        ["nat", "", "-A POSTROUTING -j SNAT -o %s --to-source %s" % (self.dev, self.address['public_ip'])])
    

    see line 584 to line 590 in
    https://github.com/apache/cloudstack/blob/master/systemvm/debian/opt/cloud/bin/cs/CsAddress.py

    Could you post /etc/cloudstack/ips.json in the virtual router ?

  2. added this to the 4.11.1.0 milestone on May 31, 2018
  3. yadvr commented on May 31, 2018

    @yadvr
    Member

    @PaulAngus @DaanHoogland I've tentatively marked this against the 4.11.1.0 milestone, given the problem may have been in ACS since version 4.9, please triage if this should be moved to 4.11.2.0 instead.

  4. Slair1 commented on Jun 1, 2018

    @Slair1
    ContributorAuthor

    @ustcweizhou Could you take a look at this pull request?

    #2681

    I made the change in a different area of CsAddress.py. CloudStack treats the Private Gateway creation as a network type of Guest not Public. The section you updated updates if the type is Public. The code i updated is for when the type is Guest. We are currently running with our patch in place and it seems to work well, however, would like some additional review on the PR.

    Thanks!!

  5. ustcweizhou commented on Jun 2, 2018

    @ustcweizhou
    Contributor

    @Slair1 I saw your PR.

    I am using 4.7.1. The ip in private gateway network is Public, not Guest. It seems to be a regression issue.
    If the type is Guest, your PR should fix the issue. However, it will impact all guest networks (vpc tiers), I am not sure if it will lead to security issues.

    Is it good to add a check before the code ?

    if self.address["source_nat"]:
        self.fw.append(new rule)
    else:
        self.fw.append(old rule)
    

    the source_nat should be true for the ip in private gateway network, but false for ips in guest networks. This is why I asked you if you can post the /etc/cloudstack/ips.json .

  6. yadvr commented on Jun 7, 2018

    @yadvr
    Member

    @Slair1 can you reply to @ustcweizhou 's comment ^^ ?

  7. yadvr commented on Jun 8, 2018

    @yadvr
    Member

    I went ahead and applied @ustcweizhou 's advised fix on your PR @Slair1 /cc @PaulAngus @DaanHoogland

  8. DaanHoogland commented on Jun 8, 2018

    @DaanHoogland
    Contributor

    noticed, lgtm

  9. Slair1 commented on Jun 8, 2018

    @Slair1
    ContributorAuthor

    @rhtyd @ustcweizhou @DaanHoogland
    Actually, thinking about this some more, why are we even creating a SNAT for traffic destined towards a guest network? Looking at the existing functionality:

    Chain POSTROUTING (policy ACCEPT 1 packets, 52 bytes)
    pkts bytes target     prot opt in     out     source               destination
        2   736 SNAT       all  --  any    eth2    10.0.0.0/24          anywhere             to:10.0.0.1
       16  1039 SNAT       all  --  any    eth1    anywhere             anywhere             to:46.99.52.18
    

    eth2 is the interface connected to the guest network. That SNAT doesn't even make sense. Nothing should have a source address of 10.0.0.0/24 if it is destined to the guest network (10.0.0.0/24). The only traffic with a source of 10.0.0.0/24 would be traffic originating from the vRouter itself - which would already have a source IP of 10.0.0.1.

    Actually, we we shouldn't event have a SNAT at all, we should never be NAT'ing traffic destined towards a Guest network... The only exception to that is when using a Private Gateways with SNAT enabled - since a Private Gateway is treated as a Guest network.

    Wouldn't this make more sense?

                if self.address["source_nat"]:
                    self.fw.append(["nat", "front",
                                "-A POSTROUTING -o %s -j SNAT --to-source %s" %
                                (self.dev, self.address['public_ip'])]
    

    We don't even need this code at all:

                else:
                    self.fw.append(["nat", "front",
                                 "-A POSTROUTING -s %s -o %s -j SNAT --to-source %s" %
                                 (guestNetworkCidr, self.dev, self.address['public_ip'])])
    

    Thoughts?

  10. ustcweizhou commented on Jun 9, 2018

    @ustcweizhou
    Contributor

    I agree with @Slair1

  11. yadvr commented on Jun 11, 2018

    @yadvr
    Member

    @Slair1 /cc @ustcweizhou what if you want to acquire a public IP to attach to a VM directly (NAT), and in case one VM within the same network/tier want to access other VM by a public NAT-ed IP?

  12. Slair1 commented on Jun 11, 2018

    @Slair1
    ContributorAuthor

    @rhtyd /cc @ustcweizhou

    I think you're right @rhtyd, it isn't obvious, but when a guest VM attempts to communicate with another guest VM's public IP, the packet flows into iptables and then hairpins back out. Without the SNAT in place, the reply from the destination VM would go straight to source VM. Which breaks any stateful, protocol such as TCP, since the source IP in the reply packet will be the private IP of the original destination VM while the original source VM is expecting the source IP to be the public IP... If that makes sense... if not let me know and i can explain more.

    I'll take a look and update the code some!

  13. Slair1 commented on Jun 12, 2018

    @Slair1
    ContributorAuthor

    @rhtyd /cc @ustcweizhou

    It looks like that old SNAT in CsAddress.py (before this PR) was allowing the static NATs hair-pinning to work. However, that seems to only be because the actual static NAT hairpin code in configure.py wasn't working correctly because it had eth0 hard-coded in it instead of dynamic determining the guest network interface. So, I left our CsAddress.py as it (see current PR #2681) and modified configure.py to fix the hairpin SNAT. Please see the following and let me know your thoughts. It may need some others to do some additional testing also.

    Before Changes to configure.py

    IP Address and Interfaces

    root@r-336-VM:~# ip add
    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN
        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
        inet 127.0.0.1/8 scope host lo
    2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
        link/ether 0e:00:a9:fe:01:dd brd ff:ff:ff:ff:ff:ff
        inet 169.254.1.221/16 brd 169.254.255.255 scope global eth0
    3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
        link/ether 06:18:90:00:00:30 brd ff:ff:ff:ff:ff:ff
        inet 46.99.52.18/26 brd 46.99.52.63 scope global eth1
        inet 46.99.52.19/26 brd 46.99.52.63 scope global secondary eth1
        inet 46.99.52.20/26 brd 46.99.52.63 scope global secondary eth1
    4: eth2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
        link/ether 06:1d:54:00:02:6b brd ff:ff:ff:ff:ff:ff
        inet 10.101.41.10/24 brd 10.101.41.255 scope global eth2
    5: eth3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
        link/ether 02:00:6d:75:00:0a brd ff:ff:ff:ff:ff:ff
        inet 10.0.0.1/24 brd 10.0.0.255 scope global eth3
    

    OUTPUT DNAT (looks good)

    root@r-336-VM:~# iptables -L OUTPUT -t nat -vn
    Chain OUTPUT (policy ACCEPT 28 packets, 1871 bytes)
     pkts bytes target     prot opt in     out     source               destination
        0     0 DNAT       all  --  *      *       0.0.0.0/0            46.99.52.20        to:10.0.0.154
        0     0 DNAT       all  --  *      *       0.0.0.0/0            46.99.52.19        to:10.0.0.209
    

    PREROUTING DNAT (1st and 3rd line are not needed)

    root@r-336-VM:~# iptables -L PREROUTING -t nat -vn
    Chain PREROUTING (policy ACCEPT 551 packets, 51744 bytes)
     pkts bytes target     prot opt in     out     source               destination
        0     0 DNAT       all  --  eth0   *       0.0.0.0/0            46.99.52.20        to:10.0.0.154
        6   240 DNAT       all  --  *      *       0.0.0.0/0            46.99.52.20        to:10.0.0.154
        0     0 DNAT       all  --  eth0   *       0.0.0.0/0            46.99.52.19        to:10.0.0.209
       11   492 DNAT       all  --  *      *       0.0.0.0/0            46.99.52.19        to:10.0.0.209
    

    POSTROUTING SNAT (1st and 3rd line are incorrect. Last line isn't needed)

    The 1st and 3rd line are close to getting hair-pinning with a static NAT working, but the interface is wrong. If you look above, eth0 was our 169.254.0.0/16 network...

    root@r-336-VM:~# iptables -L POSTROUTING -t nat -vn
    Chain POSTROUTING (policy ACCEPT 26 packets, 1100 bytes)
     pkts bytes target     prot opt in     out     source               destination
        0     0 SNAT       all  --  *      eth0    10.0.0.0/24          10.0.0.154           to:10.0.0.1
       34  2584 SNAT       all  --  *      eth1    10.0.0.154           0.0.0.0/0            to:46.99.52.20
        0     0 SNAT       all  --  *      eth0    10.0.0.0/24          10.0.0.209           to:10.0.0.1
       28  1456 SNAT       all  --  *      eth1    10.0.0.209           0.0.0.0/0            to:46.99.52.19
      243 14660 SNAT       all  --  *      eth2    0.0.0.0/0            0.0.0.0/0            to:10.101.41.10
      222 14854 SNAT       all  --  *      eth1    0.0.0.0/0            0.0.0.0/0            to:46.99.52.18
        0     0 SNAT       all  --  *      eth1    0.0.0.0/0            0.0.0.0/0            to:46.99.52.19
    

    After Changes to configure.py

    IP Address and Interfaces

    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN
        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
        inet 127.0.0.1/8 scope host lo
    2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
        link/ether 0e:00:a9:fe:01:c9 brd ff:ff:ff:ff:ff:ff
        inet 169.254.1.201/16 brd 169.254.255.255 scope global eth0
    3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
        link/ether 06:c7:80:00:00:30 brd ff:ff:ff:ff:ff:ff
        inet 46.99.52.18/26 brd 46.99.52.63 scope global eth1
        inet 46.99.52.20/26 brd 46.99.52.63 scope global secondary eth1
        inet 46.99.52.19/26 brd 46.99.52.63 scope global secondary eth1
    4: eth2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
        link/ether 06:5a:34:00:02:6b brd ff:ff:ff:ff:ff:ff
        inet 10.101.41.10/24 brd 10.101.41.255 scope global eth2
    5: eth3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
        link/ether 02:00:6c:02:00:16 brd ff:ff:ff:ff:ff:ff
        inet 10.0.0.1/24 brd 10.0.0.255 scope global eth3
    

    OUTPUT DNAT

    root@r-342-VM:~# iptables -L OUTPUT -t nat -vn
    Chain OUTPUT (policy ACCEPT 104 packets, 8128 bytes)
     pkts bytes target     prot opt in     out     source               destination
        0     0 DNAT       all  --  *      *       0.0.0.0/0            46.99.52.19        to:10.0.0.209
        0     0 DNAT       all  --  *      *       0.0.0.0/0            46.99.52.20        to:10.0.0.154
    

    PREROUTING DNAT

    root@r-342-VM:~# iptables -L PREROUTING -t nat -vn
    Chain PREROUTING (policy ACCEPT 19526 packets, 1424K bytes)
     pkts bytes target     prot opt in     out     source               destination
      160  8277 DNAT       all  --  *      *       0.0.0.0/0            46.99.52.19        to:10.0.0.209
      181  9086 DNAT       all  --  *      *       0.0.0.0/0            46.99.52.20        to:10.0.0.154
    

    POSTROUTING SNAT

    That last line is still not needed, but not sure how/why that is there

    root@r-342-VM:~# iptables -L POSTROUTING -t nat -vn
    Chain POSTROUTING (policy ACCEPT 339 packets, 17781 bytes)
     pkts bytes target     prot opt in     out     source               destination
        4   240 SNAT       all  --  *      eth3    10.0.0.0/24          10.0.0.209           to:10.0.0.1
       34  1768 SNAT       all  --  *      eth1    10.0.0.209           0.0.0.0/0            to:46.99.52.19
        2   405 SNAT       all  --  *      eth3    10.0.0.0/24          10.0.0.154           to:10.0.0.1
    13800  830K SNAT       all  --  *      eth1    10.0.0.154           0.0.0.0/0            to:46.99.52.20
        0     0 SNAT       all  --  *      eth2    0.0.0.0/0            0.0.0.0/0            to:10.101.41.10
      154 10809 SNAT       all  --  *      eth1    0.0.0.0/0            0.0.0.0/0            to:46.99.52.18
        0     0 SNAT       all  --  *      eth1    0.0.0.0/0            0.0.0.0/0            to:46.99.52.20
    
  14. Slair1 commented on Jun 12, 2018

    @Slair1
    ContributorAuthor

    FYI, my test doesn't have PR #2579 - so i can't be 100% sure how my changes work with that PR. Unfortunately my test environment won't be available for a while to do more testing.

  15. DaanHoogland commented on Jun 14, 2018

    @DaanHoogland
    Contributor

    did you add your changes to #2681, @Slair1 ? (or anywhere in this repo?)

  16. Slair1 commented on Jun 14, 2018

    @Slair1
    ContributorAuthor

    @DaanHoogland Yep, changes are in #2681

  17. added a commit that references this issue on Jun 19, 2018
    08a59e8
  18. yadvr commented on Jun 20, 2018

    @yadvr
    Member

    PR was merged by Daan, closing this manually.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions