prashanthr2 opened a new issue, #13942:
URL: https://github.com/apache/cloudstack/issues/13942

   ### problem
   
   On a VPC with public IPs from more than one range/VLAN, the virtual router 
installs an unscoped source NAT rule on every public interface using the single 
VPC source NAT address. Because the rule has no source match, it also rewrites 
traffic the router itself originates, so the VR cannot emit packets with the 
correct source address from any public interface other than the source-NAT one.
   
   Generated in systemvm/debian/opt/cloud/bin/cs/CsAddress.py:
   ```
   if self.get_type() == "public" and self.config.is_vpc() and method == "add" 
and not self.config.is_routed():
       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'])])
       elif cmdline.get_source_nat_ip() and not self.is_private_gateway():
           self.fw.append(["nat", "", "-A POSTROUTING -j SNAT -o %s --to-source 
%s" % (self.dev, cmdline.get_source_nat_ip())])
   ```
   The if branch pairs its SNAT with a FORWARD -s <vpccidr> rule, showing the 
intent is forwarded guest traffic. The elif branch has no equivalent scoping 
and no -s match
   
   **VPC router r-21-VM — two public ranges on separate VLANs:**
   ```
   eth1  10.1.30.5/24    public range 1 (source NAT IP), gateway 10.1.30.1
   eth2  10.1.31.2/24    public range 2,                 gateway 10.1.31.1
   eth3  10.0.0.1/24     guest tier
   eth4  10.1.1.1/24     guest tier
   ```
   
   Resulting NAT rules:
   ```
   -A POSTROUTING -o eth1 -j SNAT --to-source 10.1.30.5
   -A POSTROUTING -o eth2 -j SNAT --to-source 10.1.30.5     ← eth2 is 
10.1.31.2; wrong subnet
   -A POSTROUTING -s 10.1.1.0/24 -d 10.1.1.52/32 -o eth4 -p tcp -m tcp --dport 
80 -j SNAT --to-source 10.1.1.1
   ```
   
   /etc/cloudstack/ips.json is correct. 
   ```
   root@r-21-VM:~#  cat /etc/cloudstack/ips.json | grep -iE 
'eth|public_ip|source_nat|network|gateway'
     "eth0": [
         "device": "eth0",
         "gateway": "",
         "network": "169.254.0.0/16",
         "public_ip": "169.254.82.31",
         "source_nat": false
     "eth1": [
         "device": "eth1",
         "gateway": "10.1.30.1",
         "is_private_gateway": false,
         "network": "10.1.30.0/24",
         "public_ip": "10.1.30.5",
         "source_nat": true,
     "eth2": [
         "device": "eth2",
         "gateway": "10.1.31.1",
         "is_private_gateway": false,
         "network": "10.1.31.0/24",
         "public_ip": "10.1.31.2",
         "source_nat": false,
     "eth3": [
         "device": "eth3",
         "gateway": "10.0.0.1",
         "network": "10.0.0.0/24",
         "public_ip": "10.0.0.1",
         "source_nat": false
     "eth4": [
         "device": "eth4",
         "gateway": "10.1.1.1",
         "network": "10.1.1.0/24",
         "public_ip": "10.1.1.1",
         "source_nat": false
   ```
   
   The eth2 entry has source_nat: false and public_ip: 10.1.31.2 so this is not 
bad input from the management server; the elif deliberately substitutes the 
VPC-wide source NAT IP.
   
   
   
   **Packet capture, VR pinging its own eth2 gateway (10.1.31.1):**
   ```
   root@r-21-VM:~# tcpdump -i any -nnnvvv 'host 10.1.31.1 and icmp'
   tcpdump: data link type LINUX_SLL2
   tcpdump: listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot 
length 262144 bytes
   10:01:07.711338 eth2  Out IP (tos 0x0, ttl 64, id 10156, offset 0, flags 
[DF], proto ICMP (1), length 84)
       10.1.30.5 > 10.1.31.1: ICMP echo request, id 6524, seq 0, length 64
   10:01:08.712408 eth2  Out IP (tos 0x0, ttl 64, id 10259, offset 0, flags 
[DF], proto ICMP (1), length 84)
       10.1.30.5 > 10.1.31.1: ICMP echo request, id 6524, seq 1, length 64
   10:01:09.713031 eth2  Out IP (tos 0x0, ttl 64, id 10338, offset 0, flags 
[DF], proto ICMP (1), length 84)
       10.1.30.5 > 10.1.31.1: ICMP echo request, id 6524, seq 2, length 64
   ```
   
   Requests and replies traverse different interfaces. On a permissive network 
