From 140f3eef1a91390e97bb75b2514807d58a5b5410 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Sat, 2 Aug 2025 20:20:02 -0600 Subject: [PATCH 01/28] added support for docker macvlan network type. creates docker network and modifies containerlab host networking elements. --- runtime/docker/docker.go | 541 ++++++++++++++++++++++++++++++++++++--- types/types.go | 5 + 2 files changed, 517 insertions(+), 29 deletions(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index a1e4414c26..d5ade35fc1 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -14,6 +14,8 @@ import ( "strconv" "strings" "time" + "os/exec" + "net" "github.com/docker/docker/api/types/image" "github.com/docker/go-units" @@ -166,52 +168,239 @@ func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) { nctx, cancel := context.WithTimeout(ctx, d.config.Timeout) defer cancel() - // linux bridge name that is used by docker network + // Determine the driver to use (default to bridge if not specified) + driver := d.mgmt.MacvlanDriver + if driver == "" { + driver = "bridge" + } + + // linux bridge name that is used by docker network (only relevant for bridge driver) bridgeName := d.mgmt.Bridge log.Debugf("Checking if docker network %q exists", d.mgmt.Network) netResource, err := d.Client.NetworkInspect(nctx, d.mgmt.Network, networkapi.InspectOptions{}) + switch { case dockerC.IsErrNotFound(err): - bridgeName, err = d.createMgmtBridge(nctx, bridgeName) - if err != nil { - return err + // Network doesn't exist, create it based on driver type + switch driver { + case "bridge": + bridgeName, err = d.createMgmtBridge(nctx, bridgeName) + if err != nil { + return err + } + + case "macvlan": + err = d.createMgmtMacvlan(nctx) + if err != nil { + return err + } + // For macvlan, we don't need to track a bridge name + bridgeName = "" + + default: + return fmt.Errorf("unsupported network driver: %s", driver) } + case err == nil: + // Network exists, validate it matches expected driver log.Debugf("network %q was found. Reusing it...", d.mgmt.Network) - if len(netResource.ID) < 12 { - return fmt.Errorf("could not get bridge ID") + if netResource.Driver != driver { + return fmt.Errorf("existing network %q has driver %q but configuration specifies %q", + d.mgmt.Network, netResource.Driver, driver) } - switch d.mgmt.Network { - case "bridge": - bridgeName = "docker0" - default: - if netResource.Options["com.docker.network.bridge.name"] != "" { - bridgeName = netResource.Options["com.docker.network.bridge.name"] - } else { - bridgeName = "br-" + netResource.ID[:12] + + // Handle existing network based on driver type + if driver == "bridge" { + if len(netResource.ID) < 12 { + return fmt.Errorf("could not get bridge ID") + } + switch d.mgmt.Network { + case "bridge": + bridgeName = "docker0" + default: + if netResource.Options["com.docker.network.bridge.name"] != "" { + bridgeName = netResource.Options["com.docker.network.bridge.name"] + } else { + bridgeName = "br-" + netResource.ID[:12] + } } } - + // For macvlan, we just reuse the existing network without any special handling + default: return err } - if d.mgmt.Bridge == "" { - d.mgmt.Bridge = bridgeName + // Only set bridge name for bridge driver + if driver == "bridge" { + if d.mgmt.Bridge == "" { + d.mgmt.Bridge = bridgeName + } + + // get management bridge v4/6 addresses and save it under mgmt struct + // so that nodes can use this information prior to being deployed + // this was added to allow mgmt network gw ip to be available in a startup config templation step (ceos) + d.mgmt.IPv4Gw, d.mgmt.IPv6Gw, err = getMgmtBridgeIPs(bridgeName, netResource) + if err != nil { + return err + } + + log.Debugf("Docker network %q, bridge name %q", d.mgmt.Network, bridgeName) + return d.postCreateNetActions() } - // get management bridge v4/6 addresses and save it under mgmt struct - // so that nodes can use this information prior to being deployed - // this was added to allow mgmt network gw ip to be available in a startup config templation step (ceos) - d.mgmt.IPv4Gw, d.mgmt.IPv6Gw, err = getMgmtBridgeIPs(bridgeName, netResource) - if err != nil { - return err + // For macvlan networks + if driver == "macvlan" { + // Re-inspect to get gateway information if network was just created + if dockerC.IsErrNotFound(err) { + netResource, err = d.Client.NetworkInspect(nctx, d.mgmt.Network, networkapi.InspectOptions{}) + if err != nil { + return fmt.Errorf("failed to inspect newly created macvlan network: %w", err) + } + } + + // Extract gateway IPs from IPAM config for macvlan + for _, ipamConfig := range netResource.IPAM.Config { + if ipamConfig.Gateway != "" { + // Determine if it's IPv4 or IPv6 based on the address format + if strings.Count(ipamConfig.Gateway, ".") == 3 { + d.mgmt.IPv4Gw = ipamConfig.Gateway + } else if strings.Contains(ipamConfig.Gateway, ":") { + d.mgmt.IPv6Gw = ipamConfig.Gateway + } + } + } + + log.Debugf("Docker macvlan network %q created/reused with parent interface %q", d.mgmt.Network, d.mgmt.MacvlanParent) + return d.postCreateMacvlanActions() } - log.Debugf("Docker network %q, bridge name %q", d.mgmt.Network, bridgeName) + return nil +} - return d.postCreateNetActions() +// createMgmtMacvlan creates a macvlan network for management +func (d *DockerRuntime) createMgmtMacvlan(nctx context.Context) error { + // Validate parent interface is specified + if d.mgmt.MacvlanParent == "" { + return fmt.Errorf("macvlan-parent interface must be specified for macvlan driver") + } + + // Check if parent interface exists + if _, err := netlink.LinkByName(d.mgmt.MacvlanParent); err != nil { + return fmt.Errorf("parent interface %q not found: %v", d.mgmt.MacvlanParent, err) + } + + log.Info("Creating macvlan network", + "name", d.mgmt.Network, + "parent", d.mgmt.MacvlanParent, + "IPv4 subnet", d.mgmt.IPv4Subnet, + "IPv6 subnet", d.mgmt.IPv6Subnet, + "mode", d.mgmt.MacvlanMode, + "aux-address", d.mgmt.MacvlanAux) + + // Prepare IPAM configuration + var ipamConfig []networkapi.IPAMConfig + + // Handle IPv4 configuration + if d.mgmt.IPv4Subnet != "" && d.mgmt.IPv4Subnet != "auto" { + ipamCfg := networkapi.IPAMConfig{ + Subnet: d.mgmt.IPv4Subnet, + } + if d.mgmt.IPv4Gw != "" { + ipamCfg.Gateway = d.mgmt.IPv4Gw + } + if d.mgmt.IPv4Range != "" { + ipamCfg.IPRange = d.mgmt.IPv4Range + } + // Add aux address if specified + if d.mgmt.MacvlanAux != "" { + ipamCfg.AuxAddress = map[string]string{ + "host": d.mgmt.MacvlanAux, + } + } + ipamConfig = append(ipamConfig, ipamCfg) + } + + // Handle IPv6 configuration + var enableIPv6 bool + var ipv6_subnet string + + if d.mgmt.IPv6Subnet == "auto" { + var err error + ipv6_subnet, err = utils.GenerateIPv6ULASubnet() + if err != nil { + return err + } + } else { + ipv6_subnet = d.mgmt.IPv6Subnet + } + + if ipv6_subnet != "" { + ipamCfg := networkapi.IPAMConfig{ + Subnet: ipv6_subnet, + } + if d.mgmt.IPv6Gw != "" { + ipamCfg.Gateway = d.mgmt.IPv6Gw + } + if d.mgmt.IPv6Range != "" { + ipamCfg.IPRange = d.mgmt.IPv6Range + } + ipamConfig = append(ipamConfig, ipamCfg) + enableIPv6 = true + } + + // Prepare network options + netwOpts := map[string]string{ + "parent": d.mgmt.MacvlanParent, + } + + // Set macvlan mode (default to bridge if not specified) + macvlanMode := d.mgmt.MacvlanMode + if macvlanMode == "" { + macvlanMode = "bridge" + } + netwOpts["macvlan_mode"] = macvlanMode + + // Add any additional driver options from config + for k, v := range d.mgmt.DriverOpts { + log.Debug("Adding macvlan network driver option", "option", k, "value", v) + netwOpts[k] = v + } + + // Create the network + opts := networkapi.CreateOptions{ + Driver: "macvlan", + EnableIPv6: utils.Pointer(enableIPv6), + IPAM: &networkapi.IPAM{ + Driver: "default", + Config: ipamConfig, + }, + Internal: false, + Attachable: false, + Labels: map[string]string{ + "containerlab": "", + }, + Options: netwOpts, + } + + _, err := d.Client.NetworkCreate(nctx, d.mgmt.Network, opts) + if err != nil { + return fmt.Errorf("failed to create macvlan network: %w", err) + } + + log.Info("Macvlan network created successfully", "name", d.mgmt.Network) + + // If aux address was specified, provide instructions for creating host macvlan interface + if d.mgmt.MacvlanAux != "" { + log.Info("To enable host communication with containers, create a macvlan interface on the host:") + log.Infof(" sudo ip link add %s-host link %s type macvlan mode bridge", d.mgmt.Network, d.mgmt.MacvlanParent) + log.Infof(" sudo ip addr add %s/32 dev %s-host", d.mgmt.MacvlanAux, d.mgmt.Network) + log.Infof(" sudo ip link set %s-host up", d.mgmt.Network) + log.Infof(" sudo ip route add %s dev %s-host", d.mgmt.IPv4Subnet, d.mgmt.Network) + } + + return nil } // skipcq: GO-R1005 @@ -372,6 +561,13 @@ func getMgmtBridgeIPs(bridgeName string, netResource networkapi.Inspect) (string // postCreateNetActions performs additional actions after the network has been created. func (d *DockerRuntime) postCreateNetActions() (err error) { + // Skip all post-creation actions for macvlan networks + if d.mgmt.MacvlanDriver == "macvlan" { + log.Debug("Skipping post-creation actions for macvlan network") + return nil + } + + // Original bridge-specific actions below... log.Debug("Disable RPF check on the docker host") err = setSysctl("net/ipv4/conf/all/rp_filter", 0) if err != nil { @@ -403,7 +599,283 @@ func (d *DockerRuntime) postCreateNetActions() (err error) { return nil } -// DeleteNet deletes a docker bridge. +// postCreateMacvlanActions performs macvlan-specific post-creation actions +func (d *DockerRuntime) postCreateMacvlanActions() error { + // 1. Verify parent interface exists and is UP + parentLink, err := netlink.LinkByName(d.mgmt.MacvlanParent) + if err != nil { + return fmt.Errorf("failed to get parent interface %s: %w", d.mgmt.MacvlanParent, err) + } + + // Check if interface is UP + if parentLink.Attrs().OperState != netlink.OperUp { + log.Warnf("Parent interface %s is not UP (state: %s), containers may not have connectivity", + d.mgmt.MacvlanParent, parentLink.Attrs().OperState) + } + + // 2. Check promiscuous mode + if parentLink.Attrs().Promisc == 0 { + log.Debugf("Parent interface %s is not in promiscuous mode, enabling it for better macvlan compatibility", + d.mgmt.MacvlanParent) + // Enable promiscuous mode using command execution as a fallback + if err := enablePromiscuousMode(d.mgmt.MacvlanParent); err != nil { + log.Warnf("failed to enable promiscuous mode on %s: %v", d.mgmt.MacvlanParent, err) + } + } + + // 3. Log MTU information + parentMTU := parentLink.Attrs().MTU + log.Debugf("Parent interface %s has MTU %d, macvlan interfaces will inherit this", + d.mgmt.MacvlanParent, parentMTU) + + // 4. Create host macvlan interface if aux address is specified + if d.mgmt.MacvlanAux != "" { + if err := d.createHostMacvlanInterface(); err != nil { + // Don't fail the entire operation, just warn + log.Warnf("Failed to create host macvlan interface: %v", err) + log.Info("You can manually create it with:") + log.Infof(" sudo ip link add %s-host link %s type macvlan mode bridge", + d.mgmt.Network, d.mgmt.MacvlanParent) + log.Infof(" sudo ip addr add %s/%s dev %s-host", + d.mgmt.MacvlanAux, getSubnetPrefix(d.mgmt.IPv4Subnet), d.mgmt.Network) + log.Infof(" sudo ip link set %s-host up", d.mgmt.Network) + } else { + log.Infof("Created host macvlan interface %s-host with IP %s", + d.mgmt.Network, d.mgmt.MacvlanAux) + } + } else { + // Still warn about the limitation + log.Info("Note: Host cannot directly communicate with macvlan containers due to kernel limitations. " + + "Consider setting 'macvlan-aux' to create a host interface.") + } + + return nil +} + +// createHostMacvlanInterface creates a macvlan interface on the host for container communication +func (d *DockerRuntime) createHostMacvlanInterface() error { + hostIfName := d.mgmt.Network + "-host" + + // Check if interface already exists + if _, err := netlink.LinkByName(hostIfName); err == nil { + log.Debugf("Host macvlan interface %s already exists, skipping creation", hostIfName) + return nil + } + + // Get parent link + parentLink, err := netlink.LinkByName(d.mgmt.MacvlanParent) + if err != nil { + return fmt.Errorf("parent interface %s not found: %w", d.mgmt.MacvlanParent, err) + } + + // Create macvlan link + macvlan := &netlink.Macvlan{ + LinkAttrs: netlink.LinkAttrs{ + Name: hostIfName, + ParentIndex: parentLink.Attrs().Index, + }, + Mode: netlink.MACVLAN_MODE_BRIDGE, + } + + // Parse mode if specified differently + if d.mgmt.MacvlanMode != "" && d.mgmt.MacvlanMode != "bridge" { + switch d.mgmt.MacvlanMode { + case "vepa": + macvlan.Mode = netlink.MACVLAN_MODE_VEPA + case "private": + macvlan.Mode = netlink.MACVLAN_MODE_PRIVATE + case "passthru": + macvlan.Mode = netlink.MACVLAN_MODE_PASSTHRU + } + } + + // Create the interface + if err := netlink.LinkAdd(macvlan); err != nil { + return fmt.Errorf("failed to create macvlan interface: %w", err) + } + + // Get the created interface + link, err := netlink.LinkByName(hostIfName) + if err != nil { + return fmt.Errorf("failed to get created interface: %w", err) + } + + // Parse and add IP address + addr, err := netlink.ParseAddr(d.mgmt.MacvlanAux + "/" + getSubnetPrefix(d.mgmt.IPv4Subnet)) + if err != nil { + // Cleanup on failure + netlink.LinkDel(link) + return fmt.Errorf("failed to parse IP address %s: %w", d.mgmt.MacvlanAux, err) + } + + if err := netlink.AddrAdd(link, addr); err != nil { + // Cleanup on failure + netlink.LinkDel(link) + return fmt.Errorf("failed to add IP address: %w", err) + } + + // Bring the interface up + if err := netlink.LinkSetUp(link); err != nil { + // Cleanup on failure + netlink.LinkDel(link) + return fmt.Errorf("failed to bring interface up: %w", err) + } + + // Add route to the subnet via the aux address + _, ipnet, err := net.ParseCIDR(d.mgmt.IPv4Subnet) + if err != nil { + log.Warnf("Failed to parse subnet for route: %v", err) + // Don't fail entirely, the interface is still useful + return nil + } + + // Parse the aux address to use as gateway + auxIP := net.ParseIP(d.mgmt.MacvlanAux) + if auxIP == nil { + log.Warnf("Failed to parse aux IP for route: %s", d.mgmt.MacvlanAux) + return nil + } + + route := &netlink.Route{ + LinkIndex: link.Attrs().Index, + Dst: ipnet, + Gw: auxIP, + Scope: netlink.SCOPE_UNIVERSE, + } + + if err := netlink.RouteAdd(route); err != nil { + // This might fail if route already exists, which is ok + log.Debugf("Failed to add route (might already exist): %v", err) + } else { + log.Infof("Added route %s via %s dev %s", d.mgmt.IPv4Subnet, d.mgmt.MacvlanAux, hostIfName) + } + + return nil +} + +// getSubnetPrefix extracts the prefix length from a CIDR notation +func getSubnetPrefix(subnet string) string { + parts := strings.Split(subnet, "/") + if len(parts) == 2 { + return parts[1] + } + return "24" // default +} + +// cleanupMacvlanPostActions reverses the changes made in postCreateMacvlanActions +func (d *DockerRuntime) cleanupMacvlanPostActions() error { + // First, remove the static route if it exists + if d.mgmt.MacvlanAux != "" && d.mgmt.IPv4Subnet != "" { + _, ipnet, err := net.ParseCIDR(d.mgmt.IPv4Subnet) + if err == nil { + auxIP := net.ParseIP(d.mgmt.MacvlanAux) + if auxIP != nil { + // Find and delete the route + routes, err := netlink.RouteList(nil, netlink.FAMILY_V4) + if err == nil { + for _, route := range routes { + if route.Dst != nil && route.Dst.String() == ipnet.String() && + route.Gw != nil && route.Gw.Equal(auxIP) { + if err := netlink.RouteDel(&route); err != nil { + log.Debugf("Failed to delete route %s via %s: %v", + d.mgmt.IPv4Subnet, d.mgmt.MacvlanAux, err) + } else { + log.Infof("Removed route %s via %s", + d.mgmt.IPv4Subnet, d.mgmt.MacvlanAux) + } + break + } + } + } + } + } + } + + // Then cleanup the host interface (this might also remove associated routes) + if err := d.cleanupHostMacvlanInterface(); err != nil { + log.Warnf("Failed to cleanup host macvlan interface: %v", err) + } + + // Disable promiscuous mode on parent interface + parentLink, err := netlink.LinkByName(d.mgmt.MacvlanParent) + if err != nil { + // Parent interface might not exist anymore + log.Debugf("Parent interface %s not found during cleanup: %v", d.mgmt.MacvlanParent, err) + return nil + } + + // Check if there are other macvlan interfaces using this parent + links, err := netlink.LinkList() + if err == nil { + otherMacvlans := false + for _, link := range links { + if macvlan, ok := link.(*netlink.Macvlan); ok { + if macvlan.ParentIndex == parentLink.Attrs().Index && + macvlan.Name != d.mgmt.Network+"-host" { + otherMacvlans = true + break + } + } + } + + // Only disable promiscuous mode if no other macvlans are using this parent + if !otherMacvlans { + if err := disablePromiscuousMode(d.mgmt.MacvlanParent); err != nil { + log.Warnf("Failed to disable promiscuous mode on %s: %v", d.mgmt.MacvlanParent, err) + } else { + log.Debugf("Disabled promiscuous mode on %s", d.mgmt.MacvlanParent) + } + } else { + log.Debugf("Other macvlan interfaces exist on %s, keeping promiscuous mode enabled", d.mgmt.MacvlanParent) + } + } + + return nil +} + +// enablePromiscuousMode enables promiscuous mode on an interface +func enablePromiscuousMode(ifName string) error { + // Try using exec to run ip command as a fallback + cmd := exec.Command("ip", "link", "set", ifName, "promisc", "on") + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to enable promiscuous mode: %w", err) + } + return nil +} + +// disablePromiscuousMode disables promiscuous mode on an interface +func disablePromiscuousMode(ifName string) error { + // Try using exec to run ip command as a fallback + cmd := exec.Command("ip", "link", "set", ifName, "promisc", "off") + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to disable promiscuous mode: %w", err) + } + return nil +} + +// cleanupHostMacvlanInterface removes the host macvlan interface if it exists +func (d *DockerRuntime) cleanupHostMacvlanInterface() error { + if d.mgmt.MacvlanAux == "" { + return nil + } + + hostIfName := d.mgmt.Network + "-host" + + link, err := netlink.LinkByName(hostIfName) + if err != nil { + // Interface doesn't exist, nothing to clean up + return nil + } + + if err := netlink.LinkDel(link); err != nil { + return fmt.Errorf("failed to delete host macvlan interface: %w", err) + } + + log.Infof("Removed host macvlan interface %s", hostIfName) + return nil +} + +// DeleteNet deletes a docker bridge or macvlan network. func (d *DockerRuntime) DeleteNet(ctx context.Context) (err error) { network := d.mgmt.Network if network == "bridge" || d.config.KeepMgmtNet { @@ -427,14 +899,25 @@ func (d *DockerRuntime) DeleteNet(ctx context.Context) (err error) { } return nil } + + // For macvlan networks, cleanup host interface first + if d.mgmt.MacvlanDriver == "macvlan" { + if err := d.cleanupMacvlanPostActions(); err != nil { + log.Warnf("Failed to cleanup macvlan post-actions: %v", err) + } + } + err = d.Client.NetworkRemove(nctx, network) if err != nil { return err } - err = d.deleteMgmtNetworkFwdRule() - if err != nil { - log.Warnf("errors during iptables rules removal: %v", err) + // Only run bridge-specific cleanup for bridge networks + if d.mgmt.MacvlanDriver != "macvlan" { + err = d.deleteMgmtNetworkFwdRule() + if err != nil { + log.Warnf("errors during iptables rules removal: %v", err) + } } return nil diff --git a/types/types.go b/types/types.go index 81f0f9a0b5..a39fe06403 100644 --- a/types/types.go +++ b/types/types.go @@ -57,6 +57,11 @@ type MgmtNet struct { MTU int `yaml:"mtu,omitempty" json:"mtu,omitempty"` ExternalAccess *bool `yaml:"external-access,omitempty" json:"external-access,omitempty"` DriverOpts map[string]string `yaml:"driver-opts,omitempty" json:"driver-opts,omitempty"` + // Macvlan specific options + MacvlanParent string `yaml:"macvlan-parent,omitempty" json:"macvlan-parent,omitempty"` // Parent interface for macvlan + MacvlanMode string `yaml:"macvlan-mode,omitempty" json:"macvlan-mode,omitempty"` // bridge, vepa, passthru, private + MacvlanDriver string `yaml:"macvlan-driver,omitempty" json:"macvlan-driver,omitempty"` // "bridge" or "macvlan" + MacvlanAux string `yaml:"macvlan-aux,omitempty" json:"macvlan-aux,omitempty"` // Reserved IP Address for containerlab host connectivity } // Interface compliance. From 934873aca57b3325fd351ca9cc247ea1d0b50e54 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Sat, 2 Aug 2025 23:02:43 -0600 Subject: [PATCH 02/28] schema and compile changes --- runtime/docker/docker.go | 16 ++++++++-------- runtime/docker/nl_linux.go | 10 ++++++++++ runtime/docker/nl_stub.go | 8 ++++++++ schemas/clab.schema.json | 20 ++++++++++++++++++++ 4 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 runtime/docker/nl_linux.go create mode 100644 runtime/docker/nl_stub.go diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index d5ade35fc1..5c89b3cf85 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -10,11 +10,11 @@ import ( "errors" "fmt" "os" + osexec "os/exec" "path" "strconv" "strings" "time" - "os/exec" "net" "github.com/docker/docker/api/types/image" @@ -151,7 +151,7 @@ func (d *DockerRuntime) WithMgmtNet(n *types.MgmtNet) { if d.mgmt.Bridge == "" && d.mgmt.Network != "" { // fetch the network by the name set in the topo and populate the bridge name used by this network netRes, err := d.Client.NetworkInspect(context.TODO(), d.mgmt.Network, networkapi.InspectOptions{}) - // if the network is succesfully found, set the bridge used by it + // if the network is successfully found, set the bridge used by it if err == nil { if name, exists := netRes.Options["com.docker.network.bridge.name"]; exists { d.mgmt.Bridge = name @@ -240,7 +240,7 @@ func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) { // get management bridge v4/6 addresses and save it under mgmt struct // so that nodes can use this information prior to being deployed - // this was added to allow mgmt network gw ip to be available in a startup config templation step (ceos) + // this was added to allow mgmt network gw ip to be available in a startup config template step (ceos) d.mgmt.IPv4Gw, d.mgmt.IPv6Gw, err = getMgmtBridgeIPs(bridgeName, netResource) if err != nil { return err @@ -551,7 +551,7 @@ func getMgmtBridgeIPs(bridgeName string, netResource networkapi.Inspect) (string } } - // didnt find any gateways, fallthrough to returning the error + // didn't find any gateways, fallthrough to returning the error if v4 == "" && v6 == "" { return "", "", err } @@ -740,7 +740,7 @@ func (d *DockerRuntime) createHostMacvlanInterface() error { LinkIndex: link.Attrs().Index, Dst: ipnet, Gw: auxIP, - Scope: netlink.SCOPE_UNIVERSE, + Scope: netlink.Scope(0), } if err := netlink.RouteAdd(route); err != nil { @@ -771,7 +771,7 @@ func (d *DockerRuntime) cleanupMacvlanPostActions() error { auxIP := net.ParseIP(d.mgmt.MacvlanAux) if auxIP != nil { // Find and delete the route - routes, err := netlink.RouteList(nil, netlink.FAMILY_V4) + routes, err := netlink.RouteList(nil, getIPv4Family()) if err == nil { for _, route := range routes { if route.Dst != nil && route.Dst.String() == ipnet.String() && @@ -836,7 +836,7 @@ func (d *DockerRuntime) cleanupMacvlanPostActions() error { // enablePromiscuousMode enables promiscuous mode on an interface func enablePromiscuousMode(ifName string) error { // Try using exec to run ip command as a fallback - cmd := exec.Command("ip", "link", "set", ifName, "promisc", "on") + cmd := osexec.Command("ip", "link", "set", ifName, "promisc", "on") if err := cmd.Run(); err != nil { return fmt.Errorf("failed to enable promiscuous mode: %w", err) } @@ -846,7 +846,7 @@ func enablePromiscuousMode(ifName string) error { // disablePromiscuousMode disables promiscuous mode on an interface func disablePromiscuousMode(ifName string) error { // Try using exec to run ip command as a fallback - cmd := exec.Command("ip", "link", "set", ifName, "promisc", "off") + cmd := osexec.Command("ip", "link", "set", ifName, "promisc", "off") if err := cmd.Run(); err != nil { return fmt.Errorf("failed to disable promiscuous mode: %w", err) } diff --git a/runtime/docker/nl_linux.go b/runtime/docker/nl_linux.go new file mode 100644 index 0000000000..c354627ffd --- /dev/null +++ b/runtime/docker/nl_linux.go @@ -0,0 +1,10 @@ +//go:build linux +// +build linux + +package docker + +import "github.com/vishvananda/netlink" + +func getIPv4Family() int { + return netlink.FAMILY_V4 +} diff --git a/runtime/docker/nl_stub.go b/runtime/docker/nl_stub.go new file mode 100644 index 0000000000..20bb59ba0b --- /dev/null +++ b/runtime/docker/nl_stub.go @@ -0,0 +1,8 @@ +//go:build !linux +// +build !linux + +package docker + +func getIPv4Family() int { + return 2 // syscall.AF_INET; safe fallback if you just want to compile +} diff --git a/schemas/clab.schema.json b/schemas/clab.schema.json index 69e1aa1983..7606659915 100644 --- a/schemas/clab.schema.json +++ b/schemas/clab.schema.json @@ -1291,6 +1291,26 @@ "description": "MTU for the custom network", "markdownDescription": "[MTU](https://containerlab.dev/manual/network/#mtu) in Bytes for the custom management network", "$ref": "#/definitions/mtu" + }, + "macvlan-parent": { + "description": "MTU for the custom network", + "markdownDescription": "[MTU](https://containerlab.dev/manual/network/#mtu) in Bytes for the custom management network", + "$ref": "#/definitions/mtu" + }, + "macvlan-mode": { + "description": "MTU for the custom network", + "markdownDescription": "[MTU](https://containerlab.dev/manual/network/#mtu) in Bytes for the custom management network", + "$ref": "#/definitions/mtu" + }, + "macvlan-driver": { + "description": "MTU for the custom network", + "markdownDescription": "[MTU](https://containerlab.dev/manual/network/#mtu) in Bytes for the custom management network", + "$ref": "#/definitions/mtu" + }, + "macvlan-aux": { + "description": "MTU for the custom network", + "markdownDescription": "[MTU](https://containerlab.dev/manual/network/#mtu) in Bytes for the custom management network", + "$ref": "#/definitions/mtu" } }, "minProperties": 1, From c859bd78c36f1923f29b139ef85d076b0b96b63f Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Sat, 2 Aug 2025 23:39:11 -0600 Subject: [PATCH 03/28] type and schema adjustments --- schemas/clab.schema.json | 26 ++++++++++++-------------- types/types.go | 2 +- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/schemas/clab.schema.json b/schemas/clab.schema.json index 7606659915..c7186d478d 100644 --- a/schemas/clab.schema.json +++ b/schemas/clab.schema.json @@ -1292,25 +1292,23 @@ "markdownDescription": "[MTU](https://containerlab.dev/manual/network/#mtu) in Bytes for the custom management network", "$ref": "#/definitions/mtu" }, + "driver": { + "type": "string", + "enum": ["bridge", "macvlan"], + "description": "Network driver type (bridge or macvlan)" + }, "macvlan-parent": { - "description": "MTU for the custom network", - "markdownDescription": "[MTU](https://containerlab.dev/manual/network/#mtu) in Bytes for the custom management network", - "$ref": "#/definitions/mtu" + "type": "string", + "description": "Parent interface for macvlan network" }, "macvlan-mode": { - "description": "MTU for the custom network", - "markdownDescription": "[MTU](https://containerlab.dev/manual/network/#mtu) in Bytes for the custom management network", - "$ref": "#/definitions/mtu" - }, - "macvlan-driver": { - "description": "MTU for the custom network", - "markdownDescription": "[MTU](https://containerlab.dev/manual/network/#mtu) in Bytes for the custom management network", - "$ref": "#/definitions/mtu" + "type": "string", + "enum": ["bridge", "vepa", "private", "passthru"], + "description": "Macvlan mode (bridge, vepa, private, or passthru)" }, "macvlan-aux": { - "description": "MTU for the custom network", - "markdownDescription": "[MTU](https://containerlab.dev/manual/network/#mtu) in Bytes for the custom management network", - "$ref": "#/definitions/mtu" + "type": "string", + "description": "Auxiliary IP address for host macvlan interface" } }, "minProperties": 1, diff --git a/types/types.go b/types/types.go index a39fe06403..e21ed91a6e 100644 --- a/types/types.go +++ b/types/types.go @@ -60,7 +60,7 @@ type MgmtNet struct { // Macvlan specific options MacvlanParent string `yaml:"macvlan-parent,omitempty" json:"macvlan-parent,omitempty"` // Parent interface for macvlan MacvlanMode string `yaml:"macvlan-mode,omitempty" json:"macvlan-mode,omitempty"` // bridge, vepa, passthru, private - MacvlanDriver string `yaml:"macvlan-driver,omitempty" json:"macvlan-driver,omitempty"` // "bridge" or "macvlan" + Driver string `yaml:"macvlan-driver,omitempty" json:"macvlan-driver,omitempty"` // "bridge" or "macvlan" MacvlanAux string `yaml:"macvlan-aux,omitempty" json:"macvlan-aux,omitempty"` // Reserved IP Address for containerlab host connectivity } From 77ac8ca245171db0bb37f492c495c0e88ea3c196 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Sat, 2 Aug 2025 23:56:50 -0600 Subject: [PATCH 04/28] types and host adapter logging --- runtime/docker/docker.go | 11 +++++++---- types/types.go | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 5c89b3cf85..42fc74dc3d 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -169,7 +169,7 @@ func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) { defer cancel() // Determine the driver to use (default to bridge if not specified) - driver := d.mgmt.MacvlanDriver + driver := d.mgmt.Driver if driver == "" { driver = "bridge" } @@ -562,7 +562,7 @@ func getMgmtBridgeIPs(bridgeName string, netResource networkapi.Inspect) (string // postCreateNetActions performs additional actions after the network has been created. func (d *DockerRuntime) postCreateNetActions() (err error) { // Skip all post-creation actions for macvlan networks - if d.mgmt.MacvlanDriver == "macvlan" { + if d.mgmt.Driver == "macvlan" { log.Debug("Skipping post-creation actions for macvlan network") return nil } @@ -601,6 +601,9 @@ func (d *DockerRuntime) postCreateNetActions() (err error) { // postCreateMacvlanActions performs macvlan-specific post-creation actions func (d *DockerRuntime) postCreateMacvlanActions() error { + //debugging + log.Info("Starting macvlan post-creation actions") + log.Debugf("MacvlanAux: %s, IPv4Subnet: %s", d.mgmt.MacvlanAux, d.mgmt.IPv4Subnet) // 1. Verify parent interface exists and is UP parentLink, err := netlink.LinkByName(d.mgmt.MacvlanParent) if err != nil { @@ -901,7 +904,7 @@ func (d *DockerRuntime) DeleteNet(ctx context.Context) (err error) { } // For macvlan networks, cleanup host interface first - if d.mgmt.MacvlanDriver == "macvlan" { + if d.mgmt.Driver == "macvlan" { if err := d.cleanupMacvlanPostActions(); err != nil { log.Warnf("Failed to cleanup macvlan post-actions: %v", err) } @@ -913,7 +916,7 @@ func (d *DockerRuntime) DeleteNet(ctx context.Context) (err error) { } // Only run bridge-specific cleanup for bridge networks - if d.mgmt.MacvlanDriver != "macvlan" { + if d.mgmt.Driver != "macvlan" { err = d.deleteMgmtNetworkFwdRule() if err != nil { log.Warnf("errors during iptables rules removal: %v", err) diff --git a/types/types.go b/types/types.go index e21ed91a6e..81623917f6 100644 --- a/types/types.go +++ b/types/types.go @@ -60,7 +60,7 @@ type MgmtNet struct { // Macvlan specific options MacvlanParent string `yaml:"macvlan-parent,omitempty" json:"macvlan-parent,omitempty"` // Parent interface for macvlan MacvlanMode string `yaml:"macvlan-mode,omitempty" json:"macvlan-mode,omitempty"` // bridge, vepa, passthru, private - Driver string `yaml:"macvlan-driver,omitempty" json:"macvlan-driver,omitempty"` // "bridge" or "macvlan" + Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` // "bridge" or "macvlan" MacvlanAux string `yaml:"macvlan-aux,omitempty" json:"macvlan-aux,omitempty"` // Reserved IP Address for containerlab host connectivity } From ec221ada245fa1439cc283f217f00c8d48617e55 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Sun, 3 Aug 2025 00:57:42 -0600 Subject: [PATCH 05/28] createHostMacvlanInterface refinements --- runtime/docker/docker.go | 57 +++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 42fc74dc3d..7da998a39c 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -660,9 +660,23 @@ func (d *DockerRuntime) createHostMacvlanInterface() error { hostIfName := d.mgmt.Network + "-host" // Check if interface already exists - if _, err := netlink.LinkByName(hostIfName); err == nil { - log.Debugf("Host macvlan interface %s already exists, skipping creation", hostIfName) - return nil + if existingLink, err := netlink.LinkByName(hostIfName); err == nil { + log.Debugf("Host macvlan interface %s already exists", hostIfName) + // Check if it has the correct IP + addrs, err := netlink.AddrList(existingLink, netlink.FAMILY_V4) + if err == nil { + for _, addr := range addrs { + if addr.IP.String() == d.mgmt.MacvlanAux { + log.Debugf("Interface %s already has IP %s", hostIfName, d.mgmt.MacvlanAux) + return nil + } + } + } + // Interface exists but might not have the right IP, delete and recreate + log.Debugf("Removing existing interface %s to recreate with correct settings", hostIfName) + if err := netlink.LinkDel(existingLink); err != nil { + log.Warnf("Failed to delete existing interface: %v", err) + } } // Get parent link @@ -671,11 +685,15 @@ func (d *DockerRuntime) createHostMacvlanInterface() error { return fmt.Errorf("parent interface %s not found: %w", d.mgmt.MacvlanParent, err) } + log.Debugf("Creating macvlan interface %s on parent %s (index %d)", + hostIfName, d.mgmt.MacvlanParent, parentLink.Attrs().Index) + // Create macvlan link macvlan := &netlink.Macvlan{ LinkAttrs: netlink.LinkAttrs{ Name: hostIfName, ParentIndex: parentLink.Attrs().Index, + MTU: parentLink.Attrs().MTU, // Inherit MTU from parent }, Mode: netlink.MACVLAN_MODE_BRIDGE, } @@ -700,15 +718,21 @@ func (d *DockerRuntime) createHostMacvlanInterface() error { // Get the created interface link, err := netlink.LinkByName(hostIfName) if err != nil { + // Cleanup on failure + netlink.LinkDel(macvlan) return fmt.Errorf("failed to get created interface: %w", err) } // Parse and add IP address - addr, err := netlink.ParseAddr(d.mgmt.MacvlanAux + "/" + getSubnetPrefix(d.mgmt.IPv4Subnet)) + // Use /32 for the host interface to avoid subnet conflicts + addrStr := d.mgmt.MacvlanAux + "/32" + log.Debugf("Adding IP address %s to interface %s", addrStr, hostIfName) + + addr, err := netlink.ParseAddr(addrStr) if err != nil { // Cleanup on failure netlink.LinkDel(link) - return fmt.Errorf("failed to parse IP address %s: %w", d.mgmt.MacvlanAux, err) + return fmt.Errorf("failed to parse IP address %s: %w", addrStr, err) } if err := netlink.AddrAdd(link, addr); err != nil { @@ -724,33 +748,30 @@ func (d *DockerRuntime) createHostMacvlanInterface() error { return fmt.Errorf("failed to bring interface up: %w", err) } + log.Infof("Created host macvlan interface %s with IP %s", hostIfName, d.mgmt.MacvlanAux) + // Add route to the subnet via the aux address _, ipnet, err := net.ParseCIDR(d.mgmt.IPv4Subnet) if err != nil { log.Warnf("Failed to parse subnet for route: %v", err) - // Don't fail entirely, the interface is still useful - return nil - } - - // Parse the aux address to use as gateway - auxIP := net.ParseIP(d.mgmt.MacvlanAux) - if auxIP == nil { - log.Warnf("Failed to parse aux IP for route: %s", d.mgmt.MacvlanAux) return nil } + // For the route, we need to use the macvlan interface as the output device + // The route should be: dev scope link route := &netlink.Route{ LinkIndex: link.Attrs().Index, Dst: ipnet, - Gw: auxIP, - Scope: netlink.Scope(0), + Scope: netlink.SCOPE_LINK, // Use SCOPE_LINK for local subnet } if err := netlink.RouteAdd(route); err != nil { - // This might fail if route already exists, which is ok - log.Debugf("Failed to add route (might already exist): %v", err) + // Check if route already exists + if !strings.Contains(err.Error(), "file exists") { + log.Warnf("Failed to add route %s dev %s: %v", d.mgmt.IPv4Subnet, hostIfName, err) + } } else { - log.Infof("Added route %s via %s dev %s", d.mgmt.IPv4Subnet, d.mgmt.MacvlanAux, hostIfName) + log.Infof("Added route %s dev %s", d.mgmt.IPv4Subnet, hostIfName) } return nil From 5bbe2cf84a7b82cd136dbde86095dab5152cc000 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Sun, 3 Aug 2025 01:30:01 -0600 Subject: [PATCH 06/28] createHostMacvlanInterface debug logging --- runtime/docker/docker.go | 73 +++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 7da998a39c..3ff9e6fb80 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -659,6 +659,9 @@ func (d *DockerRuntime) postCreateMacvlanActions() error { func (d *DockerRuntime) createHostMacvlanInterface() error { hostIfName := d.mgmt.Network + "-host" + log.Debugf("Creating host macvlan interface: name=%s, parent=%s, mode=%s", + hostIfName, d.mgmt.MacvlanParent, d.mgmt.MacvlanMode) + // Check if interface already exists if existingLink, err := netlink.LinkByName(hostIfName); err == nil { log.Debugf("Host macvlan interface %s already exists", hostIfName) @@ -685,36 +688,69 @@ func (d *DockerRuntime) createHostMacvlanInterface() error { return fmt.Errorf("parent interface %s not found: %w", d.mgmt.MacvlanParent, err) } - log.Debugf("Creating macvlan interface %s on parent %s (index %d)", - hostIfName, d.mgmt.MacvlanParent, parentLink.Attrs().Index) + log.Debugf("Parent interface details: name=%s, index=%d, mtu=%d, state=%s", + parentLink.Attrs().Name, + parentLink.Attrs().Index, + parentLink.Attrs().MTU, + parentLink.Attrs().OperState) + + // Determine macvlan mode + mode := netlink.MACVLAN_MODE_BRIDGE + if d.mgmt.MacvlanMode != "" && d.mgmt.MacvlanMode != "bridge" { + switch d.mgmt.MacvlanMode { + case "vepa": + mode = netlink.MACVLAN_MODE_VEPA + case "private": + mode = netlink.MACVLAN_MODE_PRIVATE + case "passthru": + mode = netlink.MACVLAN_MODE_PASSTHRU + default: + log.Warnf("Unknown macvlan mode %s, using bridge", d.mgmt.MacvlanMode) + } + } + + log.Debugf("Creating macvlan with mode=%d (0=private, 1=vepa, 2=bridge, 3=passthru)", mode) - // Create macvlan link + // Create macvlan link with minimal attributes first macvlan := &netlink.Macvlan{ LinkAttrs: netlink.LinkAttrs{ Name: hostIfName, ParentIndex: parentLink.Attrs().Index, - MTU: parentLink.Attrs().MTU, // Inherit MTU from parent }, - Mode: netlink.MACVLAN_MODE_BRIDGE, + Mode: mode, } - // Parse mode if specified differently - if d.mgmt.MacvlanMode != "" && d.mgmt.MacvlanMode != "bridge" { - switch d.mgmt.MacvlanMode { - case "vepa": - macvlan.Mode = netlink.MACVLAN_MODE_VEPA - case "private": - macvlan.Mode = netlink.MACVLAN_MODE_PRIVATE - case "passthru": - macvlan.Mode = netlink.MACVLAN_MODE_PASSTHRU - } + // Log the structure before creation + log.Debugf("Macvlan struct: Name=%s, ParentIndex=%d, Mode=%d", + macvlan.LinkAttrs.Name, + macvlan.LinkAttrs.ParentIndex, + macvlan.Mode) + + // Try to create using command line as a test + cmd := exec.Command("ip", "link", "add", hostIfName, "link", d.mgmt.MacvlanParent, "type", "macvlan", "mode", "bridge") + if output, err := cmd.CombinedOutput(); err != nil { + log.Debugf("Command line test failed: %v, output: %s", err, string(output)) + // Don't return here, continue with netlink + } else { + log.Debug("Command line creation succeeded, removing to use netlink") + // Remove it so we can create via netlink + delCmd := exec.Command("ip", "link", "del", hostIfName) + delCmd.Run() } - // Create the interface + // Create the interface via netlink if err := netlink.LinkAdd(macvlan); err != nil { + // Try to get more error details + if strings.Contains(err.Error(), "numerical result") { + log.Errorf("Netlink error details - this often indicates an issue with the parent interface index or mode value") + log.Errorf("Parent index: %d, Mode: %d", parentLink.Attrs().Index, mode) + } return fmt.Errorf("failed to create macvlan interface: %w", err) } + // Rest of the function remains the same... + log.Debug("Macvlan interface created successfully via netlink") + // Get the created interface link, err := netlink.LinkByName(hostIfName) if err != nil { @@ -750,7 +786,7 @@ func (d *DockerRuntime) createHostMacvlanInterface() error { log.Infof("Created host macvlan interface %s with IP %s", hostIfName, d.mgmt.MacvlanAux) - // Add route to the subnet via the aux address + // Add route to the subnet _, ipnet, err := net.ParseCIDR(d.mgmt.IPv4Subnet) if err != nil { log.Warnf("Failed to parse subnet for route: %v", err) @@ -758,11 +794,10 @@ func (d *DockerRuntime) createHostMacvlanInterface() error { } // For the route, we need to use the macvlan interface as the output device - // The route should be: dev scope link route := &netlink.Route{ LinkIndex: link.Attrs().Index, Dst: ipnet, - Scope: netlink.SCOPE_LINK, // Use SCOPE_LINK for local subnet + Scope: netlink.SCOPE_LINK, } if err := netlink.RouteAdd(route); err != nil { From 2de58a362bdcdec94ad0405662fa6755bc4e5756 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Sun, 3 Aug 2025 01:39:20 -0600 Subject: [PATCH 07/28] exec to osexec correction --- runtime/docker/docker.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 3ff9e6fb80..b8b8eef2de 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -727,14 +727,14 @@ func (d *DockerRuntime) createHostMacvlanInterface() error { macvlan.Mode) // Try to create using command line as a test - cmd := exec.Command("ip", "link", "add", hostIfName, "link", d.mgmt.MacvlanParent, "type", "macvlan", "mode", "bridge") + cmd := osexec.Command("ip", "link", "add", hostIfName, "link", d.mgmt.MacvlanParent, "type", "macvlan", "mode", "bridge") if output, err := cmd.CombinedOutput(); err != nil { log.Debugf("Command line test failed: %v, output: %s", err, string(output)) // Don't return here, continue with netlink } else { log.Debug("Command line creation succeeded, removing to use netlink") // Remove it so we can create via netlink - delCmd := exec.Command("ip", "link", "del", hostIfName) + delCmd := osexec.Command("ip", "link", "del", hostIfName) delCmd.Run() } From 7b25a13a149326b95ebfba802d45d84d7d1d5a4a Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Sun, 3 Aug 2025 10:33:41 -0600 Subject: [PATCH 08/28] macvlan mode adjustment --- runtime/docker/docker.go | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index b8b8eef2de..1de57236af 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -694,21 +694,25 @@ func (d *DockerRuntime) createHostMacvlanInterface() error { parentLink.Attrs().MTU, parentLink.Attrs().OperState) - // Determine macvlan mode - mode := netlink.MACVLAN_MODE_BRIDGE - if d.mgmt.MacvlanMode != "" && d.mgmt.MacvlanMode != "bridge" { - switch d.mgmt.MacvlanMode { - case "vepa": - mode = netlink.MACVLAN_MODE_VEPA - case "private": - mode = netlink.MACVLAN_MODE_PRIVATE - case "passthru": - mode = netlink.MACVLAN_MODE_PASSTHRU - default: - log.Warnf("Unknown macvlan mode %s, using bridge", d.mgmt.MacvlanMode) - } + + // Determine macvlan mode - explicitly handle all cases + var mode netlink.MacvlanMode + switch d.mgmt.MacvlanMode { + case "", "bridge": // default to bridge if empty + mode = netlink.MACVLAN_MODE_BRIDGE + case "vepa": + mode = netlink.MACVLAN_MODE_VEPA + case "private": + mode = netlink.MACVLAN_MODE_PRIVATE + case "passthru": + mode = netlink.MACVLAN_MODE_PASSTHRU + default: + log.Warnf("Unknown macvlan mode %s, defaulting to bridge", d.mgmt.MacvlanMode) + mode = netlink.MACVLAN_MODE_BRIDGE } + log.Debugf("Creating macvlan with mode='%s' value=%d", d.mgmt.MacvlanMode, mode) + log.Debugf("Creating macvlan with mode=%d (0=private, 1=vepa, 2=bridge, 3=passthru)", mode) // Create macvlan link with minimal attributes first From e0c949b31f2ea008f6b81e7cd802f036c15f63ea Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Sun, 3 Aug 2025 12:00:42 -0600 Subject: [PATCH 09/28] host adapter --- runtime/docker/docker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 1de57236af..85cc532859 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -721,7 +721,7 @@ func (d *DockerRuntime) createHostMacvlanInterface() error { Name: hostIfName, ParentIndex: parentLink.Attrs().Index, }, - Mode: mode, + Mode: netlink.MACVLAN_MODE_BRIDGE, } // Log the structure before creation From 2d3616f6436d1f041f1f8a0c6fa9bb45bb97e870 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Sun, 3 Aug 2025 15:54:55 -0600 Subject: [PATCH 10/28] set hostIfName alphanumeric --- runtime/docker/docker.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 85cc532859..595fac98b6 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -16,6 +16,7 @@ import ( "strings" "time" "net" + "regexp" "github.com/docker/docker/api/types/image" "github.com/docker/go-units" @@ -655,9 +656,15 @@ func (d *DockerRuntime) postCreateMacvlanActions() error { return nil } +func sanitizeAlphanumeric(input string) string { + re := regexp.MustCompile(`[^a-zA-Z0-9]+`) + return re.ReplaceAllString(input, "") +} + // createHostMacvlanInterface creates a macvlan interface on the host for container communication func (d *DockerRuntime) createHostMacvlanInterface() error { - hostIfName := d.mgmt.Network + "-host" + hostIfNameNonAlpha := d.mgmt.Network + "-host" + hostIfName := sanitizeAlphanumeric(hostIfNameNonAlpha) log.Debugf("Creating host macvlan interface: name=%s, parent=%s, mode=%s", hostIfName, d.mgmt.MacvlanParent, d.mgmt.MacvlanMode) From 775f0268ce7ec7a77d034f28669c184ffd0e7738 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Sun, 3 Aug 2025 16:36:51 -0600 Subject: [PATCH 11/28] cleaning up --- runtime/docker/docker.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 595fac98b6..8052b735d1 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -728,7 +728,7 @@ func (d *DockerRuntime) createHostMacvlanInterface() error { Name: hostIfName, ParentIndex: parentLink.Attrs().Index, }, - Mode: netlink.MACVLAN_MODE_BRIDGE, + Mode: mode, } // Log the structure before creation @@ -841,7 +841,7 @@ func (d *DockerRuntime) cleanupMacvlanPostActions() error { auxIP := net.ParseIP(d.mgmt.MacvlanAux) if auxIP != nil { // Find and delete the route - routes, err := netlink.RouteList(nil, getIPv4Family()) + routes, err := netlink.RouteList(nil, netlink.FAMILY_V4) if err == nil { for _, route := range routes { if route.Dst != nil && route.Dst.String() == ipnet.String() && @@ -929,8 +929,9 @@ func (d *DockerRuntime) cleanupHostMacvlanInterface() error { return nil } - hostIfName := d.mgmt.Network + "-host" - + hostIfNameNonAlpha := d.mgmt.Network + "-host" + hostIfName := sanitizeAlphanumeric(hostIfNameNonAlpha) + link, err := netlink.LinkByName(hostIfName) if err != nil { // Interface doesn't exist, nothing to clean up From e7706f95dbae03f580492c106f86762b886f7077 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Sun, 3 Aug 2025 23:15:44 -0600 Subject: [PATCH 12/28] postActions cleanuup --- runtime/docker/docker.go | 46 +++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 8052b735d1..bae4a0df6a 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -180,7 +180,8 @@ func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) { log.Debugf("Checking if docker network %q exists", d.mgmt.Network) netResource, err := d.Client.NetworkInspect(nctx, d.mgmt.Network, networkapi.InspectOptions{}) - + + var networkCreated bool switch { case dockerC.IsErrNotFound(err): // Network doesn't exist, create it based on driver type @@ -192,6 +193,7 @@ func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) { } case "macvlan": + networkCreated = true err = d.createMgmtMacvlan(nctx) if err != nil { return err @@ -205,6 +207,7 @@ func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) { case err == nil: // Network exists, validate it matches expected driver + networkCreated = false log.Debugf("network %q was found. Reusing it...", d.mgmt.Network) if netResource.Driver != driver { return fmt.Errorf("existing network %q has driver %q but configuration specifies %q", @@ -274,7 +277,9 @@ func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) { } log.Debugf("Docker macvlan network %q created/reused with parent interface %q", d.mgmt.Network, d.mgmt.MacvlanParent) - return d.postCreateMacvlanActions() + if networkCreated { + return d.postCreateMacvlanActions() + } } return nil @@ -835,25 +840,32 @@ func getSubnetPrefix(subnet string) string { // cleanupMacvlanPostActions reverses the changes made in postCreateMacvlanActions func (d *DockerRuntime) cleanupMacvlanPostActions() error { // First, remove the static route if it exists + // In cleanupMacvlanPostActions(), fix the route cleanup: if d.mgmt.MacvlanAux != "" && d.mgmt.IPv4Subnet != "" { + // Get the sanitized host interface name + hostIfNameNonAlpha := d.mgmt.Network + "-host" + hostIfName := sanitizeAlphanumeric(hostIfNameNonAlpha) + _, ipnet, err := net.ParseCIDR(d.mgmt.IPv4Subnet) if err == nil { - auxIP := net.ParseIP(d.mgmt.MacvlanAux) - if auxIP != nil { - // Find and delete the route - routes, err := netlink.RouteList(nil, netlink.FAMILY_V4) - if err == nil { - for _, route := range routes { - if route.Dst != nil && route.Dst.String() == ipnet.String() && - route.Gw != nil && route.Gw.Equal(auxIP) { - if err := netlink.RouteDel(&route); err != nil { - log.Debugf("Failed to delete route %s via %s: %v", - d.mgmt.IPv4Subnet, d.mgmt.MacvlanAux, err) - } else { - log.Infof("Removed route %s via %s", - d.mgmt.IPv4Subnet, d.mgmt.MacvlanAux) + // Find and delete routes associated with this interface + routes, err := netlink.RouteList(nil, netlink.FAMILY_V4) + if err == nil { + for _, route := range routes { + // Check if this route is for our subnet and uses our interface + if route.Dst != nil && route.Dst.String() == ipnet.String() { + // Get the link to check if it's our interface + if route.LinkIndex > 0 { + link, err := netlink.LinkByIndex(route.LinkIndex) + if err == nil && link.Attrs().Name == hostIfName { + if err := netlink.RouteDel(&route); err != nil { + log.Debugf("Failed to delete route %s dev %s: %v", + route.Dst.String(), hostIfName, err) + } else { + log.Infof("Removed route %s dev %s", + route.Dst.String(), hostIfName) + } } - break } } } From 47ac4986b7c1cd57d66addd6c50971d8b9a5ce81 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Mon, 4 Aug 2025 00:12:43 -0600 Subject: [PATCH 13/28] log adjustment and commented out cmd line tests --- runtime/docker/docker.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index bae4a0df6a..cc37e80b52 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -725,7 +725,7 @@ func (d *DockerRuntime) createHostMacvlanInterface() error { log.Debugf("Creating macvlan with mode='%s' value=%d", d.mgmt.MacvlanMode, mode) - log.Debugf("Creating macvlan with mode=%d (0=private, 1=vepa, 2=bridge, 3=passthru)", mode) + log.Debugf("Creating macvlan with mode=%d (0=private, 1=vepa, 2=passthru, 3=bridge)", mode) // Create macvlan link with minimal attributes first macvlan := &netlink.Macvlan{ @@ -742,17 +742,17 @@ func (d *DockerRuntime) createHostMacvlanInterface() error { macvlan.LinkAttrs.ParentIndex, macvlan.Mode) - // Try to create using command line as a test - cmd := osexec.Command("ip", "link", "add", hostIfName, "link", d.mgmt.MacvlanParent, "type", "macvlan", "mode", "bridge") - if output, err := cmd.CombinedOutput(); err != nil { - log.Debugf("Command line test failed: %v, output: %s", err, string(output)) - // Don't return here, continue with netlink - } else { - log.Debug("Command line creation succeeded, removing to use netlink") - // Remove it so we can create via netlink - delCmd := osexec.Command("ip", "link", "del", hostIfName) - delCmd.Run() - } + // // Try to create using command line as a test + // cmd := osexec.Command("ip", "link", "add", hostIfName, "link", d.mgmt.MacvlanParent, "type", "macvlan", "mode", "bridge") + // if output, err := cmd.CombinedOutput(); err != nil { + // log.Debugf("Command line test failed: %v, output: %s", err, string(output)) + // // Don't return here, continue with netlink + // } else { + // log.Debug("Command line creation succeeded, removing to use netlink") + // // Remove it so we can create via netlink + // delCmd := osexec.Command("ip", "link", "del", hostIfName) + // delCmd.Run() + // } // Create the interface via netlink if err := netlink.LinkAdd(macvlan); err != nil { From 94180990fc45dd93f8e2e46704c102a31497779e Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Mon, 4 Aug 2025 21:56:41 -0600 Subject: [PATCH 14/28] breaking out hostnet --- hostnet/macvlan.go | 339 ++++++++++++++++++++++++++++++++++ runtime/docker/docker.go | 379 +++------------------------------------ 2 files changed, 362 insertions(+), 356 deletions(-) create mode 100644 hostnet/macvlan.go diff --git a/hostnet/macvlan.go b/hostnet/macvlan.go new file mode 100644 index 0000000000..731b89576f --- /dev/null +++ b/hostnet/macvlan.go @@ -0,0 +1,339 @@ +package hostnet +import ( + "fmt" + osexec "os/exec" + "strings" + "net" + "regexp" + + "github.com/charmbracelet/log" + "github.com/vishvananda/netlink" +) + +package hostnet + +import ( + "fmt" + "net" + osexec "os/exec" + "regexp" + "strings" + + "github.com/charmbracelet/log" + "github.com/vishvananda/netlink" +) + +// MacvlanConfig contains all the configuration needed for macvlan operations +type MacvlanConfig struct { + NetworkName string + ParentIface string + MacvlanMode string + AuxAddress string + IPv4Subnet string +} + +// PostCreateMacvlanActions performs macvlan-specific post-creation actions +func PostCreateMacvlanActions(cfg *MacvlanConfig) error { + log.Info("Starting macvlan post-creation actions") + log.Debugf("AuxAddress: %s, IPv4Subnet: %s", cfg.AuxAddress, cfg.IPv4Subnet) + + // 1. Verify parent interface exists and is UP + parentLink, err := netlink.LinkByName(cfg.ParentIface) + if err != nil { + return fmt.Errorf("failed to get parent interface %s: %w", cfg.ParentIface, err) + } + + // Check if interface is UP + if parentLink.Attrs().OperState != netlink.OperUp { + log.Warnf("Parent interface %s is not UP (state: %s), containers may not have connectivity", + cfg.ParentIface, parentLink.Attrs().OperState) + } + + // 2. Check promiscuous mode + if parentLink.Attrs().Promisc == 0 { + log.Debugf("Parent interface %s is not in promiscuous mode, enabling it for better macvlan compatibility", + cfg.ParentIface) + if err := EnablePromiscuousMode(cfg.ParentIface); err != nil { + log.Warnf("failed to enable promiscuous mode on %s: %v", cfg.ParentIface, err) + } + } + + // 3. Log MTU information + parentMTU := parentLink.Attrs().MTU + log.Debugf("Parent interface %s has MTU %d, macvlan interfaces will inherit this", + cfg.ParentIface, parentMTU) + + // 4. Create host macvlan interface if aux address is specified + if cfg.AuxAddress != "" { + if err := CreateHostMacvlanInterface(cfg); err != nil { + // Don't fail the entire operation, just warn + log.Warnf("Failed to create host macvlan interface: %v", err) + log.Info("You can manually create it with:") + log.Infof(" sudo ip link add %s-host link %s type macvlan mode bridge", + cfg.NetworkName, cfg.ParentIface) + log.Infof(" sudo ip addr add %s/%s dev %s-host", + cfg.AuxAddress, getSubnetPrefix(cfg.IPv4Subnet), cfg.NetworkName) + log.Infof(" sudo ip link set %s-host up", cfg.NetworkName) + } else { + log.Infof("Created host macvlan interface %s-host with IP %s", + cfg.NetworkName, cfg.AuxAddress) + } + } else { + // Still warn about the limitation + log.Info("Note: Host cannot directly communicate with macvlan containers due to kernel limitations. " + + "Consider setting 'macvlan-aux' to create a host interface.") + } + + return nil +} + +// CreateHostMacvlanInterface creates a macvlan interface on the host for container communication +func CreateHostMacvlanInterface(cfg *MacvlanConfig) error { + hostIfNameNonAlpha := cfg.NetworkName + "-host" + hostIfName := SanitizeInterfaceName(hostIfNameNonAlpha) + + log.Debugf("Creating host macvlan interface: name=%s, parent=%s, mode=%s", + hostIfName, cfg.ParentIface, cfg.MacvlanMode) + + // Check if interface already exists + if existingLink, err := netlink.LinkByName(hostIfName); err == nil { + log.Debugf("Host macvlan interface %s already exists", hostIfName) + // Check if it has the correct IP + addrs, err := netlink.AddrList(existingLink, netlink.FAMILY_V4) + if err == nil { + for _, addr := range addrs { + if addr.IP.String() == cfg.AuxAddress { + log.Debugf("Interface %s already has IP %s", hostIfName, cfg.AuxAddress) + return nil + } + } + } + // Interface exists but might not have the right IP, delete and recreate + log.Debugf("Removing existing interface %s to recreate with correct settings", hostIfName) + if err := netlink.LinkDel(existingLink); err != nil { + log.Warnf("Failed to delete existing interface: %v", err) + } + } + + // Get parent link + parentLink, err := netlink.LinkByName(cfg.ParentIface) + if err != nil { + return fmt.Errorf("parent interface %s not found: %w", cfg.ParentIface, err) + } + + // Determine macvlan mode + mode := parseMacvlanMode(cfg.MacvlanMode) + + // Create macvlan link + macvlan := &netlink.Macvlan{ + LinkAttrs: netlink.LinkAttrs{ + Name: hostIfName, + ParentIndex: parentLink.Attrs().Index, + }, + Mode: mode, + } + + // Create the interface via netlink + if err := netlink.LinkAdd(macvlan); err != nil { + if strings.Contains(err.Error(), "numerical result") { + log.Errorf("Netlink error details - this often indicates an issue with the parent interface index or mode value") + log.Errorf("Parent index: %d, Mode: %d", parentLink.Attrs().Index, mode) + } + return fmt.Errorf("failed to create macvlan interface: %w", err) + } + + // Get the created interface + link, err := netlink.LinkByName(hostIfName) + if err != nil { + netlink.LinkDel(macvlan) + return fmt.Errorf("failed to get created interface: %w", err) + } + + // Parse and add IP address + addrStr := cfg.AuxAddress + "/32" + addr, err := netlink.ParseAddr(addrStr) + if err != nil { + netlink.LinkDel(link) + return fmt.Errorf("failed to parse IP address %s: %w", addrStr, err) + } + + if err := netlink.AddrAdd(link, addr); err != nil { + netlink.LinkDel(link) + return fmt.Errorf("failed to add IP address: %w", err) + } + + // Bring the interface up + if err := netlink.LinkSetUp(link); err != nil { + netlink.LinkDel(link) + return fmt.Errorf("failed to bring interface up: %w", err) + } + + // Add route to the subnet + _, ipnet, err := net.ParseCIDR(cfg.IPv4Subnet) + if err != nil { + log.Warnf("Failed to parse subnet for route: %v", err) + return nil + } + + route := &netlink.Route{ + LinkIndex: link.Attrs().Index, + Dst: ipnet, + Scope: netlink.SCOPE_LINK, + } + + if err := netlink.RouteAdd(route); err != nil { + if !strings.Contains(err.Error(), "file exists") { + log.Warnf("Failed to add route %s dev %s: %v", cfg.IPv4Subnet, hostIfName, err) + } + } else { + log.Infof("Added route %s dev %s", cfg.IPv4Subnet, hostIfName) + } + + return nil +} + +// CleanupMacvlanPostActions reverses the changes made in PostCreateMacvlanActions +func CleanupMacvlanPostActions(cfg *MacvlanConfig) error { + // First, remove the static route if it exists + if cfg.AuxAddress != "" && cfg.IPv4Subnet != "" { + hostIfNameNonAlpha := cfg.NetworkName + "-host" + hostIfName := SanitizeInterfaceName(hostIfNameNonAlpha) + + _, ipnet, err := net.ParseCIDR(cfg.IPv4Subnet) + if err == nil { + routes, err := netlink.RouteList(nil, netlink.FAMILY_V4) + if err == nil { + for _, route := range routes { + if route.Dst != nil && route.Dst.String() == ipnet.String() { + if route.LinkIndex > 0 { + link, err := netlink.LinkByIndex(route.LinkIndex) + if err == nil && link.Attrs().Name == hostIfName { + if err := netlink.RouteDel(&route); err != nil { + log.Debugf("Failed to delete route %s dev %s: %v", + route.Dst.String(), hostIfName, err) + } else { + log.Infof("Removed route %s dev %s", + route.Dst.String(), hostIfName) + } + } + } + } + } + } + } + } + + // Then cleanup the host interface + if err := CleanupHostMacvlanInterface(cfg); err != nil { + log.Warnf("Failed to cleanup host macvlan interface: %v", err) + } + + // Disable promiscuous mode on parent interface + parentLink, err := netlink.LinkByName(cfg.ParentIface) + if err != nil { + log.Debugf("Parent interface %s not found during cleanup: %v", cfg.ParentIface, err) + return nil + } + + // Check if there are other macvlan interfaces using this parent + links, err := netlink.LinkList() + if err == nil { + otherMacvlans := false + hostIfName := SanitizeInterfaceName(cfg.NetworkName + "-host") + for _, link := range links { + if macvlan, ok := link.(*netlink.Macvlan); ok { + if macvlan.ParentIndex == parentLink.Attrs().Index && + macvlan.Name != hostIfName { + otherMacvlans = true + break + } + } + } + + // Only disable promiscuous mode if no other macvlans are using this parent + if !otherMacvlans { + if err := DisablePromiscuousMode(cfg.ParentIface); err != nil { + log.Warnf("Failed to disable promiscuous mode on %s: %v", cfg.ParentIface, err) + } else { + log.Debugf("Disabled promiscuous mode on %s", cfg.ParentIface) + } + } else { + log.Debugf("Other macvlan interfaces exist on %s, keeping promiscuous mode enabled", cfg.ParentIface) + } + } + + return nil +} + +// CleanupHostMacvlanInterface removes the host macvlan interface if it exists +func CleanupHostMacvlanInterface(cfg *MacvlanConfig) error { + if cfg.AuxAddress == "" { + return nil + } + + hostIfNameNonAlpha := cfg.NetworkName + "-host" + hostIfName := SanitizeInterfaceName(hostIfNameNonAlpha) + + link, err := netlink.LinkByName(hostIfName) + if err != nil { + // Interface doesn't exist, nothing to clean up + return nil + } + + if err := netlink.LinkDel(link); err != nil { + return fmt.Errorf("failed to delete host macvlan interface: %w", err) + } + + log.Infof("Removed host macvlan interface %s", hostIfName) + return nil +} + +// EnablePromiscuousMode enables promiscuous mode on an interface +func EnablePromiscuousMode(ifName string) error { + cmd := osexec.Command("ip", "link", "set", ifName, "promisc", "on") + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to enable promiscuous mode: %w", err) + } + return nil +} + +// DisablePromiscuousMode disables promiscuous mode on an interface +func DisablePromiscuousMode(ifName string) error { + cmd := osexec.Command("ip", "link", "set", ifName, "promisc", "off") + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to disable promiscuous mode: %w", err) + } + return nil +} + +// SanitizeInterfaceName removes non-alphanumeric characters from interface names +func SanitizeInterfaceName(input string) string { + re := regexp.MustCompile(`[^a-zA-Z0-9]+`) + return re.ReplaceAllString(input, "") +} + +// Helper functions + +func getSubnetPrefix(subnet string) string { + parts := strings.Split(subnet, "/") + if len(parts) == 2 { + return parts[1] + } + return "24" // default +} + +func parseMacvlanMode(mode string) netlink.MacvlanMode { + switch mode { + case "", "bridge": + return netlink.MACVLAN_MODE_BRIDGE + case "vepa": + return netlink.MACVLAN_MODE_VEPA + case "private": + return netlink.MACVLAN_MODE_PRIVATE + case "passthru": + return netlink.MACVLAN_MODE_PASSTHRU + default: + log.Warnf("Unknown macvlan mode %s, defaulting to bridge", mode) + return netlink.MACVLAN_MODE_BRIDGE + } +} \ No newline at end of file diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index cc37e80b52..2b7a877dbc 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -10,13 +10,10 @@ import ( "errors" "fmt" "os" - osexec "os/exec" "path" "strconv" "strings" "time" - "net" - "regexp" "github.com/docker/docker/api/types/image" "github.com/docker/go-units" @@ -38,6 +35,7 @@ import ( "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/types" "github.com/srl-labs/containerlab/utils" + "github.com/srl-labs/containerlab/hostnet" "github.com/vishvananda/netlink" "golang.org/x/mod/semver" ) @@ -285,6 +283,17 @@ func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) { return nil } +func (d *DockerRuntime) postCreateMacvlanActions() error { + cfg := &hostnet.MacvlanConfig{ + NetworkName: d.mgmt.Network, + ParentIface: d.mgmt.MacvlanParent, + MacvlanMode: d.mgmt.MacvlanMode, + AuxAddress: d.mgmt.MacvlanAux, + IPv4Subnet: d.mgmt.IPv4Subnet, + } + return hostnet.PostCreateMacvlanActions(cfg) +} + // createMgmtMacvlan creates a macvlan network for management func (d *DockerRuntime) createMgmtMacvlan(nctx context.Context) error { // Validate parent interface is specified @@ -605,359 +614,6 @@ func (d *DockerRuntime) postCreateNetActions() (err error) { return nil } -// postCreateMacvlanActions performs macvlan-specific post-creation actions -func (d *DockerRuntime) postCreateMacvlanActions() error { - //debugging - log.Info("Starting macvlan post-creation actions") - log.Debugf("MacvlanAux: %s, IPv4Subnet: %s", d.mgmt.MacvlanAux, d.mgmt.IPv4Subnet) - // 1. Verify parent interface exists and is UP - parentLink, err := netlink.LinkByName(d.mgmt.MacvlanParent) - if err != nil { - return fmt.Errorf("failed to get parent interface %s: %w", d.mgmt.MacvlanParent, err) - } - - // Check if interface is UP - if parentLink.Attrs().OperState != netlink.OperUp { - log.Warnf("Parent interface %s is not UP (state: %s), containers may not have connectivity", - d.mgmt.MacvlanParent, parentLink.Attrs().OperState) - } - - // 2. Check promiscuous mode - if parentLink.Attrs().Promisc == 0 { - log.Debugf("Parent interface %s is not in promiscuous mode, enabling it for better macvlan compatibility", - d.mgmt.MacvlanParent) - // Enable promiscuous mode using command execution as a fallback - if err := enablePromiscuousMode(d.mgmt.MacvlanParent); err != nil { - log.Warnf("failed to enable promiscuous mode on %s: %v", d.mgmt.MacvlanParent, err) - } - } - - // 3. Log MTU information - parentMTU := parentLink.Attrs().MTU - log.Debugf("Parent interface %s has MTU %d, macvlan interfaces will inherit this", - d.mgmt.MacvlanParent, parentMTU) - - // 4. Create host macvlan interface if aux address is specified - if d.mgmt.MacvlanAux != "" { - if err := d.createHostMacvlanInterface(); err != nil { - // Don't fail the entire operation, just warn - log.Warnf("Failed to create host macvlan interface: %v", err) - log.Info("You can manually create it with:") - log.Infof(" sudo ip link add %s-host link %s type macvlan mode bridge", - d.mgmt.Network, d.mgmt.MacvlanParent) - log.Infof(" sudo ip addr add %s/%s dev %s-host", - d.mgmt.MacvlanAux, getSubnetPrefix(d.mgmt.IPv4Subnet), d.mgmt.Network) - log.Infof(" sudo ip link set %s-host up", d.mgmt.Network) - } else { - log.Infof("Created host macvlan interface %s-host with IP %s", - d.mgmt.Network, d.mgmt.MacvlanAux) - } - } else { - // Still warn about the limitation - log.Info("Note: Host cannot directly communicate with macvlan containers due to kernel limitations. " + - "Consider setting 'macvlan-aux' to create a host interface.") - } - - return nil -} - -func sanitizeAlphanumeric(input string) string { - re := regexp.MustCompile(`[^a-zA-Z0-9]+`) - return re.ReplaceAllString(input, "") -} - -// createHostMacvlanInterface creates a macvlan interface on the host for container communication -func (d *DockerRuntime) createHostMacvlanInterface() error { - hostIfNameNonAlpha := d.mgmt.Network + "-host" - hostIfName := sanitizeAlphanumeric(hostIfNameNonAlpha) - - log.Debugf("Creating host macvlan interface: name=%s, parent=%s, mode=%s", - hostIfName, d.mgmt.MacvlanParent, d.mgmt.MacvlanMode) - - // Check if interface already exists - if existingLink, err := netlink.LinkByName(hostIfName); err == nil { - log.Debugf("Host macvlan interface %s already exists", hostIfName) - // Check if it has the correct IP - addrs, err := netlink.AddrList(existingLink, netlink.FAMILY_V4) - if err == nil { - for _, addr := range addrs { - if addr.IP.String() == d.mgmt.MacvlanAux { - log.Debugf("Interface %s already has IP %s", hostIfName, d.mgmt.MacvlanAux) - return nil - } - } - } - // Interface exists but might not have the right IP, delete and recreate - log.Debugf("Removing existing interface %s to recreate with correct settings", hostIfName) - if err := netlink.LinkDel(existingLink); err != nil { - log.Warnf("Failed to delete existing interface: %v", err) - } - } - - // Get parent link - parentLink, err := netlink.LinkByName(d.mgmt.MacvlanParent) - if err != nil { - return fmt.Errorf("parent interface %s not found: %w", d.mgmt.MacvlanParent, err) - } - - log.Debugf("Parent interface details: name=%s, index=%d, mtu=%d, state=%s", - parentLink.Attrs().Name, - parentLink.Attrs().Index, - parentLink.Attrs().MTU, - parentLink.Attrs().OperState) - - - // Determine macvlan mode - explicitly handle all cases - var mode netlink.MacvlanMode - switch d.mgmt.MacvlanMode { - case "", "bridge": // default to bridge if empty - mode = netlink.MACVLAN_MODE_BRIDGE - case "vepa": - mode = netlink.MACVLAN_MODE_VEPA - case "private": - mode = netlink.MACVLAN_MODE_PRIVATE - case "passthru": - mode = netlink.MACVLAN_MODE_PASSTHRU - default: - log.Warnf("Unknown macvlan mode %s, defaulting to bridge", d.mgmt.MacvlanMode) - mode = netlink.MACVLAN_MODE_BRIDGE - } - - log.Debugf("Creating macvlan with mode='%s' value=%d", d.mgmt.MacvlanMode, mode) - - log.Debugf("Creating macvlan with mode=%d (0=private, 1=vepa, 2=passthru, 3=bridge)", mode) - - // Create macvlan link with minimal attributes first - macvlan := &netlink.Macvlan{ - LinkAttrs: netlink.LinkAttrs{ - Name: hostIfName, - ParentIndex: parentLink.Attrs().Index, - }, - Mode: mode, - } - - // Log the structure before creation - log.Debugf("Macvlan struct: Name=%s, ParentIndex=%d, Mode=%d", - macvlan.LinkAttrs.Name, - macvlan.LinkAttrs.ParentIndex, - macvlan.Mode) - - // // Try to create using command line as a test - // cmd := osexec.Command("ip", "link", "add", hostIfName, "link", d.mgmt.MacvlanParent, "type", "macvlan", "mode", "bridge") - // if output, err := cmd.CombinedOutput(); err != nil { - // log.Debugf("Command line test failed: %v, output: %s", err, string(output)) - // // Don't return here, continue with netlink - // } else { - // log.Debug("Command line creation succeeded, removing to use netlink") - // // Remove it so we can create via netlink - // delCmd := osexec.Command("ip", "link", "del", hostIfName) - // delCmd.Run() - // } - - // Create the interface via netlink - if err := netlink.LinkAdd(macvlan); err != nil { - // Try to get more error details - if strings.Contains(err.Error(), "numerical result") { - log.Errorf("Netlink error details - this often indicates an issue with the parent interface index or mode value") - log.Errorf("Parent index: %d, Mode: %d", parentLink.Attrs().Index, mode) - } - return fmt.Errorf("failed to create macvlan interface: %w", err) - } - - // Rest of the function remains the same... - log.Debug("Macvlan interface created successfully via netlink") - - // Get the created interface - link, err := netlink.LinkByName(hostIfName) - if err != nil { - // Cleanup on failure - netlink.LinkDel(macvlan) - return fmt.Errorf("failed to get created interface: %w", err) - } - - // Parse and add IP address - // Use /32 for the host interface to avoid subnet conflicts - addrStr := d.mgmt.MacvlanAux + "/32" - log.Debugf("Adding IP address %s to interface %s", addrStr, hostIfName) - - addr, err := netlink.ParseAddr(addrStr) - if err != nil { - // Cleanup on failure - netlink.LinkDel(link) - return fmt.Errorf("failed to parse IP address %s: %w", addrStr, err) - } - - if err := netlink.AddrAdd(link, addr); err != nil { - // Cleanup on failure - netlink.LinkDel(link) - return fmt.Errorf("failed to add IP address: %w", err) - } - - // Bring the interface up - if err := netlink.LinkSetUp(link); err != nil { - // Cleanup on failure - netlink.LinkDel(link) - return fmt.Errorf("failed to bring interface up: %w", err) - } - - log.Infof("Created host macvlan interface %s with IP %s", hostIfName, d.mgmt.MacvlanAux) - - // Add route to the subnet - _, ipnet, err := net.ParseCIDR(d.mgmt.IPv4Subnet) - if err != nil { - log.Warnf("Failed to parse subnet for route: %v", err) - return nil - } - - // For the route, we need to use the macvlan interface as the output device - route := &netlink.Route{ - LinkIndex: link.Attrs().Index, - Dst: ipnet, - Scope: netlink.SCOPE_LINK, - } - - if err := netlink.RouteAdd(route); err != nil { - // Check if route already exists - if !strings.Contains(err.Error(), "file exists") { - log.Warnf("Failed to add route %s dev %s: %v", d.mgmt.IPv4Subnet, hostIfName, err) - } - } else { - log.Infof("Added route %s dev %s", d.mgmt.IPv4Subnet, hostIfName) - } - - return nil -} - -// getSubnetPrefix extracts the prefix length from a CIDR notation -func getSubnetPrefix(subnet string) string { - parts := strings.Split(subnet, "/") - if len(parts) == 2 { - return parts[1] - } - return "24" // default -} - -// cleanupMacvlanPostActions reverses the changes made in postCreateMacvlanActions -func (d *DockerRuntime) cleanupMacvlanPostActions() error { - // First, remove the static route if it exists - // In cleanupMacvlanPostActions(), fix the route cleanup: - if d.mgmt.MacvlanAux != "" && d.mgmt.IPv4Subnet != "" { - // Get the sanitized host interface name - hostIfNameNonAlpha := d.mgmt.Network + "-host" - hostIfName := sanitizeAlphanumeric(hostIfNameNonAlpha) - - _, ipnet, err := net.ParseCIDR(d.mgmt.IPv4Subnet) - if err == nil { - // Find and delete routes associated with this interface - routes, err := netlink.RouteList(nil, netlink.FAMILY_V4) - if err == nil { - for _, route := range routes { - // Check if this route is for our subnet and uses our interface - if route.Dst != nil && route.Dst.String() == ipnet.String() { - // Get the link to check if it's our interface - if route.LinkIndex > 0 { - link, err := netlink.LinkByIndex(route.LinkIndex) - if err == nil && link.Attrs().Name == hostIfName { - if err := netlink.RouteDel(&route); err != nil { - log.Debugf("Failed to delete route %s dev %s: %v", - route.Dst.String(), hostIfName, err) - } else { - log.Infof("Removed route %s dev %s", - route.Dst.String(), hostIfName) - } - } - } - } - } - } - } - } - - // Then cleanup the host interface (this might also remove associated routes) - if err := d.cleanupHostMacvlanInterface(); err != nil { - log.Warnf("Failed to cleanup host macvlan interface: %v", err) - } - - // Disable promiscuous mode on parent interface - parentLink, err := netlink.LinkByName(d.mgmt.MacvlanParent) - if err != nil { - // Parent interface might not exist anymore - log.Debugf("Parent interface %s not found during cleanup: %v", d.mgmt.MacvlanParent, err) - return nil - } - - // Check if there are other macvlan interfaces using this parent - links, err := netlink.LinkList() - if err == nil { - otherMacvlans := false - for _, link := range links { - if macvlan, ok := link.(*netlink.Macvlan); ok { - if macvlan.ParentIndex == parentLink.Attrs().Index && - macvlan.Name != d.mgmt.Network+"-host" { - otherMacvlans = true - break - } - } - } - - // Only disable promiscuous mode if no other macvlans are using this parent - if !otherMacvlans { - if err := disablePromiscuousMode(d.mgmt.MacvlanParent); err != nil { - log.Warnf("Failed to disable promiscuous mode on %s: %v", d.mgmt.MacvlanParent, err) - } else { - log.Debugf("Disabled promiscuous mode on %s", d.mgmt.MacvlanParent) - } - } else { - log.Debugf("Other macvlan interfaces exist on %s, keeping promiscuous mode enabled", d.mgmt.MacvlanParent) - } - } - - return nil -} - -// enablePromiscuousMode enables promiscuous mode on an interface -func enablePromiscuousMode(ifName string) error { - // Try using exec to run ip command as a fallback - cmd := osexec.Command("ip", "link", "set", ifName, "promisc", "on") - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to enable promiscuous mode: %w", err) - } - return nil -} - -// disablePromiscuousMode disables promiscuous mode on an interface -func disablePromiscuousMode(ifName string) error { - // Try using exec to run ip command as a fallback - cmd := osexec.Command("ip", "link", "set", ifName, "promisc", "off") - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to disable promiscuous mode: %w", err) - } - return nil -} - -// cleanupHostMacvlanInterface removes the host macvlan interface if it exists -func (d *DockerRuntime) cleanupHostMacvlanInterface() error { - if d.mgmt.MacvlanAux == "" { - return nil - } - - hostIfNameNonAlpha := d.mgmt.Network + "-host" - hostIfName := sanitizeAlphanumeric(hostIfNameNonAlpha) - - link, err := netlink.LinkByName(hostIfName) - if err != nil { - // Interface doesn't exist, nothing to clean up - return nil - } - - if err := netlink.LinkDel(link); err != nil { - return fmt.Errorf("failed to delete host macvlan interface: %w", err) - } - - log.Infof("Removed host macvlan interface %s", hostIfName) - return nil -} - // DeleteNet deletes a docker bridge or macvlan network. func (d *DockerRuntime) DeleteNet(ctx context.Context) (err error) { network := d.mgmt.Network @@ -1006,6 +662,17 @@ func (d *DockerRuntime) DeleteNet(ctx context.Context) (err error) { return nil } +func (d *DockerRuntime) cleanupMacvlanPostActions() error { + cfg := &hostnet.MacvlanConfig{ + NetworkName: d.mgmt.Network, + ParentIface: d.mgmt.MacvlanParent, + MacvlanMode: d.mgmt.MacvlanMode, + AuxAddress: d.mgmt.MacvlanAux, + IPv4Subnet: d.mgmt.IPv4Subnet, + } + return hostnet.CleanupMacvlanPostActions(cfg) +} + // PauseContainer Pauses a container identified by its name. func (d *DockerRuntime) PauseContainer(ctx context.Context, cID string) error { return d.Client.ContainerPause(ctx, cID) From f26a991d2479e36a85195bce804fbcdf73e022fd Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Mon, 4 Aug 2025 21:59:14 -0600 Subject: [PATCH 15/28] imports fix --- hostnet/macvlan.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/hostnet/macvlan.go b/hostnet/macvlan.go index 731b89576f..5b544bcae7 100644 --- a/hostnet/macvlan.go +++ b/hostnet/macvlan.go @@ -10,19 +10,6 @@ import ( "github.com/vishvananda/netlink" ) -package hostnet - -import ( - "fmt" - "net" - osexec "os/exec" - "regexp" - "strings" - - "github.com/charmbracelet/log" - "github.com/vishvananda/netlink" -) - // MacvlanConfig contains all the configuration needed for macvlan operations type MacvlanConfig struct { NetworkName string From e075b95951c0ac51649601cfabd79579af89e1a6 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Tue, 5 Aug 2025 00:16:44 -0600 Subject: [PATCH 16/28] log items --- runtime/docker/docker.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 2b7a877dbc..0a72eaecf8 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -409,10 +409,10 @@ func (d *DockerRuntime) createMgmtMacvlan(nctx context.Context) error { // If aux address was specified, provide instructions for creating host macvlan interface if d.mgmt.MacvlanAux != "" { log.Info("To enable host communication with containers, create a macvlan interface on the host:") - log.Infof(" sudo ip link add %s-host link %s type macvlan mode bridge", d.mgmt.Network, d.mgmt.MacvlanParent) - log.Infof(" sudo ip addr add %s/32 dev %s-host", d.mgmt.MacvlanAux, d.mgmt.Network) - log.Infof(" sudo ip link set %s-host up", d.mgmt.Network) - log.Infof(" sudo ip route add %s dev %s-host", d.mgmt.IPv4Subnet, d.mgmt.Network) + log.Infof(" sudo ip link add %shost link %s type macvlan mode bridge", hostnet.SanitizeInterfaceName(d.mgmt.Network), d.mgmt.MacvlanParent) + log.Infof(" sudo ip addr add %s/32 dev %shost", d.mgmt.MacvlanAux, hostnet.SanitizeInterfaceName(d.mgmt.Network)) + log.Infof(" sudo ip link set %shost up", hostnet.SanitizeInterfaceName(d.mgmt.Network)) + log.Infof(" sudo ip route add %s dev %shost", d.mgmt.IPv4Subnet, hostnet.SanitizeInterfaceName(d.mgmt.Network)) } return nil From 88c1bfee3b936c2a321d32fe0458e3cd06a4e15e Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Tue, 5 Aug 2025 10:29:02 -0600 Subject: [PATCH 17/28] removed unused build tags. hard coded host macvlan adapter subnet for testing. --- hostnet/macvlan.go | 2 +- runtime/docker/docker.go | 2 +- runtime/docker/nl_linux.go | 10 ---------- runtime/docker/nl_stub.go | 8 -------- 4 files changed, 2 insertions(+), 20 deletions(-) delete mode 100644 runtime/docker/nl_linux.go delete mode 100644 runtime/docker/nl_stub.go diff --git a/hostnet/macvlan.go b/hostnet/macvlan.go index 5b544bcae7..ec7b10d1c4 100644 --- a/hostnet/macvlan.go +++ b/hostnet/macvlan.go @@ -137,7 +137,7 @@ func CreateHostMacvlanInterface(cfg *MacvlanConfig) error { } // Parse and add IP address - addrStr := cfg.AuxAddress + "/32" + addrStr := cfg.AuxAddress + "/26" addr, err := netlink.ParseAddr(addrStr) if err != nil { netlink.LinkDel(link) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 0a72eaecf8..22f10f1bf7 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -410,7 +410,7 @@ func (d *DockerRuntime) createMgmtMacvlan(nctx context.Context) error { if d.mgmt.MacvlanAux != "" { log.Info("To enable host communication with containers, create a macvlan interface on the host:") log.Infof(" sudo ip link add %shost link %s type macvlan mode bridge", hostnet.SanitizeInterfaceName(d.mgmt.Network), d.mgmt.MacvlanParent) - log.Infof(" sudo ip addr add %s/32 dev %shost", d.mgmt.MacvlanAux, hostnet.SanitizeInterfaceName(d.mgmt.Network)) + log.Infof(" sudo ip addr add %s/26 dev %shost", d.mgmt.MacvlanAux, hostnet.SanitizeInterfaceName(d.mgmt.Network)) log.Infof(" sudo ip link set %shost up", hostnet.SanitizeInterfaceName(d.mgmt.Network)) log.Infof(" sudo ip route add %s dev %shost", d.mgmt.IPv4Subnet, hostnet.SanitizeInterfaceName(d.mgmt.Network)) } diff --git a/runtime/docker/nl_linux.go b/runtime/docker/nl_linux.go deleted file mode 100644 index c354627ffd..0000000000 --- a/runtime/docker/nl_linux.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build linux -// +build linux - -package docker - -import "github.com/vishvananda/netlink" - -func getIPv4Family() int { - return netlink.FAMILY_V4 -} diff --git a/runtime/docker/nl_stub.go b/runtime/docker/nl_stub.go deleted file mode 100644 index 20bb59ba0b..0000000000 --- a/runtime/docker/nl_stub.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build !linux -// +build !linux - -package docker - -func getIPv4Family() int { - return 2 // syscall.AF_INET; safe fallback if you just want to compile -} From 6066bfb32d1c56852cf2ba1bc68785fc6c58ceaf Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Tue, 5 Aug 2025 13:26:18 -0600 Subject: [PATCH 18/28] improved host static route --- hostnet/macvlan.go | 152 +++++++++++++++++++++++++++++++++------------ 1 file changed, 111 insertions(+), 41 deletions(-) diff --git a/hostnet/macvlan.go b/hostnet/macvlan.go index ec7b10d1c4..5ee313d8a3 100644 --- a/hostnet/macvlan.go +++ b/hostnet/macvlan.go @@ -51,29 +51,95 @@ func PostCreateMacvlanActions(cfg *MacvlanConfig) error { cfg.ParentIface, parentMTU) // 4. Create host macvlan interface if aux address is specified - if cfg.AuxAddress != "" { - if err := CreateHostMacvlanInterface(cfg); err != nil { - // Don't fail the entire operation, just warn - log.Warnf("Failed to create host macvlan interface: %v", err) - log.Info("You can manually create it with:") - log.Infof(" sudo ip link add %s-host link %s type macvlan mode bridge", - cfg.NetworkName, cfg.ParentIface) - log.Infof(" sudo ip addr add %s/%s dev %s-host", - cfg.AuxAddress, getSubnetPrefix(cfg.IPv4Subnet), cfg.NetworkName) - log.Infof(" sudo ip link set %s-host up", cfg.NetworkName) - } else { - log.Infof("Created host macvlan interface %s-host with IP %s", - cfg.NetworkName, cfg.AuxAddress) - } - } else { - // Still warn about the limitation - log.Info("Note: Host cannot directly communicate with macvlan containers due to kernel limitations. " + - "Consider setting 'macvlan-aux' to create a host interface.") - } + if cfg.AuxAddress != "" { + // Check for potential subnet conflicts + if err := checkSubnetConflicts(cfg); err != nil { + log.Warnf("Subnet configuration warning: %v", err) + log.Info("") + log.Info("=== MACVLAN SUBNET CONFIGURATION GUIDANCE ===") + log.Info("When the macvlan subnet matches your host's subnet, you have three options:") + log.Info("") + log.Info("Option 1: Use a smaller subnet for container routes") + log.Info(" - If host is on 192.168.1.0/24, use a /26 or /27 for containers") + log.Info(" - Example: ipv4-subnet: 192.168.1.128/26") + log.Info(" - This allows 62 container IPs while avoiding route conflicts") + log.Info("") + log.Info("Option 2: Use a different subnet with proper routing") + log.Info(" - Use a completely different subnet (e.g., 10.100.0.0/24)") + log.Info(" - Configure routing on your network to reach this subnet") + log.Info(" - Containers won't be on the same L2 segment as other devices") + log.Info("") + log.Info("Option 3: Accept no host-to-container connectivity") + log.Info(" - Don't set macvlan-aux (no host interface)") + log.Info(" - Containers can reach external networks") + log.Info(" - Host cannot directly communicate with containers") + log.Info("=============================================") + log.Info("") + } + + if err := CreateHostMacvlanInterface(cfg); err != nil { + // Don't fail the entire operation, just warn + log.Warnf("Failed to create host macvlan interface: %v", err) + // ... rest of manual instructions ... + } else { + log.Infof("Created host macvlan interface %s-host with IP %s", + cfg.NetworkName, cfg.AuxAddress) + } + } else { + // Still warn about the limitation + log.Info("Note: Host cannot directly communicate with macvlan containers due to kernel limitations. " + + "Consider setting 'macvlan-aux' to create a host interface.") + } return nil } +// checkSubnetConflicts checks if the macvlan subnet conflicts with existing routes +func checkSubnetConflicts(cfg *MacvlanConfig) error { + _, macvlanNet, err := net.ParseCIDR(cfg.IPv4Subnet) + if err != nil { + return fmt.Errorf("invalid subnet: %w", err) + } + + // Get existing routes + routes, err := netlink.RouteList(nil, netlink.FAMILY_V4) + if err != nil { + return fmt.Errorf("failed to list routes: %w", err) + } + + // Check for conflicts + for _, route := range routes { + if route.Dst != nil { + // Skip default route + if route.Dst.String() == "0.0.0.0/0" { + continue + } + + // Check if macvlan subnet overlaps with existing route + if netsOverlap(macvlanNet, route.Dst) { + // Get the interface name for the route + var ifaceName string + if route.LinkIndex > 0 { + link, err := netlink.LinkByIndex(route.LinkIndex) + if err == nil { + ifaceName = link.Attrs().Name + } + } + + return fmt.Errorf("macvlan subnet %s conflicts with existing route %s on interface %s", + cfg.IPv4Subnet, route.Dst.String(), ifaceName) + } + } + } + + return nil +} + +// netsOverlap checks if two networks overlap +func netsOverlap(n1, n2 *net.IPNet) bool { + return n1.Contains(n2.IP) || n2.Contains(n1.IP) +} + // CreateHostMacvlanInterface creates a macvlan interface on the host for container communication func CreateHostMacvlanInterface(cfg *MacvlanConfig) error { hostIfNameNonAlpha := cfg.NetworkName + "-host" @@ -155,28 +221,32 @@ func CreateHostMacvlanInterface(cfg *MacvlanConfig) error { return fmt.Errorf("failed to bring interface up: %w", err) } - // Add route to the subnet - _, ipnet, err := net.ParseCIDR(cfg.IPv4Subnet) - if err != nil { - log.Warnf("Failed to parse subnet for route: %v", err) - return nil - } - - route := &netlink.Route{ - LinkIndex: link.Attrs().Index, - Dst: ipnet, - Scope: netlink.SCOPE_LINK, - } - - if err := netlink.RouteAdd(route); err != nil { - if !strings.Contains(err.Error(), "file exists") { - log.Warnf("Failed to add route %s dev %s: %v", cfg.IPv4Subnet, hostIfName, err) - } - } else { - log.Infof("Added route %s dev %s", cfg.IPv4Subnet, hostIfName) - } - - return nil + // Add route to the subnet with better error handling + _, ipnet, err := net.ParseCIDR(cfg.IPv4Subnet) + if err != nil { + log.Warnf("Failed to parse subnet for route: %v", err) + return nil + } + + route := &netlink.Route{ + LinkIndex: link.Attrs().Index, + Dst: ipnet, + Scope: netlink.SCOPE_LINK, + } + + if err := netlink.RouteAdd(route); err != nil { + if strings.Contains(err.Error(), "file exists") { + log.Warnf("Route %s already exists - this usually means the subnet overlaps with your host network", cfg.IPv4Subnet) + log.Info("Consider using a smaller subnet (e.g., /26 or /27) for the macvlan network") + } else { + log.Warnf("Failed to add route %s dev %s: %v", cfg.IPv4Subnet, hostIfName, err) + } + } else { + log.Infof("Added route %s dev %s", cfg.IPv4Subnet, hostIfName) + } + + return nil + } // CleanupMacvlanPostActions reverses the changes made in PostCreateMacvlanActions From 53792265a56e9c7ceca510d6f348b39c37a1d080 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Tue, 5 Aug 2025 15:38:30 -0600 Subject: [PATCH 19/28] containerlab macvlan static route adjustments --- hostnet/macvlan.go | 139 +++++++++++++++++++++++++-------------- runtime/docker/docker.go | 2 +- 2 files changed, 89 insertions(+), 52 deletions(-) diff --git a/hostnet/macvlan.go b/hostnet/macvlan.go index 5ee313d8a3..1841973d02 100644 --- a/hostnet/macvlan.go +++ b/hostnet/macvlan.go @@ -15,8 +15,8 @@ type MacvlanConfig struct { NetworkName string ParentIface string MacvlanMode string - AuxAddress string - IPv4Subnet string + AuxAddress string // Can be IP or IP/CIDR + IPv4Subnet string // The main macvlan network subnet } // PostCreateMacvlanActions performs macvlan-specific post-creation actions @@ -82,7 +82,7 @@ func PostCreateMacvlanActions(cfg *MacvlanConfig) error { log.Warnf("Failed to create host macvlan interface: %v", err) // ... rest of manual instructions ... } else { - log.Infof("Created host macvlan interface %s-host with IP %s", + log.Infof("Created host macvlan interface %shost with IP %s", cfg.NetworkName, cfg.AuxAddress) } } else { @@ -94,6 +94,27 @@ func PostCreateMacvlanActions(cfg *MacvlanConfig) error { return nil } +// parseAuxAddress extracts IP and subnet from aux address +// Returns: IP address, subnet CIDR, error +func parseAuxAddress(auxAddr string, defaultSubnet string) (string, string, error) { + // Check if it contains CIDR notation + if strings.Contains(auxAddr, "/") { + // Parse as CIDR + ip, ipnet, err := net.ParseCIDR(auxAddr) + if err != nil { + return "", "", fmt.Errorf("invalid CIDR notation in aux address: %w", err) + } + return ip.String(), ipnet.String(), nil + } + + // Just an IP address - use the default subnet + ip := net.ParseIP(auxAddr) + if ip == nil { + return "", "", fmt.Errorf("invalid IP address: %s", auxAddr) + } + return ip.String(), defaultSubnet, nil +} + // checkSubnetConflicts checks if the macvlan subnet conflicts with existing routes func checkSubnetConflicts(cfg *MacvlanConfig) error { _, macvlanNet, err := net.ParseCIDR(cfg.IPv4Subnet) @@ -142,11 +163,17 @@ func netsOverlap(n1, n2 *net.IPNet) bool { // CreateHostMacvlanInterface creates a macvlan interface on the host for container communication func CreateHostMacvlanInterface(cfg *MacvlanConfig) error { - hostIfNameNonAlpha := cfg.NetworkName + "-host" + hostIfNameNonAlpha := cfg.NetworkName + "host" hostIfName := SanitizeInterfaceName(hostIfNameNonAlpha) - log.Debugf("Creating host macvlan interface: name=%s, parent=%s, mode=%s", - hostIfName, cfg.ParentIface, cfg.MacvlanMode) + // Parse aux address to get IP and route subnet + auxIP, routeSubnet, err := parseAuxAddress(cfg.AuxAddress, cfg.IPv4Subnet) + if err != nil { + return fmt.Errorf("failed to parse aux address: %w", err) + } + + log.Debugf("Creating host macvlan interface: name=%s, parent=%s, mode=%s, IP=%s, route=%s", + hostIfName, cfg.ParentIface, cfg.MacvlanMode, auxIP, routeSubnet) // Check if interface already exists if existingLink, err := netlink.LinkByName(hostIfName); err == nil { @@ -155,8 +182,8 @@ func CreateHostMacvlanInterface(cfg *MacvlanConfig) error { addrs, err := netlink.AddrList(existingLink, netlink.FAMILY_V4) if err == nil { for _, addr := range addrs { - if addr.IP.String() == cfg.AuxAddress { - log.Debugf("Interface %s already has IP %s", hostIfName, cfg.AuxAddress) + if addr.IP.String() == auxIP { + log.Debugf("Interface %s already has IP %s", hostIfName, auxIP) return nil } } @@ -202,8 +229,8 @@ func CreateHostMacvlanInterface(cfg *MacvlanConfig) error { return fmt.Errorf("failed to get created interface: %w", err) } - // Parse and add IP address - addrStr := cfg.AuxAddress + "/26" + // Parse and add IP address (always use /32 for the interface itself) + addrStr := auxIP + "/32" addr, err := netlink.ParseAddr(addrStr) if err != nil { netlink.LinkDel(link) @@ -221,56 +248,66 @@ func CreateHostMacvlanInterface(cfg *MacvlanConfig) error { return fmt.Errorf("failed to bring interface up: %w", err) } - // Add route to the subnet with better error handling - _, ipnet, err := net.ParseCIDR(cfg.IPv4Subnet) - if err != nil { - log.Warnf("Failed to parse subnet for route: %v", err) - return nil - } - - route := &netlink.Route{ - LinkIndex: link.Attrs().Index, - Dst: ipnet, - Scope: netlink.SCOPE_LINK, - } - - if err := netlink.RouteAdd(route); err != nil { - if strings.Contains(err.Error(), "file exists") { - log.Warnf("Route %s already exists - this usually means the subnet overlaps with your host network", cfg.IPv4Subnet) - log.Info("Consider using a smaller subnet (e.g., /26 or /27) for the macvlan network") - } else { - log.Warnf("Failed to add route %s dev %s: %v", cfg.IPv4Subnet, hostIfName, err) - } - } else { - log.Infof("Added route %s dev %s", cfg.IPv4Subnet, hostIfName) - } - - return nil + log.Infof("Created host macvlan interface %s with IP %s", hostIfName, auxIP) + + // Add route using the specified or default subnet + _, ipnet, err := net.ParseCIDR(routeSubnet) + if err != nil { + log.Warnf("Failed to parse subnet for route: %v", err) + return nil + } + + route := &netlink.Route{ + LinkIndex: link.Attrs().Index, + Dst: ipnet, + Scope: netlink.SCOPE_LINK, + } + + if err := netlink.RouteAdd(route); err != nil { + if strings.Contains(err.Error(), "file exists") { + log.Warnf("Route %s already exists - this usually means the subnet overlaps with your host network", routeSubnet) + if routeSubnet == cfg.IPv4Subnet { + log.Info("Consider using CIDR notation in macvlan-aux (e.g., 192.168.1.129/26) to specify a smaller route subnet") + } + } else { + log.Warnf("Failed to add route %s dev %s: %v", routeSubnet, hostIfName, err) + } + } else { + log.Infof("Added route %s dev %s", routeSubnet, hostIfName) + if routeSubnet != cfg.IPv4Subnet { + log.Infof("Note: Using route subnet %s (from aux CIDR) instead of full network %s", routeSubnet, cfg.IPv4Subnet) + } + } + return nil } // CleanupMacvlanPostActions reverses the changes made in PostCreateMacvlanActions func CleanupMacvlanPostActions(cfg *MacvlanConfig) error { // First, remove the static route if it exists if cfg.AuxAddress != "" && cfg.IPv4Subnet != "" { - hostIfNameNonAlpha := cfg.NetworkName + "-host" + hostIfNameNonAlpha := cfg.NetworkName + "host" hostIfName := SanitizeInterfaceName(hostIfNameNonAlpha) - _, ipnet, err := net.ParseCIDR(cfg.IPv4Subnet) + // Parse aux address to get the route subnet + _, routeSubnet, err := parseAuxAddress(cfg.AuxAddress, cfg.IPv4Subnet) if err == nil { - routes, err := netlink.RouteList(nil, netlink.FAMILY_V4) + _, ipnet, err := net.ParseCIDR(routeSubnet) if err == nil { - for _, route := range routes { - if route.Dst != nil && route.Dst.String() == ipnet.String() { - if route.LinkIndex > 0 { - link, err := netlink.LinkByIndex(route.LinkIndex) - if err == nil && link.Attrs().Name == hostIfName { - if err := netlink.RouteDel(&route); err != nil { - log.Debugf("Failed to delete route %s dev %s: %v", - route.Dst.String(), hostIfName, err) - } else { - log.Infof("Removed route %s dev %s", - route.Dst.String(), hostIfName) + routes, err := netlink.RouteList(nil, netlink.FAMILY_V4) + if err == nil { + for _, route := range routes { + if route.Dst != nil && route.Dst.String() == ipnet.String() { + if route.LinkIndex > 0 { + link, err := netlink.LinkByIndex(route.LinkIndex) + if err == nil && link.Attrs().Name == hostIfName { + if err := netlink.RouteDel(&route); err != nil { + log.Debugf("Failed to delete route %s dev %s: %v", + route.Dst.String(), hostIfName, err) + } else { + log.Infof("Removed route %s dev %s", + route.Dst.String(), hostIfName) + } } } } @@ -296,7 +333,7 @@ func CleanupMacvlanPostActions(cfg *MacvlanConfig) error { links, err := netlink.LinkList() if err == nil { otherMacvlans := false - hostIfName := SanitizeInterfaceName(cfg.NetworkName + "-host") + hostIfName := SanitizeInterfaceName(cfg.NetworkName + "host") for _, link := range links { if macvlan, ok := link.(*netlink.Macvlan); ok { if macvlan.ParentIndex == parentLink.Attrs().Index && @@ -328,7 +365,7 @@ func CleanupHostMacvlanInterface(cfg *MacvlanConfig) error { return nil } - hostIfNameNonAlpha := cfg.NetworkName + "-host" + hostIfNameNonAlpha := cfg.NetworkName + "host" hostIfName := SanitizeInterfaceName(hostIfNameNonAlpha) link, err := netlink.LinkByName(hostIfName) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 22f10f1bf7..def6802c81 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -410,7 +410,7 @@ func (d *DockerRuntime) createMgmtMacvlan(nctx context.Context) error { if d.mgmt.MacvlanAux != "" { log.Info("To enable host communication with containers, create a macvlan interface on the host:") log.Infof(" sudo ip link add %shost link %s type macvlan mode bridge", hostnet.SanitizeInterfaceName(d.mgmt.Network), d.mgmt.MacvlanParent) - log.Infof(" sudo ip addr add %s/26 dev %shost", d.mgmt.MacvlanAux, hostnet.SanitizeInterfaceName(d.mgmt.Network)) + log.Infof(" sudo ip addr add %s/ dev %shost", d.mgmt.MacvlanAux, hostnet.SanitizeInterfaceName(d.mgmt.Network)) log.Infof(" sudo ip link set %shost up", hostnet.SanitizeInterfaceName(d.mgmt.Network)) log.Infof(" sudo ip route add %s dev %shost", d.mgmt.IPv4Subnet, hostnet.SanitizeInterfaceName(d.mgmt.Network)) } From bfe22e22f4b64ea66978c7e6043fc1b64410b7c3 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Tue, 5 Aug 2025 16:01:34 -0600 Subject: [PATCH 20/28] handle aux CIDR in topo --- runtime/docker/docker.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index def6802c81..b3d32af447 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -14,6 +14,7 @@ import ( "strconv" "strings" "time" + "net" "github.com/docker/docker/api/types/image" "github.com/docker/go-units" @@ -330,8 +331,24 @@ func (d *DockerRuntime) createMgmtMacvlan(nctx context.Context) error { } // Add aux address if specified if d.mgmt.MacvlanAux != "" { + // Extract IP address from either IP or CIDR format + auxIP := d.mgmt.MacvlanAux + if strings.Contains(auxIP, "/") { + // It's a CIDR, extract just the IP part + ip, _, err := net.ParseCIDR(auxIP) + if err != nil { + return fmt.Errorf("invalid CIDR format for MacvlanAux: %v", err) + } + auxIP = ip.String() + } else { + // It's just an IP, validate it + if net.ParseIP(auxIP) == nil { + return fmt.Errorf("invalid IP address format for MacvlanAux: %s", auxIP) + } + } + ipamCfg.AuxAddress = map[string]string{ - "host": d.mgmt.MacvlanAux, + "host": auxIP, } } ipamConfig = append(ipamConfig, ipamCfg) From ccde65f4f7e7d6bafbfd840010ab1fe712085c0a Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Tue, 5 Aug 2025 16:18:30 -0600 Subject: [PATCH 21/28] removed redundant instructional logging --- runtime/docker/docker.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index b3d32af447..78c1ff1e28 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -423,15 +423,6 @@ func (d *DockerRuntime) createMgmtMacvlan(nctx context.Context) error { log.Info("Macvlan network created successfully", "name", d.mgmt.Network) - // If aux address was specified, provide instructions for creating host macvlan interface - if d.mgmt.MacvlanAux != "" { - log.Info("To enable host communication with containers, create a macvlan interface on the host:") - log.Infof(" sudo ip link add %shost link %s type macvlan mode bridge", hostnet.SanitizeInterfaceName(d.mgmt.Network), d.mgmt.MacvlanParent) - log.Infof(" sudo ip addr add %s/ dev %shost", d.mgmt.MacvlanAux, hostnet.SanitizeInterfaceName(d.mgmt.Network)) - log.Infof(" sudo ip link set %shost up", hostnet.SanitizeInterfaceName(d.mgmt.Network)) - log.Infof(" sudo ip route add %s dev %shost", d.mgmt.IPv4Subnet, hostnet.SanitizeInterfaceName(d.mgmt.Network)) - } - return nil } From fc0729532000012a477e2e42f5c3e001e39fb71e Mon Sep 17 00:00:00 2001 From: bird Date: Tue, 5 Aug 2025 18:46:19 -0600 Subject: [PATCH 22/28] Update types/types.go copilot recommended consistent spacing Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- types/types.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/types.go b/types/types.go index 81623917f6..af3f4c81d9 100644 --- a/types/types.go +++ b/types/types.go @@ -58,8 +58,8 @@ type MgmtNet struct { ExternalAccess *bool `yaml:"external-access,omitempty" json:"external-access,omitempty"` DriverOpts map[string]string `yaml:"driver-opts,omitempty" json:"driver-opts,omitempty"` // Macvlan specific options - MacvlanParent string `yaml:"macvlan-parent,omitempty" json:"macvlan-parent,omitempty"` // Parent interface for macvlan - MacvlanMode string `yaml:"macvlan-mode,omitempty" json:"macvlan-mode,omitempty"` // bridge, vepa, passthru, private + MacvlanParent string `yaml:"macvlan-parent,omitempty" json:"macvlan-parent,omitempty"` // Parent interface for macvlan + MacvlanMode string `yaml:"macvlan-mode,omitempty" json:"macvlan-mode,omitempty"` // bridge, vepa, passthru, private Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` // "bridge" or "macvlan" MacvlanAux string `yaml:"macvlan-aux,omitempty" json:"macvlan-aux,omitempty"` // Reserved IP Address for containerlab host connectivity } From 7b3ea859a85ec937537293d21bb64af43b91f556 Mon Sep 17 00:00:00 2001 From: bird Date: Tue, 5 Aug 2025 18:47:33 -0600 Subject: [PATCH 23/28] Update types/types.go copilot reccomended spacing Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- types/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/types.go b/types/types.go index af3f4c81d9..a3580d9c27 100644 --- a/types/types.go +++ b/types/types.go @@ -60,7 +60,7 @@ type MgmtNet struct { // Macvlan specific options MacvlanParent string `yaml:"macvlan-parent,omitempty" json:"macvlan-parent,omitempty"` // Parent interface for macvlan MacvlanMode string `yaml:"macvlan-mode,omitempty" json:"macvlan-mode,omitempty"` // bridge, vepa, passthru, private - Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` // "bridge" or "macvlan" + Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` // "bridge" or "macvlan" MacvlanAux string `yaml:"macvlan-aux,omitempty" json:"macvlan-aux,omitempty"` // Reserved IP Address for containerlab host connectivity } From 3c77c6eb92fdc5b0c5121debc5a970881111e927 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Tue, 5 Aug 2025 23:12:37 -0600 Subject: [PATCH 24/28] removed unused helper function --- hostnet/macvlan.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/hostnet/macvlan.go b/hostnet/macvlan.go index 1841973d02..83483cc9d6 100644 --- a/hostnet/macvlan.go +++ b/hostnet/macvlan.go @@ -408,14 +408,6 @@ func SanitizeInterfaceName(input string) string { // Helper functions -func getSubnetPrefix(subnet string) string { - parts := strings.Split(subnet, "/") - if len(parts) == 2 { - return parts[1] - } - return "24" // default -} - func parseMacvlanMode(mode string) netlink.MacvlanMode { switch mode { case "", "bridge": From 7710ed14483368d02704ada5aa0bd68b3528ab37 Mon Sep 17 00:00:00 2001 From: bird Date: Tue, 5 Aug 2025 23:21:09 -0600 Subject: [PATCH 25/28] Update hostnet/macvlan.go copilot recommended logging addition Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- hostnet/macvlan.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hostnet/macvlan.go b/hostnet/macvlan.go index 83483cc9d6..0b9d9d893c 100644 --- a/hostnet/macvlan.go +++ b/hostnet/macvlan.go @@ -215,9 +215,14 @@ func CreateHostMacvlanInterface(cfg *MacvlanConfig) error { // Create the interface via netlink if err := netlink.LinkAdd(macvlan); err != nil { - if strings.Contains(err.Error(), "numerical result") { + errMsg := err.Error() + if strings.Contains(errMsg, "numerical result") { log.Errorf("Netlink error details - this often indicates an issue with the parent interface index or mode value") log.Errorf("Parent index: %d, Mode: %d", parentLink.Attrs().Index, mode) + } else if strings.Contains(errMsg, "permission denied") { + log.Errorf("Permission denied while creating macvlan interface. Are you running as root or with sufficient privileges?") + } else if strings.Contains(errMsg, "file exists") { + log.Errorf("The macvlan interface %s already exists.", hostIfName) } return fmt.Errorf("failed to create macvlan interface: %w", err) } From 0cffdccef460e5ef87ec70433f361b1c06251205 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Tue, 5 Aug 2025 23:43:08 -0600 Subject: [PATCH 26/28] removed unnecessary err check on network reinspection, identified by copilot --- runtime/docker/docker.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 78c1ff1e28..5ef0dbe6c6 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -179,8 +179,8 @@ func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) { log.Debugf("Checking if docker network %q exists", d.mgmt.Network) netResource, err := d.Client.NetworkInspect(nctx, d.mgmt.Network, networkapi.InspectOptions{}) - var networkCreated bool + switch { case dockerC.IsErrNotFound(err): // Network doesn't exist, create it based on driver type @@ -256,11 +256,9 @@ func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) { // For macvlan networks if driver == "macvlan" { // Re-inspect to get gateway information if network was just created - if dockerC.IsErrNotFound(err) { - netResource, err = d.Client.NetworkInspect(nctx, d.mgmt.Network, networkapi.InspectOptions{}) - if err != nil { - return fmt.Errorf("failed to inspect newly created macvlan network: %w", err) - } + netResource, err = d.Client.NetworkInspect(nctx, d.mgmt.Network, networkapi.InspectOptions{}) + if err != nil { + return fmt.Errorf("failed to inspect newly created macvlan network: %w", err) } // Extract gateway IPs from IPAM config for macvlan From c6da989b6b11d936ac1ae30f9f2b25325dd72ed4 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Thu, 7 Aug 2025 11:09:00 -0600 Subject: [PATCH 27/28] linter nits --- hostnet/macvlan.go | 2 +- runtime/docker/docker.go | 6 ++---- types/types.go | 16 ++++++++-------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/hostnet/macvlan.go b/hostnet/macvlan.go index 0b9d9d893c..c8f1a165d6 100644 --- a/hostnet/macvlan.go +++ b/hostnet/macvlan.go @@ -96,7 +96,7 @@ func PostCreateMacvlanActions(cfg *MacvlanConfig) error { // parseAuxAddress extracts IP and subnet from aux address // Returns: IP address, subnet CIDR, error -func parseAuxAddress(auxAddr string, defaultSubnet string) (string, string, error) { +func parseAuxAddress(auxAddr, defaultSubnet string) (string, string, error) { // Check if it contains CIDR notation if strings.Contains(auxAddr, "/") { // Parse as CIDR diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index 5ef0dbe6c6..f8639013de 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -338,11 +338,9 @@ func (d *DockerRuntime) createMgmtMacvlan(nctx context.Context) error { return fmt.Errorf("invalid CIDR format for MacvlanAux: %v", err) } auxIP = ip.String() - } else { + } else if net.ParseIP(auxIP) == nil { // It's just an IP, validate it - if net.ParseIP(auxIP) == nil { - return fmt.Errorf("invalid IP address format for MacvlanAux: %s", auxIP) - } + return fmt.Errorf("invalid IP address format for MacvlanAux: %s", auxIP) } ipamCfg.AuxAddress = map[string]string{ diff --git a/types/types.go b/types/types.go index a3580d9c27..a238f70231 100644 --- a/types/types.go +++ b/types/types.go @@ -85,12 +85,12 @@ func (m *MgmtNet) UnmarshalYAML(unmarshal func(interface{}) error) error { } // process deprecated fields and use their values for new fields if new fields are not set - if len(mn.DeprecatedIPv4Subnet) > 0 && len(mn.IPv4Subnet) == 0 { + if mn.DeprecatedIPv4Subnet != "" && mn.IPv4Subnet == "" { log.Warnf("Attribute \"ipv4_subnet\" is deprecated and will be removed in the future. Change it to \"ipv4-subnet\"") mn.IPv4Subnet = mn.DeprecatedIPv4Subnet } // map old to new if old defined but new not - if len(mn.DeprecatedIPv6Subnet) > 0 && len(mn.IPv6Subnet) == 0 { + if mn.DeprecatedIPv6Subnet != "" && mn.IPv6Subnet == "" { log.Warnf("Attribute \"ipv6_subnet\" is deprecated and will be removed in the future. Change it to \"ipv6-subnet\"") mn.IPv6Subnet = mn.DeprecatedIPv6Subnet } @@ -332,10 +332,10 @@ type K8sKindExtras struct { } func (k *K8sKindExtras) Copy() *K8sKindExtras { - copy := &K8sKindExtras{ + clone := &K8sKindExtras{ Deploy: k.Deploy.Copy(), } - return copy + return clone } // K8sKindDeployExtras represents the options used for the kind cluster creation. @@ -346,8 +346,8 @@ type K8sKindDeployExtras struct { } func (k *K8sKindDeployExtras) Copy() *K8sKindDeployExtras { - copy := *k - return © + clone := *k + return &clone } // ContainerDetails contains information that is commonly outputted to tables or graphs. @@ -407,8 +407,8 @@ func (p *GenericPortBinding) String() string { } func (p *GenericPortBinding) Copy() *GenericPortBinding { - copy := *p - return © + clone := *p + return &clone } type LabData struct { From 679d008e22df5bf70c753ff82328b11eec3e21e1 Mon Sep 17 00:00:00 2001 From: Josh Bernardini Date: Sun, 17 Aug 2025 13:27:45 -0600 Subject: [PATCH 28/28] added CreateNet caller parameter to prevent net creation when called from destroy --- cmd/destroy.go | 2 +- core/network.go | 4 ++-- runtime/docker/docker.go | 23 +++++++++++++++-------- runtime/runtime.go | 2 +- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/cmd/destroy.go b/cmd/destroy.go index 295d94769f..3f269d5a08 100644 --- a/cmd/destroy.go +++ b/cmd/destroy.go @@ -187,7 +187,7 @@ func destroyFn(_ *cobra.Command, _ []string) error { // create management network or use existing one // we call this to populate the nc.cfg.mgmt.bridge variable // which is needed for the removal of the iptables rules - if err = nc.CreateNetwork(ctx); err != nil { + if err = nc.CreateNetwork(ctx, "destroy"); err != nil { return err } diff --git a/core/network.go b/core/network.go index 7abd959fa2..f16eac02fe 100644 --- a/core/network.go +++ b/core/network.go @@ -6,9 +6,9 @@ import ( "github.com/srl-labs/containerlab/labels" ) -func (c *CLab) CreateNetwork(ctx context.Context) error { +func (c *CLab) CreateNetwork(ctx context.Context, caller ...string) error { // create docker network or use existing one - if err := c.globalRuntime().CreateNet(ctx); err != nil { + if err := c.globalRuntime().CreateNet(ctx, caller...); err != nil { return err } diff --git a/runtime/docker/docker.go b/runtime/docker/docker.go index f8639013de..6443f438ab 100644 --- a/runtime/docker/docker.go +++ b/runtime/docker/docker.go @@ -164,10 +164,18 @@ func (d *DockerRuntime) WithMgmtNet(n *types.MgmtNet) { } // CreateNet creates a docker network or reusing if it exists. -func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) { +func (d *DockerRuntime) CreateNet(ctx context.Context, caller ...string) (err error) { nctx, cancel := context.WithTimeout(ctx, d.config.Timeout) defer cancel() + // Determine the caller context (default to "create" if not specified) + var callerContext string + if len(caller) > 0 { + callerContext = caller[0] + } else { + callerContext = "create" + } + // Determine the driver to use (default to bridge if not specified) driver := d.mgmt.Driver if driver == "" { @@ -179,8 +187,7 @@ func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) { log.Debugf("Checking if docker network %q exists", d.mgmt.Network) netResource, err := d.Client.NetworkInspect(nctx, d.mgmt.Network, networkapi.InspectOptions{}) - var networkCreated bool - + switch { case dockerC.IsErrNotFound(err): // Network doesn't exist, create it based on driver type @@ -192,7 +199,10 @@ func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) { } case "macvlan": - networkCreated = true + if callerContext == "destroy" { + log.Debugf("Network %q not found, but skipping creation since called from destroy operation", d.mgmt.Network) + return nil + } err = d.createMgmtMacvlan(nctx) if err != nil { return err @@ -206,7 +216,6 @@ func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) { case err == nil: // Network exists, validate it matches expected driver - networkCreated = false log.Debugf("network %q was found. Reusing it...", d.mgmt.Network) if netResource.Driver != driver { return fmt.Errorf("existing network %q has driver %q but configuration specifies %q", @@ -274,9 +283,7 @@ func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) { } log.Debugf("Docker macvlan network %q created/reused with parent interface %q", d.mgmt.Network, d.mgmt.MacvlanParent) - if networkCreated { - return d.postCreateMacvlanActions() - } + return d.postCreateMacvlanActions() } return nil diff --git a/runtime/runtime.go b/runtime/runtime.go index 32d9063d58..809888851e 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -27,7 +27,7 @@ type ContainerRuntime interface { // Instructs the runtime not to delete the mgmt network on destroy WithKeepMgmtNet() // Create container (bridge) network - CreateNet(context.Context) error + CreateNet(context.Context, ...string) error // Delete container (bridge) network DeleteNet(context.Context) error // Pull container image if not present