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/hostnet/macvlan.go b/hostnet/macvlan.go new file mode 100644 index 0000000000..c8f1a165d6 --- /dev/null +++ b/hostnet/macvlan.go @@ -0,0 +1,430 @@ +package hostnet +import ( + "fmt" + osexec "os/exec" + "strings" + "net" + "regexp" + + "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 // Can be IP or IP/CIDR + IPv4Subnet string // The main macvlan network subnet +} + +// 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 != "" { + // 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 %shost 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 +} + +// parseAuxAddress extracts IP and subnet from aux address +// Returns: IP address, subnet CIDR, error +func parseAuxAddress(auxAddr, 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) + 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" + hostIfName := SanitizeInterfaceName(hostIfNameNonAlpha) + + // 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 { + 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() == auxIP { + log.Debugf("Interface %s already has IP %s", hostIfName, auxIP) + 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 { + 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) + } + + // 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 (always use /32 for the interface itself) + addrStr := auxIP + "/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) + } + + 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" + hostIfName := SanitizeInterfaceName(hostIfNameNonAlpha) + + // Parse aux address to get the route subnet + _, routeSubnet, err := parseAuxAddress(cfg.AuxAddress, cfg.IPv4Subnet) + if err == nil { + _, ipnet, err := net.ParseCIDR(routeSubnet) + 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 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 a1e4414c26..6443f438ab 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" @@ -35,6 +36,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" ) @@ -149,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 @@ -162,56 +164,269 @@ 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() - // linux bridge name that is used by docker network + // 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 == "" { + 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": + 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 + } + // 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 template 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 + 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 +} + +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 + 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) - return d.postCreateNetActions() + // 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 != "" { + // 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 if net.ParseIP(auxIP) == nil { + // It's just an IP, validate it + return fmt.Errorf("invalid IP address format for MacvlanAux: %s", auxIP) + } + + ipamCfg.AuxAddress = map[string]string{ + "host": auxIP, + } + } + 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) + + return nil } // skipcq: GO-R1005 @@ -362,7 +577,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 } @@ -372,6 +587,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.Driver == "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 +625,7 @@ func (d *DockerRuntime) postCreateNetActions() (err error) { return nil } -// DeleteNet deletes a docker bridge. +// 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,19 +649,41 @@ func (d *DockerRuntime) DeleteNet(ctx context.Context) (err error) { } return nil } + + // For macvlan networks, cleanup host interface first + if d.mgmt.Driver == "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.Driver != "macvlan" { + err = d.deleteMgmtNetworkFwdRule() + if err != nil { + log.Warnf("errors during iptables rules removal: %v", err) + } } 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) 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 diff --git a/schemas/clab.schema.json b/schemas/clab.schema.json index 69e1aa1983..c7186d478d 100644 --- a/schemas/clab.schema.json +++ b/schemas/clab.schema.json @@ -1291,6 +1291,24 @@ "description": "MTU for the custom network", "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": { + "type": "string", + "description": "Parent interface for macvlan network" + }, + "macvlan-mode": { + "type": "string", + "enum": ["bridge", "vepa", "private", "passthru"], + "description": "Macvlan mode (bridge, vepa, private, or passthru)" + }, + "macvlan-aux": { + "type": "string", + "description": "Auxiliary IP address for host macvlan interface" } }, "minProperties": 1, diff --git a/types/types.go b/types/types.go index 81f0f9a0b5..a238f70231 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 + 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 } // Interface compliance. @@ -80,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 } @@ -327,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. @@ -341,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. @@ -402,8 +407,8 @@ func (p *GenericPortBinding) String() string { } func (p *GenericPortBinding) Copy() *GenericPortBinding { - copy := *p - return © + clone := *p + return &clone } type LabData struct {