the ping still succeeds via that asymmetry; on any gateway enforcing 
reverse-path forwarding the packet is dropped at ingress and no reply is 
generated.
   
   **User-visible impact**: /root/health_checks/gateways_check.py pings every 
NIC's gateway, so 10.1.31.1 is reported unreachable despite being perfectly 
reachable, producing repeated Health checks failed alerts. If 
router.health.checks.failures.to.recreate.vr includes gateways_check.py, 
CloudStack will also repeatedly recreate a healthy VR.
   
   More generally, any VR-originated traffic leaving a non-source-NAT public 
interface carries an address that does not belong on that segment.
   
   **Not affected**: guest egress. A VPC has a single source NAT IP by design 
and guest traffic follows the default route out the source-NAT interface, so it 
is unchanged.
   
   ### versions
   
   CloudStack : 4.22.1
   Hypervisor: KVM
   Gateway: pfSense (reverse-path filtering enabled)
   
   CsAddress.py is byte-identical in 4.20.1.0, 4.20.3.0, 4.22.1.0 and main 
long-standing behaviour, not a regression.
   
   ### The steps to reproduce the bug
   
   1. Create a VPC with the default VPC offering and at least one tier.
   2. Acquire a public IP from a second public range on a different VLAN/subnet 
and associate it with the VPC (e.g. add a port-forwarding rule), so the VR 
gains a second public NIC.
   3. On the VR, confirm the interface and its SNAT rule:
   ```
   ip addr show eth2
   iptables -t nat -S POSTROUTING | grep eth2
   ```
   The --to-source value is the VPC source NAT IP (10.1.30.5), not the eth2 
address (10.1.31.2).
   4. Ping the second range's gateway from the VR while capturing both public 
interfaces:
   ```
   tcpdump -lnni eth1 -s 96 'icmp and host 10.1.31.1' > /tmp/eth1.txt 2>&1 &
   tcpdump -lnni eth2 -s 96 'icmp and host 10.1.31.1' > /tmp/eth2.txt 2>&1 &
   sleep 1; ping -c 3 10.1.31.1; sleep 2; kill %1 %2
   cat /tmp/eth1.txt /tmp/eth2.txt 
   ```
   4. Run the health check directly:
   `python3 /root/health_checks/gateways_check.py basic`
   
   **Expected**: requests leave eth2 sourced from 10.1.31.2, replies return on 
eth2, and the check reports all gateways reachable.
   **Actual**: requests leave eth2 sourced from 10.1.30.5; replies arrive on 
eth1, or are dropped entirely by a reverse-path-enforcing gateway, and the 
check reports 10.1.31.1 unreachable.
   
   ### What to do about it?
   
   Scope the elif rule so it does not match locally-generated packets:
   ```
   elif cmdline.get_source_nat_ip() and not self.is_private_gateway():
       self.fw.append(["nat", "", "-A POSTROUTING -m addrtype ! --src-type 
LOCAL -j SNAT -o %s --to-source %s" % (self.dev, cmdline.get_source_nat_ip())])
   ```
   ! --src-type LOCAL excludes packets whose source is an address on the 
router, while forwarded guest traffic still matches and is SNATed as before.
   
   Verified by replacing the rule manually on the VR:
   ```
   iptables -t nat -D POSTROUTING -o eth2 -j SNAT --to-source 10.1.30.5
   iptables -t nat -A POSTROUTING -o eth2 -m addrtype ! --src-type LOCAL -j 
SNAT --to-source 10.1.30.5
   ```
   
   | Behaviour | Before | After |
   | --- | --- | ---|
   | VR → 10.1.31.1| 10.1.30.5 > 10.1.31.1, reply on eth1 / dropped|10.1.31.2 > 
10.1.31.1, reply on eth2 |
   | Guest → internet | SNAT to 10.1.30.5 via eth1 | unchanged |
   
   A CIDR-based scope (-s <vpccidr>) was considered and rejected: it fails when 
the public ranges fall inside the VPC CIDR.
   
   Workaround for affected deployments — a manual iptables edit is reverted by 
CloudStack on any config refresh, so suppress the check instead:
   ```
   cmk update configuration name=router.health.checks.to.exclude 
value=gateways_check.py zoneid=<zone-uuid>
   cmk list configurations name=router.health.checks.failures.to.recreate.vr   
# ensure gateways_check.py is NOT listed
   ```


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to