master pull

This commit is contained in:
MV Shiva Prasad 2025-07-17 13:53:06 +05:30
commit 5bb1ef5786
26 changed files with 695 additions and 62 deletions

View File

@ -11,8 +11,6 @@ for general contribution guidelines.
- [ejona86](https://github.com/ejona86), Google LLC
- [jdcormie](https://github.com/jdcormie), Google LLC
- [kannanjgithub](https://github.com/kannanjgithub), Google LLC
- [larry-safran](https://github.com/larry-safran), Google LLC
- [markb74](https://github.com/markb74), Google LLC
- [ran-su](https://github.com/ran-su), Google LLC
- [sergiitk](https://github.com/sergiitk), Google LLC
- [temawi](https://github.com/temawi), Google LLC
@ -26,7 +24,9 @@ for general contribution guidelines.
- [ericgribkoff](https://github.com/ericgribkoff)
- [jiangtaoli2016](https://github.com/jiangtaoli2016)
- [jtattermusch](https://github.com/jtattermusch)
- [larry-safran](https://github.com/larry-safran)
- [louiscryan](https://github.com/louiscryan)
- [markb74](https://github.com/markb74)
- [nicolasnoble](https://github.com/nicolasnoble)
- [nmittler](https://github.com/nmittler)
- [sanjaypujare](https://github.com/sanjaypujare)

View File

@ -239,6 +239,9 @@ public abstract class NameResolver {
* {@link ResolutionResult#getAddressesOrError()} is empty, {@link #onError(Status)} will be
* called.
*
* <p>Newer NameResolver implementations should prefer calling onResult2. This method exists to
* facilitate older {@link Listener} implementations to migrate to {@link Listener2}.
*
* @param resolutionResult the resolved server addresses, attributes, and Service Config.
* @since 1.21.0
*/
@ -248,6 +251,10 @@ public abstract class NameResolver {
* Handles a name resolving error from the resolver. The listener is responsible for eventually
* invoking {@link NameResolver#refresh()} to re-attempt resolution.
*
* <p>New NameResolver implementations should prefer calling onResult2 which will have the
* address resolution error in {@link ResolutionResult}'s addressesOrError. This method exists
* to facilitate older implementations using {@link Listener} to migrate to {@link Listener2}.
*
* @param error a non-OK status
* @since 1.21.0
*/
@ -255,9 +262,14 @@ public abstract class NameResolver {
public abstract void onError(Status error);
/**
* Handles updates on resolved addresses and attributes.
* Handles updates on resolved addresses and attributes. Must be called from the same
* {@link SynchronizationContext} available in {@link NameResolver.Args} that is passed
* from the channel.
*
* @param resolutionResult the resolved server addresses, attributes, and Service Config.
* @param resolutionResult the resolved server addresses or error in address resolution,
* attributes, and Service Config or error
* @return status indicating whether the resolutionResult was accepted by the listener,
* typically the result from a load balancer.
* @since 1.66
*/
public Status onResult2(ResolutionResult resolutionResult) {

View File

@ -72,6 +72,7 @@ dependencies {
androidTestImplementation testFixtures(project(':grpc-core'))
testFixturesImplementation libraries.guava.testlib
testFixturesImplementation testFixtures(project(':grpc-core'))
}
import net.ltgt.gradle.errorprone.CheckSeverity

View File

@ -172,6 +172,12 @@ public final class BinderClientTransportTest {
return this;
}
@CanIgnoreReturnValue
public BinderClientTransportBuilder setPreAuthorizeServer(boolean preAuthorizeServer) {
factoryBuilder.setPreAuthorizeServers(preAuthorizeServer);
return this;
}
public BinderTransport.BinderClientTransport build() {
return factoryBuilder
.buildClientTransportFactory()
@ -372,11 +378,12 @@ public final class BinderClientTransportTest {
}
@Test
public void testBlackHoleSecurityPolicyConnectTimeout() throws Exception {
public void testBlackHoleSecurityPolicyAuthTimeout() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport =
new BinderClientTransportBuilder()
.setSecurityPolicy(securityPolicy)
.setPreAuthorizeServer(false)
.setReadyTimeoutMillis(1_234)
.build();
transport.start(transportListener).run();
@ -387,15 +394,39 @@ public final class BinderClientTransportTest {
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
assertThat(transportStatus.getDescription()).contains("1234");
transportListener.awaitTermination();
// If the transport gave up waiting on auth, it should cancel its request.
assertThat(authRequest.isCancelled()).isTrue();
}
@Test
public void testAsyncSecurityPolicyFailure() throws Exception {
public void testBlackHoleSecurityPolicyPreAuthTimeout() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
transport =
new BinderClientTransportBuilder()
.setSecurityPolicy(securityPolicy)
.setPreAuthorizeServer(true)
.setReadyTimeoutMillis(1_234)
.build();
transport.start(transportListener).run();
// Take the next authRequest but don't respond to it, in order to trigger the ready timeout.
AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS);
Status transportStatus = transportListener.awaitShutdown();
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
assertThat(transportStatus.getDescription()).contains("1234");
transportListener.awaitTermination();
// If the transport gave up waiting on auth, it should cancel its request.
assertThat(preAuthRequest.isCancelled()).isTrue();
}
@Test
public void testAsyncSecurityPolicyAuthFailure() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport =
new BinderClientTransportBuilder()
.setPreAuthorizeServer(false)
.setSecurityPolicy(securityPolicy)
.build();
RuntimeException exception = new NullPointerException();
transport.start(transportListener).run();
securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS).setResult(exception);
@ -406,15 +437,55 @@ public final class BinderClientTransportTest {
}
@Test
public void testAsyncSecurityPolicySuccess() throws Exception {
public void testAsyncSecurityPolicyPreAuthFailure() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
transport =
new BinderClientTransportBuilder()
.setPreAuthorizeServer(true)
.setSecurityPolicy(securityPolicy)
.build();
RuntimeException exception = new NullPointerException();
transport.start(transportListener).run();
securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS).setResult(exception);
Status transportStatus = transportListener.awaitShutdown();
assertThat(transportStatus.getCode()).isEqualTo(Code.INTERNAL);
assertThat(transportStatus.getCause()).isEqualTo(exception);
transportListener.awaitTermination();
}
@Test
public void testAsyncSecurityPolicyAuthSuccess() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport =
new BinderClientTransportBuilder()
.setPreAuthorizeServer(false)
.setSecurityPolicy(securityPolicy)
.build();
transport.start(transportListener).run();
securityPolicy
.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS)
.setResult(Status.PERMISSION_DENIED);
.setResult(Status.PERMISSION_DENIED.withDescription("xyzzy"));
Status transportStatus = transportListener.awaitShutdown();
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
assertThat(transportStatus.getDescription()).contains("xyzzy");
transportListener.awaitTermination();
}
@Test
public void testAsyncSecurityPolicyPreAuthSuccess() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport =
new BinderClientTransportBuilder()
.setPreAuthorizeServer(true)
.setSecurityPolicy(securityPolicy)
.build();
transport.start(transportListener).run();
securityPolicy
.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS)
.setResult(Status.PERMISSION_DENIED.withDescription("xyzzy"));
Status transportStatus = transportListener.awaitShutdown();
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
assertThat(transportStatus.getDescription()).contains("xyzzy");
transportListener.awaitTermination();
}

View File

@ -18,6 +18,8 @@ package io.grpc.binder;
import android.content.Intent;
import android.os.UserHandle;
import io.grpc.Attributes;
import io.grpc.EquivalentAddressGroup;
import io.grpc.ExperimentalApi;
import io.grpc.NameResolver;
@ -46,4 +48,16 @@ public final class ApiConstants {
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
public static final NameResolver.Args.Key<UserHandle> TARGET_ANDROID_USER =
NameResolver.Args.Key.create("target-android-user");
/**
* Lets you override a Channel's pre-auth configuration (see {@link
* BinderChannelBuilder#preAuthorizeServers(boolean)}) for a given {@link EquivalentAddressGroup}.
*
* <p>A {@link NameResolver} that discovers servers from an untrusted source like PackageManager
* can use this to force server pre-auth and prevent abuse.
*/
@EquivalentAddressGroup.Attr
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12191")
public static final Attributes.Key<Boolean> PRE_AUTH_SERVER_OVERRIDE =
Attributes.Key.create("pre-auth-server-override");
}

View File

@ -279,6 +279,35 @@ public final class BinderChannelBuilder extends ForwardingChannelBuilder<BinderC
return this;
}
/**
* Checks servers against this Channel's {@link SecurityPolicy} *before* binding.
*
* <p>Android users can be tricked into installing a malicious app with the same package name as a
* legitimate server. That's why we don't send calls to a server until it has been authorized by
* an appropriate {@link SecurityPolicy}. But merely binding to a malicious server can enable
* "keep-alive" and "background activity launch" abuse, even if it's ultimately unauthorized.
* Pre-authorization mitigates these threats by performing a preliminary {@link SecurityPolicy}
* check against a server app's PackageManager-registered identity without actually creating an
* instance of it. This is especially important for security when the server's direct address
* isn't known in advance but rather resolved via target URI or discovered by other means.
*
* <p>Note that, unlike ordinary authorization, pre-authorization is performed against the server
* app's UID, not the UID of the process hosting the bound Service. These can be different, most
* commonly due to services that set `android:isolatedProcess=true`.
*
* <p>Pre-authorization is strongly recommended but it remains optional for now because of this
* behavior change and the small performance cost.
*
* <p>The default value of this property is false but it will become true in a future release.
* Clients that require a particular behavior should configure it explicitly using this method
* rather than relying on the default.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12191")
public BinderChannelBuilder preAuthorizeServers(boolean preAuthorize) {
transportFactoryBuilder.setPreAuthorizeServers(preAuthorize);
return this;
}
@Override
public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) {
checkState(

View File

@ -16,10 +16,12 @@
package io.grpc.binder.internal;
import android.content.pm.ServiceInfo;
import android.os.IBinder;
import androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import io.grpc.Status;
import io.grpc.StatusException;
/** An interface for managing a {@code Binder} connection. */
interface Bindable {
@ -45,6 +47,19 @@ interface Bindable {
void onUnbound(Status reason);
}
/**
* Fetches details about the remote Service from PackageManager without binding to it.
*
* <p>Resolving an untrusted address before binding to it lets you screen out problematic servers
* before giving them a chance to run. However, note that the identity/existence of the resolved
* Service can change between the time this method returns and the time you actually bind/connect
* to it. For example, suppose the target package gets uninstalled or upgraded right after this
* method returns. In {@link Observer#onBound}, you should verify that the server you resolved is
* the same one you connected to.
*/
@AnyThread
ServiceInfo resolve() throws StatusException;
/**
* Attempt to bind with the remote service.
*

View File

@ -55,6 +55,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
final InboundParcelablePolicy inboundParcelablePolicy;
final OneWayBinderProxy.Decorator binderDecorator;
final long readyTimeoutMillis;
final boolean preAuthorizeServers; // TODO(jdcormie): Default to true.
ScheduledExecutorService executorService;
Executor offloadExecutor;
@ -75,6 +76,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
inboundParcelablePolicy = checkNotNull(builder.inboundParcelablePolicy);
binderDecorator = checkNotNull(builder.binderDecorator);
readyTimeoutMillis = builder.readyTimeoutMillis;
preAuthorizeServers = builder.preAuthorizeServers;
executorService = scheduledExecutorPool.getObject();
offloadExecutor = offloadExecutorPool.getObject();
@ -128,6 +130,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
InboundParcelablePolicy inboundParcelablePolicy = InboundParcelablePolicy.DEFAULT;
OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR;
long readyTimeoutMillis = 60_000;
boolean preAuthorizeServers;
@Override
public BinderClientTransportFactory buildClientTransportFactory() {
@ -216,5 +219,11 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
this.readyTimeoutMillis = readyTimeoutMillis;
return this;
}
/** Whether to check server addresses against the SecurityPolicy *before* binding to them. */
public Builder setPreAuthorizeServers(boolean preAuthorizeServers) {
this.preAuthorizeServers = preAuthorizeServers;
return this;
}
}
}

View File

@ -19,9 +19,11 @@ package io.grpc.binder.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
import android.content.pm.ServiceInfo;
import android.os.Binder;
import android.os.DeadObjectException;
import android.os.IBinder;
@ -78,7 +80,6 @@ import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
@ -574,6 +575,7 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
private final long readyTimeoutMillis;
private final PingTracker pingTracker;
private final boolean preAuthorizeServer;
@Nullable private ManagedClientTransport.Listener clientTransportListener;
@ -585,6 +587,10 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
@GuardedBy("this")
@Nullable private ListenableFuture<Status> authResultFuture; // null before we check auth.
@GuardedBy("this")
@Nullable
private ListenableFuture<Status> preAuthResultFuture; // null before we pre-auth.
/**
* Constructs a new transport instance.
*
@ -609,6 +615,9 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
this.securityPolicy = factory.securityPolicy;
this.offloadExecutor = offloadExecutorPool.getObject();
this.readyTimeoutMillis = factory.readyTimeoutMillis;
Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE);
this.preAuthorizeServer =
preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers;
numInUseStreams = new AtomicInteger();
pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id));
@ -650,7 +659,16 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
synchronized (BinderClientTransport.this) {
if (inState(TransportState.NOT_STARTED)) {
setState(TransportState.SETUP);
try {
if (preAuthorizeServer) {
preAuthorize(serviceBinding.resolve());
} else {
serviceBinding.bind();
}
} catch (StatusException e) {
shutdownInternal(e.getStatus(), true);
return;
}
if (readyTimeoutMillis >= 0) {
readyTimeoutFuture =
getScheduledExecutorService()
@ -664,6 +682,43 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
};
}
@GuardedBy("this")
private void preAuthorize(ServiceInfo serviceInfo) {
// It's unlikely, but the identity/existence of this Service could change by the time we
// actually connect. It doesn't matter though, because:
// - If pre-auth fails (but would succeed against the server's new state), the grpc-core layer
// will eventually retry using a new transport instance that will see the Service's new state.
// - If pre-auth succeeds (but would fail against the server's new state), we might give an
// unauthorized server a chance to run, but the connection will still fail by SecurityPolicy
// check later in handshake. Pre-auth remains effective at mitigating abuse because malware
// can't typically control the exact timing of its installation.
preAuthResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid);
Futures.addCallback(
preAuthResultFuture,
new FutureCallback<Status>() {
@Override
public void onSuccess(Status result) {
handlePreAuthResult(result);
}
@Override
public void onFailure(Throwable t) {
handleAuthResult(t);
}
},
offloadExecutor);
}
private synchronized void handlePreAuthResult(Status authorization) {
if (inState(TransportState.SETUP)) {
if (!authorization.isOk()) {
shutdownInternal(authorization, true);
} else {
serviceBinding.bind();
}
}
}
private synchronized void onReadyTimeout() {
if (inState(TransportState.SETUP)) {
readyTimeoutFuture = null;
@ -758,6 +813,9 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
readyTimeoutFuture.cancel(false);
readyTimeoutFuture = null;
}
if (preAuthResultFuture != null) {
preAuthResultFuture.cancel(false); // No effect if already complete.
}
if (authResultFuture != null) {
authResultFuture.cancel(false); // No effect if already complete.
}
@ -780,11 +838,7 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true);
} else {
authResultFuture =
(securityPolicy instanceof AsyncSecurityPolicy)
? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid)
: Futures.submit(
() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor);
authResultFuture = checkServerAuthorizationAsync(remoteUid);
Futures.addCallback(
authResultFuture,
new FutureCallback<Status>() {
@ -803,6 +857,12 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
}
}
private ListenableFuture<Status> checkServerAuthorizationAsync(int remoteUid) {
return (securityPolicy instanceof AsyncSecurityPolicy)
? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid)
: Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor);
}
private synchronized void handleAuthResult(IBinder binder, Status authorization) {
if (inState(TransportState.SETUP)) {
if (!authorization.isOk()) {

View File

@ -23,14 +23,22 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.IBinder;
import android.os.UserHandle;
import androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.VerifyException;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.binder.BinderChannelCredentials;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -85,6 +93,8 @@ final class ServiceBinding implements Bindable, ServiceConnection {
private final Observer observer;
private final Executor mainThreadExecutor;
private static volatile Method queryIntentServicesAsUserMethod;
@GuardedBy("this")
private State state;
@ -247,6 +257,71 @@ final class ServiceBinding implements Bindable, ServiceConnection {
}
}
// Sadly the PackageManager#resolveServiceAsUser() API we need isn't part of the SDK or even a
// @SystemApi as of this writing. Modern Android prevents even system apps from calling it, by any
// means (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces).
// So instead we call queryIntentServicesAsUser(), which does more than we need but *is* a
// @SystemApi in all the SDK versions where we support cross-user Channels.
@Nullable
private static ResolveInfo resolveServiceAsUser(
PackageManager packageManager, Intent intent, int flags, UserHandle targetUserHandle) {
List<ResolveInfo> results =
queryIntentServicesAsUser(packageManager, intent, flags, targetUserHandle);
// The first query result is "what would be returned by resolveService", per the javadoc.
return (results != null && !results.isEmpty()) ? results.get(0) : null;
}
// The cross-user Channel feature requires the client to be a system app so we assume @SystemApi
// queryIntentServicesAsUser() is visible to us at runtime. It would be visible at build time too,
// if our host system app were written to call it directly. We only have to use reflection here
// because grpc-java is a library built outside the Android source tree where the compiler can't
// see the "non-SDK" @SystemApis that we need.
@Nullable
@SuppressWarnings("unchecked") // Safe by PackageManager#queryIntentServicesAsUser spec in AOSP.
private static List<ResolveInfo> queryIntentServicesAsUser(
PackageManager packageManager, Intent intent, int flags, UserHandle targetUserHandle) {
try {
if (queryIntentServicesAsUserMethod == null) {
synchronized (ServiceBinding.class) {
if (queryIntentServicesAsUserMethod == null) {
queryIntentServicesAsUserMethod =
PackageManager.class.getMethod(
"queryIntentServicesAsUser", Intent.class, int.class, UserHandle.class);
}
}
}
return (List<ResolveInfo>)
queryIntentServicesAsUserMethod.invoke(packageManager, intent, flags, targetUserHandle);
} catch (ReflectiveOperationException e) {
throw new VerifyException(e);
}
}
@AnyThread
@Override
public ServiceInfo resolve() throws StatusException {
checkState(sourceContext != null);
PackageManager packageManager = sourceContext.getPackageManager();
int flags = 0;
if (Build.VERSION.SDK_INT >= 29) {
// Filter out non-'directBootAware' <service>s when 'targetUserHandle' is locked. Here's why:
// Callers want 'bindIntent' to #resolve() to the same thing a follow-up call to #bind() will.
// But bindService() *always* ignores services that can't presently be created for lack of
// 'directBootAware'-ness. This flag explicitly tells resolveService() to act the same way.
flags |= PackageManager.MATCH_DIRECT_BOOT_AUTO;
}
ResolveInfo resolveInfo =
targetUserHandle != null
? resolveServiceAsUser(packageManager, bindIntent, flags, targetUserHandle)
: packageManager.resolveService(bindIntent, flags);
if (resolveInfo == null) {
throw Status.UNIMPLEMENTED // Same status code as when bindService() returns false.
.withDescription("resolveService(" + bindIntent + " / " + targetUserHandle + ") was null")
.asException();
}
return resolveInfo.serviceInfo;
}
@MainThread
private void clearReferences() {
sourceContext = null;

View File

@ -22,7 +22,13 @@ import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.robolectric.Shadows.shadowOf;
import android.app.Application;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.ServiceInfo;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.core.content.pm.ApplicationInfoBuilder;
import androidx.test.core.content.pm.PackageInfoBuilder;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
@ -46,11 +52,13 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
@RunWith(RobolectricTestRunner.class)
@RunWith(ParameterizedRobolectricTestRunner.class)
@LooperMode(Mode.INSTRUMENTATION_TEST)
public final class RobolectricBinderSecurityTest {
@ -62,10 +70,33 @@ public final class RobolectricBinderSecurityTest {
private ManagedChannel channel;
private Server server;
@Parameter public boolean preAuthServersParam;
@Parameters(name = "preAuthServersParam={0}")
public static ImmutableList<Boolean> data() {
return ImmutableList.of(true, false);
}
@Before
public void setUp() {
ApplicationInfo serverAppInfo =
ApplicationInfoBuilder.newBuilder().setPackageName(context.getPackageName()).build();
serverAppInfo.uid = android.os.Process.myUid();
PackageInfo serverPkgInfo =
PackageInfoBuilder.newBuilder()
.setPackageName(serverAppInfo.packageName)
.setApplicationInfo(serverAppInfo)
.build();
shadowOf(context.getPackageManager()).installPackage(serverPkgInfo);
ServiceInfo serviceInfo = new ServiceInfo();
serviceInfo.name = "SomeService";
serviceInfo.packageName = serverAppInfo.packageName;
serviceInfo.applicationInfo = serverAppInfo;
shadowOf(context.getPackageManager()).addOrUpdateService(serviceInfo);
AndroidComponentAddress listenAddress =
AndroidComponentAddress.forRemoteComponent(context.getPackageName(), "HostService");
AndroidComponentAddress.forRemoteComponent(serviceInfo.packageName, serviceInfo.name);
MethodDescriptor<Empty, Empty> methodDesc = getMethodDescriptor();
ServerCallHandler<Empty, Empty> callHandler =
@ -110,6 +141,7 @@ public final class RobolectricBinderSecurityTest {
checkNotNull(binderReceiver.get()));
channel =
BinderChannelBuilder.forAddress(listenAddress, context)
.preAuthorizeServers(preAuthServersParam)
.build();
}

View File

@ -16,13 +16,29 @@
package io.grpc.binder.internal;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
import android.app.Application;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.ServiceInfo;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.core.content.pm.ApplicationInfoBuilder;
import androidx.test.core.content.pm.PackageInfoBuilder;
import com.google.common.collect.ImmutableList;
import io.grpc.Attributes;
import io.grpc.ServerStreamTracer;
import io.grpc.Status;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.ApiConstants;
import io.grpc.binder.AsyncSecurityPolicy;
import io.grpc.binder.internal.SettableAsyncSecurityPolicy.AuthRequest;
import io.grpc.internal.AbstractTransportTest;
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
import io.grpc.internal.GrpcUtil;
@ -33,12 +49,20 @@ import io.grpc.internal.SharedResourcePool;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
import org.robolectric.shadows.ShadowBinder;
/**
* All of the AbstractTransportTest cases applied to {@link BinderTransport} running in a
@ -52,7 +76,7 @@ import org.robolectric.annotation.LooperMode.Mode;
* meaning test cases don't run on the main thread. This supports the AbstractTransportTest approach
* where the test thread frequently blocks waiting for transport state changes to take effect.
*/
@RunWith(RobolectricTestRunner.class)
@RunWith(ParameterizedRobolectricTestRunner.class)
@LooperMode(Mode.INSTRUMENTATION_TEST)
public final class RobolectricBinderTransportTest extends AbstractTransportTest {
@ -64,13 +88,56 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest
private final ObjectPool<Executor> serverExecutorPool =
SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR);
@Rule public MockitoRule mocks = MockitoJUnit.rule();
@Mock AsyncSecurityPolicy mockClientSecurityPolicy;
ApplicationInfo serverAppInfo;
PackageInfo serverPkgInfo;
ServiceInfo serviceInfo;
private int nextServerAddress;
@Parameter public boolean preAuthServersParam;
@Parameters(name = "preAuthServersParam={0}")
public static ImmutableList<Boolean> data() {
return ImmutableList.of(true, false);
}
@Override
public void setUp() {
serverAppInfo =
ApplicationInfoBuilder.newBuilder().setPackageName("the.server.package").build();
serverAppInfo.uid = android.os.Process.myUid();
serverPkgInfo =
PackageInfoBuilder.newBuilder()
.setPackageName(serverAppInfo.packageName)
.setApplicationInfo(serverAppInfo)
.build();
shadowOf(application.getPackageManager()).installPackage(serverPkgInfo);
serviceInfo = new ServiceInfo();
serviceInfo.name = "SomeService";
serviceInfo.packageName = serverAppInfo.packageName;
serviceInfo.applicationInfo = serverAppInfo;
shadowOf(application.getPackageManager()).addOrUpdateService(serviceInfo);
super.setUp();
}
@Before
public void requestRealisticBindServiceBehavior() {
shadowOf(application).setBindServiceCallsOnServiceConnectedDirectly(false);
shadowOf(application).setUnbindServiceCallsOnServiceDisconnected(false);
}
@Override
protected InternalServer newServer(List<ServerStreamTracer.Factory> streamTracerFactories) {
AndroidComponentAddress listenAddr = AndroidComponentAddress.forBindIntent(
AndroidComponentAddress listenAddr =
AndroidComponentAddress.forBindIntent(
new Intent()
.setClassName(application.getPackageName(), "HostService")
.setClassName(serviceInfo.packageName, serviceInfo.name)
.setAction("io.grpc.action.BIND." + nextServerAddress++));
BinderServer binderServer =
@ -81,6 +148,7 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest
.setStreamTracerFactories(streamTracerFactories)
.build();
shadowOf(application.getPackageManager()).addServiceIfNotPresent(listenAddr.getComponent());
shadowOf(application)
.setComponentNameAndServiceForBindServiceForIntent(
listenAddr.asBindIntent(), listenAddr.getComponent(), binderServer.getHostBinder());
@ -97,22 +165,30 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest
return newServer(streamTracerFactories);
}
@Override
protected ManagedClientTransport newClientTransport(InternalServer server) {
BinderClientTransportFactory.Builder builder =
new BinderClientTransportFactory.Builder()
BinderClientTransportFactory.Builder newClientTransportFactoryBuilder() {
return new BinderClientTransportFactory.Builder()
.setPreAuthorizeServers(preAuthServersParam)
.setSourceContext(application)
.setScheduledExecutorPool(executorServicePool)
.setOffloadExecutorPool(offloadExecutorPool);
}
BinderClientTransportBuilder newClientTransportBuilder() {
return new BinderClientTransportBuilder()
.setFactory(newClientTransportFactoryBuilder().buildClientTransportFactory())
.setServerAddress(server.getListenSocketAddress());
}
@Override
protected ManagedClientTransport newClientTransport(InternalServer server) {
ClientTransportOptions options = new ClientTransportOptions();
options.setEagAttributes(eagAttrs());
options.setChannelLogger(transportLogger());
return new BinderTransport.BinderClientTransport(
builder.buildClientTransportFactory(),
(AndroidComponentAddress) server.getListenSocketAddress(),
options);
return newClientTransportBuilder()
.setServerAddress(server.getListenSocketAddress())
.setOptions(options)
.build();
}
@Override
@ -120,6 +196,74 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest
return ((AndroidComponentAddress) server.getListenSocketAddress()).getAuthority();
}
@Test
public void clientAuthorizesServerUidsInOrder() throws Exception {
// TODO(jdcormie): In real Android, Binder#getCallingUid is thread-local but Robolectric only
// lets us fake value this *globally*. So the ShadowBinder#setCallingUid() here unrealistically
// affects the server's view of the client's uid too. For now this doesn't matter because this
// test never exercises server SecurityPolicy.
ShadowBinder.setCallingUid(11111); // UID of the server *process*.
serverPkgInfo.applicationInfo.uid = 22222; // UID of the server *app*, which can be different.
shadowOf(application.getPackageManager()).installPackage(serverPkgInfo);
shadowOf(application.getPackageManager()).addOrUpdateService(serviceInfo);
server = newServer(ImmutableList.of());
server.start(serverListener);
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
client =
newClientTransportBuilder()
.setFactory(
newClientTransportFactoryBuilder()
.setSecurityPolicy(securityPolicy)
.buildClientTransportFactory())
.build();
runIfNotNull(client.start(mockClientTransportListener));
if (preAuthServersParam) {
AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
assertThat(preAuthRequest.uid).isEqualTo(22222);
verify(mockClientTransportListener, never()).transportReady();
preAuthRequest.setResult(Status.OK);
}
AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
assertThat(authRequest.uid).isEqualTo(11111);
verify(mockClientTransportListener, never()).transportReady();
authRequest.setResult(Status.OK);
verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportReady();
}
@Test
public void eagAttributeCanOverrideChannelPreAuthServerSetting() throws Exception {
server.start(serverListener);
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
ClientTransportOptions options = new ClientTransportOptions();
options.setEagAttributes(
Attributes.newBuilder().set(ApiConstants.PRE_AUTH_SERVER_OVERRIDE, true).build());
client =
newClientTransportBuilder()
.setOptions(options)
.setFactory(
newClientTransportFactoryBuilder()
.setPreAuthorizeServers(preAuthServersParam) // To be overridden.
.setSecurityPolicy(securityPolicy)
.buildClientTransportFactory())
.build();
runIfNotNull(client.start(mockClientTransportListener));
AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
verify(mockClientTransportListener, never()).transportReady();
preAuthRequest.setResult(Status.OK);
AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
verify(mockClientTransportListener, never()).transportReady();
authRequest.setResult(Status.OK);
verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportReady();
}
@Test
@Ignore("See BinderTransportTest#socketStats.")
@Override

View File

@ -19,6 +19,7 @@ package io.grpc.binder.internal;
import static android.content.Context.BIND_AUTO_CREATE;
import static android.os.Looper.getMainLooper;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.robolectric.Shadows.shadowOf;
@ -27,6 +28,7 @@ import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.os.IBinder;
import android.os.Parcel;
import android.os.UserHandle;
@ -34,6 +36,7 @@ import androidx.core.content.ContextCompat;
import androidx.test.core.app.ApplicationProvider;
import io.grpc.Status;
import io.grpc.Status.Code;
import io.grpc.StatusException;
import io.grpc.binder.BinderChannelCredentials;
import io.grpc.binder.internal.Bindable.Observer;
import java.util.Arrays;
@ -59,6 +62,7 @@ public final class ServiceBindingTest {
private Application appContext;
private ComponentName serviceComponent;
private ServiceInfo serviceInfo = new ServiceInfo();
private ShadowApplication shadowApplication;
private TestObserver observer;
private ServiceBinding binding;
@ -67,13 +71,17 @@ public final class ServiceBindingTest {
public void setUp() {
appContext = ApplicationProvider.getApplicationContext();
serviceComponent = new ComponentName("DUMMY", "SERVICE");
serviceInfo.packageName = serviceComponent.getPackageName();
serviceInfo.name = serviceComponent.getClassName();
observer = new TestObserver();
shadowApplication = shadowOf(appContext);
shadowApplication.setComponentNameAndServiceForBindService(serviceComponent, mockBinder);
shadowOf(appContext.getPackageManager()).addOrUpdateService(serviceInfo);
// Don't call onServiceDisconnected() upon unbindService(), just like the real Android doesn't.
shadowApplication.setUnbindServiceCallsOnServiceDisconnected(false);
shadowApplication.setBindServiceCallsOnServiceConnectedDirectly(false);
binding = newBuilder().build();
shadowOf(getMainLooper()).idle();
@ -276,6 +284,49 @@ public final class ServiceBindingTest {
assertThat(binding.isSourceContextCleared()).isFalse();
}
@Test
public void testResolve() throws Exception {
serviceInfo.processName = "x"; // ServiceInfo has no equals() so look for one distinctive field.
shadowOf(appContext.getPackageManager()).addOrUpdateService(serviceInfo);
ServiceInfo resolvedServiceInfo = binding.resolve();
assertThat(resolvedServiceInfo.processName).isEqualTo(serviceInfo.processName);
}
@Test
@Config(sdk = 33)
public void testResolveWithTargetUserHandle() throws Exception {
serviceInfo.processName = "x"; // ServiceInfo has no equals() so look for one distinctive field.
// Robolectric just ignores the user arg to resolveServiceAsUser() so this is all we can do.
shadowOf(appContext.getPackageManager()).addOrUpdateService(serviceInfo);
binding = newBuilder().setTargetUserHandle(generateUserHandle(/* userId= */ 0)).build();
ServiceInfo resolvedServiceInfo = binding.resolve();
assertThat(resolvedServiceInfo.processName).isEqualTo(serviceInfo.processName);
}
@Test
public void testResolveNonExistentServiceThrows() throws Exception {
ComponentName doesNotExistService = new ComponentName("does.not.exist", "NoService");
binding = newBuilder().setTargetComponent(doesNotExistService).build();
StatusException statusException = assertThrows(StatusException.class, binding::resolve);
assertThat(statusException.getStatus().getCode()).isEqualTo(Code.UNIMPLEMENTED);
assertThat(statusException.getStatus().getDescription()).contains("does.not.exist");
}
@Test
@Config(sdk = 33)
public void testResolveNonExistentServiceWithTargetUserThrows() throws Exception {
ComponentName doesNotExistService = new ComponentName("does.not.exist", "NoService");
binding =
newBuilder()
.setTargetUserHandle(generateUserHandle(/* userId= */ 12345))
.setTargetComponent(doesNotExistService)
.build();
StatusException statusException = assertThrows(StatusException.class, binding::resolve);
assertThat(statusException.getStatus().getCode()).isEqualTo(Code.UNIMPLEMENTED);
assertThat(statusException.getStatus().getDescription()).contains("does.not.exist");
assertThat(statusException.getStatus().getDescription()).contains("12345");
}
@Test
@Config(sdk = 30)
public void testBindWithDeviceAdmin() throws Exception {
@ -284,7 +335,7 @@ public final class ServiceBindingTest {
allowBindDeviceAdminForUser(appContext, adminComponent, /* userId= */ 0);
binding =
newBuilder()
.setTargetUserHandle(UserHandle.getUserHandleForUid(/* userId= */ 0))
.setTargetUserHandle(UserHandle.getUserHandleForUid(/* uid= */ 0))
.setTargetUserHandle(generateUserHandle(/* userId= */ 0))
.setChannelCredentials(BinderChannelCredentials.forDevicePolicyAdmin(adminComponent))
.build();

View File

@ -0,0 +1,61 @@
/*
* Copyright 2025 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.binder.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import io.grpc.ChannelLogger;
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
import io.grpc.internal.TestUtils.NoopChannelLogger;
import java.net.SocketAddress;
/**
* Helps unit tests create {@link BinderTransport.BinderClientTransport} instances without having to
* mention irrelevant details (go/tott/719).
*/
public class BinderClientTransportBuilder {
private BinderClientTransportFactory factory;
private SocketAddress serverAddress;
private ChannelLogger channelLogger = new NoopChannelLogger();
private io.grpc.internal.ClientTransportFactory.ClientTransportOptions options =
new ClientTransportOptions();
public BinderClientTransportBuilder setServerAddress(SocketAddress serverAddress) {
this.serverAddress = checkNotNull(serverAddress);
return this;
}
public BinderClientTransportBuilder setChannelLogger(ChannelLogger channelLogger) {
this.channelLogger = checkNotNull(channelLogger);
return this;
}
public BinderClientTransportBuilder setOptions(ClientTransportOptions options) {
this.options = checkNotNull(options);
return this;
}
public BinderClientTransportBuilder setFactory(BinderClientTransportFactory factory) {
this.factory = checkNotNull(factory);
return this;
}
public BinderTransport.BinderClientTransport build() {
return factory.newClientTransport(
checkNotNull(serverAddress), checkNotNull(options), checkNotNull(channelLogger));
}
}

View File

@ -92,7 +92,7 @@ public abstract class AbstractTransportTest {
*/
public static final int TEST_FLOW_CONTROL_WINDOW = 65 * 1024;
private static final int TIMEOUT_MS = 5000;
protected static final int TIMEOUT_MS = 5000;
protected static final String GRPC_EXPERIMENTAL_SUPPORT_TRACING_MESSAGE_SIZES =
"GRPC_EXPERIMENTAL_SUPPORT_TRACING_MESSAGE_SIZES";
@ -2163,7 +2163,7 @@ public abstract class AbstractTransportTest {
return true;
}
private static void runIfNotNull(Runnable runnable) {
protected static void runIfNotNull(Runnable runnable) {
if (runnable != null) {
runnable.run();
}

View File

@ -101,10 +101,10 @@ def grpc_java_repositories(bzlmod = False):
if not native.existing_rule("com_github_cncf_xds"):
http_archive(
name = "com_github_cncf_xds",
strip_prefix = "xds-024c85f92f20cab567a83acc50934c7f9711d124",
sha256 = "5f403aa681711500ca8e62387be3e37d971977db6e88616fc21862a406430649",
strip_prefix = "xds-2ac532fd44436293585084f8d94c6bdb17835af0",
sha256 = "790c4c83b6950bb602fec221f6a529d9f368cdc8852aae7d2592d0d04b015f37",
urls = [
"https://github.com/cncf/xds/archive/024c85f92f20cab567a83acc50934c7f9711d124.tar.gz",
"https://github.com/cncf/xds/archive/2ac532fd44436293585084f8d94c6bdb17835af0.tar.gz",
],
)
if not bzlmod and not native.existing_rule("com_github_grpc_grpc"):

View File

@ -89,7 +89,7 @@ public final class ClusterResolverLoadBalancerProvider extends LoadBalancerProvi
@Override
public int hashCode() {
return Objects.hash(discoveryMechanisms, lbConfig);
return Objects.hash(discoveryMechanisms, lbConfig, isHttp11ProxyAvailable);
}
@Override
@ -102,7 +102,8 @@ public final class ClusterResolverLoadBalancerProvider extends LoadBalancerProvi
}
ClusterResolverConfig that = (ClusterResolverConfig) o;
return discoveryMechanisms.equals(that.discoveryMechanisms)
&& lbConfig.equals(that.lbConfig);
&& lbConfig.equals(that.lbConfig)
&& isHttp11ProxyAvailable == that.isHttp11ProxyAvailable;
}
@Override
@ -110,6 +111,7 @@ public final class ClusterResolverLoadBalancerProvider extends LoadBalancerProvi
return MoreObjects.toStringHelper(this)
.add("discoveryMechanisms", discoveryMechanisms)
.add("lbConfig", lbConfig)
.add("isHttp11ProxyAvailable", isHttp11ProxyAvailable)
.toString();
}

View File

@ -51,6 +51,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
@ -523,6 +524,22 @@ final class RingHashLoadBalancer extends MultiChildLoadBalancer {
this.requestHashHeader = requestHashHeader;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof RingHashConfig)) {
return false;
}
RingHashConfig that = (RingHashConfig) o;
return this.minRingSize == that.minRingSize
&& this.maxRingSize == that.maxRingSize
&& Objects.equals(this.requestHashHeader, that.requestHashHeader);
}
@Override
public int hashCode() {
return Objects.hash(minRingSize, maxRingSize, requestHashHeader);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)

View File

@ -32,6 +32,7 @@ import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.testing.EqualsTester;
import io.grpc.Attributes;
import io.grpc.ChannelLogger;
import io.grpc.ConnectivityState;
@ -1199,6 +1200,27 @@ public class ClusterResolverLoadBalancerTest {
any(ConnectivityState.class), any(SubchannelPicker.class));
}
@Test
public void config_equalsTester() {
new EqualsTester()
.addEqualityGroup(
new ClusterResolverConfig(
Collections.singletonList(edsDiscoveryMechanism1), leastRequest, false),
new ClusterResolverConfig(
Collections.singletonList(edsDiscoveryMechanism1), leastRequest, false))
.addEqualityGroup(new ClusterResolverConfig(
Collections.singletonList(edsDiscoveryMechanism1), roundRobin, false))
.addEqualityGroup(new ClusterResolverConfig(
Collections.singletonList(edsDiscoveryMechanism1), leastRequest, true))
.addEqualityGroup(new ClusterResolverConfig(
Collections.singletonList(edsDiscoveryMechanismWithOutlierDetection),
leastRequest,
false))
.addEqualityGroup(new ClusterResolverConfig(
Arrays.asList(edsDiscoveryMechanism1, edsDiscoveryMechanism2), leastRequest, false))
.testEquals();
}
private void deliverLbConfig(ClusterResolverConfig config) {
loadBalancer.acceptResolvedAddresses(
ResolvedAddresses.newBuilder()

View File

@ -42,6 +42,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.google.common.collect.Iterables;
import com.google.common.primitives.UnsignedInteger;
import com.google.common.testing.EqualsTester;
import io.grpc.Attributes;
import io.grpc.CallOptions;
import io.grpc.ConnectivityState;
@ -1113,6 +1114,19 @@ public class RingHashLoadBalancerTest {
assertThat(picks).containsExactly(subchannel1);
}
@Test
public void config_equalsTester() {
new EqualsTester()
.addEqualityGroup(
new RingHashConfig(1, 2, "headerA"),
new RingHashConfig(1, 2, "headerA"))
.addEqualityGroup(new RingHashConfig(1, 1, "headerA"))
.addEqualityGroup(new RingHashConfig(2, 2, "headerA"))
.addEqualityGroup(new RingHashConfig(1, 2, "headerB"))
.addEqualityGroup(new RingHashConfig(1, 2, ""))
.testEquals();
}
private List<Subchannel> initializeLbSubchannels(RingHashConfig config,
List<EquivalentAddressGroup> servers, InitializationFlags... initFlags) {

View File

@ -17,7 +17,7 @@
set -e
# import VERSION from one of the google internal CLs
VERSION=024c85f92f20cab567a83acc50934c7f9711d124
VERSION=2ac532fd44436293585084f8d94c6bdb17835af0
DOWNLOAD_URL="https://github.com/cncf/xds/archive/${VERSION}.tar.gz"
DOWNLOAD_BASE_DIR="xds-${VERSION}"
SOURCE_PROTO_BASE_DIR="${DOWNLOAD_BASE_DIR}"
@ -40,6 +40,7 @@ xds/annotations/v3/versioning.proto
xds/core/v3/authority.proto
xds/core/v3/collection_entry.proto
xds/core/v3/context_params.proto
xds/core/v3/cidr.proto
xds/core/v3/extension.proto
xds/core/v3/resource_locator.proto
xds/core/v3/resource_name.proto

View File

@ -10,7 +10,7 @@ option go_package = "github.com/cncf/xds/go/xds/data/orca/v3";
import "validate/validate.proto";
// See section `ORCA load report format` of the design document in
// :ref:`https://github.com/envoyproxy/envoy/issues/6614`.
// https://github.com/envoyproxy/envoy/issues/6614.
message OrcaLoadReport {
// CPU utilization expressed as a fraction of available CPU resources. This

View File

@ -2,9 +2,7 @@ syntax = "proto3";
package xds.type.matcher.v3;
import "xds/annotations/v3/status.proto";
import "xds/type/v3/cel.proto";
import "validate/validate.proto";
option java_package = "com.github.xds.type.matcher.v3";
@ -12,8 +10,6 @@ option java_outer_classname = "CelProto";
option java_multiple_files = true;
option go_package = "github.com/cncf/xds/go/xds/type/matcher/v3";
option (xds.annotations.v3.file_status).work_in_progress = true;
// [#protodoc-title: Common Expression Language (CEL) matchers]
// Performs a match by evaluating a `Common Expression Language
@ -30,8 +26,7 @@ option (xds.annotations.v3.file_status).work_in_progress = true;
// Refer to :ref:`Unified Matcher API <envoy_v3_api_msg_.xds.type.matcher.v3.Matcher>` documentation
// for usage details.
//
// [#comment:TODO(sergiitk): Link HttpAttributesMatchInput + usage example.]
// [#comment:TODO(sergiitk): When implemented, add the extension tag.]
// [#comment: envoy.matching.matchers.cel_matcher]
message CelMatcher {
// Either parsed or checked representation of the CEL program.
type.v3.CelExpression expr_match = 1 [(validate.rules).message = {required: true}];

View File

@ -2,15 +2,11 @@ syntax = "proto3";
package xds.type.matcher.v3;
import "xds/annotations/v3/status.proto";
option java_package = "com.github.xds.type.matcher.v3";
option java_outer_classname = "HttpInputsProto";
option java_multiple_files = true;
option go_package = "github.com/cncf/xds/go/xds/type/matcher/v3";
option (xds.annotations.v3.file_status).work_in_progress = true;
// [#protodoc-title: Common HTTP Inputs]
// Specifies that matching should be performed on the set of :ref:`HTTP attributes
@ -22,6 +18,6 @@ option (xds.annotations.v3.file_status).work_in_progress = true;
// Refer to :ref:`Unified Matcher API <envoy_v3_api_msg_.xds.type.matcher.v3.Matcher>` documentation
// for usage details.
//
// [#comment:TODO(sergiitk): When implemented, add the extension tag.]
// [#comment: envoy.matching.inputs.cel_data_input]
message HttpAttributesCelMatchInput {
}

View File

@ -2,7 +2,6 @@ syntax = "proto3";
package xds.type.matcher.v3;
import "xds/annotations/v3/status.proto";
import "xds/core/v3/extension.proto";
import "xds/type/matcher/v3/string.proto";
@ -21,8 +20,6 @@ option go_package = "github.com/cncf/xds/go/xds/type/matcher/v3";
// As an on_no_match might result in another matching tree being evaluated, this process
// might repeat several times until the final OnMatch (or no match) is decided.
message Matcher {
option (xds.annotations.v3.message_status).work_in_progress = true;
// What to do if a match is successful.
message OnMatch {
oneof on_match {
@ -38,6 +35,14 @@ message Matcher {
// Protocol-specific action to take.
core.v3.TypedExtensionConfig action = 2;
}
// If true and the Matcher matches, the action will be taken but the caller
// will behave as if the Matcher did not match. A subsequent matcher or
// on_no_match action will be used instead.
// This field is not supported in all contexts in which the matcher API is
// used. If this field is set in a context in which it's not supported,
// the resource will be rejected.
bool keep_matching = 3;
}
// A linear list of field matchers.

View File

@ -47,6 +47,13 @@ message CelExpression {
//
// If set, takes precedence over ``cel_expr_parsed``.
cel.expr.CheckedExpr cel_expr_checked = 4;
// Unparsed expression in string form. For example, ``request.headers['x-env'] == 'prod'`` will
// get ``x-env`` header value and compare it with ``prod``.
// Check the `Common Expression Language <https://github.com/google/cel-spec>`_ for more details.
//
// If set, takes precedence over ``cel_expr_parsed`` and ``cel_expr_checked``.
string cel_expr_string = 5;
}
// Extracts a string by evaluating a `Common Expression Language