Snap for 11051817 from d023c314fa5f6a2b14c75c9c3e741449cda83470 to mainline-appsearch-release Change-Id: I55d8edee87e22f8c8718fdcb2d069331598403e6
diff --git a/Android.bp b/Android.bp index ebe5217..61b4a1e 100644 --- a/Android.bp +++ b/Android.bp
@@ -193,7 +193,6 @@ "framework-connectivity", "framework-connectivity-t.stubs.module_lib", "framework-tethering", - "android.net.ipsec.ike.stubs.module_lib", ], sdk_version: "module_33", visibility: ["//visibility:private"], @@ -215,7 +214,6 @@ "framework-connectivity", "framework-connectivity-t.stubs.module_lib", "framework-tethering", - "android.net.ipsec.ike.stubs.module_lib" ], sdk_version: module_34_version, visibility: ["//visibility:private"], @@ -339,7 +337,10 @@ "src/**/*.java", ":statslog-networkstack-java-gen-current" ], - static_libs: ["NetworkStackApiCurrentShims"], + static_libs: [ + "NetworkStackApiCurrentShims", + "net-utils-device-common-struct", + ], manifest: "AndroidManifestBase.xml", visibility: [ "//frameworks/base/tests/net/integration", @@ -357,7 +358,10 @@ "src/**/*.java", ":statslog-networkstack-java-gen-stable", ], - static_libs: ["NetworkStackApiStableShims"], + static_libs: [ + "NetworkStackApiStableShims", + "net-utils-device-common-struct", + ], manifest: "AndroidManifestBase.xml", visibility: [ "//frameworks/base/packages/Connectivity/tests/integration",
diff --git a/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileBuilderShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileBuilderShimImpl.java deleted file mode 100644 index 2597840..0000000 --- a/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileBuilderShimImpl.java +++ /dev/null
@@ -1,122 +0,0 @@ -/* - * Copyright (C) 2023 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 com.android.networkstack.apishim.api30; - -import android.net.Ikev2VpnProfile; -import android.net.ProxyInfo; -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; - -import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim; -import com.android.networkstack.apishim.common.Ikev2VpnProfileShim; -import com.android.networkstack.apishim.common.UnsupportedApiLevelException; - -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.util.List; - -/** - * Implementation of Ikev2VpnProfileBuilderShim for API 30. - */ -@RequiresApi(Build.VERSION_CODES.R) -public class Ikev2VpnProfileBuilderShimImpl - implements Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> { - protected final Ikev2VpnProfile.Builder mBuilder; - - protected Ikev2VpnProfileBuilderShimImpl(@NonNull Ikev2VpnProfile.Builder builder) { - mBuilder = builder; - } - - protected Ikev2VpnProfileBuilderShimImpl(@NonNull String serverAddr, - @NonNull String identity) { - mBuilder = new Ikev2VpnProfile.Builder(serverAddr, identity); - } - - /** - * Returns a new instance of this shim impl. - */ - public static Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> newInstance( - @NonNull String serverAddr, @NonNull String identity) { - return new Ikev2VpnProfileBuilderShimImpl(serverAddr, identity); - } - - @Override - public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setAuthPsk(@NonNull byte[] psk) { - mBuilder.setAuthPsk(psk); - return this; - } - - @Override - public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setAuthUsernamePassword( - @NonNull String user, @NonNull String pass, @Nullable X509Certificate serverRootCa) - throws UnsupportedApiLevelException { - mBuilder.setAuthUsernamePassword(user, pass, serverRootCa); - return this; - } - - @Override - public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setAuthDigitalSignature( - @NonNull X509Certificate userCert, @NonNull PrivateKey key, - @Nullable X509Certificate serverRootCa) { - mBuilder.setAuthDigitalSignature(userCert, key, serverRootCa); - return this; - } - - @Override - public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setBypassable(boolean isBypassable) { - mBuilder.setBypassable(true); - return this; - } - - @Override - public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setProxy(@Nullable ProxyInfo proxy) { - mBuilder.setProxy(proxy); - return this; - } - - @Override - public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setMaxMtu(int mtu) { - mBuilder.setMaxMtu(mtu); - return this; - } - - @Override - public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setMetered(boolean isMetered) { - mBuilder.setMetered(isMetered); - return this; - } - - @Override - public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setAllowedAlgorithms( - @NonNull List<String> algorithmNames) { - mBuilder.setAllowedAlgorithms(algorithmNames); - return this; - } - - @Override - public Ikev2VpnProfile.Builder getBuilder() { - return mBuilder; - } - - @Override - public Ikev2VpnProfileShim build() { - return Ikev2VpnProfileShimImpl.newInstance(mBuilder.build()); - } -}
diff --git a/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileShimImpl.java deleted file mode 100644 index 54a2a35..0000000 --- a/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileShimImpl.java +++ /dev/null
@@ -1,46 +0,0 @@ -/* - * Copyright (C) 2023 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 com.android.networkstack.apishim.api30; - -import android.net.Ikev2VpnProfile; - -import androidx.annotation.NonNull; - -import com.android.networkstack.apishim.common.Ikev2VpnProfileShim; -/** - * Implementation of Ikev2VpnProfileShim for API 30. - */ -// TODO : when API29 is no longer supported, remove the type argument -public class Ikev2VpnProfileShimImpl implements Ikev2VpnProfileShim<Ikev2VpnProfile> { - protected final Ikev2VpnProfile mProfile; - - protected Ikev2VpnProfileShimImpl(Ikev2VpnProfile profile) { - mProfile = profile; - } - - /** - * Returns a new instance of this shim impl. - */ - public static Ikev2VpnProfileShim<Ikev2VpnProfile> newInstance( - @NonNull Ikev2VpnProfile profile) { - return new Ikev2VpnProfileShimImpl(profile); - } - - public Ikev2VpnProfile getProfile() { - return mProfile; - } -}
diff --git a/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileBuilderShimImpl.java b/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileBuilderShimImpl.java deleted file mode 100644 index 308407b..0000000 --- a/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileBuilderShimImpl.java +++ /dev/null
@@ -1,90 +0,0 @@ -/* - * Copyright (C) 2023 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 com.android.networkstack.apishim.api33; - -import android.net.Ikev2VpnProfile; -import android.net.ipsec.ike.IkeTunnelConnectionParams; -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; - -import com.android.modules.utils.build.SdkLevel; -import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim; -import com.android.networkstack.apishim.common.UnsupportedApiLevelException; - -/** - * A shim for Ikev2VpnProfile.Builder - */ -@RequiresApi(Build.VERSION_CODES.TIRAMISU) -public class Ikev2VpnProfileBuilderShimImpl - extends com.android.networkstack.apishim.api30.Ikev2VpnProfileBuilderShimImpl { - protected Ikev2VpnProfileBuilderShimImpl(@NonNull IkeTunnelConnectionParams params) { - super(new Ikev2VpnProfile.Builder(params)); - } - - protected Ikev2VpnProfileBuilderShimImpl(@NonNull String serverAddr, - @NonNull String identity) { - super(serverAddr, identity); - } - - /** - * Returns a new instance of this shim impl. - */ - @RequiresApi(Build.VERSION_CODES.R) - public static Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> newInstance( - @NonNull String serverAddr, @NonNull String identity) { - if (SdkLevel.isAtLeastT()) { - return new Ikev2VpnProfileBuilderShimImpl(serverAddr, identity); - } - return com.android.networkstack.apishim.api30.Ikev2VpnProfileBuilderShimImpl - .newInstance(serverAddr, identity); - } - - /** - * Returns a new instance of this shim impl. - */ - @RequiresApi(Build.VERSION_CODES.TIRAMISU) - public static Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> newInstance( - @NonNull IkeTunnelConnectionParams params) throws UnsupportedApiLevelException { - if (SdkLevel.isAtLeastT()) { - return new Ikev2VpnProfileBuilderShimImpl(params); - } else { - throw new UnsupportedApiLevelException("Only supported from API 33"); - } - } - - /** - * @see Ikev2VpnProfile.Builder#setRequiresInternetValidation(boolean) - */ - @Override - public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setRequiresInternetValidation( - boolean requiresInternetValidation) { - mBuilder.setRequiresInternetValidation(requiresInternetValidation); - return this; - } - - /** - * @see Ikev2VpnProfile.Builder#setLocalRoutesExcluded(boolean) - */ - @Override - public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setLocalRoutesExcluded( - boolean excludeLocalRoutes) { - mBuilder.setLocalRoutesExcluded(excludeLocalRoutes); - return this; - } -}
diff --git a/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileShimImpl.java b/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileShimImpl.java deleted file mode 100644 index 8e82218..0000000 --- a/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileShimImpl.java +++ /dev/null
@@ -1,29 +0,0 @@ -/* - * Copyright (C) 2023 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 com.android.networkstack.apishim.api33; - -import android.net.Ikev2VpnProfile; - -/** - * A shim for Ikev2VpnProfile - */ -public class Ikev2VpnProfileShimImpl - extends com.android.networkstack.apishim.api30.Ikev2VpnProfileShimImpl { - protected Ikev2VpnProfileShimImpl(Ikev2VpnProfile profile) { - super(profile); - } -}
diff --git a/apishim/34/com/android/networkstack/apishim/api34/Ikev2VpnProfileBuilderShimImpl.java b/apishim/34/com/android/networkstack/apishim/api34/Ikev2VpnProfileBuilderShimImpl.java deleted file mode 100644 index ac74028..0000000 --- a/apishim/34/com/android/networkstack/apishim/api34/Ikev2VpnProfileBuilderShimImpl.java +++ /dev/null
@@ -1,92 +0,0 @@ -/* - * Copyright (C) 2023 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 com.android.networkstack.apishim.api34; - -import android.net.Ikev2VpnProfile; -import android.net.ipsec.ike.IkeTunnelConnectionParams; -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; - -import com.android.modules.utils.build.SdkLevel; -import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim; -import com.android.networkstack.apishim.common.UnsupportedApiLevelException; - -/** - * A shim for Ikev2VpnProfile.Builder - */ -@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) -public class Ikev2VpnProfileBuilderShimImpl - extends com.android.networkstack.apishim.api33.Ikev2VpnProfileBuilderShimImpl { - - protected Ikev2VpnProfileBuilderShimImpl(@NonNull IkeTunnelConnectionParams params) { - super(params); - } - - protected Ikev2VpnProfileBuilderShimImpl(@NonNull String serverAddr, - @NonNull String identity) { - super(serverAddr, identity); - } - - /** - * Returns a new instance of this shim impl. - */ - @RequiresApi(Build.VERSION_CODES.R) - public static Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> newInstance( - @NonNull String serverAddr, @NonNull String identity) { - if (SdkLevel.isAtLeastU()) { - return new Ikev2VpnProfileBuilderShimImpl(serverAddr, identity); - } - return com.android.networkstack.apishim.api33.Ikev2VpnProfileBuilderShimImpl - .newInstance(serverAddr, identity); - } - - /** - * Returns a new instance of this shim impl. - */ - @RequiresApi(Build.VERSION_CODES.TIRAMISU) - public static Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> newInstance( - @NonNull IkeTunnelConnectionParams params) throws UnsupportedApiLevelException { - if (SdkLevel.isAtLeastU()) { - return new Ikev2VpnProfileBuilderShimImpl(params); - } else { - return com.android.networkstack.apishim.api33.Ikev2VpnProfileBuilderShimImpl - .newInstance(params); - } - } - - /** - * @see Ikev2VpnProfile.Builder#setAutomaticIpVersionSelectionEnabled(boolean) - */ - @Override - public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> - setAutomaticIpVersionSelectionEnabled(boolean isEnabled) { - mBuilder.setAutomaticIpVersionSelectionEnabled(isEnabled); - return this; - } - - /** - * @see Ikev2VpnProfile.Builder#setAutomaticNattKeepaliveTimerEnabled(boolean) - */ - @Override - public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> - setAutomaticNattKeepaliveTimerEnabled(boolean isEnabled) { - mBuilder.setAutomaticNattKeepaliveTimerEnabled(isEnabled); - return this; - } -}
diff --git a/apishim/34/com/android/networkstack/apishim/api34/Ikev2VpnProfileShimImpl.java b/apishim/34/com/android/networkstack/apishim/api34/Ikev2VpnProfileShimImpl.java deleted file mode 100644 index 848bbd1..0000000 --- a/apishim/34/com/android/networkstack/apishim/api34/Ikev2VpnProfileShimImpl.java +++ /dev/null
@@ -1,62 +0,0 @@ -/* - * Copyright (C) 2023 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 com.android.networkstack.apishim.api34; - -import android.net.Ikev2VpnProfile; -import android.os.Build; - -import androidx.annotation.RequiresApi; - -import com.android.modules.utils.build.SdkLevel; -import com.android.networkstack.apishim.common.Ikev2VpnProfileShim; - -/** - * A shim for Ikev2VpnProfile - */ -@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) -public class Ikev2VpnProfileShimImpl - extends com.android.networkstack.apishim.api33.Ikev2VpnProfileShimImpl { - protected Ikev2VpnProfileShimImpl(Ikev2VpnProfile profile) { - super(profile); - } - /** - * Returns a new instance of this shim impl. - */ - @RequiresApi(Build.VERSION_CODES.R) - public static Ikev2VpnProfileShim<Ikev2VpnProfile> newInstance(Ikev2VpnProfile profile) { - if (SdkLevel.isAtLeastU()) { - return new Ikev2VpnProfileShimImpl(profile); - } else { - return com.android.networkstack.apishim.api33.Ikev2VpnProfileShimImpl - .newInstance(profile); - } - } - - /** - * @see Ikev2VpnProfile#isAutomaticIpVersionSelectionEnabled() - */ - public boolean isAutomaticIpVersionSelectionEnabled() { - return mProfile.isAutomaticIpVersionSelectionEnabled(); - } - - /** - * @see Ikev2VpnProfile#isAutomaticNattKeepaliveTimerEnabled() - */ - public boolean isAutomaticNattKeepaliveTimerEnabled() { - return mProfile.isAutomaticNattKeepaliveTimerEnabled(); - } -}
diff --git a/apishim/35/com/android/networkstack/apishim/Ikev2VpnProfileBuilderShimImpl.java b/apishim/35/com/android/networkstack/apishim/Ikev2VpnProfileBuilderShimImpl.java deleted file mode 100644 index 486063f..0000000 --- a/apishim/35/com/android/networkstack/apishim/Ikev2VpnProfileBuilderShimImpl.java +++ /dev/null
@@ -1,36 +0,0 @@ -/* - * Copyright (C) 2023 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 com.android.networkstack.apishim; - -import android.net.ipsec.ike.IkeTunnelConnectionParams; -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; - -/** - * A shim for Ikev2VpnProfile.Builder - */ -// TODO: when available in all active branches: @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) -@RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT) -public class Ikev2VpnProfileBuilderShimImpl - extends com.android.networkstack.apishim.api34.Ikev2VpnProfileBuilderShimImpl { - // Currently identical to the API 34 shim, so inherit everything - protected Ikev2VpnProfileBuilderShimImpl(@NonNull IkeTunnelConnectionParams params) { - super(params); - } -}
diff --git a/apishim/35/com/android/networkstack/apishim/Ikev2VpnProfileShimImpl.java b/apishim/35/com/android/networkstack/apishim/Ikev2VpnProfileShimImpl.java deleted file mode 100644 index bc40e16..0000000 --- a/apishim/35/com/android/networkstack/apishim/Ikev2VpnProfileShimImpl.java +++ /dev/null
@@ -1,35 +0,0 @@ -/* - * Copyright (C) 2023 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 com.android.networkstack.apishim; - -import android.net.Ikev2VpnProfile; -import android.os.Build; - -import androidx.annotation.RequiresApi; - -/** - * A shim for Ikev2VpnProfile - */ -// TODO: when available in all active branches: @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) -@RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT) -public class Ikev2VpnProfileShimImpl - extends com.android.networkstack.apishim.api34.Ikev2VpnProfileShimImpl { - // Currently identical to the API 34 shim, so inherit everything - protected Ikev2VpnProfileShimImpl(Ikev2VpnProfile profile) { - super(profile); - } -}
diff --git a/apishim/common/com/android/networkstack/apishim/common/Ikev2VpnProfileBuilderShim.java b/apishim/common/com/android/networkstack/apishim/common/Ikev2VpnProfileBuilderShim.java deleted file mode 100644 index 4232885..0000000 --- a/apishim/common/com/android/networkstack/apishim/common/Ikev2VpnProfileBuilderShim.java +++ /dev/null
@@ -1,148 +0,0 @@ -/* - * Copyright (C) 2023 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 com.android.networkstack.apishim.common; - -import android.net.ProxyInfo; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.util.List; - -/** - * A shim for Ikev2VpnProfile.Builder. - * - * T should extend Ikev2VpnProfile.Builder, but this can't be written here as that class is not - * available in API29. - * @param <T> type of builder, typically Ikev2VpnProfile.Builder - */ -// TODO : when API29 is no longer supported, remove the type argument -public interface Ikev2VpnProfileBuilderShim<T> { - /** - * @see Ikev2VpnProfile.Builder#setRequiresInternetValidation(boolean) - */ - default Ikev2VpnProfileBuilderShim<T> setRequiresInternetValidation( - boolean requiresInternetValidation) throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API 33"); - } - - /** - * @see Ikev2VpnProfile.Builder#setAuthPsk(byte[]) - */ - default Ikev2VpnProfileBuilderShim<T> setAuthPsk(@NonNull byte[] psk) - throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API 30"); - } - - /** - * @see Ikev2VpnProfile.Builder#setAuthUsernamePassword(String, String, X509Certificate) - */ - default Ikev2VpnProfileBuilderShim<T> setAuthUsernamePassword(@NonNull String user, - @NonNull String pass, @Nullable X509Certificate serverRootCa) - throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API 30"); - } - - /** - * @see Ikev2VpnProfile.Builder#setAuthDigitalSignature(X509Certificate, PrivateKey, - * X509Certificate) - */ - default Ikev2VpnProfileBuilderShim<T> setAuthDigitalSignature(@NonNull X509Certificate userCert, - @NonNull PrivateKey key, @Nullable X509Certificate serverRootCa) - throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API 30"); - } - - /** - * @see Ikev2VpnProfile.Builder#setBypassable(boolean) - */ - default Ikev2VpnProfileBuilderShim<T> setBypassable(boolean isBypassable) - throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API 30"); - } - - /** - * @see Ikev2VpnProfile.Builder#setProxy(ProxyInfo) - */ - default Ikev2VpnProfileBuilderShim<T> setProxy(@Nullable ProxyInfo proxy) - throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API 30"); - } - - /** - * @see Ikev2VpnProfile.Builder#setMaxMtu(int) - */ - default Ikev2VpnProfileBuilderShim<T> setMaxMtu(int mtu) throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API 30"); - } - - /** - * @see Ikev2VpnProfile.Builder#setMetered(boolean) - */ - default Ikev2VpnProfileBuilderShim<T> setMetered(boolean isMetered) - throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API 30"); - } - - /** - * @see Ikev2VpnProfile.Builder#setAllowedAlgorithms(List<String>) - */ - default Ikev2VpnProfileBuilderShim<T> setAllowedAlgorithms(@NonNull List<String> algorithmNames) - throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API 30"); - } - - /** - * @see Ikev2VpnProfile.Builder#setLocalRoutesExcluded(boolean) - */ - default Ikev2VpnProfileBuilderShim<T> setLocalRoutesExcluded(boolean excludeLocalRoutes) - throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API 33"); - } - - /** - * @see Ikev2VpnProfile.Builder#setAutomaticIpVersionSelectionEnabled(boolean) - */ - default Ikev2VpnProfileBuilderShim<T> setAutomaticIpVersionSelectionEnabled(boolean isEnabled) - throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API 34"); - } - - /** - * @see Ikev2VpnProfile.Builder#setAutomaticNattKeepaliveTimerEnabled(boolean) - */ - default Ikev2VpnProfileBuilderShim<T> setAutomaticNattKeepaliveTimerEnabled(boolean isEnabled) - throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API 34"); - } - - /** - * Get <T> type of builder, typically Ikev2VpnProfile.Builder - */ - default T getBuilder() throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API 30"); - } - - /** - * Build an Ikev2VpnProfileShim - */ - default Ikev2VpnProfileShim build() throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API 30"); - } -}
diff --git a/apishim/common/com/android/networkstack/apishim/common/Ikev2VpnProfileShim.java b/apishim/common/com/android/networkstack/apishim/common/Ikev2VpnProfileShim.java deleted file mode 100644 index ad0bdcd..0000000 --- a/apishim/common/com/android/networkstack/apishim/common/Ikev2VpnProfileShim.java +++ /dev/null
@@ -1,48 +0,0 @@ -/* - * Copyright (C) 2023 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 com.android.networkstack.apishim.common; - -/** - * A shim for Ikev2VpnProfile. - * - * T should extend Ikev2VpnProfile, but this can't be written here as that class is not - * available in API29. - * @param <T> type of profile, typically Ikev2VpnProfile - */ -// TODO : when API29 is no longer supported, remove the type argument -public interface Ikev2VpnProfileShim<T> { - /** - * @see Ikev2VpnProfile#isAutomaticNattKeepaliveTimerEnabled() - */ - default boolean isAutomaticNattKeepaliveTimerEnabled() throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API level 34."); - } - - /** - * @see Ikev2VpnProfile#isAutomaticIpVersionSelectionEnabled() - */ - default boolean isAutomaticIpVersionSelectionEnabled() throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API level 34."); - } - /** - * Return the <T> type of profile. - * TODO: remove when Q is no longer supported. - */ - default T getProfile() throws UnsupportedApiLevelException { - throw new UnsupportedApiLevelException("Only supported from API level 30."); - } -}
diff --git a/jni/network_stack_utils_jni.cpp b/jni/network_stack_utils_jni.cpp index b5d97be..6f47d7e 100644 --- a/jni/network_stack_utils_jni.cpp +++ b/jni/network_stack_utils_jni.cpp
@@ -42,15 +42,6 @@ constexpr const char NETWORKSTACKUTILS_PKG_NAME[] = "com/android/networkstack/util/NetworkStackUtils"; -static const uint32_t kEtherTypeOffset = offsetof(ether_header, ether_type); -static const uint32_t kEtherHeaderLen = sizeof(ether_header); -static const uint32_t kIPv4Protocol = kEtherHeaderLen + offsetof(iphdr, protocol); -static const uint32_t kIPv4FlagsOffset = kEtherHeaderLen + offsetof(iphdr, frag_off); -static const uint32_t kIPv6NextHeader = kEtherHeaderLen + offsetof(ip6_hdr, ip6_nxt); -static const uint32_t kIPv6PayloadStart = kEtherHeaderLen + sizeof(ip6_hdr); -static const uint32_t kICMPv6TypeOffset = kIPv6PayloadStart + offsetof(icmp6_hdr, icmp6_type); -static const uint32_t kUDPSrcPortIndirectOffset = kEtherHeaderLen + offsetof(udphdr, source); -static const uint32_t kUDPDstPortIndirectOffset = kEtherHeaderLen + offsetof(udphdr, dest); static const uint16_t kDhcpClientPort = 68; static bool checkLenAndCopy(JNIEnv* env, const jbyteArray& addr, int len, void* dst) { @@ -100,6 +91,8 @@ } } +// fd is a "socket(AF_PACKET, SOCK_RAW, ETH_P_IP)" +// which guarantees packets already have skb->protocol == htons(ETH_P_IP) static void network_stack_utils_attachDhcpFilter(JNIEnv *env, jclass clazz, jobject javaFd) { static sock_filter filter_code[] = { // Check the protocol is UDP. @@ -108,14 +101,13 @@ // Check this is not a fragment. BPF_LOAD_IPV4_BE16(frag_off), - BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, IP_MF | IP_OFFMASK, 0, 1), - BPF_REJECT, + BPF2_REJECT_IF_ANY_MASKED_BITS_SET(IP_MF | IP_OFFMASK), // Get the IP header length. - BPF_STMT(BPF_LDX | BPF_B | BPF_MSH, kEtherHeaderLen), + BPF_LOADX_NET_RELATIVE_IPV4_HLEN, // Check the destination port. - BPF_STMT(BPF_LD | BPF_H | BPF_IND, kUDPDstPortIndirectOffset), + BPF_LOAD_NETX_RELATIVE_DST_PORT, BPF2_REJECT_IF_NOT_EQUAL(kDhcpClientPort), BPF_ACCEPT, @@ -131,14 +123,18 @@ } } +// fd is a "socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6)" +// which guarantees packets already have skb->protocol == htons(ETH_P_IPV6) static void network_stack_utils_attachRaFilter(JNIEnv *env, jclass clazz, jobject javaFd) { static sock_filter filter_code[] = { + BPF_LOADX_CONSTANT_IPV6_HLEN, + // Check IPv6 Next Header is ICMPv6. BPF_LOAD_IPV6_U8(nexthdr), BPF2_REJECT_IF_NOT_EQUAL(IPPROTO_ICMPV6), // Check ICMPv6 type is Router Advertisement. - BPF_LOAD_NET_RELATIVE_U8(sizeof(ipv6hdr) + offsetof(icmp6_hdr, icmp6_type)), + BPF_LOAD_NETX_RELATIVE_ICMP_TYPE, BPF2_REJECT_IF_NOT_EQUAL(ND_ROUTER_ADVERT), BPF_ACCEPT, @@ -150,20 +146,14 @@ int fd = netjniutils::GetNativeFileDescriptor(env, javaFd); if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) { - jniThrowExceptionFmt(env, "java/net/SocketException", - "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno)); + jniThrowErrnoException(env, "setsockopt(SO_ATTACH_FILTER)", errno); } } // TODO: Move all this filter code into libnetutils. +// fd is a "socket(AF_PACKET, SOCK_RAW, ETH_P_ALL)" static void network_stack_utils_attachControlPacketFilter( - JNIEnv *env, jclass clazz, jobject javaFd, jint hardwareAddressType) { - if (hardwareAddressType != ARPHRD_ETHER) { - jniThrowExceptionFmt(env, "java/net/SocketException", - "attachControlPacketFilter only supports ARPHRD_ETHER"); - return; - } - + JNIEnv *env, jclass clazz, jobject javaFd) { // Capture all: // - ARPs // - DHCPv4 packets @@ -175,48 +165,50 @@ // '(ip and udp port 68)' or // '(icmp6 and ip6[40] >= 133 and ip6[40] <= 136)' static sock_filter filter_code[] = { - // Load the link layer next payload field. - BPF_STMT(BPF_LD | BPF_H | BPF_ABS, kEtherTypeOffset), + // Load the ethertype from skb->protocol + BPF_LOAD_SKB_PROTOCOL, // Accept all ARP. // TODO: Figure out how to better filter ARPs on noisy networks. - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ETHERTYPE_ARP, 16, 0), + BPF2_ACCEPT_IF_EQUAL(ETHERTYPE_ARP), - // If IPv4: - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ETHERTYPE_IP, 0, 9), + // If IPv4: (otherwise jump to the 'IPv6 ...' below) + BPF_JUMP_IF_NOT_EQUAL(ETHERTYPE_IP, 14), // Check the protocol is UDP. - BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kIPv4Protocol), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_UDP, 0, 14), + BPF_LOAD_IPV4_U8(protocol), + BPF2_REJECT_IF_NOT_EQUAL(IPPROTO_UDP), // Check this is not a fragment. - BPF_STMT(BPF_LD | BPF_H | BPF_ABS, kIPv4FlagsOffset), - BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, IP_OFFMASK, 12, 0), + BPF_LOAD_IPV4_BE16(frag_off), + BPF2_REJECT_IF_ANY_MASKED_BITS_SET(IP_MF | IP_OFFMASK), // Get the IP header length. - BPF_STMT(BPF_LDX | BPF_B | BPF_MSH, kEtherHeaderLen), + BPF_LOADX_NET_RELATIVE_IPV4_HLEN, // Check the source port. - BPF_STMT(BPF_LD | BPF_H | BPF_IND, kUDPSrcPortIndirectOffset), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, kDhcpClientPort, 8, 0), + BPF_LOAD_NETX_RELATIVE_SRC_PORT, + BPF2_ACCEPT_IF_EQUAL(kDhcpClientPort), // Check the destination port. - BPF_STMT(BPF_LD | BPF_H | BPF_IND, kUDPDstPortIndirectOffset), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, kDhcpClientPort, 6, 7), + BPF_LOAD_NETX_RELATIVE_DST_PORT, + BPF2_ACCEPT_IF_EQUAL(kDhcpClientPort), + + // Reject any other UDPv4 + BPF_REJECT, // IPv6 ... - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ETHERTYPE_IPV6, 0, 6), + BPF2_REJECT_IF_NOT_EQUAL(ETHERTYPE_IPV6), + // Assume standard, 40-byte, extension header-less ipv6 packet + BPF_LOADX_CONSTANT_IPV6_HLEN, // ... check IPv6 Next Header is ICMPv6 (ignore fragments), ... - BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kIPv6NextHeader), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_ICMPV6, 0, 4), + BPF_LOAD_IPV6_U8(nexthdr), + BPF2_REJECT_IF_NOT_EQUAL(IPPROTO_ICMPV6), // ... and check the ICMPv6 type is one of RS/RA/NS/NA. - BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kICMPv6TypeOffset), - BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, ND_ROUTER_SOLICIT, 0, 2), - BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, ND_NEIGHBOR_ADVERT, 1, 0), + BPF_LOAD_NETX_RELATIVE_ICMP_TYPE, + BPF3_REJECT_IF_NOT_IN_RANGE(ND_ROUTER_SOLICIT, ND_NEIGHBOR_ADVERT), BPF_ACCEPT, - - BPF_REJECT, }; static const sock_fprog filter = { sizeof(filter_code) / sizeof(filter_code[0]), @@ -225,8 +217,7 @@ int fd = netjniutils::GetNativeFileDescriptor(env, javaFd); if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) { - jniThrowExceptionFmt(env, "java/net/SocketException", - "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno)); + jniThrowErrnoException(env, "setsockopt(SO_ATTACH_FILTER)", errno); } } @@ -238,7 +229,7 @@ { "addArpEntry", "([B[BLjava/lang/String;Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_addArpEntry }, { "attachDhcpFilter", "(Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_attachDhcpFilter }, { "attachRaFilter", "(Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_attachRaFilter }, - { "attachControlPacketFilter", "(Ljava/io/FileDescriptor;I)V", (void*) network_stack_utils_attachControlPacketFilter }, + { "attachControlPacketFilter", "(Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_attachControlPacketFilter }, }; extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml index a590ee6..f254610 100644 --- a/res/values-sq/strings.xml +++ b/res/values-sq/strings.xml
@@ -21,6 +21,6 @@ <string name="notification_channel_name_network_venue_info" msgid="6526543187249265733">"Informacionet për vendin e rrjetit"</string> <string name="notification_channel_description_network_venue_info" msgid="5131499595382733605">"Njoftimet e shfaqura për të treguar se rrjeti ka një faqe me informacione për vendin"</string> <string name="connected" msgid="4563643884927480998">"U lidh"</string> - <string name="tap_for_info" msgid="6849746325626883711">"Lidhur / Trokit për të parë faqen e internetit"</string> + <string name="tap_for_info" msgid="6849746325626883711">"Lidhur / Trokit për të parë uebsajtin"</string> <string name="application_label" msgid="1322847171305285454">"Menaxheri i rrjetit"</string> </resources>
diff --git a/src/android/net/apf/AndroidPacketFilter.java b/src/android/net/apf/AndroidPacketFilter.java new file mode 100644 index 0000000..18c704e --- /dev/null +++ b/src/android/net/apf/AndroidPacketFilter.java
@@ -0,0 +1,80 @@ +/* + * Copyright (C) 2023 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.apf; + +import android.net.LinkProperties; +import android.net.NattKeepalivePacketDataParcelable; +import android.net.TcpKeepalivePacketDataParcelable; + +import com.android.internal.util.IndentingPrintWriter; + +/** + * The interface for AndroidPacketFilter + */ +public interface AndroidPacketFilter { + /** + * Update the LinkProperties that will be used by APF. + */ + void setLinkProperties(LinkProperties lp); + + /** + * Shutdown the APF. + */ + void shutdown(); + + /** + * Switch for the multicast filter. + * @param isEnabled if the multicast filter should be enabled or not. + */ + void setMulticastFilter(boolean isEnabled); + + /** + * Set the APF data snapshot. + */ + void setDataSnapshot(byte[] data); + + /** + * Add TCP keepalive ack packet filter. + * This will add a filter to drop acks to the keepalive packet passed as an argument. + * + * @param slot The index used to access the filter. + * @param sentKeepalivePacket The attributes of the sent keepalive packet. + */ + void addTcpKeepalivePacketFilter(int slot, + TcpKeepalivePacketDataParcelable sentKeepalivePacket); + + /** + * Add NAT-T keepalive packet filter. + * This will add a filter to drop NAT-T keepalive packet which is passed as an argument. + * + * @param slot The index used to access the filter. + * @param sentKeepalivePacket The attributes of the sent keepalive packet. + */ + void addNattKeepalivePacketFilter(int slot, + NattKeepalivePacketDataParcelable sentKeepalivePacket); + + /** + * Remove keepalive packet filter. + * + * @param slot The index used to access the filter. + */ + void removeKeepalivePacketFilter(int slot); + + /** + * Dump the status of APF. + */ + void dump(IndentingPrintWriter pw); +}
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java index c70ac73..41c6726 100644 --- a/src/android/net/apf/ApfFilter.java +++ b/src/android/net/apf/ApfFilter.java
@@ -25,6 +25,7 @@ import static android.system.OsConstants.IPPROTO_ICMPV6; import static android.system.OsConstants.IPPROTO_TCP; import static android.system.OsConstants.IPPROTO_UDP; +import static android.system.OsConstants.SOCK_CLOEXEC; import static android.system.OsConstants.SOCK_RAW; import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST; @@ -45,10 +46,6 @@ import android.net.apf.ApfGenerator.IllegalInstructionException; import android.net.apf.ApfGenerator.Register; import android.net.ip.IpClient.IpClientCallbacksWrapper; -import android.net.metrics.ApfProgramEvent; -import android.net.metrics.ApfStats; -import android.net.metrics.IpConnectivityLog; -import android.net.metrics.RaEvent; import android.os.PowerManager; import android.os.SystemClock; import android.system.ErrnoException; @@ -63,6 +60,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.HexDump; import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.TokenBucket; import com.android.net.module.util.CollectionUtils; import com.android.net.module.util.ConnectivityUtils; import com.android.net.module.util.InterfaceParams; @@ -104,7 +102,7 @@ * * @hide */ -public class ApfFilter { +public class ApfFilter implements AndroidPacketFilter { // Helper class for specifying functional filter parameters. public static class ApfConfiguration { @@ -113,16 +111,7 @@ public boolean ieee802_3Filter; public int[] ethTypeBlackList; public int minRdnssLifetimeSec; - } - - // Enums describing the outcome of receiving an RA packet. - private static enum ProcessRaResult { - MATCH, // Received RA matched a known RA - DROPPED, // Received RA ignored due to MAX_RAS - PARSE_ERROR, // Received RA could not be parsed - ZERO_LIFETIME, // Received RA had 0 lifetime - UPDATE_NEW_RA, // APF program updated for new RA - UPDATE_EXPIRY // APF program updated for expiry + public int acceptRaMinLft; } /** @@ -200,14 +189,6 @@ public class ReceiveThread extends Thread { private final byte[] mPacket = new byte[1514]; private final FileDescriptor mSocket; - private final long mStart = SystemClock.elapsedRealtime(); - - private int mReceivedRas = 0; - private int mMatchingRas = 0; - private int mDroppedRas = 0; - private int mParseErrors = 0; - private int mZeroLifetimeRas = 0; - private int mProgramUpdates = 0; private volatile boolean mStopped; @@ -227,59 +208,13 @@ while (!mStopped) { try { int length = Os.read(mSocket, mPacket, 0, mPacket.length); - updateStats(processRa(mPacket, length)); + processRa(mPacket, length); } catch (IOException|ErrnoException e) { if (!mStopped) { Log.e(TAG, "Read error", e); } } } - logStats(); - } - - private void updateStats(ProcessRaResult result) { - mReceivedRas++; - switch(result) { - case MATCH: - mMatchingRas++; - return; - case DROPPED: - mDroppedRas++; - return; - case PARSE_ERROR: - mParseErrors++; - return; - case ZERO_LIFETIME: - mZeroLifetimeRas++; - return; - case UPDATE_EXPIRY: - mMatchingRas++; - mProgramUpdates++; - return; - case UPDATE_NEW_RA: - mProgramUpdates++; - return; - } - } - - private void logStats() { - final long nowMs = SystemClock.elapsedRealtime(); - synchronized (this) { - final ApfStats stats = new ApfStats.Builder() - .setReceivedRas(mReceivedRas) - .setMatchingRas(mMatchingRas) - .setDroppedRas(mDroppedRas) - .setParseErrors(mParseErrors) - .setZeroLifetimeRas(mZeroLifetimeRas) - .setProgramUpdates(mProgramUpdates) - .setDurationMs(nowMs - mStart) - .setMaxProgramSize(mApfCapabilities.maximumApfProgramSize) - .setProgramUpdatesAll(mNumProgramUpdates) - .setProgramUpdatesAllowingMulticast(mNumProgramUpdatesAllowingMulticast) - .build(); - mMetricsLog.log(stats); - logApfProgramEventLocked(nowMs / DateUtils.SECOND_IN_MILLIS); - } } } @@ -344,8 +279,6 @@ private static final short ARP_OPCODE_REPLY = 2; private static final int ARP_SOURCE_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 14; private static final int ARP_TARGET_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 24; - // Do not log ApfProgramEvents whose actual lifetimes was less than this. - private static final int APF_PROGRAM_EVENT_LIFETIME_THRESHOLD = 2; // Limit on the Black List size to cap on program usage for this // TODO: Select a proper max length private static final int APF_MAX_ETH_TYPE_BLACK_LIST_LEN = 20; @@ -369,7 +302,7 @@ private final ApfCapabilities mApfCapabilities; private final IpClientCallbacksWrapper mIpClientCallback; private final InterfaceParams mInterfaceParams; - private final IpConnectivityLog mMetricsLog; + private final TokenBucket mTokenBucket; @VisibleForTesting public byte[] mHardwareAddress; @@ -387,6 +320,10 @@ // Ignore non-zero RDNSS lifetimes below this value. private final int mMinRdnssLifetimeSec; + // Tracks the value of /proc/sys/ipv6/conf/$iface/accept_ra_min_lft which affects router, RIO, + // and PIO valid lifetimes. + private final int mAcceptRaMinLft; + // Detects doze mode state transitions. private final BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() { @Override @@ -411,13 +348,14 @@ @VisibleForTesting public ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams, - IpClientCallbacksWrapper ipClientCallback, IpConnectivityLog log) { + IpClientCallbacksWrapper ipClientCallback) { mApfCapabilities = config.apfCapabilities; mIpClientCallback = ipClientCallback; mInterfaceParams = ifParams; mMulticastFilter = config.multicastFilter; mDrop802_3Frames = config.ieee802_3Filter; mMinRdnssLifetimeSec = config.minRdnssLifetimeSec; + mAcceptRaMinLft = config.acceptRaMinLft; mContext = context; if (mApfCapabilities.hasDataAccess()) { @@ -433,7 +371,14 @@ // Now fill the black list from the passed array mEthTypeBlackList = filterEthTypeBlackList(config.ethTypeBlackList); - mMetricsLog = log; + // TokenBucket for rate limiting filter installation. APF filtering relies on the filter + // always being up-to-date and APF bytecode being in sync with userspace. The TokenBucket + // merely prevents illconfigured / abusive networks from impacting the system, so it does + // not need to be very restrictive. + // The TokenBucket starts with its full capacity of 20 tokens (= 20 filter updates). A new + // token is generated every 3 seconds limiting the filter update rate to at most once every + // 3 seconds. + mTokenBucket = new TokenBucket(3_000 /* deltaMs */, 20 /* capacity */, 20 /* tokens */); // TODO: ApfFilter should not generate programs until IpClient sends provisioning success. maybeStartFilter(); @@ -506,10 +451,10 @@ // Install basic filters installNewProgramLocked(); } - socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6); + socket = Os.socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC, 0); + NetworkStackUtils.attachRaFilter(socket); SocketAddress addr = makePacketSocketAddress(ETH_P_IPV6, mInterfaceParams.index); Os.bind(socket, addr); - NetworkStackUtils.attachRaFilter(socket); } catch(SocketException|ErrnoException e) { Log.e(TAG, "Error starting filter", e); return; @@ -520,8 +465,8 @@ // Returns seconds since device boot. @VisibleForTesting - protected long currentTimeSeconds() { - return SystemClock.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS; + protected int secondsSinceBoot() { + return (int) (SystemClock.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS); } public static class InvalidRaException extends Exception { @@ -536,8 +481,7 @@ private static class PacketSection { public enum Type { MATCH, // A field that should be matched (e.g., the router IP address). - IGNORE, // An ignored field such as the checksum of the flow label. Not matched. - LIFETIME, // A lifetime. Not matched, and generally counts toward minimum RA lifetime. + LIFETIME, // A lifetime. Not matched, and counts toward minimum RA lifetime if >= min. } /** The type of section. */ @@ -546,22 +490,35 @@ public final int start; /** Length of this section in bytes. */ public final int length; - /** If this is a lifetime, the ICMP option that defined it. 0 for router lifetime. */ - public final int option; /** If this is a lifetime, the lifetime value. */ public final long lifetime; + /** If this is a lifetime, the value below which the lifetime is ignored */ + public final int min; - PacketSection(int start, int length, Type type, int option, long lifetime) { + PacketSection(int start, int length, Type type, long lifetime, int min) { this.start = start; + + if (type == Type.LIFETIME && length != 2 && length != 4) { + throw new IllegalArgumentException("LIFETIME section length must be 2 or 4 bytes"); + } this.length = length; this.type = type; - this.option = option; + + if (type == Type.MATCH && (lifetime != 0 || min != 0)) { + throw new IllegalArgumentException("lifetime, min must be 0 for MATCH sections"); + } this.lifetime = lifetime; + + // It has already been asserted that min is 0 for MATCH sections. + if (min < 0) { + throw new IllegalArgumentException("min must be >= 0 for LIFETIME sections"); + } + this.min = min; } public String toString() { if (type == Type.LIFETIME) { - return String.format("%s: (%d, %d) %d %d", type, start, length, option, lifetime); + return String.format("%s: (%d, %d) %d %d", type, start, length, lifetime, min); } else { return String.format("%s: (%d, %d)", type, start, length); } @@ -589,10 +546,18 @@ private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET = 8; private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN = 4; + // From RFC4861: source link-layer address + private static final int ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE = 1; + // From RFC4861: mtu size option + private static final int ICMP6_MTU_OPTION_TYPE = 5; // From RFC6106: Recursive DNS Server option private static final int ICMP6_RDNSS_OPTION_TYPE = 25; // From RFC6106: DNS Search List option private static final int ICMP6_DNSSL_OPTION_TYPE = 31; + // From RFC8910: Captive-Portal option + private static final int ICMP6_CAPTIVE_PORTAL_OPTION_TYPE = 37; + // From RFC8781: PREF64 option + private static final int ICMP6_PREF64_OPTION_TYPE = 38; // From RFC4191: Route Information option private static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24; @@ -607,9 +572,9 @@ private final ArrayList<PacketSection> mPacketSections = new ArrayList<>(); // Minimum lifetime in packet - long mMinLifetime; + private final int mMinLifetime; // When the packet was last captured, in seconds since Unix Epoch - long mLastSeen; + private final int mLastSeen; // For debugging only. Offsets into the packet where PIOs are. private final ArrayList<Integer> mPrefixOptionOffsets = new ArrayList<>(); @@ -620,9 +585,6 @@ // For debugging only. Offsets into the packet where RIO options are. private final ArrayList<Integer> mRioOptionOffsets = new ArrayList<>(); - // For debugging only. How many times this RA was seen. - int seenCount = 0; - // For debugging only. Returns the hex representation of the last matching packet. String getLastMatchingPacket() { return HexDump.toHexString(mPacket.array(), 0, mPacket.capacity(), @@ -728,9 +690,25 @@ // check to prevent doing so in the presence of bugs or malformed or // truncated packets. if (length == 0) return; - mPacketSections.add( - new PacketSection(mPacket.position(), length, PacketSection.Type.MATCH, 0, 0)); - mPacket.position(mPacket.position() + length); + + // we need to add a MATCH section 'from, length, MATCH, 0, 0' + int from = mPacket.position(); + + // if possible try to increase the length of the previous match section + int lastIdx = mPacketSections.size() - 1; + if (lastIdx >= 0) { // there had to be a previous section + PacketSection prev = mPacketSections.get(lastIdx); + if (prev.type == PacketSection.Type.MATCH) { // of type match + if (prev.start + prev.length == from) { // ending where we start + from -= prev.length; + length += prev.length; + mPacketSections.remove(lastIdx); + } + } + } + + mPacketSections.add(new PacketSection(from, length, PacketSection.Type.MATCH, 0, 0)); + mPacket.position(from + length); } /** @@ -746,51 +724,36 @@ * @param length the length of the section in bytes */ private void addIgnoreSection(int length) { - mPacketSections.add( - new PacketSection(mPacket.position(), length, PacketSection.Type.IGNORE, 0, 0)); mPacket.position(mPacket.position() + length); } /** * Add a packet section that represents a lifetime, starting from the current position. * @param length the length of the section in bytes - * @param optionType the RA option containing this lifetime, or 0 for router lifetime * @param lifetime the lifetime + * @param min the minimum acceptable lifetime */ - private void addLifetimeSection(int length, int optionType, long lifetime) { + private void addLifetimeSection(int length, long lifetime, int min) { mPacketSections.add( new PacketSection(mPacket.position(), length, PacketSection.Type.LIFETIME, - optionType, lifetime)); + lifetime, min)); mPacket.position(mPacket.position() + length); } /** * Adds packet sections for an RA option with a 4-byte lifetime 4 bytes into the option - * @param optionType the RA option that is being added * @param optionLength the length of the option in bytes + * @param min the minimum acceptable lifetime */ - private long add4ByteLifetimeOption(int optionType, int optionLength) { + private long add4ByteLifetimeOption(int optionLength, int min) { addMatchSection(ICMP6_4_BYTE_LIFETIME_OFFSET); final long lifetime = getUint32(mPacket, mPacket.position()); - addLifetimeSection(ICMP6_4_BYTE_LIFETIME_LEN, optionType, lifetime); + addLifetimeSection(ICMP6_4_BYTE_LIFETIME_LEN, lifetime, min); addMatchSection(optionLength - ICMP6_4_BYTE_LIFETIME_OFFSET - ICMP6_4_BYTE_LIFETIME_LEN); return lifetime; } - // http://b/66928272 http://b/65056012 - // DnsServerRepository ignores RDNSS servers with lifetimes that are too low. Ignore these - // lifetimes for the purpose of filter lifetime calculations. - private boolean shouldIgnoreLifetime(int optionType, long lifetime) { - return optionType == ICMP6_RDNSS_OPTION_TYPE - && lifetime != 0 && lifetime < mMinRdnssLifetimeSec; - } - - private boolean isRelevantLifetime(PacketSection section) { - return section.type == PacketSection.Type.LIFETIME - && !shouldIgnoreLifetime(section.option, section.lifetime); - } - // Note that this parses RA and may throw InvalidRaException (from // Buffer.position(int) or due to an invalid-length option) or IndexOutOfBoundsException // (from ByteBuffer.get(int) ) if parsing encounters something non-compliant with @@ -802,7 +765,7 @@ } mPacket = ByteBuffer.wrap(Arrays.copyOf(packet, length)); - mLastSeen = currentTimeSeconds(); + mLastSeen = secondsSinceBoot(); // Check packet in case a packet arrives before we attach RA filter // to our packet socket. b/29586253 @@ -812,13 +775,14 @@ throw new InvalidRaException("Not an ICMP6 router advertisement"); } - - RaEvent.Builder builder = new RaEvent.Builder(); - // Ignore the flow label and low 4 bits of traffic class. addMatchUntil(IPV6_FLOW_LABEL_OFFSET); addIgnoreSection(IPV6_FLOW_LABEL_LEN); + // Ignore IPv6 destination address. + addMatchUntil(IPV6_DEST_ADDR_OFFSET); + addIgnoreSection(IPV6_ADDR_LEN); + // Ignore checksum. addMatchUntil(ICMP6_RA_CHECKSUM_OFFSET); addIgnoreSection(ICMP6_RA_CHECKSUM_LEN); @@ -826,8 +790,7 @@ // Parse router lifetime addMatchUntil(ICMP6_RA_ROUTER_LIFETIME_OFFSET); final long routerLifetime = getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET); - addLifetimeSection(ICMP6_RA_ROUTER_LIFETIME_LEN, 0, routerLifetime); - builder.updateRouterLifetime(routerLifetime); + addLifetimeSection(ICMP6_RA_ROUTER_LIFETIME_LEN, routerLifetime, mAcceptRaMinLft); // Add remaining fields (reachable time and retransmission timer) to match section. addMatchUntil(ICMP6_RA_OPTION_OFFSET); @@ -850,14 +813,14 @@ addMatchSection(ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET); lifetime = getUint32(mPacket, mPacket.position()); addLifetimeSection(ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN, - ICMP6_PREFIX_OPTION_TYPE, lifetime); - builder.updatePrefixValidLifetime(lifetime); + lifetime, mAcceptRaMinLft); // Parse preferred lifetime lifetime = getUint32(mPacket, mPacket.position()); + // The PIO preferred lifetime is not affected by accept_ra_min_lft and + // therefore does not have a minimum. addLifetimeSection(ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN, - ICMP6_PREFIX_OPTION_TYPE, lifetime); - builder.updatePrefixPreferredLifetime(lifetime); + lifetime, 0 /* min lifetime */); addMatchSection(4); // Reserved bytes addMatchSection(IPV6_ADDR_LEN); // The prefix itself @@ -866,18 +829,19 @@ // are processed with the same specialized add4ByteLifetimeOption: case ICMP6_RDNSS_OPTION_TYPE: mRdnssOptionOffsets.add(position); - lifetime = add4ByteLifetimeOption(optionType, optionLength); - builder.updateRdnssLifetime(lifetime); + lifetime = add4ByteLifetimeOption(optionLength, mMinRdnssLifetimeSec); break; case ICMP6_ROUTE_INFO_OPTION_TYPE: mRioOptionOffsets.add(position); - lifetime = add4ByteLifetimeOption(optionType, optionLength); - builder.updateRouteInfoLifetime(lifetime); + lifetime = add4ByteLifetimeOption(optionLength, mAcceptRaMinLft); break; - case ICMP6_DNSSL_OPTION_TYPE: - lifetime = add4ByteLifetimeOption(optionType, optionLength); - builder.updateDnsslLifetime(lifetime); + case ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE: + case ICMP6_MTU_OPTION_TYPE: + case ICMP6_PREF64_OPTION_TYPE: + addMatchSection(optionLength); break; + case ICMP6_CAPTIVE_PORTAL_OPTION_TYPE: // unlikely to ever change. + case ICMP6_DNSSL_OPTION_TYPE: // currently unsupported in userspace. default: // RFC4861 section 4.2 dictates we ignore unknown options for forwards // compatibility. @@ -889,73 +853,161 @@ } } mMinLifetime = minLifetime(); - mMetricsLog.log(builder.build()); + } + + public enum MatchType { + NO_MATCH, // the RAs do not match + MATCH_PASS, // the RAS match, and the APF program would pass. + MATCH_DROP, // the RAs match, but the APF program would drop. } // Considering only the MATCH sections, does {@code packet} match this RA? - boolean matches(byte[] packet, int length) { - if (length != mPacket.capacity()) return false; - byte[] referencePacket = mPacket.array(); + MatchType matches(Ra newRa) { + // Does their size match? + if (newRa.mPacket.capacity() != mPacket.capacity()) return MatchType.NO_MATCH; + + // If the filter has expired, it cannot match the new RA. + if (getRemainingFilterLft(secondsSinceBoot()) <= 0) return MatchType.NO_MATCH; + + // Check if all MATCH sections are byte-identical. + final byte[] newPacket = newRa.mPacket.array(); + final byte[] oldPacket = mPacket.array(); for (PacketSection section : mPacketSections) { if (section.type != PacketSection.Type.MATCH) continue; for (int i = section.start; i < (section.start + section.length); i++) { - if (packet[i] != referencePacket[i]) return false; + if (newPacket[i] != oldPacket[i]) return MatchType.NO_MATCH; } } - return true; + + // Apply APF lifetime matching to LIFETIME sections and decide whether a packet should + // be processed (MATCH_PASS) or ignored (MATCH_DROP). This logic is needed to + // consistently process / ignore packets no matter the current state of the APF program. + // Note that userspace has no control (or knowledge) over when the APF program is + // running. + for (PacketSection section : mPacketSections) { + if (section.type != PacketSection.Type.LIFETIME) continue; + + // the lifetime of the new RA. + long lft = 0; + switch (section.length) { + // section.length is guaranteed to be 2 or 4. + case 2: lft = getUint16(newRa.mPacket, section.start); break; + case 4: lft = getUint32(newRa.mPacket, section.start); break; + } + + // WARNING: keep this in sync with Ra#generateFilterLocked()! + if (section.lifetime == 0) { + // Case 1) old lft == 0 + if (section.min > 0) { + // a) in the presence of a min value. + // if lft >= min -> PASS + // gen.addJumpIfR0GreaterThan(section.min - 1, nextFilterLabel); + if (lft >= section.min) return MatchType.MATCH_PASS; + } else { + // b) if min is 0 / there is no min value. + // if lft > 0 -> PASS + // gen.addJumpIfR0GreaterThan(0, nextFilterLabel); + if (lft > 0) return MatchType.MATCH_PASS; + } + } else if (section.min == 0) { + // Case 2b) section is not affected by any minimum. + // + // if lft < (oldLft + 2) // 3 -> PASS + // if lft > oldLft -> PASS + // gen.addJumpIfR0LessThan((int) ((section.lifetime + 2) / 3), + // nextFilterLabel); + if (lft < (section.lifetime + 2) / 3) return MatchType.MATCH_PASS; + // gen.addJumpIfR0GreaterThan((int) section.lifetime, nextFilterLabel); + if (lft > section.lifetime) return MatchType.MATCH_PASS; + } else if (section.lifetime < section.min) { + // Case 2a) 0 < old lft < min + // + // if lft == 0 -> PASS + // if lft >= min -> PASS + // gen.addJumpIfR0Equals(0, nextFilterLabel); + if (lft == 0) return MatchType.MATCH_PASS; + // gen.addJumpIfR0GreaterThan(section.min - 1, nextFilterLabel); + if (lft >= section.min) return MatchType.MATCH_PASS; + } else if (section.lifetime <= 3 * (long) section.min) { + // Case 3a) min <= old lft <= 3 * min + // Note that: + // "(old lft + 2) / 3 <= min" is equivalent to "old lft <= 3 * min" + // + // Essentially, in this range there is no "renumbering support", as the + // renumbering constant of 1/3 * old lft is smaller than the minimum + // lifetime accepted by the kernel / userspace. + // + // if lft == 0 -> PASS + // if lft > oldLft -> PASS + // gen.addJumpIfR0Equals(0, nextFilterLabel); + if (lft == 0) return MatchType.MATCH_PASS; + // gen.addJumpIfR0GreaterThan((int) section.lifetime, nextFilterLabel); + if (lft > section.lifetime) return MatchType.MATCH_PASS; + } else { + // Case 4a) otherwise + // + // if lft == 0 -> PASS + // if lft < min -> CONTINUE + // if lft < (oldLft + 2) // 3 -> PASS + // if lft > oldLft -> PASS + // gen.addJumpIfR0Equals(0, nextFilterLabel); + if (lft == 0) return MatchType.MATCH_PASS; + // gen.addJumpIfR0LessThan(section.min, continueLabel); + if (lft < section.min) continue; + // gen.addJumpIfR0LessThan((int) ((section.lifetime + 2) / 3), + // nextFilterLabel); + if (lft < (section.lifetime + 2) / 3) return MatchType.MATCH_PASS; + // gen.addJumpIfR0GreaterThan((int) section.lifetime, nextFilterLabel); + if (lft > section.lifetime) return MatchType.MATCH_PASS; + } + } + + return MatchType.MATCH_DROP; } // What is the minimum of all lifetimes within {@code packet} in seconds? // Precondition: matches(packet, length) already returned true. - long minLifetime() { - long minLifetime = Long.MAX_VALUE; + private int minLifetime() { + // While technically most lifetimes in the RA are u32s, as far as the RA filter is + // concerned, INT_MAX is still a *much* longer lifetime than any filter would ever + // reasonably be active for. + // Clamp minLifetime at INT_MAX. + int minLifetime = Integer.MAX_VALUE; for (PacketSection section : mPacketSections) { - if (isRelevantLifetime(section)) { - minLifetime = Math.min(minLifetime, section.lifetime); + if (section.type != PacketSection.Type.LIFETIME) { + continue; } + // Ignore lifetimes below section.min and always ignore 0 lifetimes. + if (section.lifetime < Math.max(section.min, 1)) { + continue; + } + + minLifetime = (int) Math.min(minLifetime, section.lifetime); } return minLifetime; } - // How many seconds does this RA's have to live, taking into account the fact - // that we might have seen it a while ago. - long currentLifetime() { - return mMinLifetime - (currentTimeSeconds() - mLastSeen); - } - - boolean isExpired() { - // TODO: We may want to handle 0 lifetime RAs differently, if they are common. We'll - // have to calculate the filter lifetime specially as a fraction of 0 is still 0. - return currentLifetime() <= 0; - } - // Filter for a fraction of the lifetime and adjust for the age of the RA. - @GuardedBy("ApfFilter.this") - int filterLifetime() { - return (int) (mMinLifetime / FRACTION_OF_LIFETIME_TO_FILTER) - - (int) (mProgramBaseTime - mLastSeen); - } - - @GuardedBy("ApfFilter.this") - boolean shouldFilter() { - return filterLifetime() > 0; + int getRemainingFilterLft(int currentTimeSeconds) { + int filterLifetime = (int) ((mMinLifetime / FRACTION_OF_LIFETIME_TO_FILTER) + - (currentTimeSeconds - mLastSeen)); + filterLifetime = Math.max(0, filterLifetime); + // Clamp filterLifetime to <= 65535, so it fits in 2 bytes. + return Math.min(65535, filterLifetime); } // Append a filter for this RA to {@code gen}. Jump to DROP_LABEL if it should be dropped. // Jump to the next filter if packet doesn't match this RA. - // Return Long.MAX_VALUE if we don't install any filter program for this RA. As the return - // value of this function is used to calculate the program min lifetime (which corresponds - // to the smallest generated filter lifetime). Returning Long.MAX_VALUE in the case no - // filter gets generated makes sure the program lifetime stays unaffected. @GuardedBy("ApfFilter.this") - long generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException { + void generateFilterLocked(ApfGenerator gen, int timeSeconds) + throws IllegalInstructionException { String nextFilterLabel = "Ra" + getUniqueNumberLocked(); // Skip if packet is not the right size gen.addLoadFromMemory(Register.R0, gen.PACKET_SIZE_MEMORY_SLOT); gen.addJumpIfR0NotEquals(mPacket.capacity(), nextFilterLabel); // Skip filter if expired gen.addLoadFromMemory(Register.R0, gen.FILTER_AGE_MEMORY_SLOT); - gen.addJumpIfR0GreaterThan(filterLifetime(), nextFilterLabel); + gen.addJumpIfR0GreaterThan(getRemainingFilterLft(timeSeconds), nextFilterLabel); for (PacketSection section : mPacketSections) { // Generate code to match the packet bytes. if (section.type == PacketSection.Type.MATCH) { @@ -964,25 +1016,77 @@ Arrays.copyOfRange(mPacket.array(), section.start, section.start + section.length), nextFilterLabel); - } - - // Generate code to test the lifetimes haven't gone down too far. - // The packet is accepted if any non-ignored lifetime is lower than filterLifetime. - if (isRelevantLifetime(section)) { + } else { switch (section.length) { - case 4: gen.addLoad32(Register.R0, section.start); break; + // length asserted to be either 2 or 4 on PacketSection construction case 2: gen.addLoad16(Register.R0, section.start); break; - default: - throw new IllegalStateException( - "bogus lifetime size " + section.length); + case 4: gen.addLoad32(Register.R0, section.start); break; } - gen.addJumpIfR0LessThan(filterLifetime(), nextFilterLabel); + + // WARNING: keep this in sync with matches()! + // For more information on lifetime comparisons in the APF bytecode, see + // go/apf-ra-filter. + if (section.lifetime == 0) { + // Case 1) old lft == 0 + if (section.min > 0) { + // a) in the presence of a min value. + // if lft >= min -> PASS + gen.addJumpIfR0GreaterThan(section.min - 1, nextFilterLabel); + } else { + // b) if min is 0 / there is no min value. + // if lft > 0 -> PASS + gen.addJumpIfR0GreaterThan(0, nextFilterLabel); + } + } else if (section.min == 0) { + // Case 2b) section is not affected by any minimum. + // + // if lft < (oldLft + 2) // 3 -> PASS + // if lft > oldLft -> PASS + gen.addJumpIfR0LessThan((int) ((section.lifetime + 2) / 3), + nextFilterLabel); + gen.addJumpIfR0GreaterThan((int) section.lifetime, nextFilterLabel); + } else if (section.lifetime < section.min) { + // Case 2a) 0 < old lft < min + // + // if lft == 0 -> PASS + // if lft >= min -> PASS + gen.addJumpIfR0Equals(0, nextFilterLabel); + gen.addJumpIfR0GreaterThan(section.min - 1, nextFilterLabel); + } else if (section.lifetime <= 3 * (long) section.min) { + // Case 3a) min <= old lft <= 3 * min + // Note that: + // "(old lft + 2) / 3 <= min" is equivalent to "old lft <= 3 * min" + // + // Essentially, in this range there is no "renumbering support", as the + // renumbering constant of 1/3 * old lft is smaller than the minimum + // lifetime accepted by the kernel / userspace. + // + // if lft == 0 -> PASS + // if lft > oldLft -> PASS + gen.addJumpIfR0Equals(0, nextFilterLabel); + gen.addJumpIfR0GreaterThan((int) section.lifetime, nextFilterLabel); + } else { + final String continueLabel = "Continue" + getUniqueNumberLocked(); + // Case 4a) otherwise + // + // if lft == 0 -> PASS + // if lft < min -> CONTINUE + // if lft < (oldLft + 2) // 3 -> PASS + // if lft > oldLft -> PASS + gen.addJumpIfR0Equals(0, nextFilterLabel); + gen.addJumpIfR0LessThan(section.min, continueLabel); + gen.addJumpIfR0LessThan((int) ((section.lifetime + 2) / 3), + nextFilterLabel); + gen.addJumpIfR0GreaterThan((int) section.lifetime, nextFilterLabel); + + // CONTINUE + gen.defineLabel(continueLabel); + } } } maybeSetupCounter(gen, Counter.DROPPED_RA); gen.addJump(mCountAndDropLabel); gen.defineLabel(nextFilterLabel); - return filterLifetime(); } } @@ -1212,29 +1316,17 @@ @GuardedBy("this") private final List<String[]> mMdnsAllowList = new ArrayList<>(); - // There is always some marginal benefit to updating the installed APF program when an RA is - // seen because we can extend the program's lifetime slightly, but there is some cost to - // updating the program, so don't bother unless the program is going to expire soon. This - // constant defines "soon" in seconds. - private static final long MAX_PROGRAM_LIFETIME_WORTH_REFRESHING = 30; // We don't want to filter an RA for it's whole lifetime as it'll be expired by the time we ever // see a refresh. Using half the lifetime might be a good idea except for the fact that // packets may be dropped, so let's use 6. private static final int FRACTION_OF_LIFETIME_TO_FILTER = 6; - // The base time for this filter program. In seconds since Unix Epoch. - // This is the time when the APF program was generated. All filters in the program should use - // this base time as their current time for consistency purposes. - @GuardedBy("this") - private long mProgramBaseTime; // When did we last install a filter program? In seconds since Unix Epoch. @GuardedBy("this") - private long mLastTimeInstalledProgram; + private int mLastTimeInstalledProgram; // How long should the last installed filter program live for? In seconds. @GuardedBy("this") - private long mLastInstalledProgramMinLifetime; - @GuardedBy("this") - private ApfProgramEvent.Builder mLastInstallEvent; + private int mLastInstalledProgramMinLifetime; // For debugging only. The last program installed. @GuardedBy("this") @@ -1798,17 +1890,17 @@ @GuardedBy("this") @VisibleForTesting public void installNewProgramLocked() { - purgeExpiredRasLocked(); ArrayList<Ra> rasToFilter = new ArrayList<>(); final byte[] program; - long programMinLifetime = Long.MAX_VALUE; - long maximumApfProgramSize = mApfCapabilities.maximumApfProgramSize; + int programMinLft = Integer.MAX_VALUE; + int maximumApfProgramSize = mApfCapabilities.maximumApfProgramSize; if (mApfCapabilities.hasDataAccess()) { // Reserve space for the counters. maximumApfProgramSize -= Counter.totalSize(); } - mProgramBaseTime = currentTimeSeconds(); + // Ensure the entire APF program uses the same time base. + int timeSeconds = secondsSinceBoot(); try { // Step 1: Determine how many RA filters we can fit in the program. ApfGenerator gen = emitPrologueLocked(); @@ -1824,8 +1916,9 @@ } for (Ra ra : mRas) { - if (!ra.shouldFilter()) continue; - ra.generateFilterLocked(gen); + // skip filter if it has expired. + if (ra.getRemainingFilterLft(timeSeconds) <= 0) continue; + ra.generateFilterLocked(gen, timeSeconds); // Stop if we get too big. if (gen.programLengthOverEstimate() > maximumApfProgramSize) { if (VDBG) Log.d(TAG, "Past maximum program size, skipping RAs"); @@ -1838,7 +1931,8 @@ // Step 2: Actually generate the program gen = emitPrologueLocked(); for (Ra ra : rasToFilter) { - programMinLifetime = Math.min(programMinLifetime, ra.generateFilterLocked(gen)); + ra.generateFilterLocked(gen, timeSeconds); + programMinLft = Math.min(programMinLft, ra.getRemainingFilterLft(timeSeconds)); } emitEpilogue(gen); program = gen.generate(); @@ -1847,79 +1941,48 @@ return; } mIpClientCallback.installPacketFilter(program); - mLastTimeInstalledProgram = mProgramBaseTime; - mLastInstalledProgramMinLifetime = programMinLifetime; + mLastTimeInstalledProgram = timeSeconds; + mLastInstalledProgramMinLifetime = programMinLft; mLastInstalledProgram = program; mNumProgramUpdates++; if (VDBG) { hexDump("Installing filter: ", program, program.length); } - logApfProgramEventLocked(mProgramBaseTime); - mLastInstallEvent = new ApfProgramEvent.Builder() - .setLifetime(programMinLifetime) - .setFilteredRas(rasToFilter.size()) - .setCurrentRas(mRas.size()) - .setProgramLength(program.length) - .setFlags(mIPv4Address != null, mMulticastFilter); - } - - @GuardedBy("this") - private void logApfProgramEventLocked(long now) { - if (mLastInstallEvent == null) { - return; - } - ApfProgramEvent.Builder ev = mLastInstallEvent; - mLastInstallEvent = null; - final long actualLifetime = now - mLastTimeInstalledProgram; - ev.setActualLifetime(actualLifetime); - if (actualLifetime < APF_PROGRAM_EVENT_LIFETIME_THRESHOLD) { - return; - } - mMetricsLog.log(ev.build()); - } - - /** - * Returns {@code true} if a new program should be installed because the current one dies soon. - */ - private boolean shouldInstallnewProgram() { - long expiry = mLastTimeInstalledProgram + mLastInstalledProgramMinLifetime; - return expiry < currentTimeSeconds() + MAX_PROGRAM_LIFETIME_WORTH_REFRESHING; } private void hexDump(String msg, byte[] packet, int length) { log(msg + HexDump.toHexString(packet, 0, length, false /* lowercase */)); } - @GuardedBy("this") - private void purgeExpiredRasLocked() { - for (int i = 0; i < mRas.size();) { - if (mRas.get(i).isExpired()) { - log("Expiring " + mRas.get(i)); - mRas.remove(i); - } else { - i++; - } - } - } - /** * Process an RA packet, updating the list of known RAs and installing a new APF program * if the current APF program should be updated. - * @return a ProcessRaResult enum describing what action was performed. */ @VisibleForTesting - public synchronized ProcessRaResult processRa(byte[] packet, int length) { + public synchronized void processRa(byte[] packet, int length) { if (VDBG) hexDump("Read packet = ", packet, length); + final Ra ra; + try { + ra = new Ra(packet, length); + } catch (Exception e) { + Log.e(TAG, "Error parsing RA", e); + return; + } + + // Remove all expired RA filters before trying to match the new RA. + // TODO: matches() still checks that the old RA filter has not expired. Consider removing + // that check. + final int now = secondsSinceBoot(); + mRas.removeIf(item -> item.getRemainingFilterLft(now) <= 0); + // Have we seen this RA before? for (int i = 0; i < mRas.size(); i++) { - Ra ra = mRas.get(i); - if (ra.matches(packet, length)) { - if (VDBG) log("matched RA " + ra); - // Update lifetimes. - ra.mLastSeen = currentTimeSeconds(); - ra.seenCount++; + final Ra oldRa = mRas.get(i); + final Ra.MatchType result = oldRa.matches(ra); + if (result == Ra.MatchType.MATCH_PASS) { + log("Updating RA from " + oldRa + " to " + ra); // Keep mRas in LRU order so as to prioritize generating filters for recently seen // RAs. LRU prioritizes this because RA filters are generated in order from mRas @@ -1928,36 +1991,33 @@ // filter program. // TODO: consider sorting the RAs in order of increasing expiry time as well. // Swap to front of array. - mRas.add(0, mRas.remove(i)); + mRas.remove(i); + mRas.add(0, ra); - // If the current program doesn't expire for a while, don't update. - if (shouldInstallnewProgram()) { + // Rate limit program installation + if (mTokenBucket.get()) { installNewProgramLocked(); - return ProcessRaResult.UPDATE_EXPIRY; + } else { + Log.e(TAG, "Failed to install prog for tracked RA, too many updates. " + ra); } - return ProcessRaResult.MATCH; + return; + } else if (result == Ra.MatchType.MATCH_DROP) { + log("Ignoring RA " + ra + " which matches " + oldRa); + return; } } - purgeExpiredRasLocked(); - // TODO: figure out how to proceed when we've received more then MAX_RAS RAs. if (mRas.size() >= MAX_RAS) { - return ProcessRaResult.DROPPED; - } - final Ra ra; - try { - ra = new Ra(packet, length); - } catch (Exception e) { - Log.e(TAG, "Error parsing RA", e); - return ProcessRaResult.PARSE_ERROR; - } - // Ignore 0 lifetime RAs. - if (ra.isExpired()) { - return ProcessRaResult.ZERO_LIFETIME; + // Remove the last (i.e. oldest) RA. + mRas.remove(mRas.size() - 1); } log("Adding " + ra); - mRas.add(ra); - installNewProgramLocked(); - return ProcessRaResult.UPDATE_NEW_RA; + mRas.add(0, ra); + // Rate limit program installation + if (mTokenBucket.get()) { + installNewProgramLocked(); + } else { + Log.e(TAG, "Failed to install prog for new RA, too many updates. " + ra); + } } /** @@ -1975,16 +2035,14 @@ return null; } // For now only support generating programs for Ethernet frames. If this restriction is - // lifted: - // 1. the program generator will need its offsets adjusted. - // 2. the packet filter attached to our packet socket will need its offset adjusted. + // lifted the program generator will need its offsets adjusted. if (apfCapabilities.apfPacketFormat != ARPHRD_ETHER) return null; if (!ApfGenerator.supportsVersion(apfCapabilities.apfVersionSupported)) { Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported); return null; } - return new ApfFilter(context, config, ifParams, ipClientCallback, new IpConnectivityLog()); + return new ApfFilter(context, config, ifParams, ipClientCallback); } public synchronized void shutdown() { @@ -2145,7 +2203,7 @@ pw.println("Program updates: " + mNumProgramUpdates); pw.println(String.format( "Last program length %d, installed %ds ago, lifetime %ds", - mLastInstalledProgram.length, currentTimeSeconds() - mLastTimeInstalledProgram, + mLastInstalledProgram.length, secondsSinceBoot() - mLastTimeInstalledProgram, mLastInstalledProgramMinLifetime)); pw.print("Denylisted Ethertypes:"); @@ -2159,7 +2217,7 @@ pw.println(ra); pw.increaseIndent(); pw.println(String.format( - "Seen: %d, last %ds ago", ra.seenCount, currentTimeSeconds() - ra.mLastSeen)); + "Last seen %ds ago", secondsSinceBoot() - ra.mLastSeen)); if (DBG) { pw.println("Last match:"); pw.increaseIndent();
diff --git a/src/android/net/apf/ApfGenerator.java b/src/android/net/apf/ApfGenerator.java index 7fc5fd3..0460c83 100644 --- a/src/android/net/apf/ApfGenerator.java +++ b/src/android/net/apf/ApfGenerator.java
@@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.List; /** * APF assembler/generator. A tool for generating an APF program. @@ -62,7 +63,8 @@ JNEBS(20), // Compare not equal byte sequence, e.g. "jnebs R0,5,label,0x1122334455" EXT(21), // Followed by immediate indicating ExtendedOpcodes. LDDW(22), // Load 4 bytes from data memory address (register + immediate): "lddw R0, [5]R1" - STDW(23); // Store 4 bytes to data memory address (register + immediate): "stdw R0, [5]R1" + STDW(23), // Store 4 bytes to data memory address (register + immediate): "stdw R0, [5]R1" + WRITE(24); // Write 1, 2 or 4 bytes imm to the output buffer, e.g. "WRITE 5" final int value; @@ -78,7 +80,12 @@ NOT(32), // Not, e.g. "not R0" NEG(33), // Negate, e.g. "neg R0" SWAP(34), // Swap, e.g. "swap R0,R1" - MOVE(35); // Move, e.g. "move R0,R1" + MOVE(35), // Move, e.g. "move R0,R1" + ALLOC(36), // Allocate buffer, "e.g. ALLOC R0" + TRANS(37), // Transmit buffer, "e.g. TRANS R0" + EWRITE1(38), // Write 1 byte from register to the output buffer, e.g. "EWRITE1 R0" + EWRITE2(39), // Write 2 bytes from register to the output buffer, e.g. "EWRITE2 R0" + EWRITE4(40); // Write 4 bytes from register to the output buffer, e.g. "EWRITE4 R0" final int value; @@ -96,13 +103,28 @@ this.value = value; } } + + private static class Immediate { + public final boolean mSigned; + public final byte mImmSize; + public final int mValue; + + Immediate(int value, boolean signed) { + this(value, signed, calculateImmSize(value, signed)); + } + + Immediate(int value, boolean signed, byte size) { + mValue = value; + mSigned = signed; + mImmSize = size; + } + } + private class Instruction { private final byte mOpcode; // A "Opcode" value. private final byte mRegister; // A "Register" value. - private boolean mHasImm; - private byte mImmSize; - private boolean mImmSigned; - private int mImm; + private final int mMaxSupportedImmediates; + public final List<Immediate> mImms = new ArrayList<>(); // When mOpcode is a jump: private byte mTargetLabelSize; private String mTargetLabel; @@ -114,27 +136,38 @@ int offset; Instruction(Opcodes opcode, Register register) { - mOpcode = (byte)opcode.value; - mRegister = (byte)register.value; + this(opcode, register, 1 /* mMaxSupportedImmediates */); + } + + Instruction(Opcodes opcode, Register register, int maxSupportedImm) { + mOpcode = (byte) opcode.value; + mRegister = (byte) register.value; + mMaxSupportedImmediates = maxSupportedImm; } Instruction(Opcodes opcode) { this(opcode, Register.R0); } - void setImm(int imm, boolean signed) { - mHasImm = true; - mImm = imm; - mImmSigned = signed; - mImmSize = calculateImmSize(imm, signed); + void addUnsignedImm(int imm) { + addImm(new Immediate(imm, false)); } - void setUnsignedImm(int imm) { - setImm(imm, false); + void addUnsignedImm(int imm, byte size) { + addImm(new Immediate(imm, false, size)); } - void setSignedImm(int imm) { - setImm(imm, true); + void addSignedImm(int imm) { + addImm(new Immediate(imm, true)); + } + + void addImm(Immediate imm) { + if (mImms.size() == mMaxSupportedImmediates) { + throw new IllegalArgumentException( + String.format("Opcode: %d only support at max: %d imms", mOpcode, + mMaxSupportedImmediates)); + } + mImms.add(imm); } void setLabel(String label) throws IllegalInstructionException { @@ -168,9 +201,7 @@ return 0; } int size = 1; - if (mHasImm) { - size += generatedImmSize(); - } + size += mImms.size() * generatedImmSize(); if (mTargetLabel != null) { size += generatedImmSize(); } @@ -243,8 +274,8 @@ if (mTargetLabel != null) { writingOffset = writeValue(calculateTargetLabelOffset(), bytecode, writingOffset); } - if (mHasImm) { - writingOffset = writeValue(mImm, bytecode, writingOffset); + for (int i = 0; i < mImms.size(); ++i) { + writingOffset = writeValue(mImms.get(i).mValue, bytecode, writingOffset); } if (mCompareBytes != null) { System.arraycopy(mCompareBytes, 0, bytecode, writingOffset, mCompareBytes.length); @@ -257,15 +288,19 @@ } /** - * Calculate the size of either the immediate field or the target label field, if either is - * present. Most instructions have either an immediate or a target label field, but for the + * Calculate the size of either the immediate fields or the target label field, if either is + * present. Most instructions have either immediates or a target label field, but for the * instructions that have both, the size of the target label field must be the same as the - * size of the immediate field, because there is only one length field in the instruction - * byte, hence why this function simply takes the maximum of the two sizes, so neither is + * size of the immediate fields, because there is only one length field in the instruction + * byte, hence why this function simply takes the maximum of those sizes, so neither is * truncated. */ private byte generatedImmSize() { - return mImmSize > mTargetLabelSize ? mImmSize : mTargetLabelSize; + byte maxSize = mTargetLabelSize; + for (int i = 0; i < mImms.size(); ++i) { + maxSize = (byte) Math.max(maxSize, mImms.get(i).mImmSize); + } + return maxSize; } private int calculateTargetLabelOffset() throws IllegalInstructionException { @@ -284,21 +319,6 @@ final int targetLabelOffset = targetLabelInstruction.offset - (offset + size()); return targetLabelOffset; } - - private byte calculateImmSize(int imm, boolean signed) { - if (imm == 0) { - return 0; - } - if (signed && (imm >= -128 && imm <= 127) || - !signed && (imm >= 0 && imm <= 255)) { - return 1; - } - if (signed && (imm >= -32768 && imm <= 32767) || - !signed && (imm >= 0 && imm <= 65535)) { - return 2; - } - return 4; - } } /** @@ -434,7 +454,7 @@ */ public ApfGenerator addLoad8(Register register, int offset) { Instruction instruction = new Instruction(Opcodes.LDB, register); - instruction.setUnsignedImm(offset); + instruction.addUnsignedImm(offset); addInstruction(instruction); return this; } @@ -445,7 +465,7 @@ */ public ApfGenerator addLoad16(Register register, int offset) { Instruction instruction = new Instruction(Opcodes.LDH, register); - instruction.setUnsignedImm(offset); + instruction.addUnsignedImm(offset); addInstruction(instruction); return this; } @@ -456,7 +476,7 @@ */ public ApfGenerator addLoad32(Register register, int offset) { Instruction instruction = new Instruction(Opcodes.LDW, register); - instruction.setUnsignedImm(offset); + instruction.addUnsignedImm(offset); addInstruction(instruction); return this; } @@ -468,7 +488,7 @@ */ public ApfGenerator addLoad8Indexed(Register register, int offset) { Instruction instruction = new Instruction(Opcodes.LDBX, register); - instruction.setUnsignedImm(offset); + instruction.addUnsignedImm(offset); addInstruction(instruction); return this; } @@ -480,7 +500,7 @@ */ public ApfGenerator addLoad16Indexed(Register register, int offset) { Instruction instruction = new Instruction(Opcodes.LDHX, register); - instruction.setUnsignedImm(offset); + instruction.addUnsignedImm(offset); addInstruction(instruction); return this; } @@ -492,7 +512,7 @@ */ public ApfGenerator addLoad32Indexed(Register register, int offset) { Instruction instruction = new Instruction(Opcodes.LDWX, register); - instruction.setUnsignedImm(offset); + instruction.addUnsignedImm(offset); addInstruction(instruction); return this; } @@ -502,7 +522,7 @@ */ public ApfGenerator addAdd(int value) { Instruction instruction = new Instruction(Opcodes.ADD); - instruction.setUnsignedImm(value); + instruction.addUnsignedImm(value); addInstruction(instruction); return this; } @@ -512,7 +532,7 @@ */ public ApfGenerator addMul(int value) { Instruction instruction = new Instruction(Opcodes.MUL); - instruction.setUnsignedImm(value); + instruction.addUnsignedImm(value); addInstruction(instruction); return this; } @@ -522,7 +542,7 @@ */ public ApfGenerator addDiv(int value) { Instruction instruction = new Instruction(Opcodes.DIV); - instruction.setUnsignedImm(value); + instruction.addUnsignedImm(value); addInstruction(instruction); return this; } @@ -532,7 +552,7 @@ */ public ApfGenerator addAnd(int value) { Instruction instruction = new Instruction(Opcodes.AND); - instruction.setUnsignedImm(value); + instruction.addUnsignedImm(value); addInstruction(instruction); return this; } @@ -542,7 +562,7 @@ */ public ApfGenerator addOr(int value) { Instruction instruction = new Instruction(Opcodes.OR); - instruction.setUnsignedImm(value); + instruction.addUnsignedImm(value); addInstruction(instruction); return this; } @@ -552,7 +572,7 @@ */ public ApfGenerator addLeftShift(int value) { Instruction instruction = new Instruction(Opcodes.SH); - instruction.setSignedImm(value); + instruction.addSignedImm(value); addInstruction(instruction); return this; } @@ -563,7 +583,7 @@ */ public ApfGenerator addRightShift(int value) { Instruction instruction = new Instruction(Opcodes.SH); - instruction.setSignedImm(-value); + instruction.addSignedImm(-value); addInstruction(instruction); return this; } @@ -630,7 +650,7 @@ */ public ApfGenerator addLoadImmediate(Register register, int value) { Instruction instruction = new Instruction(Opcodes.LI, register); - instruction.setSignedImm(value); + instruction.addSignedImm(value); addInstruction(instruction); return this; } @@ -641,7 +661,7 @@ */ public ApfGenerator addJumpIfR0Equals(int value, String target) { Instruction instruction = new Instruction(Opcodes.JEQ); - instruction.setUnsignedImm(value); + instruction.addUnsignedImm(value); instruction.setTargetLabel(target); addInstruction(instruction); return this; @@ -653,7 +673,7 @@ */ public ApfGenerator addJumpIfR0NotEquals(int value, String target) { Instruction instruction = new Instruction(Opcodes.JNE); - instruction.setUnsignedImm(value); + instruction.addUnsignedImm(value); instruction.setTargetLabel(target); addInstruction(instruction); return this; @@ -665,7 +685,7 @@ */ public ApfGenerator addJumpIfR0GreaterThan(int value, String target) { Instruction instruction = new Instruction(Opcodes.JGT); - instruction.setUnsignedImm(value); + instruction.addUnsignedImm(value); instruction.setTargetLabel(target); addInstruction(instruction); return this; @@ -677,7 +697,7 @@ */ public ApfGenerator addJumpIfR0LessThan(int value, String target) { Instruction instruction = new Instruction(Opcodes.JLT); - instruction.setUnsignedImm(value); + instruction.addUnsignedImm(value); instruction.setTargetLabel(target); addInstruction(instruction); return this; @@ -689,7 +709,7 @@ */ public ApfGenerator addJumpIfR0AnyBitsSet(int value, String target) { Instruction instruction = new Instruction(Opcodes.JSET); - instruction.setUnsignedImm(value); + instruction.addUnsignedImm(value); instruction.setTargetLabel(target); addInstruction(instruction); return this; @@ -760,7 +780,7 @@ throw new IllegalInstructionException("JNEBS fails with R1"); } Instruction instruction = new Instruction(Opcodes.JNEBS, register); - instruction.setUnsignedImm(bytes.length); + instruction.addUnsignedImm(bytes.length); instruction.setTargetLabel(target); instruction.setCompareBytes(bytes); addInstruction(instruction); @@ -777,7 +797,7 @@ throw new IllegalInstructionException("illegal memory slot number: " + slot); } Instruction instruction = new Instruction(Opcodes.EXT, register); - instruction.setUnsignedImm(ExtendedOpcodes.LDM.value + slot); + instruction.addUnsignedImm(ExtendedOpcodes.LDM.value + slot); addInstruction(instruction); return this; } @@ -792,7 +812,7 @@ throw new IllegalInstructionException("illegal memory slot number: " + slot); } Instruction instruction = new Instruction(Opcodes.EXT, register); - instruction.setUnsignedImm(ExtendedOpcodes.STM.value + slot); + instruction.addUnsignedImm(ExtendedOpcodes.STM.value + slot); addInstruction(instruction); return this; } @@ -802,7 +822,7 @@ */ public ApfGenerator addNot(Register register) { Instruction instruction = new Instruction(Opcodes.EXT, register); - instruction.setUnsignedImm(ExtendedOpcodes.NOT.value); + instruction.addUnsignedImm(ExtendedOpcodes.NOT.value); addInstruction(instruction); return this; } @@ -812,7 +832,7 @@ */ public ApfGenerator addNeg(Register register) { Instruction instruction = new Instruction(Opcodes.EXT, register); - instruction.setUnsignedImm(ExtendedOpcodes.NEG.value); + instruction.addUnsignedImm(ExtendedOpcodes.NEG.value); addInstruction(instruction); return this; } @@ -822,7 +842,7 @@ */ public ApfGenerator addSwap() { Instruction instruction = new Instruction(Opcodes.EXT); - instruction.setUnsignedImm(ExtendedOpcodes.SWAP.value); + instruction.addUnsignedImm(ExtendedOpcodes.SWAP.value); addInstruction(instruction); return this; } @@ -833,7 +853,84 @@ */ public ApfGenerator addMove(Register register) { Instruction instruction = new Instruction(Opcodes.EXT, register); - instruction.setUnsignedImm(ExtendedOpcodes.MOVE.value); + instruction.addUnsignedImm(ExtendedOpcodes.MOVE.value); + addInstruction(instruction); + return this; + } + + /** + * Add an instruction to the end of the program to call the apf_allocate_buffer() function. + * + * @param register the register value contains the buffer size. + */ + public ApfGenerator addAlloc(Register register) throws IllegalInstructionException { + requireApfVersion(5); + Instruction instruction = new Instruction(Opcodes.EXT, register); + instruction.addUnsignedImm(ExtendedOpcodes.ALLOC.value); + addInstruction(instruction); + return this; + } + + /** + * Add an instruction to the end of the program to call the apf_transmit_buffer() function. + * + * @param register the register value contains the packet type. + */ + public ApfGenerator addTrans(Register register) throws IllegalInstructionException { + requireApfVersion(5); + Instruction instruction = new Instruction(Opcodes.EXT, register); + instruction.addUnsignedImm(ExtendedOpcodes.TRANS.value); + addInstruction(instruction); + return this; + } + + /** + * Add an instruction to the end of the program to write 1, 2 or 4 bytes value to output buffer. + * + * @param value the value to write + * @param size the size of the value + * @return the ApfGenerator object + * @throws IllegalInstructionException throws when size is not 1, 2 or 4 + */ + public ApfGenerator addWrite(int value, byte size) throws IllegalInstructionException { + requireApfVersion(5); + if (!(size == 1 || size == 2 || size == 4)) { + throw new IllegalInstructionException("length field must be 1, 2 or 4"); + } + if (size < calculateImmSize(value, false)) { + throw new IllegalInstructionException( + String.format("the value %d is unfit into size: %d", value, size)); + } + Instruction instruction = new Instruction(Opcodes.WRITE); + instruction.addUnsignedImm(value, size); + addInstruction(instruction); + return this; + } + + /** + * Add an instruction to the end of the program to write 1, 2 or 4 bytes value from register + * to output buffer. + * + * @param register the register contains the value to be written + * @param size the size of the value + * @return the ApfGenerator object + * @throws IllegalInstructionException throws when size is not 1, 2 or 4 + */ + public ApfGenerator addWrite(Register register, byte size) + throws IllegalInstructionException { + requireApfVersion(5); + if (!(size == 1 || size == 2 || size == 4)) { + throw new IllegalInstructionException( + "length field must be 1, 2 or 4"); + } + Instruction instruction = new Instruction(Opcodes.EXT, register); + if (size == 1) { + instruction.addUnsignedImm(ExtendedOpcodes.EWRITE1.value); + } else if (size == 2) { + instruction.addUnsignedImm(ExtendedOpcodes.EWRITE2.value); + } else { + instruction.addUnsignedImm(ExtendedOpcodes.EWRITE4.value); + } addInstruction(instruction); return this; } @@ -848,7 +945,7 @@ throws IllegalInstructionException { requireApfVersion(3); Instruction instruction = new Instruction(Opcodes.LDDW, destinationRegister); - instruction.setSignedImm(offset); + instruction.addSignedImm(offset); addInstruction(instruction); return this; } @@ -863,7 +960,7 @@ throws IllegalInstructionException { requireApfVersion(3); Instruction instruction = new Instruction(Opcodes.STDW, sourceRegister); - instruction.setSignedImm(offset); + instruction.addSignedImm(offset); addInstruction(instruction); return this; } @@ -882,6 +979,22 @@ } /** + * Calculate the size of the imm. + */ + private static byte calculateImmSize(int imm, boolean signed) { + if (imm == 0) { + return 0; + } + if (signed && (imm >= -128 && imm <= 127) || !signed && (imm >= 0 && imm <= 255)) { + return 1; + } + if (signed && (imm >= -32768 && imm <= 32767) || !signed && (imm >= 0 && imm <= 65535)) { + return 2; + } + return 4; + } + + /** * Returns an overestimate of the size of the generated program. {@link #generate} may return * a program that is smaller. */
diff --git a/src/android/net/apf/LegacyApfFilter.java b/src/android/net/apf/LegacyApfFilter.java new file mode 100644 index 0000000..63e2bbc --- /dev/null +++ b/src/android/net/apf/LegacyApfFilter.java
@@ -0,0 +1,2263 @@ +/* + * Copyright (C) 2016 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.apf; + +import static android.net.util.SocketUtils.makePacketSocketAddress; +import static android.system.OsConstants.AF_PACKET; +import static android.system.OsConstants.ARPHRD_ETHER; +import static android.system.OsConstants.ETH_P_ARP; +import static android.system.OsConstants.ETH_P_IP; +import static android.system.OsConstants.ETH_P_IPV6; +import static android.system.OsConstants.IPPROTO_ICMPV6; +import static android.system.OsConstants.IPPROTO_TCP; +import static android.system.OsConstants.IPPROTO_UDP; +import static android.system.OsConstants.SOCK_RAW; + +import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION; +import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.NattKeepalivePacketDataParcelable; +import android.net.TcpKeepalivePacketDataParcelable; +import android.net.apf.ApfGenerator.IllegalInstructionException; +import android.net.apf.ApfGenerator.Register; +import android.net.ip.IpClient.IpClientCallbacksWrapper; +import android.net.metrics.ApfProgramEvent; +import android.net.metrics.ApfStats; +import android.net.metrics.IpConnectivityLog; +import android.net.metrics.RaEvent; +import android.os.PowerManager; +import android.os.SystemClock; +import android.system.ErrnoException; +import android.system.Os; +import android.text.format.DateUtils; +import android.util.Log; +import android.util.SparseArray; + +import androidx.annotation.Nullable; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.HexDump; +import com.android.internal.util.IndentingPrintWriter; +import com.android.net.module.util.CollectionUtils; +import com.android.net.module.util.ConnectivityUtils; +import com.android.net.module.util.InterfaceParams; +import com.android.net.module.util.SocketUtils; +import com.android.networkstack.util.NetworkStackUtils; + +import java.io.ByteArrayOutputStream; +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * For networks that support packet filtering via APF programs, {@code ApfFilter} + * listens for IPv6 ICMPv6 router advertisements (RAs) and generates APF programs to + * filter out redundant duplicate ones. + * + * Threading model: + * A collection of RAs we've received is kept in mRas. Generating APF programs uses mRas to + * know what RAs to filter for, thus generating APF programs is dependent on mRas. + * mRas can be accessed by multiple threads: + * - ReceiveThread, which listens for RAs and adds them to mRas, and generates APF programs. + * - callers of: + * - setMulticastFilter(), which can cause an APF program to be generated. + * - dump(), which dumps mRas among other things. + * - shutdown(), which clears mRas. + * So access to mRas is synchronized. + * + * @hide + */ +public class LegacyApfFilter implements AndroidPacketFilter { + + // Enums describing the outcome of receiving an RA packet. + private static enum ProcessRaResult { + MATCH, // Received RA matched a known RA + DROPPED, // Received RA ignored due to MAX_RAS + PARSE_ERROR, // Received RA could not be parsed + ZERO_LIFETIME, // Received RA had 0 lifetime + UPDATE_NEW_RA, // APF program updated for new RA + UPDATE_EXPIRY // APF program updated for expiry + } + + /** + * APF packet counters. + * + * Packet counters are 32bit big-endian values, and allocated near the end of the APF data + * buffer, using negative byte offsets, where -4 is equivalent to maximumApfProgramSize - 4, + * the last writable 32bit word. + */ + @VisibleForTesting + public static enum Counter { + RESERVED_OOB, // Points to offset 0 from the end of the buffer (out-of-bounds) + TOTAL_PACKETS, + PASSED_ARP, + PASSED_DHCP, + PASSED_IPV4, + PASSED_IPV6_NON_ICMP, + PASSED_IPV4_UNICAST, + PASSED_IPV6_ICMP, + PASSED_IPV6_UNICAST_NON_ICMP, + PASSED_ARP_NON_IPV4, + PASSED_ARP_UNKNOWN, + PASSED_ARP_UNICAST_REPLY, + PASSED_NON_IP_UNICAST, + PASSED_MDNS, + DROPPED_ETH_BROADCAST, + DROPPED_RA, + DROPPED_GARP_REPLY, + DROPPED_ARP_OTHER_HOST, + DROPPED_IPV4_L2_BROADCAST, + DROPPED_IPV4_BROADCAST_ADDR, + DROPPED_IPV4_BROADCAST_NET, + DROPPED_IPV4_MULTICAST, + DROPPED_IPV6_ROUTER_SOLICITATION, + DROPPED_IPV6_MULTICAST_NA, + DROPPED_IPV6_MULTICAST, + DROPPED_IPV6_MULTICAST_PING, + DROPPED_IPV6_NON_ICMP_MULTICAST, + DROPPED_802_3_FRAME, + DROPPED_ETHERTYPE_BLACKLISTED, + DROPPED_ARP_REPLY_SPA_NO_HOST, + DROPPED_IPV4_KEEPALIVE_ACK, + DROPPED_IPV6_KEEPALIVE_ACK, + DROPPED_IPV4_NATT_KEEPALIVE, + DROPPED_MDNS; + + // Returns the negative byte offset from the end of the APF data segment for + // a given counter. + public int offset() { + return - this.ordinal() * 4; // Currently, all counters are 32bit long. + } + + // Returns the total size of the data segment in bytes. + public static int totalSize() { + return (Counter.class.getEnumConstants().length - 1) * 4; + } + } + + /** + * When APFv4 is supported, loads R1 with the offset of the specified counter. + */ + private void maybeSetupCounter(ApfGenerator gen, Counter c) { + if (mApfCapabilities.hasDataAccess()) { + gen.addLoadImmediate(Register.R1, c.offset()); + } + } + + // When APFv4 is supported, these point to the trampolines generated by emitEpilogue(). + // Otherwise, they're just aliases for PASS_LABEL and DROP_LABEL. + private final String mCountAndPassLabel; + private final String mCountAndDropLabel; + + // Thread to listen for RAs. + @VisibleForTesting + public class ReceiveThread extends Thread { + private final byte[] mPacket = new byte[1514]; + private final FileDescriptor mSocket; + private final long mStart = SystemClock.elapsedRealtime(); + + private int mReceivedRas = 0; + private int mMatchingRas = 0; + private int mDroppedRas = 0; + private int mParseErrors = 0; + private int mZeroLifetimeRas = 0; + private int mProgramUpdates = 0; + + private volatile boolean mStopped; + + public ReceiveThread(FileDescriptor socket) { + mSocket = socket; + } + + public void halt() { + mStopped = true; + // Interrupts the read() call the thread is blocked in. + SocketUtils.closeSocketQuietly(mSocket); + } + + @Override + public void run() { + log("begin monitoring"); + while (!mStopped) { + try { + int length = Os.read(mSocket, mPacket, 0, mPacket.length); + updateStats(processRa(mPacket, length)); + } catch (IOException|ErrnoException e) { + if (!mStopped) { + Log.e(TAG, "Read error", e); + } + } + } + logStats(); + } + + private void updateStats(ProcessRaResult result) { + mReceivedRas++; + switch(result) { + case MATCH: + mMatchingRas++; + return; + case DROPPED: + mDroppedRas++; + return; + case PARSE_ERROR: + mParseErrors++; + return; + case ZERO_LIFETIME: + mZeroLifetimeRas++; + return; + case UPDATE_EXPIRY: + mMatchingRas++; + mProgramUpdates++; + return; + case UPDATE_NEW_RA: + mProgramUpdates++; + return; + } + } + + private void logStats() { + final long nowMs = SystemClock.elapsedRealtime(); + synchronized (this) { + final ApfStats stats = new ApfStats.Builder() + .setReceivedRas(mReceivedRas) + .setMatchingRas(mMatchingRas) + .setDroppedRas(mDroppedRas) + .setParseErrors(mParseErrors) + .setZeroLifetimeRas(mZeroLifetimeRas) + .setProgramUpdates(mProgramUpdates) + .setDurationMs(nowMs - mStart) + .setMaxProgramSize(mApfCapabilities.maximumApfProgramSize) + .setProgramUpdatesAll(mNumProgramUpdates) + .setProgramUpdatesAllowingMulticast(mNumProgramUpdatesAllowingMulticast) + .build(); + mMetricsLog.log(stats); + logApfProgramEventLocked(nowMs / DateUtils.SECOND_IN_MILLIS); + } + } + } + + private static final String TAG = "ApfFilter"; + private static final boolean DBG = true; + private static final boolean VDBG = false; + + private static final int ETH_HEADER_LEN = 14; + private static final int ETH_DEST_ADDR_OFFSET = 0; + private static final int ETH_ETHERTYPE_OFFSET = 12; + private static final int ETH_TYPE_MIN = 0x0600; + private static final int ETH_TYPE_MAX = 0xFFFF; + // TODO: Make these offsets relative to end of link-layer header; don't include ETH_HEADER_LEN. + private static final int IPV4_TOTAL_LENGTH_OFFSET = ETH_HEADER_LEN + 2; + private static final int IPV4_FRAGMENT_OFFSET_OFFSET = ETH_HEADER_LEN + 6; + // Endianness is not an issue for this constant because the APF interpreter always operates in + // network byte order. + private static final int IPV4_FRAGMENT_OFFSET_MASK = 0x1fff; + private static final int IPV4_PROTOCOL_OFFSET = ETH_HEADER_LEN + 9; + private static final int IPV4_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 16; + private static final int IPV4_ANY_HOST_ADDRESS = 0; + private static final int IPV4_BROADCAST_ADDRESS = -1; // 255.255.255.255 + private static final int IPV4_HEADER_LEN = 20; // Without options + + // Traffic class and Flow label are not byte aligned. Luckily we + // don't care about either value so we'll consider bytes 1-3 of the + // IPv6 header as don't care. + private static final int IPV6_FLOW_LABEL_OFFSET = ETH_HEADER_LEN + 1; + private static final int IPV6_FLOW_LABEL_LEN = 3; + private static final int IPV6_NEXT_HEADER_OFFSET = ETH_HEADER_LEN + 6; + private static final int IPV6_SRC_ADDR_OFFSET = ETH_HEADER_LEN + 8; + private static final int IPV6_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 24; + private static final int IPV6_HEADER_LEN = 40; + // The IPv6 all nodes address ff02::1 + private static final byte[] IPV6_ALL_NODES_ADDRESS = + { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }; + + private static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN; + + private static final int IPPROTO_HOPOPTS = 0; + + // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT + private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 2; + private static final int UDP_HEADER_LEN = 8; + + private static final int TCP_HEADER_SIZE_OFFSET = 12; + + private static final int DHCP_CLIENT_PORT = 68; + // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT + private static final int DHCP_CLIENT_MAC_OFFSET = ETH_HEADER_LEN + UDP_HEADER_LEN + 28; + + private static final int ARP_HEADER_OFFSET = ETH_HEADER_LEN; + private static final byte[] ARP_IPV4_HEADER = { + 0, 1, // Hardware type: Ethernet (1) + 8, 0, // Protocol type: IP (0x0800) + 6, // Hardware size: 6 + 4, // Protocol size: 4 + }; + private static final int ARP_OPCODE_OFFSET = ARP_HEADER_OFFSET + 6; + // Opcode: ARP request (0x0001), ARP reply (0x0002) + private static final short ARP_OPCODE_REQUEST = 1; + private static final short ARP_OPCODE_REPLY = 2; + private static final int ARP_SOURCE_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 14; + private static final int ARP_TARGET_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 24; + // Do not log ApfProgramEvents whose actual lifetimes was less than this. + private static final int APF_PROGRAM_EVENT_LIFETIME_THRESHOLD = 2; + // Limit on the Black List size to cap on program usage for this + // TODO: Select a proper max length + private static final int APF_MAX_ETH_TYPE_BLACK_LIST_LEN = 20; + + private static final byte[] ETH_MULTICAST_MDNS_V4_MAC_ADDRESS = + {(byte) 0x01, (byte) 0x00, (byte) 0x5e, (byte) 0x00, (byte) 0x00, (byte) 0xfb}; + private static final byte[] ETH_MULTICAST_MDNS_V6_MAC_ADDRESS = + {(byte) 0x33, (byte) 0x33, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xfb}; + private static final int MDNS_PORT = 5353; + private static final int DNS_HEADER_LEN = 12; + private static final int DNS_QDCOUNT_OFFSET = 4; + // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT, or the + // IPv6 header length. + private static final int MDNS_QDCOUNT_OFFSET = + ETH_HEADER_LEN + UDP_HEADER_LEN + DNS_QDCOUNT_OFFSET; + private static final int MDNS_QNAME_OFFSET = + ETH_HEADER_LEN + UDP_HEADER_LEN + DNS_HEADER_LEN; + + + + private final ApfCapabilities mApfCapabilities; + private final IpClientCallbacksWrapper mIpClientCallback; + private final InterfaceParams mInterfaceParams; + private final IpConnectivityLog mMetricsLog; + + @VisibleForTesting + public byte[] mHardwareAddress; + @VisibleForTesting + public ReceiveThread mReceiveThread; + @GuardedBy("this") + private long mUniqueCounter; + @GuardedBy("this") + private boolean mMulticastFilter; + @GuardedBy("this") + private boolean mInDozeMode; + private final boolean mDrop802_3Frames; + private final int[] mEthTypeBlackList; + + // Ignore non-zero RDNSS lifetimes below this value. + private final int mMinRdnssLifetimeSec; + + // Detects doze mode state transitions. + private final BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)) { + PowerManager powerManager = + (PowerManager) context.getSystemService(Context.POWER_SERVICE); + final boolean deviceIdle = powerManager.isDeviceIdleMode(); + setDozeMode(deviceIdle); + } + } + }; + private final Context mContext; + + // Our IPv4 address, if we have just one, otherwise null. + @GuardedBy("this") + private byte[] mIPv4Address; + // The subnet prefix length of our IPv4 network. Only valid if mIPv4Address is not null. + @GuardedBy("this") + private int mIPv4PrefixLength; + + @VisibleForTesting + public LegacyApfFilter(Context context, ApfFilter.ApfConfiguration config, + InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback, + IpConnectivityLog log) { + mApfCapabilities = config.apfCapabilities; + mIpClientCallback = ipClientCallback; + mInterfaceParams = ifParams; + mMulticastFilter = config.multicastFilter; + mDrop802_3Frames = config.ieee802_3Filter; + mMinRdnssLifetimeSec = config.minRdnssLifetimeSec; + mContext = context; + + if (mApfCapabilities.hasDataAccess()) { + mCountAndPassLabel = "countAndPass"; + mCountAndDropLabel = "countAndDrop"; + } else { + // APFv4 unsupported: turn jumps to the counter trampolines to immediately PASS or DROP, + // preserving the original pre-APFv4 behavior. + mCountAndPassLabel = ApfGenerator.PASS_LABEL; + mCountAndDropLabel = ApfGenerator.DROP_LABEL; + } + + // Now fill the black list from the passed array + mEthTypeBlackList = filterEthTypeBlackList(config.ethTypeBlackList); + + mMetricsLog = log; + + // TODO: ApfFilter should not generate programs until IpClient sends provisioning success. + maybeStartFilter(); + + // Listen for doze-mode transition changes to enable/disable the IPv6 multicast filter. + mContext.registerReceiver(mDeviceIdleReceiver, + new IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)); + } + + public synchronized void setDataSnapshot(byte[] data) { + mDataSnapshot = data; + } + + private void log(String s) { + Log.d(TAG, "(" + mInterfaceParams.name + "): " + s); + } + + @GuardedBy("this") + private long getUniqueNumberLocked() { + return mUniqueCounter++; + } + + @GuardedBy("this") + private static int[] filterEthTypeBlackList(int[] ethTypeBlackList) { + ArrayList<Integer> bl = new ArrayList<Integer>(); + + for (int p : ethTypeBlackList) { + // Check if the protocol is a valid ether type + if ((p < ETH_TYPE_MIN) || (p > ETH_TYPE_MAX)) { + continue; + } + + // Check if the protocol is not repeated in the passed array + if (bl.contains(p)) { + continue; + } + + // Check if list reach its max size + if (bl.size() == APF_MAX_ETH_TYPE_BLACK_LIST_LEN) { + Log.w(TAG, "Passed EthType Black List size too large (" + bl.size() + + ") using top " + APF_MAX_ETH_TYPE_BLACK_LIST_LEN + " protocols"); + break; + } + + // Now add the protocol to the list + bl.add(p); + } + + return bl.stream().mapToInt(Integer::intValue).toArray(); + } + + /** + * Attempt to start listening for RAs and, if RAs are received, generating and installing + * filters to ignore useless RAs. + */ + @VisibleForTesting + public void maybeStartFilter() { + FileDescriptor socket; + try { + mHardwareAddress = mInterfaceParams.macAddr.toByteArray(); + synchronized(this) { + // Clear the APF memory to reset all counters upon connecting to the first AP + // in an SSID. This is limited to APFv4 devices because this large write triggers + // a crash on some older devices (b/78905546). + if (mApfCapabilities.hasDataAccess()) { + byte[] zeroes = new byte[mApfCapabilities.maximumApfProgramSize]; + mIpClientCallback.installPacketFilter(zeroes); + } + + // Install basic filters + installNewProgramLocked(); + } + socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6); + SocketAddress addr = makePacketSocketAddress(ETH_P_IPV6, mInterfaceParams.index); + Os.bind(socket, addr); + NetworkStackUtils.attachRaFilter(socket); + } catch(SocketException|ErrnoException e) { + Log.e(TAG, "Error starting filter", e); + return; + } + mReceiveThread = new ReceiveThread(socket); + mReceiveThread.start(); + } + + // Returns seconds since device boot. + @VisibleForTesting + protected long currentTimeSeconds() { + return SystemClock.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS; + } + + public static class InvalidRaException extends Exception { + public InvalidRaException(String m) { + super(m); + } + } + + /** + * Class to keep track of a section in a packet. + */ + private static class PacketSection { + public enum Type { + MATCH, // A field that should be matched (e.g., the router IP address). + IGNORE, // An ignored field such as the checksum of the flow label. Not matched. + LIFETIME, // A lifetime. Not matched, and generally counts toward minimum RA lifetime. + } + + /** The type of section. */ + public final Type type; + /** Offset into the packet at which this section begins. */ + public final int start; + /** Length of this section in bytes. */ + public final int length; + /** If this is a lifetime, the ICMP option that defined it. 0 for router lifetime. */ + public final int option; + /** If this is a lifetime, the lifetime value. */ + public final long lifetime; + + PacketSection(int start, int length, Type type, int option, long lifetime) { + this.start = start; + this.length = length; + this.type = type; + this.option = option; + this.lifetime = lifetime; + } + + public String toString() { + if (type == Type.LIFETIME) { + return String.format("%s: (%d, %d) %d %d", type, start, length, option, lifetime); + } else { + return String.format("%s: (%d, %d)", type, start, length); + } + } + } + + // A class to hold information about an RA. + @VisibleForTesting + public class Ra { + // From RFC4861: + private static final int ICMP6_RA_HEADER_LEN = 16; + private static final int ICMP6_RA_CHECKSUM_OFFSET = + ETH_HEADER_LEN + IPV6_HEADER_LEN + 2; + private static final int ICMP6_RA_CHECKSUM_LEN = 2; + private static final int ICMP6_RA_OPTION_OFFSET = + ETH_HEADER_LEN + IPV6_HEADER_LEN + ICMP6_RA_HEADER_LEN; + private static final int ICMP6_RA_ROUTER_LIFETIME_OFFSET = + ETH_HEADER_LEN + IPV6_HEADER_LEN + 6; + private static final int ICMP6_RA_ROUTER_LIFETIME_LEN = 2; + // Prefix information option. + private static final int ICMP6_PREFIX_OPTION_TYPE = 3; + private static final int ICMP6_PREFIX_OPTION_LEN = 32; + private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET = 4; + private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN = 4; + private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET = 8; + private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN = 4; + + // From RFC6106: Recursive DNS Server option + private static final int ICMP6_RDNSS_OPTION_TYPE = 25; + // From RFC6106: DNS Search List option + private static final int ICMP6_DNSSL_OPTION_TYPE = 31; + + // From RFC4191: Route Information option + private static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24; + // Above three options all have the same format: + private static final int ICMP6_4_BYTE_LIFETIME_OFFSET = 4; + private static final int ICMP6_4_BYTE_LIFETIME_LEN = 4; + + // Note: mPacket's position() cannot be assumed to be reset. + private final ByteBuffer mPacket; + + // List of sections in the packet. + private final ArrayList<PacketSection> mPacketSections = new ArrayList<>(); + + // Minimum lifetime in packet + long mMinLifetime; + // When the packet was last captured, in seconds since Unix Epoch + long mLastSeen; + + // For debugging only. Offsets into the packet where PIOs are. + private final ArrayList<Integer> mPrefixOptionOffsets = new ArrayList<>(); + + // For debugging only. Offsets into the packet where RDNSS options are. + private final ArrayList<Integer> mRdnssOptionOffsets = new ArrayList<>(); + + // For debugging only. Offsets into the packet where RIO options are. + private final ArrayList<Integer> mRioOptionOffsets = new ArrayList<>(); + + // For debugging only. How many times this RA was seen. + int seenCount = 0; + + // For debugging only. Returns the hex representation of the last matching packet. + String getLastMatchingPacket() { + return HexDump.toHexString(mPacket.array(), 0, mPacket.capacity(), + false /* lowercase */); + } + + // For debugging only. Returns the string representation of the IPv6 address starting at + // position pos in the packet. + private String IPv6AddresstoString(int pos) { + try { + byte[] array = mPacket.array(); + // Can't just call copyOfRange() and see if it throws, because if it reads past the + // end it pads with zeros instead of throwing. + if (pos < 0 || pos + 16 > array.length || pos + 16 < pos) { + return "???"; + } + byte[] addressBytes = Arrays.copyOfRange(array, pos, pos + 16); + InetAddress address = (Inet6Address) InetAddress.getByAddress(addressBytes); + return address.getHostAddress(); + } catch (UnsupportedOperationException e) { + // array() failed. Cannot happen, mPacket is array-backed and read-write. + return "???"; + } catch (ClassCastException|UnknownHostException e) { + // Cannot happen. + return "???"; + } + } + + // Can't be static because it's in a non-static inner class. + // TODO: Make this static once RA is its own class. + private void prefixOptionToString(StringBuffer sb, int offset) { + String prefix = IPv6AddresstoString(offset + 16); + int length = getUint8(mPacket, offset + 2); + long valid = getUint32(mPacket, offset + 4); + long preferred = getUint32(mPacket, offset + 8); + sb.append(String.format("%s/%d %ds/%ds ", prefix, length, valid, preferred)); + } + + private void rdnssOptionToString(StringBuffer sb, int offset) { + int optLen = getUint8(mPacket, offset + 1) * 8; + if (optLen < 24) return; // Malformed or empty. + long lifetime = getUint32(mPacket, offset + 4); + int numServers = (optLen - 8) / 16; + sb.append("DNS ").append(lifetime).append("s"); + for (int server = 0; server < numServers; server++) { + sb.append(" ").append(IPv6AddresstoString(offset + 8 + 16 * server)); + } + sb.append(" "); + } + + private void rioOptionToString(StringBuffer sb, int offset) { + int optLen = getUint8(mPacket, offset + 1) * 8; + if (optLen < 8 || optLen > 24) return; // Malformed or empty. + int prefixLen = getUint8(mPacket, offset + 2); + long lifetime = getUint32(mPacket, offset + 4); + + // This read is variable length because the prefix can be 0, 8 or 16 bytes long. + // We can't use any of the ByteBuffer#get methods here because they all start reading + // from the buffer's current position. + byte[] prefix = new byte[IPV6_ADDR_LEN]; + System.arraycopy(mPacket.array(), offset + 8, prefix, 0, optLen - 8); + sb.append("RIO ").append(lifetime).append("s "); + try { + InetAddress address = (Inet6Address) InetAddress.getByAddress(prefix); + sb.append(address.getHostAddress()); + } catch (UnknownHostException impossible) { + sb.append("???"); + } + sb.append("/").append(prefixLen).append(" "); + } + + public String toString() { + try { + StringBuffer sb = new StringBuffer(); + sb.append(String.format("RA %s -> %s %ds ", + IPv6AddresstoString(IPV6_SRC_ADDR_OFFSET), + IPv6AddresstoString(IPV6_DEST_ADDR_OFFSET), + getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET))); + for (int i: mPrefixOptionOffsets) { + prefixOptionToString(sb, i); + } + for (int i: mRdnssOptionOffsets) { + rdnssOptionToString(sb, i); + } + for (int i: mRioOptionOffsets) { + rioOptionToString(sb, i); + } + return sb.toString(); + } catch (BufferUnderflowException|IndexOutOfBoundsException e) { + return "<Malformed RA>"; + } + } + + /** + * Add a packet section that should be matched, starting from the current position. + * @param length the length of the section + */ + private void addMatchSection(int length) { + // Don't generate JNEBS instruction for 0 bytes as they will fail the + // ASSERT_FORWARD_IN_PROGRAM(pc + cmp_imm - 1) check (where cmp_imm is + // the number of bytes to compare) and immediately pass the packet. + // The code does not attempt to generate such matches, but add a safety + // check to prevent doing so in the presence of bugs or malformed or + // truncated packets. + if (length == 0) return; + mPacketSections.add( + new PacketSection(mPacket.position(), length, PacketSection.Type.MATCH, 0, 0)); + mPacket.position(mPacket.position() + length); + } + + /** + * Add a packet section that should be matched, starting from the current position. + * @param end the offset in the packet before which the section ends + */ + private void addMatchUntil(int end) { + addMatchSection(end - mPacket.position()); + } + + /** + * Add a packet section that should be ignored, starting from the current position. + * @param length the length of the section in bytes + */ + private void addIgnoreSection(int length) { + mPacketSections.add( + new PacketSection(mPacket.position(), length, PacketSection.Type.IGNORE, 0, 0)); + mPacket.position(mPacket.position() + length); + } + + /** + * Add a packet section that represents a lifetime, starting from the current position. + * @param length the length of the section in bytes + * @param optionType the RA option containing this lifetime, or 0 for router lifetime + * @param lifetime the lifetime + */ + private void addLifetimeSection(int length, int optionType, long lifetime) { + mPacketSections.add( + new PacketSection(mPacket.position(), length, PacketSection.Type.LIFETIME, + optionType, lifetime)); + mPacket.position(mPacket.position() + length); + } + + /** + * Adds packet sections for an RA option with a 4-byte lifetime 4 bytes into the option + * @param optionType the RA option that is being added + * @param optionLength the length of the option in bytes + */ + private long add4ByteLifetimeOption(int optionType, int optionLength) { + addMatchSection(ICMP6_4_BYTE_LIFETIME_OFFSET); + final long lifetime = getUint32(mPacket, mPacket.position()); + addLifetimeSection(ICMP6_4_BYTE_LIFETIME_LEN, optionType, lifetime); + addMatchSection(optionLength - ICMP6_4_BYTE_LIFETIME_OFFSET + - ICMP6_4_BYTE_LIFETIME_LEN); + return lifetime; + } + + // http://b/66928272 http://b/65056012 + // DnsServerRepository ignores RDNSS servers with lifetimes that are too low. Ignore these + // lifetimes for the purpose of filter lifetime calculations. + private boolean shouldIgnoreLifetime(int optionType, long lifetime) { + return optionType == ICMP6_RDNSS_OPTION_TYPE + && lifetime != 0 && lifetime < mMinRdnssLifetimeSec; + } + + private boolean isRelevantLifetime(PacketSection section) { + return section.type == PacketSection.Type.LIFETIME + && !shouldIgnoreLifetime(section.option, section.lifetime); + } + + // Note that this parses RA and may throw InvalidRaException (from + // Buffer.position(int) or due to an invalid-length option) or IndexOutOfBoundsException + // (from ByteBuffer.get(int) ) if parsing encounters something non-compliant with + // specifications. + @VisibleForTesting + public Ra(byte[] packet, int length) throws InvalidRaException { + if (length < ICMP6_RA_OPTION_OFFSET) { + throw new InvalidRaException("Not an ICMP6 router advertisement: too short"); + } + + mPacket = ByteBuffer.wrap(Arrays.copyOf(packet, length)); + mLastSeen = currentTimeSeconds(); + + // Check packet in case a packet arrives before we attach RA filter + // to our packet socket. b/29586253 + if (getUint16(mPacket, ETH_ETHERTYPE_OFFSET) != ETH_P_IPV6 || + getUint8(mPacket, IPV6_NEXT_HEADER_OFFSET) != IPPROTO_ICMPV6 || + getUint8(mPacket, ICMP6_TYPE_OFFSET) != ICMPV6_ROUTER_ADVERTISEMENT) { + throw new InvalidRaException("Not an ICMP6 router advertisement"); + } + + + RaEvent.Builder builder = new RaEvent.Builder(); + + // Ignore the flow label and low 4 bits of traffic class. + addMatchUntil(IPV6_FLOW_LABEL_OFFSET); + addIgnoreSection(IPV6_FLOW_LABEL_LEN); + + // Ignore checksum. + addMatchUntil(ICMP6_RA_CHECKSUM_OFFSET); + addIgnoreSection(ICMP6_RA_CHECKSUM_LEN); + + // Parse router lifetime + addMatchUntil(ICMP6_RA_ROUTER_LIFETIME_OFFSET); + final long routerLifetime = getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET); + addLifetimeSection(ICMP6_RA_ROUTER_LIFETIME_LEN, 0, routerLifetime); + builder.updateRouterLifetime(routerLifetime); + + // Add remaining fields (reachable time and retransmission timer) to match section. + addMatchUntil(ICMP6_RA_OPTION_OFFSET); + + while (mPacket.hasRemaining()) { + final int position = mPacket.position(); + final int optionType = getUint8(mPacket, position); + final int optionLength = getUint8(mPacket, position + 1) * 8; + long lifetime; + switch (optionType) { + case ICMP6_PREFIX_OPTION_TYPE: + mPrefixOptionOffsets.add(position); + + // Parse valid lifetime + addMatchSection(ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET); + lifetime = getUint32(mPacket, mPacket.position()); + addLifetimeSection(ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN, + ICMP6_PREFIX_OPTION_TYPE, lifetime); + builder.updatePrefixValidLifetime(lifetime); + + // Parse preferred lifetime + lifetime = getUint32(mPacket, mPacket.position()); + addLifetimeSection(ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN, + ICMP6_PREFIX_OPTION_TYPE, lifetime); + builder.updatePrefixPreferredLifetime(lifetime); + + addMatchSection(4); // Reserved bytes + addMatchSection(IPV6_ADDR_LEN); // The prefix itself + break; + // These three options have the same lifetime offset and size, and + // are processed with the same specialized add4ByteLifetimeOption: + case ICMP6_RDNSS_OPTION_TYPE: + mRdnssOptionOffsets.add(position); + lifetime = add4ByteLifetimeOption(optionType, optionLength); + builder.updateRdnssLifetime(lifetime); + break; + case ICMP6_ROUTE_INFO_OPTION_TYPE: + mRioOptionOffsets.add(position); + lifetime = add4ByteLifetimeOption(optionType, optionLength); + builder.updateRouteInfoLifetime(lifetime); + break; + case ICMP6_DNSSL_OPTION_TYPE: + lifetime = add4ByteLifetimeOption(optionType, optionLength); + builder.updateDnsslLifetime(lifetime); + break; + default: + // RFC4861 section 4.2 dictates we ignore unknown options for forwards + // compatibility. + mPacket.position(position + optionLength); + break; + } + if (optionLength <= 0) { + throw new InvalidRaException(String.format( + "Invalid option length opt=%d len=%d", optionType, optionLength)); + } + } + mMinLifetime = minLifetime(); + mMetricsLog.log(builder.build()); + } + + // Considering only the MATCH sections, does {@code packet} match this RA? + boolean matches(byte[] packet, int length) { + if (length != mPacket.capacity()) return false; + byte[] referencePacket = mPacket.array(); + for (PacketSection section : mPacketSections) { + if (section.type != PacketSection.Type.MATCH) continue; + for (int i = section.start; i < (section.start + section.length); i++) { + if (packet[i] != referencePacket[i]) return false; + } + } + return true; + } + + // What is the minimum of all lifetimes within {@code packet} in seconds? + // Precondition: matches(packet, length) already returned true. + long minLifetime() { + long minLifetime = Long.MAX_VALUE; + for (PacketSection section : mPacketSections) { + if (isRelevantLifetime(section)) { + minLifetime = Math.min(minLifetime, section.lifetime); + } + } + return minLifetime; + } + + // How many seconds does this RA's have to live, taking into account the fact + // that we might have seen it a while ago. + long currentLifetime() { + return mMinLifetime - (currentTimeSeconds() - mLastSeen); + } + + boolean isExpired() { + // TODO: We may want to handle 0 lifetime RAs differently, if they are common. We'll + // have to calculate the filter lifetime specially as a fraction of 0 is still 0. + return currentLifetime() <= 0; + } + + // Filter for a fraction of the lifetime and adjust for the age of the RA. + @GuardedBy("ApfFilter.this") + int filterLifetime() { + return (int) (mMinLifetime / FRACTION_OF_LIFETIME_TO_FILTER) + - (int) (mProgramBaseTime - mLastSeen); + } + + @GuardedBy("ApfFilter.this") + boolean shouldFilter() { + return filterLifetime() > 0; + } + + // Append a filter for this RA to {@code gen}. Jump to DROP_LABEL if it should be dropped. + // Jump to the next filter if packet doesn't match this RA. + // Return Long.MAX_VALUE if we don't install any filter program for this RA. As the return + // value of this function is used to calculate the program min lifetime (which corresponds + // to the smallest generated filter lifetime). Returning Long.MAX_VALUE in the case no + // filter gets generated makes sure the program lifetime stays unaffected. + @GuardedBy("ApfFilter.this") + long generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException { + String nextFilterLabel = "Ra" + getUniqueNumberLocked(); + // Skip if packet is not the right size + gen.addLoadFromMemory(Register.R0, gen.PACKET_SIZE_MEMORY_SLOT); + gen.addJumpIfR0NotEquals(mPacket.capacity(), nextFilterLabel); + // Skip filter if expired + gen.addLoadFromMemory(Register.R0, gen.FILTER_AGE_MEMORY_SLOT); + gen.addJumpIfR0GreaterThan(filterLifetime(), nextFilterLabel); + for (PacketSection section : mPacketSections) { + // Generate code to match the packet bytes. + if (section.type == PacketSection.Type.MATCH) { + gen.addLoadImmediate(Register.R0, section.start); + gen.addJumpIfBytesNotEqual(Register.R0, + Arrays.copyOfRange(mPacket.array(), section.start, + section.start + section.length), + nextFilterLabel); + } + + // Generate code to test the lifetimes haven't gone down too far. + // The packet is accepted if any non-ignored lifetime is lower than filterLifetime. + if (isRelevantLifetime(section)) { + switch (section.length) { + case 4: gen.addLoad32(Register.R0, section.start); break; + case 2: gen.addLoad16(Register.R0, section.start); break; + default: + throw new IllegalStateException( + "bogus lifetime size " + section.length); + } + gen.addJumpIfR0LessThan(filterLifetime(), nextFilterLabel); + } + } + maybeSetupCounter(gen, Counter.DROPPED_RA); + gen.addJump(mCountAndDropLabel); + gen.defineLabel(nextFilterLabel); + return filterLifetime(); + } + } + + // TODO: Refactor these subclasses to avoid so much repetition. + private abstract static class KeepalivePacket { + // Note that the offset starts from IP header. + // These must be added ether header length when generating program. + static final int IP_HEADER_OFFSET = 0; + static final int IPV4_SRC_ADDR_OFFSET = IP_HEADER_OFFSET + 12; + + // Append a filter for this keepalive ack to {@code gen}. + // Jump to drop if it matches the keepalive ack. + // Jump to the next filter if packet doesn't match the keepalive ack. + abstract void generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException; + } + + // A class to hold NAT-T keepalive ack information. + private class NattKeepaliveResponse extends KeepalivePacket { + static final int UDP_LENGTH_OFFSET = 4; + static final int UDP_HEADER_LEN = 8; + + protected class NattKeepaliveResponseData { + public final byte[] srcAddress; + public final int srcPort; + public final byte[] dstAddress; + public final int dstPort; + + NattKeepaliveResponseData(final NattKeepalivePacketDataParcelable sentKeepalivePacket) { + srcAddress = sentKeepalivePacket.dstAddress; + srcPort = sentKeepalivePacket.dstPort; + dstAddress = sentKeepalivePacket.srcAddress; + dstPort = sentKeepalivePacket.srcPort; + } + } + + protected final NattKeepaliveResponseData mPacket; + protected final byte[] mSrcDstAddr; + protected final byte[] mPortFingerprint; + // NAT-T keepalive packet + protected final byte[] mPayload = {(byte) 0xff}; + + NattKeepaliveResponse(final NattKeepalivePacketDataParcelable sentKeepalivePacket) { + mPacket = new NattKeepaliveResponseData(sentKeepalivePacket); + mSrcDstAddr = concatArrays(mPacket.srcAddress, mPacket.dstAddress); + mPortFingerprint = generatePortFingerprint(mPacket.srcPort, mPacket.dstPort); + } + + byte[] generatePortFingerprint(int srcPort, int dstPort) { + final ByteBuffer fp = ByteBuffer.allocate(4); + fp.order(ByteOrder.BIG_ENDIAN); + fp.putShort((short) srcPort); + fp.putShort((short) dstPort); + return fp.array(); + } + + @Override + void generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException { + final String nextFilterLabel = "natt_keepalive_filter" + getUniqueNumberLocked(); + + gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET); + gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel); + + // A NAT-T keepalive packet contains 1 byte payload with the value 0xff + // Check payload length is 1 + gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT); + gen.addAdd(UDP_HEADER_LEN); + gen.addSwap(); + gen.addLoad16(Register.R0, IPV4_TOTAL_LENGTH_OFFSET); + gen.addNeg(Register.R1); + gen.addAddR1(); + gen.addJumpIfR0NotEquals(1, nextFilterLabel); + + // Check that the ports match + gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT); + gen.addAdd(ETH_HEADER_LEN); + gen.addJumpIfBytesNotEqual(Register.R0, mPortFingerprint, nextFilterLabel); + + // Payload offset = R0 + UDP header length + gen.addAdd(UDP_HEADER_LEN); + gen.addJumpIfBytesNotEqual(Register.R0, mPayload, nextFilterLabel); + + maybeSetupCounter(gen, Counter.DROPPED_IPV4_NATT_KEEPALIVE); + gen.addJump(mCountAndDropLabel); + gen.defineLabel(nextFilterLabel); + } + + public String toString() { + try { + return String.format("%s -> %s", + ConnectivityUtils.addressAndPortToString( + InetAddress.getByAddress(mPacket.srcAddress), mPacket.srcPort), + ConnectivityUtils.addressAndPortToString( + InetAddress.getByAddress(mPacket.dstAddress), mPacket.dstPort)); + } catch (UnknownHostException e) { + return "Unknown host"; + } + } + } + + // A class to hold TCP keepalive ack information. + private abstract static class TcpKeepaliveAck extends KeepalivePacket { + protected static class TcpKeepaliveAckData { + public final byte[] srcAddress; + public final int srcPort; + public final byte[] dstAddress; + public final int dstPort; + public final int seq; + public final int ack; + + // Create the characteristics of the ack packet from the sent keepalive packet. + TcpKeepaliveAckData(final TcpKeepalivePacketDataParcelable sentKeepalivePacket) { + srcAddress = sentKeepalivePacket.dstAddress; + srcPort = sentKeepalivePacket.dstPort; + dstAddress = sentKeepalivePacket.srcAddress; + dstPort = sentKeepalivePacket.srcPort; + seq = sentKeepalivePacket.ack; + ack = sentKeepalivePacket.seq + 1; + } + } + + protected final TcpKeepaliveAckData mPacket; + protected final byte[] mSrcDstAddr; + protected final byte[] mPortSeqAckFingerprint; + + TcpKeepaliveAck(final TcpKeepaliveAckData packet, final byte[] srcDstAddr) { + mPacket = packet; + mSrcDstAddr = srcDstAddr; + mPortSeqAckFingerprint = generatePortSeqAckFingerprint(mPacket.srcPort, + mPacket.dstPort, mPacket.seq, mPacket.ack); + } + + static byte[] generatePortSeqAckFingerprint(int srcPort, int dstPort, int seq, int ack) { + final ByteBuffer fp = ByteBuffer.allocate(12); + fp.order(ByteOrder.BIG_ENDIAN); + fp.putShort((short) srcPort); + fp.putShort((short) dstPort); + fp.putInt(seq); + fp.putInt(ack); + return fp.array(); + } + + public String toString() { + try { + return String.format("%s -> %s , seq=%d, ack=%d", + ConnectivityUtils.addressAndPortToString( + InetAddress.getByAddress(mPacket.srcAddress), mPacket.srcPort), + ConnectivityUtils.addressAndPortToString( + InetAddress.getByAddress(mPacket.dstAddress), mPacket.dstPort), + Integer.toUnsignedLong(mPacket.seq), + Integer.toUnsignedLong(mPacket.ack)); + } catch (UnknownHostException e) { + return "Unknown host"; + } + } + + // Append a filter for this keepalive ack to {@code gen}. + // Jump to drop if it matches the keepalive ack. + // Jump to the next filter if packet doesn't match the keepalive ack. + abstract void generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException; + } + + private class TcpKeepaliveAckV4 extends TcpKeepaliveAck { + + TcpKeepaliveAckV4(final TcpKeepalivePacketDataParcelable sentKeepalivePacket) { + this(new TcpKeepaliveAckData(sentKeepalivePacket)); + } + TcpKeepaliveAckV4(final TcpKeepaliveAckData packet) { + super(packet, concatArrays(packet.srcAddress, packet.dstAddress) /* srcDstAddr */); + } + + @Override + void generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException { + final String nextFilterLabel = "keepalive_ack" + getUniqueNumberLocked(); + + gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET); + gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel); + + // Skip to the next filter if it's not zero-sized : + // TCP_HEADER_SIZE + IPV4_HEADER_SIZE - ipv4_total_length == 0 + // Load the IP header size into R1 + gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT); + // Load the TCP header size into R0 (it's indexed by R1) + gen.addLoad8Indexed(Register.R0, ETH_HEADER_LEN + TCP_HEADER_SIZE_OFFSET); + // Size offset is in the top nibble, but it must be multiplied by 4, and the two + // top bits of the low nibble are guaranteed to be zeroes. Right-shift R0 by 2. + gen.addRightShift(2); + // R0 += R1 -> R0 contains TCP + IP headers length + gen.addAddR1(); + // Load IPv4 total length + gen.addLoad16(Register.R1, IPV4_TOTAL_LENGTH_OFFSET); + gen.addNeg(Register.R0); + gen.addAddR1(); + gen.addJumpIfR0NotEquals(0, nextFilterLabel); + // Add IPv4 header length + gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT); + gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN); + gen.addAddR1(); + gen.addJumpIfBytesNotEqual(Register.R0, mPortSeqAckFingerprint, nextFilterLabel); + + maybeSetupCounter(gen, Counter.DROPPED_IPV4_KEEPALIVE_ACK); + gen.addJump(mCountAndDropLabel); + gen.defineLabel(nextFilterLabel); + } + } + + private class TcpKeepaliveAckV6 extends TcpKeepaliveAck { + TcpKeepaliveAckV6(final TcpKeepalivePacketDataParcelable sentKeepalivePacket) { + this(new TcpKeepaliveAckData(sentKeepalivePacket)); + } + TcpKeepaliveAckV6(final TcpKeepaliveAckData packet) { + super(packet, concatArrays(packet.srcAddress, packet.dstAddress) /* srcDstAddr */); + } + + @Override + void generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException { + throw new UnsupportedOperationException("IPv6 TCP Keepalive is not supported yet"); + } + } + + // Maximum number of RAs to filter for. + private static final int MAX_RAS = 10; + + @GuardedBy("this") + private ArrayList<Ra> mRas = new ArrayList<>(); + @GuardedBy("this") + private SparseArray<KeepalivePacket> mKeepalivePackets = new SparseArray<>(); + @GuardedBy("this") + private final List<String[]> mMdnsAllowList = new ArrayList<>(); + + // There is always some marginal benefit to updating the installed APF program when an RA is + // seen because we can extend the program's lifetime slightly, but there is some cost to + // updating the program, so don't bother unless the program is going to expire soon. This + // constant defines "soon" in seconds. + private static final long MAX_PROGRAM_LIFETIME_WORTH_REFRESHING = 30; + // We don't want to filter an RA for it's whole lifetime as it'll be expired by the time we ever + // see a refresh. Using half the lifetime might be a good idea except for the fact that + // packets may be dropped, so let's use 6. + private static final int FRACTION_OF_LIFETIME_TO_FILTER = 6; + + // The base time for this filter program. In seconds since Unix Epoch. + // This is the time when the APF program was generated. All filters in the program should use + // this base time as their current time for consistency purposes. + @GuardedBy("this") + private long mProgramBaseTime; + // When did we last install a filter program? In seconds since Unix Epoch. + @GuardedBy("this") + private long mLastTimeInstalledProgram; + // How long should the last installed filter program live for? In seconds. + @GuardedBy("this") + private long mLastInstalledProgramMinLifetime; + @GuardedBy("this") + private ApfProgramEvent.Builder mLastInstallEvent; + + // For debugging only. The last program installed. + @GuardedBy("this") + private byte[] mLastInstalledProgram; + + /** + * For debugging only. Contains the latest APF buffer snapshot captured from the firmware. + * + * A typical size for this buffer is 4KB. It is present only if the WiFi HAL supports + * IWifiStaIface#readApfPacketFilterData(), and the APF interpreter advertised support for + * the opcodes to access the data buffer (LDDW and STDW). + */ + @GuardedBy("this") @Nullable + private byte[] mDataSnapshot; + + // How many times the program was updated since we started. + @GuardedBy("this") + private int mNumProgramUpdates = 0; + // How many times the program was updated since we started for allowing multicast traffic. + @GuardedBy("this") + private int mNumProgramUpdatesAllowingMulticast = 0; + + /** + * Generate filter code to process ARP packets. Execution of this code ends in either the + * DROP_LABEL or PASS_LABEL and does not fall off the end. + * Preconditions: + * - Packet being filtered is ARP + */ + @GuardedBy("this") + private void generateArpFilterLocked(ApfGenerator gen) throws IllegalInstructionException { + // Here's a basic summary of what the ARP filter program does: + // + // if not ARP IPv4 + // pass + // if not ARP IPv4 reply or request + // pass + // if ARP reply source ip is 0.0.0.0 + // drop + // if unicast ARP reply + // pass + // if interface has no IPv4 address + // if target ip is 0.0.0.0 + // drop + // else + // if target ip is not the interface ip + // drop + // pass + + final String checkTargetIPv4 = "checkTargetIPv4"; + + // Pass if not ARP IPv4. + gen.addLoadImmediate(Register.R0, ARP_HEADER_OFFSET); + maybeSetupCounter(gen, Counter.PASSED_ARP_NON_IPV4); + gen.addJumpIfBytesNotEqual(Register.R0, ARP_IPV4_HEADER, mCountAndPassLabel); + + // Pass if unknown ARP opcode. + gen.addLoad16(Register.R0, ARP_OPCODE_OFFSET); + gen.addJumpIfR0Equals(ARP_OPCODE_REQUEST, checkTargetIPv4); // Skip to unicast check + maybeSetupCounter(gen, Counter.PASSED_ARP_UNKNOWN); + gen.addJumpIfR0NotEquals(ARP_OPCODE_REPLY, mCountAndPassLabel); + + // Drop if ARP reply source IP is 0.0.0.0 + gen.addLoad32(Register.R0, ARP_SOURCE_IP_ADDRESS_OFFSET); + maybeSetupCounter(gen, Counter.DROPPED_ARP_REPLY_SPA_NO_HOST); + gen.addJumpIfR0Equals(IPV4_ANY_HOST_ADDRESS, mCountAndDropLabel); + + // Pass if unicast reply. + gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET); + maybeSetupCounter(gen, Counter.PASSED_ARP_UNICAST_REPLY); + gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel); + + // Either a unicast request, a unicast reply, or a broadcast reply. + gen.defineLabel(checkTargetIPv4); + if (mIPv4Address == null) { + // When there is no IPv4 address, drop GARP replies (b/29404209). + gen.addLoad32(Register.R0, ARP_TARGET_IP_ADDRESS_OFFSET); + maybeSetupCounter(gen, Counter.DROPPED_GARP_REPLY); + gen.addJumpIfR0Equals(IPV4_ANY_HOST_ADDRESS, mCountAndDropLabel); + } else { + // When there is an IPv4 address, drop unicast/broadcast requests + // and broadcast replies with a different target IPv4 address. + gen.addLoadImmediate(Register.R0, ARP_TARGET_IP_ADDRESS_OFFSET); + maybeSetupCounter(gen, Counter.DROPPED_ARP_OTHER_HOST); + gen.addJumpIfBytesNotEqual(Register.R0, mIPv4Address, mCountAndDropLabel); + } + + maybeSetupCounter(gen, Counter.PASSED_ARP); + gen.addJump(mCountAndPassLabel); + } + + /** + * Generate filter code to process IPv4 packets. Execution of this code ends in either the + * DROP_LABEL or PASS_LABEL and does not fall off the end. + * Preconditions: + * - Packet being filtered is IPv4 + */ + @GuardedBy("this") + private void generateIPv4FilterLocked(ApfGenerator gen) throws IllegalInstructionException { + // Here's a basic summary of what the IPv4 filter program does: + // + // if filtering multicast (i.e. multicast lock not held): + // if it's DHCP destined to our MAC: + // pass + // if it's L2 broadcast: + // drop + // if it's IPv4 multicast: + // drop + // if it's IPv4 broadcast: + // drop + // if keepalive ack + // drop + // pass + + if (mMulticastFilter) { + final String skipDhcpv4Filter = "skip_dhcp_v4_filter"; + + // Pass DHCP addressed to us. + // Check it's UDP. + gen.addLoad8(Register.R0, IPV4_PROTOCOL_OFFSET); + gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipDhcpv4Filter); + // Check it's not a fragment. This matches the BPF filter installed by the DHCP client. + gen.addLoad16(Register.R0, IPV4_FRAGMENT_OFFSET_OFFSET); + gen.addJumpIfR0AnyBitsSet(IPV4_FRAGMENT_OFFSET_MASK, skipDhcpv4Filter); + // Check it's addressed to DHCP client port. + gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT); + gen.addLoad16Indexed(Register.R0, UDP_DESTINATION_PORT_OFFSET); + gen.addJumpIfR0NotEquals(DHCP_CLIENT_PORT, skipDhcpv4Filter); + // Check it's DHCP to our MAC address. + gen.addLoadImmediate(Register.R0, DHCP_CLIENT_MAC_OFFSET); + // NOTE: Relies on R1 containing IPv4 header offset. + gen.addAddR1(); + gen.addJumpIfBytesNotEqual(Register.R0, mHardwareAddress, skipDhcpv4Filter); + maybeSetupCounter(gen, Counter.PASSED_DHCP); + gen.addJump(mCountAndPassLabel); + + // Drop all multicasts/broadcasts. + gen.defineLabel(skipDhcpv4Filter); + + // If IPv4 destination address is in multicast range, drop. + gen.addLoad8(Register.R0, IPV4_DEST_ADDR_OFFSET); + gen.addAnd(0xf0); + maybeSetupCounter(gen, Counter.DROPPED_IPV4_MULTICAST); + gen.addJumpIfR0Equals(0xe0, mCountAndDropLabel); + + // If IPv4 broadcast packet, drop regardless of L2 (b/30231088). + maybeSetupCounter(gen, Counter.DROPPED_IPV4_BROADCAST_ADDR); + gen.addLoad32(Register.R0, IPV4_DEST_ADDR_OFFSET); + gen.addJumpIfR0Equals(IPV4_BROADCAST_ADDRESS, mCountAndDropLabel); + if (mIPv4Address != null && mIPv4PrefixLength < 31) { + maybeSetupCounter(gen, Counter.DROPPED_IPV4_BROADCAST_NET); + int broadcastAddr = ipv4BroadcastAddress(mIPv4Address, mIPv4PrefixLength); + gen.addJumpIfR0Equals(broadcastAddr, mCountAndDropLabel); + } + + // If any TCP keepalive filter matches, drop + generateV4KeepaliveFilters(gen); + + // If any NAT-T keepalive filter matches, drop + generateV4NattKeepaliveFilters(gen); + + // Otherwise, this is an IPv4 unicast, pass + // If L2 broadcast packet, drop. + // TODO: can we invert this condition to fall through to the common pass case below? + maybeSetupCounter(gen, Counter.PASSED_IPV4_UNICAST); + gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET); + gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel); + maybeSetupCounter(gen, Counter.DROPPED_IPV4_L2_BROADCAST); + gen.addJump(mCountAndDropLabel); + } else { + generateV4KeepaliveFilters(gen); + generateV4NattKeepaliveFilters(gen); + } + + // Otherwise, pass + maybeSetupCounter(gen, Counter.PASSED_IPV4); + gen.addJump(mCountAndPassLabel); + } + + private void generateKeepaliveFilters(ApfGenerator gen, Class<?> filterType, int proto, + int offset, String label) throws IllegalInstructionException { + final boolean haveKeepaliveResponses = CollectionUtils.any(mKeepalivePackets, + ack -> filterType.isInstance(ack)); + + // If no keepalive packets of this type + if (!haveKeepaliveResponses) return; + + // If not the right proto, skip keepalive filters + gen.addLoad8(Register.R0, offset); + gen.addJumpIfR0NotEquals(proto, label); + + // Drop Keepalive responses + for (int i = 0; i < mKeepalivePackets.size(); ++i) { + final KeepalivePacket response = mKeepalivePackets.valueAt(i); + if (filterType.isInstance(response)) response.generateFilterLocked(gen); + } + + gen.defineLabel(label); + } + + private void generateV4KeepaliveFilters(ApfGenerator gen) throws IllegalInstructionException { + generateKeepaliveFilters(gen, TcpKeepaliveAckV4.class, IPPROTO_TCP, IPV4_PROTOCOL_OFFSET, + "skip_v4_keepalive_filter"); + } + + private void generateV4NattKeepaliveFilters(ApfGenerator gen) + throws IllegalInstructionException { + generateKeepaliveFilters(gen, NattKeepaliveResponse.class, + IPPROTO_UDP, IPV4_PROTOCOL_OFFSET, "skip_v4_nattkeepalive_filter"); + } + + /** + * Generate filter code to process IPv6 packets. Execution of this code ends in either the + * DROP_LABEL or PASS_LABEL, or falls off the end for ICMPv6 packets. + * Preconditions: + * - Packet being filtered is IPv6 + */ + @GuardedBy("this") + private void generateIPv6FilterLocked(ApfGenerator gen) throws IllegalInstructionException { + // Here's a basic summary of what the IPv6 filter program does: + // + // if there is a hop-by-hop option present (e.g. MLD query) + // pass + // if we're dropping multicast + // if it's not IPCMv6 or it's ICMPv6 but we're in doze mode: + // if it's multicast: + // drop + // pass + // if it's ICMPv6 RS to any: + // drop + // if it's ICMPv6 NA to anything in ff02::/120 + // drop + // if keepalive ack + // drop + + gen.addLoad8(Register.R0, IPV6_NEXT_HEADER_OFFSET); + + // MLD packets set the router-alert hop-by-hop option. + // TODO: be smarter about not blindly passing every packet with HBH options. + gen.addJumpIfR0Equals(IPPROTO_HOPOPTS, mCountAndPassLabel); + + // Drop multicast if the multicast filter is enabled. + if (mMulticastFilter) { + final String skipIPv6MulticastFilterLabel = "skipIPv6MulticastFilter"; + final String dropAllIPv6MulticastsLabel = "dropAllIPv6Multicast"; + + // While in doze mode, drop ICMPv6 multicast pings, let the others pass. + // While awake, let all ICMPv6 multicasts through. + if (mInDozeMode) { + // Not ICMPv6? -> Proceed to multicast filtering + gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, dropAllIPv6MulticastsLabel); + + // ICMPv6 but not ECHO? -> Skip the multicast filter. + // (ICMPv6 ECHO requests will go through the multicast filter below). + gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET); + gen.addJumpIfR0NotEquals(ICMPV6_ECHO_REQUEST_TYPE, skipIPv6MulticastFilterLabel); + } else { + gen.addJumpIfR0Equals(IPPROTO_ICMPV6, skipIPv6MulticastFilterLabel); + } + + // Drop all other packets sent to ff00::/8 (multicast prefix). + gen.defineLabel(dropAllIPv6MulticastsLabel); + maybeSetupCounter(gen, Counter.DROPPED_IPV6_NON_ICMP_MULTICAST); + gen.addLoad8(Register.R0, IPV6_DEST_ADDR_OFFSET); + gen.addJumpIfR0Equals(0xff, mCountAndDropLabel); + // If any keepalive filter matches, drop + generateV6KeepaliveFilters(gen); + // Not multicast. Pass. + maybeSetupCounter(gen, Counter.PASSED_IPV6_UNICAST_NON_ICMP); + gen.addJump(mCountAndPassLabel); + gen.defineLabel(skipIPv6MulticastFilterLabel); + } else { + generateV6KeepaliveFilters(gen); + // If not ICMPv6, pass. + maybeSetupCounter(gen, Counter.PASSED_IPV6_NON_ICMP); + gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, mCountAndPassLabel); + } + + // If we got this far, the packet is ICMPv6. Drop some specific types. + + // Add unsolicited multicast neighbor announcements filter + String skipUnsolicitedMulticastNALabel = "skipUnsolicitedMulticastNA"; + gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET); + // Drop all router solicitations (b/32833400) + maybeSetupCounter(gen, Counter.DROPPED_IPV6_ROUTER_SOLICITATION); + gen.addJumpIfR0Equals(ICMPV6_ROUTER_SOLICITATION, mCountAndDropLabel); + // If not neighbor announcements, skip filter. + gen.addJumpIfR0NotEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, skipUnsolicitedMulticastNALabel); + // Drop all multicast NA to ff02::/120. + // This is a way to cover ff02::1 and ff02::2 with a single JNEBS. + // TODO: Drop only if they don't contain the address of on-link neighbours. + final byte[] unsolicitedNaDropPrefix = Arrays.copyOf(IPV6_ALL_NODES_ADDRESS, 15); + gen.addLoadImmediate(Register.R0, IPV6_DEST_ADDR_OFFSET); + gen.addJumpIfBytesNotEqual(Register.R0, unsolicitedNaDropPrefix, + skipUnsolicitedMulticastNALabel); + + maybeSetupCounter(gen, Counter.DROPPED_IPV6_MULTICAST_NA); + gen.addJump(mCountAndDropLabel); + gen.defineLabel(skipUnsolicitedMulticastNALabel); + } + + /** Encodes qname in TLV pattern. */ + @VisibleForTesting + public static byte[] encodeQname(String[] labels) { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + for (String label : labels) { + byte[] labelBytes = label.getBytes(StandardCharsets.UTF_8); + out.write(labelBytes.length); + out.write(labelBytes, 0, labelBytes.length); + } + out.write(0); + return out.toByteArray(); + } + + /** + * Generate filter code to process mDNS packets. Execution of this code ends in * DROP_LABEL + * or PASS_LABEL if the packet is mDNS packets. Otherwise, skip this check. + */ + @GuardedBy("this") + private void generateMdnsFilterLocked(ApfGenerator gen) + throws IllegalInstructionException { + final String skipMdnsv4Filter = "skip_mdns_v4_filter"; + final String skipMdnsFilter = "skip_mdns_filter"; + final String checkMdnsUdpPort = "check_mdns_udp_port"; + final String mDnsAcceptPacket = "mdns_accept_packet"; + final String mDnsDropPacket = "mdns_drop_packet"; + + // Only turn on the filter if multicast filter is on and the qname allowlist is non-empty. + if (!mMulticastFilter || mMdnsAllowList.isEmpty()) { + return; + } + + // Here's a basic summary of what the mDNS filter program does: + // + // if it is a multicast mDNS packet + // if QDCOUNT != 1 + // pass + // else if the QNAME is in the allowlist + // pass + // else: + // drop + // + // A packet is considered as a multicast mDNS packet if it matches all the following + // conditions + // 1. its destination MAC address matches 01:00:5E:00:00:FB or 33:33:00:00:00:FB, for + // v4 and v6 respectively. + // 2. it is an IPv4/IPv6 packet + // 3. it is a UDP packet with port 5353 + + // Check it's L2 mDNS multicast address. + gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET); + gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V4_MAC_ADDRESS, + skipMdnsv4Filter); + + // Checks it's IPv4. + gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET); + gen.addJumpIfR0NotEquals(ETH_P_IP, skipMdnsFilter); + + // Checks it's UDP. + gen.addLoad8(Register.R0, IPV4_PROTOCOL_OFFSET); + gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipMdnsFilter); + // Set R1 to IPv4 header. + gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT); + gen.addJump(checkMdnsUdpPort); + + gen.defineLabel(skipMdnsv4Filter); + + // Checks it's L2 mDNS multicast address. + // Relies on R0 containing the ethernet destination mac address offset. + gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V6_MAC_ADDRESS, + skipMdnsFilter); + + // Checks it's IPv6. + gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET); + gen.addJumpIfR0NotEquals(ETH_P_IPV6, skipMdnsFilter); + + // Checks it's UDP. + gen.addLoad8(Register.R0, IPV6_NEXT_HEADER_OFFSET); + gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipMdnsFilter); + + // Set R1 to IPv6 header. + gen.addLoadImmediate(Register.R1, IPV6_HEADER_LEN); + + // Checks it's mDNS UDP port + gen.defineLabel(checkMdnsUdpPort); + gen.addLoad16Indexed(Register.R0, UDP_DESTINATION_PORT_OFFSET); + gen.addJumpIfR0NotEquals(MDNS_PORT, skipMdnsFilter); + + gen.addLoad16Indexed(Register.R0, MDNS_QDCOUNT_OFFSET); + // If QDCOUNT != 1, pass the packet + gen.addJumpIfR0NotEquals(1, mDnsAcceptPacket); + + // If QDCOUNT == 1, matches the QNAME with allowlist. + // Load offset for the first QNAME. + gen.addLoadImmediate(Register.R0, MDNS_QNAME_OFFSET); + gen.addAddR1(); + + // Check first QNAME against allowlist + for (int i = 0; i < mMdnsAllowList.size(); ++i) { + final String mDnsNextAllowedQnameCheck = "mdns_next_allowed_qname_check" + i; + final byte[] encodedQname = encodeQname(mMdnsAllowList.get(i)); + gen.addJumpIfBytesNotEqual(Register.R0, encodedQname, mDnsNextAllowedQnameCheck); + // QNAME matched + gen.addJump(mDnsAcceptPacket); + // QNAME not matched + gen.defineLabel(mDnsNextAllowedQnameCheck); + } + // If QNAME doesn't match any entries in allowlist, drop the packet. + gen.defineLabel(mDnsDropPacket); + maybeSetupCounter(gen, Counter.DROPPED_MDNS); + gen.addJump(mCountAndDropLabel); + + gen.defineLabel(mDnsAcceptPacket); + maybeSetupCounter(gen, Counter.PASSED_MDNS); + gen.addJump(mCountAndPassLabel); + + + gen.defineLabel(skipMdnsFilter); + } + + + private void generateV6KeepaliveFilters(ApfGenerator gen) throws IllegalInstructionException { + generateKeepaliveFilters(gen, TcpKeepaliveAckV6.class, IPPROTO_TCP, IPV6_NEXT_HEADER_OFFSET, + "skip_v6_keepalive_filter"); + } + + /** + * Begin generating an APF program to: + * <ul> + * <li>Drop/Pass 802.3 frames (based on policy) + * <li>Drop packets with EtherType within the Black List + * <li>Drop ARP requests not for us, if mIPv4Address is set, + * <li>Drop IPv4 broadcast packets, except DHCP destined to our MAC, + * <li>Drop IPv4 multicast packets, if mMulticastFilter, + * <li>Pass all other IPv4 packets, + * <li>Drop all broadcast non-IP non-ARP packets. + * <li>Pass all non-ICMPv6 IPv6 packets, + * <li>Pass all non-IPv4 and non-IPv6 packets, + * <li>Drop IPv6 ICMPv6 NAs to anything in ff02::/120. + * <li>Drop IPv6 ICMPv6 RSs. + * <li>Filter IPv4 packets (see generateIPv4FilterLocked()) + * <li>Filter IPv6 packets (see generateIPv6FilterLocked()) + * <li>Let execution continue off the end of the program for IPv6 ICMPv6 packets. This allows + * insertion of RA filters here, or if there aren't any, just passes the packets. + * </ul> + */ + @GuardedBy("this") + private ApfGenerator emitPrologueLocked() throws IllegalInstructionException { + // This is guaranteed to succeed because of the check in maybeCreate. + ApfGenerator gen = new ApfGenerator(mApfCapabilities.apfVersionSupported); + + if (mApfCapabilities.hasDataAccess()) { + // Increment TOTAL_PACKETS + maybeSetupCounter(gen, Counter.TOTAL_PACKETS); + gen.addLoadData(Register.R0, 0); // load counter + gen.addAdd(1); + gen.addStoreData(Register.R0, 0); // write-back counter + } + + // Here's a basic summary of what the initial program does: + // + // if it's a 802.3 Frame (ethtype < 0x0600): + // drop or pass based on configurations + // if it has a ether-type that belongs to the black list + // drop + // if it's ARP: + // insert ARP filter to drop or pass these appropriately + // if it's IPv4: + // insert IPv4 filter to drop or pass these appropriately + // if it's not IPv6: + // if it's broadcast: + // drop + // pass + // insert IPv6 filter to drop, pass, or fall off the end for ICMPv6 packets + + gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET); + + if (mDrop802_3Frames) { + // drop 802.3 frames (ethtype < 0x0600) + maybeSetupCounter(gen, Counter.DROPPED_802_3_FRAME); + gen.addJumpIfR0LessThan(ETH_TYPE_MIN, mCountAndDropLabel); + } + + // Handle ether-type black list + maybeSetupCounter(gen, Counter.DROPPED_ETHERTYPE_BLACKLISTED); + for (int p : mEthTypeBlackList) { + gen.addJumpIfR0Equals(p, mCountAndDropLabel); + } + + // Add ARP filters: + String skipArpFiltersLabel = "skipArpFilters"; + gen.addJumpIfR0NotEquals(ETH_P_ARP, skipArpFiltersLabel); + generateArpFilterLocked(gen); + gen.defineLabel(skipArpFiltersLabel); + + // Add mDNS filter: + generateMdnsFilterLocked(gen); + gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET); + + // Add IPv4 filters: + String skipIPv4FiltersLabel = "skipIPv4Filters"; + gen.addJumpIfR0NotEquals(ETH_P_IP, skipIPv4FiltersLabel); + generateIPv4FilterLocked(gen); + gen.defineLabel(skipIPv4FiltersLabel); + + // Check for IPv6: + // NOTE: Relies on R0 containing ethertype. This is safe because if we got here, we did + // not execute the IPv4 filter, since this filter do not fall through, but either drop or + // pass. + String ipv6FilterLabel = "IPv6Filters"; + gen.addJumpIfR0Equals(ETH_P_IPV6, ipv6FilterLabel); + + // Drop non-IP non-ARP broadcasts, pass the rest + gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET); + maybeSetupCounter(gen, Counter.PASSED_NON_IP_UNICAST); + gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel); + maybeSetupCounter(gen, Counter.DROPPED_ETH_BROADCAST); + gen.addJump(mCountAndDropLabel); + + // Add IPv6 filters: + gen.defineLabel(ipv6FilterLabel); + generateIPv6FilterLocked(gen); + return gen; + } + + /** + * Append packet counting epilogue to the APF program. + * + * Currently, the epilogue consists of two trampolines which count passed and dropped packets + * before jumping to the actual PASS and DROP labels. + */ + @GuardedBy("this") + private void emitEpilogue(ApfGenerator gen) throws IllegalInstructionException { + // If APFv4 is unsupported, no epilogue is necessary: if execution reached this far, it + // will just fall-through to the PASS label. + if (!mApfCapabilities.hasDataAccess()) return; + + // Execution will reach the bottom of the program if none of the filters match, + // which will pass the packet to the application processor. + maybeSetupCounter(gen, Counter.PASSED_IPV6_ICMP); + + // Append the count & pass trampoline, which increments the counter at the data address + // pointed to by R1, then jumps to the pass label. This saves a few bytes over inserting + // the entire sequence inline for every counter. + gen.defineLabel(mCountAndPassLabel); + gen.addLoadData(Register.R0, 0); // R0 = *(R1 + 0) + gen.addAdd(1); // R0++ + gen.addStoreData(Register.R0, 0); // *(R1 + 0) = R0 + gen.addJump(gen.PASS_LABEL); + + // Same as above for the count & drop trampoline. + gen.defineLabel(mCountAndDropLabel); + gen.addLoadData(Register.R0, 0); // R0 = *(R1 + 0) + gen.addAdd(1); // R0++ + gen.addStoreData(Register.R0, 0); // *(R1 + 0) = R0 + gen.addJump(gen.DROP_LABEL); + } + + /** + * Generate and install a new filter program. + */ + @GuardedBy("this") + @VisibleForTesting + public void installNewProgramLocked() { + purgeExpiredRasLocked(); + ArrayList<Ra> rasToFilter = new ArrayList<>(); + final byte[] program; + long programMinLifetime = Long.MAX_VALUE; + long maximumApfProgramSize = mApfCapabilities.maximumApfProgramSize; + if (mApfCapabilities.hasDataAccess()) { + // Reserve space for the counters. + maximumApfProgramSize -= Counter.totalSize(); + } + + mProgramBaseTime = currentTimeSeconds(); + try { + // Step 1: Determine how many RA filters we can fit in the program. + ApfGenerator gen = emitPrologueLocked(); + + // The epilogue normally goes after the RA filters, but add it early to include its + // length when estimating the total. + emitEpilogue(gen); + + // Can't fit the program even without any RA filters? + if (gen.programLengthOverEstimate() > maximumApfProgramSize) { + Log.e(TAG, "Program exceeds maximum size " + maximumApfProgramSize); + return; + } + + for (Ra ra : mRas) { + if (!ra.shouldFilter()) continue; + ra.generateFilterLocked(gen); + // Stop if we get too big. + if (gen.programLengthOverEstimate() > maximumApfProgramSize) { + if (VDBG) Log.d(TAG, "Past maximum program size, skipping RAs"); + break; + } + + rasToFilter.add(ra); + } + + // Step 2: Actually generate the program + gen = emitPrologueLocked(); + for (Ra ra : rasToFilter) { + programMinLifetime = Math.min(programMinLifetime, ra.generateFilterLocked(gen)); + } + emitEpilogue(gen); + program = gen.generate(); + } catch (IllegalInstructionException|IllegalStateException e) { + Log.e(TAG, "Failed to generate APF program.", e); + return; + } + mIpClientCallback.installPacketFilter(program); + mLastTimeInstalledProgram = mProgramBaseTime; + mLastInstalledProgramMinLifetime = programMinLifetime; + mLastInstalledProgram = program; + mNumProgramUpdates++; + + if (VDBG) { + hexDump("Installing filter: ", program, program.length); + } + logApfProgramEventLocked(mProgramBaseTime); + mLastInstallEvent = new ApfProgramEvent.Builder() + .setLifetime(programMinLifetime) + .setFilteredRas(rasToFilter.size()) + .setCurrentRas(mRas.size()) + .setProgramLength(program.length) + .setFlags(mIPv4Address != null, mMulticastFilter); + } + + @GuardedBy("this") + private void logApfProgramEventLocked(long now) { + if (mLastInstallEvent == null) { + return; + } + ApfProgramEvent.Builder ev = mLastInstallEvent; + mLastInstallEvent = null; + final long actualLifetime = now - mLastTimeInstalledProgram; + ev.setActualLifetime(actualLifetime); + if (actualLifetime < APF_PROGRAM_EVENT_LIFETIME_THRESHOLD) { + return; + } + mMetricsLog.log(ev.build()); + } + + /** + * Returns {@code true} if a new program should be installed because the current one dies soon. + */ + private boolean shouldInstallnewProgram() { + long expiry = mLastTimeInstalledProgram + mLastInstalledProgramMinLifetime; + return expiry < currentTimeSeconds() + MAX_PROGRAM_LIFETIME_WORTH_REFRESHING; + } + + private void hexDump(String msg, byte[] packet, int length) { + log(msg + HexDump.toHexString(packet, 0, length, false /* lowercase */)); + } + + @GuardedBy("this") + private void purgeExpiredRasLocked() { + for (int i = 0; i < mRas.size();) { + if (mRas.get(i).isExpired()) { + log("Expiring " + mRas.get(i)); + mRas.remove(i); + } else { + i++; + } + } + } + + /** + * Process an RA packet, updating the list of known RAs and installing a new APF program + * if the current APF program should be updated. + * @return a ProcessRaResult enum describing what action was performed. + */ + @VisibleForTesting + public synchronized ProcessRaResult processRa(byte[] packet, int length) { + if (VDBG) hexDump("Read packet = ", packet, length); + + // Have we seen this RA before? + for (int i = 0; i < mRas.size(); i++) { + Ra ra = mRas.get(i); + if (ra.matches(packet, length)) { + if (VDBG) log("matched RA " + ra); + // Update lifetimes. + ra.mLastSeen = currentTimeSeconds(); + ra.seenCount++; + + // Keep mRas in LRU order so as to prioritize generating filters for recently seen + // RAs. LRU prioritizes this because RA filters are generated in order from mRas + // until the filter program exceeds the maximum filter program size allowed by the + // chipset, so RAs appearing earlier in mRas are more likely to make it into the + // filter program. + // TODO: consider sorting the RAs in order of increasing expiry time as well. + // Swap to front of array. + mRas.add(0, mRas.remove(i)); + + // If the current program doesn't expire for a while, don't update. + if (shouldInstallnewProgram()) { + installNewProgramLocked(); + return ProcessRaResult.UPDATE_EXPIRY; + } + return ProcessRaResult.MATCH; + } + } + purgeExpiredRasLocked(); + // TODO: figure out how to proceed when we've received more than MAX_RAS RAs. + if (mRas.size() >= MAX_RAS) { + return ProcessRaResult.DROPPED; + } + final Ra ra; + try { + ra = new Ra(packet, length); + } catch (Exception e) { + Log.e(TAG, "Error parsing RA", e); + return ProcessRaResult.PARSE_ERROR; + } + // Ignore 0 lifetime RAs. + if (ra.isExpired()) { + return ProcessRaResult.ZERO_LIFETIME; + } + log("Adding " + ra); + mRas.add(ra); + installNewProgramLocked(); + return ProcessRaResult.UPDATE_NEW_RA; + } + + /** + * Create an {@link LegacyApfFilter} if {@code apfCapabilities} indicates support for packet + * filtering using APF programs. + */ + public static LegacyApfFilter maybeCreate(Context context, ApfFilter.ApfConfiguration config, + InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback) { + if (context == null || config == null || ifParams == null) return null; + ApfCapabilities apfCapabilities = config.apfCapabilities; + if (apfCapabilities == null) return null; + if (apfCapabilities.apfVersionSupported == 0) return null; + if (apfCapabilities.maximumApfProgramSize < 512) { + Log.e(TAG, "Unacceptably small APF limit: " + apfCapabilities.maximumApfProgramSize); + return null; + } + // For now only support generating programs for Ethernet frames. If this restriction is + // lifted: + // 1. the program generator will need its offsets adjusted. + // 2. the packet filter attached to our packet socket will need its offset adjusted. + if (apfCapabilities.apfPacketFormat != ARPHRD_ETHER) return null; + if (!ApfGenerator.supportsVersion(apfCapabilities.apfVersionSupported)) { + Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported); + return null; + } + + return new LegacyApfFilter(context, config, ifParams, ipClientCallback, new IpConnectivityLog()); + } + + public synchronized void shutdown() { + if (mReceiveThread != null) { + log("shutting down"); + mReceiveThread.halt(); // Also closes socket. + mReceiveThread = null; + } + mRas.clear(); + mContext.unregisterReceiver(mDeviceIdleReceiver); + } + + public synchronized void setMulticastFilter(boolean isEnabled) { + if (mMulticastFilter == isEnabled) return; + mMulticastFilter = isEnabled; + if (!isEnabled) { + mNumProgramUpdatesAllowingMulticast++; + } + installNewProgramLocked(); + } + + /** Adds qname to the mDNS allowlist */ + public synchronized void addToMdnsAllowList(String[] labels) { + mMdnsAllowList.add(labels); + if (mMulticastFilter) { + installNewProgramLocked(); + } + } + + /** Removes qname from the mDNS allowlist */ + public synchronized void removeFromAllowList(String[] labels) { + mMdnsAllowList.removeIf(e -> Arrays.equals(labels, e)); + if (mMulticastFilter) { + installNewProgramLocked(); + } + } + + @VisibleForTesting + public synchronized void setDozeMode(boolean isEnabled) { + if (mInDozeMode == isEnabled) return; + mInDozeMode = isEnabled; + installNewProgramLocked(); + } + + /** Find the single IPv4 LinkAddress if there is one, otherwise return null. */ + private static LinkAddress findIPv4LinkAddress(LinkProperties lp) { + LinkAddress ipv4Address = null; + for (LinkAddress address : lp.getLinkAddresses()) { + if (!(address.getAddress() instanceof Inet4Address)) { + continue; + } + if (ipv4Address != null && !ipv4Address.isSameAddressAs(address)) { + // More than one IPv4 address, abort. + return null; + } + ipv4Address = address; + } + return ipv4Address; + } + + public synchronized void setLinkProperties(LinkProperties lp) { + // NOTE: Do not keep a copy of LinkProperties as it would further duplicate state. + final LinkAddress ipv4Address = findIPv4LinkAddress(lp); + final byte[] addr = (ipv4Address != null) ? ipv4Address.getAddress().getAddress() : null; + final int prefix = (ipv4Address != null) ? ipv4Address.getPrefixLength() : 0; + if ((prefix == mIPv4PrefixLength) && Arrays.equals(addr, mIPv4Address)) { + return; + } + mIPv4Address = addr; + mIPv4PrefixLength = prefix; + installNewProgramLocked(); + } + + /** + * Add TCP keepalive ack packet filter. + * This will add a filter to drop acks to the keepalive packet passed as an argument. + * + * @param slot The index used to access the filter. + * @param sentKeepalivePacket The attributes of the sent keepalive packet. + */ + public synchronized void addTcpKeepalivePacketFilter(final int slot, + final TcpKeepalivePacketDataParcelable sentKeepalivePacket) { + log("Adding keepalive ack(" + slot + ")"); + if (null != mKeepalivePackets.get(slot)) { + throw new IllegalArgumentException("Keepalive slot " + slot + " is occupied"); + } + final int ipVersion = sentKeepalivePacket.srcAddress.length == 4 ? 4 : 6; + mKeepalivePackets.put(slot, (ipVersion == 4) + ? new TcpKeepaliveAckV4(sentKeepalivePacket) + : new TcpKeepaliveAckV6(sentKeepalivePacket)); + installNewProgramLocked(); + } + + /** + * Add NAT-T keepalive packet filter. + * This will add a filter to drop NAT-T keepalive packet which is passed as an argument. + * + * @param slot The index used to access the filter. + * @param sentKeepalivePacket The attributes of the sent keepalive packet. + */ + public synchronized void addNattKeepalivePacketFilter(final int slot, + final NattKeepalivePacketDataParcelable sentKeepalivePacket) { + log("Adding NAT-T keepalive packet(" + slot + ")"); + if (null != mKeepalivePackets.get(slot)) { + throw new IllegalArgumentException("NAT-T Keepalive slot " + slot + " is occupied"); + } + + // TODO : update ApfFilter to support dropping v6 keepalives + if (sentKeepalivePacket.srcAddress.length != 4) { + return; + } + + mKeepalivePackets.put(slot, new NattKeepaliveResponse(sentKeepalivePacket)); + installNewProgramLocked(); + } + + /** + * Remove keepalive packet filter. + * + * @param slot The index used to access the filter. + */ + public synchronized void removeKeepalivePacketFilter(int slot) { + log("Removing keepalive packet(" + slot + ")"); + mKeepalivePackets.remove(slot); + installNewProgramLocked(); + } + + static public long counterValue(byte[] data, Counter counter) + throws ArrayIndexOutOfBoundsException { + // Follow the same wrap-around addressing scheme of the interpreter. + int offset = counter.offset(); + if (offset < 0) { + offset = data.length + offset; + } + + // Decode 32bit big-endian integer into a long so we can count up beyond 2^31. + long value = 0; + for (int i = 0; i < 4; i++) { + value = value << 8 | (data[offset] & 0xFF); + offset++; + } + return value; + } + + public synchronized void dump(IndentingPrintWriter pw) { + pw.println("Capabilities: " + mApfCapabilities); + pw.println("Receive thread: " + (mReceiveThread != null ? "RUNNING" : "STOPPED")); + pw.println("Multicast: " + (mMulticastFilter ? "DROP" : "ALLOW")); + pw.println("Minimum RDNSS lifetime: " + mMinRdnssLifetimeSec); + try { + pw.println("IPv4 address: " + InetAddress.getByAddress(mIPv4Address).getHostAddress()); + } catch (UnknownHostException|NullPointerException e) {} + + if (mLastTimeInstalledProgram == 0) { + pw.println("No program installed."); + return; + } + pw.println("Program updates: " + mNumProgramUpdates); + pw.println(String.format( + "Last program length %d, installed %ds ago, lifetime %ds", + mLastInstalledProgram.length, currentTimeSeconds() - mLastTimeInstalledProgram, + mLastInstalledProgramMinLifetime)); + + pw.print("Denylisted Ethertypes:"); + for (int p : mEthTypeBlackList) { + pw.print(String.format(" %04x", p)); + } + pw.println(); + pw.println("RA filters:"); + pw.increaseIndent(); + for (Ra ra: mRas) { + pw.println(ra); + pw.increaseIndent(); + pw.println(String.format( + "Seen: %d, last %ds ago", ra.seenCount, currentTimeSeconds() - ra.mLastSeen)); + if (DBG) { + pw.println("Last match:"); + pw.increaseIndent(); + pw.println(ra.getLastMatchingPacket()); + pw.decreaseIndent(); + } + pw.decreaseIndent(); + } + pw.decreaseIndent(); + + pw.println("TCP Keepalive filters:"); + pw.increaseIndent(); + for (int i = 0; i < mKeepalivePackets.size(); ++i) { + final KeepalivePacket keepalivePacket = mKeepalivePackets.valueAt(i); + if (keepalivePacket instanceof TcpKeepaliveAck) { + pw.print("Slot "); + pw.print(mKeepalivePackets.keyAt(i)); + pw.print(": "); + pw.println(keepalivePacket); + } + } + pw.decreaseIndent(); + + pw.println("NAT-T Keepalive filters:"); + pw.increaseIndent(); + for (int i = 0; i < mKeepalivePackets.size(); ++i) { + final KeepalivePacket keepalivePacket = mKeepalivePackets.valueAt(i); + if (keepalivePacket instanceof NattKeepaliveResponse) { + pw.print("Slot "); + pw.print(mKeepalivePackets.keyAt(i)); + pw.print(": "); + pw.println(keepalivePacket); + } + } + pw.decreaseIndent(); + + if (DBG) { + pw.println("Last program:"); + pw.increaseIndent(); + pw.println(HexDump.toHexString(mLastInstalledProgram, false /* lowercase */)); + pw.decreaseIndent(); + } + + pw.println("APF packet counters: "); + pw.increaseIndent(); + if (!mApfCapabilities.hasDataAccess()) { + pw.println("APF counters not supported"); + } else if (mDataSnapshot == null) { + pw.println("No last snapshot."); + } else { + try { + Counter[] counters = Counter.class.getEnumConstants(); + for (Counter c : Arrays.asList(counters).subList(1, counters.length)) { + long value = counterValue(mDataSnapshot, c); + // Only print non-zero counters + if (value != 0) { + pw.println(c.toString() + ": " + value); + } + } + } catch (ArrayIndexOutOfBoundsException e) { + pw.println("Uh-oh: " + e); + } + if (VDBG) { + pw.println("Raw data dump: "); + pw.println(HexDump.dumpHexString(mDataSnapshot)); + } + } + pw.decreaseIndent(); + } + + // TODO: move to android.net.NetworkUtils + @VisibleForTesting + public static int ipv4BroadcastAddress(byte[] addrBytes, int prefixLength) { + return bytesToBEInt(addrBytes) | (int) (Integer.toUnsignedLong(-1) >>> prefixLength); + } + + private static int uint8(byte b) { + return b & 0xff; + } + + private static int getUint16(ByteBuffer buffer, int position) { + return buffer.getShort(position) & 0xffff; + } + + private static long getUint32(ByteBuffer buffer, int position) { + return Integer.toUnsignedLong(buffer.getInt(position)); + } + + private static int getUint8(ByteBuffer buffer, int position) { + return uint8(buffer.get(position)); + } + + private static int bytesToBEInt(byte[] bytes) { + return (uint8(bytes[0]) << 24) + + (uint8(bytes[1]) << 16) + + (uint8(bytes[2]) << 8) + + (uint8(bytes[3])); + } + + private static byte[] concatArrays(final byte[]... arr) { + int size = 0; + for (byte[] a : arr) { + size += a.length; + } + final byte[] result = new byte[size]; + int offset = 0; + for (byte[] a : arr) { + System.arraycopy(a, 0, result, offset, a.length); + offset += a.length; + } + return result; + } +}
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java index e6339a0..b09a2b1 100644 --- a/src/android/net/dhcp/DhcpClient.java +++ b/src/android/net/dhcp/DhcpClient.java
@@ -75,7 +75,6 @@ import android.net.networkstack.aidl.dhcp.DhcpOption; import android.net.util.HostnameTransliterator; import android.net.util.SocketUtils; -import android.os.Build; import android.os.Handler; import android.os.Message; import android.os.PowerManager; @@ -105,7 +104,6 @@ import com.android.networkstack.R; import com.android.networkstack.apishim.CaptivePortalDataShimImpl; import com.android.networkstack.apishim.SocketUtilsShimImpl; -import com.android.networkstack.apishim.common.ShimUtils; import com.android.networkstack.metrics.IpProvisioningMetrics; import com.android.networkstack.util.NetworkStackUtils; @@ -446,12 +444,18 @@ /** * Return whether a feature guarded by a feature flag is enabled. - * @see DeviceConfigUtils#isFeatureEnabled(Context, String, String) + * @see DeviceConfigUtils#isNetworkStackFeatureEnabled(Context, String) */ - public boolean isFeatureEnabled(final Context context, final String name, - boolean defaultEnabled) { - return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name, - defaultEnabled); + public boolean isFeatureEnabled(final Context context, final String name) { + return DeviceConfigUtils.isNetworkStackFeatureEnabled(context, name); + } + + /** + * Check whether one specific feature is not disabled. + * @see DeviceConfigUtils#isNetworkStackFeatureNotChickenedOut(Context, String) + */ + public boolean isFeatureNotChickenedOut(final Context context, final String name) { + return DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(context, name); } /** @@ -543,32 +547,23 @@ /** * check whether or not to support caching the last lease info and INIT-REBOOT state. - * - * INIT-REBOOT state is supported on Android R by default if there is no experiment flag set to - * disable this feature explicitly, meanwhile turning this feature on/off by pushing experiment - * flag makes it possible to do A/B test and metrics collection on both of Android Q and R, but - * it's disabled on Android Q by default. */ public boolean isDhcpLeaseCacheEnabled() { - final boolean defaultEnabled = - ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q); - return mDependencies.isFeatureEnabled(mContext, DHCP_INIT_REBOOT_VERSION, defaultEnabled); + return mDependencies.isFeatureNotChickenedOut(mContext, DHCP_INIT_REBOOT_VERSION); } /** * check whether or not to support DHCP Rapid Commit option. */ public boolean isDhcpRapidCommitEnabled() { - return mDependencies.isFeatureEnabled(mContext, DHCP_RAPID_COMMIT_VERSION, - false /* defaultEnabled */); + return mDependencies.isFeatureEnabled(mContext, DHCP_RAPID_COMMIT_VERSION); } /** * check whether or not to support IP address conflict detection and DHCPDECLINE. */ public boolean isDhcpIpConflictDetectEnabled() { - return mDependencies.isFeatureEnabled(mContext, DHCP_IP_CONFLICT_DETECT_VERSION, - false /* defaultEnabled */); + return mDependencies.isFeatureEnabled(mContext, DHCP_IP_CONFLICT_DETECT_VERSION); } /** @@ -578,8 +573,7 @@ * disable this feature explicitly. */ public boolean isIPv6OnlyPreferredModeEnabled() { - return mDependencies.isFeatureEnabled(mContext, DHCP_IPV6_ONLY_PREFERRED_VERSION, - true /* defaultEnabled */); + return mDependencies.isFeatureNotChickenedOut(mContext, DHCP_IPV6_ONLY_PREFERRED_VERSION); } /** @@ -587,8 +581,7 @@ * suggested in RFC2131 section 4.4.5. */ public boolean isSlowRetransmissionEnabled() { - return mDependencies.isFeatureEnabled(mContext, DHCP_SLOW_RETRANSMISSION_VERSION, - false /* defaultEnabled */); + return mDependencies.isFeatureEnabled(mContext, DHCP_SLOW_RETRANSMISSION_VERSION); } private void recordMetricEnabledFeatures() {
diff --git a/src/android/net/dhcp/DhcpPacket.java b/src/android/net/dhcp/DhcpPacket.java index 65145e8..770baac 100644 --- a/src/android/net/dhcp/DhcpPacket.java +++ b/src/android/net/dhcp/DhcpPacket.java
@@ -1283,8 +1283,14 @@ captivePortalUrl = readAsciiString(packet, optionLen, true); break; case DHCP_IPV6_ONLY_PREFERRED: - expectedLen = 4; - ipv6OnlyWaitTime = Integer.valueOf(packet.getInt()); + if (optionLen == 4) { + expectedLen = optionLen; + ipv6OnlyWaitTime = Integer.valueOf(packet.getInt()); + } else { + // rfc8925#section-3.1: The client MUST ignore the IPv6-Only + // Preferred option if the length field value is not 4. + expectedLen = skipOption(packet, optionLen); + } break; default: expectedLen = skipOption(packet, optionLen);
diff --git a/src/android/net/dhcp/DhcpServer.java b/src/android/net/dhcp/DhcpServer.java index dac9258..041417d 100644 --- a/src/android/net/dhcp/DhcpServer.java +++ b/src/android/net/dhcp/DhcpServer.java
@@ -23,7 +23,6 @@ import static android.net.dhcp.IDhcpServer.STATUS_INVALID_ARGUMENT; import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS; import static android.net.dhcp.IDhcpServer.STATUS_UNKNOWN_ERROR; -import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; import static android.system.OsConstants.AF_INET; import static android.system.OsConstants.IPPROTO_UDP; import static android.system.OsConstants.SOCK_DGRAM; @@ -233,7 +232,7 @@ @Override public boolean isFeatureEnabled(@NonNull Context context, @NonNull String name) { - return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name); + return DeviceConfigUtils.isNetworkStackFeatureEnabled(context, name); } }
diff --git a/src/android/net/dhcp6/Dhcp6Client.java b/src/android/net/dhcp6/Dhcp6Client.java index d16dad4..7359c7c 100644 --- a/src/android/net/dhcp6/Dhcp6Client.java +++ b/src/android/net/dhcp6/Dhcp6Client.java
@@ -16,6 +16,7 @@ package android.net.dhcp6; +import static android.net.dhcp6.Dhcp6Packet.IAID; import static android.net.dhcp6.Dhcp6Packet.PrefixDelegation; import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; import static android.system.OsConstants.AF_INET6; @@ -63,10 +64,10 @@ import java.io.IOException; import java.net.Inet6Address; import java.net.SocketException; -import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Random; +import java.util.function.IntSupplier; /** * A DHCPv6 client. @@ -93,6 +94,7 @@ public static final int DHCP6_PD_SUCCESS = 1; public static final int DHCP6_PD_PREFIX_EXPIRED = 2; public static final int DHCP6_PD_PREFIX_CHANGED = 3; + public static final int DHCP6_PD_PREFIX_MSG_EXCHANGE_TERMINATED = 4; // Notification from DHCPv6 state machine before quitting public static final int CMD_ON_QUIT = PUBLIC_BASE + 4; @@ -105,18 +107,20 @@ private static final int CMD_DHCP6_PD_REBIND = PRIVATE_BASE + 4; private static final int CMD_DHCP6_PD_EXPIRE = PRIVATE_BASE + 5; - // Timers and timeouts. - // TODO: comply with RFC8415 section 15(Reliability of Client-Initiated Message Exchanges) - private static final int SECONDS = 1000; - private static final int FIRST_TIMEOUT_MS = 1 * SECONDS; - private static final int MAX_TIMEOUT_MS = 512 * SECONDS; + // Transmission and Retransmission parameters in milliseconds. + private static final int SECONDS = 1000; + private static final int SOL_TIMEOUT = 1 * SECONDS; + private static final int SOL_MAX_RT = 3600 * SECONDS; + private static final int REQ_TIMEOUT = 1 * SECONDS; + private static final int REQ_MAX_RT = 30 * SECONDS; + private static final int REQ_MAX_RC = 10; + private static final int REN_TIMEOUT = 10 * SECONDS; + private static final int REN_MAX_RT = 600 * SECONDS; + private static final int REB_TIMEOUT = 10 * SECONDS; + private static final int REB_MAX_RT = 600 * SECONDS; - // Per rfc8415#section-12, the IAID MUST be consistent across restarts. - // Since currently only one IAID is supported, a well-known value can be used (0). - private static final int IAID = 0; + private int mSolMaxRtMs = SOL_MAX_RT; - private int mTransId; - private long mTransStartMillis; @Nullable private PrefixDelegation mAdvertise; @Nullable private PrefixDelegation mReply; @Nullable private byte[] mServerDuid; @@ -225,24 +229,70 @@ } /** - * Retransmits packets using jittered exponential backoff with an optional timeout. Packet - * transmission is triggered by CMD_KICK, which is sent by an AlarmManager alarm. Kicks are - * cancelled when leaving the state. + * Retransmits packets per algorithm defined in RFC8415 section 15. Packet transmission is + * triggered by CMD_KICK, which is sent by an AlarmManager alarm. Kicks are cancelled when + * leaving the state. * - * Concrete subclasses must implement sendPacket, which is called when the alarm fires and a - * packet needs to be transmitted, and receivePacket, which is triggered by CMD_RECEIVED_PACKET - * sent by the receive thread. - * - * TODO: deduplicate with the similar code in DhcpClient.java + * Concrete subclasses must initialize retransmission parameters and implement sendPacket, + * which is called when the alarm fires and a packet needs to be transmitted, and receivePacket, + * which is triggered by CMD_RECEIVED_PACKET sent by the receive thread. */ - abstract class PacketRetransmittingState extends State { - private int mTimer; + abstract class MessageExchangeState extends State { + private int mTransId = 0; + private long mTransStartMs = 0; + private long mMaxRetransTimeMs = 0; + + private long mRetransTimeout = -1; + private int mRetransCount = 0; + private final long mInitialDelayMs; + private final long mInitialRetransTimeMs; + private final int mMaxRetransCount; + private final IntSupplier mMaxRetransTimeSupplier; + + MessageExchangeState(final int delay, final int irt, final int mrc, final IntSupplier mrt) { + mInitialDelayMs = delay; + mInitialRetransTimeMs = irt; + mMaxRetransCount = mrc; + mMaxRetransTimeSupplier = mrt; + } @Override public void enter() { super.enter(); - mTimer = FIRST_TIMEOUT_MS; - sendMessage(CMD_KICK); + mMaxRetransTimeMs = mMaxRetransTimeSupplier.getAsInt(); + // Every message exchange generates a new transaction id. + mTransId = mRandom.nextInt() & 0xffffff; + sendMessageDelayed(CMD_KICK, mInitialDelayMs); + } + + private void handleKick() { + // rfc8415#section-21.9: The elapsed time is measured from the time at which the + // client sent the first message in the message exchange, and the elapsed-time field + // is set to 0 in the first message in the message exchange. + final long elapsedTimeMs; + if (mRetransCount == 0) { + elapsedTimeMs = 0; + mTransStartMs = SystemClock.elapsedRealtime(); + } else { + elapsedTimeMs = SystemClock.elapsedRealtime() - mTransStartMs; + } + + sendPacket(mTransId, elapsedTimeMs); + // Compares retransmission parameters and reschedules alarm accordingly. + scheduleKick(); + } + + private void handleReceivedPacket(Dhcp6Packet packet) { + // Technically it is valid for the server to not include a prefix in an IA in certain + // scenarios (specifically in a reply to Renew / Rebind, which means: do not extend the + // prefix). However, while only supporting a single prefix, this never works well, so if + // the server decides to do so, ignore it. + // TODO: revisit this when adding multi-prefix support. + final boolean validIpo = packet.mPrefixDelegation.ipo != null + && packet.mPrefixDelegation.ipo.isValid(); + if (packet.isValid(mTransId, mClientDuid) && validIpo) { + receivePacket(packet); + } } @Override @@ -253,11 +303,10 @@ switch (message.what) { case CMD_KICK: - sendPacket(); - scheduleKick(); + handleKick(); return HANDLED; case CMD_RECEIVED_PACKET: - receivePacket((Dhcp6Packet) message.obj); + handleReceivedPacket((Dhcp6Packet) message.obj); return HANDLED; default: return NOT_HANDLED; @@ -268,26 +317,58 @@ public void exit() { super.exit(); mKickAlarm.cancel(); + mRetransTimeout = -1; + mRetransCount = 0; + mMaxRetransTimeMs = 0; } - protected abstract boolean sendPacket(); + protected abstract boolean sendPacket(int transId, long elapsedTimeMs); protected abstract void receivePacket(Dhcp6Packet packet); + // If the message exchange is considered to have failed according to the retransmission + // mechanism(i.e. client has transmitted the message MRC times or MRD seconds has elapsed + // since the first message transmission), this method will be called to roll back to Solicit + // state and restart the configuration, and notify IpClient the DHCPv6 message exchange + // failure if needed. + protected void onMessageExchangeFailed() {} - protected int jitterTimer(int baseTimer) { - int maxJitter = baseTimer / 10; - int jitter = mRandom.nextInt(2 * maxJitter) - maxJitter; - return baseTimer + jitter; + /** + * Per RFC8415 section 15, each of the computations of a new RT includes a randomization + * factor (RAND), which is a random number chosen with a uniform distribution between -0.1 + * and +0.1. + */ + private double rand() { + return mRandom.nextDouble() / 5 - 0.1; } protected void scheduleKick() { - long now = SystemClock.elapsedRealtime(); - long timeout = jitterTimer(mTimer); - long alarmTime = now + timeout; - mKickAlarm.schedule(alarmTime); - mTimer *= 2; - if (mTimer > MAX_TIMEOUT_MS) { - mTimer = MAX_TIMEOUT_MS; + if (mRetransTimeout == -1) { + // RT for the first message transmission is based on IRT. + mRetransTimeout = mInitialRetransTimeMs + (long) (rand() * mInitialRetransTimeMs); + } else { + // RT for each subsequent message transmission is based on the previous value of RT. + mRetransTimeout = 2 * mRetransTimeout + (long) (rand() * mRetransTimeout); } + if (mMaxRetransTimeMs != 0 && mRetransTimeout > mMaxRetransTimeMs) { + mRetransTimeout = mMaxRetransTimeMs + (long) (rand() * mMaxRetransTimeMs); + } + // Per RFC8415 section 18.2.4 and 18.2.5, MRD equals to the remaining time until + // earliest T2(RenewState) or valid lifetimes of all leases in all IA have expired + // (RebindState), and message exchange is terminated when the earliest time T2 is + // reached, at which point client begins the Rebind message exchange, however, section + // 15 says the message exchange fails(terminated) once MRD seconds have elapsed since + // the client first transmitted the message. So far MRD is being used for Renew, Rebind + // and Confirm message retransmission. Given we don't support Confirm message yet, we + // can just use rebindTimeout and expirationTimeout on behalf of MRD which have been + // scheduled in BoundState to simplify the implementation, therefore, we don't need to + // explicitly assign the MRD in the subclasses. + if (mMaxRetransCount != 0 && mRetransCount > mMaxRetransCount) { + onMessageExchangeFailed(); + Log.i(TAG, "client has transmitted the message " + mMaxRetransCount + + " times, stopping retransmission"); + return; + } + mKickAlarm.schedule(SystemClock.elapsedRealtime() + mRetransTimeout); + mRetransCount++; } } @@ -349,41 +430,33 @@ mAdvertise = null; mReply = null; mServerDuid = null; - } - - private void startNewTransaction() { - mTransId = mRandom.nextInt() & 0xffffff; - mTransStartMillis = SystemClock.elapsedRealtime(); - } - - private long getElapsedTimeMs() { - return SystemClock.elapsedRealtime() - mTransStartMillis; + mSolMaxRtMs = SOL_MAX_RT; } @SuppressWarnings("ByteBufferBackingArray") - private boolean sendSolicitPacket(final ByteBuffer iapd) { - final ByteBuffer packet = Dhcp6Packet.buildSolicitPacket(mTransId, - getElapsedTimeMs(), iapd.array(), mClientDuid, true /* rapidCommit */); + private boolean sendSolicitPacket(int transId, long elapsedTimeMs, final ByteBuffer iapd) { + final ByteBuffer packet = Dhcp6Packet.buildSolicitPacket(transId, elapsedTimeMs, + iapd.array(), mClientDuid, true /* rapidCommit */); return transmitPacket(packet, "solicit"); } @SuppressWarnings("ByteBufferBackingArray") - private boolean sendRequestPacket(final ByteBuffer iapd) { - final ByteBuffer packet = Dhcp6Packet.buildRequestPacket(mTransId, getElapsedTimeMs(), + private boolean sendRequestPacket(int transId, long elapsedTimeMs, final ByteBuffer iapd) { + final ByteBuffer packet = Dhcp6Packet.buildRequestPacket(transId, elapsedTimeMs, iapd.array(), mClientDuid, mServerDuid); return transmitPacket(packet, "request"); } @SuppressWarnings("ByteBufferBackingArray") - private boolean sendRenewPacket(final ByteBuffer iapd) { - final ByteBuffer packet = Dhcp6Packet.buildRenewPacket(mTransId, getElapsedTimeMs(), + private boolean sendRenewPacket(int transId, long elapsedTimeMs, final ByteBuffer iapd) { + final ByteBuffer packet = Dhcp6Packet.buildRenewPacket(transId, elapsedTimeMs, iapd.array(), mClientDuid, mServerDuid); return transmitPacket(packet, "renew"); } @SuppressWarnings("ByteBufferBackingArray") - private boolean sendRebindPacket(final ByteBuffer iapd) { - final ByteBuffer packet = Dhcp6Packet.buildRebindPacket(mTransId, getElapsedTimeMs(), + private boolean sendRebindPacket(int transId, long elapsedTimeMs, final ByteBuffer iapd) { + final ByteBuffer packet = Dhcp6Packet.buildRebindPacket(transId, elapsedTimeMs, iapd.array(), mClientDuid); return transmitPacket(packet, "rebind"); } @@ -457,42 +530,45 @@ * * Note: Not implement DHCPv6 server selection, always request the first Advertise we receive. */ - class SolicitState extends PacketRetransmittingState { - @Override - public void enter() { - super.enter(); - startNewTransaction(); + class SolicitState extends MessageExchangeState { + SolicitState() { + // First Solicit message should be delayed by a random amount of time between 0 + // and SOL_MAX_DELAY(1s). + super((int) (new Random().nextDouble() * SECONDS) /* delay */, SOL_TIMEOUT /* IRT */, + 0 /* MRC */, () -> mSolMaxRtMs /* MRT */); } @Override - protected boolean sendPacket() { - return sendSolicitPacket(buildEmptyIaPdOption()); + public void enter() { + super.enter(); + } + + @Override + protected boolean sendPacket(int transId, long elapsedTimeMs) { + return sendSolicitPacket(transId, elapsedTimeMs, buildEmptyIaPdOption()); } // TODO: support multiple prefixes. @Override protected void receivePacket(Dhcp6Packet packet) { - if (!packet.isValid(mTransId, mClientDuid)) return; + final PrefixDelegation pd = packet.mPrefixDelegation; if (packet instanceof Dhcp6AdvertisePacket) { - mAdvertise = packet.mPrefixDelegation; - if (mAdvertise != null && mAdvertise.iaid == IAID) { - Log.d(TAG, "Get prefix delegation option from Advertise: " + mAdvertise); - mServerDuid = packet.mServerDuid; - transitionTo(mRequestState); - } + Log.d(TAG, "Get prefix delegation option from Advertise: " + pd); + mAdvertise = pd; + mServerDuid = packet.mServerDuid; + mSolMaxRtMs = packet.getSolMaxRtMs().orElse(mSolMaxRtMs); + transitionTo(mRequestState); } else if (packet instanceof Dhcp6ReplyPacket) { if (!packet.mRapidCommit) { - Log.e(TAG, "Server responded to SOLICIT with REPLY without rapid commit option" + Log.e(TAG, "Server responded to Solicit with Reply without rapid commit option" + ", ignoring"); return; } - final PrefixDelegation pd = packet.mPrefixDelegation; - if (pd != null && pd.iaid == IAID) { - Log.d(TAG, "Get prefix delegation option from RapidCommit Reply: " + pd); - mReply = pd; - mServerDuid = packet.mServerDuid; - transitionTo(mBoundState); - } + Log.d(TAG, "Get prefix delegation option from RapidCommit Reply: " + pd); + mReply = pd; + mServerDuid = packet.mServerDuid; + mSolMaxRtMs = packet.getSolMaxRtMs().orElse(mSolMaxRtMs); + transitionTo(mBoundState); } } } @@ -501,22 +577,30 @@ * Client (re)transmits a Request message to request configuration from a specific server and * process the Reply message in this state. */ - class RequestState extends PacketRetransmittingState { + class RequestState extends MessageExchangeState { + RequestState() { + super(0 /* delay */, REQ_TIMEOUT /* IRT */, REQ_MAX_RC /* MRC */, + () -> REQ_MAX_RT /* MRT */); + } + @Override - protected boolean sendPacket() { - return sendRequestPacket(buildIaPdOption(mAdvertise)); + protected boolean sendPacket(int transId, long elapsedTimeMs) { + return sendRequestPacket(transId, elapsedTimeMs, buildIaPdOption(mAdvertise)); } @Override protected void receivePacket(Dhcp6Packet packet) { if (!(packet instanceof Dhcp6ReplyPacket)) return; - if (!packet.isValid(mTransId, mClientDuid)) return; final PrefixDelegation pd = packet.mPrefixDelegation; - if (pd != null && pd.iaid == IAID) { - Log.d(TAG, "Get prefix delegation option from Reply: " + pd); - mReply = pd; - transitionTo(mBoundState); - } + Log.d(TAG, "Get prefix delegation option from Reply: " + pd); + mReply = pd; + mSolMaxRtMs = packet.getSolMaxRtMs().orElse(mSolMaxRtMs); + transitionTo(mBoundState); + } + + @Override + protected void onMessageExchangeFailed() { + transitionTo(mSolicitState); } } @@ -558,29 +642,16 @@ // TODO: roll back to SOLICIT state after a delay if something wrong happens // instead of returning directly. - if (!Dhcp6Packet.hasValidPrefixDelegation(mReply)) { - Log.e(TAG, "Invalid prefix delegatioin " + mReply); - return; - } - // Configure the IPv6 addresses based on the delegated prefix on the interface. - // We've checked that delegated prefix is valid upon receiving the response - // from DHCPv6 server, and the server may assign a prefix with length less - // than 64. So for SLAAC use case we always set the prefix length to 64 even - // if the delegated prefix length is less than 64. - final IpPrefix prefix; - try { - prefix = new IpPrefix(Inet6Address.getByAddress(mReply.ipo.prefix), - RFC7421_PREFIX_LENGTH); - } catch (UnknownHostException e) { - Log.wtf(TAG, "Invalid delegated prefix " - + HexDump.toHexString(mReply.ipo.prefix)); - return; - } - // Create an IPv6 address from the interface mac address with IFA_F_MANAGETEMPADDR - // flag, kernel will create another privacy IPv6 address on behalf of user space. - // We don't need to remember IPv6 addresses that need to extend the lifetime every - // time it enters BoundState. - final Inet6Address address = createInet6AddressFromEui64(prefix, + // The server may assign a prefix with length less than 64. To support automatic address + // generation (with IFA_F_MANAGETEMPADDR), we always set the address prefix length to + // 64, even if the delegated prefix length is less than 64. However, the unreachable + // route should still use the assigned prefix length. + final IpPrefix routePrefix = mReply.ipo.getIpPrefix(); + final IpPrefix addressPrefix = new IpPrefix(routePrefix.getAddress(), + RFC7421_PREFIX_LENGTH); + // Create EUI-64, so we don't need to remember IPv6 addresses that need to extend the + // lifetime every time it enters BoundState. + final Inet6Address address = createInet6AddressFromEui64(addressPrefix, macAddressToEui64(mIface.macAddr)); final int flags = IFA_F_NOPREFIXROUTE | IFA_F_MANAGETEMPADDR | IFA_F_NODAD; final long now = SystemClock.elapsedRealtime(); @@ -616,39 +687,38 @@ } } - abstract class ReacquireState extends PacketRetransmittingState { + abstract class ReacquireState extends MessageExchangeState { + ReacquireState(final int irt, final int mrt) { + super(0 /* delay */, irt, 0 /* MRC */, () -> mrt /* MRT */); + } + @Override public void enter() { super.enter(); - startNewTransaction(); } @Override protected void receivePacket(Dhcp6Packet packet) { if (!(packet instanceof Dhcp6ReplyPacket)) return; - if (!packet.isValid(mTransId, mClientDuid)) return; final PrefixDelegation pd = packet.mPrefixDelegation; - if (pd != null) { - if (pd.iaid != IAID - || !(Arrays.equals(pd.ipo.prefix, mReply.ipo.prefix) - && pd.ipo.prefixLen == mReply.ipo.prefixLen)) { - Log.i(TAG, "Renewal prefix " + HexDump.toHexString(pd.ipo.prefix) - + " does not match current prefix " - + HexDump.toHexString(mReply.ipo.prefix)); - notifyPrefixDelegation(DHCP6_PD_PREFIX_CHANGED, null); - transitionTo(mSolicitState); - return; - } - mReply = pd; - mServerDuid = packet.mServerDuid; - // Once the delegated prefix gets refreshed successfully we have to extend the - // preferred lifetime and valid lifetime of global IPv6 addresses, otherwise - // these addresses will become depreacated finally and then provisioning failure - // happens. So we transit to mBoundState to update the address with refreshed - // preferred and valid lifetime via sending RTM_NEWADDR message, going back to - // Bound state after a success update. - transitionTo(mBoundState); + if (!(Arrays.equals(pd.ipo.prefix, mReply.ipo.prefix) + && pd.ipo.prefixLen == mReply.ipo.prefixLen)) { + Log.i(TAG, "Renewal prefix " + HexDump.toHexString(pd.ipo.prefix) + + " does not match current prefix " + + HexDump.toHexString(mReply.ipo.prefix)); + notifyPrefixDelegation(DHCP6_PD_PREFIX_CHANGED, null); + transitionTo(mSolicitState); + return; } + mReply = pd; + mServerDuid = packet.mServerDuid; + // Once the delegated prefix gets refreshed successfully we have to extend the + // preferred lifetime and valid lifetime of global IPv6 addresses, otherwise + // these addresses will become depreacated finally and then provisioning failure + // happens. So we transit to mBoundState to update the address with refreshed + // preferred and valid lifetime via sending RTM_NEWADDR message, going back to + // Bound state after a success update. + transitionTo(mBoundState); } } @@ -658,6 +728,10 @@ * extend the lifetimes on the leases assigned to the client. */ class RenewState extends ReacquireState { + RenewState() { + super(REN_TIMEOUT, REN_MAX_RT); + } + @Override public boolean processMessage(Message message) { if (super.processMessage(message) == HANDLED) { @@ -673,8 +747,8 @@ } @Override - protected boolean sendPacket() { - return sendRenewPacket(buildIaPdOption(mReply)); + protected boolean sendPacket(int transId, long elapsedTimeMs) { + return sendRenewPacket(transId, elapsedTimeMs, buildIaPdOption(mReply)); } } @@ -684,9 +758,13 @@ * update other configuration parameters. */ class RebindState extends ReacquireState { + RebindState() { + super(REB_TIMEOUT, REB_MAX_RT); + } + @Override - protected boolean sendPacket() { - return sendRebindPacket(buildIaPdOption(mReply)); + protected boolean sendPacket(int transId, long elapsedTimeMs) { + return sendRebindPacket(transId, elapsedTimeMs, buildIaPdOption(mReply)); } } @@ -723,16 +801,6 @@ return mUdpSock; } - @Override - protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception { - try { - return Os.read(fd, packetBuffer, 0, packetBuffer.length); - } catch (IOException | ErrnoException e) { - Log.e(TAG, "Fail to read packet"); - throw e; - } - } - public int transmitPacket(final ByteBuffer buf) throws ErrnoException, SocketException { int ret = Os.sendto(mUdpSock, buf.array(), 0 /* byteOffset */, buf.limit() /* byteCount */, 0 /* flags */, ALL_DHCP_RELAY_AGENTS_AND_SERVERS,
diff --git a/src/android/net/dhcp6/Dhcp6Packet.java b/src/android/net/dhcp6/Dhcp6Packet.java index 8a11547..540a670 100644 --- a/src/android/net/dhcp6/Dhcp6Packet.java +++ b/src/android/net/dhcp6/Dhcp6Packet.java
@@ -34,6 +34,7 @@ import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.OptionalInt; /** * Defines basic data and operations needed to build and use packets for the @@ -76,6 +77,11 @@ protected final byte[] mServerDuid; /** + * DHCPv6 Optional Type: Option Request Option. + */ + public static final byte DHCP6_OPTION_REQUEST_OPTION = 6; + + /** * DHCPv6 Optional Type: Elapsed time. * This time is expressed in hundredths of a second. */ @@ -114,6 +120,12 @@ protected PrefixDelegation mPrefixDelegation; /** + * DHCPv6 Optional Type: SOL_MAX_RT. + */ + public static final byte DHCP6_SOL_MAX_RT = 82; + private OptionalInt mSolMaxRt; + + /** * The transaction identifier used in this particular DHCPv6 negotiation */ protected final int mTransId; @@ -122,6 +134,9 @@ * The unique identifier for IA_NA, IA_TA, IA_PD used in this particular DHCPv6 negotiation */ protected int mIaId; + // Per rfc8415#section-12, the IAID MUST be consistent across restarts. + // Since currently only one IAID is supported, a well-known value can be used (0). + public static final int IAID = 0; Dhcp6Packet(int transId, int elapsedTime, @NonNull final byte[] clientDuid, final byte[] serverDuid, @NonNull final byte[] iapd) { @@ -162,6 +177,13 @@ } /** + * Returns the SOL_MAX_RT option value in milliseconds. + */ + public OptionalInt getSolMaxRtMs() { + return mSolMaxRt; + } + + /** * A class to take DHCPv6 IA_PD option allocated from server. * https://www.rfc-editor.org/rfc/rfc8415.html#section-21.21 */ @@ -178,6 +200,29 @@ this.ipo = ipo; } + /** + * Check whether or not the delegated prefix in DHCPv6 packet is valid. + * + * TODO: ensure that the prefix has a reasonable lifetime, and the timers aren't too short. + */ + public boolean isValid() { + if (iaid != IAID) { + Log.w(TAG, "IA_ID doesn't match, expected: " + IAID + ", actual: " + iaid); + return false; + } + if (t1 < 0 || t2 < 0) { + Log.e(TAG, "IA_PD option with invalid T1 " + t1 + " or T2 " + t2); + return false; + } + + // Generally, t1 must be smaller or equal to t2 (except when t2 is 0). + if (t2 != 0 && t1 > t2) { + Log.e(TAG, "IA_PD option with T1 " + t1 + " greater than T2 " + t2); + return false; + } + return true; + } + @Override public String toString() { return "Prefix Delegation: iaid " + iaid + ", t1 " + t1 + ", t2 " + t2 @@ -257,6 +302,7 @@ short statusCode = STATUS_SUCCESS; String statusMsg = null; boolean rapidCommit = false; + int solMaxRt = 0; packet.order(ByteOrder.BIG_ENDIAN); @@ -315,6 +361,10 @@ statusCode = packet.getShort(); statusMsg = readAsciiString(packet, expectedLen - 2, false /* isNullOk */); break; + case DHCP6_SOL_MAX_RT: + expectedLen = 4; + solMaxRt = packet.getInt(); + break; default: expectedLen = optionLen; // BufferUnderflowException will be thrown if option is truncated. @@ -372,6 +422,10 @@ newPacket.mStatusCode = statusCode; newPacket.mStatusMsg = statusMsg; newPacket.mRapidCommit = rapidCommit; + newPacket.mSolMaxRt = + (solMaxRt >= 60 && solMaxRt <= 86400) + ? OptionalInt.of(solMaxRt * 1000) + : OptionalInt.empty(); return newPacket; } @@ -402,49 +456,15 @@ Log.e(TAG, "Unexpected transaction ID " + mTransId + ", expected " + transId); return false; } - return true; - } - - /** - * Check whether or not the delegated prefix in DHCPv6 packet is valid. - * - * TODO: ensure that the prefix has a reasonable lifetime, and the timers aren't too short. - */ - public static boolean hasValidPrefixDelegation(@NonNull final PrefixDelegation pd) { - if (pd == null) { - Log.e(TAG, "DHCPv6 packet without IA_PD option, ignoring"); + if (mPrefixDelegation == null) { + Log.e(TAG, "DHCPv6 message without IA_PD option, ignoring"); return false; } - if (pd.ipo.prefixLen > 64) { - Log.e(TAG, "IA_PD option with prefix length " + pd.ipo.prefixLen + " longer than 64"); + if (!mPrefixDelegation.isValid()) { + Log.e(TAG, "DHCPv6 message takes invalid IA_PD option, ignoring"); return false; } - final long t1 = pd.t1; - final long t2 = pd.t2; - if (t1 < 0 || t2 < 0) { - Log.e(TAG, "IA_PD option with invalid T1 " + t1 + " or T2 " + t2); - return false; - } - if (t1 > t2) { - Log.e(TAG, "IA_PD option with T1 " + t1 + " greater than T2 " + t2); - return false; - } - final long preferred = pd.ipo.preferred; - final long valid = pd.ipo.valid; - if (preferred < 0 || valid < 0) { - Log.e(TAG, "IA_PD option with invalid lifetime, preferred lifetime " + preferred - + ", valid lifetime " + valid); - return false; - } - if (preferred > valid) { - Log.e(TAG, "IA_PD option with preferred lifetime " + preferred - + " greater than valid lifetime " + valid); - return false; - } - if (preferred < t2) { - Log.e(TAG, "preferred lifetime " + preferred + " is samller than T2 " + t2); - return false; - } + //TODO: check if the status code is success or not. return true; }
diff --git a/src/android/net/dhcp6/Dhcp6RequestPacket.java b/src/android/net/dhcp6/Dhcp6RequestPacket.java index 0c65f17..6d4dfdf 100644 --- a/src/android/net/dhcp6/Dhcp6RequestPacket.java +++ b/src/android/net/dhcp6/Dhcp6RequestPacket.java
@@ -49,6 +49,7 @@ addTlv(packet, DHCP6_CLIENT_IDENTIFIER, mClientDuid); addTlv(packet, DHCP6_ELAPSED_TIME, (short) (mElapsedTime & 0xFFFF)); addTlv(packet, DHCP6_IA_PD, mIaPd); + addTlv(packet, DHCP6_OPTION_REQUEST_OPTION, DHCP6_SOL_MAX_RT); packet.flip(); return packet;
diff --git a/src/android/net/dhcp6/Dhcp6SolicitPacket.java b/src/android/net/dhcp6/Dhcp6SolicitPacket.java index 4916c1e..5cf5d01 100644 --- a/src/android/net/dhcp6/Dhcp6SolicitPacket.java +++ b/src/android/net/dhcp6/Dhcp6SolicitPacket.java
@@ -48,6 +48,7 @@ addTlv(packet, DHCP6_ELAPSED_TIME, (short) (mElapsedTime & 0xFFFF)); addTlv(packet, DHCP6_CLIENT_IDENTIFIER, mClientDuid); addTlv(packet, DHCP6_IA_PD, mIaPd); + addTlv(packet, DHCP6_OPTION_REQUEST_OPTION, DHCP6_SOL_MAX_RT); if (mRapidCommit) { addTlv(packet, DHCP6_RAPID_COMMIT); }
diff --git a/src/android/net/ip/ConnectivityPacketTracker.java b/src/android/net/ip/ConnectivityPacketTracker.java index 4b92179..51fb428 100644 --- a/src/android/net/ip/ConnectivityPacketTracker.java +++ b/src/android/net/ip/ConnectivityPacketTracker.java
@@ -18,7 +18,6 @@ import static android.net.util.SocketUtils.makePacketSocketAddress; import static android.system.OsConstants.AF_PACKET; -import static android.system.OsConstants.ARPHRD_ETHER; import static android.system.OsConstants.ETH_P_ALL; import static android.system.OsConstants.SOCK_NONBLOCK; import static android.system.OsConstants.SOCK_RAW; @@ -110,7 +109,7 @@ FileDescriptor s = null; try { s = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0); - NetworkStackUtils.attachControlPacketFilter(s, ARPHRD_ETHER); + NetworkStackUtils.attachControlPacketFilter(s); Os.bind(s, makePacketSocketAddress(ETH_P_ALL, mInterface.index)); } catch (ErrnoException | IOException e) { logError("Failed to create packet tracking socket: ", e);
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java index 9fa04ac..11cdd20 100644 --- a/src/android/net/ip/IpClient.java +++ b/src/android/net/ip/IpClient.java
@@ -34,6 +34,7 @@ import static android.system.OsConstants.ETH_P_IPV6; import static android.system.OsConstants.SOCK_NONBLOCK; import static android.system.OsConstants.SOCK_RAW; + import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY; import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST; import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST; @@ -42,6 +43,7 @@ import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION; +import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_IGNORE_LOW_RA_LIFETIME_FORCE_DISABLE; import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION; import static com.android.server.util.PermissionUtil.enforceNetworkStackCallingPermission; @@ -64,8 +66,10 @@ import android.net.RouteInfo; import android.net.TcpKeepalivePacketDataParcelable; import android.net.Uri; +import android.net.apf.AndroidPacketFilter; import android.net.apf.ApfCapabilities; import android.net.apf.ApfFilter; +import android.net.apf.LegacyApfFilter; import android.net.dhcp.DhcpClient; import android.net.dhcp.DhcpPacket; import android.net.dhcp6.Dhcp6Client; @@ -109,6 +113,7 @@ import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.internal.util.WakeupMessage; +import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.CollectionUtils; import com.android.net.module.util.DeviceConfigUtils; import com.android.net.module.util.InterfaceParams; @@ -130,6 +135,7 @@ import com.android.server.NetworkObserverRegistry; import com.android.server.NetworkStackService.NetworkStackServiceManager; +import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; import java.net.Inet4Address; @@ -443,8 +449,14 @@ * Set maximum acceptable DTIM multiplier to hardware driver. */ public void setMaxDtimMultiplier(int multiplier) { - log("setMaxDtimMultiplier(" + multiplier + ")"); try { + // {@link IWifiStaIface#setDtimMultiplier} has been implemented since U, calling + // this method on U- platforms does nothing actually. + if (!SdkLevel.isAtLeastU()) { + log("SDK level is lower than U, do not call setMaxDtimMultiplier method"); + return; + } + log("setMaxDtimMultiplier(" + multiplier + ")"); mCallback.setMaxDtimMultiplier(multiplier); } catch (RemoteException e) { log("Failed to call setMaxDtimMultiplier", e); @@ -475,6 +487,8 @@ // Sysctl parameter strings. private static final String ACCEPT_RA = "accept_ra"; private static final String ACCEPT_RA_DEFRTR = "accept_ra_defrtr"; + @VisibleForTesting + static final String ACCEPT_RA_MIN_LFT = "accept_ra_min_lft"; private static final String DAD_TRANSMITS = "dad_transmits"; // Below constants are picked up by MessageUtils and exempt from ProGuard optimization. @@ -525,6 +539,11 @@ private static final int DEFAULT_MIN_RDNSS_LIFETIME = ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q) ? 120 : 0; + @VisibleForTesting + static final String CONFIG_ACCEPT_RA_MIN_LFT = "ipclient_accept_ra_min_lft"; + @VisibleForTesting + static final int DEFAULT_ACCEPT_RA_MIN_LFT = 180; + // Used to wait for the provisioning to complete eventually and then decide the target // network type, which gives the accurate hint to set DTIM multiplier. Per current IPv6 // provisioning connection latency metrics, the latency of 95% can go up to 16s, so pick @@ -596,11 +615,11 @@ // Maps each DHCP option code to a list of IEs, any of which will allow that option. private static final Map<Byte, List<byte[]>> DHCP_OPTIONS_ALLOWED = Map.of( (byte) 60, Collections.singletonList( - // KT OUI: 00:17:C3, type: 17. See b/170928882. - new byte[]{ (byte) 0x00, (byte) 0x17, (byte) 0xc3, (byte) 0x11 }), + // KT OUI: 00:17:C3, type: 33(0x21). See b/236745261. + new byte[]{ (byte) 0x00, (byte) 0x17, (byte) 0xc3, (byte) 0x21 }), (byte) 77, Collections.singletonList( - // KT OUI: 00:17:C3, type: 17. See b/170928882. - new byte[]{ (byte) 0x00, (byte) 0x17, (byte) 0xc3, (byte) 0x11 }) + // KT OUI: 00:17:C3, type: 33(0x21). See b/236745261. + new byte[]{ (byte) 0x00, (byte) 0x17, (byte) 0xc3, (byte) 0x21 }) ); // Initialize configurable particular SSID set supporting DHCP Roaming feature. See @@ -644,6 +663,9 @@ // Ignore nonzero RDNSS option lifetimes below this value. 0 = disabled. private final int mMinRdnssLifetimeSec; + // Ignore any nonzero RA section with lifetime below this value. + private final int mAcceptRaMinLft; + // Experiment flag read from device config. private final boolean mDhcp6PrefixDelegationEnabled; @@ -660,7 +682,7 @@ private DhcpResults mDhcpResults; private String mTcpBufferSizes; private ProxyInfo mHttpProxy; - private ApfFilter mApfFilter; + private AndroidPacketFilter mApfFilter; private String mL2Key; // The L2 key for this network, for writing into the memory store private String mCluster; // The cluster for this network, for writing into the memory store private boolean mMulticastFiltering; @@ -779,12 +801,18 @@ /** * Return whether a feature guarded by a feature flag is enabled. - * @see NetworkStackUtils#isFeatureEnabled(Context, String, String) + * @see DeviceConfigUtils#isNetworkStackFeatureEnabled(Context, String) */ - public boolean isFeatureEnabled(final Context context, final String name, - boolean defaultEnabled) { - return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name, - defaultEnabled); + public boolean isFeatureEnabled(final Context context, final String name) { + return DeviceConfigUtils.isNetworkStackFeatureEnabled(context, name); + } + + /** + * Check whether one specific feature is not disabled. + * @see DeviceConfigUtils#isNetworkStackFeatureNotChickenedOut(Context, String) + */ + public boolean isFeatureNotChickenedOut(final Context context, final String name) { + return DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(context, name); } /** @@ -792,18 +820,23 @@ * APF programs. * @see ApfFilter#maybeCreate */ - public ApfFilter maybeCreateApfFilter(Context context, ApfFilter.ApfConfiguration config, - InterfaceParams ifParams, IpClientCallbacksWrapper cb) { - return ApfFilter.maybeCreate(context, config, ifParams, cb); + public AndroidPacketFilter maybeCreateApfFilter(Context context, + ApfFilter.ApfConfiguration config, InterfaceParams ifParams, + IpClientCallbacksWrapper cb) { + if (isFeatureEnabled(context, NetworkStackUtils.APF_NEW_RA_FILTER_VERSION)) { + return ApfFilter.maybeCreate(context, config, ifParams, cb); + } else { + return LegacyApfFilter.maybeCreate(context, config, ifParams, cb); + } } /** - * Check whether one specific experimental feature in NetworkStack module from - * {@link DeviceConfig} is not disabled. - * @see DeviceConfigUtils#isNetworkStackFeatureNotChickenedOut(String) + * Check if a specific IPv6 sysctl file exists or not. */ - public boolean isNetworkStackFeatureNotChickenedOut(final String name) { - return DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(name); + public boolean hasIpv6Sysctl(final String ifname, final String name) { + final String path = "/proc/sys/net/ipv6/conf/" + ifname + "/" + name; + final File sysctl = new File(path); + return sysctl.exists(); } } @@ -845,10 +878,12 @@ mInterfaceCtrl = new InterfaceController(mInterfaceName, mNetd, mLog); mDhcp6PrefixDelegationEnabled = mDependencies.isFeatureEnabled(mContext, - IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION, false /* defaultEnabled */); + IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION); mMinRdnssLifetimeSec = mDependencies.getDeviceConfigPropertyInt( CONFIG_MIN_RDNSS_LIFETIME, DEFAULT_MIN_RDNSS_LIFETIME); + mAcceptRaMinLft = mDependencies.getDeviceConfigPropertyInt(CONFIG_ACCEPT_RA_MIN_LFT, + DEFAULT_ACCEPT_RA_MIN_LFT); IpClientLinkObserver.Configuration config = new IpClientLinkObserver.Configuration( mMinRdnssLifetimeSec); @@ -1042,18 +1077,15 @@ } private boolean isGratuitousNaEnabled() { - return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GRATUITOUS_NA_VERSION, - false /* defaultEnabled */); + return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GRATUITOUS_NA_VERSION); } private boolean isGratuitousArpNaRoamingEnabled() { - return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GARP_NA_ROAMING_VERSION, - false /* defaultEnabled */); + return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GARP_NA_ROAMING_VERSION); } private boolean isMulticastNsEnabled() { - return mDependencies.isFeatureEnabled(mContext, IPCLIENT_MULTICAST_NS_VERSION, - true /* defaultEnabled */); + return mDependencies.isFeatureNotChickenedOut(mContext, IPCLIENT_MULTICAST_NS_VERSION); } @VisibleForTesting @@ -1253,7 +1285,7 @@ } // Thread-unsafe access to mApfFilter but just used for debugging. - final ApfFilter apfFilter = mApfFilter; + final AndroidPacketFilter apfFilter = mApfFilter; final android.net.shared.ProvisioningConfiguration provisioningConfig = mConfiguration; final ApfCapabilities apfCapabilities = (provisioningConfig != null) ? provisioningConfig.mApfCapabilities : null; @@ -1459,19 +1491,20 @@ // Set "/proc/sys/net/ipv6/conf/${iface}/${name}" with the given specific value. private void setIpv6Sysctl(@NonNull final String name, int value) { try { - mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mInterfaceParams.name, + mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mInterfaceName, name, Integer.toString(value)); } catch (Exception e) { - Log.e(mTag, "Failed to set " + name + " to + " + value + ": " + e); + Log.e(mTag, "Failed to set " + name + " to " + value + ": " + e); } } - private Integer getIpv6DadTransmits() { + // Read "/proc/sys/net/ipv6/conf/${iface}/${name}". + private Integer getIpv6Sysctl(@NonNull final String name) { try { - return Integer.parseUnsignedInt(mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, - mInterfaceName, DAD_TRANSMITS)); + return Integer.parseInt(mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, + mInterfaceName, name)); } catch (RemoteException | ServiceSpecificException e) { - logError("Couldn't read dad_transmits on " + mInterfaceName, e); + logError("Couldn't read " + name + " on " + mInterfaceName, e); return null; } } @@ -2121,12 +2154,19 @@ mConfiguration.mIPv6ProvisioningMode == PROV_IPV6_LINKLOCAL ? 0 : 2); setIpv6Sysctl(ACCEPT_RA_DEFRTR, acceptRaDefrtr); if (shouldDisableDad()) { - final Integer dadTransmits = getIpv6DadTransmits(); + final Integer dadTransmits = getIpv6Sysctl(DAD_TRANSMITS); if (dadTransmits != null) { mDadTransmits = dadTransmits; setIpv6Sysctl(DAD_TRANSMITS, 0 /* dad_transmits */); } } + // Check chickened out flag first before reading IPv6 sysctl, which can prevent from + // triggering a potential kernel bug about the sysctl. + if (mDependencies.isFeatureNotChickenedOut(mContext, + IPCLIENT_IGNORE_LOW_RA_LIFETIME_FORCE_DISABLE) + && mDependencies.hasIpv6Sysctl(mInterfaceName, ACCEPT_RA_MIN_LFT)) { + setIpv6Sysctl(ACCEPT_RA_MIN_LFT, mAcceptRaMinLft); + } return mInterfaceCtrl.setIPv6PrivacyExtensions(true) && mInterfaceCtrl.setIPv6AddrGenModeIfSupported(mConfiguration.mIPv6AddrGenMode) && mInterfaceCtrl.enableIPv6(); @@ -2194,9 +2234,21 @@ // - disableIpv6() will clear autoconf IPv6 routes as well, and // - we don't get IPv4 routes from netlink // so we neither react to nor need to wait for changes in either. - mInterfaceCtrl.disableIPv6(); mInterfaceCtrl.clearAllAddresses(); + + // Reset IPv6 sysctls to their initial state. It's better to restore + // sysctls after IPv6 stack is disabled, which prevents a potential + // race where receiving an RA between restoring accept_ra and disabling + // IPv6 stack, although it's unlikely. + setIpv6Sysctl(ACCEPT_RA, 2); + setIpv6Sysctl(ACCEPT_RA_DEFRTR, 1); + maybeRestoreDadTransmits(); + if (mDependencies.isFeatureNotChickenedOut(mContext, + IPCLIENT_IGNORE_LOW_RA_LIFETIME_FORCE_DISABLE) + && mDependencies.hasIpv6Sysctl(mInterfaceName, ACCEPT_RA_MIN_LFT)) { + setIpv6Sysctl(ACCEPT_RA_MIN_LFT, 0 /* sysctl default */); + } } private void maybeSaveNetworkToIpMemoryStore() { @@ -2284,7 +2336,7 @@ } @Nullable - private ApfFilter maybeCreateApfFilter(final ApfCapabilities apfCapabilities) { + private AndroidPacketFilter maybeCreateApfFilter(final ApfCapabilities apfCapabilities) { ApfFilter.ApfConfiguration apfConfig = new ApfFilter.ApfConfiguration(); apfConfig.apfCapabilities = apfCapabilities; apfConfig.multicastFilter = mMulticastFiltering; @@ -2300,6 +2352,7 @@ } apfConfig.minRdnssLifetimeSec = mMinRdnssLifetimeSec; + apfConfig.acceptRaMinLft = mAcceptRaMinLft; return mDependencies.maybeCreateApfFilter(mContext, apfConfig, mInterfaceParams, mCallback); } @@ -2415,8 +2468,6 @@ // Restore the interface MTU to initial value if it has changed. maybeRestoreInterfaceMtu(); - // Reset number of dad_transmits to default value if changed. - maybeRestoreDadTransmits(); // Reset DTIM multiplier to default value if changed. if (mMaxDtimMultiplier != DTIM_MULTIPLIER_RESET) { mCallback.setMaxDtimMultiplier(DTIM_MULTIPLIER_RESET); @@ -3024,6 +3075,7 @@ case Dhcp6Client.DHCP6_PD_PREFIX_EXPIRED: case Dhcp6Client.DHCP6_PD_PREFIX_CHANGED: + case Dhcp6Client.DHCP6_PD_PREFIX_MSG_EXCHANGE_TERMINATED: clearIpv6PrefixDelegationAddresses(); mPrefixDelegation = null; handleLinkPropertiesUpdate(SEND_CALLBACKS);
diff --git a/src/android/net/ip/IpClientLinkObserver.java b/src/android/net/ip/IpClientLinkObserver.java index 761f039..2068caa 100644 --- a/src/android/net/ip/IpClientLinkObserver.java +++ b/src/android/net/ip/IpClientLinkObserver.java
@@ -19,6 +19,7 @@ import static android.system.OsConstants.AF_INET6; import static android.system.OsConstants.AF_UNSPEC; import static android.system.OsConstants.IFF_LOOPBACK; + import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT; import static com.android.net.module.util.netlink.NetlinkConstants.IFF_LOWER_UP; import static com.android.net.module.util.netlink.NetlinkConstants.RTM_F_CLONED; @@ -188,7 +189,7 @@ mDnsServerRepository = new DnsServerRepository(config.minRdnssLifetime); mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); mDependencies = deps; - mNetlinkEventParsingEnabled = deps.isNetworkStackFeatureNotChickenedOut( + mNetlinkEventParsingEnabled = deps.isFeatureNotChickenedOut(context, IPCLIENT_PARSE_NETLINK_EVENTS_FORCE_DISABLE); mNetlinkMonitor = new MyNetlinkMonitor(h, log, mTag); mHandler.post(() -> { @@ -203,8 +204,8 @@ } private boolean isIpv6LinkLocalDnsAccepted() { - return mDependencies.isFeatureEnabled(mContext, - IPCLIENT_ACCEPT_IPV6_LINK_LOCAL_DNS_VERSION, true /* default value */); + return mDependencies.isFeatureNotChickenedOut(mContext, + IPCLIENT_ACCEPT_IPV6_LINK_LOCAL_DNS_VERSION); } private void maybeLog(String operation, String iface, LinkAddress address) {
diff --git a/src/android/net/ip/IpReachabilityMonitor.java b/src/android/net/ip/IpReachabilityMonitor.java index 4d40c4f..40a4bb6 100644 --- a/src/android/net/ip/IpReachabilityMonitor.java +++ b/src/android/net/ip/IpReachabilityMonitor.java
@@ -20,7 +20,6 @@ import static android.net.metrics.IpReachabilityEvent.NUD_FAILED_ORGANIC; import static android.net.metrics.IpReachabilityEvent.PROVISIONING_LOST; import static android.net.metrics.IpReachabilityEvent.PROVISIONING_LOST_ORGANIC; -import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION; @@ -182,7 +181,8 @@ public interface Dependencies { void acquireWakeLock(long durationMs); IpNeighborMonitor makeIpNeighborMonitor(Handler h, SharedLog log, NeighborEventConsumer cb); - boolean isFeatureEnabled(Context context, String name, boolean defaultEnabled); + boolean isFeatureEnabled(Context context, String name); + boolean isFeatureNotChickenedOut(Context context, String name); IpReachabilityMonitorMetrics getIpReachabilityMonitorMetrics(); static Dependencies makeDefault(Context context, String iface) { @@ -200,10 +200,12 @@ return new IpNeighborMonitor(h, log, cb); } - public boolean isFeatureEnabled(final Context context, final String name, - boolean defaultEnabled) { - return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name, - defaultEnabled); + public boolean isFeatureEnabled(final Context context, final String name) { + return DeviceConfigUtils.isNetworkStackFeatureEnabled(context, name); + } + + public boolean isFeatureNotChickenedOut(final Context context, final String name) { + return DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(context, name); } public IpReachabilityMonitorMetrics getIpReachabilityMonitorMetrics() { @@ -258,14 +260,12 @@ mUsingMultinetworkPolicyTracker = usingMultinetworkPolicyTracker; mCm = context.getSystemService(ConnectivityManager.class); mDependencies = dependencies; - mMulticastResolicitEnabled = dependencies.isFeatureEnabled(context, - IP_REACHABILITY_MCAST_RESOLICIT_VERSION, true /* defaultEnabled */); - mIgnoreIncompleteIpv6DnsServerEnabled = dependencies.isFeatureEnabled(context, - IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, - true /* defaultEnabled */); + mMulticastResolicitEnabled = dependencies.isFeatureNotChickenedOut(context, + IP_REACHABILITY_MCAST_RESOLICIT_VERSION); + mIgnoreIncompleteIpv6DnsServerEnabled = dependencies.isFeatureNotChickenedOut(context, + IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION); mIgnoreIncompleteIpv6DefaultRouterEnabled = dependencies.isFeatureEnabled(context, - IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, - false /* defaultEnabled */); + IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION); mMetricsLog = metricsLog; mNetd = netd; Preconditions.checkNotNull(mNetd);
diff --git a/src/com/android/networkstack/netlink/TcpInfo.java b/src/com/android/networkstack/netlink/TcpInfo.java index 31a408f..73d206e 100644 --- a/src/com/android/networkstack/netlink/TcpInfo.java +++ b/src/com/android/networkstack/netlink/TcpInfo.java
@@ -19,8 +19,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import com.android.internal.annotations.VisibleForTesting; +import androidx.annotation.VisibleForTesting; import java.nio.BufferOverflowException; import java.nio.BufferUnderflowException; @@ -97,10 +96,35 @@ static final int SEGS_IN_OFFSET = getFieldOffset(Field.SEGS_IN); @VisibleForTesting static final int SEGS_OUT_OFFSET = getFieldOffset(Field.SEGS_OUT); + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final int TOTAL_RETRANS_OFFSET = getFieldOffset(Field.TOTAL_RETRANS); + /** + * This counts individual incoming packets that appeared on the wire, including: + * SYN, SYN-ACK, pure ACKs, data segments (after segmentation offload into small <=mtu + * packets), FIN, FIN-ACK, and any retransmits. + * + * This field is read from the tcpi_segs_in field from {@code struct tcp_info} + * in bionic/libc/kernel/uapi/linux/tcp.h. Also see [tcpEStatsPerfSegsIn] in the RFC4898. + */ final int mSegsIn; + /** + * This counts individual outgoing packets that have been sent to the network, including: + * SYN, SYN-ACK, pure ACKs, data segments (after segmentation offload into small <=mtu + * packets), FIN, FIN-ACK, and any retransmits. + * + * This field is read from the tcpi_segs_out field from {@code struct tcp_info} + * in bionic/libc/kernel/uapi/linux/tcp.h. Also see [tcpEStatsPerfSegsOut] in the RFC4898. + */ final int mSegsOut; - final int mLost; - final int mRetransmits; + /** + * This counts individual accumulated retransmitted packets that have been sent to the network, + * including any retransmits for SYN, SYN-ACK, pure ACKs, data segments (after segmentation + * offload into small <=mtu packets), FIN and FIN-ACK. + * + * This field is read from the tcpi_total_retrans field from {@code struct tcp_info} + * in bionic/libc/kernel/uapi/linux/tcp.h. + */ + final int mTotalRetrans; private static int getFieldOffset(@NonNull final Field needle) { int offset = 0; @@ -120,20 +144,18 @@ final int start = bytes.position(); mSegsIn = bytes.getInt(start + SEGS_IN_OFFSET); mSegsOut = bytes.getInt(start + SEGS_OUT_OFFSET); - mLost = bytes.getInt(start + LOST_OFFSET); - mRetransmits = bytes.get(start + RETRANSMITS_OFFSET); + mTotalRetrans = bytes.get(start + TOTAL_RETRANS_OFFSET); // tcp_info structure grows over time as new fields are added. Jump to the end of the // structure, as unknown fields might remain at the end of the structure if the tcp_info // struct was expanded. bytes.position(Math.min(infolen + start, bytes.limit())); } - @VisibleForTesting - TcpInfo(int retransmits, int lost, int segsOut, int segsIn) { - mRetransmits = retransmits; - mLost = lost; + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + TcpInfo(int segsOut, int segsIn, int totalRetrans) { mSegsOut = segsOut; mSegsIn = segsIn; + mTotalRetrans = totalRetrans; } /** Parse a TcpInfo from a giving ByteBuffer with a specific length. */ @@ -180,17 +202,17 @@ TcpInfo other = (TcpInfo) obj; return mSegsIn == other.mSegsIn && mSegsOut == other.mSegsOut - && mRetransmits == other.mRetransmits && mLost == other.mLost; + && mTotalRetrans == other.mTotalRetrans; } @Override public int hashCode() { - return Objects.hash(mLost, mRetransmits, mSegsIn, mSegsOut); + return Objects.hash(mSegsIn, mSegsOut, mTotalRetrans); } @Override public String toString() { - return "TcpInfo{lost=" + mLost + ", retransmit=" + mRetransmits + ", received=" + mSegsIn - + ", sent=" + mSegsOut + "}"; + return "TcpInfo{received=" + mSegsIn + ", sent=" + mSegsOut + + ", totalRetrans=" + mTotalRetrans + "}"; } }
diff --git a/src/com/android/networkstack/netlink/TcpSocketTracker.java b/src/com/android/networkstack/netlink/TcpSocketTracker.java index c0a5e64..fea858f 100644 --- a/src/com/android/networkstack/netlink/TcpSocketTracker.java +++ b/src/com/android/networkstack/netlink/TcpSocketTracker.java
@@ -19,6 +19,8 @@ import static android.net.util.DataStallUtils.CONFIG_TCP_PACKETS_FAIL_PERCENTAGE; import static android.net.util.DataStallUtils.DEFAULT_DATA_STALL_MIN_PACKETS_THRESHOLD; import static android.net.util.DataStallUtils.DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE; +import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED; +import static android.os.PowerManager.ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED; import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; import static android.system.OsConstants.AF_INET; import static android.system.OsConstants.AF_INET6; @@ -31,7 +33,9 @@ import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY; import static com.android.net.module.util.netlink.NetlinkUtils.DEFAULT_RECV_BUFSIZE; import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS; +import static com.android.networkstack.util.NetworkStackUtils.SKIP_TCP_POLL_IN_LIGHT_DOZE; +import android.annotation.TargetApi; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -60,15 +64,15 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.DeviceConfigUtils; import com.android.net.module.util.SocketUtils; import com.android.net.module.util.netlink.InetDiagMessage; -import com.android.net.module.util.netlink.NetlinkConstants; import com.android.net.module.util.netlink.NetlinkUtils; import com.android.net.module.util.netlink.StructInetDiagMsg; +import com.android.net.module.util.netlink.StructNlAttr; import com.android.net.module.util.netlink.StructNlMsgHdr; import com.android.networkstack.apishim.NetworkShimImpl; -import com.android.networkstack.apishim.common.ShimUtils; import com.android.networkstack.apishim.common.UnsupportedApiLevelException; import java.io.FileDescriptor; @@ -136,6 +140,8 @@ @NonNull private LinkProperties mLinkProperties; + private final boolean mShouldDisableInLightDoze; + @VisibleForTesting protected final DeviceConfig.OnPropertiesChangedListener mConfigListener = new DeviceConfig.OnPropertiesChangedListener() { @@ -152,14 +158,30 @@ } }; + private static boolean isDeviceIdleModeChangedAction(Intent intent) { + return ACTION_DEVICE_IDLE_MODE_CHANGED.equals(intent.getAction()); + } + + @TargetApi(Build.VERSION_CODES.TIRAMISU) + private boolean isDeviceLightIdleModeChangedAction(Intent intent) { + return mShouldDisableInLightDoze + && ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED.equals(intent.getAction()); + } + final BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() { @Override + @TargetApi(Build.VERSION_CODES.TIRAMISU) public void onReceive(Context context, Intent intent) { if (intent == null) return; - if (PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED.equals(intent.getAction())) { + if (isDeviceIdleModeChangedAction(intent) + || isDeviceLightIdleModeChangedAction(intent)) { final PowerManager powerManager = context.getSystemService(PowerManager.class); - final boolean deviceIdle = powerManager.isDeviceIdleMode(); + // For tcp polling mechanism, there is no difference between deep doze mode and + // light doze mode. The deep doze mode and light doze mode block networking + // for uids in the same way, use single variable to control. + final boolean deviceIdle = powerManager.isDeviceIdleMode() + || (mShouldDisableInLightDoze && powerManager.isDeviceLightIdleMode()); setDozeMode(deviceIdle); } } @@ -169,6 +191,7 @@ mDependencies = dps; mNetwork = network; mNetd = mDependencies.getNetd(); + mShouldDisableInLightDoze = mDependencies.shouldDisableInLightDoze(); // If the parcel is null, nothing should be matched which is achieved by the combination of // {@code NetlinkUtils#NULL_MASK} and {@code NetlinkUtils#UNKNOWN_MARK}. @@ -176,16 +199,13 @@ mNetworkMark = (parcel != null) ? parcel.mark : NetlinkUtils.UNKNOWN_MARK; mNetworkMask = (parcel != null) ? parcel.mask : NetlinkUtils.NULL_MASK; - // Request tcp info from NetworkStack directly needs extra SELinux permission added after Q - // release. - if (!mDependencies.isTcpInfoParsingSupported()) return; // Build SocketDiag messages. for (final int family : ADDRESS_FAMILIES) { mSockDiagMsg.put( family, InetDiagMessage.buildInetDiagReqForAliveTcpSockets(family)); } mDependencies.addDeviceConfigChangedListener(mConfigListener); - mDependencies.addDeviceIdleReceiver(mDeviceIdleReceiver); + mDependencies.addDeviceIdleReceiver(mDeviceIdleReceiver, mShouldDisableInLightDoze); } @Nullable @@ -208,7 +228,6 @@ * @Return if this polling request is sent to kernel and executes successfully or not. */ public boolean pollSocketsInfo() { - if (!mDependencies.isTcpInfoParsingSupported()) return false; // Traffic will be restricted in doze mode. TCP info may not reflect the correct network // behavior. // TODO: Traffic may be restricted by other reason. Get the restriction info from bpf in T+. @@ -263,7 +282,7 @@ ? (mSentSinceLastRecv + stat.sentCount) : 0; mLatestReceivedCount = stat.receivedCount; mLatestPacketFailPercentage = ((stat.sentCount != 0) - ? ((stat.retransmitCount + stat.lostCount) * 100 / stat.sentCount) : 0); + ? (stat.retransCount * 100 / stat.sentCount) : 0); // Remove out-of-date socket info. cleanupSocketInfo(time); @@ -382,18 +401,13 @@ int mark = NetlinkUtils.INIT_MARK_VALUE; // Get a tcp_info. while (bytes.position() < remainingDataSize) { - final RoutingAttribute rtattr = - new RoutingAttribute(bytes.getShort(), bytes.getShort()); - final short dataLen = rtattr.getDataLength(); - if (rtattr.rtaType == NetlinkUtils.INET_DIAG_INFO) { - tcpInfo = TcpInfo.parse(bytes, dataLen); - } else if (rtattr.rtaType == NetlinkUtils.INET_DIAG_MARK) { - mark = bytes.getInt(); - } else { - // Data provided by kernel will include both valid data and padding data. The data - // len provided from kernel indicates the valid data size. Readers must deduce the - // alignment by themselves. - skipRemainingAttributesBytesAligned(bytes, dataLen); + final StructNlAttr nlattr = StructNlAttr.parse(bytes); + if (nlattr == null) break; + + if (nlattr.nla_type == NetlinkUtils.INET_DIAG_MARK) { + mark = nlattr.getValueAsInteger(); + } else if (nlattr.nla_type == NetlinkUtils.INET_DIAG_INFO) { + tcpInfo = TcpInfo.parse(nlattr.getValueAsByteBuffer(), nlattr.getAlignedLength()); } } final SocketInfo info = new SocketInfo(tcpInfo, family, mark, time, uid, cookie, dstPort); @@ -407,8 +421,6 @@ * statemachine thread of NetworkMonitor. */ public boolean isDataStallSuspected() { - if (!mDependencies.isTcpInfoParsingSupported()) return false; - // Skip checking data stall since the traffic will be restricted and it will not be real // network stall. // TODO: Traffic may be restricted by other reason. Get the restriction info from bpf in T+. @@ -439,14 +451,12 @@ stat.sentCount = current.tcpInfo.mSegsOut; stat.receivedCount = current.tcpInfo.mSegsIn; - stat.lostCount = current.tcpInfo.mLost; - stat.retransmitCount = current.tcpInfo.mRetransmits; + stat.retransCount = current.tcpInfo.mTotalRetrans; if (previous != null && previous.tcpInfo != null) { stat.sentCount -= previous.tcpInfo.mSegsOut; stat.receivedCount -= previous.tcpInfo.mSegsIn; - stat.lostCount -= previous.tcpInfo.mLost; - stat.retransmitCount -= previous.tcpInfo.mRetransmits; + stat.retransCount -= previous.tcpInfo.mTotalRetrans; } log("calculateLatestPacketsStat, stat:" + stat); return stat; @@ -458,7 +468,6 @@ * @return the latest packet fail percentage. -1 denotes that there is no available data. */ public int getLatestPacketFailPercentage() { - if (!mDependencies.isTcpInfoParsingSupported()) return -1; // Only return fail rate if device sent enough packets. if (getSentSinceLastRecv() < getMinPacketsThreshold()) return -1; return mLatestPacketFailPercentage; @@ -469,13 +478,11 @@ * between each polling period, not an accurate number. */ public int getSentSinceLastRecv() { - if (!mDependencies.isTcpInfoParsingSupported()) return -1; return mSentSinceLastRecv; } /** Return the number of the packets received in the latest polling cycle. */ public int getLatestReceivedCount() { - if (!mDependencies.isTcpInfoParsingSupported()) return -1; return mLatestReceivedCount; } @@ -491,70 +498,17 @@ return mTcpPacketsFailRateThreshold; } - /** - * Method to skip the remaining attributes bytes. - * Corresponds to NLMSG_NEXT in bionic/libc/kernel/uapi/linux/netlink.h. - * - * @param buffer the target ByteBuffer - * @param len the remaining length to skip. - */ - private static void skipRemainingAttributesBytesAligned(@NonNull final ByteBuffer buffer, - final short len) { - // Data in {@Code RoutingAttribute} is followed after header with size {@Code NLA_ALIGNTO} - // bytes long for each block. Next attribute will start after the padding bytes if any. - // If all remaining bytes after header are valid in a data block, next attr will just start - // after valid bytes. - // - // E.g. With NLA_ALIGNTO(4), an attr struct with length 5 means 1 byte valid data remains - // after header and 3(4-1) padding bytes. Next attr with length 8 will start after the - // padding bytes and contain 4(8-4) valid bytes of data. The next attr start after the - // valid bytes, like: - // - // [HEADER(L=5)][ 4-Bytes DATA ][ HEADER(L=8) ][4 bytes DATA][Next attr] - // [ 5 valid bytes ][3 padding bytes ][ 8 valid bytes ] ... - final int cur = buffer.position(); - buffer.position(cur + NetlinkConstants.alignedLengthOf(len)); - } - private static void log(final String str) { if (DBG) Log.d(TAG, str); } /** Stops monitoring and releases resources. */ public void quit() { - // Do not need to unregister receiver and listener since registration is skipped - // in the constructor. - if (!mDependencies.isTcpInfoParsingSupported()) return; - mDependencies.removeDeviceConfigChangedListener(mConfigListener); mDependencies.removeBroadcastReceiver(mDeviceIdleReceiver); } /** - * Corresponds to {@code struct rtattr} from bionic/libc/kernel/uapi/linux/rtnetlink.h - * - * struct rtattr { - * unsigned short rta_len; // Length of option - * unsigned short rta_type; // Type of option - * // Data follows - * }; - */ - static class RoutingAttribute { - public static final int HEADER_LENGTH = 4; - - public final short rtaLen; // The whole valid size of the struct. - public final short rtaType; - - RoutingAttribute(final short len, final short type) { - rtaLen = len; - rtaType = type; - } - public short getDataLength() { - return (short) (rtaLen - HEADER_LENGTH); - } - } - - /** * Data class for keeping the socket info. */ @VisibleForTesting @@ -609,23 +563,21 @@ * */ private class TcpStat { public int sentCount; - public int lostCount; - public int retransmitCount; public int receivedCount; + public int retransCount; void accumulate(@Nullable final TcpStat stat) { if (stat == null) return; sentCount += stat.sentCount; - lostCount += stat.lostCount; receivedCount += stat.receivedCount; - retransmitCount += stat.retransmitCount; + retransCount += stat.retransCount; } @Override public String toString() { - return "TcpStat {sent=" + sentCount + ", lost=" + lostCount - + ", retransmit=" + retransmitCount + ", received=" + receivedCount + "}"; + return "TcpStat {sent=" + sentCount + ", retransCount=" + retransCount + + ", received=" + receivedCount + "}"; } } @@ -699,15 +651,6 @@ } /** - * Return if request tcp info via netlink socket is supported or not. - */ - public boolean isTcpInfoParsingSupported() { - // Request tcp info from NetworkStack directly needs extra SELinux permission added - // after Q release. - return ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q); - } - - /** * Receive the request message from kernel via given fd. */ public ByteBuffer recvMessage(@NonNull final FileDescriptor fd) @@ -741,14 +684,31 @@ } /** Add receiver for detecting doze mode change to control TCP detection. */ - public void addDeviceIdleReceiver(@NonNull final BroadcastReceiver receiver) { - mContext.registerReceiver(receiver, - new IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)); + @TargetApi(Build.VERSION_CODES.TIRAMISU) + public void addDeviceIdleReceiver(@NonNull final BroadcastReceiver receiver, + boolean shouldDisableInLightDoze) { + final IntentFilter intentFilter = new IntentFilter(ACTION_DEVICE_IDLE_MODE_CHANGED); + if (shouldDisableInLightDoze) { + intentFilter.addAction(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED); + } + mContext.registerReceiver(receiver, intentFilter); } /** Remove broadcast receiver. */ public void removeBroadcastReceiver(@NonNull final BroadcastReceiver receiver) { mContext.unregisterReceiver(receiver); } + + /** + * Get whether polling should be disabled in light doze mode. This method should + * only be called once in the constructor, to ensure that the code does not need + * to deal with flag values changing at runtime. + */ + @TargetApi(Build.VERSION_CODES.TIRAMISU) + public boolean shouldDisableInLightDoze() { + // Light doze mode status checking API is only available at T or later releases. + return SdkLevel.isAtLeastT() && DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut( + mContext, SKIP_TCP_POLL_IN_LIGHT_DOZE); + } } }
diff --git a/src/com/android/networkstack/util/NetworkStackUtils.java b/src/com/android/networkstack/util/NetworkStackUtils.java index 05f4a52..c9589e3 100755 --- a/src/com/android/networkstack/util/NetworkStackUtils.java +++ b/src/com/android/networkstack/util/NetworkStackUtils.java
@@ -267,6 +267,11 @@ public static final String IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION = "ipclient_dhcpv6_prefix_delegation_version"; + /** + * Experiment flag to enable new ra filter. + */ + public static final String APF_NEW_RA_FILTER_VERSION = "apf_new_ra_filter_version"; + /**** BEGIN Feature Kill Switch Flags ****/ /** @@ -276,6 +281,19 @@ public static final String IPCLIENT_PARSE_NETLINK_EVENTS_FORCE_DISABLE = "ipclient_parse_netlink_events_force_disable"; + /** + * Kill switch flag to disable the feature of ignoring any individual RA section with lifetime + * below accept_ra_min_lft sysctl. + */ + public static final String IPCLIENT_IGNORE_LOW_RA_LIFETIME_FORCE_DISABLE = + "ipclient_ignore_low_ra_lifetime_force_disable"; + + /** + * Kill switch flag to disable the feature of skipping Tcp socket info polling when light + * doze mode is enabled. + */ + public static final String SKIP_TCP_POLL_IN_LIGHT_DOZE = "skip_tcp_poll_in_light_doze_mode"; + static { System.loadLibrary("networkstackutilsjni"); } @@ -375,8 +393,7 @@ * Attaches a socket filter that accepts ICMPv6 router advertisements to the given socket. * @param fd the socket's {@link FileDescriptor}. */ - public static native void attachRaFilter(FileDescriptor fd) - throws SocketException; + public static native void attachRaFilter(FileDescriptor fd) throws ErrnoException; /** * Attaches a socket filter that accepts L2-L4 signaling traffic required for IP connectivity. @@ -384,10 +401,8 @@ * This includes: all ARP, ICMPv6 RS/RA/NS/NA messages, and DHCPv4 exchanges. * * @param fd the socket's {@link FileDescriptor}. - * @param packetType the hardware address type, one of ARPHRD_*. */ - public static native void attachControlPacketFilter(FileDescriptor fd, int packetType) - throws SocketException; + public static native void attachControlPacketFilter(FileDescriptor fd) throws ErrnoException; /** * Add an entry into the ARP cache.
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java index 3be8979..5b551a0 100755 --- a/src/com/android/server/connectivity/NetworkMonitor.java +++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -125,7 +125,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; -import android.provider.DeviceConfig; import android.provider.Settings; import android.stats.connectivity.ProbeResult; import android.stats.connectivity.ProbeType; @@ -627,8 +626,8 @@ mTestCaptivePortalHttpUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTP_URL, validationLogs, deps); mIsCaptivePortalCheckEnabled = getIsCaptivePortalCheckEnabled(context, deps); mPrivateIpNoInternetEnabled = getIsPrivateIpNoInternetEnabled(); - mMetricsEnabled = deps.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, - NetworkStackUtils.VALIDATION_METRICS_VERSION, true /* defaultEnabled */); + mMetricsEnabled = deps.isFeatureNotChickenedOut(context, + NetworkStackUtils.VALIDATION_METRICS_VERSION); mUseHttps = getUseHttpsValidation(); mCaptivePortalUserAgent = getCaptivePortalUserAgent(); mCaptivePortalFallbackSpecs = @@ -3325,27 +3324,19 @@ * Check whether or not one experimental feature in the connectivity namespace is * enabled. * @param name Flag name of the experiment in the connectivity namespace. - * @see DeviceConfigUtils#isFeatureEnabled(Context, String, String) + * @see DeviceConfigUtils#isNetworkStackFeatureEnabled(Context, String) */ public boolean isFeatureEnabled(@NonNull Context context, @NonNull String name) { - return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name); + return DeviceConfigUtils.isNetworkStackFeatureEnabled(context, name); } /** - * Check whether or not one specific experimental feature for a particular namespace from - * {@link DeviceConfig} is enabled by comparing NetworkStack module version - * {@link NetworkStack} with current version of property. If this property version is valid, - * the corresponding experimental feature would be enabled, otherwise disabled. - * @param context The global context information about an app environment. - * @param namespace The namespace containing the property to look up. - * @param name The name of the property to look up. - * @param defaultEnabled The value to return if the property does not exist or its value is - * null. - * @return true if this feature is enabled, or false if disabled. + * Check whether one specific feature is not disabled. + * @param name Flag name of the experiment in the connectivity namespace. + * @see DeviceConfigUtils#isNetworkStackFeatureNotChickenedOut(Context, String) */ - public boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace, - @NonNull String name, boolean defaultEnabled) { - return DeviceConfigUtils.isFeatureEnabled(context, namespace, name, defaultEnabled); + public boolean isFeatureNotChickenedOut(@NonNull Context context, @NonNull String name) { + return DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(context, name); } /**
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java index 83043e5..c54906d 100644 --- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java +++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -40,6 +40,8 @@ import static android.net.dhcp.DhcpPacket.MIN_V6ONLY_WAIT_MS; import static android.net.ip.IIpClientCallbacks.DTIM_MULTIPLIER_RESET; import static android.net.ip.IpClient.CONFIG_IPV6_AUTOCONF_TIMEOUT; +import static android.net.ip.IpClient.CONFIG_ACCEPT_RA_MIN_LFT; +import static android.net.ip.IpClient.DEFAULT_ACCEPT_RA_MIN_LFT; import static android.net.ip.IpClientLinkObserver.CLAT_PREFIX; import static android.net.ip.IpClientLinkObserver.CONFIG_SOCKET_RECV_BUFSIZE; import static android.net.ip.IpReachabilityMonitor.NUD_MCAST_RESOLICIT_NUM; @@ -50,6 +52,7 @@ import static android.system.OsConstants.IPPROTO_ICMPV6; import static android.system.OsConstants.IPPROTO_IPV6; import static android.system.OsConstants.IPPROTO_UDP; + import static com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress; import static com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address; import static com.android.net.module.util.NetworkStackConstants.ALL_DHCP_RELAY_AGENTS_AND_SERVERS; @@ -78,7 +81,9 @@ import static com.android.testutils.MiscAsserts.assertThrows; import static com.android.testutils.ParcelUtils.parcelingRoundTrip; import static com.android.testutils.TestPermissionUtil.runAsShell; + import static junit.framework.Assert.fail; + import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -174,6 +179,7 @@ import android.system.ErrnoException; import android.system.Os; import android.util.ArrayMap; +import android.util.Log; import androidx.annotation.NonNull; import androidx.test.InstrumentationRegistry; @@ -276,6 +282,7 @@ @RunWith(Parameterized.class) @SmallTest public abstract class IpClientIntegrationTestCommon { + private static final String TAG = IpClientIntegrationTestCommon.class.getSimpleName(); private static final int DATA_BUFFER_LEN = 4096; private static final int PACKET_TIMEOUT_MS = 5_000; private static final String TEST_CLUSTER = "some cluster"; @@ -438,7 +445,9 @@ private static final byte[] TEST_HOTSPOT_OUI = new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }; - private static final byte TEST_VENDOR_SPECIFIC_TYPE = 0x06; + private static final byte LEGACY_TEST_VENDOR_SPECIFIC_IE_TYPE = 0x11; + private static final byte TEST_VENDOR_SPECIFIC_IE_TYPE = 0x21; + private static final int TEST_VENDOR_SPECIFIC_IE_ID = 0xdd; private static final String TEST_DEFAULT_SSID = "test_ssid"; private static final String TEST_DEFAULT_BSSID = "00:11:22:33:44:55"; @@ -522,9 +531,13 @@ } @Override - public boolean isFeatureEnabled(final Context context, final String name, - final boolean defaultEnabled) { - return IpClientIntegrationTestCommon.this.isFeatureEnabled(name, defaultEnabled); + public boolean isFeatureEnabled(final Context context, final String name) { + return IpClientIntegrationTestCommon.this.isFeatureEnabled(name); + } + + @Override + public boolean isFeatureNotChickenedOut(final Context context, final String name) { + return IpClientIntegrationTestCommon.this.isFeatureNotChickenedOut(name); } @Override @@ -543,9 +556,13 @@ NetworkStackIpMemoryStore ipMemoryStore, IpProvisioningMetrics metrics) { return new DhcpClient.Dependencies(ipMemoryStore, metrics) { @Override - public boolean isFeatureEnabled(final Context context, final String name, - final boolean defaultEnabled) { - return Dependencies.this.isFeatureEnabled(context, name, defaultEnabled); + public boolean isFeatureEnabled(final Context context, final String name) { + return Dependencies.this.isFeatureEnabled(context, name); + } + + @Override + public boolean isFeatureNotChickenedOut(final Context context, final String name) { + return Dependencies.this.isFeatureNotChickenedOut(context, name); } @Override @@ -586,9 +603,12 @@ return new IpNeighborMonitor(h, log, cb); } - public boolean isFeatureEnabled(final Context context, final String name, - boolean defaultEnabled) { - return Dependencies.this.isFeatureEnabled(context, name, defaultEnabled); + public boolean isFeatureEnabled(final Context context, final String name) { + return Dependencies.this.isFeatureEnabled(context, name); + } + + public boolean isFeatureNotChickenedOut(final Context context, final String name) { + return Dependencies.this.isFeatureNotChickenedOut(context, name); } public IpReachabilityMonitorMetrics getIpReachabilityMonitorMetrics() { @@ -622,7 +642,9 @@ protected abstract void setFeatureEnabled(String name, boolean enabled); - protected abstract boolean isFeatureEnabled(String name, boolean defaultEnabled); + protected abstract boolean isFeatureEnabled(String name); + + protected abstract boolean isFeatureNotChickenedOut(String name); protected abstract boolean useNetworkStackSignature(); @@ -667,7 +689,7 @@ } private void setFeatureChickenedOut(String name, boolean chickenedOut) { - setDeviceConfigProperty(name, chickenedOut ? "1" : "0"); + setDeviceConfigProperty(name, chickenedOut ? "-1" : "0"); } protected void setDhcpFeatures(final boolean isDhcpLeaseCacheEnabled, @@ -802,6 +824,10 @@ // Set the timeout to wait IPv6 autoconf to complete. mDependencies.setDeviceConfigProperty(CONFIG_IPV6_AUTOCONF_TIMEOUT, 500); + + // Set the minimal RA lifetime value, any RA section with liftime below this value will be + // ignored. + mDependencies.setDeviceConfigProperty(CONFIG_ACCEPT_RA_MIN_LFT, DEFAULT_ACCEPT_RA_MIN_LFT); } private void awaitIpClientShutdown() throws Exception { @@ -2891,7 +2917,8 @@ private ScanResultInfo makeScanResultInfo(final String ssid, final String bssid) { byte[] data = new byte[10]; new Random().nextBytes(data); - return makeScanResultInfo(0xdd, ssid, bssid, TEST_AP_OUI, (byte) 0x06, data); + return makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, ssid, bssid, TEST_AP_OUI, + (byte) 0x06, data); } private void doUpstreamHotspotDetectionTest(final int id, final String displayName, @@ -2935,7 +2962,7 @@ public void testUpstreamHotspotDetection() throws Exception { byte[] data = new byte[10]; new Random().nextBytes(data); - doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid", + doUpstreamHotspotDetectionTest(TEST_VENDOR_SPECIFIC_IE_ID, "\"ssid\"", "ssid", new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data, true /* expectMetered */); } @@ -2953,7 +2980,7 @@ public void testUpstreamHotspotDetection_incorrectOUI() throws Exception { byte[] data = new byte[10]; new Random().nextBytes(data); - doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid", + doUpstreamHotspotDetectionTest(TEST_VENDOR_SPECIFIC_IE_ID, "\"ssid\"", "ssid", new byte[] { (byte) 0x00, (byte) 0x1A, (byte) 0x11 }, (byte) 0x06, data, false /* expectMetered */); } @@ -2962,7 +2989,7 @@ public void testUpstreamHotspotDetection_incorrectSsid() throws Exception { byte[] data = new byte[10]; new Random().nextBytes(data); - doUpstreamHotspotDetectionTest(0xdd, "\"another ssid\"", "ssid", + doUpstreamHotspotDetectionTest(TEST_VENDOR_SPECIFIC_IE_ID, "\"another ssid\"", "ssid", new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data, false /* expectMetered */); } @@ -2971,7 +2998,7 @@ public void testUpstreamHotspotDetection_incorrectType() throws Exception { byte[] data = new byte[10]; new Random().nextBytes(data); - doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid", + doUpstreamHotspotDetectionTest(TEST_VENDOR_SPECIFIC_IE_ID, "\"ssid\"", "ssid", new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x0a, data, false /* expectMetered */); } @@ -2979,7 +3006,7 @@ @Test public void testUpstreamHotspotDetection_zeroLengthData() throws Exception { byte[] data = new byte[0]; - doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid", + doUpstreamHotspotDetectionTest(TEST_VENDOR_SPECIFIC_IE_ID, "\"ssid\"", "ssid", new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data, true /* expectMetered */); } @@ -3164,7 +3191,7 @@ return lp; } - private void doDualStackProvisioning() throws Exception { + private LinkProperties doDualStackProvisioning() throws Exception { final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder() .withoutIpReachabilityMonitor() .build(); @@ -3180,7 +3207,7 @@ mIIpClient.startProvisioning(config.toStableParcelable()); } - performDualStackProvisioning(); + return performDualStackProvisioning(); } private boolean hasRouteTo(@NonNull final LinkProperties lp, @NonNull final String prefix) { @@ -3207,8 +3234,10 @@ } @Test + @SignatureRequiredTest(reason = "Out of SLO flakiness") public void testIgnoreIpv6ProvisioningLoss_disableAcceptRaDefrtr() throws Exception { - doDualStackProvisioning(); + LinkProperties lp = doDualStackProvisioning(); + Log.d(TAG, "current LinkProperties: " + lp); final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>(); @@ -3229,7 +3258,8 @@ lpFuture.complete(x); return true; })); - final LinkProperties lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); + lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); + Log.d(TAG, "After receiving RA with 0 router lifetime, LinkProperties: " + lp); assertNotNull(lp); assertEquals(lp.getAddresses().get(0), CLIENT_ADDR); assertEquals(lp.getDnsServers().get(0), SERVER_ADDR); @@ -3243,7 +3273,7 @@ // Wait for RS after IPv6 stack has been restarted and reply with a normal RA to verify // that device gains the IPv6 provisioning without default route and off-link DNS server. sendBasicRouterAdvertisement(true /* waitForRs */); - verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat( + verify(mCb, timeout(TEST_TIMEOUT_MS).atLeastOnce()).onLinkPropertiesChange(argThat( x -> x.hasGlobalIpv6Address() // IPv4, IPv6 link local, privacy and stable privacy && x.getLinkAddresses().size() == 4 @@ -3537,8 +3567,8 @@ @Test public void testDiscoverCustomizedDhcpOptions() throws Exception { - final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI, - (byte) 0x11 /* vendor-specific IE type */); + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI, + TEST_VENDOR_SPECIFIC_IE_TYPE); final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info, false /* isDhcpLeaseCacheEnabled */); @@ -3549,8 +3579,8 @@ @Test public void testDiscoverCustomizedDhcpOptions_nullDhcpOptions() throws Exception { - final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI, - (byte) 0x11 /* vendor-specific IE type */); + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI, + TEST_VENDOR_SPECIFIC_IE_TYPE); final DhcpPacket packet = doCustomizedDhcpOptionsTest(null /* options */, info, false /* isDhcpLeaseCacheEnabled */); @@ -3571,8 +3601,8 @@ @Test public void testDiscoverCustomizedDhcpOptions_disallowedOui() throws Exception { - final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, - new byte[]{ 0x00, 0x11, 0x22} /* oui */, (byte) 0x11 /* vendor-specific IE type */); + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, + new byte[]{ 0x00, 0x11, 0x22} /* oui */, TEST_VENDOR_SPECIFIC_IE_TYPE); final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info, false /* isDhcpLeaseCacheEnabled */); @@ -3584,7 +3614,7 @@ @Test public void testDiscoverCustomizedDhcpOptions_invalidIeId() throws Exception { final ScanResultInfo info = makeScanResultInfo(0xde /* vendor-specific IE */, TEST_OEM_OUI, - (byte) 0x11 /* vendor-specific IE type */); + TEST_VENDOR_SPECIFIC_IE_TYPE); final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info, false /* isDhcpLeaseCacheEnabled */); @@ -3595,7 +3625,7 @@ @Test public void testDiscoverCustomizedDhcpOptions_invalidVendorSpecificType() throws Exception { - final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI, + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI, (byte) 0x10 /* vendor-specific IE type */); final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info, false /* isDhcpLeaseCacheEnabled */); @@ -3606,14 +3636,26 @@ } @Test + public void testDiscoverCustomizedDhcpOptions_legacyVendorSpecificType() throws Exception { + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI, + LEGACY_TEST_VENDOR_SPECIFIC_IE_TYPE); + final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info, + false /* isDhcpLeaseCacheEnabled */); + + assertTrue(packet instanceof DhcpDiscoverPacket); + assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE)); + assertNull(packet.mUserClass); + } + + @Test public void testDisoverCustomizedDhcpOptions_disallowedOption() throws Exception { final List<DhcpOption> options = Arrays.asList( makeDhcpOption((byte) 60, TEST_OEM_VENDOR_ID.getBytes()), makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO), // Option 26: MTU makeDhcpOption((byte) 26, HexDump.toByteArray(TEST_DEFAULT_MTU))); - final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI, - (byte) 0x11 /* vendor-specific IE type */); + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI, + TEST_VENDOR_SPECIFIC_IE_TYPE); final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info, false /* isDhcpLeaseCacheEnabled */); @@ -3630,8 +3672,8 @@ makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO), // NTP_SERVER makeDhcpOption((byte) 42, null)); - final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI, - (byte) 0x11 /* vendor-specific IE type */); + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI, + TEST_VENDOR_SPECIFIC_IE_TYPE); final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info, false /* isDhcpLeaseCacheEnabled */); @@ -3646,8 +3688,8 @@ final List<DhcpOption> options = Arrays.asList( // DHCP_USER_CLASS makeDhcpOption((byte) 77, null)); - final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI, - (byte) 0x11 /* vendor-specific IE type */); + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI, + TEST_VENDOR_SPECIFIC_IE_TYPE); final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info, false /* isDhcpLeaseCacheEnabled */); @@ -3660,8 +3702,8 @@ public void testRequestCustomizedDhcpOptions() throws Exception { setUpRetrievedNetworkAttributesForInitRebootState(); - final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI, - (byte) 0x11 /* vendor-specific IE type */); + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI, + TEST_VENDOR_SPECIFIC_IE_TYPE); final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info, true /* isDhcpLeaseCacheEnabled */); @@ -3674,8 +3716,8 @@ public void testRequestCustomizedDhcpOptions_nullDhcpOptions() throws Exception { setUpRetrievedNetworkAttributesForInitRebootState(); - final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI, - (byte) 0x11 /* vendor-specific IE type */); + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI, + TEST_VENDOR_SPECIFIC_IE_TYPE); final DhcpPacket packet = doCustomizedDhcpOptionsTest(null /* options */, info, true /* isDhcpLeaseCacheEnabled */); @@ -3700,8 +3742,8 @@ public void testRequestCustomizedDhcpOptions_disallowedOui() throws Exception { setUpRetrievedNetworkAttributesForInitRebootState(); - final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, - new byte[]{ 0x00, 0x11, 0x22} /* oui */, (byte) 0x11 /* vendor-specific IE type */); + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, + new byte[]{ 0x00, 0x11, 0x22} /* oui */, TEST_VENDOR_SPECIFIC_IE_TYPE); final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info, true /* isDhcpLeaseCacheEnabled */); @@ -3715,7 +3757,7 @@ setUpRetrievedNetworkAttributesForInitRebootState(); final ScanResultInfo info = makeScanResultInfo(0xde /* vendor-specific IE */, TEST_OEM_OUI, - (byte) 0x11 /* vendor-specific IE type */); + TEST_VENDOR_SPECIFIC_IE_TYPE); final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info, true /* isDhcpLeaseCacheEnabled */); @@ -3728,8 +3770,22 @@ public void testRequestCustomizedDhcpOptions_invalidVendorSpecificType() throws Exception { setUpRetrievedNetworkAttributesForInitRebootState(); - final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI, - (byte) 0x10 /* vendor-specific IE type */); + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI, + (byte) 0x20 /* vendor-specific IE type */); + final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info, + true /* isDhcpLeaseCacheEnabled */); + + assertTrue(packet instanceof DhcpRequestPacket); + assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE)); + assertNull(packet.mUserClass); + } + + @Test + public void testRequestCustomizedDhcpOptions_legacyVendorSpecificType() throws Exception { + setUpRetrievedNetworkAttributesForInitRebootState(); + + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI, + LEGACY_TEST_VENDOR_SPECIFIC_IE_TYPE); final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info, true /* isDhcpLeaseCacheEnabled */); @@ -3747,8 +3803,8 @@ makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO), // Option 26: MTU makeDhcpOption((byte) 26, HexDump.toByteArray(TEST_DEFAULT_MTU))); - final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI, - (byte) 0x11 /* vendor-specific IE type */); + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI, + TEST_VENDOR_SPECIFIC_IE_TYPE); final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info, true /* isDhcpLeaseCacheEnabled */); @@ -3767,8 +3823,8 @@ makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO), // NTP_SERVER makeDhcpOption((byte) 42, null)); - final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI, - (byte) 0x11 /* vendor-specific IE type */); + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI, + TEST_VENDOR_SPECIFIC_IE_TYPE); final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info, true /* isDhcpLeaseCacheEnabled */); @@ -3785,8 +3841,8 @@ final List<DhcpOption> options = Arrays.asList( // DHCP_USER_CLASS makeDhcpOption((byte) 77, null)); - final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI, - (byte) 0x11 /* vendor-specific IE type */); + final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI, + TEST_VENDOR_SPECIFIC_IE_TYPE); final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info, true /* isDhcpLeaseCacheEnabled */); @@ -3840,7 +3896,7 @@ setFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION, true /* isGratuitousNaEnabled */); - assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION, false)); + assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION)); startIpClientProvisioning(config); doIpv6OnlyProvisioning(); @@ -3872,9 +3928,19 @@ // not strictly necessary. setDhcpFeatures(false /* isDhcpLeaseCacheEnabled */, true /* isRapidCommitEnabled */, false /* isDhcpIpConflictDetectEnabled */, false /* isIPv6OnlyPreferredEnabled */); + + // Disable gratuitious neighbor discovery feature manually, if the feature is enabled on + // the DUT during experiment launch, that will send another two duplicate NA packets and + // mess up the assert of received NA packets. + setFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION, + false /* isGratuitousNaEnabled */); + assumeFalse(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION)); if (isGratuitousArpNaRoamingEnabled) { setFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION, true); - assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION, false)); + assumeTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION)); + } else { + setFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION, false); + assumeFalse(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION)); } startIpClientProvisioning(prov.build()); } @@ -4564,7 +4630,7 @@ setFeatureEnabled(NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION, true /* isUnsolicitedNsEnabled */); - assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION, false)); + assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION)); startIpClientProvisioning(config); doIpv6OnlyProvisioning(); @@ -4636,6 +4702,7 @@ } @Test + @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) public void testMaxDtimMultiplier_IPv6OnlyNetwork() throws Exception { ProvisioningConfiguration config = new ProvisioningConfiguration.Builder() .withoutIPv4() @@ -4653,6 +4720,7 @@ } @Test + @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) public void testMaxDtimMultiplier_IPv6LinkLocalOnlyMode() throws Exception { final InOrder inOrder = inOrder(mCb); ProvisioningConfiguration config = new ProvisioningConfiguration.Builder() @@ -4670,6 +4738,7 @@ } @Test + @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) public void testMaxDtimMultiplier_IPv4OnlyNetwork() throws Exception { performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */, @@ -4691,12 +4760,14 @@ } @Test + @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) public void testMaxDtimMultiplier_DualStackNetwork() throws Exception { final InOrder inOrder = inOrder(mCb); runDualStackNetworkDtimMultiplierSetting(inOrder); } @Test + @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) public void testMaxDtimMultiplier_MulticastLock() throws Exception { final InOrder inOrder = inOrder(mCb); runDualStackNetworkDtimMultiplierSetting(inOrder); @@ -4714,6 +4785,7 @@ } @Test + @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) public void testMaxDtimMultiplier_MulticastLockEnabled_StoppedState() throws Exception { // Simulate to hold the multicast lock by disabling the multicast filter at StoppedState, // verify no callback to be sent, start dual-stack provisioning and verify the multiplier @@ -4728,6 +4800,7 @@ } @Test + @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) public void testMaxDtimMultiplier_resetMultiplier() throws Exception { final InOrder inOrder = inOrder(mCb); runDualStackNetworkDtimMultiplierSetting(inOrder); @@ -5030,4 +5103,34 @@ } verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(any()); } + + @Test + @SignatureRequiredTest(reason = "requires mocked netd to read/write IPv6 sysctl") + public void testIpv6SysctlsRestAfterStoppingIpClient() throws Exception { + ProvisioningConfiguration config = new ProvisioningConfiguration.Builder() + .withoutIPv4() + .build(); + // dad_transmits has been set to 0 in disableIpv6ProvisioningDelays, re-enable + // dad_transmits for testing, production code will restore all IPv6 sysctls at + // StoppedState#enter anyway, read this parameter value after IpClient shutdown + // to check if that's default value 1. + mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "dad_transmits", "1"); + startIpClientProvisioning(config); + verify(mNetd, timeout(TEST_TIMEOUT_MS)).interfaceSetEnableIPv6(mIfaceName, true); + doIpv6OnlyProvisioning(); + + // Shutdown IpClient and check if the IPv6 sysctls: accept_ra, accept_ra_defrtr and + // dad_transmits have been reset to the default values. + mIpc.shutdown(); + awaitIpClientShutdown(); + final int dadTransmits = Integer.parseUnsignedInt( + mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "dad_transmits")); + assertEquals(1, dadTransmits); + final int acceptRa = Integer.parseUnsignedInt( + mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "accept_ra")); + assertEquals(2, acceptRa); + final int acceptRaDefRtr = Integer.parseUnsignedInt( + mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "accept_ra_defrtr")); + assertEquals(1, acceptRaDefRtr); + } }
diff --git a/tests/integration/root/android/net/ip/IpClientRootTest.kt b/tests/integration/root/android/net/ip/IpClientRootTest.kt index 05dc358..77d327f 100644 --- a/tests/integration/root/android/net/ip/IpClientRootTest.kt +++ b/tests/integration/root/android/net/ip/IpClientRootTest.kt
@@ -27,7 +27,6 @@ import android.net.ipmemorystore.Status import android.net.networkstack.TestNetworkStackServiceClient import android.os.Process -import android.provider.DeviceConfig import android.util.Log import androidx.test.platform.app.InstrumentationRegistry import com.android.net.module.util.DeviceConfigUtils @@ -185,11 +184,19 @@ setDeviceConfigProperty(feature, if (enabled) "1" else "999999999") } - override fun isFeatureEnabled(name: String, defaultEnabled: Boolean): Boolean { + override fun isFeatureEnabled(name: String): Boolean { automation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG) try { - return DeviceConfigUtils.isFeatureEnabled(mContext, DeviceConfig.NAMESPACE_CONNECTIVITY, - name, defaultEnabled) + return DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, name) + } finally { + automation.dropShellPermissionIdentity() + } + } + + override fun isFeatureNotChickenedOut(name: String): Boolean { + automation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG) + try { + return DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(mContext, name) } finally { automation.dropShellPermissionIdentity() }
diff --git a/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt b/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt index b494732..2f91f4f 100644 --- a/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt +++ b/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt
@@ -44,8 +44,12 @@ override fun useNetworkStackSignature() = true - override fun isFeatureEnabled(name: String, defaultEnabled: Boolean): Boolean { - return mEnabledFeatures.get(name) ?: defaultEnabled + override fun isFeatureEnabled(name: String): Boolean { + return mEnabledFeatures.get(name) ?: false + } + + override fun isFeatureNotChickenedOut(name: String): Boolean { + return mEnabledFeatures.get(name) ?: true } override fun setFeatureEnabled(name: String, enabled: Boolean) {
diff --git a/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt b/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt index 2377ffa..3f01bea 100644 --- a/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt +++ b/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt
@@ -33,6 +33,7 @@ import android.system.OsConstants.ARPHRD_ETHER import android.system.OsConstants.ETH_P_IPV6 import android.system.OsConstants.IPPROTO_UDP +import android.system.OsConstants.SOCK_CLOEXEC import android.system.OsConstants.SOCK_DGRAM import android.system.OsConstants.SOCK_NONBLOCK import android.system.OsConstants.SOCK_RAW @@ -159,9 +160,8 @@ assertArrayEquals("Sent packet != original packet", originalPacket, sentDhcpPacket) } - @Test - fun testAttachRaFilter() { - val socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6) + private fun doTestAttachRaFilter(generic: Boolean) { + val socket = Os.socket(AF_PACKET, SOCK_RAW or SOCK_CLOEXEC, 0) val ifParams = InterfaceParams.getByName(iface.interfaceName) ?: fail("Could not obtain interface params for ${iface.interfaceName}") val socketAddr = SocketUtils.makePacketSocketAddress(ETH_P_IPV6, ifParams.index) @@ -176,7 +176,11 @@ echo.rewind() assertNextPacketEquals(socket, echo.readAsArray(), "ICMPv6 echo") - NetworkStackUtils.attachRaFilter(socket) + if (generic) { + NetworkStackUtils.attachControlPacketFilter(socket) + } else { + NetworkStackUtils.attachRaFilter(socket) + } // Send another echo, then an RA. After setting the filter expect only the RA. echo.rewind() reader.sendResponse(echo) @@ -192,6 +196,16 @@ assertNextPacketEquals(socket, ra.readAsArray(), "ICMPv6 RA") } + @Test + fun testAttachRaFilter() { + doTestAttachRaFilter(false) + } + + @Test + fun testRaViaAttachControlPacketFilter() { + doTestAttachRaFilter(true) + } + private fun assertNextPacketEquals(socket: FileDescriptor, expected: ByteArray, descr: String) { val buffer = ByteArray(TEST_MTU) val readPacket = Os.read(socket, buffer, 0 /* byteOffset */, buffer.size) @@ -293,12 +307,15 @@ packet.putShort(checksumOffset, IpUtils.ipChecksum(packet, ETHER_HEADER_LEN)) } - @Test - fun testDhcpResponseWithMfBitDropped() { + private fun doTestDhcpResponseWithMfBitDropped(generic: Boolean) { val ifindex = InterfaceParams.getByName(iface.interfaceName).index val packetSock = Os.socket(AF_PACKET, SOCK_RAW or SOCK_NONBLOCK, /*protocol=*/0) try { - NetworkStackUtils.attachDhcpFilter(packetSock) + if (generic) { + NetworkStackUtils.attachControlPacketFilter(packetSock) + } else { + NetworkStackUtils.attachDhcpFilter(packetSock) + } val addr = SocketUtils.makePacketSocketAddress(OsConstants.ETH_P_IP, ifindex) Os.bind(packetSock, addr) val packet = DhcpPacket.buildNakPacket(DhcpPacket.ENCAP_L2, 42, @@ -319,6 +336,16 @@ Os.close(packetSock) } } + + @Test + fun testDhcpResponseWithMfBitDropped() { + doTestDhcpResponseWithMfBitDropped(false) + } + + @Test + fun testGenericDhcpResponseWithMfBitDropped() { + doTestDhcpResponseWithMfBitDropped(true) + } } private fun ByteBuffer.readAsArray(): ByteArray {
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp index 1233ae5..464e4a1 100644 --- a/tests/unit/Android.bp +++ b/tests/unit/Android.bp
@@ -76,6 +76,7 @@ min_sdk_version: "30", defaults: ["NetworkStackTestsDefaults"], static_libs: ["NetworkStackApiStableLib"], + lint: { test: true }, visibility: [ "//packages/modules/NetworkStack/tests/integration", "//packages/modules/Connectivity/tests:__subpackages__",
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp index 1dbfbbe..50576cc 100644 --- a/tests/unit/jni/Android.bp +++ b/tests/unit/jni/Android.bp
@@ -37,6 +37,8 @@ ], static_libs: [ "libapf", + "libapf_v5", + "libapfdisassembler", "libpcap", ], sdk_version: "30",
diff --git a/tests/unit/jni/apf_jni.cpp b/tests/unit/jni/apf_jni.cpp index ff30bd1..5ae3ada 100644 --- a/tests/unit/jni/apf_jni.cpp +++ b/tests/unit/jni/apf_jni.cpp
@@ -24,15 +24,32 @@ #include <vector> #include "apf_interpreter.h" +#include "disassembler.h" #include "nativehelper/scoped_primitive_array.h" +#include "v5/apf_interpreter.h" +#include "v5/test_buf_allocator.h" #define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) #define LOG_TAG "NetworkStackUtils-JNI" +static int run_apf_interpreter(int apf_version, uint8_t* program, + uint32_t program_len, uint32_t ram_len, + const uint8_t* packet, uint32_t packet_len, + uint32_t filter_age) { + if (apf_version == 4) { + return accept_packet(program, program_len, ram_len, packet, packet_len, + filter_age); + } else { + return apf_run(program, program_len, ram_len, packet, packet_len, + filter_age); + } +} + // JNI function acting as simply call-through to native APF interpreter. -static jint com_android_server_ApfTest_apfSimulate( - JNIEnv* env, jclass, jbyteArray jprogram, jbyteArray jpacket, - jbyteArray jdata, jint filter_age) { +static jint +com_android_server_ApfTest_apfSimulate(JNIEnv* env, jclass, jint apf_version, + jbyteArray jprogram, jbyteArray jpacket, + jbyteArray jdata, jint filter_age) { ScopedByteArrayRO packet(env, jpacket); uint32_t packet_len = (uint32_t)packet.size(); @@ -47,9 +64,10 @@ reinterpret_cast<jbyte*>(buf.data() + program_len)); } - jint result = - accept_packet(buf.data(), program_len, program_len + data_len, - reinterpret_cast<const uint8_t*>(packet.get()), packet_len, filter_age); + jint result = run_apf_interpreter( + apf_version, buf.data(), program_len, program_len + data_len, + reinterpret_cast<const uint8_t *>(packet.get()), packet_len, + filter_age); if (jdata) { env->SetByteArrayRegion(jdata, 0, data_len, @@ -118,8 +136,9 @@ return env->NewStringUTF(bpf_string.c_str()); } -static jboolean com_android_server_ApfTest_compareBpfApf(JNIEnv* env, jclass, jstring jfilter, - jstring jpcap_filename, jbyteArray japf_program) { +static jboolean com_android_server_ApfTest_compareBpfApf( + JNIEnv* env, jclass, jint apf_version, jstring jfilter, + jstring jpcap_filename, jbyteArray japf_program) { ScopedUtfChars filter(env, jfilter); ScopedUtfChars pcap_filename(env, jpcap_filename); ScopedByteArrayRO apf_program(env, japf_program); @@ -163,7 +182,7 @@ const uint8_t* apf_packet; do { apf_packet = pcap_next(apf_pcap.get(), &apf_header); - } while (apf_packet != NULL && !accept_packet( + } while (apf_packet != NULL && !run_apf_interpreter(apf_version, reinterpret_cast<uint8_t*>(const_cast<int8_t*>(apf_program.get())), apf_program.size(), 0 /* data_len */, apf_packet, apf_header.len, 0 /* filter_age */)); @@ -182,8 +201,9 @@ return true; } -static jboolean com_android_server_ApfTest_dropsAllPackets(JNIEnv* env, jclass, jbyteArray jprogram, - jbyteArray jdata, jstring jpcap_filename) { +static jboolean com_android_server_ApfTest_dropsAllPackets( + JNIEnv* env, jclass, jint apf_version, jbyteArray jprogram, + jbyteArray jdata, jstring jpcap_filename) { ScopedUtfChars pcap_filename(env, jpcap_filename); ScopedByteArrayRO apf_program(env, jprogram); uint32_t apf_program_len = (uint32_t)apf_program.size(); @@ -208,8 +228,9 @@ } while ((apf_packet = pcap_next(apf_pcap.get(), &apf_header)) != NULL) { - int result = accept_packet(buf.data(), apf_program_len, - apf_program_len + data_len, apf_packet, apf_header.len, 0); + int result = run_apf_interpreter( + apf_version, buf.data(), apf_program_len, + apf_program_len + data_len, apf_packet, apf_header.len, 0); // Return false once packet passes the filter if (result) { @@ -224,6 +245,52 @@ return true; } +static char output_buffer[512]; + +static jobjectArray com_android_server_ApfTest_disassembleApf( + JNIEnv* env, jclass, jbyteArray jprogram) { + uint32_t program_len = env->GetArrayLength(jprogram); + std::vector<uint8_t> buf(program_len, 0); + + env->GetByteArrayRegion(jprogram, 0, program_len, + reinterpret_cast<jbyte*>(buf.data())); + std::vector<std::string> disassemble_output; + for (uint32_t pc = 0; pc < program_len;) { + pc = apf_disassemble(buf.data(), program_len, pc, output_buffer, + sizeof(output_buffer) / sizeof(output_buffer[0])); + disassemble_output.emplace_back(output_buffer); + } + jclass stringClass = env->FindClass("java/lang/String"); + jobjectArray disassembleOutput = + env->NewObjectArray(disassemble_output.size(), stringClass, nullptr); + + for (jsize i = 0; i < (jsize) disassemble_output.size(); i++) { + jstring j_disassemble_output = + env->NewStringUTF(disassemble_output[i].c_str()); + env->SetObjectArrayElement(disassembleOutput, i, j_disassemble_output); + env->DeleteLocalRef(j_disassemble_output); + } + + return disassembleOutput; +} + +jbyteArray com_android_server_ApfTest_getTransmittedPacket(JNIEnv* env, + jclass) { + jbyteArray jdata = env->NewByteArray((jint) apf_test_tx_packet_len); + if (jdata == NULL) { return NULL; } + if (apf_test_tx_packet_len == 0) { return jdata; } + + env->SetByteArrayRegion(jdata, 0, (jint) apf_test_tx_packet_len, + reinterpret_cast<jbyte*>(apf_test_tx_packet)); + + return jdata; +} + +void com_android_server_ApfTest_resetTransmittedPacketMemory(JNIEnv, jclass) { + apf_test_tx_packet_len = 0; + memset(apf_test_tx_packet, 0, APF_TX_BUFFER_SIZE); +} + extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { JNIEnv *env; if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { @@ -232,17 +299,23 @@ } static JNINativeMethod gMethods[] = { - { "apfSimulate", "([B[B[BI)I", + { "apfSimulate", "(I[B[B[BI)I", (void*)com_android_server_ApfTest_apfSimulate }, { "compileToBpf", "(Ljava/lang/String;)Ljava/lang/String;", (void*)com_android_server_ApfTest_compileToBpf }, - { "compareBpfApf", "(Ljava/lang/String;Ljava/lang/String;[B)Z", + { "compareBpfApf", "(ILjava/lang/String;Ljava/lang/String;[B)Z", (void*)com_android_server_ApfTest_compareBpfApf }, - { "dropsAllPackets", "([B[BLjava/lang/String;)Z", + { "dropsAllPackets", "(I[B[BLjava/lang/String;)Z", (void*)com_android_server_ApfTest_dropsAllPackets }, + { "disassembleApf", "([B)[Ljava/lang/String;", + (void*)com_android_server_ApfTest_disassembleApf }, + { "getTransmittedPacket", "()[B", + (void*)com_android_server_ApfTest_getTransmittedPacket }, + { "resetTransmittedPacketMemory", "()V", + (void*)com_android_server_ApfTest_resetTransmittedPacketMemory }, }; - jniRegisterNativeMethods(env, "android/net/apf/ApfTest", + jniRegisterNativeMethods(env, "android/net/apf/ApfJniUtils", gMethods, ARRAY_SIZE(gMethods)); return JNI_VERSION_1_6;
diff --git a/tests/unit/src/android/net/apf/ApfJniUtils.java b/tests/unit/src/android/net/apf/ApfJniUtils.java new file mode 100644 index 0000000..e6a7ad7 --- /dev/null +++ b/tests/unit/src/android/net/apf/ApfJniUtils.java
@@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 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.apf; + +/** + * The class contains the helper method for interacting with native apf code. + */ +public class ApfJniUtils { + + static { + // Load up native shared library containing APF interpreter exposed via JNI. + System.loadLibrary("networkstacktestsjni"); + } + + /** + * Call the APF interpreter to run {@code program} on {@code packet} with persistent memory + * segment {@data} pretending the filter was installed {@code filter_age} seconds ago. + */ + public static native int apfSimulate(int apfVersion, byte[] program, byte[] packet, + byte[] data, int filterAge); + + /** + * Compile a tcpdump human-readable filter (e.g. "icmp" or "tcp port 54") into a BPF + * prorgam and return a human-readable dump of the BPF program identical to "tcpdump -d". + */ + public static native String compileToBpf(String filter); + + /** + * Open packet capture file {@code pcap_filename} and filter the packets using tcpdump + * human-readable filter (e.g. "icmp" or "tcp port 54") compiled to a BPF program and + * at the same time using APF program {@code apf_program}. Return {@code true} if + * both APF and BPF programs filter out exactly the same packets. + */ + public static native boolean compareBpfApf(int apfVersion, String filter, + String pcapFilename, byte[] apfProgram); + + /** + * Open packet capture file {@code pcapFilename} and run it through APF filter. Then + * checks whether all the packets are dropped and populates data[] {@code data} with + * the APF counters. + */ + public static native boolean dropsAllPackets(int apfVersion, byte[] program, byte[] data, + String pcapFilename); + + /** + * Disassemble the Apf program into human-readable text. + */ + public static native String[] disassembleApf(byte[] program); + + /** + * Get the transmitted packet. + */ + public static native byte[] getTransmittedPacket(); + + /** + * Reset the memory region that stored the transmitted packet. + */ + public static native void resetTransmittedPacketMemory(); +}
diff --git a/tests/unit/src/android/net/apf/ApfTest.java b/tests/unit/src/android/net/apf/ApfTest.java index dd1ccb6..69c8917 100644 --- a/tests/unit/src/android/net/apf/ApfTest.java +++ b/tests/unit/src/android/net/apf/ApfTest.java
@@ -18,6 +18,13 @@ import static android.net.apf.ApfGenerator.Register.R0; import static android.net.apf.ApfGenerator.Register.R1; +import static android.net.apf.ApfJniUtils.compareBpfApf; +import static android.net.apf.ApfJniUtils.compileToBpf; +import static android.net.apf.ApfJniUtils.dropsAllPackets; +import static android.net.apf.ApfTestUtils.DROP; +import static android.net.apf.ApfTestUtils.MIN_PKT_SIZE; +import static android.net.apf.ApfTestUtils.PASS; +import static android.net.apf.ApfTestUtils.assertProgramEquals; import static android.system.OsConstants.AF_UNIX; import static android.system.OsConstants.ARPHRD_ETHER; import static android.system.OsConstants.ETH_P_ARP; @@ -30,15 +37,12 @@ import static android.system.OsConstants.SOCK_STREAM; import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE; -import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import android.content.Context; import android.net.InetAddresses; @@ -52,10 +56,7 @@ import android.net.apf.ApfGenerator.IllegalInstructionException; import android.net.ip.IIpClientCallbacks; import android.net.ip.IpClient.IpClientCallbacksWrapper; -import android.net.metrics.IpConnectivityLog; -import android.net.metrics.RaEvent; import android.os.ConditionVariable; -import android.os.Parcelable; import android.os.SystemClock; import android.system.ErrnoException; import android.system.Os; @@ -66,7 +67,6 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.HexDump; import com.android.net.module.util.DnsPacket; @@ -85,10 +85,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; +import org.junit.runners.Parameterized; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileOutputStream; @@ -109,33 +110,31 @@ /** * Tests for APF program generator and interpreter. * - * Build, install and run with: - * runtest frameworks-net -c android.net.apf.ApfTest + * The test cases will be executed by both APFv4 and APFv6 interpreter. */ -@RunWith(AndroidJUnit4.class) +@RunWith(Parameterized.class) @SmallTest public class ApfTest { private static final int TIMEOUT_MS = 500; private static final int MIN_APF_VERSION = 2; - @Mock IpConnectivityLog mLog; - @Mock Context mContext; + // Indicates which apf interpreter to run. + @Parameterized.Parameter() + public int mApfVersion; + @Parameterized.Parameters + public static Iterable<? extends Object> data() { + return Arrays.asList(4, 6); + } + + @Mock Context mContext; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - // Load up native shared library containing APF interpreter exposed via JNI. - System.loadLibrary("networkstacktestsjni"); } private static final String TAG = "ApfTest"; // Expected return codes from APF interpreter. - private static final int PASS = 1; - private static final int DROP = 0; - // Interpreter will just accept packets without link layer headers, so pad fake packet to at - // least the minimum packet size. - private static final int MIN_PKT_SIZE = 15; - private static final ApfCapabilities MOCK_APF_CAPABILITIES = new ApfCapabilities(2, 4096, ARPHRD_ETHER); @@ -168,101 +167,53 @@ return config; } - private static String label(int code) { - switch (code) { - case PASS: return "PASS"; - case DROP: return "DROP"; - default: return "UNKNOWN"; - } + private void assertPass(ApfGenerator gen) throws ApfGenerator.IllegalInstructionException { + ApfTestUtils.assertPass(mApfVersion, gen); } - private static void assertReturnCodesEqual(String msg, int expected, int got) { - assertEquals(msg, label(expected), label(got)); - } - - private static void assertReturnCodesEqual(int expected, int got) { - assertEquals(label(expected), label(got)); - } - - private void assertVerdict(int expected, byte[] program, byte[] packet, int filterAge) { - final String msg = "Unexpected APF verdict. To debug:\n" - + " apf_run --program " + HexDump.toHexString(program) - + " --packet " + HexDump.toHexString(packet) + " --trace | less\n "; - assertReturnCodesEqual(msg, expected, apfSimulate(program, packet, null, filterAge)); - } - - private void assertVerdict(String msg, int expected, byte[] program, byte[] packet, - int filterAge) { - assertReturnCodesEqual(msg, expected, apfSimulate(program, packet, null, filterAge)); - } - - private void assertVerdict(int expected, byte[] program, byte[] packet) { - assertVerdict(expected, program, packet, 0); - } - - private void assertPass(byte[] program, byte[] packet, int filterAge) { - assertVerdict(PASS, program, packet, filterAge); + private void assertDrop(ApfGenerator gen) throws ApfGenerator.IllegalInstructionException { + ApfTestUtils.assertDrop(mApfVersion, gen); } private void assertPass(byte[] program, byte[] packet) { - assertVerdict(PASS, program, packet); - } - - private void assertDrop(byte[] program, byte[] packet, int filterAge) { - assertVerdict(DROP, program, packet, filterAge); + ApfTestUtils.assertPass(mApfVersion, program, packet); } private void assertDrop(byte[] program, byte[] packet) { - assertVerdict(DROP, program, packet); + ApfTestUtils.assertDrop(mApfVersion, program, packet); } - private void assertProgramEquals(byte[] expected, byte[] program) throws AssertionError { - // assertArrayEquals() would only print one byte, making debugging difficult. - if (!Arrays.equals(expected, program)) { - throw new AssertionError( - "\nexpected: " + HexDump.toHexString(expected) + - "\nactual: " + HexDump.toHexString(program)); - } + private void assertPass(byte[] program, byte[] packet, int filterAge) { + ApfTestUtils.assertPass(mApfVersion, program, packet, filterAge); } - private void assertDataMemoryContents( - int expected, byte[] program, byte[] packet, byte[] data, byte[] expected_data) - throws IllegalInstructionException, Exception { - assertReturnCodesEqual(expected, apfSimulate(program, packet, data, 0 /* filterAge */)); - - // assertArrayEquals() would only print one byte, making debugging difficult. - if (!Arrays.equals(expected_data, data)) { - throw new Exception( - "\nprogram: " + HexDump.toHexString(program) + - "\ndata memory: " + HexDump.toHexString(data) + - "\nexpected: " + HexDump.toHexString(expected_data)); - } - } - - private void assertVerdict(int expected, ApfGenerator gen, byte[] packet, int filterAge) - throws IllegalInstructionException { - assertReturnCodesEqual(expected, apfSimulate(gen.generate(), packet, null, - filterAge)); + private void assertDrop(byte[] program, byte[] packet, int filterAge) { + ApfTestUtils.assertDrop(mApfVersion, program, packet, filterAge); } private void assertPass(ApfGenerator gen, byte[] packet, int filterAge) - throws IllegalInstructionException { - assertVerdict(PASS, gen, packet, filterAge); + throws ApfGenerator.IllegalInstructionException { + ApfTestUtils.assertPass(mApfVersion, gen, packet, filterAge); } private void assertDrop(ApfGenerator gen, byte[] packet, int filterAge) - throws IllegalInstructionException { - assertVerdict(DROP, gen, packet, filterAge); + throws ApfGenerator.IllegalInstructionException { + ApfTestUtils.assertDrop(mApfVersion, gen, packet, filterAge); } - private void assertPass(ApfGenerator gen) - throws IllegalInstructionException { - assertVerdict(PASS, gen, new byte[MIN_PKT_SIZE], 0); + private void assertDataMemoryContents(int expected, byte[] program, byte[] packet, + byte[] data, byte[] expectedData) throws Exception { + ApfTestUtils.assertDataMemoryContents(mApfVersion, expected, program, packet, data, + expectedData); } - private void assertDrop(ApfGenerator gen) - throws IllegalInstructionException { - assertVerdict(DROP, gen, new byte[MIN_PKT_SIZE], 0); + private void assertVerdict(String msg, int expected, byte[] program, + byte[] packet, int filterAge) { + ApfTestUtils.assertVerdict(mApfVersion, msg, expected, program, packet, filterAge); + } + + private void assertVerdict(int expected, byte[] program, byte[] packet) { + ApfTestUtils.assertVerdict(mApfVersion, expected, program, packet); } /** @@ -928,7 +879,7 @@ for (String tcpdump_filter : tcpdump_filters) { byte[] apf_program = Bpf2Apf.convert(compileToBpf(tcpdump_filter)); assertTrue("Failed to match for filter: " + tcpdump_filter, - compareBpfApf(tcpdump_filter, pcap_filename, apf_program)); + compareBpfApf(mApfVersion, tcpdump_filter, pcap_filename, apf_program)); } } @@ -950,17 +901,18 @@ config.apfCapabilities = MOCK_APF_PCAP_CAPABILITIES; config.multicastFilter = DROP_MULTICAST; config.ieee802_3Filter = DROP_802_3_FRAMES; - TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); + TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); apfFilter.setLinkProperties(lp); - byte[] program = ipClientCallback.getApfProgram(); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); byte[] data = new byte[ApfFilter.Counter.totalSize()]; final boolean result; - result = dropsAllPackets(program, data, pcapFilename); + result = dropsAllPackets(mApfVersion, program, data, pcapFilename); Log.i(TAG, "testApfFilterPcapFile(): Data counters: " + HexDump.toHexString(data, false)); assertTrue("Failed to drop all packets by filter. \nAPF counters:" + HexDump.toHexString(data, false), result); + apfFilter.shutdown(); } private class MockIpClientCallback extends IpClientCallbacksWrapper { @@ -982,7 +934,7 @@ mGotApfProgram.close(); } - public byte[] getApfProgram() { + public byte[] assertProgramUpdateAndGet() { assertTrue(mGotApfProgram.block(TIMEOUT_MS)); return mLastApfProgram; } @@ -997,14 +949,17 @@ private FileDescriptor mWriteSocket; private long mCurrentTimeMs = SystemClock.elapsedRealtime(); + private final MockIpClientCallback mMockIpClientCb; public TestApfFilter(Context context, ApfConfiguration config, - IpClientCallbacksWrapper ipClientCallback, IpConnectivityLog log) throws Exception { - super(context, config, InterfaceParams.getByName("lo"), ipClientCallback, log); + MockIpClientCallback ipClientCallback) throws Exception { + super(context, config, InterfaceParams.getByName("lo"), ipClientCallback); + mMockIpClientCb = ipClientCallback; } // Pretend an RA packet has been received and show it to ApfFilter. public void pretendPacketReceived(byte[] packet) throws IOException, ErrnoException { + mMockIpClientCb.resetApfProgramWait(); // ApfFilter's ReceiveThread will be waiting to read this. Os.write(mWriteSocket, packet, 0, packet.length); } @@ -1015,8 +970,8 @@ } @Override - protected long currentTimeSeconds() { - return mCurrentTimeMs / DateUtils.SECOND_IN_MILLIS; + protected int secondsSinceBoot() { + return (int) (mCurrentTimeMs / DateUtils.SECOND_IN_MILLIS); } @Override @@ -1042,6 +997,10 @@ @Override public void shutdown() { super.shutdown(); + if (mReceiveThread != null) { + mReceiveThread.halt(); + mReceiveThread = null; + } IoUtils.closeQuietly(mWriteSocket); } } @@ -1177,11 +1136,11 @@ // Helper to initialize a default apfFilter. private ApfFilter setupApfFilter( - IpClientCallbacksWrapper ipClientCallback, ApfConfiguration config) throws Exception { + MockIpClientCallback ipClientCallback, ApfConfiguration config) throws Exception { LinkAddress link = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 19); LinkProperties lp = new LinkProperties(); lp.addLinkAddress(link); - TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); + TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); apfFilter.setLinkProperties(lp); return apfFilter; } @@ -1219,10 +1178,10 @@ ApfConfiguration config = getDefaultConfig(); config.multicastFilter = DROP_MULTICAST; - TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); + TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); apfFilter.setLinkProperties(lp); - byte[] program = ipClientCallback.getApfProgram(); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); // Verify empty packet of 100 zero bytes is passed ByteBuffer packet = ByteBuffer.wrap(new byte[100]); @@ -1271,8 +1230,8 @@ public void testApfFilterIPv6() throws Exception { MockIpClientCallback ipClientCallback = new MockIpClientCallback(); ApfConfiguration config = getDefaultConfig(); - TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); - byte[] program = ipClientCallback.getApfProgram(); + TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); // Verify empty IPv6 packet is passed ByteBuffer packet = makeIpv6Packet(IPPROTO_UDP); @@ -1489,7 +1448,7 @@ /** Adds to the program a no-op instruction that is one byte long. */ private void addOneByteNoop(ApfGenerator gen) { - gen.addOr(0); + gen.addLeftShift(0); } @Test @@ -1526,13 +1485,13 @@ lp.addLinkAddress(link); ApfConfiguration config = getDefaultConfig(); - TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); + TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); apfFilter.setLinkProperties(lp); // Construct IPv4 mDNS packet byte[] mdnsv4packet = makeMdnsV4Packet("test.local"); byte[] mdnsv6packet = makeMdnsV6Packet("test.local"); - byte[] program = ipClientCallback.getApfProgram(); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); // mDNSv4 packet is passed if no mDns filter is turned on assertPass(program, mdnsv4packet); // mDNSv6 packet is passed if no mDNS filter is turned on @@ -1542,7 +1501,7 @@ apfFilter.addToMdnsAllowList(new String[]{"test", "local"}); apfFilter.addToMdnsAllowList(new String[]{"abcd", "local"}); apfFilter.setMulticastFilter(true); - program = ipClientCallback.getApfProgram(); + program = ipClientCallback.assertProgramUpdateAndGet(); assertPass(program, mdnsv4packet); assertPass(program, mdnsv6packet); // If packet contains more than one qname, pass the packet @@ -1568,7 +1527,7 @@ assertDrop(program, mdnsv6packet); apfFilter.removeFromAllowList(new String[]{"abcd", "local"}); - program = ipClientCallback.getApfProgram(); + program = ipClientCallback.assertProgramUpdateAndGet(); mdnsv4packet = makeMdnsV4Packet("abcd.local"); mdnsv6packet = makeMdnsV6Packet("abcd.local"); assertDrop(program, mdnsv4packet); @@ -1763,10 +1722,10 @@ ApfConfiguration config = getDefaultConfig(); config.ieee802_3Filter = DROP_802_3_FRAMES; - TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); + TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); apfFilter.setLinkProperties(lp); - byte[] program = ipClientCallback.getApfProgram(); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); // Construct IPv4 and IPv6 multicast packets. ByteBuffer mcastv4packet = makeIpv4Packet(IPPROTO_UDP); @@ -1802,7 +1761,7 @@ // Turn on multicast filter and verify it works ipClientCallback.resetApfProgramWait(); apfFilter.setMulticastFilter(true); - program = ipClientCallback.getApfProgram(); + program = ipClientCallback.assertProgramUpdateAndGet(); assertDrop(program, mcastv4packet.array()); assertDrop(program, mcastv6packet.array()); assertDrop(program, bcastv4packet1.array()); @@ -1812,7 +1771,7 @@ // Turn off multicast filter and verify it's off ipClientCallback.resetApfProgramWait(); apfFilter.setMulticastFilter(false); - program = ipClientCallback.getApfProgram(); + program = ipClientCallback.assertProgramUpdateAndGet(); assertPass(program, mcastv4packet.array()); assertPass(program, mcastv6packet.array()); assertPass(program, bcastv4packet1.array()); @@ -1824,9 +1783,9 @@ apfFilter.shutdown(); config.multicastFilter = DROP_MULTICAST; config.ieee802_3Filter = DROP_802_3_FRAMES; - apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); + apfFilter = new TestApfFilter(mContext, config, ipClientCallback); apfFilter.setLinkProperties(lp); - program = ipClientCallback.getApfProgram(); + program = ipClientCallback.assertProgramUpdateAndGet(); assertDrop(program, mcastv4packet.array()); assertDrop(program, mcastv6packet.array()); assertDrop(program, bcastv4packet1.array()); @@ -1851,27 +1810,27 @@ put(packet, IPV6_DEST_ADDR_OFFSET, multicastIpv6Addr); // Normally, we let multicast pings alone... - assertPass(ipClientCallback.getApfProgram(), packet.array()); + assertPass(ipClientCallback.assertProgramUpdateAndGet(), packet.array()); // ...and even while dozing... apfFilter.setDozeMode(true); - assertPass(ipClientCallback.getApfProgram(), packet.array()); + assertPass(ipClientCallback.assertProgramUpdateAndGet(), packet.array()); // ...but when the multicast filter is also enabled, drop the multicast pings to save power. apfFilter.setMulticastFilter(true); - assertDrop(ipClientCallback.getApfProgram(), packet.array()); + assertDrop(ipClientCallback.assertProgramUpdateAndGet(), packet.array()); // However, we should still let through all other ICMPv6 types. ByteBuffer raPacket = ByteBuffer.wrap(packet.array().clone()); setIpv6VersionFields(packet); packet.put(IPV6_NEXT_HEADER_OFFSET, (byte) IPPROTO_ICMPV6); raPacket.put(ICMP6_TYPE_OFFSET, (byte) NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT); - assertPass(ipClientCallback.getApfProgram(), raPacket.array()); + assertPass(ipClientCallback.assertProgramUpdateAndGet(), raPacket.array()); // Now wake up from doze mode to ensure that we no longer drop the packets. // (The multicast filter is still enabled at this point). apfFilter.setDozeMode(false); - assertPass(ipClientCallback.getApfProgram(), packet.array()); + assertPass(ipClientCallback.assertProgramUpdateAndGet(), packet.array()); apfFilter.shutdown(); } @@ -1881,7 +1840,7 @@ MockIpClientCallback ipClientCallback = new MockIpClientCallback(); ApfConfiguration config = getDefaultConfig(); ApfFilter apfFilter = setupApfFilter(ipClientCallback, config); - byte[] program = ipClientCallback.getApfProgram(); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); // Verify empty packet of 100 zero bytes is passed // Note that eth-type = 0 makes it an IEEE802.3 frame @@ -1901,7 +1860,7 @@ apfFilter.shutdown(); config.ieee802_3Filter = DROP_802_3_FRAMES; apfFilter = setupApfFilter(ipClientCallback, config); - program = ipClientCallback.getApfProgram(); + program = ipClientCallback.assertProgramUpdateAndGet(); // Verify that IEEE802.3 frame is dropped // In this case ethtype is used for payload length @@ -1928,7 +1887,7 @@ MockIpClientCallback ipClientCallback = new MockIpClientCallback(); ApfConfiguration config = getDefaultConfig(); ApfFilter apfFilter = setupApfFilter(ipClientCallback, config); - byte[] program = ipClientCallback.getApfProgram(); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); // Verify empty packet of 100 zero bytes is passed // Note that eth-type = 0 makes it an IEEE802.3 frame @@ -1948,7 +1907,7 @@ apfFilter.shutdown(); config.ethTypeBlackList = ipv4BlackList; apfFilter = setupApfFilter(ipClientCallback, config); - program = ipClientCallback.getApfProgram(); + program = ipClientCallback.assertProgramUpdateAndGet(); // Verify that IPv4 frame will be dropped setIpv4VersionFields(packet); @@ -1963,7 +1922,7 @@ apfFilter.shutdown(); config.ethTypeBlackList = ipv4Ipv6BlackList; apfFilter = setupApfFilter(ipClientCallback, config); - program = ipClientCallback.getApfProgram(); + program = ipClientCallback.assertProgramUpdateAndGet(); // Verify that IPv4 frame will be dropped setIpv4VersionFields(packet); @@ -1979,7 +1938,7 @@ private byte[] getProgram(MockIpClientCallback cb, ApfFilter filter, LinkProperties lp) { cb.resetApfProgramWait(); filter.setLinkProperties(lp); - return cb.getApfProgram(); + return cb.assertProgramUpdateAndGet(); } private void verifyArpFilter(byte[] program, int filterResult) { @@ -2009,10 +1968,10 @@ ApfConfiguration config = getDefaultConfig(); config.multicastFilter = DROP_MULTICAST; config.ieee802_3Filter = DROP_802_3_FRAMES; - TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); + TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); // Verify initially ARP request filter is off, and GARP filter is on. - verifyArpFilter(ipClientCallback.getApfProgram(), PASS); + verifyArpFilter(ipClientCallback.assertProgramUpdateAndGet(), PASS); // Inform ApfFilter of our address and verify ARP filtering is on LinkAddress linkAddress = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 24); @@ -2069,7 +2028,7 @@ final ApfConfiguration config = getDefaultConfig(); config.multicastFilter = DROP_MULTICAST; config.ieee802_3Filter = DROP_802_3_FRAMES; - final TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mLog); + final TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb); byte[] program; final int srcPort = 12345; final int dstPort = 54321; @@ -2098,7 +2057,7 @@ parcel.ack = ackNum; apfFilter.addTcpKeepalivePacketFilter(slot1, parcel); - program = cb.getApfProgram(); + program = cb.assertProgramUpdateAndGet(); // Verify IPv4 keepalive ack packet is dropped // src: 10.0.0.6, port: 54321 @@ -2137,7 +2096,7 @@ ipv6Parcel.ack = ackNum; apfFilter.addTcpKeepalivePacketFilter(slot1, ipv6Parcel); - program = cb.getApfProgram(); + program = cb.assertProgramUpdateAndGet(); // Verify IPv6 keepalive ack packet is dropped // src: 2404:0:0:0:0:0:faf2, port: 54321 @@ -2160,7 +2119,7 @@ // Verify multiple filters apfFilter.addTcpKeepalivePacketFilter(slot1, parcel); apfFilter.addTcpKeepalivePacketFilter(slot2, ipv6Parcel); - program = cb.getApfProgram(); + program = cb.assertProgramUpdateAndGet(); // Verify IPv4 keepalive ack packet is dropped // src: 10.0.0.6, port: 54321 @@ -2199,7 +2158,7 @@ // TODO: support V6 packets } - program = cb.getApfProgram(); + program = cb.assertProgramUpdateAndGet(); // Verify IPv4, IPv6 packets are passed assertPass(program, @@ -2262,7 +2221,7 @@ final ApfConfiguration config = getDefaultConfig(); config.multicastFilter = DROP_MULTICAST; config.ieee802_3Filter = DROP_802_3_FRAMES; - final TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mLog); + final TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb); byte[] program; final int srcPort = 1024; final int dstPort = 4500; @@ -2283,7 +2242,7 @@ parcel.dstPort = dstPort; apfFilter.addNattKeepalivePacketFilter(slot1, parcel); - program = cb.getApfProgram(); + program = cb.assertProgramUpdateAndGet(); // Verify IPv4 keepalive packet is dropped // src: 10.0.0.6, port: 4500 @@ -2330,100 +2289,188 @@ return packet.array(); } - private void addRdnssOption(ByteBuffer packet, int lifetime, String... servers) - throws Exception { - int optionLength = 1 + 2 * servers.length; // In 8-byte units - packet.put((byte) ICMP6_RDNSS_OPTION_TYPE); // Type - packet.put((byte) optionLength); // Length - packet.putShort((short) 0); // Reserved - packet.putInt(lifetime); // Lifetime - for (String server : servers) { - packet.put(InetAddress.getByName(server).getAddress()); - } - } + private static class RaPacketBuilder { + final ByteArrayOutputStream mPacket = new ByteArrayOutputStream(); + int mFlowLabel = 0x12345; + int mReachableTime = 30_000; + int mRetransmissionTimer = 1000; - private void addRioOption(ByteBuffer packet, int lifetime, String prefixString) - throws Exception { - IpPrefix prefix = new IpPrefix(prefixString); + public RaPacketBuilder(int routerLft) throws Exception { + InetAddress src = InetAddress.getByName("fe80::1234:abcd"); + ByteBuffer buffer = ByteBuffer.allocate(ICMP6_RA_OPTION_OFFSET); - int optionLength; - if (prefix.getPrefixLength() == 0) { - optionLength = 1; - } else if (prefix.getPrefixLength() <= 64) { - optionLength = 2; - } else { - optionLength = 3; + buffer.putShort(ETH_ETHERTYPE_OFFSET, (short) ETH_P_IPV6); + buffer.position(ETH_HEADER_LEN); + + // skip version, tclass, flowlabel; set in build() + buffer.position(buffer.position() + 4); + + buffer.putShort((short) 0); // Payload length; updated later + buffer.put((byte) IPPROTO_ICMPV6); // Next header + buffer.put((byte) 0xff); // Hop limit + buffer.put(src.getAddress()); // Source address + buffer.put(IPV6_ALL_NODES_ADDRESS); // Destination address + + buffer.put((byte) ICMP6_ROUTER_ADVERTISEMENT); // Type + buffer.put((byte) 0); // Code (0) + buffer.putShort((short) 0); // Checksum (ignored) + buffer.put((byte) 64); // Hop limit + buffer.put((byte) 0); // M/O, reserved + buffer.putShort((short) routerLft); // Router lifetime + // skip reachable time; set in build() + // skip retransmission timer; set in build(); + + mPacket.write(buffer.array(), 0, buffer.capacity()); } - packet.put((byte) ICMP6_ROUTE_INFO_OPTION_TYPE); // Type - packet.put((byte) optionLength); // Length in 8-byte units - packet.put((byte) prefix.getPrefixLength()); // Prefix length - packet.put((byte) 0b00011000); // Pref = high - packet.putInt(lifetime); // Lifetime + public RaPacketBuilder setFlowLabel(int flowLabel) { + mFlowLabel = flowLabel; + return this; + } - byte[] prefixBytes = prefix.getRawAddress(); - packet.put(prefixBytes, 0, (optionLength - 1) * 8); - } + public RaPacketBuilder setReachableTime(int reachable) { + mReachableTime = reachable; + return this; + } - private void addPioOption(ByteBuffer packet, int valid, int preferred, String prefixString) { - IpPrefix prefix = new IpPrefix(prefixString); - packet.put((byte) ICMP6_PREFIX_OPTION_TYPE); // Type - packet.put((byte) 4); // Length in 8-byte units - packet.put((byte) prefix.getPrefixLength()); // Prefix length - packet.put((byte) 0b11000000); // L = 1, A = 1 - packet.putInt(valid); - packet.putInt(preferred); - packet.putInt(0); // Reserved - packet.put(prefix.getRawAddress()); + public RaPacketBuilder setRetransmissionTimer(int retrans) { + mRetransmissionTimer = retrans; + return this; + } + + public RaPacketBuilder addPioOption(int valid, int preferred, String prefixString) + throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(ICMP6_PREFIX_OPTION_LEN); + + IpPrefix prefix = new IpPrefix(prefixString); + buffer.put((byte) ICMP6_PREFIX_OPTION_TYPE); // Type + buffer.put((byte) 4); // Length in 8-byte units + buffer.put((byte) prefix.getPrefixLength()); // Prefix length + buffer.put((byte) 0b11000000); // L = 1, A = 1 + buffer.putInt(valid); + buffer.putInt(preferred); + buffer.putInt(0); // Reserved + buffer.put(prefix.getRawAddress()); + + mPacket.write(buffer.array(), 0, buffer.capacity()); + return this; + } + + public RaPacketBuilder addRioOption(int lifetime, String prefixString) throws Exception { + IpPrefix prefix = new IpPrefix(prefixString); + + int optionLength; + if (prefix.getPrefixLength() == 0) { + optionLength = 1; + } else if (prefix.getPrefixLength() <= 64) { + optionLength = 2; + } else { + optionLength = 3; + } + + ByteBuffer buffer = ByteBuffer.allocate(optionLength * 8); + + buffer.put((byte) ICMP6_ROUTE_INFO_OPTION_TYPE); // Type + buffer.put((byte) optionLength); // Length in 8-byte units + buffer.put((byte) prefix.getPrefixLength()); // Prefix length + buffer.put((byte) 0b00011000); // Pref = high + buffer.putInt(lifetime); // Lifetime + + byte[] prefixBytes = prefix.getRawAddress(); + buffer.put(prefixBytes, 0, (optionLength - 1) * 8); + + mPacket.write(buffer.array(), 0, buffer.capacity()); + return this; + } + + public RaPacketBuilder addDnsslOption(int lifetime, String... domains) { + ByteArrayOutputStream dnssl = new ByteArrayOutputStream(); + for (String domain : domains) { + for (String label : domain.split(".")) { + final byte[] bytes = label.getBytes(StandardCharsets.UTF_8); + dnssl.write((byte) bytes.length); + dnssl.write(bytes, 0, bytes.length); + } + dnssl.write((byte) 0); + } + + // Extend with 0s to make it 8-byte aligned. + while (dnssl.size() % 8 != 0) { + dnssl.write((byte) 0); + } + + final int length = ICMP6_4_BYTE_OPTION_LEN + dnssl.size(); + ByteBuffer buffer = ByteBuffer.allocate(length); + + buffer.put((byte) ICMP6_DNSSL_OPTION_TYPE); // Type + buffer.put((byte) (length / 8)); // Length + // skip past reserved bytes + buffer.position(buffer.position() + 2); + buffer.putInt(lifetime); // Lifetime + buffer.put(dnssl.toByteArray()); // Domain names + + mPacket.write(buffer.array(), 0, buffer.capacity()); + return this; + } + + public RaPacketBuilder addRdnssOption(int lifetime, String... servers) throws Exception { + int optionLength = 1 + 2 * servers.length; // In 8-byte units + ByteBuffer buffer = ByteBuffer.allocate(optionLength * 8); + + buffer.put((byte) ICMP6_RDNSS_OPTION_TYPE); // Type + buffer.put((byte) optionLength); // Length + buffer.putShort((short) 0); // Reserved + buffer.putInt(lifetime); // Lifetime + for (String server : servers) { + buffer.put(InetAddress.getByName(server).getAddress()); + } + + mPacket.write(buffer.array(), 0, buffer.capacity()); + return this; + } + + public RaPacketBuilder addZeroLengthOption() throws Exception { + ByteBuffer buffer = ByteBuffer.allocate(ICMP6_4_BYTE_OPTION_LEN); + buffer.put((byte) ICMP6_PREFIX_OPTION_TYPE); + buffer.put((byte) 0); + + mPacket.write(buffer.array(), 0, buffer.capacity()); + return this; + } + + public byte[] build() { + ByteBuffer buffer = ByteBuffer.wrap(mPacket.toByteArray()); + // IPv6, traffic class = 0, flow label = mFlowLabel + buffer.putInt(IP_HEADER_OFFSET, 0x60000000 | (0xFFFFF & mFlowLabel)); + buffer.putShort(IPV6_PAYLOAD_LENGTH_OFFSET, (short) buffer.capacity()); + + buffer.position(ICMP6_RA_REACHABLE_TIME_OFFSET); + buffer.putInt(mReachableTime); + buffer.putInt(mRetransmissionTimer); + + return buffer.array(); + } } private byte[] buildLargeRa() throws Exception { - InetAddress src = InetAddress.getByName("fe80::1234:abcd"); + RaPacketBuilder builder = new RaPacketBuilder(1800 /* router lft */); - ByteBuffer packet = ByteBuffer.wrap(new byte[1514]); - packet.putShort(ETH_ETHERTYPE_OFFSET, (short) ETH_P_IPV6); - packet.position(ETH_HEADER_LEN); + builder.addRioOption(1200, "64:ff9b::/96"); + builder.addRdnssOption(7200, "2001:db8:1::1", "2001:db8:1::2"); + builder.addRioOption(2100, "2000::/3"); + builder.addRioOption(2400, "::/0"); + builder.addPioOption(600, 300, "2001:db8:a::/64"); + builder.addRioOption(1500, "2001:db8:c:d::/64"); + builder.addPioOption(86400, 43200, "fd95:d1e:12::/64"); - packet.putInt(0x60012345); // Version, tclass, flowlabel - packet.putShort((short) 0); // Payload length; updated later - packet.put((byte) IPPROTO_ICMPV6); // Next header - packet.put((byte) 0xff); // Hop limit - packet.put(src.getAddress()); // Source address - packet.put(IPV6_ALL_NODES_ADDRESS); // Destination address - - packet.put((byte) ICMP6_ROUTER_ADVERTISEMENT); // Type - packet.put((byte) 0); // Code (0) - packet.putShort((short) 0); // Checksum (ignored) - packet.put((byte) 64); // Hop limit - packet.put((byte) 0); // M/O, reserved - packet.putShort((short) 1800); // Router lifetime - packet.putInt(30_000); // Reachable time - packet.putInt(1000); // Retrans timer - - addRioOption(packet, 1200, "64:ff9b::/96"); - addRdnssOption(packet, 7200, "2001:db8:1::1", "2001:db8:1::2"); - addRioOption(packet, 2100, "2000::/3"); - addRioOption(packet, 2400, "::/0"); - addPioOption(packet, 600, 300, "2001:db8:a::/64"); - addRioOption(packet, 1500, "2001:db8:c:d::/64"); - addPioOption(packet, 86400, 43200, "fd95:d1e:12::/64"); - - int length = packet.position(); - packet.putShort(IPV6_PAYLOAD_LENGTH_OFFSET, (short) length); - - // Don't pass the Ra constructor a packet that is longer than the actual RA. - // This relies on the fact that all the relative writes to the byte buffer are at the end. - byte[] packetArray = new byte[length]; - packet.rewind(); - packet.get(packetArray); - return packetArray; + return builder.build(); } @Test public void testRaToString() throws Exception { MockIpClientCallback cb = new MockIpClientCallback(); ApfConfiguration config = getDefaultConfig(); - TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mLog); + TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb); byte[] packet = buildLargeRa(); ApfFilter.Ra ra = apfFilter.new Ra(packet, packet.length); @@ -2476,76 +2523,25 @@ private void verifyRaLifetime(TestApfFilter apfFilter, MockIpClientCallback ipClientCallback, ByteBuffer packet, int lifetime) throws IOException, ErrnoException { // Verify new program generated if ApfFilter witnesses RA - ipClientCallback.resetApfProgramWait(); apfFilter.pretendPacketReceived(packet.array()); - byte[] program = ipClientCallback.getApfProgram(); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); verifyRaLifetime(program, packet, lifetime); } - private void verifyRaEvent(RaEvent expected) { - ArgumentCaptor<IpConnectivityLog.Event> captor = - ArgumentCaptor.forClass(IpConnectivityLog.Event.class); - verify(mLog, atLeastOnce()).log(captor.capture()); - RaEvent got = lastRaEvent(captor.getAllValues()); - if (!raEventEquals(expected, got)) { - assertEquals(expected, got); // fail for printing an assertion error message. - } - } - - private RaEvent lastRaEvent(List<IpConnectivityLog.Event> events) { - RaEvent got = null; - for (Parcelable ev : events) { - if (ev instanceof RaEvent) { - got = (RaEvent) ev; - } - } - return got; - } - - private boolean raEventEquals(RaEvent ev1, RaEvent ev2) { - return (ev1 != null) && (ev2 != null) - && (ev1.routerLifetime == ev2.routerLifetime) - && (ev1.prefixValidLifetime == ev2.prefixValidLifetime) - && (ev1.prefixPreferredLifetime == ev2.prefixPreferredLifetime) - && (ev1.routeInfoLifetime == ev2.routeInfoLifetime) - && (ev1.rdnssLifetime == ev2.rdnssLifetime) - && (ev1.dnsslLifetime == ev2.dnsslLifetime); - } - private void assertInvalidRa(TestApfFilter apfFilter, MockIpClientCallback ipClientCallback, ByteBuffer packet) throws IOException, ErrnoException { - ipClientCallback.resetApfProgramWait(); apfFilter.pretendPacketReceived(packet.array()); ipClientCallback.assertNoProgramUpdate(); } - private ByteBuffer makeBaseRaPacket() { - ByteBuffer basePacket = ByteBuffer.wrap(new byte[ICMP6_RA_OPTION_OFFSET]); - final int ROUTER_LIFETIME = 1000; - final int VERSION_TRAFFIC_CLASS_FLOW_LABEL_OFFSET = ETH_HEADER_LEN; - // IPv6, traffic class = 0, flow label = 0x12345 - final int VERSION_TRAFFIC_CLASS_FLOW_LABEL = 0x60012345; - - basePacket.putShort(ETH_ETHERTYPE_OFFSET, (short) ETH_P_IPV6); - basePacket.putInt(VERSION_TRAFFIC_CLASS_FLOW_LABEL_OFFSET, - VERSION_TRAFFIC_CLASS_FLOW_LABEL); - basePacket.put(IPV6_NEXT_HEADER_OFFSET, (byte) IPPROTO_ICMPV6); - basePacket.put(ICMP6_TYPE_OFFSET, (byte) ICMP6_ROUTER_ADVERTISEMENT); - basePacket.putShort(ICMP6_RA_ROUTER_LIFETIME_OFFSET, (short) ROUTER_LIFETIME); - basePacket.position(IPV6_DEST_ADDR_OFFSET); - basePacket.put(IPV6_ALL_NODES_ADDRESS); - - return basePacket; - } - @Test public void testApfFilterRa() throws Exception { MockIpClientCallback ipClientCallback = new MockIpClientCallback(); ApfConfiguration config = getDefaultConfig(); config.multicastFilter = DROP_MULTICAST; config.ieee802_3Filter = DROP_802_3_FRAMES; - TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); - byte[] program = ipClientCallback.getApfProgram(); + TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); final int ROUTER_LIFETIME = 1000; final int PREFIX_VALID_LIFETIME = 200; @@ -2554,102 +2550,69 @@ final int ROUTE_LIFETIME = 400; // Note that lifetime of 2000 will be ignored in favor of shorter route lifetime of 1000. final int DNSSL_LIFETIME = 2000; - final int VERSION_TRAFFIC_CLASS_FLOW_LABEL_OFFSET = ETH_HEADER_LEN; - // IPv6, traffic class = 0, flow label = 0x12345 - final int VERSION_TRAFFIC_CLASS_FLOW_LABEL = 0x60012345; // Verify RA is passed the first time - ByteBuffer basePacket = makeBaseRaPacket(); + RaPacketBuilder ra = new RaPacketBuilder(ROUTER_LIFETIME); + ByteBuffer basePacket = ByteBuffer.wrap(ra.build()); assertPass(program, basePacket.array()); verifyRaLifetime(apfFilter, ipClientCallback, basePacket, ROUTER_LIFETIME); - verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, -1, -1, -1)); - ByteBuffer newFlowLabelPacket = ByteBuffer.wrap(new byte[ICMP6_RA_OPTION_OFFSET]); - basePacket.clear(); - newFlowLabelPacket.put(basePacket); + ra = new RaPacketBuilder(ROUTER_LIFETIME); // Check that changes are ignored in every byte of the flow label. - newFlowLabelPacket.putInt(VERSION_TRAFFIC_CLASS_FLOW_LABEL_OFFSET, - VERSION_TRAFFIC_CLASS_FLOW_LABEL + 0x11111); + ra.setFlowLabel(0x56789); + ByteBuffer newFlowLabelPacket = ByteBuffer.wrap(ra.build()); // Ensure zero-length options cause the packet to be silently skipped. // Do this before we test other packets. http://b/29586253 - ByteBuffer zeroLengthOptionPacket = ByteBuffer.wrap( - new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN]); - basePacket.clear(); - zeroLengthOptionPacket.put(basePacket); - zeroLengthOptionPacket.put((byte)ICMP6_PREFIX_OPTION_TYPE); - zeroLengthOptionPacket.put((byte)0); + ra = new RaPacketBuilder(ROUTER_LIFETIME); + ra.addZeroLengthOption(); + ByteBuffer zeroLengthOptionPacket = ByteBuffer.wrap(ra.build()); assertInvalidRa(apfFilter, ipClientCallback, zeroLengthOptionPacket); // Generate several RAs with different options and lifetimes, and verify when // ApfFilter is shown these packets, it generates programs to filter them for the // appropriate lifetime. - ByteBuffer prefixOptionPacket = ByteBuffer.wrap( - new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_PREFIX_OPTION_LEN]); - basePacket.clear(); - prefixOptionPacket.put(basePacket); - addPioOption(prefixOptionPacket, PREFIX_VALID_LIFETIME, PREFIX_PREFERRED_LIFETIME, - "2001:db8::/64"); + ra = new RaPacketBuilder(ROUTER_LIFETIME); + ra.addPioOption(PREFIX_VALID_LIFETIME, PREFIX_PREFERRED_LIFETIME, "2001:db8::/64"); + ByteBuffer prefixOptionPacket = ByteBuffer.wrap(ra.build()); verifyRaLifetime( apfFilter, ipClientCallback, prefixOptionPacket, PREFIX_PREFERRED_LIFETIME); - verifyRaEvent(new RaEvent( - ROUTER_LIFETIME, PREFIX_VALID_LIFETIME, PREFIX_PREFERRED_LIFETIME, -1, -1, -1)); - ByteBuffer rdnssOptionPacket = ByteBuffer.wrap( - new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN + 2 * IPV6_ADDR_LEN]); - basePacket.clear(); - rdnssOptionPacket.put(basePacket); - addRdnssOption(rdnssOptionPacket, RDNSS_LIFETIME, - "2001:4860:4860::8888", "2001:4860:4860::8844"); + ra = new RaPacketBuilder(ROUTER_LIFETIME); + ra.addRdnssOption(RDNSS_LIFETIME, "2001:4860:4860::8888", "2001:4860:4860::8844"); + ByteBuffer rdnssOptionPacket = ByteBuffer.wrap(ra.build()); verifyRaLifetime(apfFilter, ipClientCallback, rdnssOptionPacket, RDNSS_LIFETIME); - verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, -1, RDNSS_LIFETIME, -1)); final int lowLifetime = 60; - ByteBuffer lowLifetimeRdnssOptionPacket = ByteBuffer.wrap( - new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN + IPV6_ADDR_LEN]); - basePacket.clear(); - lowLifetimeRdnssOptionPacket.put(basePacket); - addRdnssOption(lowLifetimeRdnssOptionPacket, lowLifetime, "2620:fe::9"); + ra = new RaPacketBuilder(ROUTER_LIFETIME); + ra.addRdnssOption(lowLifetime, "2620:fe::9"); + ByteBuffer lowLifetimeRdnssOptionPacket = ByteBuffer.wrap(ra.build()); verifyRaLifetime(apfFilter, ipClientCallback, lowLifetimeRdnssOptionPacket, ROUTER_LIFETIME); - verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, -1, lowLifetime, -1)); - ByteBuffer routeInfoOptionPacket = ByteBuffer.wrap( - new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN + IPV6_ADDR_LEN]); - basePacket.clear(); - routeInfoOptionPacket.put(basePacket); - addRioOption(routeInfoOptionPacket, ROUTE_LIFETIME, "64:ff9b::/96"); + ra = new RaPacketBuilder(ROUTER_LIFETIME); + ra.addRioOption(ROUTE_LIFETIME, "64:ff9b::/96"); + ByteBuffer routeInfoOptionPacket = ByteBuffer.wrap(ra.build()); verifyRaLifetime(apfFilter, ipClientCallback, routeInfoOptionPacket, ROUTE_LIFETIME); - verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, ROUTE_LIFETIME, -1, -1)); // Check that RIOs differing only in the first 4 bytes are different. - ByteBuffer similarRouteInfoOptionPacket = ByteBuffer.wrap( - new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN + IPV6_ADDR_LEN]); - basePacket.clear(); - similarRouteInfoOptionPacket.put(basePacket); - addRioOption(similarRouteInfoOptionPacket, ROUTE_LIFETIME, "64:ff9b::/64"); + ra = new RaPacketBuilder(ROUTER_LIFETIME); + ra.addRioOption(ROUTE_LIFETIME, "64:ff9b::/64"); // Packet should be passed because it is different. - program = ipClientCallback.getApfProgram(); - assertPass(program, similarRouteInfoOptionPacket.array()); + program = ipClientCallback.assertProgramUpdateAndGet(); + assertPass(program, ra.build()); - ByteBuffer dnsslOptionPacket = ByteBuffer.wrap( - new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN]); - basePacket.clear(); - dnsslOptionPacket.put(basePacket); - dnsslOptionPacket.put((byte)ICMP6_DNSSL_OPTION_TYPE); - dnsslOptionPacket.put((byte)(ICMP6_4_BYTE_OPTION_LEN / 8)); - dnsslOptionPacket.putInt( - ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_LIFETIME_OFFSET, DNSSL_LIFETIME); + ra = new RaPacketBuilder(ROUTER_LIFETIME); + ra.addDnsslOption(DNSSL_LIFETIME, "test.example.com", "one.more.example.com"); + ByteBuffer dnsslOptionPacket = ByteBuffer.wrap(ra.build()); verifyRaLifetime(apfFilter, ipClientCallback, dnsslOptionPacket, ROUTER_LIFETIME); - verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, -1, -1, DNSSL_LIFETIME)); ByteBuffer largeRaPacket = ByteBuffer.wrap(buildLargeRa()); verifyRaLifetime(apfFilter, ipClientCallback, largeRaPacket, 300); - verifyRaEvent(new RaEvent(1800, 600, 300, 1200, 7200, -1)); // Verify that current program filters all the RAs (note: ApfFilter.MAX_RAS == 10). - program = ipClientCallback.getApfProgram(); + program = ipClientCallback.assertProgramUpdateAndGet(); verifyRaLifetime(program, basePacket, ROUTER_LIFETIME); verifyRaLifetime(program, newFlowLabelPacket, ROUTER_LIFETIME); verifyRaLifetime(program, prefixOptionPacket, PREFIX_PREFERRED_LIFETIME); @@ -2668,38 +2631,37 @@ final ApfConfiguration config = getDefaultConfig(); config.multicastFilter = DROP_MULTICAST; config.ieee802_3Filter = DROP_802_3_FRAMES; - final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); - byte[] program = ipClientCallback.getApfProgram(); + final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); final int RA_REACHABLE_TIME = 1800; final int RA_RETRANSMISSION_TIMER = 1234; // Create an Ra packet without options // Reachable time = 1800, retransmission timer = 1234 - ByteBuffer raPacket = makeBaseRaPacket(); - raPacket.position(ICMP6_RA_REACHABLE_TIME_OFFSET); - raPacket.putInt(RA_REACHABLE_TIME); - raPacket.putInt(RA_RETRANSMISSION_TIMER); + RaPacketBuilder ra = new RaPacketBuilder(1800 /* router lft */); + ra.setReachableTime(RA_REACHABLE_TIME); + ra.setRetransmissionTimer(RA_RETRANSMISSION_TIMER); + byte[] raPacket = ra.build(); // First RA passes filter - assertPass(program, raPacket.array()); + assertPass(program, raPacket); // Assume apf is shown the given RA, it generates program to filter it. - ipClientCallback.resetApfProgramWait(); - apfFilter.pretendPacketReceived(raPacket.array()); - program = ipClientCallback.getApfProgram(); - assertDrop(program, raPacket.array()); + apfFilter.pretendPacketReceived(raPacket); + program = ipClientCallback.assertProgramUpdateAndGet(); + assertDrop(program, raPacket); // A packet with different reachable time should be passed. // Reachable time = 2300, retransmission timer = 1234 - raPacket.clear(); - raPacket.putInt(ICMP6_RA_REACHABLE_TIME_OFFSET, RA_REACHABLE_TIME + 500); - assertPass(program, raPacket.array()); + ra.setReachableTime(RA_REACHABLE_TIME + 500); + raPacket = ra.build(); + assertPass(program, raPacket); // A packet with different retransmission timer should be passed. // Reachable time = 1800, retransmission timer = 2234 - raPacket.clear(); - raPacket.putInt(ICMP6_RA_REACHABLE_TIME_OFFSET, RA_REACHABLE_TIME); - raPacket.putInt(ICMP6_RA_RETRANSMISSION_TIMER_OFFSET, RA_RETRANSMISSION_TIMER + 1000); - assertPass(program, raPacket.array()); + ra.setReachableTime(RA_REACHABLE_TIME); + ra.setRetransmissionTimer(RA_RETRANSMISSION_TIMER + 1000); + raPacket = ra.build(); + assertPass(program, raPacket); } // The ByteBuffer is always created by ByteBuffer#wrap in the helper functions @@ -2710,21 +2672,21 @@ final ApfConfiguration config = getDefaultConfig(); config.multicastFilter = DROP_MULTICAST; config.ieee802_3Filter = DROP_802_3_FRAMES; - final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); - byte[] program = ipClientCallback.getApfProgram(); + final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); final int routerLifetime = 1000; final int timePassedSeconds = 12; // Verify that when the program is generated and installed some time after RA is last seen // it should be installed with the correct remaining lifetime. - ByteBuffer basePacket = makeBaseRaPacket(); + ByteBuffer basePacket = ByteBuffer.wrap(new RaPacketBuilder(routerLifetime).build()); verifyRaLifetime(apfFilter, ipClientCallback, basePacket, routerLifetime); apfFilter.increaseCurrentTimeSeconds(timePassedSeconds); synchronized (apfFilter) { apfFilter.installNewProgramLocked(); } - program = ipClientCallback.getApfProgram(); + program = ipClientCallback.assertProgramUpdateAndGet(); verifyRaLifetime(program, basePacket, routerLifetime, timePassedSeconds); // Packet should be passed if the program is installed after 1/6 * lifetime from last seen @@ -2732,13 +2694,13 @@ synchronized (apfFilter) { apfFilter.installNewProgramLocked(); } - program = ipClientCallback.getApfProgram(); + program = ipClientCallback.assertProgramUpdateAndGet(); assertDrop(program, basePacket.array()); apfFilter.increaseCurrentTimeSeconds(1); synchronized (apfFilter) { apfFilter.installNewProgramLocked(); } - program = ipClientCallback.getApfProgram(); + program = ipClientCallback.assertProgramUpdateAndGet(); assertPass(program, basePacket.array()); apfFilter.shutdown(); @@ -2779,7 +2741,7 @@ ApfConfiguration config = getDefaultConfig(); config.multicastFilter = DROP_MULTICAST; config.ieee802_3Filter = DROP_802_3_FRAMES; - TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mLog); + TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb); for (int i = 0; i < 1000; i++) { byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)]; r.nextBytes(packet); @@ -2790,6 +2752,7 @@ throw new Exception("bad packet: " + HexDump.toHexString(packet), e); } } + apfFilter.shutdown(); } @Test @@ -2800,7 +2763,7 @@ ApfConfiguration config = getDefaultConfig(); config.multicastFilter = DROP_MULTICAST; config.ieee802_3Filter = DROP_802_3_FRAMES; - TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mLog); + TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb); for (int i = 0; i < 1000; i++) { byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)]; r.nextBytes(packet); @@ -2810,37 +2773,293 @@ throw new Exception("bad packet: " + HexDump.toHexString(packet), e); } } + apfFilter.shutdown(); } - /** - * Call the APF interpreter to run {@code program} on {@code packet} with persistent memory - * segment {@data} pretending the filter was installed {@code filter_age} seconds ago. - */ - private native static int apfSimulate(byte[] program, byte[] packet, byte[] data, - int filter_age); + @Test + public void testMatchedRaUpdatesLifetime() throws Exception { + final MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + final TestApfFilter apfFilter = new TestApfFilter(mContext, getDefaultConfig(), + ipClientCallback); - /** - * Compile a tcpdump human-readable filter (e.g. "icmp" or "tcp port 54") into a BPF - * prorgam and return a human-readable dump of the BPF program identical to "tcpdump -d". - */ - private native static String compileToBpf(String filter); + // Create an RA and build an APF program + byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build(); + apfFilter.pretendPacketReceived(ra); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); - /** - * Open packet capture file {@code pcap_filename} and filter the packets using tcpdump - * human-readable filter (e.g. "icmp" or "tcp port 54") compiled to a BPF program and - * at the same time using APF program {@code apf_program}. Return {@code true} if - * both APF and BPF programs filter out exactly the same packets. - */ - private native static boolean compareBpfApf(String filter, String pcap_filename, - byte[] apf_program); + // lifetime dropped significantly, assert pass + ra = new RaPacketBuilder(200 /* router lifetime */).build(); + assertPass(program, ra); + + // update program with the new RA + apfFilter.pretendPacketReceived(ra); + program = ipClientCallback.assertProgramUpdateAndGet(); + + // assert program was updated and new lifetimes were taken into account. + assertDrop(program, ra); + apfFilter.shutdown(); + } + + // Test for go/apf-ra-filter Case 1a. + // Old lifetime is 0 + @Test + public void testAcceptRaMinLftCase1a() throws Exception { + final MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + // configure accept_ra_min_lft + final ApfConfiguration config = getDefaultConfig(); + config.acceptRaMinLft = 180; + final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); + + // Create an initial RA and build an APF program + byte[] ra = new RaPacketBuilder(1800 /* router lifetime */) + .addPioOption(1800 /*valid*/, 0 /*preferred*/, "2001:db8::/64") + .build(); + + apfFilter.pretendPacketReceived(ra); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); + + // repeated RA is dropped + assertDrop(program, ra); + + // PIO preferred lifetime increases + ra = new RaPacketBuilder(1800 /* router lifetime */) + .addPioOption(1800 /*valid*/, 1 /*preferred*/, "2001:db8::/64") + .build(); + assertPass(program, ra); + apfFilter.shutdown(); + } + + // Test for go/apf-ra-filter Case 2a. + // Old lifetime is > 0 + @Test + public void testAcceptRaMinLftCase2a() throws Exception { + final MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + // configure accept_ra_min_lft + final ApfConfiguration config = getDefaultConfig(); + config.acceptRaMinLft = 180; + final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); + + // Create an initial RA and build an APF program + byte[] ra = new RaPacketBuilder(1800 /* router lifetime */) + .addPioOption(1800 /*valid*/, 100 /*preferred*/, "2001:db8::/64") + .build(); + + apfFilter.pretendPacketReceived(ra); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); + + // repeated RA is dropped + assertDrop(program, ra); + + // PIO preferred lifetime increases + ra = new RaPacketBuilder(1800 /* router lifetime */) + .addPioOption(1800 /*valid*/, 101 /*preferred*/, "2001:db8::/64") + .build(); + assertPass(program, ra); + + // PIO preferred lifetime decreases significantly + ra = new RaPacketBuilder(1800 /* router lifetime */) + .addPioOption(1800 /*valid*/, 33 /*preferred*/, "2001:db8::/64") + .build(); + assertPass(program, ra); + apfFilter.shutdown(); + } - /** - * Open packet capture file {@code pcapFilename} and run it through APF filter. Then - * checks whether all the packets are dropped and populates data[] {@code data} with - * the APF counters. - */ - private native static boolean dropsAllPackets(byte[] program, byte[] data, String pcapFilename); + // Test for go/apf-ra-filter Case 1b. + // Old lifetime is 0 + @Test + public void testAcceptRaMinLftCase1b() throws Exception { + final MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + // configure accept_ra_min_lft + final ApfConfiguration config = getDefaultConfig(); + config.acceptRaMinLft = 180; + final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); + + // Create an initial RA and build an APF program + byte[] ra = new RaPacketBuilder(0 /* router lifetime */).build(); + + apfFilter.pretendPacketReceived(ra); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); + + // repeated RA is dropped + assertDrop(program, ra); + + // lifetime increases below accept_ra_min_lft + ra = new RaPacketBuilder(179 /* router lifetime */).build(); + assertDrop(program, ra); + + // lifetime increases to accept_ra_min_lft + ra = new RaPacketBuilder(180 /* router lifetime */).build(); + assertPass(program, ra); + apfFilter.shutdown(); + } + + + // Test for go/apf-ra-filter Case 2b. + // Old lifetime is < accept_ra_min_lft (but not 0). + @Test + public void testAcceptRaMinLftCase2b() throws Exception { + final MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + // configure accept_ra_min_lft + final ApfConfiguration config = getDefaultConfig(); + config.acceptRaMinLft = 180; + final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); + + // Create an initial RA and build an APF program + byte[] ra = new RaPacketBuilder(100 /* router lifetime */).build(); + + apfFilter.pretendPacketReceived(ra); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); + + // repeated RA is dropped + assertDrop(program, ra); + + // lifetime increases + ra = new RaPacketBuilder(101 /* router lifetime */).build(); + assertDrop(program, ra); + + // lifetime decreases significantly + ra = new RaPacketBuilder(1 /* router lifetime */).build(); + assertDrop(program, ra); + + // equals accept_ra_min_lft + ra = new RaPacketBuilder(180 /* router lifetime */).build(); + assertPass(program, ra); + + // lifetime is 0 + ra = new RaPacketBuilder(0 /* router lifetime */).build(); + assertPass(program, ra); + apfFilter.shutdown(); + } + + // Test for go/apf-ra-filter Case 3b. + // Old lifetime is >= accept_ra_min_lft and <= 3 * accept_ra_min_lft + @Test + public void testAcceptRaMinLftCase3b() throws Exception { + final MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + // configure accept_ra_min_lft + final ApfConfiguration config = getDefaultConfig(); + config.acceptRaMinLft = 180; + final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); + + // Create an initial RA and build an APF program + byte[] ra = new RaPacketBuilder(200 /* router lifetime */).build(); + + apfFilter.pretendPacketReceived(ra); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); + + // repeated RA is dropped + assertDrop(program, ra); + + // lifetime increases + ra = new RaPacketBuilder(201 /* router lifetime */).build(); + assertPass(program, ra); + + // lifetime is below accept_ra_min_lft (but not 0) + ra = new RaPacketBuilder(1 /* router lifetime */).build(); + assertDrop(program, ra); + + // lifetime is 0 + ra = new RaPacketBuilder(0 /* router lifetime */).build(); + assertPass(program, ra); + apfFilter.shutdown(); + } + + // Test for go/apf-ra-filter Case 4b. + // Old lifetime is > 3 * accept_ra_min_lft + @Test + public void testAcceptRaMinLftCase4b() throws Exception { + final MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + // configure accept_ra_min_lft + final ApfConfiguration config = getDefaultConfig(); + config.acceptRaMinLft = 180; + final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); + + // Create an initial RA and build an APF program + byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build(); + + apfFilter.pretendPacketReceived(ra); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); + + // repeated RA is dropped + assertDrop(program, ra); + + // lifetime increases + ra = new RaPacketBuilder(1801 /* router lifetime */).build(); + assertPass(program, ra); + + // lifetime is 1/3 of old lft + ra = new RaPacketBuilder(600 /* router lifetime */).build(); + assertDrop(program, ra); + + // lifetime is below 1/3 of old lft + ra = new RaPacketBuilder(599 /* router lifetime */).build(); + assertPass(program, ra); + + // lifetime is below accept_ra_min_lft (but not 0) + ra = new RaPacketBuilder(1 /* router lifetime */).build(); + assertDrop(program, ra); + + // lifetime is 0 + ra = new RaPacketBuilder(0 /* router lifetime */).build(); + assertPass(program, ra); + apfFilter.shutdown(); + } + + @Test + public void testRaFilterIsUpdated() throws Exception { + final MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + // configure accept_ra_min_lft + final ApfConfiguration config = getDefaultConfig(); + config.acceptRaMinLft = 180; + final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback); + + // Create an initial RA and build an APF program + byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build(); + apfFilter.pretendPacketReceived(ra); + byte[] program = ipClientCallback.assertProgramUpdateAndGet(); + + // repeated RA is dropped. + assertDrop(program, ra); + + // updated RA is passed, repeated RA is dropped after program update. + ra = new RaPacketBuilder(599 /* router lifetime */).build(); + assertPass(program, ra); + apfFilter.pretendPacketReceived(ra); + program = ipClientCallback.assertProgramUpdateAndGet(); + assertDrop(program, ra); + + ra = new RaPacketBuilder(180 /* router lifetime */).build(); + assertPass(program, ra); + apfFilter.pretendPacketReceived(ra); + program = ipClientCallback.assertProgramUpdateAndGet(); + assertDrop(program, ra); + + ra = new RaPacketBuilder(0 /* router lifetime */).build(); + assertPass(program, ra); + apfFilter.pretendPacketReceived(ra); + program = ipClientCallback.assertProgramUpdateAndGet(); + assertDrop(program, ra); + + ra = new RaPacketBuilder(180 /* router lifetime */).build(); + assertPass(program, ra); + apfFilter.pretendPacketReceived(ra); + program = ipClientCallback.assertProgramUpdateAndGet(); + assertDrop(program, ra); + + ra = new RaPacketBuilder(599 /* router lifetime */).build(); + assertPass(program, ra); + apfFilter.pretendPacketReceived(ra); + program = ipClientCallback.assertProgramUpdateAndGet(); + assertDrop(program, ra); + + ra = new RaPacketBuilder(1800 /* router lifetime */).build(); + assertPass(program, ra); + apfFilter.pretendPacketReceived(ra); + program = ipClientCallback.assertProgramUpdateAndGet(); + assertDrop(program, ra); + apfFilter.shutdown(); + } @Test public void testBroadcastAddress() throws Exception {
diff --git a/tests/unit/src/android/net/apf/ApfTestUtils.java b/tests/unit/src/android/net/apf/ApfTestUtils.java new file mode 100644 index 0000000..555b92e --- /dev/null +++ b/tests/unit/src/android/net/apf/ApfTestUtils.java
@@ -0,0 +1,177 @@ +/* + * Copyright (C) 2023 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.apf; + +import static android.net.apf.ApfJniUtils.apfSimulate; + +import static org.junit.Assert.assertEquals; + +import com.android.internal.util.HexDump; + +import java.util.Arrays; + +/** + * The util class for calling the APF interpreter and check the return value + */ +public class ApfTestUtils { + public static final int PASS = 1; + public static final int DROP = 0; + // Interpreter will just accept packets without link layer headers, so pad fake packet to at + // least the minimum packet size. + public static final int MIN_PKT_SIZE = 15; + + private ApfTestUtils() { + } + + private static String label(int code) { + switch (code) { + case PASS: + return "PASS"; + case DROP: + return "DROP"; + default: + return "UNKNOWN"; + } + } + + private static void assertReturnCodesEqual(String msg, int expected, int got) { + assertEquals(msg, label(expected), label(got)); + } + + private static void assertReturnCodesEqual(int expected, int got) { + assertEquals(label(expected), label(got)); + } + + private static void assertVerdict(int apfVersion, int expected, byte[] program, byte[] packet, + int filterAge) { + final String msg = "Unexpected APF verdict. To debug:\n" + " apf_run --program " + + HexDump.toHexString(program) + " --packet " + HexDump.toHexString(packet) + + " --trace | less\n "; + assertReturnCodesEqual(msg, expected, + apfSimulate(apfVersion, program, packet, null, filterAge)); + } + + /** + * Runs the APF program and checks the return code is equals to expected value. If not, the + * customized message is printed. + */ + public static void assertVerdict(int apfVersion, String msg, int expected, byte[] program, + byte[] packet, int filterAge) { + assertReturnCodesEqual(msg, expected, + apfSimulate(apfVersion, program, packet, null, filterAge)); + } + + /** + * Runs the APF program and checks the return code is equals to expected value. + */ + public static void assertVerdict(int apfVersion, int expected, byte[] program, byte[] packet) { + assertVerdict(apfVersion, expected, program, packet, 0); + } + + /** + * Runs the APF program and checks the return code is PASS. + */ + public static void assertPass(int apfVersion, byte[] program, byte[] packet, int filterAge) { + assertVerdict(apfVersion, PASS, program, packet, filterAge); + } + + /** + * Runs the APF program and checks the return code is PASS. + */ + public static void assertPass(int apfVersion, byte[] program, byte[] packet) { + assertVerdict(apfVersion, PASS, program, packet); + } + + /** + * Runs the APF program and checks the return code is DROP. + */ + public static void assertDrop(int apfVersion, byte[] program, byte[] packet, int filterAge) { + assertVerdict(apfVersion, DROP, program, packet, filterAge); + } + + /** + * Runs the APF program and checks the return code is DROP. + */ + public static void assertDrop(int apfVersion, byte[] program, byte[] packet) { + assertVerdict(apfVersion, DROP, program, packet); + } + + /** + * Checks the generated APF program equals to the expected value. + */ + public static void assertProgramEquals(byte[] expected, byte[] program) throws AssertionError { + // assertArrayEquals() would only print one byte, making debugging difficult. + if (!Arrays.equals(expected, program)) { + throw new AssertionError("\nexpected: " + HexDump.toHexString(expected) + "\nactual: " + + HexDump.toHexString(program)); + } + } + + /** + * Runs the APF program and checks the return code and data regions equals to expected value. + */ + public static void assertDataMemoryContents(int apfVersion, int expected, byte[] program, + byte[] packet, byte[] data, byte[] expectedData) + throws ApfGenerator.IllegalInstructionException, Exception { + assertReturnCodesEqual(expected, + apfSimulate(apfVersion, program, packet, data, 0 /* filterAge */)); + + // assertArrayEquals() would only print one byte, making debugging difficult. + if (!Arrays.equals(expectedData, data)) { + throw new Exception("\nprogram: " + HexDump.toHexString(program) + "\ndata memory: " + + HexDump.toHexString(data) + "\nexpected: " + HexDump.toHexString( + expectedData)); + } + } + + private static void assertVerdict(int apfVersion, int expected, ApfGenerator gen, byte[] packet, + int filterAge) throws ApfGenerator.IllegalInstructionException { + assertReturnCodesEqual(expected, + apfSimulate(apfVersion, gen.generate(), packet, null, filterAge)); + } + + /** + * Runs the APF program and checks the return code is PASS. + */ + public static void assertPass(int apfVersion, ApfGenerator gen, byte[] packet, int filterAge) + throws ApfGenerator.IllegalInstructionException { + assertVerdict(apfVersion, PASS, gen, packet, filterAge); + } + + /** + * Runs the APF program and checks the return code is DROP. + */ + public static void assertDrop(int apfVersion, ApfGenerator gen, byte[] packet, int filterAge) + throws ApfGenerator.IllegalInstructionException { + assertVerdict(apfVersion, DROP, gen, packet, filterAge); + } + + /** + * Runs the APF program and checks the return code is PASS. + */ + public static void assertPass(int apfVersion, ApfGenerator gen) + throws ApfGenerator.IllegalInstructionException { + assertVerdict(apfVersion, PASS, gen, new byte[MIN_PKT_SIZE], 0); + } + + /** + * Runs the APF program and checks the return code is DROP. + */ + public static void assertDrop(int apfVersion, ApfGenerator gen) + throws ApfGenerator.IllegalInstructionException { + assertVerdict(apfVersion, DROP, gen, new byte[MIN_PKT_SIZE], 0); + } +}
diff --git a/tests/unit/src/android/net/apf/ApfV5Test.kt b/tests/unit/src/android/net/apf/ApfV5Test.kt new file mode 100644 index 0000000..d42396a --- /dev/null +++ b/tests/unit/src/android/net/apf/ApfV5Test.kt
@@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 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.apf + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import kotlin.test.assertContentEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for APFv6 specific instructions. + */ +@RunWith(AndroidJUnit4::class) +@SmallTest +class ApfV5Test { + + @Test + fun testApfInstructionsEncoding() { + var gen = ApfGenerator(MIN_APF_VERSION) + gen.addAlloc(ApfGenerator.Register.R0) + var program = gen.generate() + assertContentEquals(byteArrayOf(encodeInstruction(21, 1, 0), 36), program) + assertContentEquals(arrayOf(" 0: alloc r0"), ApfJniUtils.disassembleApf(program)) + + gen = ApfGenerator(MIN_APF_VERSION) + gen.addTrans(ApfGenerator.Register.R1) + program = gen.generate() + assertContentEquals(byteArrayOf(encodeInstruction(21, 1, 1), 37), program) + assertContentEquals(arrayOf(" 0: trans r1"), ApfJniUtils.disassembleApf(program)) + + gen = ApfGenerator(MIN_APF_VERSION) + gen.addWrite(0x01, 1) + gen.addWrite(0x0102, 2) + gen.addWrite(0x01020304, 4) + program = gen.generate() + assertContentEquals(byteArrayOf( + encodeInstruction(24, 1, 0), 0x01, + encodeInstruction(24, 2, 0), 0x01, 0x02, + encodeInstruction(24, 4, 0), 0x01, 0x02, 0x03, 0x04 + ), program) + assertContentEquals(arrayOf( + " 0: write 0x01", + " 2: write 0x0102", + " 5: write 0x01020304"), ApfJniUtils.disassembleApf(program)) + + gen = ApfGenerator(MIN_APF_VERSION) + gen.addWrite(ApfGenerator.Register.R0, 1) + gen.addWrite(ApfGenerator.Register.R0, 2) + gen.addWrite(ApfGenerator.Register.R0, 4) + program = gen.generate() + assertContentEquals(byteArrayOf( + encodeInstruction(21, 1, 0), 38, + encodeInstruction(21, 1, 0), 39, + encodeInstruction(21, 1, 0), 40 + ), program) + assertContentEquals(arrayOf( + " 0: write r0, 1", + " 2: write r0, 2", + " 4: write r0, 4"), ApfJniUtils.disassembleApf(program)) + } + + private fun encodeInstruction(opcode: Int, immLength: Int, register: Int): Byte { + val immLengthEncoding = if (immLength == 4) 3 else immLength + return opcode.shl(3).or(immLengthEncoding.shl(1)).or(register).toByte() + } + + companion object { + private const val MIN_APF_VERSION = 5 + } +}
diff --git a/tests/unit/src/android/net/dhcp/DhcpPacketTest.java b/tests/unit/src/android/net/dhcp/DhcpPacketTest.java index 1a1f6c3..42ea54b 100644 --- a/tests/unit/src/android/net/dhcp/DhcpPacketTest.java +++ b/tests/unit/src/android/net/dhcp/DhcpPacketTest.java
@@ -1300,6 +1300,44 @@ checkBuildOfferPacket(3600, null); } + @Test + public void testInvalidLengthIpv6OnlyPreferredOption() throws Exception { + // CHECKSTYLE:OFF Generated code + final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray( + // IP header. + "45100158000040004011B5CEC0A80164C0A80102" + + // UDP header + "004300440144CE63" + + // BOOTP header + "02010600B8BF41E60000000000000000C0A80102C0A8016400000000" + + // MAC address. + "22B3614EE01200000000000000000000" + + // Server name and padding. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // File. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // Options + "638253633501023604C0A80164330400000E103A04000007083B0400000C4E01" + + "04FFFFFF001C04C0A801FF0304C0A801640604C0A801640C0C74657374686F73" + + "746E616D651A0205DC" + + // Option 108 (0x6c, IPv6-Only preferred option), length 8 (0x08) + "6C080102030405060708" + + // End of options. + "FF")); + // CHECKSTYLE:ON Generated code + + final DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3, + TEST_EMPTY_OPTIONS_SKIP_LIST); + // rfc8925#section-3.1: The client MUST ignore the IPv6-Only Preferred option if the length + // field value is not 4. + assertTrue(offerPacket instanceof DhcpOfferPacket); + assertEquals(offerPacket.mIpv6OnlyWaitTime, null); + } + private static byte[] intToByteArray(int val) { return ByteBuffer.allocate(4).putInt(val).array(); }
diff --git a/tests/unit/src/android/net/ip/IpClientTest.java b/tests/unit/src/android/net/ip/IpClientTest.java index 7db939b..d8b2cc2 100644 --- a/tests/unit/src/android/net/ip/IpClientTest.java +++ b/tests/unit/src/android/net/ip/IpClientTest.java
@@ -17,6 +17,7 @@ package android.net.ip; import static android.system.OsConstants.RT_SCOPE_UNIVERSE; + import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -36,6 +37,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; + import static java.util.Collections.emptySet; import android.annotation.SuppressLint; @@ -420,6 +422,8 @@ verifyNetworkAttributesStored(l2Key, new NetworkAttributes.Builder() .setCluster(cluster) .build()); + + verifyShutdown(ipc); } private void verifyShutdown(IpClient ipc) throws Exception { @@ -479,6 +483,8 @@ fail(testcase.errorMessage()); } } + + ipc.shutdown(); } static class IsProvisionedTestCase { @@ -827,6 +833,7 @@ final MacAddress bssid = ipc.getInitialBssid(layer2Info, scanResultInfo, true /* isAtLeastS */); assertEquals(bssid, MacAddress.fromString(TEST_BSSID)); + ipc.shutdown(); } @Test @@ -837,6 +844,7 @@ final MacAddress bssid = ipc.getInitialBssid(layer2Info, null /* ScanResultInfo */, true /* isAtLeastS */); assertEquals(bssid, MacAddress.fromString(TEST_BSSID)); + ipc.shutdown(); } @Test @@ -848,6 +856,7 @@ final MacAddress bssid = ipc.getInitialBssid(layer2Info, scanResultInfo, true /* isAtLeastS */); assertNull(bssid); + ipc.shutdown(); } @Test @@ -857,6 +866,7 @@ final MacAddress bssid = ipc.getInitialBssid(null /* layer2Info */, scanResultInfo, true /* isAtLeastS */); assertNull(bssid); + ipc.shutdown(); } @Test @@ -868,6 +878,7 @@ final MacAddress bssid = ipc.getInitialBssid(layer2Info, scanResultInfo, false /* isAtLeastS */); assertEquals(bssid, MacAddress.fromString(TEST_BSSID)); + ipc.shutdown(); } @Test @@ -877,6 +888,7 @@ final MacAddress bssid = ipc.getInitialBssid(null /* layer2Info */, scanResultInfo, false /* isAtLeastS */); assertEquals(bssid, MacAddress.fromString(TEST_BSSID)); + ipc.shutdown(); } @Test @@ -886,6 +898,7 @@ final MacAddress bssid = ipc.getInitialBssid(null /* layer2Info */, scanResultInfo, false /* isAtLeastS */); assertNull(bssid); + ipc.shutdown(); } @Test @@ -897,6 +910,7 @@ final MacAddress bssid = ipc.getInitialBssid(layer2Info, scanResultInfo, false /* isAtLeastS */); assertEquals(bssid, MacAddress.fromString(TEST_BSSID)); + ipc.shutdown(); } @Test @@ -907,6 +921,7 @@ final MacAddress bssid = ipc.getInitialBssid(layer2Info, null /* scanResultInfo */, false /* isAtLeastS */); assertEquals(bssid, MacAddress.fromString(TEST_BSSID)); + ipc.shutdown(); } @Test @@ -915,6 +930,7 @@ final MacAddress bssid = ipc.getInitialBssid(null /* layer2Info */, null /* scanResultInfo */, false /* isAtLeastS */); assertNull(bssid); + ipc.shutdown(); } interface Fn<A,B> {
diff --git a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt index 3800752..6471f3a 100644 --- a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt +++ b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
@@ -24,7 +24,6 @@ import android.net.LinkProperties import android.net.RouteInfo import android.net.metrics.IpConnectivityLog -import com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION import android.os.Handler import android.os.HandlerThread import android.os.MessageQueue @@ -36,12 +35,12 @@ import android.stats.connectivity.NudEventType.NUD_CONFIRM_FAILED import android.stats.connectivity.NudEventType.NUD_CONFIRM_FAILED_CRITICAL import android.stats.connectivity.NudEventType.NUD_CONFIRM_MAC_ADDRESS_CHANGED -import android.stats.connectivity.NudEventType.NUD_POST_ROAMING_FAILED -import android.stats.connectivity.NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL -import android.stats.connectivity.NudEventType.NUD_POST_ROAMING_MAC_ADDRESS_CHANGED import android.stats.connectivity.NudEventType.NUD_ORGANIC_FAILED import android.stats.connectivity.NudEventType.NUD_ORGANIC_FAILED_CRITICAL import android.stats.connectivity.NudEventType.NUD_ORGANIC_MAC_ADDRESS_CHANGED +import android.stats.connectivity.NudEventType.NUD_POST_ROAMING_FAILED +import android.stats.connectivity.NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL +import android.stats.connectivity.NudEventType.NUD_POST_ROAMING_MAC_ADDRESS_CHANGED import android.stats.connectivity.NudNeighborType import android.stats.connectivity.NudNeighborType.NUD_NEIGHBOR_BOTH import android.stats.connectivity.NudNeighborType.NUD_NEIGHBOR_DNS @@ -50,32 +49,16 @@ import android.system.OsConstants.EAGAIN import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 -import com.android.networkstack.metrics.IpReachabilityMonitorMetrics import com.android.net.module.util.InterfaceParams import com.android.net.module.util.SharedLog import com.android.net.module.util.ip.IpNeighborMonitor import com.android.net.module.util.netlink.StructNdMsg.NUD_FAILED import com.android.net.module.util.netlink.StructNdMsg.NUD_REACHABLE import com.android.net.module.util.netlink.StructNdMsg.NUD_STALE +import com.android.networkstack.metrics.IpReachabilityMonitorMetrics +import com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION import com.android.testutils.makeNewNeighMessage import com.android.testutils.waitForIdle -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.ArgumentMatchers.anyInt -import org.mockito.ArgumentMatchers.anyObject -import org.mockito.ArgumentMatchers.anyString -import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.doAnswer -import org.mockito.Mockito.doReturn -import org.mockito.Mockito.mock -import org.mockito.Mockito.never -import org.mockito.Mockito.timeout -import org.mockito.Mockito.verify import java.io.FileDescriptor import java.net.Inet4Address import java.net.Inet6Address @@ -86,6 +69,21 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue import kotlin.test.fail +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.timeout +import org.mockito.Mockito.verify private const val TEST_TIMEOUT_MS = 10_000L @@ -261,8 +259,8 @@ }.`when`(dependencies).makeIpNeighborMonitor(any(), any(), any()) doReturn(mIpReachabilityMonitorMetrics) .`when`(dependencies).getIpReachabilityMonitorMetrics() - doReturn(true).`when`(dependencies).isFeatureEnabled(anyObject(), - eq(IP_REACHABILITY_MCAST_RESOLICIT_VERSION), anyBoolean()) + doReturn(true).`when`(dependencies).isFeatureNotChickenedOut(any(), + eq(IP_REACHABILITY_MCAST_RESOLICIT_VERSION)) val monitorFuture = CompletableFuture<IpReachabilityMonitor>() // IpReachabilityMonitor needs to be started from the handler thread
diff --git a/tests/unit/src/com/android/networkstack/netlink/TcpInfoTest.java b/tests/unit/src/com/android/networkstack/netlink/TcpInfoTest.java index c362044..380f4ec 100644 --- a/tests/unit/src/com/android/networkstack/netlink/TcpInfoTest.java +++ b/tests/unit/src/com/android/networkstack/netlink/TcpInfoTest.java
@@ -75,7 +75,7 @@ "02000000" + // reordering = 3 "00000000" + // rcvrtt = 0 "30560100" + // rcvspace = 87600 - "00000000" + // totalRetrans = 0 + "05000000" + // totalRetrans = 5 "53AC000000000000" + // pacingRate = 44115 "FFFFFFFFFFFFFFFF" + // maxPacingRate = 18446744073709551615 "0100000000000001" + // bytesAcked = 1 @@ -93,7 +93,7 @@ private static final byte[] TCP_INFO_BYTES = HexEncoding.decode(TCP_INFO_HEX.toCharArray(), false); private static final TcpInfo TEST_TCPINFO = - new TcpInfo(0 /* retransmits */, 0 /* lost */, 2 /* segsOut */, 1 /* segsIn */); + new TcpInfo(2 /* segsOut */, 1 /* segsIn */, 5 /* totalRetrans */); private static final String EXPANDED_TCP_INFO_HEX = TCP_INFO_HEX + "00000000" // tcpi_delivered @@ -134,6 +134,7 @@ public void testFieldOffset() { assertEquals(TcpInfo.RETRANSMITS_OFFSET, 2); assertEquals(TcpInfo.LOST_OFFSET, 32); + assertEquals(TcpInfo.TOTAL_RETRANS_OFFSET, 100); assertEquals(TcpInfo.SEGS_OUT_OFFSET, 136); assertEquals(TcpInfo.SEGS_IN_OFFSET, 140); }
diff --git a/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java b/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java index 1d15719..165deab 100644 --- a/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java +++ b/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java
@@ -18,6 +18,7 @@ import static android.net.util.DataStallUtils.CONFIG_TCP_PACKETS_FAIL_PERCENTAGE; import static android.net.util.DataStallUtils.DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE; +import static android.os.PowerManager.ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED; import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; import static android.system.OsConstants.AF_INET; @@ -27,16 +28,13 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; -import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.content.BroadcastReceiver; @@ -58,9 +56,7 @@ import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.netlink.NetlinkUtils; import com.android.net.module.util.netlink.StructNlMsgHdr; -import com.android.networkstack.apishim.ConstantsShim; import com.android.testutils.DevSdkIgnoreRule; -import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import libcore.util.HexEncoding; @@ -95,12 +91,12 @@ private static final byte[] SOCK_DIAG_MSG_BYTES = HexEncoding.decode(DIAG_MSG_HEX.toCharArray(), false); // Hexadecimal representation of a SOCK_DIAG response with tcp info. - private static final String SOCK_DIAG_TCP_ZERO_LOST_HEX = - composeSockDiagTcpHex(0 /* lost */, 10 /* sent */); - private static final byte[] SOCK_DIAG_TCP_INET_ZERO_LOST_BYTES = - HexEncoding.decode(SOCK_DIAG_TCP_ZERO_LOST_HEX.toCharArray(), false); + private static final String SOCK_DIAG_TCP_TEST_HEX = + composeSockDiagTcpHex(5 /* retrans */, 10 /* sent */); + private static final byte[] SOCK_DIAG_TCP_INET_TEST_BYTES = + HexEncoding.decode(SOCK_DIAG_TCP_TEST_HEX.toCharArray(), false); private static final TcpInfo TEST_TCPINFO = - new TcpInfo(5 /* retransmits */, 0 /* lost */, 10 /* segsOut */, 0 /* segsIn */); + new TcpInfo(10 /* segsOut */, 0 /* segsIn */, 5 /* totalRetrans */); private static final String NLMSG_DONE_HEX = // struct nlmsghdr "14000000" // length = 20 @@ -113,7 +109,7 @@ + "06" // state + "00" // timer + "00"; // retrans - private static final String TEST_RESPONSE_HEX = SOCK_DIAG_TCP_ZERO_LOST_HEX + NLMSG_DONE_HEX; + private static final String TEST_RESPONSE_HEX = SOCK_DIAG_TCP_TEST_HEX + NLMSG_DONE_HEX; private static final byte[] TEST_RESPONSE_BYTES = HexEncoding.decode(TEST_RESPONSE_HEX.toCharArray(), false); private static final int TEST_NETID1 = 0xA85; @@ -145,12 +141,12 @@ mOldWtfHandler = Log.setWtfHandler((tag, what, system) -> Log.e(tag, what.getMessage(), what)); when(mDependencies.getNetd()).thenReturn(mNetd); - when(mDependencies.isTcpInfoParsingSupported()).thenReturn(true); when(mDependencies.connectToKernel()).thenReturn(new FileDescriptor()); when(mDependencies.getDeviceConfigPropertyInt( eq(NAMESPACE_CONNECTIVITY), eq(CONFIG_TCP_PACKETS_FAIL_PERCENTAGE), anyInt())).thenReturn(DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE); + when(mDependencies.shouldDisableInLightDoze()).thenReturn(true); when(mNetd.getFwmarkForNetwork(eq(TEST_NETID1))) .thenReturn(makeMarkMaskParcel(NETID_MASK, TEST_NETID1_FWMARK)); @@ -182,7 +178,7 @@ @Test public void testParseSockInfo() { - final ByteBuffer buffer = getByteBuffer(SOCK_DIAG_TCP_INET_ZERO_LOST_BYTES); + final ByteBuffer buffer = getByteBuffer(SOCK_DIAG_TCP_INET_TEST_BYTES); final ArrayList<TcpSocketTracker.SocketInfo> infoList = new ArrayList<>(); TcpSocketTracker.parseMessage(buffer, AF_INET, infoList, 100L); assertEquals(1, infoList.size()); @@ -211,15 +207,10 @@ assertFalse(NetlinkUtils.enoughBytesRemainForValidNlMsg(buffer)); } - @Test @IgnoreUpTo(Build.VERSION_CODES.Q) // TCP info parsing is not supported on Q + @Test public void testPollSocketsInfo() throws Exception { - // This test requires shims that provide API 30 access - assumeTrue(ConstantsShim.VERSION >= Build.VERSION_CODES.R); - when(mDependencies.isTcpInfoParsingSupported()).thenReturn(false); final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork); - assertFalse(tst.pollSocketsInfo()); - when(mDependencies.isTcpInfoParsingSupported()).thenReturn(true); // No enough bytes remain for a valid NlMsg. final ByteBuffer invalidBuffer = ByteBuffer.allocate(1); invalidBuffer.order(ByteOrder.nativeOrder()); @@ -256,11 +247,11 @@ final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork); // Simulate 1 message with data stall happened. doReturn(getByteBufferFromHexString( - composeSockDiagTcpHex(4, 10) + NLMSG_DONE_HEX)) + composeSockDiagTcpHex(9, 10) + NLMSG_DONE_HEX)) .when(mDependencies).recvMessage(any()); assertTrue(tst.pollSocketsInfo()); - // ( Lost 4 + default 5 retransmits in the sample ) / 10 sent = 90 percent. + // 9 retrans / 10 sent = 90 percent. assertEquals(90, tst.getLatestPacketFailPercentage()); assertEquals(10, tst.getSentSinceLastRecv()); assertTrue(tst.isDataStallSuspected()); @@ -271,8 +262,8 @@ final LinkProperties testLp = new LinkProperties(); testLp.addDnsServer(TEST_DNS1); tst.setLinkProperties(testLp); - doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(4, 10) - + composeSockDiagTcpHex(5, 10, DNS_OVER_TLS_PORT, TEST_COOKIE2) + doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(9, 10) + + composeSockDiagTcpHex(9, 10, DNS_OVER_TLS_PORT, TEST_COOKIE2) + NLMSG_DONE_HEX)) .when(mDependencies).recvMessage(any()); assertTrue(tst.pollSocketsInfo()); @@ -289,12 +280,12 @@ // will be counted. testLp.addValidatedPrivateDnsServer(TEST_DNS1); tst.setLinkProperties(testLp); - doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(5, 12) - + composeSockDiagTcpHex(7, 12, DNS_OVER_TLS_PORT, TEST_COOKIE2) + doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(10, 12) + + composeSockDiagTcpHex(11, 12, DNS_OVER_TLS_PORT, TEST_COOKIE2) + NLMSG_DONE_HEX)) .when(mDependencies).recvMessage(any()); assertTrue(tst.pollSocketsInfo()); - // Lost ( 1 + 2 ) / ( 2 + 2 ) sent = 75 percent. + // Retrans ( 1 + 2 ) / ( 2 + 2 ) sent = 75 percent. assertEquals(75, tst.getLatestPacketFailPercentage()); assertEquals(14, tst.getSentSinceLastRecv()); assertFalse(tst.isDataStallSuspected()); @@ -303,40 +294,18 @@ // counted. And the stat is correctly subtracted from the stat ignored in the previous // polling cycle. tst.setOpportunisticMode(false); - doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(6, 14) - + composeSockDiagTcpHex(9, 14, DNS_OVER_TLS_PORT, TEST_COOKIE2) + doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(11, 14) + + composeSockDiagTcpHex(13, 14, DNS_OVER_TLS_PORT, TEST_COOKIE2) + NLMSG_DONE_HEX)) .when(mDependencies).recvMessage(any()); assertTrue(tst.pollSocketsInfo()); - // Lost ( 1 + 2 ) / ( 2 + 2 ) sent = 75 percent. + // Retrans ( 1 + 2 ) / ( 2 + 2 ) sent = 75 percent. assertEquals(75, tst.getLatestPacketFailPercentage()); assertEquals(18, tst.getSentSinceLastRecv()); assertFalse(tst.isDataStallSuspected()); } @Test - public void testTcpInfoParsingUnsupported() { - doReturn(false).when(mDependencies).isTcpInfoParsingSupported(); - final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork); - verify(mDependencies).getNetd(); - - assertFalse(tst.pollSocketsInfo()); - assertEquals(-1, tst.getLatestPacketFailPercentage()); - assertEquals(-1, tst.getLatestReceivedCount()); - assertEquals(-1, tst.getSentSinceLastRecv()); - assertFalse(tst.isDataStallSuspected()); - - verify(mDependencies, atLeastOnce()).isTcpInfoParsingSupported(); - verifyNoMoreInteractions(mDependencies); - - // Verify that no un-registration for the device configuration listener and broadcast - // receiver if TcpInfo parsing is not supported. - tst.quit(); - verify(mDependencies, never()).removeDeviceConfigChangedListener(any()); - verify(mDependencies, never()).removeBroadcastReceiver(any()); - } - - @Test @IgnoreUpTo(Build.VERSION_CODES.Q) public void testTcpInfoParsingWithMultipleMsgs() throws Exception { final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork); @@ -345,20 +314,20 @@ // // Mocking 6 return results for different IP families(3 for IPv6; 3 for Ipv4). Use the same // message for different IP families to reduce the complexity. - doReturn(getByteBufferFromHexString(repeat(composeSockDiagTcpHex(0, 10), 5)), - getByteBufferFromHexString(repeat(composeSockDiagTcpHex(0, 10), 2)), + doReturn(getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 10), 5)), + getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 10), 2)), getByteBufferFromHexString( - repeat(composeSockDiagTcpHex(0, 10), 2) + NLMSG_DONE_HEX), - getByteBufferFromHexString(repeat(composeSockDiagTcpHex(0, 10), 5)), - getByteBufferFromHexString(repeat(composeSockDiagTcpHex(0, 10), 2)), + repeat(composeSockDiagTcpHex(5, 10), 2) + NLMSG_DONE_HEX), + getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 10), 5)), + getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 10), 2)), getByteBufferFromHexString( - repeat(composeSockDiagTcpHex(0, 10), 2) + NLMSG_DONE_HEX)) + repeat(composeSockDiagTcpHex(5, 10), 2) + NLMSG_DONE_HEX)) .when(mDependencies).recvMessage(any()); assertTrue(tst.pollSocketsInfo()); // Verify that code reads all the messages. (3 times for IPv4, 3 times for IPv6) verify(mDependencies, times(6)).recvMessage(any()); - // Calculated from (retransmits + lost) / segsout. + // Calculated from totalRetrans / segsout. // Note that the counters cannot be verified given that the cookie of the mocked sockets // are the same, the latest SocketInfo would overwrite previous reported ones. assertEquals(50, tst.getLatestPacketFailPercentage()); @@ -371,12 +340,12 @@ // // Mocking 6 return results for different IP families(3 for IPv6; 3 for Ipv4). Use the same // message for different IP families to reduce the complexity. - doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(5, 15)), - getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 15), 5)), - getByteBufferFromHexString(composeSockDiagTcpHex(5, 15) + NLMSG_DONE_HEX), - getByteBufferFromHexString(composeSockDiagTcpHex(5, 15)), - getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 15), 5)), - getByteBufferFromHexString(composeSockDiagTcpHex(5, 15) + NLMSG_DONE_HEX)) + doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(10, 15)), + getByteBufferFromHexString(repeat(composeSockDiagTcpHex(10, 15), 5)), + getByteBufferFromHexString(composeSockDiagTcpHex(10, 15) + NLMSG_DONE_HEX), + getByteBufferFromHexString(composeSockDiagTcpHex(10, 15)), + getByteBufferFromHexString(repeat(composeSockDiagTcpHex(10, 15), 5)), + getByteBufferFromHexString(composeSockDiagTcpHex(10, 15) + NLMSG_DONE_HEX)) .when(mDependencies).recvMessage(any()); assertTrue(tst.pollSocketsInfo()); @@ -393,12 +362,12 @@ // // Mocking 4 return results for different IP families(2 for IPv6; 2 for Ipv4). Use the same // message for different IP families to reduce the complexity. - doReturn(getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 15), 5)), - getByteBufferFromHexString(composeSockDiagTcpHex(5, 15)), - getByteBufferFromHexString(composeSockDiagTcpHex(5, 15) + NLMSG_DONE_HEX), - getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 15), 5)), - getByteBufferFromHexString(composeSockDiagTcpHex(5, 15)), - getByteBufferFromHexString(composeSockDiagTcpHex(5, 15) + NLMSG_DONE_HEX)) + doReturn(getByteBufferFromHexString(repeat(composeSockDiagTcpHex(10, 15), 5)), + getByteBufferFromHexString(composeSockDiagTcpHex(10, 15)), + getByteBufferFromHexString(composeSockDiagTcpHex(10, 15) + NLMSG_DONE_HEX), + getByteBufferFromHexString(repeat(composeSockDiagTcpHex(10, 15), 5)), + getByteBufferFromHexString(composeSockDiagTcpHex(10, 15)), + getByteBufferFromHexString(composeSockDiagTcpHex(10, 15) + NLMSG_DONE_HEX)) .when(mDependencies).recvMessage(any()); assertTrue(tst.pollSocketsInfo()); @@ -415,9 +384,9 @@ // Mocking 2 return results for different IP families(1 for IPv6; 1 for Ipv4). Use the same // message for different IP families to reduce the complexity. doReturn(getByteBufferFromHexString( - repeat(composeSockDiagTcpHex(9, 20), 8) + NLMSG_DONE_HEX), + repeat(composeSockDiagTcpHex(14, 20), 8) + NLMSG_DONE_HEX), getByteBufferFromHexString( - repeat(composeSockDiagTcpHex(9, 20), 8) + NLMSG_DONE_HEX)) + repeat(composeSockDiagTcpHex(14, 20), 8) + NLMSG_DONE_HEX)) .when(mDependencies).recvMessage(any()); assertTrue(tst.pollSocketsInfo()); @@ -435,9 +404,9 @@ // Mocking 2 return results for different IP families(1 for IPv6; 1 for Ipv4). Use the same // message for different IP families to reduce the complexity. doReturn(getByteBufferFromHexString( - NLMSG_DONE_HEX + repeat(composeSockDiagTcpHex(15, 26), 2)), + NLMSG_DONE_HEX + repeat(composeSockDiagTcpHex(20, 26), 2)), getByteBufferFromHexString( - NLMSG_DONE_HEX + repeat(composeSockDiagTcpHex(15, 26), 2))) + NLMSG_DONE_HEX + repeat(composeSockDiagTcpHex(20, 26), 2))) .when(mDependencies).recvMessage(any()); assertTrue(tst.pollSocketsInfo()); // Another 1 time for IPv6 and 1 time for IPv4 @@ -480,10 +449,17 @@ final ByteBuffer bb = ByteBuffer.allocate(size); bb.order(order); switch (size) { - case Short.BYTES -> bb.putShort((short) v); - case Integer.BYTES -> bb.putInt((int) v); - case Long.BYTES -> bb.putLong(v); - default -> throw new IllegalArgumentException("Unsupported size: " + size); + case Short.BYTES: + bb.putShort((short) v); + break; + case Integer.BYTES: + bb.putInt((int) v); + break; + case Long.BYTES: + bb.putLong(v); + break; + default: + throw new IllegalArgumentException("Unsupported size: " + size); } String s = ""; for (byte b : bb.array()) { @@ -492,100 +468,97 @@ return s; } - private static String composeSockDiagTcpHex(int lost, int sent) { - return composeSockDiagTcpHex(lost, sent, TEST_DST_PORT, TEST_COOKIE1); + private static String composeSockDiagTcpHex(int retrans, int sent) { + return composeSockDiagTcpHex(retrans, sent, TEST_DST_PORT, TEST_COOKIE1); } - private static String composeSockDiagTcpHex(int lost, int sent, short dstPort, long cookie) { + private static String composeSockDiagTcpHex(int retrans, int sent, short dstPort, long cookie) { return // struct nlmsghdr. - "14010000" + // length = 276 - "1400" + // type = SOCK_DIAG_BY_FAMILY - "0301" + // flags = NLM_F_REQUEST | NLM_F_DUMP - "00000000" + // seqno - "00000000" + // pid (0 == kernel) + "14010000" // length = 276 + + "1400" // type = SOCK_DIAG_BY_FAMILY + + "0301" // flags = NLM_F_REQUEST | NLM_F_DUMP + + "00000000" // seqno + + "00000000" // pid (0 == kernel) // struct inet_diag_req_v2 - "02" + // family = AF_INET - "06" + // state - "00" + // timer - "00" + // retrans + + "02" // family = AF_INET + + "06" // state + + "00" // timer + + "00" // retrans // inet_diag_sockid: ports and addresses are always in big endian, // see StructInetDiagSockId. - "DEA5" + // idiag_sport = 56997 - getHexStringFromShort(dstPort, ByteOrder.BIG_ENDIAN) + // idiag_dport - "0a006402000000000000000000000000" + // idiag_src = 10.0.100.2 - "08080808000000000000000000000000" + // idiag_dst = 8.8.8.8 - "00000000" + // idiag_if - getHexStringFromLong(cookie) + // idiag_cookie - "00000000" + // idiag_expires - "00000000" + // idiag_rqueue - "00000000" + // idiag_wqueue - getHexStringFromInt(TEST_UID1) + // idiag_uid - "00000000" + // idiag_inode + + "DEA5" // idiag_sport = 56997 + + getHexStringFromShort(dstPort, ByteOrder.BIG_ENDIAN) // idiag_dport + + "0a006402000000000000000000000000" // idiag_src = 10.0.100.2 + + "08080808000000000000000000000000" // idiag_dst = 8.8.8.8 + + "00000000" // idiag_if + + getHexStringFromLong(cookie) // idiag_cookie + + "00000000" // idiag_expires + + "00000000" // idiag_rqueue + + "00000000" // idiag_wqueue + + getHexStringFromInt(TEST_UID1) // idiag_uid + + "00000000" // idiag_inode // rtattr - "0500" + // len = 5 - "0800" + // type = 8 - "00000000" + // data - "0800" + // len = 8 - "0F00" + // type = 15(INET_DIAG_MARK) - "850A0C00" + // data, socket mark=789125 - "AC00" + // len = 172 - "0200" + // type = 2(INET_DIAG_INFO) + + "0500" // len = 5 + + "0800" // type = 8 + + "00000000" // data + + "0800" // len = 8 + + "0F00" // type = 15(INET_DIAG_MARK) + + "850A0C00" // data, socket mark=789125 + + "AC00" // len = 172 + + "0200" // type = 2(INET_DIAG_INFO) // tcp_info - "01" + // state = TCP_ESTABLISHED - "00" + // ca_state = TCP_CA_OPEN - "05" + // retransmits = 5 - "00" + // probes = 0 - "00" + // backoff = 0 - "07" + // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS - "88" + // wscale = 8 - "00" + // delivery_rate_app_limited = 0 - "4A911B00" + // rto = 1806666 - "00000000" + // ato = 0 - "2E050000" + // sndMss = 1326 - "18020000" + // rcvMss = 536 - "00000000" + // unsacked = 0 - "00000000" + // acked = 0 - getHexStringFromInt(lost) + // lost - "00000000" + // retrans = 0 - "00000000" + // fackets = 0 - "BB000000" + // lastDataSent = 187 - "00000000" + // lastAckSent = 0 - "BB000000" + // lastDataRecv = 187 - "BB000000" + // lastDataAckRecv = 187 - "DC050000" + // pmtu = 1500 - "30560100" + // rcvSsthresh = 87600 - "3E2C0900" + // rttt = 601150 - "1F960400" + // rttvar = 300575 - "78050000" + // sndSsthresh = 1400 - "0A000000" + // sndCwnd = 10 - "A8050000" + // advmss = 1448 - "03000000" + // reordering = 3 - "00000000" + // rcvrtt = 0 - "30560100" + // rcvspace = 87600 - "00000000" + // totalRetrans = 0 - "53AC000000000000" + // pacingRate = 44115 - "FFFFFFFFFFFFFFFF" + // maxPacingRate = 18446744073709551615 - "0100000000000000" + // bytesAcked = 1 - "0000000000000000" + // bytesReceived = 0 - getHexStringFromInt(sent) + // SegsOut - "00000000" + // SegsIn = 0 - "00000000" + // NotSentBytes = 0 - "3E2C0900" + // minRtt = 601150 - "00000000" + // DataSegsIn = 0 - "00000000" + // DataSegsOut = 0 - "0000000000000000"; // deliverRate = 0 + + "01" // state = TCP_ESTABLISHED + + "00" // ca_state = TCP_CA_OPEN + + "05" // retransmits = 5 + + "00" // probes = 0 + + "00" // backoff = 0 + + "07" // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS + + "88" // wscale = 8 + + "00" // delivery_rate_app_limited = 0 + + "4A911B00" // rto = 1806666 + + "00000000" // ato = 0 + + "2E050000" // sndMss = 1326 + + "18020000" // rcvMss = 536 + + "00000000" // unsacked = 0 + + "00000000" // acked = 0 + + "00000000" // lost + + "00000000" // retrans = 0 + + "00000000" // fackets = 0 + + "BB000000" // lastDataSent = 187 + + "00000000" // lastAckSent = 0 + + "BB000000" // lastDataRecv = 187 + + "BB000000" // lastDataAckRecv = 187 + + "DC050000" // pmtu = 1500 + + "30560100" // rcvSsthresh = 87600 + + "3E2C0900" // rttt = 601150 + + "1F960400" // rttvar = 300575 + + "78050000" // sndSsthresh = 1400 + + "0A000000" // sndCwnd = 10 + + "A8050000" // advmss = 1448 + + "03000000" // reordering = 3 + + "00000000" // rcvrtt = 0 + + "30560100" // rcvspace = 87600 + + getHexStringFromInt(retrans) // totalRetrans + + "53AC000000000000" // pacingRate = 44115 + + "FFFFFFFFFFFFFFFF" // maxPacingRate = 18446744073709551615 + + "0100000000000000" // bytesAcked = 1 + + "0000000000000000" // bytesReceived = 0 + + getHexStringFromInt(sent) // SegsOut + + "00000000" // SegsIn = 0 + + "00000000" // NotSentBytes = 0 + + "3E2C0900" // minRtt = 601150 + + "00000000" // DataSegsIn = 0 + + "00000000" // DataSegsOut = 0 + + "0000000000000000"; // deliverRate = 0 } - @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + @Test public void testTcpInfoParsingWithDozeMode() throws Exception { - // This test requires shims that provide API 30 access - assumeTrue(ConstantsShim.VERSION >= Build.VERSION_CODES.R); - final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork); final ArgumentCaptor<BroadcastReceiver> receiverCaptor = ArgumentCaptor.forClass(BroadcastReceiver.class); - verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture()); + verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean()); setupNormalTestTcpInfo(); assertTrue(tst.pollSocketsInfo()); @@ -593,69 +566,110 @@ when(mDependencies.getDeviceConfigPropertyInt(any(), eq(CONFIG_TCP_PACKETS_FAIL_PERCENTAGE), anyInt())).thenReturn(40); - // Trigger a config update + // Trigger a config update. tst.mConfigListener.onPropertiesChanged(null /* properties */); assertEquals(10, tst.getSentSinceLastRecv()); assertEquals(50, tst.getLatestPacketFailPercentage()); assertTrue(tst.isDataStallSuspected()); - // Enable doze mode + // Enable doze mode, verify counters are not updated. doReturn(true).when(mPowerManager).isDeviceIdleMode(); final BroadcastReceiver receiver = receiverCaptor.getValue(); receiver.onReceive(mContext, new Intent(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)); assertFalse(tst.pollSocketsInfo()); + assertEquals(10, tst.getSentSinceLastRecv()); + assertEquals(50, tst.getLatestPacketFailPercentage()); assertFalse(tst.isDataStallSuspected()); } + @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2) + public void testTcpInfoDisableParsingWithLightDozeMode_enabled() throws Exception { + final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork); + final ArgumentCaptor<BroadcastReceiver> receiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + + // Enable light doze mode with 1 netlink message. + verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean()); + final BroadcastReceiver receiver = receiverCaptor.getValue(); + doReturn(true).when(mPowerManager).isDeviceLightIdleMode(); + receiver.onReceive(mContext, new Intent(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED)); + doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(9, 10) + + NLMSG_DONE_HEX)).when(mDependencies).recvMessage(any()); + + // Verify counters are not updated. + assertFalse(tst.pollSocketsInfo()); + assertEquals(0, tst.getSentSinceLastRecv()); + // -1 if not enough packets. + assertEquals(-1, tst.getLatestPacketFailPercentage()); + assertFalse(tst.isDataStallSuspected()); + + // Disable light doze mode, verify polling are processed and counters are updated. + doReturn(false).when(mPowerManager).isDeviceLightIdleMode(); + receiver.onReceive(mContext, new Intent(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED)); + assertTrue(tst.pollSocketsInfo()); + assertEquals(10, tst.getSentSinceLastRecv()); + // Lost 4 + default 5 retrans / 10 sent. + assertEquals(90, tst.getLatestPacketFailPercentage()); + assertTrue(tst.isDataStallSuspected()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2) + public void testTcpInfoDisableParsingWithLightDozeMode_disabled() throws Exception { + when(mDependencies.shouldDisableInLightDoze()).thenReturn(false); + final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork); + final ArgumentCaptor<BroadcastReceiver> receiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + + // Enable light doze mode with 1 netlink message. + verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean()); + final BroadcastReceiver receiver = receiverCaptor.getValue(); + doReturn(true).when(mPowerManager).isDeviceLightIdleMode(); + receiver.onReceive(mContext, new Intent(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED)); + doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(9, 10) + + NLMSG_DONE_HEX)).when(mDependencies).recvMessage(any()); + + // Verify TcpInfo is still processed. + assertTrue(tst.pollSocketsInfo()); + assertEquals(10, tst.getSentSinceLastRecv()); + assertEquals(90, tst.getLatestPacketFailPercentage()); + assertTrue(tst.isDataStallSuspected()); + } + private void setupNormalTestTcpInfo() throws Exception { final ByteBuffer tcpBufferV6 = getByteBuffer(TEST_RESPONSE_BYTES); final ByteBuffer tcpBufferV4 = getByteBuffer(TEST_RESPONSE_BYTES); doReturn(tcpBufferV6, tcpBufferV4).when(mDependencies).recvMessage(any()); } - @Test @IgnoreAfter(Build.VERSION_CODES.Q) - public void testTcpInfoParsingNotSupportedOnQ() { - assertFalse(new TcpSocketTracker.Dependencies(mContext) - .isTcpInfoParsingSupported()); - } - - @Test @IgnoreUpTo(Build.VERSION_CODES.Q) - public void testTcpInfoParsingSupportedFromR() { - assertTrue(new TcpSocketTracker.Dependencies(mContext) - .isTcpInfoParsingSupported()); - } - private static final String BAD_DIAG_MSG_HEX = // struct nlmsghdr. - "00000058" + // length = 1476395008 - "1400" + // type = SOCK_DIAG_BY_FAMILY - "0301" + // flags = NLM_F_REQUEST | NLM_F_DUMP - "00000000" + // seqno - "00000000" + // pid (0 == kernel) + "00000058" // length = 1476395008 + + "1400" // type = SOCK_DIAG_BY_FAMILY + + "0301" // flags = NLM_F_REQUEST | NLM_F_DUMP + + "00000000" // seqno + + "00000000" // pid (0 == kernel) // struct inet_diag_req_v2 - "02" + // family = AF_INET - "06" + // state - "00" + // timer - "00" + // retrans + + "02" // family = AF_INET + + "06" // state + + "00" // timer + + "00" // retrans // inet_diag_sockid - "DEA5" + // idiag_sport = 42462 - "71B9" + // idiag_dport = 47473 - "0a006402000000000000000000000000" + // idiag_src = 10.0.100.2 - "08080808000000000000000000000000" + // idiag_dst = 8.8.8.8 - "00000000" + // idiag_if - "34ED000076270000" + // idiag_cookie = 43387759684916 - "00000000" + // idiag_expires - "00000000" + // idiag_rqueue - "00000000" + // idiag_wqueue - "00000000" + // idiag_uid - "00000000"; // idiag_inode + + "DEA5" // idiag_sport = 42462 + + "71B9" // idiag_dport = 47473 + + "0a006402000000000000000000000000" // idiag_src = 10.0.100.2 + + "08080808000000000000000000000000" // idiag_dst = 8.8.8.8 + + "00000000" // idiag_if + + "34ED000076270000" // idiag_cookie = 43387759684916 + + "00000000" // idiag_expires + + "00000000" // idiag_rqueue + + "00000000" // idiag_wqueue + + "00000000" // idiag_uid + + "00000000"; // idiag_inode private static final byte[] BAD_SOCK_DIAG_MSG_BYTES = HexEncoding.decode(BAD_DIAG_MSG_HEX.toCharArray(), false); - @Test @IgnoreUpTo(Build.VERSION_CODES.Q) // TCP info parsing is not supported on Q + @Test public void testPollSocketsInfo_BadFormat() throws Exception { - // This test requires shims that provide API 30 access - assumeTrue(ConstantsShim.VERSION >= Build.VERSION_CODES.R); final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork); setupNormalTestTcpInfo(); assertTrue(tst.pollSocketsInfo());
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java index cd948b3..3f2ff31 100644 --- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java +++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -553,6 +553,7 @@ }).when(mCleartextDnsNetwork).openConnection(any()); initHttpConnection(mHttpConnection); initHttpConnection(mHttpsConnection); + initHttpConnection(mFallbackConnection); mFakeDns = new FakeDns(); mFakeDns.startMocking(); @@ -603,10 +604,6 @@ 0, mCreatedNetworkMonitors.size()); assertEquals("BroadcastReceiver still registered after disconnect", 0, mRegisteredReceivers.size()); - if (mTstDependencies.isTcpInfoParsingSupported()) { - verify(mTstDependencies, times(networkMonitors.length)) - .removeDeviceConfigChangedListener(any()); - } } private void initHttpConnection(HttpURLConnection connection) { @@ -614,11 +611,13 @@ // Explicitly set the HttpURLConnection methods so that these will not interact with real // methods to prevent threading issue in the test. doReturn(new HashMap<>()).when(connection).getHeaderFields(); + doReturn(null).when(connection).getHeaderField(eq("location")); doNothing().when(connection).setInstanceFollowRedirects(anyBoolean()); doNothing().when(connection).setConnectTimeout(anyInt()); doNothing().when(connection).setReadTimeout(anyInt()); doNothing().when(connection).setRequestProperty(any(), any()); doNothing().when(connection).setUseCaches(anyBoolean()); + doNothing().when(connection).disconnect(); } private void initCallbacks(int interfaceVersion) throws Exception { @@ -707,7 +706,6 @@ setNetworkCapabilities(nm, nc); HandlerUtils.waitForIdle(nm.getHandler(), HANDLER_TIMEOUT_MS); mCreatedNetworkMonitors.add(nm); - doReturn(false).when(mTstDependencies).isTcpInfoParsingSupported(); return nm; } @@ -1584,7 +1582,7 @@ HandlerUtils.waitForIdle(nm.getHandler(), HANDLER_TIMEOUT_MS); } - @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + @Test public void testIsCaptivePortal_CapportApiNotSupported() throws Exception { // Test that on a R+ device, if NetworkStack was compiled without CaptivePortalData support // (built against Q), NetworkMonitor behaves as expected. @@ -1859,7 +1857,6 @@ public void testIsDataStall_SkipEvaluateOnValidationNotRequiredNetwork() { // Make DNS and TCP stall condition satisfied. setDataStallEvaluationType(DATA_STALL_EVALUATION_TYPE_DNS | DATA_STALL_EVALUATION_TYPE_TCP); - doReturn(true).when(mTstDependencies).isTcpInfoParsingSupported(); doReturn(0).when(mTst).getLatestReceivedCount(); doReturn(true).when(mTst).isDataStallSuspected(); final WrappedNetworkMonitor nm = makeMonitor(CELL_NO_INTERNET_CAPABILITIES); @@ -1894,7 +1891,6 @@ @Test public void testIsDataStall_EvaluationTcp() throws Exception { - doReturn(true).when(mTstDependencies).isTcpInfoParsingSupported(); // Evaluate TCP only. Expect ignoring DNS signal. setDataStallEvaluationType(DATA_STALL_EVALUATION_TYPE_TCP); WrappedNetworkMonitor wrappedMonitor = makeMonitor(CELL_METERED_CAPABILITIES); @@ -2296,7 +2292,6 @@ @Test public void testDataStall_setOpportunisticMode() { setDataStallEvaluationType(DATA_STALL_EVALUATION_TYPE_TCP); - doReturn(true).when(mTstDependencies).isTcpInfoParsingSupported(); WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor(); InOrder inOrder = inOrder(mTst); // Initialized with default value. @@ -2402,7 +2397,6 @@ } private void setupTcpDataStall() { - doReturn(true).when(mTstDependencies).isTcpInfoParsingSupported(); doReturn(0).when(mTst).getLatestReceivedCount(); doReturn(TEST_TCP_FAIL_RATE).when(mTst).getLatestPacketFailPercentage(); doReturn(TEST_TCP_PACKET_COUNT).when(mTst).getSentSinceLastRecv();