//go:build darwin package dns import ( "bytes" "context" "fmt" "os" "path/filepath" "slices" "strings" "time" miekgdns "github.com/miekg/dns" v12 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/tools/cache" plog "github.com/wencaiwulue/kubevpn/v2/pkg/log" "github.com/wencaiwulue/kubevpn/v2/pkg/util" ) // https://github.com/golang/go/issues/12524 // man 5 resolver var ignoreSearchSuffix = []string{"com", "io", "net", "org", "cn", "ru"} // SetupDNS support like // service:port // service.namespace:port // service.namespace.svc:port // service.namespace.svc.cluster:port // service.namespace.svc.cluster.local:port func (c *Config) SetupDNS(ctx context.Context) error { defer util.HandleCrash() ticker := time.NewTicker(time.Second * 15) _, err := c.SvcInformer.AddEventHandler(cache.FilteringResourceEventHandler{ FilterFunc: func(obj interface{}) bool { if svc, ok := obj.(*v12.Service); ok && svc.Namespace == c.Ns[0] { return true } else { return false } }, Handler: cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { ticker.Reset(time.Second * 3) }, UpdateFunc: func(oldObj, newObj interface{}) { ticker.Reset(time.Second * 3) }, DeleteFunc: func(obj interface{}) { ticker.Reset(time.Second * 3) }, }, }) if err != nil { plog.G(ctx).Errorf("Failed to add service event handler: %v", err) return err } go func() { defer ticker.Stop() for ; ctx.Err() == nil; <-ticker.C { ticker.Reset(time.Second * 15) serviceList, err := c.SvcInformer.GetIndexer().ByIndex(cache.NamespaceIndex, c.Ns[0]) if err != nil { plog.G(ctx).Errorf("Failed to list service by namespace %s: %v", c.Ns[0], err) continue } var services []v12.Service for _, service := range serviceList { svc, ok := service.(*v12.Service) if !ok { continue } services = append(services, *svc) } if len(services) == 0 { continue } if ctx.Err() != nil { return } c.Services = services c.usingResolver(ctx) } }() c.usingResolver(ctx) return nil } func (c *Config) usingResolver(ctx context.Context) { var clientConfig = c.Config path := "/etc/resolver" if _, err := os.Stat(path); os.IsNotExist(err) { if err = os.MkdirAll(path, 0755); err != nil { plog.G(ctx).Errorf("Create resolver error: %v", err) } if err = os.Chmod(path, 0755); err != nil { plog.G(ctx).Errorf("Chmod resolver error: %v", err) } } newConfig := miekgdns.ClientConfig{ Servers: clientConfig.Servers, Search: []string{}, Port: clientConfig.Port, Ndots: clientConfig.Ndots, Timeout: clientConfig.Timeout, } for _, filename := range GetResolvers(c.Config.Search, c.Ns, c.Services) { // ignore search suffix like com, io, net, org, cn, ru, those are top dns server if slices.Contains(ignoreSearchSuffix, filepath.Base(filename)) { continue } content, err := os.ReadFile(filename) if os.IsNotExist(err) { _ = os.WriteFile(filename, []byte(toString(newConfig)), 0644) continue } if err != nil { plog.G(ctx).Errorf("Failed to read resovler %s: %v", filename, err) continue } var conf *miekgdns.ClientConfig conf, err = miekgdns.ClientConfigFromReader(bytes.NewBufferString(string(content))) if err != nil { plog.G(ctx).Errorf("Failed to parse resolver %s: %v", filename, err) continue } if slices.Contains(conf.Servers, clientConfig.Servers[0]) { continue } // insert current name server to first location conf.Servers = append([]string{clientConfig.Servers[0]}, conf.Servers...) err = os.WriteFile(filename, []byte(toString(*conf)), 0644) if err != nil { plog.G(ctx).Errorf("Failed to write resovler %s: %v", filename, err) } } } func toString(config miekgdns.ClientConfig) string { var builder strings.Builder // builder.WriteString(`# //# macOS Notice //# //# This file is not consulted for DNS hostname resolution, address //# resolution, or the DNS query routing mechanism used by most //# processes on this system. //# //# To view the DNS configuration used by this system, use: //# scutil --dns //# //# SEE ALSO //# dns-sd(1), scutil(8) //# //# This file is automatically generated. //#`) // builder.WriteString("\n") if len(config.Search) > 0 { builder.WriteString(fmt.Sprintf("search %s\n", strings.Join(config.Search, " "))) } for i := range config.Servers { builder.WriteString(fmt.Sprintf("nameserver %s\n", config.Servers[i])) } if len(config.Port) != 0 { builder.WriteString(fmt.Sprintf("port %s\n", config.Port)) } builder.WriteString(fmt.Sprintf("options ndots:%d\n", config.Ndots)) builder.WriteString(fmt.Sprintf("options timeout:%d\n", config.Timeout)) //builder.WriteString(fmt.Sprintf("options attempts:%d\n", config.Attempts)) return builder.String() } func (c *Config) CancelDNS() { for _, filename := range GetResolvers(c.Config.Search, c.Ns, c.Services) { content, err := os.ReadFile(filename) if err != nil { continue } var conf *miekgdns.ClientConfig conf, err = miekgdns.ClientConfigFromReader(bytes.NewBufferString(strings.TrimSpace(string(content)))) if err != nil { continue } // if not has this DNS server, do nothing if !sets.New[string](conf.Servers...).Has(c.Config.Servers[0]) { continue } // reverse delete for i := len(conf.Servers) - 1; i >= 0; i-- { if conf.Servers[i] == c.Config.Servers[0] { conf.Servers = append(conf.Servers[:i], conf.Servers[i+1:]...) i-- // remove once is enough, because if same cluster connect to different namespace // dns service ip is same break } } if len(conf.Servers) == 0 { _ = os.Remove(filename) continue } err = os.WriteFile(filename, []byte(toString(*conf)), 0644) if err != nil { plog.G(context.Background()).Errorf("Failed to write resovler %s error: %v", filename, err) } } //networkCancel() _ = c.removeHosts(sets.New[Entry]().Insert(c.Hosts...).UnsortedList()) } // GetResolvers // service name: authors // namespace: test // create resolvers suffix: // [authors.test.svc.cluster.local] // local // cluster // cluster.local // svc.cluster // test.svc func GetResolvers(searchList []string, nsList []string, serviceName []v12.Service) []string { result := sets.New[string]().Insert(searchList...).Insert(nsList...) const splitter = "." // support default.svc, cluster.local for _, s := range searchList { split := strings.Split(s, splitter) for i := range len(split) - 1 { result.Insert(fmt.Sprintf("%s.%s", split[i], split[i+1])) } result.Insert(split...) } // authors.default for _, service := range serviceName { result.Insert(fmt.Sprintf("%s.%s", service.Name, service.Namespace)) } var resolvers []string for _, s := range sets.List(result) { resolvers = append(resolvers, filepath.Join("/etc/resolver/", s)) } return resolvers } func GetHostFile() string { return "/etc/hosts" }