1 /* 2 * Copyright 2021 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.googleapis; 18 19 import static com.google.common.base.Preconditions.checkNotNull; 20 21 import com.google.common.annotations.VisibleForTesting; 22 import com.google.common.base.Charsets; 23 import com.google.common.base.Preconditions; 24 import com.google.common.base.Strings; 25 import com.google.common.collect.ImmutableList; 26 import com.google.common.collect.ImmutableMap; 27 import com.google.common.io.CharStreams; 28 import io.grpc.NameResolver; 29 import io.grpc.NameResolverRegistry; 30 import io.grpc.Status; 31 import io.grpc.SynchronizationContext; 32 import io.grpc.alts.InternalCheckGcpEnvironment; 33 import io.grpc.internal.GrpcUtil; 34 import io.grpc.internal.SharedResourceHolder; 35 import io.grpc.internal.SharedResourceHolder.Resource; 36 import java.io.IOException; 37 import java.io.InputStreamReader; 38 import java.io.Reader; 39 import java.net.HttpURLConnection; 40 import java.net.URI; 41 import java.net.URISyntaxException; 42 import java.net.URL; 43 import java.util.Map; 44 import java.util.Random; 45 import java.util.concurrent.Executor; 46 47 /** 48 * CloudToProd version of {@link NameResolver}. 49 */ 50 final class GoogleCloudToProdNameResolver extends NameResolver { 51 52 @VisibleForTesting 53 static final String METADATA_URL_ZONE = 54 "http://metadata.google.internal/computeMetadata/v1/instance/zone"; 55 @VisibleForTesting 56 static final String METADATA_URL_SUPPORT_IPV6 = 57 "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ipv6s"; 58 static final String C2P_AUTHORITY = "traffic-director-c2p.xds.googleapis.com"; 59 @VisibleForTesting 60 static boolean isOnGcp = InternalCheckGcpEnvironment.isOnGcp(); 61 @VisibleForTesting 62 static boolean xdsBootstrapProvided = 63 System.getenv("GRPC_XDS_BOOTSTRAP") != null 64 || System.getProperty("io.grpc.xds.bootstrap") != null 65 || System.getenv("GRPC_XDS_BOOTSTRAP_CONFIG") != null 66 || System.getProperty("io.grpc.xds.bootstrapConfig") != null; 67 @VisibleForTesting 68 static boolean enableFederation = 69 Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_XDS_FEDERATION")) 70 || Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_XDS_FEDERATION")); 71 72 private static final String serverUriOverride = 73 System.getenv("GRPC_TEST_ONLY_GOOGLE_C2P_RESOLVER_TRAFFIC_DIRECTOR_URI"); 74 75 private HttpConnectionProvider httpConnectionProvider = HttpConnectionFactory.INSTANCE; 76 private final String authority; 77 private final SynchronizationContext syncContext; 78 private final Resource<Executor> executorResource; 79 private final BootstrapSetter bootstrapSetter; 80 private final NameResolver delegate; 81 private final Random rand; 82 private final boolean usingExecutorResource; 83 // It's not possible to use both PSM and DirectPath C2P in the same application. 84 // Delegate to DNS if user-provided bootstrap is found. 85 private final String schemeOverride = 86 !isOnGcp 87 || (xdsBootstrapProvided && !enableFederation) 88 ? "dns" : "xds"; 89 private Executor executor; 90 private Listener2 listener; 91 private boolean succeeded; 92 private boolean resolving; 93 private boolean shutdown; 94 GoogleCloudToProdNameResolver(URI targetUri, Args args, Resource<Executor> executorResource, BootstrapSetter bootstrapSetter)95 GoogleCloudToProdNameResolver(URI targetUri, Args args, Resource<Executor> executorResource, 96 BootstrapSetter bootstrapSetter) { 97 this(targetUri, args, executorResource, new Random(), bootstrapSetter, 98 NameResolverRegistry.getDefaultRegistry().asFactory()); 99 } 100 101 @VisibleForTesting GoogleCloudToProdNameResolver(URI targetUri, Args args, Resource<Executor> executorResource, Random rand, BootstrapSetter bootstrapSetter, NameResolver.Factory nameResolverFactory)102 GoogleCloudToProdNameResolver(URI targetUri, Args args, Resource<Executor> executorResource, 103 Random rand, BootstrapSetter bootstrapSetter, NameResolver.Factory nameResolverFactory) { 104 this.executorResource = checkNotNull(executorResource, "executorResource"); 105 this.bootstrapSetter = checkNotNull(bootstrapSetter, "bootstrapSetter"); 106 this.rand = checkNotNull(rand, "rand"); 107 String targetPath = checkNotNull(checkNotNull(targetUri, "targetUri").getPath(), "targetPath"); 108 Preconditions.checkArgument( 109 targetPath.startsWith("/"), 110 "the path component (%s) of the target (%s) must start with '/'", 111 targetPath, 112 targetUri); 113 authority = GrpcUtil.checkAuthority(targetPath.substring(1)); 114 syncContext = checkNotNull(args, "args").getSynchronizationContext(); 115 targetUri = overrideUriScheme(targetUri, schemeOverride); 116 if (schemeOverride.equals("xds") && enableFederation) { 117 targetUri = overrideUriAuthority(targetUri, C2P_AUTHORITY); 118 } 119 delegate = checkNotNull(nameResolverFactory, "nameResolverFactory").newNameResolver( 120 targetUri, args); 121 executor = args.getOffloadExecutor(); 122 usingExecutorResource = executor == null; 123 } 124 125 @Override getServiceAuthority()126 public String getServiceAuthority() { 127 return authority; 128 } 129 130 @Override start(final Listener2 listener)131 public void start(final Listener2 listener) { 132 if (delegate == null) { 133 listener.onError(Status.INTERNAL.withDescription( 134 "Delegate resolver not found, scheme: " + schemeOverride)); 135 return; 136 } 137 this.listener = checkNotNull(listener, "listener"); 138 resolve(); 139 } 140 resolve()141 private void resolve() { 142 if (resolving || shutdown || delegate == null) { 143 return; 144 } 145 resolving = true; 146 if (schemeOverride.equals("dns")) { 147 delegate.start(listener); 148 succeeded = true; 149 resolving = false; 150 return; 151 } 152 if (executor == null) { 153 executor = SharedResourceHolder.get(executorResource); 154 } 155 156 class Resolve implements Runnable { 157 @Override 158 public void run() { 159 ImmutableMap<String, ?> rawBootstrap = null; 160 try { 161 // User provided bootstrap configs are only supported with federation. If federation is 162 // not enabled or there is no user provided config, we set a custom bootstrap override. 163 // Otherwise, we don't set the override, which will allow a user provided bootstrap config 164 // to take effect. 165 if (!enableFederation || !xdsBootstrapProvided) { 166 rawBootstrap = generateBootstrap(queryZoneMetadata(METADATA_URL_ZONE), 167 queryIpv6SupportMetadata(METADATA_URL_SUPPORT_IPV6)); 168 } 169 } catch (IOException e) { 170 listener.onError( 171 Status.INTERNAL.withDescription("Unable to get metadata").withCause(e)); 172 } finally { 173 final ImmutableMap<String, ?> finalRawBootstrap = rawBootstrap; 174 syncContext.execute(new Runnable() { 175 @Override 176 public void run() { 177 if (!shutdown) { 178 if (finalRawBootstrap != null) { 179 bootstrapSetter.setBootstrap(finalRawBootstrap); 180 } 181 delegate.start(listener); 182 succeeded = true; 183 } 184 resolving = false; 185 } 186 }); 187 } 188 } 189 } 190 191 executor.execute(new Resolve()); 192 } 193 generateBootstrap(String zone, boolean supportIpv6)194 private ImmutableMap<String, ?> generateBootstrap(String zone, boolean supportIpv6) { 195 ImmutableMap.Builder<String, Object> nodeBuilder = ImmutableMap.builder(); 196 nodeBuilder.put("id", "C2P-" + (rand.nextInt() & Integer.MAX_VALUE)); 197 if (!zone.isEmpty()) { 198 nodeBuilder.put("locality", ImmutableMap.of("zone", zone)); 199 } 200 if (supportIpv6) { 201 nodeBuilder.put("metadata", 202 ImmutableMap.of("TRAFFICDIRECTOR_DIRECTPATH_C2P_IPV6_CAPABLE", true)); 203 } 204 ImmutableMap.Builder<String, Object> serverBuilder = ImmutableMap.builder(); 205 String serverUri = "directpath-pa.googleapis.com"; 206 if (serverUriOverride != null && serverUriOverride.length() > 0) { 207 serverUri = serverUriOverride; 208 } 209 serverBuilder.put("server_uri", serverUri); 210 serverBuilder.put("channel_creds", 211 ImmutableList.of(ImmutableMap.of("type", "google_default"))); 212 serverBuilder.put("server_features", ImmutableList.of("xds_v3", "ignore_resource_deletion")); 213 ImmutableMap.Builder<String, Object> authoritiesBuilder = ImmutableMap.builder(); 214 authoritiesBuilder.put( 215 C2P_AUTHORITY, 216 ImmutableMap.of("xds_servers", ImmutableList.of(serverBuilder.buildOrThrow()))); 217 return ImmutableMap.of( 218 "node", nodeBuilder.buildOrThrow(), 219 "xds_servers", ImmutableList.of(serverBuilder.buildOrThrow()), 220 "authorities", authoritiesBuilder.buildOrThrow()); 221 } 222 223 @Override refresh()224 public void refresh() { 225 if (succeeded) { 226 delegate.refresh(); 227 } else if (!resolving) { 228 resolve(); 229 } 230 } 231 232 @Override shutdown()233 public void shutdown() { 234 if (shutdown) { 235 return; 236 } 237 shutdown = true; 238 if (delegate != null) { 239 delegate.shutdown(); 240 } 241 if (executor != null && usingExecutorResource) { 242 executor = SharedResourceHolder.release(executorResource, executor); 243 } 244 } 245 queryZoneMetadata(String url)246 private String queryZoneMetadata(String url) throws IOException { 247 HttpURLConnection con = null; 248 String respBody; 249 try { 250 con = httpConnectionProvider.createConnection(url); 251 if (con.getResponseCode() != 200) { 252 return ""; 253 } 254 try (Reader reader = new InputStreamReader(con.getInputStream(), Charsets.UTF_8)) { 255 respBody = CharStreams.toString(reader); 256 } 257 } finally { 258 if (con != null) { 259 con.disconnect(); 260 } 261 } 262 int index = respBody.lastIndexOf('/'); 263 return index == -1 ? "" : respBody.substring(index + 1); 264 } 265 queryIpv6SupportMetadata(String url)266 private boolean queryIpv6SupportMetadata(String url) throws IOException { 267 HttpURLConnection con = null; 268 try { 269 con = httpConnectionProvider.createConnection(url); 270 return con.getResponseCode() == 200; 271 } finally { 272 if (con != null) { 273 con.disconnect(); 274 } 275 } 276 } 277 278 @VisibleForTesting setHttpConnectionProvider(HttpConnectionProvider httpConnectionProvider)279 void setHttpConnectionProvider(HttpConnectionProvider httpConnectionProvider) { 280 this.httpConnectionProvider = httpConnectionProvider; 281 } 282 overrideUriScheme(URI uri, String scheme)283 private static URI overrideUriScheme(URI uri, String scheme) { 284 URI res; 285 try { 286 res = new URI(scheme, uri.getAuthority(), uri.getPath(), uri.getQuery(), uri.getFragment()); 287 } catch (URISyntaxException ex) { 288 throw new IllegalArgumentException("Invalid scheme: " + scheme, ex); 289 } 290 return res; 291 } 292 overrideUriAuthority(URI uri, String authority)293 private static URI overrideUriAuthority(URI uri, String authority) { 294 URI res; 295 try { 296 res = new URI(uri.getScheme(), authority, uri.getPath(), uri.getQuery(), uri.getFragment()); 297 } catch (URISyntaxException ex) { 298 throw new IllegalArgumentException("Invalid authority: " + authority, ex); 299 } 300 return res; 301 } 302 303 private enum HttpConnectionFactory implements HttpConnectionProvider { 304 INSTANCE; 305 306 @Override createConnection(String url)307 public HttpURLConnection createConnection(String url) throws IOException { 308 HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); 309 con.setRequestMethod("GET"); 310 con.setReadTimeout(10000); 311 con.setRequestProperty("Metadata-Flavor", "Google"); 312 return con; 313 } 314 } 315 316 @VisibleForTesting 317 interface HttpConnectionProvider { createConnection(String url)318 HttpURLConnection createConnection(String url) throws IOException; 319 } 320 321 public interface BootstrapSetter { setBootstrap(Map<String, ?> bootstrap)322 void setBootstrap(Map<String, ?> bootstrap); 323 } 324 } 325