mirror of https://github.com/jetkvm/kvm.git
210 lines
5.2 KiB
Go
210 lines
5.2 KiB
Go
package nmlite
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"html/template"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/jetkvm/kvm/internal/network/types"
|
|
"github.com/jetkvm/kvm/internal/sync"
|
|
"github.com/jetkvm/kvm/pkg/nmlite/link"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
const (
|
|
resolvConfPath = "/etc/resolv.conf"
|
|
resolvConfFileMode = 0644
|
|
resolvConfTemplate = `# the resolv.conf file is managed by JetKVM
|
|
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
|
|
|
|
{{ if .searchList }}
|
|
search {{ join .searchList " " }}
|
|
{{- end -}}
|
|
{{ if .domain }}
|
|
domain {{ .domain }}
|
|
{{- end -}}
|
|
{{ range $ns, $comment := .nameservers }}
|
|
nameserver {{ printf "%s" $ns }} # {{ join $comment ", " }}
|
|
{{- end }}
|
|
`
|
|
)
|
|
|
|
var (
|
|
tplFuncMap = template.FuncMap{
|
|
"join": strings.Join,
|
|
}
|
|
)
|
|
|
|
// ResolvConfManager manages the resolv.conf file
|
|
type ResolvConfManager struct {
|
|
logger *zerolog.Logger
|
|
mu sync.Mutex
|
|
conf *types.ResolvConf
|
|
|
|
hostname string
|
|
domain string
|
|
}
|
|
|
|
// 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,
|
|
mu: sync.Mutex{},
|
|
conf: &types.ResolvConf{
|
|
ConfigIPv4: make(map[string]types.InterfaceResolvConf),
|
|
ConfigIPv6: make(map[string]types.InterfaceResolvConf),
|
|
},
|
|
}
|
|
}
|
|
|
|
// SetInterfaceConfig sets the resolv.conf configuration for a specific interface
|
|
func (rcm *ResolvConfManager) SetInterfaceConfig(iface string, family int, config types.InterfaceResolvConf) error {
|
|
// DO NOT USE defer HERE, rcm.update() also locks the mutex
|
|
rcm.mu.Lock()
|
|
switch family {
|
|
case link.AfInet:
|
|
rcm.conf.ConfigIPv4[iface] = config
|
|
case link.AfInet6:
|
|
rcm.conf.ConfigIPv6[iface] = config
|
|
default:
|
|
rcm.mu.Unlock()
|
|
return fmt.Errorf("invalid family: %d", family)
|
|
}
|
|
rcm.mu.Unlock()
|
|
|
|
if err := rcm.reconcileHostname(); err != nil {
|
|
return fmt.Errorf("failed to reconcile hostname: %w", err)
|
|
}
|
|
|
|
return rcm.update()
|
|
}
|
|
|
|
// SetConfig sets the resolv.conf configuration
|
|
func (rcm *ResolvConfManager) SetConfig(resolvConf *types.ResolvConf) error {
|
|
if resolvConf == nil {
|
|
return fmt.Errorf("resolvConf cannot be nil")
|
|
}
|
|
|
|
rcm.mu.Lock()
|
|
rcm.conf = resolvConf
|
|
defer rcm.mu.Unlock()
|
|
|
|
return rcm.update()
|
|
}
|
|
|
|
// Reconcile reconciles the resolv.conf configuration
|
|
func (rcm *ResolvConfManager) Reconcile() error {
|
|
if err := rcm.reconcileHostname(); err != nil {
|
|
return fmt.Errorf("failed to reconcile hostname: %w", err)
|
|
}
|
|
return rcm.update()
|
|
}
|
|
|
|
// Update updates the resolv.conf file
|
|
func (rcm *ResolvConfManager) update() error {
|
|
rcm.mu.Lock()
|
|
defer rcm.mu.Unlock()
|
|
|
|
rcm.logger.Debug().Msg("updating resolv.conf")
|
|
|
|
// Generate resolv.conf content
|
|
content, err := rcm.generateResolvConf(rcm.conf)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate resolv.conf: %w", err)
|
|
}
|
|
|
|
// Check if the file is the same
|
|
if _, err := os.Stat(resolvConfPath); err == nil {
|
|
existingContent, err := os.ReadFile(resolvConfPath)
|
|
if err != nil {
|
|
rcm.logger.Warn().Err(err).Msg("failed to read existing resolv.conf")
|
|
}
|
|
|
|
if bytes.Equal(existingContent, content) {
|
|
rcm.logger.Debug().Msg("resolv.conf is the same, skipping write")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// 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().
|
|
Interface("config", rcm.conf).
|
|
Msg("resolv.conf updated successfully")
|
|
|
|
return nil
|
|
}
|
|
|
|
type configMap map[string][]string
|
|
|
|
func mergeConfig(nameservers *configMap, searchList *configMap, config *types.InterfaceResolvConfMap) {
|
|
localNameservers := *nameservers
|
|
localSearchList := *searchList
|
|
|
|
for ifname, iface := range *config {
|
|
comment := ifname
|
|
if iface.Source != "" {
|
|
comment += fmt.Sprintf(" (%s)", iface.Source)
|
|
}
|
|
|
|
for _, ip := range iface.NameServers {
|
|
ns := ip.String()
|
|
if _, ok := localNameservers[ns]; !ok {
|
|
localNameservers[ns] = []string{}
|
|
}
|
|
localNameservers[ns] = append(localNameservers[ns], comment)
|
|
}
|
|
|
|
for _, search := range iface.SearchList {
|
|
search = strings.Trim(search, ".")
|
|
if _, ok := localSearchList[search]; !ok {
|
|
localSearchList[search] = []string{}
|
|
}
|
|
localSearchList[search] = append(localSearchList[search], comment)
|
|
}
|
|
}
|
|
|
|
*nameservers = localNameservers
|
|
*searchList = localSearchList
|
|
}
|
|
|
|
// generateResolvConf generates resolv.conf content
|
|
func (rcm *ResolvConfManager) generateResolvConf(conf *types.ResolvConf) ([]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)
|
|
}
|
|
|
|
// merge the nameservers and searchList
|
|
nameservers := configMap{}
|
|
searchList := configMap{}
|
|
|
|
mergeConfig(&nameservers, &searchList, &conf.ConfigIPv4)
|
|
mergeConfig(&nameservers, &searchList, &conf.ConfigIPv6)
|
|
|
|
flattenedSearchList := []string{}
|
|
for search := range searchList {
|
|
flattenedSearchList = append(flattenedSearchList, search)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, map[string]any{
|
|
"nameservers": nameservers,
|
|
"searchList": flattenedSearchList,
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("failed to execute template: %w", err)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|