Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #13758 +/- ##
============================================
+ Coverage 19.89% 19.93% +0.03%
- Complexity 20144 20266 +122
============================================
Files 6371 6373 +2
Lines 576829 577446 +617
Branches 70627 70789 +162
============================================
+ Hits 114778 115110 +332
- Misses 449507 449747 +240
- Partials 12544 12589 +45
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR introduces Direct Routed (L3) guest networks where KVM hosts perform L3 routing directly to instances (no Virtual Router, no NAT, no DHCP), using ConfigDrive-only addressing and a shared link-local gateway (169.254.0.1 / fe80::1) on per-network host bridges.
Changes:
- Adds
GuestType.L3with server-side offering/network validation and a newDirectRoutedNetworkGuruthat forces NICs into host-route form (/32, /128) with link-local gateways. - Updates ConfigDrive generation to always emit
network_data.jsonfor direct-routed NICs even when DHCP/DNS services are not present. - Extends the KVM agent to create/manage per-network bridges (
brdr-<networkId>), program host routes/neighbour entries, and adapt security-group rule plumbing for routed traffic (including secondary IP handling).
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| utils/src/main/java/com/cloud/utils/net/NetUtils.java | Centralizes link-local gateway/CIDR constants and adds IPv6 link-local gateway getter. |
| ui/src/views/offering/AddNetworkOffering.vue | Adds UI support for L3 offerings and enforces L3-specific service/provider composition. |
| ui/src/views/network/CreateNetwork.vue | Adds an L3 network creation tab. |
| ui/src/views/network/CreateL3NetworkForm.vue | New UI form for creating L3 (Direct Routed) networks. |
| ui/public/locales/en.json | Adds UI strings for L3 offering/network creation messaging. |
| tools/marvin/marvin/config/test_data.py | Adds Marvin test data for L3 offerings and L3 networks. |
| test/integration/smoke/test_l3_networks.py | Adds smoke/integration tests covering L3 network lifecycle and validations. |
| server/src/test/java/com/cloud/network/guru/DirectRoutedNetworkGuruTest.java | Unit tests for the new guru selection/design and NIC host-route forcing. |
| server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java | Unit tests for L3 offering validation rules. |
| server/src/main/resources/META-INF/cloudstack/server-network/spring-server-network-context.xml | Registers the new DirectRoutedNetworkGuru bean. |
| server/src/main/java/com/cloud/network/security/SecurityGroupManagerImpl.java | Ensures agent commands are sent for secondary IPs on L3 networks even when SG rules aren’t applied. |
| server/src/main/java/com/cloud/network/NetworkServiceImpl.java | Extends network creation/IPv6 checks/VLAN handling and secondary-IP orchestration for L3. |
| server/src/main/java/com/cloud/network/guru/DirectRoutedNetworkGuru.java | New guru for L3 direct routed guest networks; forces /32 + /128 form and link-local gateways. |
| server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java | Adds L3 offering validation + zone-wide IPv4 overlap checks for L3 ranges. |
| scripts/vm/network/vnet/modifymacip.sh | Adds targeted delete support for specific -4/-6 addresses (secondary IP remove). |
| scripts/vm/network/vnet/modifybrdr.sh | New script to create/delete per-network brdr-* bridges with sysctls + gateway addresses. |
| scripts/vm/network/tests/test_security_group.py | New unit tests for classic-vs-L3 security_group.py rule streams and --directrouted plumbing. |
| scripts/vm/network/tests/golden_default_network_rules.txt | Golden output for classic rule generation (used to ensure no regression). |
| scripts/vm/network/tests/golden_add_fw_framework.txt | Golden output for classic FW framework creation (ensures byte-identical behavior). |
| scripts/vm/network/security_group.py | Adds routed-aware framework/hooks and rule generation, plus --directrouted CLI plumbing. |
| plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSecurityGroupRulesCommandWrapper.java | Passes direct-routed flag when applying SG rules for the VM’s first NIC. |
| plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtNetworkRulesVmSecondaryIpCommandWrapper.java | Threads direct-routed + apply-SG booleans into secondary-IP handling. |
| plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java | Adds --directrouted plumbing and direct-routed secondary-IP route/neigh programming via modifymacip.sh. |
| plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java | Creates per-network bridges for direct-routed NICs and ensures MAC/IP script runs for those NICs. |
| engine/storage/configdrive/src/test/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilderTest.java | Adds tests for direct-routed NIC detection and network_data.json generation behavior. |
| engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java | Always generates network_data.json for direct-routed NICs; adds NIC signature detection. |
| docs/design/direct-routed-networks.md | Adds comprehensive design document for Direct Routed networks. |
| core/src/main/java/com/cloud/agent/api/NetworkRulesVmSecondaryIpCommand.java | Adds directRouted and applySecurityGroupRules flags for secondary-IP commands. |
| api/src/main/java/org/apache/cloudstack/api/command/user/vm/RemoveIpFromVmNicCmd.java | Sends agent update for secondary IP removal on L3 even when zone SG is disabled. |
| api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkCmd.java | Allows specifying physical network ID for L3 networks. |
| api/src/main/java/com/cloud/network/Network.java | Adds GuestType.L3 and parsing support. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Network network = _networkModel.getNetwork(nic.getNetworkId()); | ||
|
|
||
| // On a Direct Routed network the host needs a route and a static neighbour entry for the | ||
| // secondary IP before it is reachable at all. That is independent of security groups, | ||
| // which are optional there and which the Instance may not be using, so the agent is told | ||
| // either way - otherwise the address would stay dark until the Instance was restarted. | ||
| boolean directRouted = Network.GuestType.L3.equals(network.getGuestType()); | ||
|
|
|
@blueorangutan package |
|
@kiranchavala a [SL] Jenkins job has been kicked to build packages. It will be bundled with no SystemVM templates. I'll keep you posted as I make progress. |
|
Packaging result [SF]: ✖️ el8 ✖️ el9 ✖️ debian ✖️ suse15. SL-JID 18739 |
|
@wido this is still marked as wip. what is the status? |
The status is that I have real hardware in a datacenter now where I’m going to deploy this code for further testing. Please keep it WIP for now as I want to validate it there. |
bf3d8a3 to
526934c
Compare
Review feedback on apache#13758: the multi-line inline comments narrating the code are not helpful there. Where a comment documents a real contract (guru design shape, bridge creation/naming, the MAC-before-IPv6 ordering, ConfigDrive as the only addressing channel, secondary-IP delivery independent of security groups, the link-local gateway constants) it now lives as Javadoc on the class, method or field it describes. Mid-method narration is removed; upstream one-line comments that this branch merely extended (mandatory start/end IP, vlan-creation cases, vnet allocation/release) are kept in their original single-line form. No code changes.
|
@DaanHoogland fair point on the inline comments — addressed in a911e9c. The multi-line inline blocks are gone throughout the PR: where a comment documented an actual contract it moved into Javadoc on the class/method/field it describes, the mid-method narration is simply removed, and upstream one-liners this PR merely extended (mandatory start/end IP, vlan-creation cases, vnet allocation/release) kept their original single-line form. That covers the three spots you flagged in ConfigurationManagerImpl and CreateL3NetworkForm.vue plus the equivalent blocks elsewhere. |
Review feedback on apache#13758: the multi-line inline comments narrating the code are not helpful there. Where a comment documents a real contract (guru design shape, bridge creation/naming, the MAC-before-IPv6 ordering, ConfigDrive as the only addressing channel, secondary-IP delivery independent of security groups, the link-local gateway constants) it now lives as Javadoc on the class, method or field it describes. Mid-method narration is removed; upstream one-line comments that this branch merely extended (mandatory start/end IP, vlan-creation cases, vnet allocation/release) are kept in their original single-line form. No code changes.
a911e9c to
655530c
Compare
|
LAB environment is working ( will share all LAB details and notes when finished ) and I have the first SystemVMs running. root@v-28-VM:~# ip route get 9.9.9.9
9.9.9.9 via 169.254.0.1 dev eth2 src 2.57.59.69 uid 0
cache
root@v-28-VM:~# ip -6 route get 2001:db8::1
2001:db8::1 from :: via fe80::1 dev eth2 src 2a00:f10:402:4:1c01:76ff:fe00:93 metric 1024 pref medium
root@v-28-VM:~#
root@v-28-VM:~# ip addr show dev eth2
4: eth2: mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 1e:01:76:00:00:93 brd ff:ff:ff:ff:ff:ff
altname enp0s5
altname ens5
inet 2.57.59.69/32 brd 2.57.59.69 scope global eth2
valid_lft forever preferred_lft forever
inet6 2a00:f10:402:4:1c01:76ff:fe00:93/128 scope global
valid_lft forever preferred_lft forever
inet6 fe80::1c01:76ff:fe00:93/64 scope link
valid_lft forever preferred_lft forever
root@v-28-VM:~#
Notice the /32 and /128 subnetmask on both IPv4 and IPv6 and the gateways on fe80::1 and 169.254.0.1 It really runs on CloudStack: root@v-28-VM:~# dmidecode memory|grep CloudStack
Product Name: CloudStack KVM Hypervisor
root@v-28-VM:~#
Aiming to have this in version 24.0 and give a demo/talk during CCC 2026. |
8a81e58 to
ff5a419
Compare
|
@weizhouapache and @DaanHoogland I have just removed the "wip" label of this PR. I have been testing this on hardware in a datacenter in Amsterdam and I've created a public repository with more information: https://github.com/wido/cloudstack-lab-2026 For me, this is working as expected and I'd like to invite people to test it. I appreciate that it might be difficult to get this replicated and setup, therefor this LAB (see above) is open to gain access for anybody who wants it. Just ask me to get access to the environment so we can test this new feature. No need to set up your own environment. |
… Instances Add a guest network type in which the hypervisor performs L3 routing for the Instance: no Virtual Router, no NAT and no DHCP. Each Instance receives a public IPv4 address as a /32 and/or an IPv6 address as a /128, with a shared, host-independent link-local gateway (169.254.0.1 and fe80::1) that every hypervisor carries. All addressing reaches the Instance via ConfigDrive/cloud-init; a routing daemon on the host (FRR, BIRD, ...) advertises the addresses to the fabric and is deliberately out of scope for CloudStack. The networks live on a dedicated physical network with the new ROUTED isolation method, so the feature can be added to existing zones without touching anything already running there. Each network carries a routed://<id> broadcast domain, the id allocated from the physical network's vnet range or chosen by the operator, which names the network's bridge on every host. Routed ids are validated and canonicalised: a positive integer of at most ten digits, given bare or as routed://<id>. A routed public range may not take an id that a guest network holds or that lies inside a ROUTED physical network's vnet range. Both address families are optional and IPv6-only networks are supported. IPv4 is a subnet (cidr=..., or netmask plus start/end IP), IPv6 is an ip6cidr alone: addresses derive from the subnet and the NIC MAC with EUI-64, so no range exists. No gateways are declared or stored. createNetwork gains an optional cidr parameter (L3 only, additive); an explicit startip/endip must lie inside the given subnet. The zone-wide IPv4 overlap check for L3 ranges runs in the range-creation path that commitNetwork() uses, and the vlan overlap check treats a subnet overlap with an L3 network as a conflict, so both directions are covered. Secondary IPs on L3 NICs follow the Shared branch of allocateSecondaryGuestIP. Zone IPv6 DNS is not required for L3 networks. A DefaultL3NetworkOffering (UserData and DNS via ConfigDrive, security groups) is created on install and upgrade. General fixes the feature surfaced but that apply beyond it: auto-allocated ids are exempt from the dynamic-vlan-range check in createVlanAndPublicIpRange; canUseForDeploy() counts the real IPv4 pool and no longer hides IPv4-less networks from the deploy wizard; the zone-wide IPv6 overlap check keys on ip6_cidr rather than ip6_gateway. Implements apache#12210
Console proxies and secondary storage VMs can take their public address from a routed public range (createVlanIpRange with vlan=routed://<id>). The management server then hands the address to the SystemVM in host-route form, a /32 or /128 with the shared link-local gateway on-link, and the SystemVM boot scripts configure it that way for IPv4 and IPv6 alike.
7680424 to
4b61a80
Compare
The agent recognises a Direct Routed NIC by its routed://<id> broadcast domain and asks the new modifybrdr.sh for the network's bridge. The script creates one uplink-less bridge per network, named brdr-<id>, with the gateway addresses passed in by the agent, forwarding sysctls, strict rp_filter, arp_ignore/arp_announce for the shared gateway address, and a MAC derived from the routed id so it is identical on every hypervisor and live migration never invalidates the guest's neighbour cache. The existing modifymacip.sh installs the per-address host route and static neighbour entry when a NIC is plugged, and removes them at unplug before the script may delete an empty bridge. How these bridges are named is known only to the script; the agent asks it (query) rather than parsing names itself. Every script operation prints exactly one token on stdout with diagnostics on stderr, inputs are validated, and the agent reads the last line and checks it is an interface name, so a stray warning can never end up as the bridge name in the domain XML. A missing modifymacip.sh is fatal for a Direct Routed NIC. Because the host routes for its Instances, their packets enter the host's own IP stack. Per bridge, the script inserts an INPUT rule that drops everything from the bridge that is not ICMP(v6), which gateway resolution and reachability checks need, and an ip6tables raw rpfilter rule as the IPv6 counterpart of rp_filter=1. Both are removed with the bridge. A failed VM start or migration prepare now unplugs the NICs it plugged. The security group wrapper passes --directrouted to security_group.py for these NICs, and a failed ipset update for secondary IPs is reported instead of returning success.
Traffic on a Direct Routed bridge is routed, not bridged, so the classic FORWARD hooks gated on --physdev-is-bridged never see it, and the kernel knows no bridged egress port for it. With --directrouted the script matches traffic from the Instance on its bridge port, as before but without --physdev-is-bridged, and traffic towards the Instance on its ipset destination instead. Rules for DHCP, DHCPv6 and router advertisements towards the guest are skipped since those do not exist on these networks. The rest of the rule stream is shared with the classic path, and golden files pin the classic command stream to prove existing deployments unchanged; framework setup (bridge netfilter, chains, notrack ipsets) is extracted into helpers used by both paths. A routed packet between two Instances on one host passes the rules of both. FORWARD holds two fixed hooks into shared chains, source first: BF-L3-IN dispatches per bridge to the source Instance's rules, BF-L3-OUT to the destination Instance's rules, followed by one ACCEPT per bridge for traffic bound for the fabric. Source-side rules never ACCEPT: allowed traffic RETURNs so the destination pass still runs, denied traffic is dropped. Egress rules therefore end in RETURN and the egress chain drops what no rule returned; until add_network_rules has run it holds a single DROP. An address on an L3 bridge no Instance claims is dropped. The periodic cleanup_rules removes the rules and chains of bridges the agent has deleted. Teardown is anchored on the chain name so i-2-7 no longer matches i-2-70, and verify_network_rules describes the L3 rule stream. Unit tests apply the generated commands to a model of iptables and walk packets through it for every traffic case in both bridge creation orders. The golden files are excluded from the RAT check.
A Direct Routed NIC has no DHCP and no router advertisements, so its addressing must come from ConfigDrive. network_data.json is now always generated when an Instance has such a NIC, and covers every NIC of the Instance: an explicit network configuration listing one interface stops cloud-init from configuring the others. The IPv4 address is written as a /32 with the default route emitted as a network-level gateway key, and the IPv6 address as a /128 with the link-local gateway, so cloud-init (netplan >= 23.1, networkd >= 24.2) renders the gateway on-link.
Adds an L3 network creation form with an admin-only physical network selector limited to ROUTED physical networks, the routed id as a number and an optional cidr; ROUTED as isolation method in the zone wizard and the physical network views; the specifyvlan switch for L3 offerings so operator-chosen routed ids are reachable; and the active IPv4/IPv6 addresses in the Instance list with click-to-copy.
test_l3_networks covers offering and network creation, IPv4-only, IPv6-only and dual-stack networks, zone-wide overlap detection, a requested IPv4 address, the subnet's .0 address and an operator-chosen routed id, and deploys an Instance to assert both address families in host-route form. Marvin gains the cidr and routed id parameters and the test data for the offering and networks.
Records the network model, the decisions taken and their alternatives, the hypervisor and security group design, the review findings that were deferred, and the implementation status. Excluded from the RAT check.
4b61a80 to
d7f67e8
Compare
Description
This Pull Request adds a new guest network type in which the hypervisor performs L3 routing for the Instance: no Virtual Router, no NAT and no DHCP. Each Instance receives a public IPv4 address as a /32 and/or an IPv6 address as a /128, with a shared, host-independent link-local gateway (169.254.0.1 and fe80::1) that every hypervisor carries on the network's bridge. All addressing reaches the Instance exclusively via ConfigDrive/cloud-init; a routing daemon on the host (FRR, BIRD, ...) advertises the addresses to the fabric and is deliberately out of scope for CloudStack.
Direct Routed networks live on their own physical network, carrying the new
ROUTEDisolation method. This is what makes the feature adoptable in existing environments: an operator adds one physical network to an existing zone and gains L3 networks next to everything already running there — no changes to existing physical networks, offerings or Instances.IPv6-only networks are fully supported. Nothing in this network type depends on IPv4 (no DHCP, no password/metadata service), so each address family is optional: IPv4 is a subnet (
cidr=2.57.59.0/24, or netmask + start/end IP), IPv6 is anip6cidralone — IPv6 addresses are computed from the subnet and the NIC MAC with EUI-64, so no range and no pool exist. No gateways are declared or stored for either family, saving those addresses; Instances always use the shared link-local gateway.Management server
GuestType.L3; the guest_type column is char(32), so no schema change.BroadcastDomainType.Routed: each network carries arouted://<id>broadcast domain whose id names the per-network bridge on every host — allocated from the ROUTED physical network's vnet range, or chosen by the operator (specifyVlan offerings) so bridge names are plannable before the network exists.DefaultL3NetworkOfferingis created on install and upgrade.DirectRoutedNetworkGurusubclasses DirectNetworkGuru, inheriting the Shared-network address lifecycle. After allocation the NicProfile is forced into host-route form (/32 or /128 + link-local gateway), which is also the signature by which ConfigDrive recognises these NICs.createVlanIpRangewithvlan=routed://<id>): their public NICs take the same host-route form, with the IPv6 address derived EUI-64 from the range's subnet.ConfigDrive
gatewaykey instead of a default-route entry: cloud-init derives the required on-link flag only from that key (netplan renderer since 23.1, networkd since 24.2), never from routes-list entries. This sets the guest requirement: cloud-init >= 23.1 with netplan, or >= 24.2 with networkd.KVM agent
brdr-<routed id>, created and removed by the new modifybrdr.sh (flock'd, idempotent, refuses to remove a bridge still in use). The bridge carries the gateway addresses, forwarding, strict rp_filter, and a MAC derived deterministically from the routed id — identical on every hypervisor, so a live-migrated Instance's ARP/NDP cache stays valid and cutover is seamless. Separate bridges make isolation between networks topological rather than a filtering concern.--directrouteddispatch: rules for these Instances match by ipset destination rather than bridge port on the return path. Because a routed packet between two Instances on one host enters on one brdr bridge and leaves on another, the L3 framework runs two passes from a single FORWARD hook (sharedBF-L3chains): the source Instance's egress rules mark the packet, the destination Instance's ingress rules give the verdict, so both are always evaluated regardless of bridge creation order. A golden-file test harness pins the classic (bridged) rule stream byte-for-byte, proving existing deployments are untouched, and walks packets through the generated L3 rules.brdr-+that admits only conntrack replies, ICMP echo and IPv6 neighbour discovery, plus an ip6tablesrpfilterrule as the IPv6 counterpart ofrp_filter=1. Forwarding towards management and storage subnets remains the operator's host firewall policy, like the routing daemon.root@hv-138-d03-22:~# ip addr show dev brdr-578 15: brdr-578: mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 0e:00:00:00:02:42 brd ff:ff:ff:ff:ff:ff inet 169.254.0.1/32 scope global brdr-578 valid_lft forever preferred_lft forever inet6 fe80::c00:ff:fe00:242/64 scope link valid_lft forever preferred_lft forever inet6 fe80::1/64 scope link valid_lft forever preferred_lft forever root@hv-138-d03-22:~#Other changes this PR makes to CloudStack
Fixes and improvements that ride along because the feature exposed them, but apply generally:
createNetworkgains an optionalcidrparameter (L3-only for now, purely additive).createVlanAndPublicIpRange— previously Shared networks were exempted one layer up but not there.canUseForDeploy()treats Shared and L3 alike and counts the real IPv4 pool; IPv6-only networks are deployable from the UI wizard.ip6_cidrinstead ofip6_gateway(what it actually compares).common.shinstalls the IPv6 default route statically fromip6gatewayinstead of relying on a router advertisement. This also applies to classic VLAN public networks with IPv6, where systemvms previously had no IPv6 default route unless a fabric router sent RAs.The design document, including the decision log and the verification notes behind each choice, is added under docs/design/.
This implements issue #12210
Types of changes
Feature/Enhancement Scale or Bug Severity
Feature/Enhancement Scale
Bug Severity
Screenshots (if appropriate):
How Has This Been Tested?
The feature was tested end to end on real hardware in a datacenter in September 2026: a KVM zone with a ROUTED physical network, SystemVMs on a routed public range, dual-stack and IPv6-only L3 networks, Instances with security groups, and live migration between hosts. Evidence of all tests is recorded, and the lab is available for remote access on request for anyone who wants to validate the feature without building their own environment.
Verified guest behaviour: Ubuntu 26.04 with cloud-init (26.1) and ConfigDrive as the datasource works as expected — IPv4 default route installed with on-link, EUI-64 IPv6 configured, dual-stack and IPv6-only.
Unit tests cover the guru, offering validation, ConfigDrive generation, the deploy gates and the address-family validation; the security_group.py golden harness pins the classic rule stream; integration tests (test_l3_networks.py) cover creation (subnet forms, operator-chosen ids, IPv6-only, gateway-ignoring, overlap and incomplete-family rejection) and deployment.
Pending lab validation for the review follow-up commits (September 2026): the two-pass security-group rules (cross-account Instance-to-Instance traffic on one host,
iptables -Cidempotency of the MARK rules), the host protection chain (Instances must still resolve and ping 169.254.0.1 / fe80::1) and the new unplug order. Design doc §12.2 and §13 list exactly what to check.