package node import ( "context" "fmt" "net" "os" "strings" "k8s.io/klog/v2" system "undecided.project/monok8s/pkg/system" ) type NetworkConfig struct { MgmtIface string MgmtAddress string MgmtGateway string DNSNameservers []string DNSSearchDomains []string } func ConfigureMgmtInterface(cfg NetworkConfig) Step { return func(ctx context.Context, nctx *NodeContext) error { if strings.TrimSpace(cfg.MgmtIface) == "" { return fmt.Errorf("mgmt interface is required") } if strings.TrimSpace(cfg.MgmtAddress) == "" { return fmt.Errorf("mgmt address is required") } ip, ipNet, err := net.ParseCIDR(strings.TrimSpace(cfg.MgmtAddress)) if err != nil { return fmt.Errorf("invalid mgmt address %q: %w", cfg.MgmtAddress, err) } ip4 := ip.To4() if ip4 == nil { return fmt.Errorf("mgmt address must be IPv4: %q", cfg.MgmtAddress) } wantIP := ip4.String() wantCIDR := fmt.Sprintf("%s/%d", wantIP, maskSize(ipNet.Mask)) if gw := strings.TrimSpace(cfg.MgmtGateway); gw != "" { gwIP := net.ParseIP(gw) if gwIP == nil || gwIP.To4() == nil { return fmt.Errorf("invalid mgmt gateway %q", gw) } } if _, err := nctx.System.Run(ctx, "ip", "link", "show", "dev", cfg.MgmtIface); err != nil { return fmt.Errorf("interface not found: %s: %w", cfg.MgmtIface, err) } if _, err := nctx.System.Run(ctx, "ip", "link", "set", "dev", cfg.MgmtIface, "up"); err != nil { return fmt.Errorf("failed to bring up interface %s: %w", cfg.MgmtIface, err) } hasAddr, err := interfaceHasIPv4(ctx, nctx, cfg.MgmtIface, wantIP) if err != nil { return fmt.Errorf("failed checking existing address on %s: %w", cfg.MgmtIface, err) } if hasAddr { klog.Infof("address already present on %s: %s", cfg.MgmtIface, wantCIDR) } else { if _, err := nctx.System.Run(ctx, "ip", "addr", "add", wantCIDR, "dev", cfg.MgmtIface); err != nil { return fmt.Errorf("failed assigning %s to %s: %w", wantCIDR, cfg.MgmtIface, err) } } if gw := strings.TrimSpace(cfg.MgmtGateway); gw != "" { if _, err := nctx.System.Run(ctx, "ip", "route", "replace", "default", "via", gw, "dev", cfg.MgmtIface); err != nil { return fmt.Errorf("failed setting default route via %s dev %s: %w", gw, cfg.MgmtIface, err) } } return nil } } func maskSize(m net.IPMask) int { ones, _ := m.Size() return ones } func EnsureIPForward(ctx context.Context, n *NodeContext) error { return system.EnsureSysctl(ctx, n.System, "net.ipv4.ip_forward", "1") } func ConfigureDNS(cfg NetworkConfig) Step { return func(context.Context, *NodeContext) error { if len(cfg.DNSNameservers) == 0 { return nil } var nameservers []string for _, ns := range cfg.DNSNameservers { ns = strings.TrimSpace(ns) if ns == "" { continue } if ip := net.ParseIP(ns); ip == nil { return fmt.Errorf("invalid DNS nameserver %q", ns) } nameservers = append(nameservers, ns) } if len(nameservers) == 0 { return fmt.Errorf("DNSNameservers is set but no valid nameservers were parsed") } var searchDomains []string for _, d := range cfg.DNSSearchDomains { d = strings.TrimSpace(d) if d == "" { continue } searchDomains = append(searchDomains, d) } var b strings.Builder if len(searchDomains) > 0 { b.WriteString("search ") b.WriteString(strings.Join(searchDomains, " ")) b.WriteByte('\n') } for _, ns := range nameservers { b.WriteString("nameserver ") b.WriteString(ns) b.WriteByte('\n') } b.WriteString("options timeout:2 attempts:3\n") const ( resolvDir = "/etc" tmpPath = "/etc/resolv.conf.monok8s.tmp" resolvPath = "/etc/resolv.conf" ) if err := os.MkdirAll(resolvDir, 0o755); err != nil { klog.Warningf("failed to create %s for DNS config: %v; leaving %s unchanged", resolvDir, err, resolvPath) return nil } if err := os.WriteFile(tmpPath, []byte(b.String()), 0o644); err != nil { klog.Warningf("failed to write temporary DNS config %s: %v; leaving %s unchanged", tmpPath, err, resolvPath) return nil } if err := os.Rename(tmpPath, resolvPath); err != nil { _ = os.Remove(tmpPath) klog.Warningf("failed to install DNS config at %s: %v; leaving existing DNS config unchanged", resolvPath, err) return nil } return nil } } func interfaceHasIPv4(ctx context.Context, nctx *NodeContext, iface, wantIP string) (bool, error) { res, err := nctx.System.Run(ctx, "ip", "-o", "-4", "addr", "show", "dev", iface) if err != nil { return false, err } for _, line := range strings.Split(res.Stdout, "\n") { fields := strings.Fields(strings.TrimSpace(line)) for i := 0; i < len(fields)-1; i++ { if fields[i] != "inet" { continue } ip, _, err := net.ParseCIDR(fields[i+1]) if err == nil && ip.String() == wantIP { return true, nil } } } return false, nil }