From 66ecf9f6d1bb536fa386b7d505ec79727226e30c Mon Sep 17 00:00:00 2001 From: Ryan Hamilton Date: Tue, 23 Aug 2022 12:46:00 -0700 Subject: [PATCH] Add a filter for applying Android socket tags (#2423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a filter for applying Android socket tags Testing: Functionality verified as part of the run of the experimentation example app. Also filed #2494 to track improvements. Docs Changes: Updated docs/root/api/starting_envoy.rst Release Notes: Added Signed-off-by: Ryan Hamilton Co-authored-by: RafaƂ Augustyniak --- bazel/android_artifacts.bzl | 2 +- docs/root/api/starting_envoy.rst | 11 ++ docs/root/intro/version_history.rst | 1 + envoy_build_config/BUILD | 1 + library/cc/engine_builder.cc | 11 ++ library/cc/engine_builder.h | 2 + library/common/config/config.cc | 6 + library/common/config/templates.h | 4 + .../extensions/filters/http/socket_tag/BUILD | 42 +++++ .../filters/http/socket_tag/config.cc | 33 ++++ .../filters/http/socket_tag/config.h | 32 ++++ .../filters/http/socket_tag/filter.cc | 44 +++++ .../filters/http/socket_tag/filter.h | 30 ++++ .../filters/http/socket_tag/filter.proto | 13 ++ library/common/jni/android_jni_utility.cc | 14 ++ library/common/jni/android_jni_utility.h | 6 + library/common/jni/jni_interface.cc | 6 + library/common/network/BUILD | 14 ++ .../network/socket_tag_socket_option_impl.cc | 58 ++++++ .../network/socket_tag_socket_option_impl.h | 39 ++++ .../io/envoyproxy/envoymobile/engine/BUILD | 1 + .../engine/EnvoyConfiguration.java | 23 ++- .../envoymobile/engine/EnvoyEngineImpl.java | 3 +- .../envoymobile/engine/JniLibrary.java | 5 + .../chromium/net/AndroidNetworkLibrary.java | 167 ++++++++++++++++++ .../java/org/chromium/net/ThreadStatsUid.java | 45 +++++ .../impl/NativeCronetEngineBuilderImpl.java | 3 +- .../envoyproxy/envoymobile/EngineBuilder.kt | 14 ++ .../envoymobile/RequestHeadersBuilder.kt | 22 +++ test/cc/unit/envoy_config_test.cc | 15 ++ .../AndroidEngineSocketTagTest.java | 166 +++++++++++++++++ test/java/integration/BUILD | 19 ++ .../engine/EnvoyConfigurationTest.kt | 22 ++- .../engine/testing/RequestScenario.java | 11 ++ test/kotlin/apps/experimental/MainActivity.kt | 2 + 35 files changed, 868 insertions(+), 19 deletions(-) create mode 100644 library/common/extensions/filters/http/socket_tag/BUILD create mode 100644 library/common/extensions/filters/http/socket_tag/config.cc create mode 100644 library/common/extensions/filters/http/socket_tag/config.h create mode 100644 library/common/extensions/filters/http/socket_tag/filter.cc create mode 100644 library/common/extensions/filters/http/socket_tag/filter.h create mode 100644 library/common/extensions/filters/http/socket_tag/filter.proto create mode 100644 library/common/network/socket_tag_socket_option_impl.cc create mode 100644 library/common/network/socket_tag_socket_option_impl.h create mode 100644 library/java/org/chromium/net/ThreadStatsUid.java create mode 100644 test/java/integration/AndroidEngineSocketTagTest.java diff --git a/bazel/android_artifacts.bzl b/bazel/android_artifacts.bzl index e3a7ad73c0..4b8d2e1a7c 100644 --- a/bazel/android_artifacts.bzl +++ b/bazel/android_artifacts.bzl @@ -238,7 +238,7 @@ def _create_classes_jar(name, manifest, android_library): classes_dir=$$(mktemp -d) echo "Creating classes.jar from $(SRCS)" pushd $$classes_dir - unzip $$original_directory/$(SRCS) "io/envoyproxy/*" "META-INF/" > /dev/null + unzip $$original_directory/$(SRCS) "io/envoyproxy/*" "org/chromium/net/*" "META-INF/" > /dev/null zip -r classes.jar * > /dev/null popd cp $$classes_dir/classes.jar $@ diff --git a/docs/root/api/starting_envoy.rst b/docs/root/api/starting_envoy.rst index a8a62c3e90..b1624d7fef 100644 --- a/docs/root/api/starting_envoy.rst +++ b/docs/root/api/starting_envoy.rst @@ -426,6 +426,17 @@ Specify whether to enable transparent response Brotli decompression. Defaults to // Swift builder.enableBrotli(true) +~~~~~~~~~~~~~~~~~~~~~~~ +``enableSocketTagging`` +~~~~~~~~~~~~~~~~~~~~~~~ + +Specify whether to enable support for Android socket tagging. Unavailable on iOS. Defaults to false. + +**Example**:: + + // Kotlin + builder.enableSocketTagging(true) + ~~~~~~~~~~~~~~~~~~~~~~~~~~ ``enableInterfaceBinding`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index ad459b26c0..5ba95369a8 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -37,6 +37,7 @@ Features: - api: add option to control whether Envoy should drain connections after a soft DNS refresh completes. (:issue:`#2225 <2225>`, :issue:`#2242 <2242>`) - api: add option to disable the gzip decompressor. (:issue: `#2321 <2321>`) (:issue: `#2349 <2349>`) - api: add option to enable the brotli decompressor. (:issue `#2342 <2342>`) (:issue: `#2349 <2349>`) +- api: add option to enable socket tagging. (:issue `#1512 <1521>`) - configuration: enable h2 ping by default. (:issue: `#2270 <2270>`) - android: enable the filtering of unroutable families by default. (:issues: `#2267 <2267>`) - instrumentation: add timers and warnings to platform-provided callbacks (:issue: `#2300 <2300>`) diff --git a/envoy_build_config/BUILD b/envoy_build_config/BUILD index c7d867fd60..d607016c78 100644 --- a/envoy_build_config/BUILD +++ b/envoy_build_config/BUILD @@ -36,6 +36,7 @@ envoy_cc_library( "@envoy_mobile//library/common/extensions/filters/http/network_configuration:config", "@envoy_mobile//library/common/extensions/filters/http/platform_bridge:config", "@envoy_mobile//library/common/extensions/filters/http/route_cache_reset:config", + "@envoy_mobile//library/common/extensions/filters/http/socket_tag:config", "@envoy_mobile//library/common/extensions/retry/options/network_configuration:config", ], ) diff --git a/library/cc/engine_builder.cc b/library/cc/engine_builder.cc index 7c6667a30e..37ba6dd81a 100644 --- a/library/cc/engine_builder.cc +++ b/library/cc/engine_builder.cc @@ -119,6 +119,11 @@ EngineBuilder& EngineBuilder::enableBrotli(bool brotli_on) { return *this; } +EngineBuilder& EngineBuilder::enableSocketTagging(bool socket_tagging_on) { + this->socket_tagging_filter_ = socket_tagging_on; + return *this; +} + std::string EngineBuilder::generateConfigStr() { #if defined(__APPLE__) std::string dns_resolver_name = "envoy.network.dns_resolver.apple"; @@ -184,6 +189,12 @@ std::string EngineBuilder::generateConfigStr() { &config_template_); } + if (this->socket_tagging_filter_) { + absl::StrReplaceAll( + {{"#{custom_filters}", absl::StrCat("#{custom_filters}\n", socket_tag_config_insert)}}, + &config_template_); + } + config_builder << config_template_; auto config_str = config_builder.str(); diff --git a/library/cc/engine_builder.h b/library/cc/engine_builder.h index 7921bd86ee..f42a9847a5 100644 --- a/library/cc/engine_builder.h +++ b/library/cc/engine_builder.h @@ -40,6 +40,7 @@ class EngineBuilder { EngineBuilder& setStreamIdleTimeoutSeconds(int stream_idle_timeout_seconds); EngineBuilder& enableGzip(bool gzip_on); EngineBuilder& enableBrotli(bool brotli_on); + EngineBuilder& enableSocketTagging(bool socket_tagging_on); // this is separated from build() for the sake of testability std::string generateConfigStr(); @@ -81,6 +82,7 @@ class EngineBuilder { int per_try_idle_timeout_seconds_ = 15; bool gzip_filter_ = true; bool brotli_filter_ = false; + bool socket_tagging_filter_ = false; absl::flat_hash_map key_value_stores_{}; diff --git a/library/common/config/config.cc b/library/common/config/config.cc index de6a619eb4..ea61a66ba9 100644 --- a/library/common/config/config.cc +++ b/library/common/config/config.cc @@ -75,6 +75,12 @@ const char* brotli_config_insert = R"( ignore_no_transform_header: true )"; +const char* socket_tag_config_insert = R"( + - name: envoy.filters.http.socket_tag + typed_config: + "@type": type.googleapis.com/envoymobile.extensions.filters.http.socket_tag.SocketTag +)"; + // clang-format off const std::string config_header = R"( !ignore default_defs: diff --git a/library/common/config/templates.h b/library/common/config/templates.h index eee45d6e3f..2e47813c51 100644 --- a/library/common/config/templates.h +++ b/library/common/config/templates.h @@ -73,6 +73,10 @@ extern const char* gzip_config_insert; */ extern const char* brotli_config_insert; +/* Insert that enables a socket tagging filter. + */ +extern const char* socket_tag_config_insert; + /** * Insert that enables the route cache reset filter in the filter chain. * Should only be added when the route cache should be cleared on every request diff --git a/library/common/extensions/filters/http/socket_tag/BUILD b/library/common/extensions/filters/http/socket_tag/BUILD new file mode 100644 index 0000000000..60045880c2 --- /dev/null +++ b/library/common/extensions/filters/http/socket_tag/BUILD @@ -0,0 +1,42 @@ +load( + "@envoy//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_extension_package", + "envoy_proto_library", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_proto_library( + name = "filter", + srcs = ["filter.proto"], +) + +envoy_cc_extension( + name = "socket_tag_filter_lib", + srcs = ["filter.cc"], + hdrs = ["filter.h"], + repository = "@envoy", + deps = [ + ":filter_cc_proto", + "//library/common/http:internal_headers_lib", + "//library/common/network:socket_tag_socket_option_lib", + "//library/common/types:c_types_lib", + "@envoy//envoy/http:codes_interface", + "@envoy//envoy/http:filter_interface", + "@envoy//source/extensions/filters/http/common:pass_through_filter_lib", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + repository = "@envoy", + deps = [ + ":socket_tag_filter_lib", + "@envoy//source/extensions/filters/http/common:factory_base_lib", + ], +) diff --git a/library/common/extensions/filters/http/socket_tag/config.cc b/library/common/extensions/filters/http/socket_tag/config.cc new file mode 100644 index 0000000000..d0a3de2f69 --- /dev/null +++ b/library/common/extensions/filters/http/socket_tag/config.cc @@ -0,0 +1,33 @@ +#include "library/common/extensions/filters/http/socket_tag/config.h" + +#include +#include +#include + +#include "envoy/network/listen_socket.h" + +#include "library/common/extensions/filters/http/socket_tag/filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace SocketTag { + +Http::FilterFactoryCb SocketTagFilterFactory::createFilterFactoryFromProtoTyped( + const envoymobile::extensions::filters::http::socket_tag::SocketTag& /*proto_config*/, + const std::string&, Server::Configuration::FactoryContext& /*context*/) { + + return [](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamFilter(std::make_shared()); + }; +} + +/** + * Static registration for the SocketTag filter. @see NamedHttpFilterConfigFactory. + */ +REGISTER_FACTORY(SocketTagFilterFactory, Server::Configuration::NamedHttpFilterConfigFactory); + +} // namespace SocketTag +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/library/common/extensions/filters/http/socket_tag/config.h b/library/common/extensions/filters/http/socket_tag/config.h new file mode 100644 index 0000000000..ae69bd6722 --- /dev/null +++ b/library/common/extensions/filters/http/socket_tag/config.h @@ -0,0 +1,32 @@ +#include + +#include "source/extensions/filters/http/common/factory_base.h" + +#include "library/common/extensions/filters/http/socket_tag/filter.pb.h" +#include "library/common/extensions/filters/http/socket_tag/filter.pb.validate.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace SocketTag { + +/** + * Config registration for the socket tag filter. @see NamedHttpFilterConfigFactory. + */ +class SocketTagFilterFactory + : public Common::FactoryBase { +public: + SocketTagFilterFactory() : FactoryBase("socket_tag") {} + +private: + ::Envoy::Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoymobile::extensions::filters::http::socket_tag::SocketTag& config, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override; +}; + +DECLARE_FACTORY(SocketTagFilterFactory); + +} // namespace SocketTag +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/library/common/extensions/filters/http/socket_tag/filter.cc b/library/common/extensions/filters/http/socket_tag/filter.cc new file mode 100644 index 0000000000..abe4006cc1 --- /dev/null +++ b/library/common/extensions/filters/http/socket_tag/filter.cc @@ -0,0 +1,44 @@ +#include "library/common/extensions/filters/http/socket_tag/filter.h" + +#include "envoy/server/filter_config.h" + +#include "library/common/network/socket_tag_socket_option_impl.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace SocketTag { + +Http::FilterHeadersStatus SocketTagFilter::decodeHeaders(Http::RequestHeaderMap& request_headers, + bool) { + static auto socket_tag_header = Http::LowerCaseString("x-envoy-mobile-socket-tag"); + Http::RequestHeaderMap::GetResult header = request_headers.get(socket_tag_header); + if (header.empty()) { + return Http::FilterHeadersStatus::Continue; + } + + // The x-envoy-mobile-socket-tag header must contain a pair of number separated by a comma, e.g.: + // x-envoy-mobile-socket-tag: 123,456 + // The first number contains the UID and the second contains the traffic stats tag. + std::string tag_string(header[0]->value().getStringView()); + std::pair data = absl::StrSplit(tag_string, ','); + uid_t uid; + uint32_t traffic_stats_tag; + if (!absl::SimpleAtoi(data.first, &uid) || !absl::SimpleAtoi(data.second, &traffic_stats_tag)) { + decoder_callbacks_->sendLocalReply( + Http::Code::BadRequest, + absl::StrCat("Invalid x-envoy-mobile-socket-tag header: ", tag_string), nullptr, + absl::nullopt, ""); + return Http::FilterHeadersStatus::StopIteration; + } + auto options = std::make_shared(); + options->push_back(std::make_shared(uid, traffic_stats_tag)); + decoder_callbacks_->addUpstreamSocketOptions(options); + request_headers.remove(socket_tag_header); + return Http::FilterHeadersStatus::Continue; +} + +} // namespace SocketTag +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/library/common/extensions/filters/http/socket_tag/filter.h b/library/common/extensions/filters/http/socket_tag/filter.h new file mode 100644 index 0000000000..c3a080d4f1 --- /dev/null +++ b/library/common/extensions/filters/http/socket_tag/filter.h @@ -0,0 +1,30 @@ +#pragma once + +#include "envoy/http/filter.h" + +#include "source/common/common/logger.h" +#include "source/extensions/filters/http/common/pass_through_filter.h" + +#include "library/common/extensions/filters/http/socket_tag/filter.pb.h" +#include "library/common/types/c_types.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace SocketTag { + +/** + * Filter to set upstream socket tags based on a request header. + * See: https://source.android.com/devices/tech/datausage/tags-explained + */ +class SocketTagFilter final : public Http::PassThroughFilter, + public Logger::Loggable { +public: + // Http::PassThroughDecoderFilter + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& request_headers, bool) override; +}; + +} // namespace SocketTag +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/library/common/extensions/filters/http/socket_tag/filter.proto b/library/common/extensions/filters/http/socket_tag/filter.proto new file mode 100644 index 0000000000..a5818bd6b1 --- /dev/null +++ b/library/common/extensions/filters/http/socket_tag/filter.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package envoymobile.extensions.filters.http.socket_tag; + +// Configuration for the socket tagging filer. This filter uses data from the +// x-envoy-mobile-socket-tag request header to apply an Android Socket Tag to the upstream +// socket. +// See: https://source.android.com/devices/tech/datausage/tags-explained +// See: https://developer.android.com/reference/android/net/TrafficStats#setThreadStatsTag(int) +// See: https://developer.android.com/reference/android/net/TrafficStats#setThreadStatsUid(int) +// See: https://developer.android.com/reference/android/net/TrafficStats#tagSocket(java.net.Socket) +message SocketTag { +} diff --git a/library/common/jni/android_jni_utility.cc b/library/common/jni/android_jni_utility.cc index ba66ba20df..5f146bbf94 100644 --- a/library/common/jni/android_jni_utility.cc +++ b/library/common/jni/android_jni_utility.cc @@ -33,3 +33,17 @@ bool is_cleartext_permitted(absl::string_view hostname) { return true; #endif } + +void tag_socket(int ifd, int uid, int tag) { +#if defined(__ANDROID_API__) + JNIEnv* env = get_env(); + jclass jcls_AndroidNetworkLibrary = find_class("org/chromium/net/AndroidNetworkLibrary"); + jmethodID jmid_tagSocket = + env->GetStaticMethodID(jcls_AndroidNetworkLibrary, "tagSocket", "(III)V"); + env->CallStaticVoidMethod(jcls_AndroidNetworkLibrary, jmid_tagSocket, ifd, uid, tag); +#else + UNREFERENCED_PARAMETER(ifd); + UNREFERENCED_PARAMETER(uid); + UNREFERENCED_PARAMETER(tag); +#endif +} diff --git a/library/common/jni/android_jni_utility.h b/library/common/jni/android_jni_utility.h index e0a3feba3c..271ba790c3 100644 --- a/library/common/jni/android_jni_utility.h +++ b/library/common/jni/android_jni_utility.h @@ -9,3 +9,9 @@ * For other platforms simply returns true. */ bool is_cleartext_permitted(absl::string_view hostname); + +/* For android, calls up through JNI to apply + * host. + * For other platforms simply returns true. + */ +void tag_socket(int ifd, int uid, int tag); diff --git a/library/common/jni/jni_interface.cc b/library/common/jni/jni_interface.cc index 56392a9e9a..0d64e14d70 100644 --- a/library/common/jni/jni_interface.cc +++ b/library/common/jni/jni_interface.cc @@ -169,6 +169,12 @@ Java_io_envoyproxy_envoymobile_engine_JniLibrary_brotliConfigInsert(JNIEnv* env, return result; } +extern "C" JNIEXPORT jstring JNICALL +Java_io_envoyproxy_envoymobile_engine_JniLibrary_socketTagConfigInsert(JNIEnv* env, jclass) { + jstring result = env->NewStringUTF(socket_tag_config_insert); + return result; +} + extern "C" JNIEXPORT jint JNICALL Java_io_envoyproxy_envoymobile_engine_JniLibrary_recordCounterInc( JNIEnv* env, jclass, // class diff --git a/library/common/network/BUILD b/library/common/network/BUILD index 66f9a76d50..f8e738c9a4 100644 --- a/library/common/network/BUILD +++ b/library/common/network/BUILD @@ -52,6 +52,20 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "socket_tag_socket_option_lib", + srcs = ["socket_tag_socket_option_impl.cc"], + hdrs = ["socket_tag_socket_option_impl.h"], + repository = "@envoy", + deps = [ + "//library/common/jni:android_jni_utility_lib", + "@envoy//envoy/network:address_interface", + "@envoy//source/common/common:scalar_to_byte_vector_lib", + "@envoy//source/common/network:socket_option_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) + envoy_cc_library( name = "synthetic_address_lib", hdrs = ["synthetic_address_impl.h"], diff --git a/library/common/network/socket_tag_socket_option_impl.cc b/library/common/network/socket_tag_socket_option_impl.cc new file mode 100644 index 0000000000..9e0942c5e3 --- /dev/null +++ b/library/common/network/socket_tag_socket_option_impl.cc @@ -0,0 +1,58 @@ +#include "library/common/network/socket_tag_socket_option_impl.h" + +#include "envoy/config/core/v3/base.pb.h" + +#include "source/common/common/assert.h" +#include "source/common/common/scalar_to_byte_vector.h" + +#include "library/common/jni/android_jni_utility.h" + +namespace Envoy { +namespace Network { + +SocketTagSocketOptionImpl::SocketTagSocketOptionImpl(uid_t uid, uint32_t traffic_stats_tag) + : optname_(0, 0, "socket_tag"), uid_(uid), traffic_stats_tag_(traffic_stats_tag) {} + +bool SocketTagSocketOptionImpl::setOption( + Socket& socket, envoy::config::core::v3::SocketOption::SocketState state) const { + if (state != envoy::config::core::v3::SocketOption::STATE_PREBIND) { + return true; + } + + // Because socket tagging happens at the socket level, not at the request level, + // requests with different socket tags must not use the same socket. As a result + // different socket tag socket options must end up in different socket pools. + // This happens because different socket tag socket option generate different + // hash keys. + // Further, this only works for sockets which have a raw fd and will be a no-op + // otherwise. + int fd = socket.ioHandle().fdDoNotUse(); + tag_socket(fd, uid_, traffic_stats_tag_); + return true; +} + +void SocketTagSocketOptionImpl::hashKey(std::vector& hash_key) const { + pushScalarToByteVector(uid_, hash_key); + pushScalarToByteVector(traffic_stats_tag_, hash_key); +} + +absl::optional SocketTagSocketOptionImpl::getOptionDetails( + const Socket&, envoy::config::core::v3::SocketOption::SocketState /*state*/) const { + if (!isSupported()) { + return absl::nullopt; + } + + static std::string name = "socket_tag"; + Socket::Option::Details details; + details.name_ = optname_; + std::vector data; + pushScalarToByteVector(uid_, data); + pushScalarToByteVector(traffic_stats_tag_, data); + details.value_ = std::string(reinterpret_cast(data.data()), data.size()); + return absl::make_optional(std::move(details)); +} + +bool SocketTagSocketOptionImpl::isSupported() const { return optname_.hasValue(); } + +} // namespace Network +} // namespace Envoy diff --git a/library/common/network/socket_tag_socket_option_impl.h b/library/common/network/socket_tag_socket_option_impl.h new file mode 100644 index 0000000000..a118c890b3 --- /dev/null +++ b/library/common/network/socket_tag_socket_option_impl.h @@ -0,0 +1,39 @@ +#pragma once + +#include "envoy/config/core/v3/base.pb.h" +#include "envoy/network/address.h" +#include "envoy/network/listen_socket.h" + +namespace Envoy { +namespace Network { + +/** + * This is a "synthetic" socket option implementation, which sets the android socket tag + * during bind. + */ +class SocketTagSocketOptionImpl : public Network::Socket::Option { +public: + SocketTagSocketOptionImpl(uid_t uid, uint32_t traffic_stats_tag); + + // Socket::Option + bool setOption(Network::Socket& socket, + envoy::config::core::v3::SocketOption::SocketState state) const override; + void hashKey(std::vector& hash_key) const override; + absl::optional
+ getOptionDetails(const Network::Socket& socket, + envoy::config::core::v3::SocketOption::SocketState state) const override; + bool isSupported() const override; + +private: + const Network::SocketOptionName optname_; + + // Thread stats UID to be applied to the socket. + // See: https://developer.android.com/reference/android/net/TrafficStats#setThreadStatsUid(int) + uid_t uid_; + // Thread stats tag to be applied to the socket. + // See: https://developer.android.com/reference/android/net/TrafficStats#setThreadStatsTag(int) + uint32_t traffic_stats_tag_; +}; + +} // namespace Network +} // namespace Envoy diff --git a/library/java/io/envoyproxy/envoymobile/engine/BUILD b/library/java/io/envoyproxy/envoymobile/engine/BUILD index b7ec90603b..be4f2a8380 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/BUILD +++ b/library/java/io/envoyproxy/envoymobile/engine/BUILD @@ -16,6 +16,7 @@ android_library( deps = [ "//library/java/io/envoyproxy/envoymobile/engine:envoy_base_engine_lib", "//library/java/io/envoyproxy/envoymobile/engine/types:envoy_c_types_lib", + "//library/java/org/chromium/net", "@maven//:androidx_core_core", ], ) diff --git a/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java b/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java index b8ec294b08..92f8e03487 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java +++ b/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java @@ -41,6 +41,7 @@ public enum TrustChainVerification { public final Boolean enableHttp3; public final Boolean enableGzip; public final Boolean enableBrotli; + public final Boolean enableSocketTagging; public final Boolean enableHappyEyeballs; public final Boolean enableInterfaceBinding; public final Boolean forceIPv6; @@ -84,6 +85,7 @@ public enum TrustChainVerification { * @param enableHttp3 whether to enable experimental support for HTTP/3 (QUIC). * @param enableGzip whether to enable response gzip decompression. * @param enableBrotli whether to enable response brotli decompression. + * @param enableSocketTagging whether to enable socket tagging. * @param enableHappyEyeballs whether to enable RFC 6555 handling for IPv4/IPv6. * @param enableInterfaceBinding whether to allow interface binding. * @param forceIPv6 whether to force connections to use IPv6. @@ -113,13 +115,13 @@ public EnvoyConfiguration( String dnsPreresolveHostnames, List dnsFallbackNameservers, boolean dnsFilterUnroutableFamilies, boolean dnsUseSystemResolver, boolean enableDrainPostDnsRefresh, boolean enableHttp3, boolean enableGzip, - boolean enableBrotli, boolean enableHappyEyeballs, boolean enableInterfaceBinding, - boolean forceIPv6, int h2ConnectionKeepaliveIdleIntervalMilliseconds, - int h2ConnectionKeepaliveTimeoutSeconds, boolean h2ExtendKeepaliveTimeout, - List h2RawDomains, int maxConnectionsPerHost, int statsFlushSeconds, - int streamIdleTimeoutSeconds, int perTryIdleTimeoutSeconds, String appVersion, String appId, - TrustChainVerification trustChainVerification, String virtualClusters, - List nativeFilterChain, + boolean enableBrotli, boolean enableSocketTagging, boolean enableHappyEyeballs, + boolean enableInterfaceBinding, boolean forceIPv6, + int h2ConnectionKeepaliveIdleIntervalMilliseconds, int h2ConnectionKeepaliveTimeoutSeconds, + boolean h2ExtendKeepaliveTimeout, List h2RawDomains, int maxConnectionsPerHost, + int statsFlushSeconds, int streamIdleTimeoutSeconds, int perTryIdleTimeoutSeconds, + String appVersion, String appId, TrustChainVerification trustChainVerification, + String virtualClusters, List nativeFilterChain, List httpPlatformFilterFactories, Map stringAccessors, Map keyValueStores) { @@ -140,6 +142,7 @@ public EnvoyConfiguration( this.enableHttp3 = enableHttp3; this.enableGzip = enableGzip; this.enableBrotli = enableBrotli; + this.enableSocketTagging = enableSocketTagging; this.enableHappyEyeballs = enableHappyEyeballs; this.enableInterfaceBinding = enableInterfaceBinding; this.forceIPv6 = forceIPv6; @@ -177,7 +180,7 @@ public EnvoyConfiguration( String resolveTemplate(final String configTemplate, final String platformFilterTemplate, final String nativeFilterTemplate, final String altProtocolCacheFilterInsert, final String gzipFilterInsert, - final String brotliFilterInsert) { + final String brotliFilterInsert, final String socketTagFilterInsert) { final StringBuilder customFiltersBuilder = new StringBuilder(); for (EnvoyHTTPFilterFactory filterFactory : httpPlatformFilterFactories) { @@ -204,6 +207,10 @@ String resolveTemplate(final String configTemplate, final String platformFilterT customFiltersBuilder.append(brotliFilterInsert); } + if (enableSocketTagging) { + customFiltersBuilder.append(socketTagFilterInsert); + } + String processedTemplate = configTemplate.replace("#{custom_filters}", customFiltersBuilder.toString()); diff --git a/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java b/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java index 268f8b7a0f..63c4ef9122 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java +++ b/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java @@ -98,7 +98,8 @@ public int runWithTemplate(String configurationYAML, EnvoyConfiguration envoyCon configurationYAML, JniLibrary.platformFilterTemplate(), JniLibrary.nativeFilterTemplate(), JniLibrary.altProtocolCacheFilterInsert(), - JniLibrary.gzipConfigInsert(), JniLibrary.brotliConfigInsert()), + JniLibrary.gzipConfigInsert(), JniLibrary.brotliConfigInsert(), + JniLibrary.socketTagConfigInsert()), logLevel); } diff --git a/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java b/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java index 0d25851212..b4412f95ad 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java +++ b/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java @@ -315,6 +315,11 @@ protected static native int recordHistogramValue(long engine, String elements, b */ public static native String brotliConfigInsert(); + /** + * Provides a configuration insert that may be used to enable socket tagging. + */ + public static native String socketTagConfigInsert(); + /** * Register a platform-provided key-value store implementation. * diff --git a/library/java/org/chromium/net/AndroidNetworkLibrary.java b/library/java/org/chromium/net/AndroidNetworkLibrary.java index d299f9a8ac..338d97ac1e 100644 --- a/library/java/org/chromium/net/AndroidNetworkLibrary.java +++ b/library/java/org/chromium/net/AndroidNetworkLibrary.java @@ -1,5 +1,25 @@ package org.chromium.net; +import android.net.TrafficStats; +import android.os.ParcelFileDescriptor; +import android.os.Build; +import android.os.Build.VERSION_CODES; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.SocketImpl; +import java.net.URLConnection; +import java.net.Socket; + import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; @@ -83,4 +103,151 @@ public static void clearTestRootCertificates() X509Util.clearTestRootCertificates(); } } + + /** + * Class to wrap FileDescriptor.setInt$() which is hidden and so must be accessed via + * reflection. + */ + private static class SetFileDescriptor { + // Reference to FileDescriptor.setInt$(int fd). + private static final Method sFileDescriptorSetInt; + + // Get reference to FileDescriptor.setInt$(int fd) via reflection. + static { + try { + sFileDescriptorSetInt = FileDescriptor.class.getMethod("setInt$", Integer.TYPE); + } catch (NoSuchMethodException | SecurityException e) { + throw new RuntimeException("Unable to get FileDescriptor.setInt$", e); + } + } + + /** Creates a FileDescriptor and calls FileDescriptor.setInt$(int fd) on it. */ + public static FileDescriptor createWithFd(int fd) { + try { + FileDescriptor fileDescriptor = new FileDescriptor(); + sFileDescriptorSetInt.invoke(fileDescriptor, fd); + return fileDescriptor; + } catch (IllegalAccessException e) { + throw new RuntimeException("FileDescriptor.setInt$() failed", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("FileDescriptor.setInt$() failed", e); + } + } + } + + /** + * This class provides an implementation of {@link java.net.Socket} that serves only as a + * conduit to pass a file descriptor integer to {@link android.net.TrafficStats#tagSocket} + * when called by {@link #tagSocket}. This class does not take ownership of the file descriptor, + * so calling {@link #close} will not actually close the file descriptor. + */ + private static class SocketFd extends Socket { + /** + * This class provides an implementation of {@link java.net.SocketImpl} that serves only as + * a conduit to pass a file descriptor integer to {@link android.net.TrafficStats#tagSocket} + * when called by {@link #tagSocket}. This class does not take ownership of the file + * descriptor, so calling {@link #close} will not actually close the file descriptor. + */ + private static class SocketImplFd extends SocketImpl { + /** + * Create a {@link java.net.SocketImpl} that sets {@code fd} as the underlying file + * descriptor. Does not take ownership of the file descriptor, so calling {@link #close} + * will not actually close the file descriptor. + */ + SocketImplFd(FileDescriptor fd) { this.fd = fd; } + + protected void accept(SocketImpl s) { throw new RuntimeException("accept not implemented"); } + protected int available() { throw new RuntimeException("accept not implemented"); } + protected void bind(InetAddress host, int port) { + throw new RuntimeException("accept not implemented"); + } + protected void close() {} + protected void connect(InetAddress address, int port) { + throw new RuntimeException("connect not implemented"); + } + protected void connect(SocketAddress address, int timeout) { + throw new RuntimeException("connect not implemented"); + } + protected void connect(String host, int port) { + throw new RuntimeException("connect not implemented"); + } + protected void create(boolean stream) {} + protected InputStream getInputStream() { + throw new RuntimeException("getInputStream not implemented"); + } + protected OutputStream getOutputStream() { + throw new RuntimeException("getOutputStream not implemented"); + } + protected void listen(int backlog) { throw new RuntimeException("listen not implemented"); } + protected void sendUrgentData(int data) { + throw new RuntimeException("sendUrgentData not implemented"); + } + public Object getOption(int optID) { + throw new RuntimeException("getOption not implemented"); + } + public void setOption(int optID, Object value) { + throw new RuntimeException("setOption not implemented"); + } + } + + /** + * Create a {@link java.net.Socket} that sets {@code fd} as the underlying file + * descriptor. Does not take ownership of the file descriptor, so calling {@link #close} + * will not actually close the file descriptor. + */ + SocketFd(FileDescriptor fd) throws IOException { super(new SocketImplFd(fd)); } + } + + /** + * Tag socket referenced by {@code ifd} with {@code tag} for UID {@code uid}. + * + * Assumes thread UID tag isn't set upon entry, and ensures thread UID tag isn't set upon exit. + * Unfortunately there is no TrafficStatis.getThreadStatsUid(). + */ + private static void tagSocket(int ifd, int uid, int tag) throws IOException { + // Set thread tags. + int oldTag = TrafficStats.getThreadStatsTag(); + if (tag != oldTag) { + TrafficStats.setThreadStatsTag(tag); + } + if (uid != -1) { + ThreadStatsUid.set(uid); + } + + // Apply thread tags to socket. + + // First, convert integer file descriptor (ifd) to FileDescriptor. + final ParcelFileDescriptor pfd; + final FileDescriptor fd; + // The only supported way to generate a FileDescriptor from an integer file + // descriptor is via ParcelFileDescriptor.adoptFd(). Unfortunately prior to Android + // Marshmallow ParcelFileDescriptor.detachFd() didn't actually detach from the + // FileDescriptor, so use reflection to set {@code fd} into the FileDescriptor for + // versions prior to Marshmallow. Here's the fix that went into Marshmallow: + // https://android.googlesource.com/platform/frameworks/base/+/b30ad6f + if (Build.VERSION.SDK_INT < VERSION_CODES.M) { + pfd = null; + fd = SetFileDescriptor.createWithFd(ifd); + } else { + pfd = ParcelFileDescriptor.adoptFd(ifd); + fd = pfd.getFileDescriptor(); + } + // Second, convert FileDescriptor to Socket. + Socket s = new SocketFd(fd); + // Third, tag the Socket. + TrafficStats.tagSocket(s); + s.close(); // No-op but always good to close() Closeables. + // Have ParcelFileDescriptor relinquish ownership of the file descriptor. + if (pfd != null) { + pfd.detachFd(); + } + + // Restore prior thread tags. + if (tag != oldTag) { + TrafficStats.setThreadStatsTag(oldTag); + } + if (uid != -1) { + ThreadStatsUid.clear(); + } + } } diff --git a/library/java/org/chromium/net/ThreadStatsUid.java b/library/java/org/chromium/net/ThreadStatsUid.java new file mode 100644 index 0000000000..55856a5c43 --- /dev/null +++ b/library/java/org/chromium/net/ThreadStatsUid.java @@ -0,0 +1,45 @@ +package org.chromium.net; + +import android.net.TrafficStats; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +/** + * Class to wrap TrafficStats.setThreadStatsUid(int uid) and TrafficStats.clearThreadStatsUid() + * which are hidden and so must be accessed via reflection. + */ +public class ThreadStatsUid { + // Reference to TrafficStats.setThreadStatsUid(int uid). + private static final Method sSetThreadStatsUid; + // Reference to TrafficStats.clearThreadStatsUid(). + private static final Method sClearThreadStatsUid; + // Get reference to TrafficStats.setThreadStatsUid(int uid) and + // TrafficStats.clearThreadStatsUid() via reflection. + static { + try { + sSetThreadStatsUid = TrafficStats.class.getMethod("setThreadStatsUid", Integer.TYPE); + sClearThreadStatsUid = TrafficStats.class.getMethod("clearThreadStatsUid"); + } catch (NoSuchMethodException | SecurityException e) { + throw new RuntimeException("Unable to get TrafficStats methods", e); + } + } + /** Calls TrafficStats.setThreadStatsUid(uid) */ + public static void set(int uid) { + try { + sSetThreadStatsUid.invoke(null, uid); // Pass null for "this" as it's a static method. + } catch (IllegalAccessException e) { + throw new RuntimeException("TrafficStats.setThreadStatsUid failed", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("TrafficStats.setThreadStatsUid failed", e); + } + } + /** Calls TrafficStats.clearThreadStatsUid() */ + public static void clear() { + try { + sClearThreadStatsUid.invoke(null); // Pass null for "this" as it's a static method. + } catch (IllegalAccessException e) { + throw new RuntimeException("TrafficStats.clearThreadStatsUid failed", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("TrafficStats.clearThreadStatsUid failed", e); + } + } +} diff --git a/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java b/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java index fc9d1e06db..c278fb6c95 100644 --- a/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java +++ b/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java @@ -47,6 +47,7 @@ public class NativeCronetEngineBuilderImpl extends CronetEngineBuilderImpl { private boolean mEnableDrainPostDnsRefresh = false; private boolean mEnableHttp3 = false; private boolean mEnableGzip = true; + private boolean mEnableSocketTag = true; private boolean mEnableHappyEyeballs = false; private boolean mEnableInterfaceBinding = false; private boolean mForceIPv6 = false; @@ -109,7 +110,7 @@ private EnvoyConfiguration createEnvoyConfiguration() { mDnsRefreshSeconds, mDnsFailureRefreshSecondsBase, mDnsFailureRefreshSecondsMax, mDnsQueryTimeoutSeconds, mDnsMinRefreshSeconds, mDnsPreresolveHostnames, mDnsFallbackNameservers, mEnableDnsFilterUnroutableFamilies, mDnsUseSystemResolver, - mEnableDrainPostDnsRefresh, mEnableHttp3, mEnableGzip, brotliEnabled(), + mEnableDrainPostDnsRefresh, mEnableHttp3, mEnableGzip, brotliEnabled(), mEnableSocketTag, mEnableHappyEyeballs, mEnableInterfaceBinding, mForceIPv6, mH2ConnectionKeepaliveIdleIntervalMilliseconds, mH2ConnectionKeepaliveTimeoutSeconds, mH2ExtendKeepaliveTimeout, mH2RawDomains, mMaxConnectionsPerHost, mStatsFlushSeconds, diff --git a/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt b/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt index 920948dafd..18a6f205cd 100644 --- a/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt +++ b/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt @@ -58,6 +58,7 @@ open class EngineBuilder( private var enableHappyEyeballs = true private var enableGzip = true private var enableBrotli = false + private var enableSocketTagging = false private var enableInterfaceBinding = false private var forceIPv6 = false private var h2ConnectionKeepaliveIdleIntervalMilliseconds = 1 @@ -301,6 +302,18 @@ open class EngineBuilder( return this } + /** + * Specify whether to support socket tagging or not. Defaults to false. + * + * @param enableSocketTagging whether or not support socket tagging. + * + * @return This builder. + */ + fun enableSocketTagging(enableSocketTagging: Boolean): EngineBuilder { + this.enableSocketTagging = enableSocketTagging + return this + } + /** * Specify whether sockets may attempt to bind to a specific interface, based on network * conditions. @@ -613,6 +626,7 @@ open class EngineBuilder( enableHttp3, enableGzip, enableBrotli, + enableSocketTagging, enableHappyEyeballs, enableInterfaceBinding, forceIPv6, diff --git a/library/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilder.kt b/library/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilder.kt index 5814915929..dbc08be513 100644 --- a/library/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilder.kt +++ b/library/kotlin/io/envoyproxy/envoymobile/RequestHeadersBuilder.kt @@ -94,6 +94,28 @@ class RequestHeadersBuilder : HeadersBuilder { return this } + /** + * Add a socket tag to be applied to the socket. + * + * @param uid: Traffic stats UID to be applied. + * @param tag: Traffic stats tag to be applied. + * + * See: https://source.android.com/devices/tech/datausage/tags-explained + * See: https://developer.android.com/reference/android/net/TrafficStats#setThreadStatsTag(int) + * See: https://developer.android.com/reference/android/net/TrafficStats#setThreadStatsUid(int) + * See: https://developer.android.com/reference/android/net/TrafficStats#tagSocket(java.net.Socket) + * + * @return RequestHeadersBuilder, This builder. + */ + fun addSocketTag(uid: Int, tag: Int): + RequestHeadersBuilder { + internalSet( + "x-envoy-mobile-socket-tag", + mutableListOf(uid.toString() + "," + tag.toString()) + ) + return this + } + /** * Build the request headers using the current builder. * diff --git a/test/cc/unit/envoy_config_test.cc b/test/cc/unit/envoy_config_test.cc index 4d180c4b39..334db550b8 100644 --- a/test/cc/unit/envoy_config_test.cc +++ b/test/cc/unit/envoy_config_test.cc @@ -119,6 +119,21 @@ TEST(TestConfig, SetBrotli) { ASSERT_THAT(bootstrap.DebugString(), HasSubstr("brotli.decompressor.v3.Brotli")); } +TEST(TestConfig, SetSocketTag) { + auto engine_builder = EngineBuilder(); + + engine_builder.enableSocketTagging(false); + auto config_str = engine_builder.generateConfigStr(); + envoy::config::bootstrap::v3::Bootstrap bootstrap; + TestUtility::loadFromYaml(absl::StrCat(config_header, config_str), bootstrap); + ASSERT_THAT(bootstrap.DebugString(), Not(HasSubstr("http.socket_tag.SocketTag"))); + + engine_builder.enableSocketTagging(true); + config_str = engine_builder.generateConfigStr(); + TestUtility::loadFromYaml(absl::StrCat(config_header, config_str), bootstrap); + ASSERT_THAT(bootstrap.DebugString(), HasSubstr("http.socket_tag.SocketTag")); +} + TEST(TestConfig, SetAltSvcCache) { auto engine_builder = EngineBuilder(); diff --git a/test/java/integration/AndroidEngineSocketTagTest.java b/test/java/integration/AndroidEngineSocketTagTest.java new file mode 100644 index 0000000000..bb269acbc9 --- /dev/null +++ b/test/java/integration/AndroidEngineSocketTagTest.java @@ -0,0 +1,166 @@ +package test.kotlin.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import io.envoyproxy.envoymobile.AndroidEngineBuilder; +import io.envoyproxy.envoymobile.Engine; +import io.envoyproxy.envoymobile.EnvoyError; +import io.envoyproxy.envoymobile.FinalStreamIntel; +import io.envoyproxy.envoymobile.LogLevel; +import io.envoyproxy.envoymobile.RequestHeaders; +import io.envoyproxy.envoymobile.RequestHeadersBuilder; +import io.envoyproxy.envoymobile.RequestMethod; +import io.envoyproxy.envoymobile.ResponseHeaders; +import io.envoyproxy.envoymobile.ResponseTrailers; +import io.envoyproxy.envoymobile.Stream; +import io.envoyproxy.envoymobile.StreamIntel; +import io.envoyproxy.envoymobile.UpstreamHttpProtocol; +import io.envoyproxy.envoymobile.engine.AndroidJniLibrary; +import io.envoyproxy.envoymobile.engine.testing.RequestScenario; +import io.envoyproxy.envoymobile.engine.testing.Response; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class AndroidEngineSocketTagTest { + + private final MockWebServer mockWebServer = new MockWebServer(); + private Engine engine; + + @BeforeClass + public static void loadJniLibrary() { + AndroidJniLibrary.loadTestLibrary(); + } + + @Before + public void setUpEngine() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + Context appContext = ApplicationProvider.getApplicationContext(); + engine = new AndroidEngineBuilder(appContext) + .addLogLevel(LogLevel.OFF) + .enableSocketTagging(true) + .setOnEngineRunning(() -> { + latch.countDown(); + return null; + }) + .build(); + latch.await(); // Don't launch a request before initialization has completed. + } + + @After + public void shutdownEngine() throws Exception { + engine.terminate(); + mockWebServer.shutdown(); + } + + @Test + public void socket_tag() throws Exception { + mockWebServer.setDispatcher(new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest recordedRequest) { + assertThat(recordedRequest.getMethod()).isEqualTo(RequestMethod.GET.name()); + assertThat(recordedRequest.getHeader("x-envoy-mobile-socket-tag")).isEqualTo(null); + return new MockResponse().setBody("This is my response Body"); + } + }); + mockWebServer.start(); + RequestScenario requestScenario = new RequestScenario() + .setHttpMethod(RequestMethod.GET) + .setUrl(mockWebServer.url("post/flowers").toString()) + .addSocketTag(1, 2); + + Response response = sendRequest(requestScenario); + + assertThat(response.getHeaders().getHttpStatus()).isEqualTo(200); + assertThat(response.getBodyAsString()).isEqualTo("This is my response Body"); + assertThat(response.getEnvoyError()).isNull(); + } + + private Response sendRequest(RequestScenario requestScenario) throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference response = new AtomicReference<>(new Response()); + final AtomicReference streamRef = new AtomicReference<>(); + + Stream stream = + engine.streamClient() + .newStreamPrototype() + .setOnResponseHeaders((responseHeaders, endStream, streamIntel) -> { + response.get().setHeaders(responseHeaders); + if (requestScenario.cancelOnResponseHeaders) { + streamRef.get().cancel(); // Should be a noop when endStream == true + } else { + if (requestScenario.waitOnReadData) { + try { + Thread.sleep(100 + (int)(Math.random() * 50)); + } catch (InterruptedException e) { + // Don't care + } + } + streamRef.get().readData(requestScenario.responseBufferSize); + } + return null; + }) + .setOnResponseData((data, endStream, streamIntel) -> { + response.get().addBody(data); + if (!endStream) { + if (requestScenario.waitOnReadData) { + try { + Thread.sleep(100 + (int)(Math.random() * 50)); + } catch (InterruptedException e) { + // Don't care + } + } + streamRef.get().readData(requestScenario.responseBufferSize); + } + return null; + }) + .setOnError((error, finalStreamIntel) -> { + response.get().setEnvoyError(error); + latch.countDown(); + return null; + }) + .setOnCancel((finalStreamIntel) -> { + response.get().setCancelled(); + latch.countDown(); + return null; + }) + .setOnComplete((finalStreamIntel) -> { + latch.countDown(); + return null; + }) + .setExplicitFlowControl(true) + .start(requestScenario.useDirectExecutor ? Runnable::run + : Executors.newSingleThreadExecutor()); + streamRef.set(stream); // Set before sending headers to avoid race conditions. + stream.sendHeaders(requestScenario.getHeaders(), !requestScenario.hasBody()); + latch.await(); + response.get().throwAssertionErrorIfAny(); + return response.get(); + } +} diff --git a/test/java/integration/BUILD b/test/java/integration/BUILD index 9febbb6b25..32e0dae3af 100644 --- a/test/java/integration/BUILD +++ b/test/java/integration/BUILD @@ -60,3 +60,22 @@ envoy_mobile_android_test( "//test/java/io/envoyproxy/envoymobile/engine/testing", ], ) + +envoy_mobile_android_test( + name = "android_engine_socket_tag_test", + srcs = [ + "AndroidEngineSocketTagTest.java", + ], + exec_properties = { + # TODO(lfpino): Remove this once the sandboxNetwork=off works for ipv4 localhost addresses. + "sandboxNetwork": "standard", + }, + native_deps = [ + "//library/common/jni:libndk_envoy_jni.so", + "//library/common/jni:libndk_envoy_jni.jnilib", + ], + deps = [ + "//library/kotlin/io/envoyproxy/envoymobile:envoy_lib", + "//test/java/io/envoyproxy/envoymobile/engine/testing", + ], +) diff --git a/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt b/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt index 41740ec430..aefadef576 100644 --- a/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt +++ b/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt @@ -39,6 +39,11 @@ private const val BROTLI_INSERT = - name: BrotliFilter """ +private const val SOCKET_TAG_INSERT = +""" + - name: SocketTag +""" + class EnvoyConfigurationTest { fun buildTestEnvoyConfiguration( @@ -59,6 +64,7 @@ class EnvoyConfigurationTest { enableHttp3: Boolean = false, enableGzip: Boolean = true, enableBrotli: Boolean = false, + enableSocketTagging: Boolean = false, enableHappyEyeballs: Boolean = false, enableInterfaceBinding: Boolean = false, forceIPv6: Boolean = false, @@ -93,6 +99,7 @@ class EnvoyConfigurationTest { enableHttp3, enableGzip, enableBrotli, + enableSocketTagging, enableHappyEyeballs, enableInterfaceBinding, forceIPv6, @@ -120,8 +127,7 @@ class EnvoyConfigurationTest { val envoyConfiguration = buildTestEnvoyConfiguration() val resolvedTemplate = envoyConfiguration.resolveTemplate( - TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT - + TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT, SOCKET_TAG_INSERT ) assertThat(resolvedTemplate).contains("&connect_timeout 123s") @@ -204,7 +210,7 @@ class EnvoyConfigurationTest { ) val resolvedTemplate = envoyConfiguration.resolveTemplate( - TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT + TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT, SOCKET_TAG_INSERT ) // DNS @@ -239,7 +245,7 @@ class EnvoyConfigurationTest { ) val resolvedTemplate = envoyConfiguration.resolveTemplate( - TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT + TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT, SOCKET_TAG_INSERT ) assertThat(resolvedTemplate).contains("&dns_resolver_config {\"@type\":\"type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig\"}") @@ -251,7 +257,7 @@ class EnvoyConfigurationTest { val envoyConfiguration = buildTestEnvoyConfiguration() try { - envoyConfiguration.resolveTemplate("{{ missing }}", "", "", "", "", "") + envoyConfiguration.resolveTemplate("{{ missing }}", "", "", "", "", "", "") fail("Unresolved configuration keys should trigger exception.") } catch (e: EnvoyConfiguration.ConfigurationException) { assertThat(e.message).contains("missing") @@ -266,7 +272,7 @@ class EnvoyConfigurationTest { ) try { - envoyConfiguration.resolveTemplate("", "", "", "", "", "") + envoyConfiguration.resolveTemplate("", "", "", "", "", "", "") fail("Conflicting stats keys should trigger exception.") } catch (e: EnvoyConfiguration.ConfigurationException) { assertThat(e.message).contains("cannot enable both statsD and gRPC metrics sink") @@ -280,7 +286,7 @@ class EnvoyConfigurationTest { ) val resolvedTemplate = envoyConfiguration.resolveTemplate( - TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT + TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT, SOCKET_TAG_INSERT ) assertThat(resolvedTemplate).contains("&h2_raw_domains [\"h2-raw.example.com\",\"h2-raw.example.com2\"]") @@ -293,7 +299,7 @@ class EnvoyConfigurationTest { ) val resolvedTemplate = envoyConfiguration.resolveTemplate( - TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT + TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT, SOCKET_TAG_INSERT ) assertThat(resolvedTemplate).contains("&dns_resolver_config {\"@type\":\"type.googleapis.com/envoy.extensions.network.dns_resolver.cares.v3.CaresDnsResolverConfig\",\"resolvers\":[{\"socket_address\":{\"address\":\"8.8.8.8\"}},{\"socket_address\":{\"address\":\"1.1.1.1\"}}],\"use_resolvers_as_fallback\": true, \"filter_unroutable_families\": true}") diff --git a/test/java/io/envoyproxy/envoymobile/engine/testing/RequestScenario.java b/test/java/io/envoyproxy/envoymobile/engine/testing/RequestScenario.java index 02d3b42c46..cecfce85ee 100644 --- a/test/java/io/envoyproxy/envoymobile/engine/testing/RequestScenario.java +++ b/test/java/io/envoyproxy/envoymobile/engine/testing/RequestScenario.java @@ -46,12 +46,17 @@ public final class RequestScenario { private RequestMethod method = null; private final List bodyChunks = new ArrayList<>(); private final List> headers = new ArrayList<>(); + private int trafficStatsUid = 0; + private int trafficStatsTag = 0; public RequestHeaders getHeaders() { RequestHeadersBuilder requestHeadersBuilder = new RequestHeadersBuilder(method, url.getProtocol(), url.getAuthority(), url.getPath()); headers.forEach(entry -> requestHeadersBuilder.add(entry.getKey(), entry.getValue())); // HTTP1 is the only way to send HTTP requests (not HTTPS) + if (trafficStatsUid != 0) { + requestHeadersBuilder.addSocketTag(trafficStatsUid, trafficStatsTag); + } return requestHeadersBuilder.addUpstreamHttpProtocol(UpstreamHttpProtocol.HTTP1).build(); } @@ -136,4 +141,10 @@ public RequestScenario useByteBufferPosition() { useByteBufferPosition = true; return this; } + + public RequestScenario addSocketTag(int uid, int tag) { + trafficStatsUid = uid; + trafficStatsTag = tag; + return this; + } } diff --git a/test/kotlin/apps/experimental/MainActivity.kt b/test/kotlin/apps/experimental/MainActivity.kt index 11eead710a..428dd7706f 100644 --- a/test/kotlin/apps/experimental/MainActivity.kt +++ b/test/kotlin/apps/experimental/MainActivity.kt @@ -59,6 +59,7 @@ class MainActivity : Activity() { .enableInterfaceBinding(true) .enableDNSUseSystemResolver(true) .forceIPv6(true) + .enableSocketTagging(true) .addNativeFilter("envoy.filters.http.buffer", "{\"@type\":\"type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer\",\"max_request_bytes\":5242880}") .addStringAccessor("demo-accessor", { "PlatformString" }) .addKeyValueStore("demo-kv-store", SharedPreferencesStore(preferences)) @@ -117,6 +118,7 @@ class MainActivity : Activity() { RequestMethod.GET, REQUEST_SCHEME, REQUEST_AUTHORITY, REQUEST_PATH ) .addUpstreamHttpProtocol(UpstreamHttpProtocol.HTTP2) + .addSocketTag(1,2) .build() engine .streamClient()