Skip to content

design-proposal: make VPC peering work end to end - #52

Draft
mattia-eleuteri wants to merge 6 commits into
cozystack:mainfrom
mattia-eleuteri:proposal/vpc-peering-end-to-end
Draft

design-proposal: make VPC peering work end to end#52
mattia-eleuteri wants to merge 6 commits into
cozystack:mainfrom
mattia-eleuteri:proposal/vpc-peering-end-to-end

Conversation

@mattia-eleuteri

@mattia-eleuteri mattia-eleuteri commented Aug 5, 2026

Copy link
Copy Markdown

VirtualPrivateCloud.spec.peers shipped in v1.2.0 (#2152) and the API reads like a finished feature. Taking it to production for the first time showed that declaring a peer produces no working connectivity at all, and leaves both Vpc objects in a permanent kube-ovn reconcile-error loop:

E net.go:265] invalid CIDR address: 169.254.27.225
E vpc.go:318] invalid cidr 169.254.27.225
E controller.go:1554] err="error syncing add/update vpc \"vpc-2cfb24\": CIDRInvalid, requeuing"

Peering turns out to be three independent layers that must all be correct: OVN routing, subnet ACLs, and routes inside the guest. The chart gets the first wrong by a missing netmask, does not address the second at all, and the third is unaddressed anywhere in Cozystack. Each layer fails silently and in a way that points the operator at the wrong layer.

The proposal is written from measurements on a production cluster (kube-ovn v1.15.10, KubeVirt v1.8.2), with commands and outputs quoted inline. What it proposes:

  1. /30 on localConnectIP — a two-character fix, and the whole reason the feature has never worked. Ships on its own, ahead of the rest.
  2. Allocated instead of hashed interconnect blocks — the current hash addresses 16 256 /30s with no conflict check, so collision probability reaches 26 % at 100 peerings in a cluster.
  3. allowSubnets derived from the declaration — today every subnet is private: true with no allow list, so peered traffic is routed correctly and then dropped by the destination switch. This also fixes an arguably worse bug that needs no new API: two subnets of the same VPC cannot reach each other.
  4. Guest routes through the mechanism kube-ovn already uses for its own VPC components (<provider>.kubernetes.io/routes, cf. pkg/controller/vpc_dns.go, pkg/controller/vpc_nat_gateway.go). Validated end to end on a Windows guest. Gated on one KubeVirt question, documented in §5.1.
  5. Per-peer status conditions — a one-sided declaration currently reports Ready=True and does nothing.

It also documents a dead end (Subnet.enableDHCP / dhcpV4Options are irrelevant for KubeVirt VMs, because virt-launcher is the DHCP server, not OVN) so that nobody spends time re-testing it, and states precisely why firewall-mediated internet egress is structurally blocked today.

Complementary to tenant-site-connectivity (external sites via gateway VMs; this one is intra-cluster VPC-to-VPC) and related to #35 for the deferred egress item.

Opened as a draft: I would like a read on the two design calls in Open questions before it goes to review, in particular whether a VPC should be internally open by default, and who should own interconnect allocation.

VirtualPrivateCloud.spec.peers shipped in v1.2.0 but declaring a peer
produces no connectivity and leaves both Vpc objects in a kube-ovn
CIDRInvalid reconcile loop. Peering is three layers (OVN routing, subnet
ACLs, guest routes); the chart gets the first wrong by a missing netmask,
does not address the second, and the third is unaddressed in Cozystack.

Documents measurements from the first production rollout and proposes:
a /30 on localConnectIP, allocated instead of hashed interconnect blocks,
allowSubnets derived from the declaration (including sibling subnets of
the same VPC), guest routes via the provider-scoped routes annotation
kube-ovn already uses for its own VPC components, and per-peer status
conditions so one-sided declarations stop reporting healthy.

Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a601d64e-3df4-471f-82d9-ac4b11f5e17a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…eferring them

Measured with ovn-trace on a live cluster, which changes the conclusion:

- Egress is dropped in the source subnet's egress pipeline on the way to
  its own router patch port, so a staticRoutes entry for the destination
  is never evaluated. The earlier text blamed the return path.
