github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/k8s_config.go (about)

     1  package client
     2  
     3  import (
     4  	"context"
     5  	"encoding/csv"
     6  	"encoding/json"
     7  	"fmt"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/spf13/pflag"
    12  	"gopkg.in/yaml.v3"
    13  	"k8s.io/apimachinery/pkg/api/meta"
    14  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  	"k8s.io/apimachinery/pkg/runtime"
    16  	"k8s.io/cli-runtime/pkg/genericclioptions"
    17  	"k8s.io/client-go/discovery"
    18  	"k8s.io/client-go/discovery/cached/memory"
    19  	_ "k8s.io/client-go/plugin/pkg/client/auth" // Important for various cloud provider auth
    20  	"k8s.io/client-go/rest"
    21  	"k8s.io/client-go/restmapper"
    22  	"k8s.io/client-go/tools/clientcmd"
    23  	"k8s.io/client-go/tools/clientcmd/api"
    24  
    25  	"github.com/datawire/dlib/dlog"
    26  	"github.com/telepresenceio/telepresence/rpc/v2/connector"
    27  	rpc "github.com/telepresenceio/telepresence/rpc/v2/daemon"
    28  	"github.com/telepresenceio/telepresence/v2/pkg/errcat"
    29  	"github.com/telepresenceio/telepresence/v2/pkg/iputil"
    30  	"github.com/telepresenceio/telepresence/v2/pkg/maps"
    31  	"github.com/telepresenceio/telepresence/v2/pkg/proc"
    32  )
    33  
    34  // DNSMapping contains a hostname and its associated alias. When requesting the name, the intended behavior is
    35  // to resolve the alias instead.
    36  type DNSMapping struct {
    37  	Name     string `json:"name,omitempty" yaml:"name,omitempty"`
    38  	AliasFor string `json:"aliasFor,omitempty" yaml:"aliasFor,omitempty"`
    39  }
    40  
    41  type DNSMappings []*DNSMapping
    42  
    43  func (d *DNSMappings) FromRPC(rpcMappings []*rpc.DNSMapping) {
    44  	*d = make(DNSMappings, 0, len(rpcMappings))
    45  	for i := range rpcMappings {
    46  		*d = append(*d, &DNSMapping{
    47  			Name:     rpcMappings[i].Name,
    48  			AliasFor: rpcMappings[i].AliasFor,
    49  		})
    50  	}
    51  }
    52  
    53  func (d DNSMappings) ToRPC() []*rpc.DNSMapping {
    54  	rpcMappings := make([]*rpc.DNSMapping, 0, len(d))
    55  	for i := range d {
    56  		rpcMappings = append(rpcMappings, &rpc.DNSMapping{
    57  			Name:     d[i].Name,
    58  			AliasFor: d[i].AliasFor,
    59  		})
    60  	}
    61  	return rpcMappings
    62  }
    63  
    64  // The DnsConfig is part of the KubeconfigExtension struct.
    65  type DnsConfig struct {
    66  	// LocalIP is the address of the local DNS server. This entry is only
    67  	// used on Linux system that are not configured to use systemd-resolved and
    68  	// can be overridden by using the option --dns on the command line and defaults
    69  	// to the first line of /etc/resolv.conf
    70  	LocalIP iputil.IPKey `json:"local-ip,omitempty"`
    71  
    72  	// RemoteIP is the address of the cluster's DNS service. It will default
    73  	// to the IP of the kube-dns.kube-system or the dns-default.openshift-dns service.
    74  	RemoteIP iputil.IPKey `json:"remote-ip,omitempty"`
    75  
    76  	// ExcludeSuffixes are suffixes for which the DNS resolver will always return
    77  	// NXDOMAIN (or fallback in case of the overriding resolver).
    78  	ExcludeSuffixes []string `json:"exclude-suffixes,omitempty"`
    79  
    80  	// IncludeSuffixes are suffixes for which the DNS resolver will always attempt to do
    81  	// a lookup. Includes have higher priority than excludes.
    82  	IncludeSuffixes []string `json:"include-suffixes,omitempty"`
    83  
    84  	// Excludes are a list of hostname that the DNS resolver will not resolve even if they exist.
    85  	Excludes []string `json:"excludes,omitempty"`
    86  
    87  	// Mappings contains a list of DNS Mappings. Each item references a hostname, and an associated alias. If a
    88  	// request is made for the name, the alias will be resolved instead.
    89  	Mappings DNSMappings `json:"mappings,omitempty"`
    90  
    91  	// The maximum time to wait for a cluster side host lookup.
    92  	LookupTimeout v1.Duration `json:"lookup-timeout,omitempty"`
    93  }
    94  
    95  // The ManagerConfig is part of the KubeconfigExtension struct. It configures discovery of the traffic manager.
    96  type ManagerConfig struct {
    97  	// Namespace is the name of the namespace where the traffic manager is to be found
    98  	Namespace string `json:"namespace,omitempty"`
    99  }
   100  
   101  // KubeconfigExtension is an extension read from the selected kubeconfig Cluster.
   102  type KubeconfigExtension struct {
   103  	DNS                     *DnsConfig       `json:"dns,omitempty"`
   104  	AlsoProxy               []*iputil.Subnet `json:"also-proxy,omitempty"`
   105  	NeverProxy              []*iputil.Subnet `json:"never-proxy,omitempty"`
   106  	AllowConflictingSubnets []*iputil.Subnet `json:"allow-conflicting-subnets,omitempty"`
   107  	Manager                 *ManagerConfig   `json:"manager,omitempty"`
   108  }
   109  
   110  // Kubeconfig implements genericclioptions.RESTClientGetter, but is using the RestConfig
   111  // instead of the ConfigFlags (which also implements that interface) since the latter
   112  // will assume that the kubeconfig is loaded from disk.
   113  type Kubeconfig struct {
   114  	KubeconfigExtension
   115  	Namespace        string // default cluster namespace.
   116  	Context          string
   117  	Server           string
   118  	OriginalFlagMap  map[string]string
   119  	EffectiveFlagMap map[string]string
   120  	ClientConfig     clientcmd.ClientConfig
   121  	RestConfig       *rest.Config
   122  }
   123  
   124  func (kf *Kubeconfig) ToRESTConfig() (*rest.Config, error) {
   125  	return kf.RestConfig, nil
   126  }
   127  
   128  func (kf *Kubeconfig) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
   129  	discoveryClient, err := discovery.NewDiscoveryClientForConfig(kf.RestConfig)
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	return memory.NewMemCacheClient(discoveryClient), nil
   134  }
   135  
   136  func (kf *Kubeconfig) ToRESTMapper() (meta.RESTMapper, error) {
   137  	discoveryClient, err := kf.ToDiscoveryClient()
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
   142  	expander := restmapper.NewShortcutExpander(mapper, discoveryClient, func(string) {})
   143  	return expander, nil
   144  }
   145  
   146  func (kf *Kubeconfig) ToRawKubeConfigLoader() clientcmd.ClientConfig {
   147  	return kf.ClientConfig
   148  }
   149  
   150  const configExtension = "telepresence.io"
   151  
   152  func ConfigFlags(flagMap map[string]string) (*genericclioptions.ConfigFlags, error) {
   153  	configFlags := genericclioptions.NewConfigFlags(false)
   154  	flags := pflag.NewFlagSet("", 0)
   155  	configFlags.AddFlags(flags)
   156  	for k, v := range flagMap {
   157  		f := flags.Lookup(k)
   158  		if f == nil {
   159  			continue
   160  		}
   161  		var err error
   162  		if sv, ok := f.Value.(pflag.SliceValue); ok {
   163  			var vs []string
   164  			if vs, err = csv.NewReader(strings.NewReader(v)).Read(); err == nil {
   165  				err = sv.Replace(vs)
   166  			}
   167  		} else {
   168  			err = flags.Set(k, v)
   169  		}
   170  		if err != nil {
   171  			return nil, errcat.User.Newf("error processing kubectl flag --%s=%s: %w", k, v, err)
   172  		}
   173  	}
   174  	return configFlags, nil
   175  }
   176  
   177  // ConfigLoader returns the name of the current Kubernetes context, and the context itself.
   178  func ConfigLoader(ctx context.Context, flagMap map[string]string, kubeConfigData []byte) (clientcmd.ClientConfig, error) {
   179  	configFlags, err := ConfigFlags(flagMap)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	return NewClientConfig(ctx, configFlags, kubeConfigData)
   184  }
   185  
   186  // CurrentContext returns the name of the current Kubernetes context, the active namespace, and the context itself.
   187  func CurrentContext(ctx context.Context, flagMap map[string]string, configBytes []byte) (string, string, *api.Context, error) {
   188  	cld, err := ConfigLoader(ctx, flagMap, configBytes)
   189  	if err != nil {
   190  		return "", "", nil, err
   191  	}
   192  	ns, _, err := cld.Namespace()
   193  	if err != nil {
   194  		return "", "", nil, err
   195  	}
   196  
   197  	config, err := cld.RawConfig()
   198  	if err != nil {
   199  		return "", "", nil, err
   200  	}
   201  	if len(config.Contexts) == 0 {
   202  		return "", "", nil, errcat.Config.New("kubeconfig has no context definition")
   203  	}
   204  	cc := flagMap["context"]
   205  	if cc == "" {
   206  		cc = config.CurrentContext
   207  	}
   208  	return cc, ns, config.Contexts[cc], nil
   209  }
   210  
   211  func NewKubeconfig(c context.Context, flagMap map[string]string, managerNamespaceOverride string) (*Kubeconfig, error) {
   212  	configFlags, err := ConfigFlags(flagMap)
   213  	if err != nil {
   214  		return nil, err
   215  	}
   216  	return newKubeconfig(c, flagMap, flagMap, managerNamespaceOverride, configFlags, nil)
   217  }
   218  
   219  func DaemonKubeconfig(c context.Context, cr *connector.ConnectRequest) (*Kubeconfig, error) {
   220  	if cr.IsPodDaemon {
   221  		return NewInClusterConfig(c, cr.KubeFlags)
   222  	}
   223  	flagMap := cr.KubeFlags
   224  	if proc.RunningInContainer() {
   225  		// Don't trust the host's KUBECONFIG env.
   226  		delete(cr.Environment, "KUBECONFIG")
   227  
   228  		// Add potential overrides for kube flags.
   229  		if len(cr.ContainerKubeFlagOverrides) > 0 {
   230  			flagMap = maps.Copy(flagMap)
   231  			maps.Merge(flagMap, cr.ContainerKubeFlagOverrides)
   232  		}
   233  	}
   234  	for k, v := range cr.Environment {
   235  		if k[0] == '-' {
   236  			_ = os.Unsetenv(k[1:])
   237  		} else {
   238  			_ = os.Setenv(k, v)
   239  		}
   240  	}
   241  	configFlags, err := ConfigFlags(flagMap)
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  	return newKubeconfig(c, cr.KubeFlags, flagMap, cr.ManagerNamespace, configFlags, cr.KubeconfigData)
   246  }
   247  
   248  // AppendKubeFlags appends the flags in the given map to the given slice in the form of
   249  // flag arguments suitable for command execution. Flags known to be multivalued are assumed
   250  // to be in the form of comma-separated list and will be added using repeated options.
   251  func AppendKubeFlags(kubeFlags map[string]string, args []string) ([]string, error) {
   252  	for k, v := range kubeFlags {
   253  		switch k {
   254  		case "as-group":
   255  			// Multivalued
   256  			r := csv.NewReader(strings.NewReader(v))
   257  			gs, err := r.Read()
   258  			if err != nil {
   259  				return nil, err
   260  			}
   261  			for _, g := range gs {
   262  				args = append(args, "--"+k, g)
   263  			}
   264  		case "disable-compression", "insecure-skip-tls-verify":
   265  			// Boolean with false default.
   266  			if v != "false" {
   267  				args = append(args, "--"+k)
   268  			}
   269  		default:
   270  			args = append(args, "--"+k, v)
   271  		}
   272  	}
   273  	return args, nil
   274  }
   275  
   276  // flagOverrides creates overrides based on the given ConfigFlags.
   277  //
   278  // The code in this function is copied from clientcmd.config_flags.go, function toRawKubeConfigLoader
   279  // but differs in that overrides are only made for non-zero values.
   280  func flagOverrides(f *genericclioptions.ConfigFlags) *clientcmd.ConfigOverrides {
   281  	overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults}
   282  
   283  	stringVal := func(vp *string) (v string, ok bool) {
   284  		if vp != nil && *vp != "" {
   285  			v, ok = *vp, true
   286  		}
   287  		return
   288  	}
   289  
   290  	// bind auth info flag values to overrides
   291  	if v, ok := stringVal(f.CertFile); ok {
   292  		overrides.AuthInfo.ClientCertificate = v
   293  	}
   294  	if v, ok := stringVal(f.KeyFile); ok {
   295  		overrides.AuthInfo.ClientKey = v
   296  	}
   297  	if v, ok := stringVal(f.BearerToken); ok {
   298  		overrides.AuthInfo.Token = v
   299  	}
   300  	if v, ok := stringVal(f.Impersonate); ok {
   301  		overrides.AuthInfo.Impersonate = v
   302  	}
   303  	if v, ok := stringVal(f.ImpersonateUID); ok {
   304  		overrides.AuthInfo.ImpersonateUID = v
   305  	}
   306  	if f.ImpersonateGroup != nil && len(*f.ImpersonateGroup) > 0 {
   307  		overrides.AuthInfo.ImpersonateGroups = *f.ImpersonateGroup
   308  	}
   309  	if v, ok := stringVal(f.Username); ok {
   310  		overrides.AuthInfo.Username = v
   311  	}
   312  	if v, ok := stringVal(f.Password); ok {
   313  		overrides.AuthInfo.Password = v
   314  	}
   315  
   316  	// bind cluster flags
   317  	if v, ok := stringVal(f.APIServer); ok {
   318  		overrides.ClusterInfo.Server = v
   319  	}
   320  	if v, ok := stringVal(f.TLSServerName); ok {
   321  		overrides.ClusterInfo.TLSServerName = v
   322  	}
   323  	if v, ok := stringVal(f.CAFile); ok {
   324  		overrides.ClusterInfo.CertificateAuthority = v
   325  	}
   326  	if f.Insecure != nil && *f.Insecure {
   327  		overrides.ClusterInfo.InsecureSkipTLSVerify = true
   328  	}
   329  	if f.DisableCompression != nil && *f.DisableCompression {
   330  		overrides.ClusterInfo.DisableCompression = true
   331  	}
   332  
   333  	// bind context flags
   334  	if v, ok := stringVal(f.Context); ok {
   335  		overrides.CurrentContext = v
   336  	}
   337  	if v, ok := stringVal(f.ClusterName); ok {
   338  		overrides.Context.Cluster = v
   339  	}
   340  	if v, ok := stringVal(f.AuthInfoName); ok {
   341  		overrides.Context.AuthInfo = v
   342  	}
   343  	if v, ok := stringVal(f.Namespace); ok {
   344  		overrides.Context.Namespace = v
   345  	}
   346  
   347  	if v, ok := stringVal(f.Timeout); ok && v != "0" {
   348  		overrides.Timeout = v
   349  	}
   350  	return overrides
   351  }
   352  
   353  type KubeconfigGetter func() (*api.Config, error)
   354  
   355  type configGetter struct {
   356  	kubeconfigGetter KubeconfigGetter
   357  	destFile         string
   358  }
   359  
   360  func (g *configGetter) Load() (*api.Config, error) {
   361  	return g.kubeconfigGetter()
   362  }
   363  
   364  func (g *configGetter) GetLoadingPrecedence() []string {
   365  	return nil
   366  }
   367  
   368  func (g *configGetter) GetStartingConfig() (*api.Config, error) {
   369  	return g.kubeconfigGetter()
   370  }
   371  
   372  func (g *configGetter) GetDefaultFilename() string {
   373  	if g.destFile == "" {
   374  		destFile, err := os.CreateTemp("", "kc-*")
   375  		if err == nil {
   376  			g.destFile = destFile.Name()
   377  			_ = os.Remove(destFile.Name())
   378  			destFile.Close()
   379  		}
   380  	}
   381  	return g.destFile
   382  }
   383  
   384  func (g *configGetter) IsExplicitFile() bool {
   385  	return false
   386  }
   387  
   388  func (g *configGetter) GetExplicitFile() string {
   389  	return ""
   390  }
   391  
   392  func (g *configGetter) IsDefaultConfig(config *rest.Config) bool {
   393  	return false
   394  }
   395  
   396  // NewClientConfig creates a clientcmd.ClientConfig, by either reading the kubeconfig from the given configData or
   397  // by loading it from files as configured by the given configFlags.
   398  func NewClientConfig(ctx context.Context, configFlags *genericclioptions.ConfigFlags, configData []byte) (clientcmd.ClientConfig, error) {
   399  	if len(configData) == 0 {
   400  		return configFlags.ToRawKubeConfigLoader(), nil
   401  	}
   402  	directConfig, err := clientcmd.NewClientConfigFromBytes(configData)
   403  	if err != nil {
   404  		dlog.Errorf(ctx, "loading kubeconfig failed: %v", err)
   405  		return nil, err
   406  	}
   407  	config, err := directConfig.RawConfig()
   408  	if err != nil {
   409  		dlog.Errorf(ctx, "raw kubeconfig failed: %v", err)
   410  		return nil, err
   411  	}
   412  	overrides := flagOverrides(configFlags)
   413  	currentContext := overrides.CurrentContext
   414  	return clientcmd.NewNonInteractiveClientConfig(config, currentContext, overrides, &configGetter{
   415  		kubeconfigGetter: func() (*api.Config, error) {
   416  			return &config, nil
   417  		},
   418  	}), nil
   419  }
   420  
   421  func newKubeconfig(
   422  	ctx context.Context,
   423  	originalFlags,
   424  	effectiveFlags map[string]string,
   425  	managerNamespaceOverride string,
   426  	configFlags *genericclioptions.ConfigFlags,
   427  	configData []byte,
   428  ) (*Kubeconfig, error) {
   429  	clientConfig, err := NewClientConfig(ctx, configFlags, configData)
   430  	if err != nil {
   431  		return nil, err
   432  	}
   433  
   434  	config, err := clientConfig.RawConfig()
   435  	if err != nil {
   436  		return nil, err
   437  	}
   438  	if len(config.Contexts) == 0 {
   439  		return nil, errcat.Config.New("kubeconfig has no context definition")
   440  	}
   441  
   442  	namespace, _, err := clientConfig.Namespace()
   443  	if err != nil {
   444  		return nil, err
   445  	}
   446  
   447  	ctxName := effectiveFlags["context"]
   448  	if ctxName == "" {
   449  		ctxName = config.CurrentContext
   450  	}
   451  
   452  	kubeCtx, ok := config.Contexts[ctxName]
   453  	if !ok {
   454  		return nil, errcat.Config.Newf("context %q does not exist in the kubeconfig", ctxName)
   455  	}
   456  
   457  	cluster, ok := config.Clusters[kubeCtx.Cluster]
   458  	if !ok {
   459  		return nil, errcat.Config.Newf("the cluster %q declared in context %q does exists in the kubeconfig", kubeCtx.Cluster, ctxName)
   460  	}
   461  
   462  	restConfig, err := clientConfig.ClientConfig()
   463  	if err != nil {
   464  		return nil, err
   465  	}
   466  
   467  	dlog.Debugf(ctx, "using namespace %q", namespace)
   468  
   469  	k := &Kubeconfig{
   470  		Context:          ctxName,
   471  		Server:           cluster.Server,
   472  		Namespace:        namespace,
   473  		EffectiveFlagMap: effectiveFlags,
   474  		OriginalFlagMap:  originalFlags,
   475  		ClientConfig:     clientConfig,
   476  		RestConfig:       restConfig,
   477  	}
   478  
   479  	if ext, ok := cluster.Extensions[configExtension].(*runtime.Unknown); ok {
   480  		if err = json.Unmarshal(ext.Raw, &k.KubeconfigExtension); err != nil {
   481  			return nil, errcat.Config.Newf("unable to parse extension %s in kubeconfig: %w", configExtension, err)
   482  		}
   483  	}
   484  
   485  	if k.KubeconfigExtension.Manager == nil {
   486  		k.KubeconfigExtension.Manager = &ManagerConfig{}
   487  	}
   488  
   489  	if managerNamespaceOverride != "" {
   490  		k.KubeconfigExtension.Manager.Namespace = managerNamespaceOverride
   491  	}
   492  
   493  	if k.KubeconfigExtension.Manager.Namespace == "" {
   494  		k.KubeconfigExtension.Manager.Namespace = GetEnv(ctx).ManagerNamespace
   495  	}
   496  	if k.KubeconfigExtension.Manager.Namespace == "" {
   497  		k.KubeconfigExtension.Manager.Namespace = GetConfig(ctx).Cluster().DefaultManagerNamespace
   498  	}
   499  	return k, nil
   500  }
   501  
   502  // NewInClusterConfig represents an inClusterConfig.
   503  func NewInClusterConfig(c context.Context, flagMap map[string]string) (*Kubeconfig, error) {
   504  	configFlags := genericclioptions.NewConfigFlags(false)
   505  	flags := pflag.NewFlagSet("", 0)
   506  	configFlags.AddFlags(flags)
   507  	for k, v := range flagMap {
   508  		if err := flags.Set(k, v); err != nil {
   509  			return nil, errcat.User.Newf("error processing kubectl flag --%s=%s: %w", k, v, err)
   510  		}
   511  	}
   512  
   513  	configLoader := configFlags.ToRawKubeConfigLoader()
   514  	restConfig, err := configLoader.ClientConfig()
   515  	if err != nil {
   516  		return nil, err
   517  	}
   518  
   519  	namespace, _, err := configLoader.Namespace()
   520  	if err != nil {
   521  		return nil, err
   522  	}
   523  
   524  	managerNamespace := GetEnv(c).ManagerNamespace
   525  	if managerNamespace == "" {
   526  		managerNamespace = GetConfig(c).Cluster().DefaultManagerNamespace
   527  	}
   528  
   529  	return &Kubeconfig{
   530  		Namespace:        namespace,
   531  		Server:           restConfig.Host,
   532  		EffectiveFlagMap: flagMap,
   533  		OriginalFlagMap:  flagMap,
   534  		RestConfig:       restConfig,
   535  		ClientConfig:     configLoader,
   536  		// it may be empty, but we should avoid nil deref
   537  		KubeconfigExtension: KubeconfigExtension{
   538  			Manager: &ManagerConfig{
   539  				Namespace: managerNamespace,
   540  			},
   541  		},
   542  	}, nil
   543  }
   544  
   545  // ContextServiceAndFlagsEqual determines if this instance is equal to the given instance with respect to context,
   546  // server, and flag arguments.
   547  func (kf *Kubeconfig) ContextServiceAndFlagsEqual(okf *Kubeconfig) bool {
   548  	return kf != nil && okf != nil &&
   549  		kf.Context == okf.Context &&
   550  		kf.Server == okf.Server &&
   551  		maps.Equal(kf.EffectiveFlagMap, okf.EffectiveFlagMap)
   552  }
   553  
   554  func (kf *Kubeconfig) GetContext() string {
   555  	return kf.Context
   556  }
   557  
   558  func (kf *Kubeconfig) GetManagerNamespace() string {
   559  	return kf.KubeconfigExtension.Manager.Namespace
   560  }
   561  
   562  func (kf *Kubeconfig) GetRestConfig() *rest.Config {
   563  	return kf.RestConfig
   564  }
   565  
   566  func (kf *Kubeconfig) AddRemoteKubeConfigExtension(ctx context.Context, cfgYaml []byte) error {
   567  	remote := struct {
   568  		DNS     *DNS     `yaml:"dns,omitempty"`
   569  		Routing *Routing `yaml:"routing,omitempty"`
   570  	}{}
   571  	if err := yaml.Unmarshal(cfgYaml, &remote); err != nil {
   572  		return fmt.Errorf("unable to parse remote kubeconfig: %w", err)
   573  	}
   574  	if kf.DNS == nil {
   575  		kf.DNS = &DnsConfig{}
   576  	}
   577  	if dns := remote.DNS; dns != nil {
   578  		if kf.DNS.LocalIP == "" {
   579  			kf.DNS.LocalIP = iputil.IPKey(dns.LocalIP)
   580  			dlog.Debugf(ctx, "Applying remote dns local IP: %s", dns.LocalIP)
   581  		}
   582  		if kf.DNS.RemoteIP == "" {
   583  			kf.DNS.RemoteIP = iputil.IPKey(dns.RemoteIP)
   584  			dlog.Debugf(ctx, "Applying remote dns remote IP: %s", dns.RemoteIP)
   585  		}
   586  		if len(dns.ExcludeSuffixes) > 0 {
   587  			dlog.Debugf(ctx, "Applying remote excludeSuffixes: %v", dns.ExcludeSuffixes)
   588  			kf.DNS.ExcludeSuffixes = append(kf.DNS.ExcludeSuffixes, dns.ExcludeSuffixes...)
   589  		}
   590  		if len(dns.IncludeSuffixes) > 0 {
   591  			dlog.Debugf(ctx, "Applying remote includeSuffixes: %v", dns.IncludeSuffixes)
   592  			kf.DNS.IncludeSuffixes = append(kf.DNS.IncludeSuffixes, dns.IncludeSuffixes...)
   593  		}
   594  		if len(dns.Excludes) > 0 {
   595  			dlog.Debugf(ctx, "Applying remote excludes: %v", dns.Excludes)
   596  			kf.DNS.Excludes = append(kf.DNS.Excludes, dns.Excludes...)
   597  		}
   598  		if len(dns.Mappings) > 0 {
   599  			for _, m := range dns.Mappings {
   600  				dlog.Debugf(ctx, "Applying remote mapping: Name: %s, AliasFor %s", m.Name, m.AliasFor)
   601  			}
   602  			kf.DNS.Mappings = append(kf.DNS.Mappings, dns.Mappings...)
   603  		}
   604  
   605  		if kf.DNS.LookupTimeout.Duration == 0 {
   606  			dlog.Debugf(ctx, "Applying remote lookupTimeout: %s", dns.LookupTimeout)
   607  			kf.DNS.LookupTimeout.Duration = dns.LookupTimeout
   608  		}
   609  	}
   610  	if routing := remote.Routing; routing != nil {
   611  		if len(routing.AlsoProxy) > 0 {
   612  			dlog.Debugf(ctx, "Applying remote alsoProxy: %v", routing.AlsoProxy)
   613  			kf.AlsoProxy = append(kf.AlsoProxy, routing.AlsoProxy...)
   614  		}
   615  		if len(routing.NeverProxy) > 0 {
   616  			dlog.Debugf(ctx, "Applying remote neverProxy: %v", routing.NeverProxy)
   617  			kf.NeverProxy = append(kf.NeverProxy, routing.NeverProxy...)
   618  		}
   619  		if len(routing.AllowConflicting) > 0 {
   620  			dlog.Debugf(ctx, "Applying remote allowConflicting: %v", routing.AllowConflicting)
   621  			kf.AllowConflictingSubnets = append(kf.AllowConflictingSubnets, routing.AllowConflicting...)
   622  		}
   623  	}
   624  	return nil
   625  }