google.golang.org/grpc@v1.72.2/xds/googledirectpath/googlec2p.go (about)

     1  /*
     2   *
     3   * Copyright 2021 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   */
    18  
    19  // Package googledirectpath implements a resolver that configures xds to make
    20  // cloud to prod directpath connection.
    21  //
    22  // It's a combo of DNS and xDS resolvers. It delegates to DNS if
    23  // - not on GCE, or
    24  // - xDS bootstrap env var is set (so this client needs to do normal xDS, not
    25  // direct path, and clients with this scheme is not part of the xDS mesh).
    26  package googledirectpath
    27  
    28  import (
    29  	"encoding/json"
    30  	"fmt"
    31  	rand "math/rand/v2"
    32  	"net/url"
    33  	"sync"
    34  	"time"
    35  
    36  	"google.golang.org/grpc/grpclog"
    37  	"google.golang.org/grpc/internal/envconfig"
    38  	"google.golang.org/grpc/internal/googlecloud"
    39  	internalgrpclog "google.golang.org/grpc/internal/grpclog"
    40  	"google.golang.org/grpc/internal/xds/bootstrap"
    41  	"google.golang.org/grpc/resolver"
    42  	"google.golang.org/grpc/xds/internal/xdsclient"
    43  
    44  	_ "google.golang.org/grpc/xds" // To register xds resolvers and balancers.
    45  )
    46  
    47  const (
    48  	c2pScheme    = "google-c2p"
    49  	c2pAuthority = "traffic-director-c2p.xds.googleapis.com"
    50  
    51  	defaultUniverseDomain   = "googleapis.com"
    52  	zoneURL                 = "http://metadata.google.internal/computeMetadata/v1/instance/zone"
    53  	ipv6URL                 = "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ipv6s"
    54  	ipv6CapableMetadataName = "TRAFFICDIRECTOR_DIRECTPATH_C2P_IPV6_CAPABLE"
    55  	httpReqTimeout          = 10 * time.Second
    56  
    57  	logPrefix        = "[google-c2p-resolver]"
    58  	dnsName, xdsName = "dns", "xds"
    59  )
    60  
    61  var (
    62  	logger           = internalgrpclog.NewPrefixLogger(grpclog.Component("directpath"), logPrefix)
    63  	universeDomainMu sync.Mutex
    64  	universeDomain   = ""
    65  	// For overriding in unittests.
    66  	onGCE         = googlecloud.OnGCE
    67  	randInt       = rand.Int
    68  	xdsClientPool = xdsclient.DefaultPool
    69  )
    70  
    71  func init() {
    72  	resolver.Register(c2pResolverBuilder{})
    73  }
    74  
    75  // SetUniverseDomain informs the gRPC library of the universe domain
    76  // in which the process is running (for example, "googleapis.com").
    77  // It is the caller's responsibility to ensure that the domain is correct.
    78  //
    79  // This setting is used by the "google-c2p" resolver (the resolver used
    80  // for URIs with the "google-c2p" scheme) to configure its dependencies.
    81  //
    82  // If a gRPC channel is created with the "google-c2p" URI scheme and this
    83  // function has NOT been called, then gRPC configures the universe domain as
    84  // "googleapis.com".
    85  //
    86  // Returns nil if either:
    87  //
    88  //	a) The universe domain has not yet been configured.
    89  //	b) The universe domain has been configured and matches the provided value.
    90  //
    91  // Otherwise, returns an error.
    92  func SetUniverseDomain(domain string) error {
    93  	universeDomainMu.Lock()
    94  	defer universeDomainMu.Unlock()
    95  	if domain == "" {
    96  		return fmt.Errorf("universe domain cannot be empty")
    97  	}
    98  	if universeDomain == "" {
    99  		universeDomain = domain
   100  		return nil
   101  	}
   102  	if universeDomain != domain {
   103  		return fmt.Errorf("universe domain cannot be set to %s, already set to different value: %s", domain, universeDomain)
   104  	}
   105  	return nil
   106  }
   107  
   108  func getXdsServerURI() string {
   109  	universeDomainMu.Lock()
   110  	defer universeDomainMu.Unlock()
   111  	if universeDomain == "" {
   112  		universeDomain = defaultUniverseDomain
   113  	}
   114  	// Put env var override logic after default value logic so
   115  	// that tests still run the default value logic.
   116  	if envconfig.C2PResolverTestOnlyTrafficDirectorURI != "" {
   117  		return envconfig.C2PResolverTestOnlyTrafficDirectorURI
   118  	}
   119  	return fmt.Sprintf("dns:///directpath-pa.%s", universeDomain)
   120  }
   121  
   122  type c2pResolverBuilder struct{}
   123  
   124  func (c2pResolverBuilder) Build(t resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
   125  	if t.URL.Host != "" {
   126  		return nil, fmt.Errorf("google-c2p URI scheme does not support authorities")
   127  	}
   128  
   129  	if !runDirectPath() {
   130  		// If not xDS, fallback to DNS.
   131  		t.URL.Scheme = dnsName
   132  		return resolver.Get(dnsName).Build(t, cc, opts)
   133  	}
   134  
   135  	// Note that the following calls to getZone() and getIPv6Capable() does I/O,
   136  	// and has 10 seconds timeout each.
   137  	//
   138  	// This should be fine in most of the cases. In certain error cases, this
   139  	// could block Dial() for up to 10 seconds (each blocking call has its own
   140  	// goroutine).
   141  	zoneCh, ipv6CapableCh := make(chan string), make(chan bool)
   142  	go func() { zoneCh <- getZone(httpReqTimeout) }()
   143  	go func() { ipv6CapableCh <- getIPv6Capable(httpReqTimeout) }()
   144  
   145  	xdsServerURI := getXdsServerURI()
   146  	nodeCfg := newNodeConfig(<-zoneCh, <-ipv6CapableCh)
   147  	xdsServerCfg := newXdsServerConfig(xdsServerURI)
   148  	authoritiesCfg := newAuthoritiesConfig(xdsServerCfg)
   149  
   150  	cfg := map[string]any{
   151  		"xds_servers": []any{xdsServerCfg},
   152  		"client_default_listener_resource_name_template": "%s",
   153  		"authorities": authoritiesCfg,
   154  		"node":        nodeCfg,
   155  	}
   156  	cfgJSON, err := json.Marshal(cfg)
   157  	if err != nil {
   158  		return nil, fmt.Errorf("failed to marshal bootstrap configuration: %v", err)
   159  	}
   160  	config, err := bootstrap.NewConfigFromContents(cfgJSON)
   161  	if err != nil {
   162  		return nil, fmt.Errorf("failed to parse bootstrap contents: %s, %v", string(cfgJSON), err)
   163  	}
   164  	xdsClientPool.SetFallbackBootstrapConfig(config)
   165  
   166  	t = resolver.Target{
   167  		URL: url.URL{
   168  			Scheme: xdsName,
   169  			Host:   c2pAuthority,
   170  			Path:   t.URL.Path,
   171  		},
   172  	}
   173  	return resolver.Get(xdsName).Build(t, cc, opts)
   174  }
   175  
   176  func (b c2pResolverBuilder) Scheme() string {
   177  	return c2pScheme
   178  }
   179  
   180  func newNodeConfig(zone string, ipv6Capable bool) map[string]any {
   181  	node := map[string]any{
   182  		"id":       fmt.Sprintf("C2P-%d", randInt()),
   183  		"locality": map[string]any{"zone": zone},
   184  	}
   185  	if ipv6Capable {
   186  		node["metadata"] = map[string]any{ipv6CapableMetadataName: true}
   187  	}
   188  	return node
   189  }
   190  
   191  func newAuthoritiesConfig(serverCfg map[string]any) map[string]any {
   192  	return map[string]any{
   193  		c2pAuthority: map[string]any{"xds_servers": []any{serverCfg}},
   194  	}
   195  }
   196  
   197  func newXdsServerConfig(uri string) map[string]any {
   198  	return map[string]any{
   199  		"server_uri":      uri,
   200  		"channel_creds":   []map[string]any{{"type": "google_default"}},
   201  		"server_features": []any{"ignore_resource_deletion"},
   202  	}
   203  }
   204  
   205  // runDirectPath returns whether this resolver should use direct path.
   206  //
   207  // direct path is enabled if this client is running on GCE.
   208  func runDirectPath() bool {
   209  	return onGCE()
   210  }