- allowSubnets renders pair rules anchored on the subnet's own CIDR, so a
  transit VM whose traffic has both endpoints foreign to its subnet can
  never be matched by any rule. Declaring the destination on both subnets
  is not enough.
- With private: false on the subnet hosting the firewall, the trace is
  delivered to its port. The design is achievable; what is missing is a
  way to express transit without disabling isolation wholesale.

Also identifies the second gap: the virtual-router UX is implemented by
Cozystack's kubeovn-webhook, which only propagates the unprefixed
ovn.kubernetes.io/routes and port_security, so it applies to the default
pod network only. kube-ovn already defines the provider-scoped forms.

Adds ovn-trace as the recommended regression gate for the dataplane.

Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
…nsit exception

Disabling isolation on the subnet hosting a gateway VM reads like it
exposes a public-facing VM, and that is the wrong axis. Verified from a
node: VPC subnets are unreachable from the node and pod networks by
routing, independently of ACLs, and a gateway VM's public exposure lives
on its pod NIC behind a Service, not on the VPC subnet's ACL.

What actually changes is the trust boundary: filtering moves into the
gateway's own configuration, and port_security: false grants it source
spoofing inside the VPC. The blast radius stays bounded by the peers'
own privacy, since their pair rules anchor on their own CIDR. Hence the
deployment shape (gateway alone in a dedicated transit subnet) matters
more than the flag, and hence the ask for a transit allowance rather
than settling for private: false.

Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
…ss mechanism exists

Three exist for custom VPCs: VpcNatGateway, the OVN gateway with
EIP/FIP/SNAT, and the newer VpcEgressGateway with ECMP and BFD, whose CRD
is served in the version Cozystack ships. The earlier text asserted a gap
that is not there.

The real gap is narrower: none of them can put a tenant-owned appliance in
the path. VpcEgressGateway is always kube-ovn's own pod pair with a macvlan
leg, snat: true is mandatory, and DNAT/EIP are unsupported. So a tenant
cannot interpose its own firewall to police egress.

Also records what I could not determine rather than asserting it: whether
an egress gateway's traffic passes a private subnet's ACL, and by which
rule. That answer decides whether the transit allowance already half
exists, so it is now an open question instead of a premise.

Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
…cardinality claim

The docs line "only interconnection of two VPCs is supported" reads as a
cardinality limit. It is not one: pkg/controller/vpc.go iterates
spec.vpcPeerings and calls CreatePeerRouterPort once per entry, with no
cap, reconciling removals against status.VpcPeerings. A VPC can hold many
pairwise peerings, the same model as Azure VNet peering. What the sentence
means is that a peering joins two VPCs and is non-transitive.

Removes the recommendation to cap the peers array and the
PeerLimitExceeded condition, which would have made the API strictly less
useful, and replaces them with the two constraints that do matter at N
peers: subnet CIDRs must be mutually non-overlapping across the whole peer
set, not just against the local VPC, and allowSubnets derivation must open
only to directly peered VPCs so the ACLs do not become a transitive mesh.

Adds an upstream docs clarification to the open questions.

Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
…ng dual-homing as the direction

Adds an appendix with the hub-and-spoke-with-a-firewall architecture a
tenant asked us to reproduce, mapped brick by brick onto Cozystack with
the state of each. It establishes that the gaps in this proposal are not
theoretical: every missing brick in that table is inside the VPC.

Two things it settles. The internet edge is not missing: a dual-homed
firewall VM already gets egress and a stable public identity through its
pod NIC and a LoadBalancer Service, which is why this proposal asks for a
transit allowance and not for a gateway object. But that same edge is what
forces dual-homing, and dual-homing is the root cause of the guest-route
problem in section 5, so moving the edge inside the VPC (external segment
plus OvnEip/OvnSnatRule/OvnDnatRule, no per-VPC runtime) is recorded as the
strategic direction that dissolves section 5 instead of working around it.
Its prerequisite is stated too, because it is smaller than it looks and
should not be mistaken for a datacenter project.

Open question 5 becomes a choice of form rather than a question of whether.

Also anonymises VM and namespace names in the ovn-trace excerpts.

Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant