package nmlite import ( "bytes" "fmt" "html/template" "net" "os" "strings" "github.com/jetkvm/kvm/internal/network/types" "github.com/rs/zerolog" ) const ( resolvConfPath = "/etc/resolv.conf" resolvConfFileMode = 0644 resolvConfTemplate = `# the resolv.conf file is managed by the jetkvm network manager # DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN {{ if .searchList }} search {{ join .searchList " " }} # {{ .iface }} {{- end -}} {{ if .domain }} domain {{ .domain }} # {{ .iface }} {{- end -}} {{ range .nameservers }} nameserver {{ printf "%s" . }} # {{ $.iface }} {{- end }} ` ) var ( tplFuncMap = template.FuncMap{ "join": strings.Join, } ) // ResolvConfManager manages the resolv.conf file type ResolvConfManager struct { logger *zerolog.Logger } // NewResolvConfManager creates a new resolv.conf manager func NewResolvConfManager(logger *zerolog.Logger) *ResolvConfManager { if logger == nil { // Create a no-op logger if none provided logger = &zerolog.Logger{} } return &ResolvConfManager{ logger: logger, } } // UpdateFromLease updates resolv.conf from a DHCP lease func (rcm *ResolvConfManager) UpdateFromLease(lease *types.DHCPLease) error { if lease == nil { return fmt.Errorf("lease cannot be nil") } rcm.logger.Info(). Str("interface", lease.InterfaceName). Msg("updating resolv.conf from DHCP lease") return rcm.Update(lease.InterfaceName, lease.DNS, lease.SearchList, lease.Domain) } // UpdateFromStaticConfig updates resolv.conf from static configuration func (rcm *ResolvConfManager) UpdateFromStaticConfig(iface string, dns []string) error { if len(dns) == 0 { rcm.logger.Debug().Str("interface", iface).Msg("no DNS servers in static config") return nil } // Parse DNS servers var dnsIPs []net.IP for _, dnsStr := range dns { dnsIP := net.ParseIP(dnsStr) if dnsIP == nil { rcm.logger.Warn().Str("dns", dnsStr).Msg("invalid DNS server, skipping") continue } dnsIPs = append(dnsIPs, dnsIP) } if len(dnsIPs) == 0 { rcm.logger.Debug().Str("interface", iface).Msg("no valid DNS servers in static config") return nil } rcm.logger.Info(). Str("interface", iface). Interface("dns", dnsIPs). Msg("updating resolv.conf from static config") return rcm.Update(iface, dnsIPs, nil, "") } // Update updates the resolv.conf file func (rcm *ResolvConfManager) Update(iface string, nameservers []net.IP, searchList []string, domain string) error { rcm.logger.Debug(). Str("interface", iface). Interface("nameservers", nameservers). Interface("searchList", searchList). Str("domain", domain). Msg("updating resolv.conf") // Generate resolv.conf content content, err := rcm.generateResolvConf(iface, nameservers, searchList, domain) if err != nil { return fmt.Errorf("failed to generate resolv.conf: %w", err) } // Write to file if err := os.WriteFile(resolvConfPath, content, resolvConfFileMode); err != nil { return fmt.Errorf("failed to write resolv.conf: %w", err) } rcm.logger.Info(). Str("interface", iface). Int("nameservers", len(nameservers)). Msg("resolv.conf updated successfully") return nil } // generateResolvConf generates resolv.conf content func (rcm *ResolvConfManager) generateResolvConf(iface string, nameservers []net.IP, searchList []string, domain string) ([]byte, error) { tmpl, err := template.New("resolv.conf").Funcs(tplFuncMap).Parse(resolvConfTemplate) if err != nil { return nil, fmt.Errorf("failed to parse template: %w", err) } var buf bytes.Buffer if err := tmpl.Execute(&buf, map[string]interface{}{ "iface": iface, "nameservers": nameservers, "searchList": searchList, "domain": domain, }); err != nil { return nil, fmt.Errorf("failed to execute template: %w", err) } return buf.Bytes(), nil } // Clear clears the resolv.conf file (removes all entries) func (rcm *ResolvConfManager) Clear() error { rcm.logger.Info().Msg("clearing resolv.conf") content := []byte("# the resolv.conf file is managed by the jetkvm network manager\n# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN\n") if err := os.WriteFile(resolvConfPath, content, resolvConfFileMode); err != nil { return fmt.Errorf("failed to clear resolv.conf: %w", err) } rcm.logger.Info().Msg("resolv.conf cleared") return nil } // GetCurrentContent returns the current content of resolv.conf func (rcm *ResolvConfManager) GetCurrentContent() ([]byte, error) { return os.ReadFile(resolvConfPath) } // Backup creates a backup of the current resolv.conf func (rcm *ResolvConfManager) Backup() error { content, err := rcm.GetCurrentContent() if err != nil { return fmt.Errorf("failed to read current resolv.conf: %w", err) } backupPath := resolvConfPath + ".backup" if err := os.WriteFile(backupPath, content, resolvConfFileMode); err != nil { return fmt.Errorf("failed to create backup: %w", err) } rcm.logger.Info().Str("backup", backupPath).Msg("resolv.conf backed up") return nil } // Restore restores resolv.conf from backup func (rcm *ResolvConfManager) Restore() error { backupPath := resolvConfPath + ".backup" content, err := os.ReadFile(backupPath) if err != nil { return fmt.Errorf("failed to read backup: %w", err) } if err := os.WriteFile(resolvConfPath, content, resolvConfFileMode); err != nil { return fmt.Errorf("failed to restore resolv.conf: %w", err) } rcm.logger.Info().Str("backup", backupPath).Msg("resolv.conf restored from backup") return nil }