package cloudinit import ( "fmt" "log/slog" "os" "os/exec" "path/filepath" "strings" ) // ApplyNetwork configures the network interface based on cloud-init config. // For static mode, it sets the IP, gateway, and DNS directly. // For DHCP mode, it runs udhcpc on the target interface. func ApplyNetwork(cfg *Config) error { iface := cfg.Network.Interface if iface == "" { var err error iface, err = detectPrimaryInterface() if err != nil { return fmt.Errorf("detecting primary interface: %w", err) } } slog.Info("configuring network", "interface", iface, "mode", cfg.Network.Mode) // Bring up the interface if err := run("ip", "link", "set", iface, "up"); err != nil { return fmt.Errorf("bringing up %s: %w", iface, err) } switch cfg.Network.Mode { case "static": return applyStatic(iface, cfg) case "dhcp", "": return applyDHCP(iface, cfg) default: return fmt.Errorf("unknown network mode: %s", cfg.Network.Mode) } } func applyStatic(iface string, cfg *Config) error { // Set IP address if err := run("ip", "addr", "add", cfg.Network.Address, "dev", iface); err != nil { return fmt.Errorf("setting address %s on %s: %w", cfg.Network.Address, iface, err) } // Set default gateway if err := run("ip", "route", "add", "default", "via", cfg.Network.Gateway, "dev", iface); err != nil { return fmt.Errorf("setting gateway %s: %w", cfg.Network.Gateway, err) } // Write DNS configuration if len(cfg.Network.DNS) > 0 { if err := writeDNS(cfg.Network.DNS); err != nil { return fmt.Errorf("writing DNS config: %w", err) } } slog.Info("static network configured", "interface", iface, "address", cfg.Network.Address, "gateway", cfg.Network.Gateway, ) return nil } func applyDHCP(iface string, cfg *Config) error { // Try udhcpc (BusyBox), then dhcpcd if path, err := exec.LookPath("udhcpc"); err == nil { args := []string{"-i", iface, "-s", "/usr/share/udhcpc/default.script", "-t", "10", "-T", "3", "-A", "5", "-b", "-q"} if err := run(path, args...); err != nil { return fmt.Errorf("udhcpc on %s: %w", iface, err) } } else if path, err := exec.LookPath("dhcpcd"); err == nil { if err := run(path, iface); err != nil { return fmt.Errorf("dhcpcd on %s: %w", iface, err) } } else { return fmt.Errorf("no DHCP client available (need udhcpc or dhcpcd)") } // Override DNS if specified in config if len(cfg.Network.DNS) > 0 { if err := writeDNS(cfg.Network.DNS); err != nil { return fmt.Errorf("writing DNS config: %w", err) } } slog.Info("DHCP network configured", "interface", iface) return nil } // SaveNetworkConfig writes a shell script that restores the current network // config on subsequent boots (before cloud-init runs). func SaveNetworkConfig(cfg *Config, destDir string) error { iface := cfg.Network.Interface if iface == "" { var err error iface, err = detectPrimaryInterface() if err != nil { return err } } if err := os.MkdirAll(destDir, 0o755); err != nil { return fmt.Errorf("creating network config dir: %w", err) } dest := filepath.Join(destDir, "interfaces.sh") var sb strings.Builder sb.WriteString("#!/bin/sh\n") sb.WriteString("# Auto-generated by KubeSolo OS cloud-init\n") sb.WriteString(fmt.Sprintf("ip link set %s up\n", iface)) switch cfg.Network.Mode { case "static": sb.WriteString(fmt.Sprintf("ip addr add %s dev %s\n", cfg.Network.Address, iface)) sb.WriteString(fmt.Sprintf("ip route add default via %s dev %s\n", cfg.Network.Gateway, iface)) if len(cfg.Network.DNS) > 0 { sb.WriteString(": > /etc/resolv.conf\n") for _, ns := range cfg.Network.DNS { sb.WriteString(fmt.Sprintf("echo 'nameserver %s' >> /etc/resolv.conf\n", ns)) } } case "dhcp", "": sb.WriteString("udhcpc -i " + iface + " -s /usr/share/udhcpc/default.script -t 10 -T 3 -A 5 -b -q 2>/dev/null\n") } if err := os.WriteFile(dest, []byte(sb.String()), 0o755); err != nil { return fmt.Errorf("writing network config: %w", err) } slog.Info("network config saved", "path", dest) return nil } func writeDNS(servers []string) error { var sb strings.Builder for _, ns := range servers { sb.WriteString("nameserver " + ns + "\n") } return os.WriteFile("/etc/resolv.conf", []byte(sb.String()), 0o644) } func detectPrimaryInterface() (string, error) { entries, err := os.ReadDir("/sys/class/net") if err != nil { return "", fmt.Errorf("reading /sys/class/net: %w", err) } for _, e := range entries { name := e.Name() switch { case name == "lo", strings.HasPrefix(name, "docker"), strings.HasPrefix(name, "veth"), strings.HasPrefix(name, "br"), strings.HasPrefix(name, "cni"), strings.HasPrefix(name, "flannel"), strings.HasPrefix(name, "cali"): continue } return name, nil } return "", fmt.Errorf("no suitable network interface found") } func run(name string, args ...string) error { cmd := exec.Command(name, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() }