This directory contains the NetworkExtension NetworkOrchestrator extension —
a CloudStack plugin that delegates all network operations to an external device
over SSH. The device can be a Linux server (using network namespaces,
bridges, and iptables), a network appliance that accepts SSH commands, or any
other host that can run the network-namespace-wrapper.sh (or a compatible
script) to perform network configurations.
The extension is implemented in
framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/NetworkExtensionElement.java
and loaded automatically by the management server — no separate plugin JAR is
required.
- Architecture
- Directory contents
- How it works
- Installation
- Step-by-step API setup
- 1. Create the extension
- 2. Register the extension with a physical network
- 3. Create a network offering
- 4. Create an isolated network
- 5. Acquire a public IP and enable Source NAT
- 6. Enable / disable Static NAT
- 7. Add / delete Port Forwarding
- 8. Delete the network
- 9. Unregister and delete the extension
- Multiple extensions on the same physical network
- Wrapper script operations reference
- Payload reference
- Custom actions
- Developer / testing notes
┌──────────────────────────────────────────────────────────┐
│ CloudStack Management Server │
│ │
│ NetworkExtensionElement.java │
│ │ executes (path resolved from Extension record) │
│ ▼ │
│ /usr/share/cloudstack-management/extensions/<ext-name>/ │
│ <ext-name>.sh (network-namespace.sh) │
└──────────────────────┬───────────────────────────────────┘
│ SSH (host : port from extension details)
│ credentials from extension_resource_map_details
▼
┌──────────────────────────────────────────────────────────┐
│ Remote Network Device (KVM Linux server) │
│ │
│ network-namespace-wrapper.sh <command> [args...] │
│ │
│ Per-network data plane (guest VLAN 1910, network 209): │
│ │
│ HOST side │
│ ───────────────────────────────────────────────── │
│ eth1.1910 ─────────────────────────────────┐ │
│ (VLAN sub-iface) │ │
│ breth1-1910 (bridge) │
│ vh-1910-d1 ─────────────────────────────────┘ │
│ │ │
│ NAMESPACE cs-net-209 (isolated) │
│ cs-vpc-5 (VPC, vpc-id=5) │
│ ───────────────────────────────────────────────── │
│ vn-1910-d1 ← gateway IP 10.1.1.1/24 │
│ │
│ PUBLIC side (source-NAT IP 10.0.56.4 on VLAN 101): │
│ │
│ HOST side │
│ eth1.101 ─────────────────────────────────┐ │
│ breth1-101 (bridge) │
│ vph-101-209 ────────────────────────────────┘ │
│ │ │
│ NAMESPACE cs-net-209 (or cs-vpc-<vpcId>) │
│ vpn-101-209 ← source-NAT IP 10.0.56.4/32 │
│ default route → 10.0.56.1 (upstream gateway) │
└──────────────────────────────────────────────────────────┘
| Object | Name pattern | Example (VLAN 1910, net 209, pub-VLAN 101) |
|---|---|---|
| Namespace (isolated network) | cs-net-<networkId> |
cs-net-209 |
| Namespace (VPC network) | cs-vpc-<vpcId> |
cs-vpc-5 |
| Guest host bridge | br<ethX>-<vlan> |
breth1-1910 |
| Guest veth – host side | vh-<vlan>-<id> |
vh-1910-d1 |
| Guest veth – namespace side | vn-<vlan>-<id> |
vn-1910-d1 |
| Public host bridge | br<pub_ethX>-<pvlan> |
breth1-101 |
| Public veth – host side | vph-<pvlan>-<id> |
vph-101-209 |
| Public veth – namespace side | vpn-<pvlan>-<id> |
vpn-101-209 |
ethX (and pub_ethX) is the NIC specified in the guest.network.device
(and public.network.device) key when registering the extension on the
physical network. Both default to eth1 when not explicitly set.
Note: when
<vlan>or<id>would make the interface name exceed the Linux 15-character limit, the<id>portion is shortened to its hex representation (for numeric IDs) or a 6-character MD5 prefix (for non-numeric IDs).
Key design principles:
- The
network-namespace.shscript runs on the management server. All connection details (host,port,username,sshkey, etc.) are passed as two named CLI arguments injected byNetworkExtensionElement— the script itself is completely generic and requires no local configuration. - The
network-namespace-wrapper.shscript runs on the remote KVM device. It creates host-side bridges, veth pairs, and iptables rules. Bridges and VLAN sub-interfaces live on the host (not inside the namespace) so that guest VMs whose NICs are connected tobrethX-<vlan>reach the namespace gateway without any additional configuration. - VPC networks share a single namespace per VPC (
cs-vpc-<vpcId>). Multiple guest VLANs are each connected via their own veth pair (vh-<vlan>-<id>/vn-<vlan>-<id>). - Isolated networks each get their own namespace (
cs-net-<networkId>). - The two scripts are intentionally decoupled: you can replace either script with a custom implementation (Python, Go, etc.) as long as the interface contract (arguments and exit codes) is maintained.
| File | Installed location | Purpose |
|---|---|---|
network-namespace.sh |
management server | SSH proxy — executed by NetworkExtensionElement |
network-namespace-wrapper.sh |
remote network device | Performs iptables / bridge operations |
README.md |
— | This documentation |
Source tree paths:
network-namespace.sh→extensions/network-namespace/network-namespace.shnetwork-namespace-wrapper.sh→extensions/network-namespace/network-namespace-wrapper.sh
-
CloudStack decides that a network operation must be applied (e.g.
implement,addStaticNat,applyPortForwardingRules). -
NetworkExtensionElement(Java) resolves the extension that is registered on the physical network whose name matches the network's service provider. It reads all device details stored inextension_resource_map_details. -
NetworkExtensionElementbuilds a command line:<extension_path>/network-namespace.sh <command> <payload-file> <timeout-seconds>The payload file includes top-level
physical-network-extension-details, top-levelnetwork-extension-details, and command-specific fields underpayload(exceptcustom-action, which is a flat top-level payload). -
network-namespace.shparses the payload JSON, writes the SSH private key to a temporary file (ifsshkeyis set in the physical-network details), uploads the payload file to the selected host, then runs the wrapper script remotely as<command> <payload-file> <timeout-seconds>. -
network-namespace-wrapper.shparses the payload and executes the requested operation usingip link,iptables,ip addr, etc. inside the network namespace. -
Exit codes from
network-namespace.sh:0— success1— usage / configuration error (missing arguments, no reachable hosts)2— SSH connection or authentication error3— remote wrapper script returned non-zero
Any non-zero exit causes CloudStack to treat the operation as failed.
sshkeyfield inphysical-network-extension-details— PEM key written to a temp file under/tmp/.cs-extnet-key-XXXXXX/, used withssh -i. Preferred — the temp file is deleted on exit.passwordfield — passed tosshpass(1)if available;sshpassmust be installed on the management server.- Neither set — relies on the SSH agent or host key on the management server.
Before every network operation NetworkExtensionElement calls ensure-network-device
on network-namespace.sh (locally, no SSH). This selects the KVM host for the
network:
- Sticky re-validation: if a host was previously selected (from
payload.current_details.hostornetwork-extension-details.host) and that host is still in the candidate list and still reachable, it is kept. - Hash-based selection: for new or failed-over networks a stable preferred index
is computed as
CRC32(<routing-key>) mod len(hosts)where the routing key isvpc_idfor VPC networks (ensuring all tiers land on the same host) ornetwork_idfor isolated networks. Hosts are probed in order starting at that index until one answers. - The result is printed as a single-line JSON object:
CloudStack stores this in
{"host":"192.168.1.10","namespace":"cs-net-42"}network_details.extension.detailsand forwards it to later calls through top-levelnetwork-extension-details.
You can override the remote wrapper path for testing:
CS_NET_SCRIPT_PATH=/custom/path/wrapper.sh network-namespace.sh implement-network ...During package installation the network-namespace.sh script is deployed to:
/usr/share/cloudstack-management/extensions/<extension-name>/<extension-name>.sh
The extension path is set to network-namespace at creation time;
NetworkExtensionElement looks for <extensionName>.sh inside the directory.
In developer mode the extensions directory defaults to extensions/ relative
to the repo root, so extensions/network-namespace/network-namespace.sh is
found automatically.
Copy network-namespace-wrapper.sh to each remote device that will act as the
network gateway, inside a subdirectory named after the extension:
# From the CloudStack source tree:
DEVICE=root@<kvm-host>
EXT_NAME=network-namespace # must match the extension name in CloudStack
ssh ${DEVICE} "mkdir -p /etc/cloudstack/extensions/${EXT_NAME}"
scp extensions/network-namespace/network-namespace-wrapper.sh \
${DEVICE}:/etc/cloudstack/extensions/${EXT_NAME}/${EXT_NAME}-wrapper.sh
ssh ${DEVICE} "chmod +x /etc/cloudstack/extensions/${EXT_NAME}/${EXT_NAME}-wrapper.sh"The wrapper derives its state directory and log path from the directory it is installed in:
- State:
/var/lib/cloudstack/<ext-name>/(e.g./var/lib/cloudstack/network-namespace/) - Log (wrapper, on KVM host):
/var/log/cloudstack/extensions/<ext-name>/<ext-name>.log(e.g./var/log/cloudstack/extensions/network-namespace/network-namespace.log) - Log (proxy, on management server):
/var/log/cloudstack/extensions/<ext-name>.log(e.g./var/log/cloudstack/extensions/network-namespace.log)
Additional per-network service logs are also written to the same directory on the
KVM host: dnsmasq-<networkId>.log, apache2-<networkId>.log,
passwd-<networkId>.log.
Prerequisites on the remote device:
| Package / tool | Purpose |
|---|---|
iproute2 (ip, ip netns) |
Namespace, bridge, veth, route management |
iptables + iptables-save |
NAT and filter rules inside namespace |
arping |
Gratuitous ARP after public IP assignment |
dnsmasq |
DHCP and DNS service inside namespace |
haproxy |
Load balancing inside namespace |
apache2 (Debian/Ubuntu) or httpd (RHEL/CentOS) |
Metadata / user-data HTTP service (port 80) |
python3 |
DHCP options parsing, haproxy config generation, vm-data processing |
util-linux (flock) |
Serialise concurrent operations per network |
sshd |
Reachable from the management server on the configured port (default 22) |
The SSH user must have permission to run ip, iptables, iptables-save,
and ip netns exec (root or passwordless sudo for those commands).
All examples below use cmk (the CloudStack CLI). Replace <zone-uuid>,
<phys-net-uuid>, etc. with real values from your environment.
cmk createExtension \
name=my-extnet \
type=NetworkOrchestrator \
path=network-namespace \
details[0].network.services="SourceNat,StaticNat,PortForwarding,Firewall,Gateway" \
details[1].network.service.capabilities="{\"SourceNat\":{\"SupportedSourceNatTypes\":\"peraccount\",\"RedundantRouter\":\"false\"},\"Firewall\":{\"TrafficStatistics\":\"per public ip\"}}"The two details declare which services this extension provides and their CloudStack capability values. These are consulted when listing network service providers and when validating network offerings.
network.services — comma-separated list of service names:
SourceNat,StaticNat,PortForwarding,Firewall,Gateway
Valid service names include: Vpn, Dhcp, Dns, SourceNat,
PortForwarding, Lb, UserData, StaticNat, NetworkACL, Firewall,
Gateway, SecurityGroup.
network.service.capabilities — JSON object mapping each service to its
CloudStack Capability key/value pairs:
{
"SourceNat": {
"SupportedSourceNatTypes": "peraccount",
"RedundantRouter": "false"
},
"Firewall": {
"TrafficStatistics": "per public ip"
}
}Services listed in network.services that have no entry in
network.service.capabilities (e.g. StaticNat, PortForwarding,
Gateway) are still offered — CloudStack treats missing capability values as
"no constraint" and accepts any value when creating the network offering.
If you omit both details entirely, the extension defaults to an empty set of services and no capabilities.
Backward compatibility: the old combined
network.capabilitiesJSON key (with a"services"array and"capabilities"object in one blob) is still accepted but deprecated. Prefer the split keys above.
Verify the extension was created and its state is Enabled:
cmk listExtensions name=my-extnetTo enable or disable the extension:
cmk updateExtension id=<ext-uuid> state=Enabled
cmk updateExtension id=<ext-uuid> state=Disabledcmk registerExtension \
id=<extension-uuid> \
resourcetype=PhysicalNetwork \
resourceid=<phys-net-uuid>This creates a Network Service Provider (NSP) entry named my-extnet on the
physical network and enables it automatically. The NSP name is the extension
name — not the generic string NetworkExtension.
After registering, set the connection details for the remote KVM device(s):
cmk updateRegisteredExtension \
extensionid=<extension-uuid> \
resourcetype=PhysicalNetwork \
resourceid=<phys-net-uuid> \
"details[0].hosts=192.168.10.1,192.168.10.2" \
"details[1].username=root" \
"details[2].sshkey=<pem-key-contents>" \
"details[3].guest.network.device=eth1" \
"details[4].public.network.device=eth1"
network.isolation.method=NetworkExtensionmust be set as an Extension detail (viacreateExtensionorupdateExtension), not as a physical-network registration detail. The network-namespace extension uses VLAN-based isolation and does not rely on the script output fromimplement-networkto override the broadcast domain type, so this detail is not strictly required for basic operation. It is included here as best practice and for forward compatibility — extensions that returnnetwork.broadcast_domain_typeornetwork.broadcast_urifromimplement-networkmust set it or those updates will be silently ignored by CloudStack.
The hosts value is a comma-separated list of KVM host IPs; ensure-network-device
picks one per network and stores it in --network-extension-details. Use sshkey
(PEM private key) for passwordless authentication, or password + sshpass.
Verify:
cmk listNetworkServiceProviders physicalnetworkid=<phys-net-uuid>
# → a provider named "my-extnet" should appear in state EnabledTo disable or re-enable the NSP:
cmk updateNetworkServiceProvider id=<nsp-uuid> state=Disabled
cmk updateNetworkServiceProvider id=<nsp-uuid> state=EnabledTo unregister:
cmk unregisterExtension \
id=<extension-uuid> \
resourcetype=PhysicalNetwork \
resourceid=<phys-net-uuid>Use the extension name (my-extnet) as the service provider — not the
generic string NetworkExtension:
cmk createNetworkOffering \
name="My ExtNet Offering" \
displaytext="Isolated network via my-extnet" \
guestiptype=Isolated \
traffictype=GUEST \
supportedservices="SourceNat,StaticNat,PortForwarding,Firewall,Gateway" \
"serviceProviderList[0].service=SourceNat" "serviceProviderList[0].provider=my-extnet" \
"serviceProviderList[1].service=StaticNat" "serviceProviderList[1].provider=my-extnet" \
"serviceProviderList[2].service=PortForwarding" "serviceProviderList[2].provider=my-extnet" \
"serviceProviderList[3].service=Firewall" "serviceProviderList[3].provider=my-extnet" \
"serviceProviderList[4].service=Gateway" "serviceProviderList[4].provider=my-extnet" \
"serviceCapabilityList[0].service=SourceNat" \
"serviceCapabilityList[0].capabilitytype=SupportedSourceNatTypes" \
"serviceCapabilityList[0].capabilityvalue=peraccount"Enable the offering:
cmk updateNetworkOffering id=<offering-uuid> state=EnabledThe
serviceCapabilityListentries must match the values declared in the extension'snetwork.service.capabilitiesdetail. If the extension's JSON does not declare a capability value for a service, CloudStack accepts any value (or no value) without error.
cmk createNetwork \
name=my-network \
displaytext="My isolated network" \
networkofferingid=<offering-uuid> \
zoneid=<zone-uuid>When a VM is first deployed into this network, CloudStack calls
NetworkExtensionElement.implement(), which triggers the implement-network command:
# Management server executes:
network-namespace.sh implement-network \
--network-id 42 \
--vlan 100 \
--gateway 10.0.1.1 \
--cidr 10.0.1.0/24
# network-namespace.sh SSHes to the host and runs inside the host:
network-namespace-wrapper.sh implement-network \
--network-id 42 \
--vlan 100 \
--gateway 10.0.1.1 \
--cidr 10.0.1.0/24The wrapper creates a VLAN sub-interface and Linux bridge, a guest veth pair
(vh-100-2a/vn-100-2a), assigns the gateway IP to the namespace veth,
enables IP forwarding inside the namespace, and creates per-network iptables
chains: CS_EXTNET_42_PR (nat PREROUTING), CS_EXTNET_42_POST (nat
POSTROUTING), and CS_EXTNET_FWD_42 (filter FORWARD).
Note on iptables chains:
Chain Table Purpose CS_EXTNET_<id>_PRnatPREROUTING DNAT (port-forward, static-NAT) CS_EXTNET_<id>_POSTnatPOSTROUTING SNAT (source-NAT, static-NAT outbound) CS_EXTNET_FWD_<id>filterFORWARD catch-all for this network CS_EXTNET_FWRULES_<id>filterFirewall egress rules (inserted at pos 1 of FWD chain) CS_EXTNET_FWI_<pubIp>mangleFirewall ingress per public IP (PREROUTING, before DNAT) CS_EXTNET_ACL_<id>filterVPC Network ACL (both ingress and egress; pos 1 of FWD) CS_EXTNET_<vpc-id>_VPC_POSTnatVPC-level SNAT for entire VPC CIDR
cmk associateIpAddress networkid=<network-uuid>CloudStack calls applyIps() which issues assign-ip with --source-nat true
for the source-NAT IP:
network-namespace.sh assign-ip \
--network-id 42 \
--vlan 100 \
--public-ip 203.0.113.10 \
--source-nat true \
--gateway 10.0.1.1 \
--cidr 10.0.1.0/24The wrapper:
- Creates public VLAN sub-interface
eth1.<pvlan>and bridgebreth1-<pvlan>on the host. - Creates veth pair
vph-<pvlan>-42(host, in bridge) /vpn-<pvlan>-42(namespace). - Assigns
203.0.113.10/32tovpn-<pvlan>-42inside the namespace. - Adds host route
203.0.113.10/32 dev vph-<pvlan>-42so the host can reach it. - Adds an iptables SNAT rule in
CS_EXTNET_42_POST: traffic from10.0.1.0/24outvpn-<pvlan>-42→ source203.0.113.10. - Adds an iptables FORWARD ACCEPT rule in
CS_EXTNET_FWD_42for the guest CIDR. - If
--public-gatewayis set, adds/replaces the namespace default route viavpn-<pvlan>-42.
When the IP is released (via disassociateIpAddress), release-ip is called,
which removes all associated rules and the IP address.
# Enable static NAT: map public IP 203.0.113.20 to VM private IP 10.0.1.5
cmk enableStaticNat \
ipaddressid=<public-ip-uuid> \
virtualmachineid=<vm-uuid> \
networkid=<network-uuid>CloudStack calls applyStaticNats() → add-static-nat:
network-namespace.sh add-static-nat \
--network-id 42 \
--vlan 100 \
--public-ip 203.0.113.20 \
--private-ip 10.0.1.5iptables rules added (all run inside the namespace via ip netns exec):
# DNAT inbound (CS_EXTNET_42_PR = nat PREROUTING chain)
iptables -t nat -A CS_EXTNET_42_PR -d 203.0.113.20 -j DNAT --to-destination 10.0.1.5
# SNAT outbound (CS_EXTNET_42_POST = nat POSTROUTING chain)
iptables -t nat -A CS_EXTNET_42_POST -s 10.0.1.5 -o vpn-<pvlan>-42 -j SNAT --to-source 203.0.113.20
# FORWARD inbound + outbound (CS_EXTNET_FWD_42 = filter FORWARD chain)
iptables -t filter -A CS_EXTNET_FWD_42 -d 10.0.1.5 -o vn-100-2a -j ACCEPT
iptables -t filter -A CS_EXTNET_FWD_42 -s 10.0.1.5 -i vn-100-2a -j ACCEPT# Disable static NAT
cmk disableStaticNat ipaddressid=<public-ip-uuid>CloudStack calls delete-static-nat, which removes all four rules above.
# Forward TCP port 2222 on public IP 203.0.113.20 → VM port 22
cmk createPortForwardingRule \
ipaddressid=<public-ip-uuid> \
privateport=22 \
publicport=2222 \
protocol=TCP \
virtualmachineid=<vm-uuid> \
networkid=<network-uuid>CloudStack calls applyPFRules() → add-port-forward:
network-namespace.sh add-port-forward \
--network-id 42 \
--vlan 100 \
--public-ip 203.0.113.20 \
--public-port 2222 \
--private-ip 10.0.1.5 \
--private-port 22 \
--protocol TCPiptables rules added (inside the namespace):
# DNAT inbound (CS_EXTNET_42_PR = nat PREROUTING chain)
iptables -t nat -A CS_EXTNET_42_PR -p tcp -d 203.0.113.20 --dport 2222 \
-j DNAT --to-destination 10.0.1.5:22
# FORWARD (CS_EXTNET_FWD_42 = filter FORWARD chain)
iptables -t filter -A CS_EXTNET_FWD_42 -p tcp -d 10.0.1.5 --dport 22 \
-o vn-100-2a -j ACCEPTPort ranges (e.g. 80:90) are supported and passed verbatim to iptables --dport.
# Delete the rule
cmk deletePortForwardingRule id=<rule-uuid>This calls delete-port-forward which removes the DNAT and FORWARD rules.
cmk deleteNetwork id=<network-uuid>CloudStack calls shutdown-network (to clean up active state) then
destroy-network (full removal):
network-namespace.sh shutdown-network --network-id 42 --vlan 100
network-namespace.sh destroy-network --network-id 42 --vlan 100shutdown-network wrapper actions:
- Removes iptables jump rules and flushes/deletes per-network chains
(
CS_EXTNET_42_PR,CS_EXTNET_42_POST,CS_EXTNET_FWD_42). - Stops dnsmasq, haproxy, apache2, and password-server processes.
- Deletes public veth pairs (
vph-<pvlan>-42/vpn-<pvlan>-42) that were created duringassign-ip(read from state files). - Deletes the guest veth host-side (
vh-100-2a). - For isolated networks: deletes the namespace
cs-net-42. - For VPC tier networks: preserves the shared namespace
cs-vpc-<vpcId>.
destroy-network wrapper actions (similar to shutdown-network, plus):
- Deletes the guest veth host-side (
vh-100-2a). - Deletes public veth pairs owned by this tier.
- Stops per-network services.
- Removes per-network state directory
/var/lib/cloudstack/<ext-name>/network-42/. - For isolated networks: deletes the namespace
cs-net-42. - For VPC tier networks: deregisters this tier from the VPC — namespace is
only removed by a subsequent
destroy-vpccall.
The host bridge
breth1-100and VLAN sub-interfaceeth1.100are not removed — they may still be used by other networks or for VM connectivity.
# Disable and delete the NSP
cmk updateNetworkServiceProvider id=<nsp-uuid> state=Disabled
cmk deleteNetworkServiceProvider id=<nsp-uuid>
# Remove external network device credentials (if any)
# Device credentials are stored as extension_resource_map_details for the
# extension registration. Remove or update them via `updateRegisteredExtension`
# (set cleanupdetails=true to wipe all details) or by supplying new details.
# Example: clear all registration details for a physical network:
cmk updateRegisteredExtension \
extensionid=<extension-uuid> \
resourcetype=PhysicalNetwork \
resourceid=<phys-net-uuid> \
cleanupdetails=true
# Unregister the extension from the physical network
cmk unregisterExtension \
id=<extension-uuid> \
resourcetype=PhysicalNetwork \
resourceid=<phys-net-uuid>
# Delete the extension
# (only possible once it is unregistered from all physical networks)
cmk deleteExtension id=<extension-uuid>Because each extension is registered as its own NSP (named after the extension), multiple independent external network providers can coexist on the same physical network:
# Register two extensions, each backed by a different device
cmk registerExtension id=<ext-a-uuid> resourcetype=PhysicalNetwork resourceid=<pn-uuid>
cmk registerExtension id=<ext-b-uuid> resourcetype=PhysicalNetwork resourceid=<pn-uuid>
# Store device connection details as registration details for each extension.
# Details are stored in extension_resource_map_details for the registration.
# Example: set hosts and guest/public network devices for ext-a on the physical network:
cmk updateRegisteredExtension \
extensionid=<ext-a-uuid> \
resourcetype=PhysicalNetwork \
resourceid=<pn-uuid> \
"details[0].hosts=10.0.0.1,10.0.0.2" \
"details[1].guest.network.device=eth1" \
"details[2].public.network.device=eth1"When creating network offerings, reference the specific extension name:
# Network offering backed by ext-a-name
cmk createNetworkOffering ... \
"serviceProviderList[0].provider=ext-a-name" ...
# Network offering backed by ext-b-name
cmk createNetworkOffering ... \
"serviceProviderList[0].provider=ext-b-name" ...CloudStack resolves which extension to call by:
- Looking up the service provider name stored in
ntwk_service_mapfor the guest network. - Finding the registered extension on the physical network whose name matches that provider name.
- Calling
NetworkExtensionElementscoped to that specific provider/extension (viaNetworkExtensionElement.withProviderName()).
The network-namespace extension supports IPv6 guest networks via stateless address auto-configuration
(SLAAC) using radvd (Router Advertisement Daemon), mirroring the approach used by CloudStack's
built-in VPC router (CsVpcGuestNetwork.py).
When network_ip6_gateway and network_ip6_cidr are present in the implement-network payload:
implement-networkenables IPv6 in the namespace (net.ipv6.conf.all.disable_ipv6=0, forwarding on, DAD and temporary addresses disabled) and assigns the IPv6 gateway address to the guest veth interface inside the namespace.config-dhcp-subnetorconfig-dns-subnetwrites/radvd/radvd.confand starts radvd inside the namespace. radvd sends Router Advertisements on the guest veth, advertising the/64(or configured) prefix so VMs can self-configure via SLAAC.add-dns-entryadds both an A record (IPv4) and an AAAA record (IPv6, fromip6_address) to the dnsmasq hosts file so guests can resolve VM hostnames over IPv6.remove-dhcp-subnet/shutdown-network/destroy-networkstops radvd and removes its state directory.
The generated radvd.conf follows the same format as CsVpcGuestNetwork.py:
interface <guest-veth>
{
AdvSendAdvert on;
MinRtrAdvInterval 5;
MaxRtrAdvInterval 15;
prefix <network_ip6_gateway>/<prefix-len>
{
AdvOnLink on;
AdvAutonomous on;
};
RDNSS <dns6-server> # one block per server from dns6
{
AdvRDNSSLifetime 30;
};
};
For VPC networks the shared namespace is created by implement-vpc (IPv6-neutral — no IPv6 is
enabled or disabled at that stage). Each VPC tier network runs its own implement-network, which
independently enables or disables IPv6 on its own guest veth. This allows mixed VPC topologies
where some tiers have IPv6 and others do not.
Each IPv6 tier also runs its own radvd instance inside the shared namespace, bound to its own guest veth. radvd correctly handles multiple instances in the same namespace as long as each instance is bound to a distinct interface.
IPv6 state is persisted under the per-network state directory:
| File | Content |
|---|---|
network-<id>/ip6-gateway |
IPv6 gateway address assigned to the namespace veth |
network-<id>/ip6-cidr |
IPv6 CIDR of the guest subnet |
network-<id>/dns6 |
Comma-separated IPv6 DNS server list (RDNSS) |
network-<id>/radvd/radvd.conf |
Generated radvd configuration |
network-<id>/radvd/radvd.pid |
PID of the running radvd process |
CloudStack now invokes the wrapper through payload files.
The network-namespace-wrapper.sh script runs on the remote KVM device.
It receives the command as its first positional argument followed by named
--option value pairs.
All commands:
- Write timestamped entries to
/var/log/cloudstack/extensions/<ext-name>/<ext-name>.log. - Use a per-network flock file (
${STATE_DIR}/lock-network-<id>) — orlock-vpc-<id>for VPC networks — to serialise concurrent operations. - Persist state under
/var/lib/cloudstack/<ext-name>/network-<network-id>/(orvpc-<vpc-id>/for VPC-wide shared state such as public IPs).
Called when CloudStack activates the network (typically on first VM deploy).
network-namespace-wrapper.sh implement-network \
--network-id <id> \
--vlan <vlan-id> \
--gateway <gateway-ip> \
--cidr <cidr> \
[--extension-ip <ext-ip>] \
[--vpc-id <vpc-id>]
Actions:
- Create namespace
cs-vpc-<vpc-id>(VPC) orcs-net-<network-id>(isolated). - Resolve
GUEST_ETHfromguest.network.deviceinphysical-network-extension-details(defaults toeth1when absent). - Create VLAN sub-interface
GUEST_ETH.<vlan>on the host. - Create host bridge
br<GUEST_ETH>-<vlan>and attachGUEST_ETH.<vlan>to it. - Create veth pair
vh-<vlan>-<id>(host, in bridge) /vn-<vlan>-<id>(namespace). - Assign
<extension-ip>/<prefix>(or<gateway>/<prefix>when--extension-ipis not given) tovn-<vlan>-<id>inside the namespace. When the extension IP differs from the gateway a default route via the gateway is also added inside the namespace. - IPv6 handling:
- When
--network-ip6-gateway/--network-ip6-cidrare provided: enable IPv6 forwarding inside the namespace (disable DAD, enable forwarding and accept_ra) and assign the IPv6 gateway address tovn-<vlan>-<id>. - When no IPv6 is configured: disable IPv6 on the guest veth interface. For standalone networks the global namespace disable is also applied; for VPC tier networks the global setting is left unchanged to avoid clobbering sibling tiers that may have IPv6.
- When
- Enable IPv4 IP forwarding inside the namespace.
- Create iptables chains
CS_EXTNET_<id>_PR(nat PREROUTING DNAT),CS_EXTNET_<id>_POST(nat POSTROUTING SNAT), andCS_EXTNET_FWD_<id>(filter FORWARD). - Save VLAN, gateway, CIDR, extension-ip, IPv6 gateway, IPv6 CIDR, and namespace to state files.
Called when a network is shut down (may be restarted later).
network-namespace-wrapper.sh shutdown-network \
--network-id <id> [--vlan <vlan-id>] [--vpc-id <vpc-id>]
Actions:
- Remove iptables jump rules for this network and flush/delete its chains
(
CS_EXTNET_<id>_PR,CS_EXTNET_<id>_POST,CS_EXTNET_FWD_<id>). - Delete public veth pairs (
vph-<pvlan>-<id>/vpn-<pvlan>-<id>) that are owned by this tier (guarded by per-IP.tierstate files). - Delete the guest veth host-side (
vh-<vlan>-<id>). - Stop dnsmasq, haproxy, apache2, and password-server processes.
- For isolated networks: delete the namespace
cs-net-<id>. - For VPC tier networks: preserve the shared namespace
cs-vpc-<vpc-id>.
Called when the network is permanently removed.
network-namespace-wrapper.sh destroy-network \
--network-id <id> [--vlan <vlan-id>] [--vpc-id <vpc-id>]
Actions:
- Delete guest veth host-side (
vh-<vlan>-<id>). - Delete public veth pairs that belong to this tier (guarded by
.tierstate files). - Stop dnsmasq, haproxy, apache2, and password-server processes.
- Remove per-network state directory
network-<id>/. - For isolated networks: delete the namespace
cs-net-<id>. - For VPC tier networks: deregister this tier from the VPC tracking directory
(
vpc-<vpc-id>/tiers/<network-id>) — the namespace is preserved and will be removed by a subsequentdestroy-vpccall.
The host bridge
br<GUEST_ETH>-<vlan>and VLAN sub-interfaceGUEST_ETH.<vlan>are NOT removed on destroy — they may still be used by other networks or for VM connectivity.
These commands manage VPC-level state. Called by NetworkExtensionElement when
implementing, shutting down, or destroying a VPC (before or after per-tier
network operations).
network-namespace-wrapper.sh implement-vpc \
--vpc-id <vpc-id> \
[--vpc-cidr <vpc-cidr>] \
[--public-ip <ip>] [--public-vlan <pvlan>] \
[--public-gateway <gw>] [--public-cidr <cidr>] \
[--source-nat true|false]
Actions:
- Create the shared VPC namespace
cs-vpc-<vpc-id>(idempotent). - Enable IPv4 IP forwarding inside the namespace.
IPv6 is managed per-tier by
implement-network(each tier enables or disables IPv6 on its own guest veth without affecting sibling tiers). - Optionally, when
--source-nat true,--public-ip, and--public-vlanare all provided and--vpc-cidr(VPC CIDR) is given:- Create public veth pair
vph-<pvlan>-<vpc-id>(host) /vpn-<pvlan>-<vpc-id>(namespace). - Assign
<public-ip>tovpn-<pvlan>-<vpc-id>inside the namespace. - Set namespace default route via
--public-gateway(if given). - Add VPC-level SNAT rule in chain
CS_EXTNET_<vpc-id>_VPC_POST: all VPC traffic (<vpc-cidr>) outvpn-<pvlan>-<vpc-id>→<public-ip>.
- Create public veth pair
- Save VPC namespace name and CIDR to
/var/lib/cloudstack/<ext-name>/vpc-<vpc-id>/.
This command runs before any tier networks are implemented. Tier networks inherit the same namespace.
network-namespace-wrapper.sh update-vpc-source-nat-ip \
--vpc-id <vpc-id> \
--public-ip <new-source-nat-ip> \
[--vpc-cidr <vpc-cidr>] \
[--public-vlan <pvlan>] \
[--public-gateway <gw>] \
[--public-cidr <cidr>] \
[--source-nat true|false]
Actions:
- Ensure the target public veth pair exists (
vph-<pvlan>-<vpc-id>/vpn-<pvlan>-<vpc-id>) and assign the new public IP inside the VPC namespace. - Update host and namespace routes for the new source NAT egress path:
- keep host route
<public-ip>/32viavph-<pvlan>-<vpc-id> - replace namespace default route via
--public-gatewayonvpn-<pvlan>-<vpc-id>when provided.
- keep host route
- Rebuild VPC SNAT chain
CS_EXTNET_<vpc-id>_VPC_POSTso exactly one SNAT rule remains:-s <vpc-cidr> -o vpn-<pvlan>-<vpc-id> -j SNAT --to-source <public-ip>.
- Reconcile persisted VPC IP markers under
/var/lib/cloudstack/<ext-name>/vpc-<vpc-id>/ips/:- set the new source NAT IP file to
true - set all other VPC public IP marker files to
false - persist/update
<ip>.pvlanfor the new source NAT IP.
- set the new source NAT IP file to
This command is used by
NetworkExtensionElement.updateVpcSourceNatIp()whenupdateVPCis called withsourcenatipaddress; it avoids full VPC restart.
network-namespace-wrapper.sh shutdown-vpc \
--vpc-id <vpc-id>
Actions:
- Delete the VPC namespace
cs-vpc-<vpc-id>(which removes all interfaces inside it, including per-tier veth pairs).
Called after all tier networks have been shut down. The namespace itself is the only resource removed — any host-side bridges and VLAN sub-interfaces are left intact.
network-namespace-wrapper.sh destroy-vpc \
--vpc-id <vpc-id>
Actions:
- Delete the VPC namespace
cs-vpc-<vpc-id>(if it still exists). - Remove VPC-wide state directory
/var/lib/cloudstack/<ext-name>/vpc-<vpc-id>/.
This is the final cleanup step; after this, all VPC namespace state is gone.
Called when a public IP is associated with the network (including source NAT).
network-namespace-wrapper.sh assign-ip \
--network-id <id> \
--vlan <guest-vlan> \
--public-ip <ip> \
--source-nat true|false \
--gateway <guest-gw> \
--cidr <guest-cidr> \
--public-vlan <pvlan> \
[--public-gateway <pub-gw>] \
[--public-cidr <pub-cidr>] \
[--vpc-id <vpc-id>]
Actions:
- Resolve
PUB_ETHfrompublic.network.deviceinphysical-network-extension-details(defaults toeth1when absent). - Create VLAN sub-interface
PUB_ETH.<pvlan>and bridgebr<PUB_ETH>-<pvlan>on the host. - Create veth pair
vph-<pvlan>-<id>(host) /vpn-<pvlan>-<id>(namespace). Attach host end tobr<PUB_ETH>-<pvlan>. - Assign
<public-ip>/32(or/<prefix>if--public-cidrgiven) tovpn-<pvlan>-<id>inside the namespace. - Add host route
<public-ip>/32 dev vph-<pvlan>-<id>so the host can reach it. - Send a gratuitous ARP (
arping -U) fromvpn-<pvlan>-<id>to flush stale ARP entries in the upstream gateway (requiresarpinginstalled on the KVM host; skipped silently when not available). - If
--public-gatewayis given, set/replace namespace default route viavpn-<pvlan>-<id>. - If
--source-nat true(and--vpc-idis not set):- SNAT rule:
<guest-cidr>outvpn-<pvlan>-<id>→<public-ip>(POSTROUTING chainCS_EXTNET_<id>_POST). - FORWARD ACCEPT for
<guest-cidr>towardsvpn-<pvlan>-<id>. - For VPC tiers (
--vpc-idpresent), SNAT is managed byimplement-vpc—assign-ipskips the SNAT rules.
- SNAT rule:
- Save public VLAN to state file
ips/<public-ip>.pvlanand owning tier toips/<public-ip>.tier(used byadd-static-nat,add-port-forward,release-ip).
Called when a public IP is released / disassociated from the namespace.
network-namespace-wrapper.sh release-ip \
--network-id <id> \
--public-ip <ip> \
[--public-vlan <pvlan>] \
[--public-cidr <pub-cidr>] \
[--vpc-id <id>]
Actions:
- Load
public_vlanfromips/<public-ip>.pvlanstate file. - Remove SNAT rule for guest CIDR →
<public-ip>. - Remove any DNAT rules targeting
<public-ip>from PREROUTING chain. - Remove host route
<public-ip>/32. - Remove IP address from
vpn-<pvlan>-<id>inside namespace. - If no other IPs share the same
<pvlan>/<id>combination, deletevph-<pvlan>-<id>(host veth). - Remove state files.
Called when Static NAT (one-to-one NAT) is enabled for a public IP.
network-namespace-wrapper.sh add-static-nat \
--network-id <id> \
--vlan <guest-vlan> \
--public-ip <public-ip> \
--private-ip <private-ip> \
[--vpc-id <vpc-id>]
The public_vlan for this IP is loaded from ips/<public-ip>.pvlan state
(written during assign-ip).
iptables rules added (chains CS_EXTNET_<id>_PR / _POST / FWD_<id>):
| Table | Chain | Rule |
|---|---|---|
nat |
CS_EXTNET_<id>_PR |
-d <public-ip> -j DNAT --to-destination <private-ip> |
nat |
CS_EXTNET_<id>_POST |
-s <private-ip> -o vpn-<pvlan>-<id> -j SNAT --to-source <public-ip> |
filter |
CS_EXTNET_FWD_<id> |
-d <private-ip> -o vn-<vlan>-<id> -j ACCEPT |
filter |
CS_EXTNET_FWD_<id> |
-s <private-ip> -i vn-<vlan>-<id> -j ACCEPT |
State saved to ${STATE_DIR}/network-<id>/static-nat/<public-ip>.
network-namespace-wrapper.sh delete-static-nat \
--network-id <id> \
--public-ip <public-ip> \
[--private-ip <private-ip>]
Removes all four rules added by add-static-nat. If --private-ip is omitted,
it is read from the state file.
Called when a Port Forwarding rule is added.
network-namespace-wrapper.sh add-port-forward \
--network-id <id> \
--vlan <vlan-id> \
--public-ip <public-ip> \
--public-port <port-or-range> \
--private-ip <private-ip> \
--private-port <port-or-range> \
--protocol tcp|udp
iptables rules added (inside the namespace):
| Table | Chain | Rule |
|---|---|---|
nat |
CS_EXTNET_<id>_PR |
-p <proto> -d <public-ip> --dport <public-port> -j DNAT --to-destination <private-ip>:<private-port> |
filter |
CS_EXTNET_FWD_<id> |
-p <proto> -d <private-ip> --dport <private-port> -o vn-<vlan>-<id> -j ACCEPT |
Port ranges (80:90) are passed verbatim to iptables --dport.
State saved to
${STATE_DIR}/network-<id>/port-forward/<proto>_<public-ip>_<public-port>.
network-namespace-wrapper.sh delete-port-forward \
--network-id <id> \
--public-ip <public-ip> \
--public-port <port-or-range> \
--private-ip <private-ip> \
--private-port <port-or-range> \
--protocol tcp|udp
Removes the DNAT and FORWARD rules added by add-port-forward.
Called when a VM NIC is being attached to the network (before the VM boots).
network-namespace-wrapper.sh prepare-nic \
--network-id <id> \
--vlan <vlan-id> \
--mac <mac> \
--ip <vm-ip> \
[--hostname <name>] \
[--default-nic true|false] \
[--gateway <gw>] \
[--cidr <cidr>] \
[--extension-ip <ip>] \
[--vpc-id <vpc-id>]
Actions (all idempotent; silently skipped when the service is not yet configured):
- If dnsmasq DHCP is active for the network — add a static lease
<mac>,<ip>[,<hostname>],infiniteto the hosts file. For secondary NICs (default_nic=false) the gateway DHCP option is suppressed via aset:norouter_<mac_tag>tag so the VM does not receive a competing default route from this NIC. - If dnsmasq DNS is active — add a
<ip> <hostname>line to the hosts file. - Sends a SIGHUP / reload to dnsmasq so the new entries take effect immediately.
Called when a VM NIC is being detached from the network (after the VM stops).
network-namespace-wrapper.sh release-nic \
--network-id <id> \
--mac <mac> \
--ip <vm-ip> \
[--vpc-id <vpc-id>]
Actions:
- Remove the MAC's DHCP static lease and any associated gateway-suppression option from dnsmasq.
- Remove the VM's hostname from the dnsmasq hosts file.
- Reload dnsmasq.
- Delete the per-VM metadata directory
${STATE_DIR}/network-<id>/metadata/<vm-ip>/. - Remove the VM's password entry from the passwords file.
Called when CloudStack applies or removes firewall rules for the network.
network-namespace-wrapper.sh apply-fw-rules \
--network-id <id> \
--vlan <vlan-id> \
[--vpc-id <vpc-id>]
The fw_rules field in the payload is a JSON object:
{
"default_egress_allow": true,
"cidr": "10.0.1.0/24",
"rules": [
{
"type": "ingress",
"protocol": "tcp",
"portStart": 22,
"portEnd": 22,
"publicIp": "203.0.113.10",
"sourceCidrs": ["0.0.0.0/0"]
},
{
"type": "egress",
"protocol": "all",
"sourceCidrs": ["0.0.0.0/0"]
}
]
}iptables design (two independent parts, both inside the namespace):
-
Ingress (mangle PREROUTING, per public IP): Per-public-IP chains
CS_EXTNET_FWI_<pubIp>check traffic before DNAT so the match is against the real public destination IP. Traffic not matched by explicit ALLOW rules is dropped. -
Egress (filter FORWARD, chain
CS_EXTNET_FWRULES_<networkId>): Inserted at position 1 ofCS_EXTNET_FWD_<networkId>. Applies thedefault_egress_allowpolicy (allow-by-default or deny-by-default) to VM outbound traffic on-i vn-<vlan>-<id>.
Apply Network ACL (Access Control List) rules for VPC networks.
network-namespace-wrapper.sh apply-network-acl \
--network-id <id> \
--vlan <vlan-id> \
[--vpc-id <vpc-id>]
The acl_rules field in the payload is a JSON array of ACL rule objects:
[
{
"id": 1,
"number": 100,
"trafficType": "Ingress",
"action": "Allow",
"protocol": "tcp",
"portStart": 80,
"portEnd": 80,
"sourceCidrs": ["0.0.0.0/0"]
},
{
"id": 2,
"number": 200,
"trafficType": "Egress",
"action": "Allow",
"protocol": "all",
"destCidrs": ["0.0.0.0/0"]
}
]iptables design:
- A single filter FORWARD chain
CS_EXTNET_ACL_<networkId>handles both ingress and egress traffic. It is inserted at position 1 ofCS_EXTNET_FWD_<networkId>so ACL rules take precedence over catch-all ACCEPT rules. RELATED,ESTABLISHEDtraffic is always accepted first (so active sessions are not interrupted).- Rules are applied in ascending
numberorder. - Ingress rules (
trafficType: Ingress) match-o vn-<vlan>-<id>(traffic going into the VM subnet, optionally filtered by-d <tier-cidr>). - Egress rules (
trafficType: Egress) match-i vn-<vlan>-<id>(traffic from the VM subnet, withsourceCidrsused as destination filter-d). - A terminal DROP rule at the end of the chain enforces the implicit deny policy.
Configure or tear down dnsmasq DHCP service for the network inside the namespace.
config-dhcp-subnet arguments:
network-namespace-wrapper.sh config-dhcp-subnet \
--network-id <id> \
--gateway <gw> \
--cidr <cidr> \
[--dns <dns-server>] \
[--domain <domain>] \
[--vpc-id <vpc-id>]
Actions: writes a dnsmasq configuration file under
${STATE_DIR}/network-<id>/dnsmasq/ and starts or reloads the dnsmasq process
inside the namespace. DNS on port 53 is disabled by config-dhcp-subnet
(use config-dns-subnet to enable it).
remove-dhcp-subnet arguments:
network-namespace-wrapper.sh remove-dhcp-subnet --network-id <id>
Actions: stops dnsmasq and removes the dnsmasq configuration directory.
Add or remove a static DHCP host reservation (MAC → IP mapping) from dnsmasq.
network-namespace-wrapper.sh add-dhcp-entry \
--network-id <id> \
--mac <mac> \
--ip <vm-ip> \
[--hostname <name>] \
[--default-nic true|false]
When --default-nic false, the DHCP option 3 (default gateway) is suppressed
for that MAC so the VM does not get a competing default route via a secondary NIC.
network-namespace-wrapper.sh remove-dhcp-entry \
--network-id <id> \
--mac <mac>
Set extra DHCP options for a specific NIC (identified by --nic-id) using a
JSON map of option-code → value pairs.
network-namespace-wrapper.sh set-dhcp-options \
--network-id <id> \
--nic-id <nic-id> \
--options '{"119":"example.com"}'
Enable or disable DNS (port 53) in the dnsmasq instance.
network-namespace-wrapper.sh config-dns-subnet \
--network-id <id> \
--gateway <gw> \
--cidr <cidr> \
[--extension-ip <ip>] \
[--domain <domain>] \
[--vpc-id <vpc-id>]
Actions: like config-dhcp-subnet but enables DNS on port 53. Also registers a
data-server hostname entry (using --extension-ip if provided, otherwise
--gateway) for metadata service discovery.
network-namespace-wrapper.sh remove-dns-subnet --network-id <id>
Actions: disables DNS (rewrites config to disable port 53) but keeps DHCP running.
Add or remove a hostname → IP mapping in the dnsmasq hosts file.
network-namespace-wrapper.sh add-dns-entry \
--network-id <id> \
--ip <vm-ip> \
--hostname <name>
network-namespace-wrapper.sh remove-dns-entry \
--network-id <id> \
--ip <vm-ip>
Write the full VM metadata/userdata/password set for a VM in a single call. Called on network restart and VM deploy.
network-namespace-wrapper.sh save-vm-data \
--network-id <id> \
--ip <vm-ip>
The vm_data field in the payload is a JSON array of {dir, file, content}
entries (same format as generateVmData() in the Java layer). Each content
value is a plain UTF-8 string. Writes files under
${STATE_DIR}/network-<id>/metadata/<vm-ip>/latest/. After writing, starts or
reloads both the apache2 metadata HTTP service (port 80) and the
VR-compatible password server (port 8080) inside the namespace.
Granular variants that write individual VM metadata fields:
network-namespace-wrapper.sh save-userdata --network-id <id> --ip <vm-ip> --userdata <plain>
network-namespace-wrapper.sh save-password --network-id <id> --ip <vm-ip> --password <plain>
network-namespace-wrapper.sh save-sshkey --network-id <id> --ip <vm-ip> --sshkey <plain>
network-namespace-wrapper.sh save-hypervisor-hostname \
--network-id <id> --ip <vm-ip> --hypervisor-hostname <name>
Each command writes the relevant file and restarts/reloads apache2 (and
the password server, for save-password).
Apply or revoke load-balancing rules via haproxy inside the namespace.
network-namespace-wrapper.sh apply-lb-rules \
--network-id <id> \
--lb-rules <json-array> \
[--vpc-id <vpc-id>]
--lb-rules is a JSON array of LB rule objects. Set "revoke": true on a
rule to remove it. The wrapper regenerates the haproxy configuration from the
persistent per-rule JSON files under ${STATE_DIR}/network-<id>/haproxy/ and
reloads haproxy inside the namespace. haproxy is stopped when no active rules
remain.
Batch-restore DHCP/DNS/metadata/services for all VMs on a network in a single call. Invoked on network restart to rebuild all state at once instead of N per-VM calls.
network-namespace-wrapper.sh restore-network \
--network-id <id> \
[--gateway <gw>] [--cidr <cidr>] [--dns <dns>] \
[--domain <dom>] [--extension-ip <ip>] [--vpc-id <vpc-id>]
The restore_data field in the payload is a JSON object (see
buildRestoreNetworkData() in NetworkExtensionElement.java).
network-namespace-wrapper.sh custom-action \
<payload-file> \
<timeout-seconds>
CloudStack now writes the custom-action request to a temporary JSON payload file and passes that file to the wrapper script. The payload contains the network or VPC identifiers, the action name, the caller-supplied action parameters, and the extension detail blobs that used to be forwarded as individual CLI flags.
Expected payload keys:
| JSON key | Description |
|---|---|
network_id |
Network ID for network-level actions |
vpc_id |
VPC ID for VPC-level actions |
action |
Custom action name |
action-params |
Caller-supplied JSON object for the action |
physical_network_extension_details |
Physical-network extension details JSON |
network_extension_details |
Per-network / per-VPC extension details JSON |
Built-in actions:
| Action | Description |
|---|---|
reboot-device |
Bounces the guest veth pair (vh-<vlan>-<id> down → up) |
dump-config |
Prints namespace IP addresses, iptables rules, and per-network state to stdout |
list-firewall-rules |
List iptables rules inside the namespace |
pbr-create-table |
Create or update a routing-table entry in /etc/iproute2/rt_tables |
pbr-delete-table |
Remove a routing-table entry from /etc/iproute2/rt_tables |
pbr-list-tables |
List non-comment routing-table entries from /etc/iproute2/rt_tables |
pbr-add-route |
Add/replace an ip route entry in a specific routing table inside the namespace |
pbr-delete-route |
Delete an ip route entry from a specific routing table inside the namespace |
pbr-list-routes |
List routes from one table (or all tables) inside the namespace |
pbr-add-rule |
Add an ip rule policy rule mapped to a specific routing table inside the namespace |
pbr-delete-rule |
Delete an ip rule policy rule mapped to a specific routing table inside the namespace |
pbr-list-rules |
List policy rules (or only rules for one table) inside the namespace |
PBR action parameter keys (action-params JSON in the payload file):
| Action | Required keys | Optional keys |
|---|---|---|
pbr-create-table |
table-id (or id), table-name (or table) |
— |
pbr-delete-table |
table-id or table-name |
— |
pbr-list-tables |
— | — |
pbr-add-route |
table, route |
— |
pbr-delete-route |
table, route |
— |
pbr-list-routes |
— | table |
pbr-add-rule |
table, rule |
— |
pbr-delete-rule |
table, rule |
— |
pbr-list-rules |
— | table |
Examples (equivalent to direct Linux commands):
{"table-id":"100","table-name":"isp1"}→100 isp1{"table":"isp1","route":"default via 192.168.1.1 dev eth0"}{"table":"vpn1","route":"default dev wg0"}{"table":"isp1","rule":"from 10.10.1.0/24"}{"table":"vpn1","rule":"to 10.10.2.0/24"}
To add custom actions, place an executable script at
${STATE_DIR}/hooks/custom-action-<name>.sh
(e.g. /var/lib/cloudstack/network-namespace/hooks/custom-action-<name>.sh).
Unknown action names are delegated to the hook if present; otherwise the command
fails with a descriptive error.
{
"physical-network-extension-details": {},
"network-extension-details": {},
"payload": {}
}For custom-action, payload is not nested; command fields are top-level.
| Top-level key | Description |
|---|---|
physical-network-extension-details |
All extension_resource_map_details plus physical network metadata automatically added by NetworkExtensionElement (see table below). |
network-extension-details |
Per-network opaque JSON blob (selected host, namespace). |
These keys are explicitly set when calling registerExtension:
| JSON key | Description |
|---|---|
hosts |
Comma-separated list of candidate host IPs for HA selection |
host |
Single host IP (used when hosts is absent) |
port |
SSH port — default: 22 |
username |
SSH user — default: root |
password |
SSH password via sshpass — sensitive, not logged |
sshkey |
PEM-encoded SSH private key — sensitive, not logged; preferred over password |
guest.network.device |
Host NIC for guest (internal) traffic, e.g. eth1 — defaults to eth1 when absent |
public.network.device |
Host NIC for public (NAT/external) traffic, e.g. eth1 — defaults to eth1 when absent |
This key is automatically injected by NetworkExtensionElement from the
physical network record:
| JSON key | Description |
|---|---|
physicalnetworkname |
Physical network name from CloudStack DB |
The wrapper script uses guest.network.device (and public.network.device) to
name bridges as br<eth>-<vlan> and veth pairs as vh-<vlan>-<id> /
vn-<vlan>-<id> (guest) and vph-<pvlan>-<id> / vpn-<pvlan>-<id> (public).
| JSON key | Description |
|---|---|
host |
Previously selected host IP (set by ensure-network-device) |
namespace |
Linux network namespace name (e.g. cs-net-<networkId> or cs-vpc-<vpcId>) |
payload key |
Description |
|---|---|
network_id |
Network ID — CHOSEN_ID for veth names is <vpc_id> when VPC, else <network_id> |
vlan |
Guest VLAN tag |
zone_id |
CloudStack zone ID |
guest_type |
Guest network type: "isolated", "shared", or "l2". The wrapper uses this to skip iptables / NAT / public-veth operations for shared networks. |
network_state |
Guest network state: "allocated", "setup", "implementing", "implemented", "shutdown" or "destroy". |
gateway |
Guest network gateway |
cidr |
Guest network CIDR |
vpc_id |
Present when the network belongs to a VPC; namespace becomes cs-vpc-<vpcId> |
network_ip6_gateway |
Guest IPv6 gateway address (assigned to the namespace veth), when configured |
network_ip6_cidr |
Guest IPv6 CIDR (e.g. 2001:db8:1::/64), when configured |
extension_ip |
IP for DHCP/DNS/metadata service — equals gateway when SourceNat/Gateway is active, otherwise a dedicated placeholder IP |
dns |
Comma-separated IPv4 DNS server list |
dns6 |
Comma-separated IPv6 DNS server list (advertised via RDNSS in radvd RA) |
domain |
Network domain suffix |
current_details |
ensure-network-device only — previous selected-device JSON, used to preserve host affinity |
payload key |
Description |
|---|---|
nic_id |
CloudStack numeric NIC ID |
nic_uuid |
NIC UUID — matches external_ids:iface-id written by the KVM agent |
mac |
VM NIC MAC address |
ip |
VM NIC IPv4 address |
gateway |
VM NIC IPv4 gateway |
netmask |
VM NIC IPv4 netmask |
default_nic |
"false" for secondary NICs (gateway DHCP option suppressed) |
device_id |
NIC device slot index |
ip6_address |
VM NIC IPv6 address, when configured |
ip6_gateway |
VM NIC IPv6 gateway, when available |
ip6_cidr |
VM NIC IPv6 CIDR, when available |
payload key |
Description |
|---|---|
public_ip |
Public IP address |
public_vlan |
Public IP VLAN tag |
public_gateway |
Gateway of the public IP segment |
public_cidr |
CIDR of the public IP |
source_nat |
"true" when this IP is the source-NAT IP |
private_ip |
VM private IP (NAT target) |
Custom-action parameters are embedded in the JSON payload file under
action-params. Hook scripts should read and decode the payload file directly
instead of expecting individual --action-params CLI arguments.
Example payload excerpt:
{
"action": "dump-config",
"network_id": "123",
"action-params": {
"key1": "value1",
"key2": "value2"
}
}Define custom actions per extension via the CloudStack API:
# Add a custom action to the extension
cmk addCustomAction \
extensionid=<ext-uuid> \
name=dump-config \
description="Dump iptables rules and bridge state" \
resourcetype=NetworkTrigger the action on a network, optionally with parameters:
cmk runNetworkCustomAction \
networkid=<network-uuid> \
actionid=<custom-action-uuid> \
"parameters[0].key=threshold" "parameters[0].value=90"# 1) Create action definitions (once per extension)
cmk addCustomAction extensionid=<ext-uuid> name=pbr-create-table resourcetype=Network
cmk addCustomAction extensionid=<ext-uuid> name=pbr-add-route resourcetype=Network
cmk addCustomAction extensionid=<ext-uuid> name=pbr-add-rule resourcetype=Network
cmk addCustomAction extensionid=<ext-uuid> name=pbr-list-tables resourcetype=Network
cmk addCustomAction extensionid=<ext-uuid> name=pbr-list-routes resourcetype=Network
cmk addCustomAction extensionid=<ext-uuid> name=pbr-list-rules resourcetype=Network
cmk addCustomAction extensionid=<ext-uuid> name=pbr-delete-rule resourcetype=Network
cmk addCustomAction extensionid=<ext-uuid> name=pbr-delete-route resourcetype=Network
cmk addCustomAction extensionid=<ext-uuid> name=pbr-delete-table resourcetype=Network
# 2) Execute against a network
cmk runNetworkCustomAction networkid=<network-uuid> actionid=<pbr-create-table-id> \
"parameters[0].key=table-id" "parameters[0].value=100" \
"parameters[1].key=table-name" "parameters[1].value=isp1"
cmk runNetworkCustomAction networkid=<network-uuid> actionid=<pbr-add-route-id> \
"parameters[0].key=table" "parameters[0].value=isp1" \
"parameters[1].key=route" "parameters[1].value=default via 192.168.1.1 dev eth0"
cmk runNetworkCustomAction networkid=<network-uuid> actionid=<pbr-add-rule-id> \
"parameters[0].key=table" "parameters[0].value=isp1" \
"parameters[1].key=rule" "parameters[1].value=from 10.10.1.0/24"CloudStack calls NetworkExtensionElement.runCustomAction(), which issues:
network-namespace.sh custom-action \
<payload-file> \
<timeout-seconds>network-namespace.sh SSHes to the device and runs network-namespace-wrapper.sh
with the same <command> <payload-file> <timeout-seconds> shape. The wrapper
extracts action, action-params, and extension-details fields from the payload.
The extension now supports VPC (Virtual Private Cloud) networks in addition to isolated networks. Key differences from isolated networks:
- Namespace sharing: All tiers of a VPC share a single namespace (
cs-vpc-<vpcId>) instead of each network getting its own (cs-net-<networkId>). - Host affinity: All tiers of a VPC land on the same KVM host via stable hash-based selection using the VPC ID as the routing key.
- VPC-level operations:
implement-vpc,update-vpc-source-nat-ip,shutdown-vpc,destroy-vpccommands manage VPC-wide state (namespace creation/teardown). - VPC tier operations:
implement-network,shutdown-network,destroy-networkcommands manage per-tier bridges and routes; the namespace is preserved across tier lifecycle operations.
The integration smoke test at
test/integration/smoke/test_network_extension_namespace.py
exercises the full lifecycle against real KVM hosts in the zone.
Management server
└── /usr/share/cloudstack-management/extensions/<ext-name>/
└── network-namespace.sh ← deployed / referenced by test
SSHes to KVM host
runs network-namespace-wrapper.sh <cmd> <args>
KVM host(s) in the zone
└── /etc/cloudstack/extensions/<ext-name>/
└── network-namespace-wrapper.sh ← copied to KVM hosts by test setup
creates cs-net-<id> or cs-vpc-<id> namespaces
manages bridges, veth pairs, iptables, dnsmasq, haproxy, apache2
The test covers:
- Create / list / update / delete external network device.
- Full network lifecycle: implement → assign-ip (source NAT) → static NAT → port forwarding → firewall rules → DHCP/DNS → shutdown / destroy.
- VPC multi-tier networks with shared namespace and automatic host affinity.
- VPC source NAT IP update flow (
test_09_vpc_source_nat_ip_update) including source NAT flag flip from old public IP to new public IP. - NSP state transitions: Disabled → Enabled → Disabled → Deleted.
- Tests
test_04,test_05,test_06(DHCP, DNS, LB) requirearping,dnsmasq, andhaproxyon the KVM hosts; the test skips them automatically if these tools are not installed. - Script cleanup on both management server and KVM hosts after each test.
Run the test:
cd test/integration/smoke
python -m pytest test_network_extension_namespace.py \
--with-marvin --marvin-config=<config.cfg> \
-s -a 'tags=advanced,smoke' 2>&1 | tee /tmp/extnet-test.logPrerequisites on KVM hosts:
iproute2(ip,ip netns)iptables+iptables-savearping(for GARP on IP assignment)dnsmasq(DHCP + DNS — required fortest_04/ DNS tests)haproxy(LB — required fortest_05/ LB tests)apache2/httpd(metadata HTTP service — required for UserData tests)python3(vm-data processing, haproxy config generation)util-linux(flock) (lock serialization)- SSH access from management server (root or sudo-capable user)
Prerequisites on the Marvin / test runner node:
- Python Marvin library installed (
pip install -r requirements.txt) - A valid Marvin config file pointing to the CloudStack environment
- The test runner must be able to SSH to the management server and to KVM hosts