How to Implement Custom Network Plugins with container-network-vmnet

Implementing custom network plugins with container-network-vmnet requires creating a Swift package that implements the Network protocol, configures a NetworkConfiguration object, and registers as an XPC service that the container runtime can discover in the plugins directory.

The apple/container repository provides a built-in network plugin called container-network-vmnet that interfaces with Apple's vmnet kernel extension to provide NAT and host-only networking for containers. Understanding how to implement custom network plugins with container-network-vmnet allows you to create specialized network configurations such as custom DHCP servers, specific bridge layouts, or pre-seeded IPv6 prefixes while maintaining full integration with the container runtime.

Understanding container-network-vmnet Architecture

The container-network-vmnet plugin operates as an XPC service that implements the Network protocol defined in ContainerNetworkServer. It provides two distinct operational modes controlled via the --variant flag, each suited for different macOS versions and networking requirements.

The Two Operation Modes

allocationOnly creates a lightweight NAT bridge available on all supported macOS versions. In this mode, the plugin only allocates an IPv4 subnet (defaulting to 192.168.64.1/24) and reports it to the container runtime without making any vmnet reservation. This implementation resides in Sources/Services/NetworkVmnet/Server/AllocationOnlyVmnetNetwork.swift.

reserved utilizes the vmnet reservation APIs to create a fully-controlled network supporting both IPv4 and IPv6. This mode requires macOS 26 or later (checked at runtime) and maintains ownership of the vmnet_network_ref. The plugin can expose the underlying XPC object to the container, enabling more advanced networking scenarios. This logic is implemented in Sources/Services/NetworkVmnet/Server/ReservedVmnetNetwork.swift.

Core Components and File Structure

The plugin architecture centers on several key files:

Creating a Custom Network Plugin

To build a custom network plugin that integrates with the container runtime, you must reuse the existing scaffolding while providing your own implementation of the network logic.

Step 1: Set Up the Swift Package Dependencies

Create a new Swift package that depends on the Container* modules, specifically ContainerPlugin, ContainerNetworkServer, and ContainerNetworkVmnetServer. These modules provide the protocols and types necessary to communicate with the container runtime.

Step 2: Configure the Plugin Metadata

Provide a PluginConfig (either config.toml or legacy config.json) that marks the plugin as a service by setting services = true. The plugin field should specify your custom identifier or reuse "container-network-vmnet" if extending the existing functionality. Place this configuration file adjacent to your binary.

Step 3: Implement the Network Protocol

Depending on your requirements, copy and modify one of the two base implementations:

  • For a simple NAT bridge, subclass or copy AllocationOnlyVmnetNetwork and adjust the default subnet or add logging.
  • For full control over the network interface, copy ReservedVmnetNetwork and modify the startNetwork function to enable custom DHCP configurations, set specific MAC addresses, or implement firewall rules.

Your implementation must conform to the Network protocol and handle the status, allocate, and lookup routes via the XPC server.

Step 4: Build the XPC Service Entry Point

Create a helper struct similar to NetworkVmnetHelper that parses command-line arguments and returns a Network-conforming object from its createNetwork function. This helper should start an XPC server (using DefaultNetworkService) that exposes the standard network routes to the container runtime.

Working with NetworkConfiguration and Network Modes

The NetworkConfiguration struct serves as the central configuration object that carries all network-creation parameters. When parsing CLI arguments in your custom plugin, you must instantiate this object with the appropriate values:

let cfg = try NetworkConfiguration(
    name: id,
    mode: mode,  // .nat or .hostOnly
    ipv4Subnet: ipv4Subnet.map { try CIDRv4($0) },
    ipv6Subnet: ipv6Subnet.map { try CIDRv6($0) },
    plugin: "my-custom-plugin",
    options: ["variant":"reserved"]  // or "allocationOnly"
)

The mode parameter determines whether the network operates as a NAT bridge or host-only interface, while the variant option in the dictionary selects between the lightweight allocationOnly implementation and the full reserved vmnet reservation.

Registering and Deploying Your Plugin

The PluginLoader automatically discovers plugins placed under the user plugins directory at installRoot/libexec/container-plugins. To register your custom network plugin:

  1. Install your binary at <plugin-dir>/bin/<plugin-name>.
  2. Place your configuration file (config.toml or config.json) next to the binary.
  3. Ensure the binary implements the service entry point and XPC server registration.

