diff --git a/api/current.txt b/api/current.txt index 891925758b555..784f454e22a9c 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6664,7 +6664,7 @@ package android.app.admin { method public void setDelegatedScopes(android.content.ComponentName, java.lang.String, java.util.List); method public void setDeviceOwnerLockScreenInfo(android.content.ComponentName, java.lang.CharSequence); method public void setEndUserSessionMessage(android.content.ComponentName, java.lang.CharSequence); - method public void setGlobalPrivateDns(android.content.ComponentName, int, java.lang.String); + method public int setGlobalPrivateDns(android.content.ComponentName, int, java.lang.String); method public void setGlobalSetting(android.content.ComponentName, java.lang.String, java.lang.String); method public void setKeepUninstalledPackages(android.content.ComponentName, java.util.List); method public boolean setKeyPairCertificate(android.content.ComponentName, java.lang.String, java.util.List, boolean); @@ -6845,6 +6845,9 @@ package android.app.admin { field public static final int PRIVATE_DNS_MODE_OPPORTUNISTIC = 2; // 0x2 field public static final int PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = 3; // 0x3 field public static final int PRIVATE_DNS_MODE_UNKNOWN = 0; // 0x0 + field public static final int PRIVATE_DNS_SET_ERROR_FAILURE_SETTING = 2; // 0x2 + field public static final int PRIVATE_DNS_SET_ERROR_HOST_NOT_SERVING = 1; // 0x1 + field public static final int PRIVATE_DNS_SET_SUCCESS = 0; // 0x0 field public static final int RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT = 2; // 0x2 field public static final int RESET_PASSWORD_REQUIRE_ENTRY = 1; // 0x1 field public static final int SKIP_SETUP_WIZARD = 1; // 0x1 diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index e8262500c7bf6..8e54961faabcb 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -49,6 +49,8 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ParceledListSlice; import android.content.pm.UserInfo; import android.graphics.Bitmap; +import android.net.NetworkUtils; +import android.net.PrivateDnsConnectivityChecker; import android.net.ProxyInfo; import android.net.Uri; import android.os.Binder; @@ -79,6 +81,7 @@ import android.security.keystore.StrongBoxUnavailableException; import android.service.restrictions.RestrictionsReceiver; import android.telephony.TelephonyManager; import android.telephony.data.ApnSetting; +import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; @@ -2031,6 +2034,35 @@ public class DevicePolicyManager { @Retention(RetentionPolicy.SOURCE) public @interface InstallUpdateCallbackErrorConstants {} + /** + * The selected mode has been set successfully. If the mode is + * {@code PRIVATE_DNS_MODE_PROVIDER_HOSTNAME} then it implies the supplied host is valid + * and reachable. + */ + public static final int PRIVATE_DNS_SET_SUCCESS = 0; + + /** + * If the {@code privateDnsHost} provided was of a valid hostname but that host was found + * to not support DNS-over-TLS. + */ + public static final int PRIVATE_DNS_SET_ERROR_HOST_NOT_SERVING = 1; + + /** + * General failure to set the Private DNS mode, not due to one of the reasons listed above. + */ + public static final int PRIVATE_DNS_SET_ERROR_FAILURE_SETTING = 2; + + /** + * @hide + */ + @IntDef(prefix = {"PRIVATE_DNS_SET_"}, value = { + PRIVATE_DNS_SET_SUCCESS, + PRIVATE_DNS_SET_ERROR_HOST_NOT_SERVING, + PRIVATE_DNS_SET_ERROR_FAILURE_SETTING + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SetPrivateDnsModeResultConstants {} + /** * Return true if the given administrator component is currently active (enabled) in the system. * @@ -9897,6 +9929,16 @@ public class DevicePolicyManager { * Sets the global Private DNS mode and host to be used. * May only be called by the device owner. * + *

Note that in case a Private DNS resolver is specified, the method is blocking as it + * will perform a connectivity check to the resolver, to ensure it is valid. Because of that, + * the method should not be called on any thread that relates to user interaction, such as the + * UI thread. + * + *

In case a VPN is used in conjunction with Private DNS resolver, the Private DNS resolver + * must be reachable both from within and outside the VPN. Otherwise, the device may lose + * the ability to resolve hostnames as system traffic to the resolver may not go through the + * VPN. + * * @param admin which {@link DeviceAdminReceiver} this request is associated with. * @param mode Which mode to set - either {@code PRIVATE_DNS_MODE_OPPORTUNISTIC} or * {@code PRIVATE_DNS_MODE_PROVIDER_HOSTNAME}. @@ -9906,6 +9948,9 @@ public class DevicePolicyManager { * @param privateDnsHost The hostname of a server that implements DNS over TLS (RFC7858), if * {@code PRIVATE_DNS_MODE_PROVIDER_HOSTNAME} was specified as the mode, * null otherwise. + * + * @return One of the values in {@link SetPrivateDnsModeResultConstants}. + * * @throws IllegalArgumentException in the following cases: if a {@code privateDnsHost} was * provided but the mode was not {@code PRIVATE_DNS_MODE_PROVIDER_HOSTNAME}, if the mode * specified was {@code PRIVATE_DNS_MODE_PROVIDER_HOSTNAME} but {@code privateDnsHost} does @@ -9913,15 +9958,23 @@ public class DevicePolicyManager { * * @throws SecurityException if the caller is not the device owner. */ - public void setGlobalPrivateDns(@NonNull ComponentName admin, + public int setGlobalPrivateDns(@NonNull ComponentName admin, @PrivateDnsMode int mode, @Nullable String privateDnsHost) { throwIfParentInstance("setGlobalPrivateDns"); + if (mService == null) { - return; + return PRIVATE_DNS_SET_ERROR_FAILURE_SETTING; + } + + if (mode == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME && !TextUtils.isEmpty(privateDnsHost) + && NetworkUtils.isWeaklyValidatedHostname(privateDnsHost)) { + if (!PrivateDnsConnectivityChecker.canConnectToPrivateDnsServer(privateDnsHost)) { + return PRIVATE_DNS_SET_ERROR_HOST_NOT_SERVING; + } } try { - mService.setGlobalPrivateDns(admin, mode, privateDnsHost); + return mService.setGlobalPrivateDns(admin, mode, privateDnsHost); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index fcf74ee301d86..114868580eadf 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -415,7 +415,7 @@ interface IDevicePolicyManager { boolean isMeteredDataDisabledPackageForUser(in ComponentName admin, String packageName, int userId); - void setGlobalPrivateDns(in ComponentName admin, int mode, in String privateDnsHost); + int setGlobalPrivateDns(in ComponentName admin, int mode, in String privateDnsHost); int getGlobalPrivateDnsMode(in ComponentName admin); String getGlobalPrivateDnsHost(in ComponentName admin); diff --git a/core/java/android/net/PrivateDnsConnectivityChecker.java b/core/java/android/net/PrivateDnsConnectivityChecker.java new file mode 100644 index 0000000000000..cfd458c65362f --- /dev/null +++ b/core/java/android/net/PrivateDnsConnectivityChecker.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * 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 android.net; + +import android.annotation.NonNull; +import android.util.Log; + +import java.io.IOException; +import java.net.InetSocketAddress; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * Class for testing connectivity to DNS-over-TLS servers. + * {@hide} + */ +public class PrivateDnsConnectivityChecker { + private static final String TAG = "NetworkUtils"; + + private static final int PRIVATE_DNS_PORT = 853; + private static final int CONNECTION_TIMEOUT_MS = 5000; + + private PrivateDnsConnectivityChecker() { } + + /** + * checks that a provided host can perform a TLS handshake on port 853. + * @param hostname host to connect to. + */ + public static boolean canConnectToPrivateDnsServer(@NonNull String hostname) { + final SocketFactory factory = SSLSocketFactory.getDefault(); + TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_APP); + + try (SSLSocket socket = (SSLSocket) factory.createSocket()) { + socket.setSoTimeout(CONNECTION_TIMEOUT_MS); + socket.connect(new InetSocketAddress(hostname, PRIVATE_DNS_PORT)); + if (!socket.isConnected()) { + Log.w(TAG, String.format("Connection to %s failed.", hostname)); + return false; + } + socket.startHandshake(); + Log.w(TAG, String.format("TLS handshake to %s succeeded.", hostname)); + return true; + } catch (IOException e) { + Log.w(TAG, String.format("TLS handshake to %s failed.", hostname), e); + return false; + } + } +} diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java index 65d32450b27f8..c44f3062c2712 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java @@ -81,7 +81,8 @@ abstract class BaseIDevicePolicyManager extends IDevicePolicyManager.Stub { } @Override - public void setGlobalPrivateDns(ComponentName who, int mode, String privateDnsHost) { + public int setGlobalPrivateDns(ComponentName who, int mode, String privateDnsHost) { + return DevicePolicyManager.PRIVATE_DNS_SET_ERROR_FAILURE_SETTING; } @Override diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index bca3b1fe3d866..041d5d8cc858a 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -59,6 +59,8 @@ import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_OFF; import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_OPPORTUNISTIC; import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_UNKNOWN; +import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_SET_ERROR_FAILURE_SETTING; +import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_SET_SUCCESS; import static android.app.admin.DevicePolicyManager.PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER; import static android.app.admin.DevicePolicyManager.WIPE_EUICC; import static android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE; @@ -13280,32 +13282,40 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } @Override - public void setGlobalPrivateDns(@NonNull ComponentName who, int mode, String privateDnsHost) { + public int setGlobalPrivateDns(@NonNull ComponentName who, int mode, String privateDnsHost) { if (!mHasFeature) { - return; + return PRIVATE_DNS_SET_ERROR_FAILURE_SETTING; } Preconditions.checkNotNull(who, "ComponentName is null"); enforceDeviceOwner(who); + final int returnCode; + switch (mode) { case PRIVATE_DNS_MODE_OPPORTUNISTIC: if (!TextUtils.isEmpty(privateDnsHost)) { - throw new IllegalArgumentException("A DNS host should not be provided when " + - "setting opportunistic mode."); + throw new IllegalArgumentException( + "Host provided for opportunistic mode, but is not needed."); } putPrivateDnsSettings(ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC, null); - break; + return PRIVATE_DNS_SET_SUCCESS; case PRIVATE_DNS_MODE_PROVIDER_HOSTNAME: - if (!NetworkUtils.isWeaklyValidatedHostname(privateDnsHost)) { + if (TextUtils.isEmpty(privateDnsHost) + || !NetworkUtils.isWeaklyValidatedHostname(privateDnsHost)) { throw new IllegalArgumentException( - String.format("Provided hostname is not valid: %s", privateDnsHost)); + String.format("Provided hostname %s is not valid", privateDnsHost)); } - putPrivateDnsSettings(ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, + + // Connectivity check will have been performed in the DevicePolicyManager before + // the call here. + putPrivateDnsSettings( + ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, privateDnsHost); - break; + return PRIVATE_DNS_SET_SUCCESS; default: - throw new IllegalArgumentException(String.format("Unsupported mode: %d", mode)); + throw new IllegalArgumentException( + String.format("Provided mode, %d, is not a valid mode.", mode)); } }