diff --git a/config/config.go b/config/config.go index ebd4ee922..31e12a9e3 100644 --- a/config/config.go +++ b/config/config.go @@ -145,7 +145,8 @@ var ( * - When flag=false and empty, uses embedded default_hook.sh as fallback */ /* Run this before getLinuxConfig() in case this is a script - * that's responsible for creating the interface */ + * that's responsible for creating the interface. + * Also captures host udev ID_NET_NAME_* properties before driver unbind. */ HookScriptBeforeIfRead = StringEnvVar("CALICOVPP_HOOK_BEFORE_IF_READ", "") /* Bash script template run just after getting config from $CALICOVPP_INTERFACE & before starting VPP */ @@ -312,6 +313,7 @@ type CalicoVppDebugConfigType struct { ServicesEnabled *bool `json:"servicesEnabled,omitempty"` GSOEnabled *bool `json:"gsoEnabled,omitempty"` SpreadTxQueuesOnWorkers *bool `json:"spreadTxQueuesOnWorkers,omitempty"` + EnableUdevNetNameRules *bool `json:"enableUdevNetNameRules,omitempty"` } func (cfg *CalicoVppDebugConfigType) String() string { @@ -329,6 +331,9 @@ func (cfg *CalicoVppDebugConfigType) Validate() (err error) { if cfg.SpreadTxQueuesOnWorkers == nil { cfg.SpreadTxQueuesOnWorkers = &False } + if cfg.EnableUdevNetNameRules == nil { + cfg.EnableUdevNetNameRules = &True + } return } diff --git a/config/default_hook.sh b/config/default_hook.sh index 7b72b6de8..9d560ad13 100644 --- a/config/default_hook.sh +++ b/config/default_hook.sh @@ -2,7 +2,9 @@ HOOK="$0" INTERFACE_NAME="$1" -chroot /host /bin/sh < /dev/null 2>&1; then @@ -83,6 +85,107 @@ remove_tweaked_network_file () { fi } +capture_udev_net_name_properties () { + echo "default_hook: Capturing udev net name properties for $INTERFACE_NAME..." + + UDEV_INFO=$(udevadm info /sys/class/net/$INTERFACE_NAME 2>/dev/null) + if [ -z "$UDEV_INFO" ]; then + echo "default_hook: Failed to get udevadm info for $INTERFACE_NAME" + return + fi + + # Extract ID_NET_NAME_* properties + ID_NET_NAME_ONBOARD=$(echo "$UDEV_INFO" | grep "ID_NET_NAME_ONBOARD=" | sed 's/.*ID_NET_NAME_ONBOARD=//') + ID_NET_NAME_SLOT=$(echo "$UDEV_INFO" | grep "ID_NET_NAME_SLOT=" | sed 's/.*ID_NET_NAME_SLOT=//') + ID_NET_NAME_PATH=$(echo "$UDEV_INFO" | grep "ID_NET_NAME_PATH=" | sed 's/.*ID_NET_NAME_PATH=//') + ID_NET_NAME_MAC=$(echo "$UDEV_INFO" | grep "ID_NET_NAME_MAC=" | sed 's/.*ID_NET_NAME_MAC=//') + + # Check if we have any properties to save + if [ -z "$ID_NET_NAME_ONBOARD" ] && [ -z "$ID_NET_NAME_SLOT" ] && [ -z "$ID_NET_NAME_PATH" ] && [ -z "$ID_NET_NAME_MAC" ]; then + echo "default_hook: No udev net name properties found for $INTERFACE_NAME" + return + fi + + # Get MAC address + MAC_ADDRESS=$(cat /sys/class/net/$INTERFACE_NAME/address 2>/dev/null) + if [ -z "$MAC_ADDRESS" ]; then + echo "default_hook: Failed to get MAC address for $INTERFACE_NAME" + return + fi + + # Save properties to temp file for later use + mkdir -p /var/run/vpp + echo "MAC_ADDRESS=$MAC_ADDRESS" > /var/run/vpp/udev_props_$INTERFACE_NAME + [ -n "$ID_NET_NAME_ONBOARD" ] && echo "ID_NET_NAME_ONBOARD=$ID_NET_NAME_ONBOARD" >> /var/run/vpp/udev_props_$INTERFACE_NAME + [ -n "$ID_NET_NAME_SLOT" ] && echo "ID_NET_NAME_SLOT=$ID_NET_NAME_SLOT" >> /var/run/vpp/udev_props_$INTERFACE_NAME + [ -n "$ID_NET_NAME_PATH" ] && echo "ID_NET_NAME_PATH=$ID_NET_NAME_PATH" >> /var/run/vpp/udev_props_$INTERFACE_NAME + [ -n "$ID_NET_NAME_MAC" ] && echo "ID_NET_NAME_MAC=$ID_NET_NAME_MAC" >> /var/run/vpp/udev_props_$INTERFACE_NAME + + echo "default_hook: Captured udev properties for $INTERFACE_NAME (MAC: $MAC_ADDRESS)" + [ -n "$ID_NET_NAME_ONBOARD" ] && echo "default_hook: ID_NET_NAME_ONBOARD=$ID_NET_NAME_ONBOARD" + [ -n "$ID_NET_NAME_SLOT" ] && echo "default_hook: ID_NET_NAME_SLOT=$ID_NET_NAME_SLOT" + [ -n "$ID_NET_NAME_PATH" ] && echo "default_hook: ID_NET_NAME_PATH=$ID_NET_NAME_PATH" + [ -n "$ID_NET_NAME_MAC" ] && echo "default_hook: ID_NET_NAME_MAC=$ID_NET_NAME_MAC" +} + +create_udev_net_name_rule () { + PROPS_FILE="/var/run/vpp/udev_props_$INTERFACE_NAME" + if [ ! -f "$PROPS_FILE" ]; then + echo "default_hook: No udev properties captured for $INTERFACE_NAME, skipping rule creation" + return + fi + + # Source the properties file + . "$PROPS_FILE" + + if [ -z "$MAC_ADDRESS" ]; then + echo "default_hook: No MAC address captured for $INTERFACE_NAME, skipping rule creation" + return + fi + + echo "default_hook: Creating udev rule for $INTERFACE_NAME with MAC $MAC_ADDRESS..." + + # Build the udev rule + RULE_FILE="/etc/udev/rules.d/99-vpp-restore-id_net_name.rules" + echo "# Re-apply ID_NET_NAME_* properties after Calico VPP creates the host-facing tap/tun netdev." > "$RULE_FILE" + printf 'ACTION=="add", SUBSYSTEM=="net", ATTR{address}=="%s"' "$MAC_ADDRESS" >> "$RULE_FILE" + + [ -n "$ID_NET_NAME_ONBOARD" ] && printf ', ENV{ID_NET_NAME_ONBOARD}:="%s"' "$ID_NET_NAME_ONBOARD" >> "$RULE_FILE" + [ -n "$ID_NET_NAME_SLOT" ] && printf ', ENV{ID_NET_NAME_SLOT}:="%s"' "$ID_NET_NAME_SLOT" >> "$RULE_FILE" + [ -n "$ID_NET_NAME_PATH" ] && printf ', ENV{ID_NET_NAME_PATH}:="%s"' "$ID_NET_NAME_PATH" >> "$RULE_FILE" + [ -n "$ID_NET_NAME_MAC" ] && printf ', ENV{ID_NET_NAME_MAC}:="%s"' "$ID_NET_NAME_MAC" >> "$RULE_FILE" + + echo "" >> "$RULE_FILE" + + echo "default_hook: Created udev rule file at $RULE_FILE" + + # Reload udev rules + udevadm control --reload-rules + + # Trigger udev for net subsystem to apply the stored ID_NET_NAME_* properties + udevadm trigger --subsystem-match=net --action=add + echo "default_hook: Triggered udev to apply the stored ID_NET_NAME_* properties" +} + +remove_udev_net_name_rule () { + RULE_FILE="/etc/udev/rules.d/99-vpp-restore-id_net_name.rules" + PROPS_FILE="/var/run/vpp/udev_props_$INTERFACE_NAME" + + if [ -f "$RULE_FILE" ]; then + echo "default_hook: Removing udev rule file $RULE_FILE..." + rm -f "$RULE_FILE" + udevadm control --reload-rules + + # Trigger udev for net subsystem to remove the stored ID_NET_NAME_* properties + udevadm trigger --subsystem-match=net --action=change + echo "default_hook: Triggered udev to remove the stored ID_NET_NAME_* properties" + fi + + if [ -f "$PROPS_FILE" ]; then + rm -f "$PROPS_FILE" + fi +} + echo "default_hook: Uplink interface name=$INTERFACE_NAME" if which systemctl > /dev/null; then echo "default_hook: using systemctl..." @@ -91,18 +194,23 @@ else exit 1 fi -if [ "$HOOK" = "BEFORE_VPP_RUN" ]; then +if [ "$HOOK" = "BEFORE_IF_READ" ]; then + capture_udev_net_name_properties +elif [ "$HOOK" = "BEFORE_VPP_RUN" ]; then fix_dns save_network_file elif [ "$HOOK" = "VPP_RUNNING" ]; then + create_udev_net_name_rule restart_network tweak_network_file elif [ "$HOOK" = "VPP_DONE_OK" ]; then undo_dns_fix + remove_udev_net_name_rule remove_tweaked_network_file restart_network elif [ "$HOOK" = "VPP_ERRORED" ]; then undo_dns_fix + remove_udev_net_name_rule remove_tweaked_network_file restart_network fi diff --git a/vpp-manager/hooks/network_manager_hook.go b/vpp-manager/hooks/network_manager_hook.go index d198f756e..7583533a3 100644 --- a/vpp-manager/hooks/network_manager_hook.go +++ b/vpp-manager/hooks/network_manager_hook.go @@ -38,6 +38,9 @@ const ( HookVppRunning HookPoint = "VPP_RUNNING" HookVppDoneOk HookPoint = "VPP_DONE_OK" HookVppErrored HookPoint = "VPP_ERRORED" + + // UdevRuleFilePath is the path to the udev rule file for restoring ID_NET_NAME_* properties + UdevRuleFilePath = "/host/etc/udev/rules.d/99-vpp-restore-id_net_name.rules" ) // SystemType represents different system configurations @@ -50,11 +53,21 @@ type SystemType struct { IsAWS bool } +// UdevNetNameProperties stores udev network naming properties for an interface +type UdevNetNameProperties struct { + MacAddress string + IDNetNameOnboard string + IDNetNameSlot string + IDNetNamePath string + IDNetNameMac string +} + // NetworkManagerHook manages network configuration during VPP lifecycle type NetworkManagerHook struct { interfaceNames []string systemType SystemType log *logrus.Logger + udevProps map[string]*UdevNetNameProperties // map[interfaceName] -> udev properties } // chrootCommand creates a command that will be executed in the host namespace @@ -153,7 +166,8 @@ func (h *NetworkManagerHook) restartService(serviceName string) error { // NewNetworkManagerHook creates a new NetworkManagerHook instance func NewNetworkManagerHook(log *logrus.Logger) *NetworkManagerHook { hook := &NetworkManagerHook{ - log: log, + log: log, + udevProps: make(map[string]*UdevNetNameProperties), } hook.detectSystem() @@ -491,6 +505,197 @@ func (h *NetworkManagerHook) removeTweakedNetworkFile(interfaceName string) erro return nil } +// captureHostUdevProps captures udev properties for all interfaces +// This must be called BEFORE VPP takes over the interfaces +func (h *NetworkManagerHook) captureHostUdevProps() error { + if !*config.GetCalicoVppDebug().EnableUdevNetNameRules { + h.log.Info("NetworkManagerHook: Skipping captureHostUdevProps (enableUdevNetNameRules=false)") + return nil + } + h.udevProps = make(map[string]*UdevNetNameProperties) + for _, interfaceName := range h.interfaceNames { + err := h.captureUdevNetNameProperties(interfaceName) + if err != nil { + h.log.Warnf("NetworkManagerHook: Failed to capture udev properties for %s: %v", interfaceName, err) + } + } + return nil +} + +// captureUdevNetNameProperties captures udev network naming properties for an interface +// This must be called BEFORE VPP takes over the interface +func (h *NetworkManagerHook) captureUdevNetNameProperties(interfaceName string) error { + h.log.Infof("NetworkManagerHook: Capturing udev net name properties for %s...", interfaceName) + + // Get udevadm info for the interface + cmd := h.chrootCommand("udevadm", "info", fmt.Sprintf("/sys/class/net/%s", interfaceName)) + output, err := cmd.Output() + if err != nil { + h.log.Warnf("NetworkManagerHook: Failed to get udevadm info for %s: %v", interfaceName, err) + return nil + } + + props := &UdevNetNameProperties{} + hasAnyProperty := false + + // Parse the udevadm output for ID_NET_NAME_* properties + extractUdevProp := func(line, key string) (string, bool) { + idx := strings.Index(line, key+"=") + if idx == -1 { + return "", false + } + return strings.TrimSpace(line[idx+len(key)+1:]), true + } + + scanner := bufio.NewScanner(strings.NewReader(string(output))) + for scanner.Scan() { + line := scanner.Text() + if value, found := extractUdevProp(line, "ID_NET_NAME_ONBOARD"); found { + props.IDNetNameOnboard = value + hasAnyProperty = true + h.log.Infof("NetworkManagerHook: Found ID_NET_NAME_ONBOARD=%s for %s", props.IDNetNameOnboard, interfaceName) + } else if value, found := extractUdevProp(line, "ID_NET_NAME_SLOT"); found { + props.IDNetNameSlot = value + hasAnyProperty = true + h.log.Infof("NetworkManagerHook: Found ID_NET_NAME_SLOT=%s for %s", props.IDNetNameSlot, interfaceName) + } else if value, found := extractUdevProp(line, "ID_NET_NAME_PATH"); found { + props.IDNetNamePath = value + hasAnyProperty = true + h.log.Infof("NetworkManagerHook: Found ID_NET_NAME_PATH=%s for %s", props.IDNetNamePath, interfaceName) + } else if value, found := extractUdevProp(line, "ID_NET_NAME_MAC"); found { + props.IDNetNameMac = value + hasAnyProperty = true + h.log.Infof("NetworkManagerHook: Found ID_NET_NAME_MAC=%s for %s", props.IDNetNameMac, interfaceName) + } + } + + if !hasAnyProperty { + h.log.Infof("NetworkManagerHook: No udev net name properties found for %s", interfaceName) + return nil + } + + // Get MAC address from the interface + cmd = h.chrootCommand("cat", fmt.Sprintf("/sys/class/net/%s/address", interfaceName)) + macOutput, err := cmd.Output() + if err != nil { + h.log.Warnf("NetworkManagerHook: Failed to get MAC address for %s: %v", interfaceName, err) + return nil + } + props.MacAddress = strings.TrimSpace(string(macOutput)) + if props.MacAddress == "" { + h.log.Warnf("NetworkManagerHook: Failed to get MAC address for %s", interfaceName) + return nil + } + h.log.Infof("NetworkManagerHook: Captured MAC address %s for %s", props.MacAddress, interfaceName) + + h.udevProps[interfaceName] = props + return nil +} + +// createUdevNetNameRules creates udev rules to restore ID_NET_NAME_* properties for all interfaces +// This must be called AFTER VPP has taken over the interfaces and created the taps +func (h *NetworkManagerHook) createUdevNetNameRules() error { + if !*config.GetCalicoVppDebug().EnableUdevNetNameRules { + h.log.Info("NetworkManagerHook: Skipping createUdevNetNameRules (enableUdevNetNameRules=false)") + return nil + } + // Build rules for all interfaces that have captured properties + var ruleBuilder strings.Builder + ruleBuilder.WriteString("# Re-apply ID_NET_NAME_* properties after Calico VPP creates the host-facing tap/tun netdev.\n") + + rulesAdded := 0 + for interfaceName, props := range h.udevProps { + if props == nil || props.MacAddress == "" { + h.log.Warnf("NetworkManagerHook: Skipping udev rule for %s - no MAC address captured", interfaceName) + continue + } + + h.log.Infof("NetworkManagerHook: Adding udev rule for %s with MAC %s", interfaceName, props.MacAddress) + + // Each interface gets its own rule line + ruleBuilder.WriteString(fmt.Sprintf("ACTION==\"add\", SUBSYSTEM==\"net\", ATTR{address}==\"%s\"", props.MacAddress)) + + if props.IDNetNameOnboard != "" { + ruleBuilder.WriteString(fmt.Sprintf(", ENV{ID_NET_NAME_ONBOARD}:=\"%s\"", props.IDNetNameOnboard)) + } + if props.IDNetNameSlot != "" { + ruleBuilder.WriteString(fmt.Sprintf(", ENV{ID_NET_NAME_SLOT}:=\"%s\"", props.IDNetNameSlot)) + } + if props.IDNetNamePath != "" { + ruleBuilder.WriteString(fmt.Sprintf(", ENV{ID_NET_NAME_PATH}:=\"%s\"", props.IDNetNamePath)) + } + if props.IDNetNameMac != "" { + ruleBuilder.WriteString(fmt.Sprintf(", ENV{ID_NET_NAME_MAC}:=\"%s\"", props.IDNetNameMac)) + } + ruleBuilder.WriteString("\n") + rulesAdded++ + } + + if rulesAdded == 0 { + h.log.Warnf("NetworkManagerHook: No udev properties to create rules for, skipping") + return nil + } + + // Write the udev rule file + err := os.WriteFile(UdevRuleFilePath, []byte(ruleBuilder.String()), 0644) + if err != nil { + return errors.Wrapf(err, "failed to write udev rule file") + } + h.log.Infof("NetworkManagerHook: Created udev rule file at %s with %d rules", UdevRuleFilePath, rulesAdded) + + // Reload udev rules + cmd := h.chrootCommand("udevadm", "control", "--reload-rules") + err = cmd.Run() + if err != nil { + return errors.Wrapf(err, "failed to reload udev rules: %v", err) + } + + // Trigger udev for net subsystem to apply the stored ID_NET_NAME_* properties + cmd = h.chrootCommand("udevadm", "trigger", "--subsystem-match=net", "--action=add") + err = cmd.Run() + if err != nil { + return errors.Wrapf(err, "failed to trigger udev: %v", err) + } + h.log.Info("NetworkManagerHook: Triggered udev to apply the stored ID_NET_NAME_* properties") + + return nil +} + +// removeUdevNetNameRules removes the udev rule file created for restoring ID_NET_NAME_* properties +func (h *NetworkManagerHook) removeUdevNetNameRules() error { + if !*config.GetCalicoVppDebug().EnableUdevNetNameRules { + h.log.Info("NetworkManagerHook: Skipping removeUdevNetNameRules (enableUdevNetNameRules=false)") + return nil + } + _, err := os.Stat(UdevRuleFilePath) + if os.IsNotExist(err) { + return nil + } + + h.log.Infof("NetworkManagerHook: Removing udev rule file %s...", UdevRuleFilePath) + err = os.Remove(UdevRuleFilePath) + if err != nil && !os.IsNotExist(err) { + return errors.Wrapf(err, "failed to remove udev rule file") + } + + // Reload udev rules after removal + cmd := h.chrootCommand("udevadm", "control", "--reload-rules") + err = cmd.Run() + if err != nil { + h.log.Warnf("NetworkManagerHook: Failed to reload udev rules: %v", err) + } + + // Trigger udev for net subsystem to remove the stored ID_NET_NAME_* properties + cmd = h.chrootCommand("udevadm", "trigger", "--subsystem-match=net", "--action=change") + err = cmd.Run() + if err != nil { + h.log.Warnf("NetworkManagerHook: Failed to trigger udev: %v", err) + } + h.log.Info("NetworkManagerHook: Triggered udev to remove stored ID_NET_NAME_* properties") + + return nil +} + // beforeVppRun handles tasks before VPP starts func (h *NetworkManagerHook) beforeVppRun() error { // Fix DNS configuration for NetworkManager @@ -512,8 +717,15 @@ func (h *NetworkManagerHook) beforeVppRun() error { // vppRunning handles tasks while VPP is running func (h *NetworkManagerHook) vppRunning() error { + // Create udev rules to restore ID_NET_NAME_* properties for all interfaces + // This must happen after VPP has created the tap/tun interfaces with the original MACs + err := h.createUdevNetNameRules() + if err != nil { + h.log.Warnf("NetworkManagerHook: Failed to create udev rules: %v", err) + } + // Restart network services - err := h.restartNetwork() + err = h.restartNetwork() if err != nil { return err } @@ -537,6 +749,12 @@ func (h *NetworkManagerHook) vppDoneOk() error { return err } + // Remove the udev rule file for ID_NET_NAME_* restoration + err = h.removeUdevNetNameRules() + if err != nil { + h.log.Warnf("NetworkManagerHook: Failed to remove udev rule file: %v", err) + } + // Remove the tweaked network file for AWS systemd-networkd for each interface for _, interfaceName := range h.interfaceNames { err = h.removeTweakedNetworkFile(interfaceName) @@ -571,7 +789,7 @@ func (h *NetworkManagerHook) Execute(hookPoint HookPoint) error { var err error switch hookPoint { case HookBeforeIfRead: - h.log.Info("NetworkManagerHook: BEFORE_IF_READ no action performed") + err = h.captureHostUdevProps() case HookBeforeVppRun: err = h.beforeVppRun() case HookVppRunning: diff --git a/vpp-manager/main.go b/vpp-manager/main.go index 5058acfac..9429defed 100644 --- a/vpp-manager/main.go +++ b/vpp-manager/main.go @@ -163,9 +163,18 @@ func main() { params := startup.NewVppManagerParams() - /* Initialize native Go NewNetworkManagerHook with empty interface names - * We will update this later once we have the actual interface names */ + /* Initialize native Go NetworkManagerHook and set interface names. + * This must be done before HookBeforeIfRead to capture udev properties + * while interfaces still have their original drivers bound. */ networkHook = hooks.NewNetworkManagerHook(log) + if len(params.UplinksSpecs) > 0 { + interfaceNames := make([]string, len(params.UplinksSpecs)) + for i, spec := range params.UplinksSpecs { + interfaceNames[i] = spec.InterfaceName + } + networkHook.SetInterfaceNames(interfaceNames) + } + networkHook.ExecuteWithUserScript(hooks.HookBeforeIfRead, config.HookScriptBeforeIfRead, params) err = utils.ClearVppManagerFiles() @@ -188,15 +197,6 @@ func main() { log.Fatalf("Error getting initial interface configuration: %s", err) } - /* Update native Go NewNetworkManagerHook with all interface names */ - if len(params.UplinksSpecs) > 0 { - interfaceNames := make([]string, len(params.UplinksSpecs)) - for i, spec := range params.UplinksSpecs { - interfaceNames[i] = spec.InterfaceName - } - networkHook.SetInterfaceNames(interfaceNames) - } - runningCond = sync.NewCond(&sync.Mutex{}) go handleSignals()