Skip to content
Networking5 min read

VPC peering has no reverse path forwarding, and AWS documents what that costs

A hub peered to two VPCs that share a CIDR will deliver one of them's replies to the other. AWS states the behaviour outright, and the configuration is legal to build because the overlap guardrail is narrower than people think.

· Venerable Networks

AWS refuses to create a peering connection between two VPCs with overlapping CIDRs. That refusal teaches people something slightly wrong: that AWS prevents overlap.

It prevents overlap within a single peering connection. A hub VPC peered to fifty spokes will happily accept two of those spokes being identical, because neither overlaps the hub. The guardrail is real and it is narrower than the conclusion drawn from it.

Here is what happens when you cross that line, in AWS's own words from the peering routing documentation:

AWS does not support unicast reverse path forwarding in VPC peering connections that checks the source IP of packets and routes reply packets back to the source.

The failure

Hub on 10.70.0.0/16. Two team VPCs, both on 10.71.0.0/16, because both teams adopted the same VPC module and neither overrode the default CIDR. The hub peers with both. Both peerings are legal, both go active.

The hub needs a route to 10.71.0.0/16. A route table holds exactly one route per destination prefix, so it gets exactly one, pointing at one of the two peerings.

Now a client in team B connects to a service in the hub. Team B's route table sends the request over its own peering, and it arrives. The hub's service answers. The reply is routed by destination address: the hub looks up 10.71.1.100, finds its single route for that prefix, and sends the packet to team C.

One prefix, two peerings, and a reply with nowhere correct to go
Team B VPC 10.71.0.0/16 requests from here contains team-b 10.71.1.0/24, default route 10.70.0.0/16 → pcx-b, holding Client. Hub VPC 10.70.0.0/16 one route per prefix contains hub 10.70.1.0/24, default route 10.71.0.0/16 → pcx-c, holding Service. Team C VPC 10.71.0.0/16 replies arrive here contains team-c 10.71.1.0/24, holding Host.pcx-bactivepcx-cactiveTeam B VPC 10.71.0.0/16requests from hereteam-bprivate10.71.1.0/2410.70.0.0/16 → pcx-bClient10.71.1.100Hub VPC 10.70.0.0/16one route per prefixhubprivate10.70.1.0/2410.71.0.0/16 → pcx-cRisk — the only route it can holdfor that prefixServiceTeam C VPC 10.71.0.0/16replies arrive hereteam-cprivate10.71.1.0/24Host10.71.1.100

Read the CIDRs on the two team VPCs, then read the single route the hub holds for that prefix. Both peerings are legal and both go active, because the overlap guardrail only compares the two VPCs inside one peering connection and neither team overlaps the hub. The request arrives from the left and the reply leaves to the right, to a host with the same address that never opened the connection.

If team C has a host at that address — and if both teams used the same subnet layout, it probably does — that host receives a SYN-ACK for a connection it never opened and answers with a RST.

Team B's client sees a timeout. The hub team runs tcpdump, sees the SYN arrive and the SYN-ACK leave, and reasonably concludes the network is fine.

Why no route change fixes it

Every option fails for the same underlying reason, and working through them is worth more than the fix.

Repoint the route at team B. Now team B works and team C times out. The failure moves.

Add a second route for 10.71.0.0/16 via the other peering. A route table cannot hold two routes for one destination. The API rejects it.

Use longest prefix match — route 10.71.1.0/24 to one and 10.71.0.0/16 to the other. This is the cleverest wrong answer. It fails because both teams also used the same subnet layout. It is a legitimate documented technique for VPCs with multiple non-overlapping CIDR blocks; it just needs the specific ranges to differ.

Filter by source with a NACL. NACLs do not route. You can turn a wrong delivery into a clean drop, which is marginally safer and equally broken.

The problem is not the route table's contents. The hub is being asked to distinguish two networks that are, from an addressing standpoint, the same network. No routing construct can do that, because routing decides on destination address and the destination addresses are identical.

Transit Gateway does not rescue you either, for the same reason: a TGW route table also holds one route per prefix, so two attachments advertising 10.71.0.0/16 cannot both be reachable.

The two fixes that work

Re-address one side. The correct fix and the expensive one. A VPC CIDR cannot be changed after creation — only added to — so this means recreating the VPC and migrating everything in it.

Put a translation layer in the path. A private NAT gateway in the team VPC performs source NAT into a routable, non-overlapping range, so the hub sees a unique address per team and routes replies correctly. AWS documents this pattern for overlapping networks, with a load balancer handling the destination side. It costs a NAT gateway per team and it is the standard answer when re-addressing is off the table.

The organisational lesson is the real one

A default CIDR in a shared VPC module is a latent outage with a long fuse. Nothing breaks while teams are isolated. The bill arrives the day two of them need the same hub, and by then both VPCs are full of running workloads.

Make the CIDR a required variable with no default, and allocate from a registry. AWS IPAM exists for this, with two caveats worth knowing before you adopt it: its allocation rules flag noncompliant CIDRs rather than blocking them, and private IPv4 management sits in a paid tier that bills per active address across every account it monitors.

Detecting it

The audit query is cheap: list every VPC CIDR in the organisation and look for duplicates.

aws ec2 describe-vpcs --query 'Vpcs[].[VpcId,CidrBlock]' --output text \
  | sort -k2 | awk '{print $2}' | uniq -d

Any prefix that appears twice is a future incident if those VPCs ever need to reach a common third party.

There is also a signal worth building a detection on. In this failure, team C's flow log records inbound accepted traffic from the hub for a connection nobody in team C initiated. That record is the anomaly, and it appears in a log belonging to a team not involved in the incident. A rule for "inbound accepted traffic on a peering with no preceding outbound flow" catches the general case.

If you want to watch a reply land in the wrong VPC, there is a lab: the reply that went to the wrong VPC.

The wider point is that AWS routing constructs decide on destination address, and any design needing the return path to depend on where a request came from is fighting the model. Transit Gateway's appliance mode exists precisely because the default cannot keep both directions of a flow on the same appliance — an explicit, opt-in mechanism for a property people assume they already have.