When the container runtime initializes, PluginLoader calls your factory method, creates a Plugin object, and registers the launchd plist via PluginLoader.registerWithLaunchd.

Complete Implementation Example

The following Swift code demonstrates a complete custom network plugin entry point that reuses the ReservedVmnetNetwork implementation with custom configuration:

import ContainerPlugin
import ContainerNetworkServer
import ContainerNetworkVmnetServer
import ArgumentParser

// 1️⃣ Define the CLI entry point
@main
struct MyCustomNetwork: AsyncParsableCommand {
    static let configuration = CommandConfiguration(
        commandName: "my-network-plugin",
        abstract: "Custom vmnet network plugin",
        subcommands: [Start.self]
    )
}

// 2️⃣ Parse arguments and build a NetworkConfiguration
extension MyCustomNetwork {
    struct Start: AsyncParsableCommand {
        @Option(name: .shortAndLong, help: "Network identifier")
        var id: String

        @Option(name: .long, help: "Network mode (nat | hostOnly)")
        var mode: NetworkMode = .nat

        @Option(name: .customLong("subnet"), help: "IPv4 CIDR")
        var ipv4Subnet: String?

        @Option(name: .customLong("subnet-v6"), help: "IPv6 CIDR")
        var ipv6Subnet: String?

        func run() async throws {
            let cfg = try NetworkConfiguration(
                name: id,
                mode: mode,
                ipv4Subnet: ipv4Subnet.map { try CIDRv4($0) },
                ipv6Subnet: ipv6Subnet.map { try CIDRv6($0) },
                plugin: "my-network-plugin",
                options: ["variant":"reserved"]
            )
            
            // Create a ReservedVmnetNetwork (or your own subclass)
            let network = try ReservedVmnetNetwork(configuration: cfg, log: ServiceLogger())
            try await network.start()
            
            // Set up XPC server as in NetworkVmnetHelper+Start
            // ... server registration code ...
        }
    }
}

Summary

  • container-network-vmnet provides two operation modes: allocationOnly for basic IPv4 NAT bridging on all macOS versions, and reserved for full IPv4/IPv6 vmnet control requiring macOS 26+.
  • The plugin architecture relies on the NetworkVmnetHelper entry point, NetworkConfiguration for parameters, and ReservedVmnetNetwork or AllocationOnlyVmnetNetwork for implementation.
  • Custom plugins must implement the Network protocol, expose an XPC service, and be installed in installRoot/libexec/container-plugins for automatic discovery by PluginLoader.
  • You can extend existing functionality by copying the reference implementations in Sources/Services/NetworkVmnet/Server/ and modifying the network setup logic while maintaining the standard XPC communication interface.

Frequently Asked Questions

What is the difference between allocationOnly and reserved modes in container-network-vmnet?

The allocationOnly mode creates a simple NAT bridge where the plugin only allocates an IPv4 subnet and reports it to the container runtime without using vmnet reservation APIs. This mode works on all supported macOS versions. The reserved mode uses the vmnet reservation APIs to create a fully-controlled network with both IPv4 and IPv6 support, maintains a vmnet_network_ref, and can expose the underlying XPC object to containers, but requires macOS 26 or later.

How do I customize DHCP settings or MAC addresses in my network plugin?

To customize DHCP settings or MAC addresses, copy the ReservedVmnetNetwork implementation from Sources/Services/NetworkVmnet/Server/ReservedVmnetNetwork.swift and modify the startNetwork function. This function contains the vmnet C-API calls where you can configure custom DHCP ranges, set specific MAC addresses for interfaces, or add firewall rules before the network becomes active.

Where does the container runtime look for custom network plugins?

The container runtime discovers plugins through the PluginLoader mechanism, which automatically searches the user plugins directory located at installRoot/libexec/container-plugins. Your plugin binary must be installed at <plugin-dir>/bin/<plugin-name> with its configuration file adjacent to it, and the loader will handle registration with launchd via PluginLoader.registerWithLaunchd.

Can I use the existing container-network-vmnet binary with custom configuration instead of writing a new plugin?

Yes, you can reuse the existing container-network-vmnet binary by passing different --variant, --subnet, or --subnet-v6 arguments to achieve different network configurations. However, if you need logic not supported by the stock implementation (such as custom DHCP servers or specific bridge layouts), you must create a new plugin that imports the Container* modules and provides a custom implementation of the Network protocol.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →