diff --git a/mobile/EnvoyMobile.podspec b/mobile/EnvoyMobile.podspec index a479650fc641..b4ca64e9f5e3 100644 --- a/mobile/EnvoyMobile.podspec +++ b/mobile/EnvoyMobile.podspec @@ -10,7 +10,7 @@ Pod::Spec.new do |s| s.platform = :ios, '11.0' s.swift_versions = ['5.5'] s.libraries = 'resolv.9', 'c++' - s.frameworks = 'SystemConfiguration', 'UIKit' + s.frameworks = 'Network', 'SystemConfiguration', 'UIKit' s.source = { http: "https://github.com/lyft/envoy-mobile/releases/download/v#{s.version}/envoy_ios_cocoapods.zip" } s.vendored_frameworks = 'Envoy.framework' s.source_files = 'Envoy.framework/Headers/*.h', 'Envoy.framework/Swift/*.swift' diff --git a/mobile/dist/BUILD b/mobile/dist/BUILD index a0c1c0d9d322..91f357b3c035 100644 --- a/mobile/dist/BUILD +++ b/mobile/dist/BUILD @@ -22,6 +22,7 @@ apple_static_framework_import( "c++", ], sdk_frameworks = [ + "Network", "SystemConfiguration", "UIKit", ], diff --git a/mobile/docs/root/api/starting_envoy.rst b/mobile/docs/root/api/starting_envoy.rst index 9e7ebdebd437..e6fe65c0f03f 100644 --- a/mobile/docs/root/api/starting_envoy.rst +++ b/mobile/docs/root/api/starting_envoy.rst @@ -36,7 +36,7 @@ After the stream client is obtained, it should be stored and used to start netwo This type is used to configure an instance of ``Engine`` before finally creating the engine using ``.build()``. -Available builders are 1:1 between iOS/Android, and are documented below. +Available builders are nearly all 1:1 between iOS/Android, and are documented below. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``addConnectTimeoutSeconds`` @@ -339,6 +339,25 @@ Specify a closure to be called by Envoy to access arbitrary strings from Platfor // Swift builder.addStringAccessor(name: "demo-accessor", accessor: { return "PlatformString" }) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``enableNetworkPathMonitor`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Configure the engine to use ``NWPathMonitor`` rather than ``SCNetworkReachability`` +on supported platforms (iOS 12+) to update the preferred Envoy network cluster (e.g. WLAN vs WWAN). + +.. attention:: + + Only available on iOS 12 or later. + +**Example**:: + + // Kotlin + // N/A + + // Swift + builder.enableNetworkPathMonitor() + ---------------------- Advanced configuration ---------------------- diff --git a/mobile/docs/root/intro/version_history.rst b/mobile/docs/root/intro/version_history.rst index 2c4b0d143047..d646e121bf1b 100644 --- a/mobile/docs/root/intro/version_history.rst +++ b/mobile/docs/root/intro/version_history.rst @@ -1,6 +1,15 @@ Version history --------------- +Next +==================== + +Features: + +- Adds support for using `NWPathMonitor `_ + instead of `SCNetworkReachability `_ + on supported platforms (iOS 12+) to update the preferred Envoy network cluster (e.g. WLAN vs WWAN). + 0.4.1 (May 28, 2021) ==================== diff --git a/mobile/library/objective-c/BUILD b/mobile/library/objective-c/BUILD index d4de614c250f..6ec18e2da4cc 100644 --- a/mobile/library/objective-c/BUILD +++ b/mobile/library/objective-c/BUILD @@ -28,6 +28,7 @@ objc_library( ], module_name = "EnvoyEngine", sdk_frameworks = [ + "Network", "SystemConfiguration", "UIKit", ], diff --git a/mobile/library/objective-c/EnvoyEngine.h b/mobile/library/objective-c/EnvoyEngine.h index f780fe5f61de..2b015e1843ff 100644 --- a/mobile/library/objective-c/EnvoyEngine.h +++ b/mobile/library/objective-c/EnvoyEngine.h @@ -385,10 +385,13 @@ extern const int kEnvoyFailure; running. @param logger Logging interface. @param eventTracker Event tracking interface. + @param enableNetworkPathMonitor Configure the engine to use `NWPathMonitor` to observe network + reachability. */ - (instancetype)initWithRunningCallback:(nullable void (^)())onEngineRunning logger:(nullable void (^)(NSString *))logger - eventTracker:(nullable void (^)(EnvoyEvent *))eventTracker; + eventTracker:(nullable void (^)(EnvoyEvent *))eventTracker + enableNetworkPathMonitor:(BOOL)enableNetworkPathMonitor; /** Run the Envoy engine with the provided configuration and log level. @@ -518,10 +521,16 @@ extern const int kEnvoyFailure; // Monitors network changes in order to update Envoy network cluster preferences. @interface EnvoyNetworkMonitor : NSObject -// Start monitoring reachability, updating the preferred Envoy network cluster on changes. +// Start monitoring reachability using `SCNetworkReachability`, updating the +// preferred Envoy network cluster on changes. // This is typically called by `EnvoyEngine` automatically on startup. + (void)startReachabilityIfNeeded; +// Start monitoring reachability using `NWPathMonitor`, updating the +// preferred Envoy network cluster on changes. +// This is typically called by `EnvoyEngine` automatically on startup. ++ (void)startPathMonitorIfNeeded API_AVAILABLE(ios(12)); + @end NS_ASSUME_NONNULL_END diff --git a/mobile/library/objective-c/EnvoyEngineImpl.m b/mobile/library/objective-c/EnvoyEngineImpl.m index dd6961845f41..471b4ce3a9da 100644 --- a/mobile/library/objective-c/EnvoyEngineImpl.m +++ b/mobile/library/objective-c/EnvoyEngineImpl.m @@ -389,7 +389,8 @@ @implementation EnvoyEngineImpl { - (instancetype)initWithRunningCallback:(nullable void (^)())onEngineRunning logger:(nullable void (^)(NSString *))logger - eventTracker:(nullable void (^)(EnvoyEvent *))eventTracker { + eventTracker:(nullable void (^)(EnvoyEvent *))eventTracker + enableNetworkPathMonitor:(BOOL)enableNetworkPathMonitor { self = [super init]; if (!self) { return nil; @@ -418,7 +419,19 @@ - (instancetype)initWithRunningCallback:(nullable void (^)())onEngineRunning } _engineHandle = init_engine(native_callbacks, native_logger, native_event_tracker); - [EnvoyNetworkMonitor startReachabilityIfNeeded]; + + if (enableNetworkPathMonitor) { + if (@available(iOS 12, *)) { + [EnvoyNetworkMonitor startPathMonitorIfNeeded]; + } else { + NSLog( + @"[Envoy] Cannot use NWPathMonitor on iOS < 12. Falling back to `SCNetworkReachability`"); + [EnvoyNetworkMonitor startReachabilityIfNeeded]; + } + } else { + [EnvoyNetworkMonitor startReachabilityIfNeeded]; + } + return self; } diff --git a/mobile/library/objective-c/EnvoyNetworkMonitor.m b/mobile/library/objective-c/EnvoyNetworkMonitor.m index f10ed261ae4f..49414d7ba088 100644 --- a/mobile/library/objective-c/EnvoyNetworkMonitor.m +++ b/mobile/library/objective-c/EnvoyNetworkMonitor.m @@ -2,6 +2,8 @@ #import "library/common/main_interface.h" +#import +#import #import @implementation EnvoyNetworkMonitor @@ -13,7 +15,74 @@ + (void)startReachabilityIfNeeded { }); } -#pragma mark - Private ++ (void)startPathMonitorIfNeeded { + static dispatch_once_t monitorStarted; + dispatch_once(&monitorStarted, ^{ + _start_path_monitor(); + }); +} + +#pragma mark - Private (Network Path Monitor) + +static nw_path_monitor_t _path_monitor; + +static void _start_path_monitor() { + if (@available(iOS 12, *)) { + _path_monitor = nw_path_monitor_create(); + + dispatch_queue_attr_t attrs = dispatch_queue_attr_make_with_qos_class( + DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, DISPATCH_QUEUE_PRIORITY_DEFAULT); + dispatch_queue_t queue = + dispatch_queue_create("io.envoyproxy.envoymobile.EnvoyNetworkMonitor", attrs); + nw_path_monitor_set_queue(_path_monitor, queue); + + nw_path_monitor_set_update_handler(_path_monitor, ^(nw_path_t _Nonnull path) { + BOOL isSatisfied = nw_path_get_status(path) == nw_path_status_satisfied; + if (!isSatisfied) { + // TODO(jpsim): Handle all possible path status values + // + // - nw_path_status_invalid: The path is not valid. + // - nw_path_status_unsatisfied: The path is not available for use. + // - nw_path_status_satisfied: The path is available to establish connections and send data. + // - nw_path_status_satisfiable: The path is not currently available, but establishing a new + // connection may activate the path. + return; + } + + BOOL isCellular = nw_path_uses_interface_type(path, nw_interface_type_cellular); + NSLog(@"[Envoy] setting preferred network to %@", isCellular ? @"WWAN" : @"WLAN"); + set_preferred_network(isCellular ? ENVOY_NET_WWAN : ENVOY_NET_WLAN); + + // TODO(jpsim): Should we shadow or otherwise compare these results with the reachability + // flags? + + // TODO(jpsim): Should we report back other properties of the reachable path? + // + // - nw_path_get_status: + // https://developer.apple.com/documentation/network/2976886-nw_path_get_status + // - nw_path_uses_interface_type: + // https://developer.apple.com/documentation/network/2976898-nw_path_uses_interface_type + // - nw_path_enumerate_gateways: + // https://developer.apple.com/documentation/network/3175017-nw_path_enumerate_gateways + // - nw_path_has_ipv4: + // https://developer.apple.com/documentation/network/2976888-nw_path_has_ipv4 + // - nw_path_has_ipv6: + // https://developer.apple.com/documentation/network/2976889-nw_path_has_ipv6 + // - nw_path_has_dns: + // https://developer.apple.com/documentation/network/2976887-nw_path_has_dns + // - nw_path_is_constrained: + // https://developer.apple.com/documentation/network/3131049-nw_path_is_constrained + // - nw_path_is_expensive: + // https://developer.apple.com/documentation/network/2976891-nw_path_is_expensive + // - nw_path_copy_effective_remote_endpoint: + // https://developer.apple.com/documentation/network/2976883-nw_path_copy_effective_remote_en + }); + + nw_path_monitor_start(_path_monitor); + } +} + +#pragma mark - Private (Reachability) static SCNetworkReachabilityRef _reachability_ref; diff --git a/mobile/library/swift/EngineBuilder.swift b/mobile/library/swift/EngineBuilder.swift index 0367f0ee7bca..8dadbf32913f 100644 --- a/mobile/library/swift/EngineBuilder.swift +++ b/mobile/library/swift/EngineBuilder.swift @@ -33,6 +33,7 @@ open class EngineBuilder: NSObject { private var onEngineRunning: (() -> Void)? private var logger: ((String) -> Void)? private var eventTracker: (([String: String]) -> Void)? + private(set) var enableNetworkPathMonitor = false private var nativeFilterChain: [EnvoyNativeFilterConfig] = [] private var platformFilterChain: [EnvoyHTTPFilterFactory] = [] private var stringAccessors: [String: EnvoyStringAccessor] = [:] @@ -294,6 +295,16 @@ open class EngineBuilder: NSObject { return self } + /// Configure the engine to use `NWPathMonitor` to observe network reachability. + /// + /// - returns: This builder. + @discardableResult + @available(iOS 12, *) + public func enableNetworkPathMonitor(_ enableNetworkPathMonitor: Bool) -> Self { + self.enableNetworkPathMonitor = enableNetworkPathMonitor + return self + } + /// Add the App Version of the App using this Envoy Client. /// /// - parameter appVersion: The version. @@ -320,7 +331,7 @@ open class EngineBuilder: NSObject { /// /// - parameter virtualClusters: The JSON configuration string for virtual clusters. /// - /// returns: This builder. + /// - returns: This builder. @discardableResult public func addVirtualClusters(_ virtualClusters: String) -> Self { self.virtualClusters = virtualClusters @@ -331,7 +342,7 @@ open class EngineBuilder: NSObject { /// used for development/debugging purposes only. Enabling it in production may open /// your app to security vulnerabilities. /// - /// returns: This builder. + /// - returns: This builder. @discardableResult public func enableAdminInterface() -> Self { self.adminInterfaceEnabled = true @@ -342,7 +353,8 @@ open class EngineBuilder: NSObject { /// public func build() -> Engine { let engine = self.engineType.init(runningCallback: self.onEngineRunning, logger: self.logger, - eventTracker: self.eventTracker) + eventTracker: self.eventTracker, + enableNetworkPathMonitor: self.enableNetworkPathMonitor) let config = EnvoyConfiguration( adminInterfaceEnabled: self.adminInterfaceEnabled, grpcStatsDomain: self.grpcStatsDomain, diff --git a/mobile/library/swift/mocks/MockEnvoyEngine.swift b/mobile/library/swift/mocks/MockEnvoyEngine.swift index decf93db7008..39acfbfa48f1 100644 --- a/mobile/library/swift/mocks/MockEnvoyEngine.swift +++ b/mobile/library/swift/mocks/MockEnvoyEngine.swift @@ -4,7 +4,7 @@ import Foundation /// Mock implementation of `EnvoyEngine`. Used internally for testing the bridging layer & mocking. final class MockEnvoyEngine: NSObject { init(runningCallback onEngineRunning: (() -> Void)? = nil, logger: ((String) -> Void)? = nil, - eventTracker: (([String: String]) -> Void)? = nil) {} + eventTracker: (([String: String]) -> Void)? = nil, enableNetworkPathMonitor: Bool = true) {} /// Closure called when `run(withConfig:)` is called. static var onRunWithConfig: ((_ config: EnvoyConfiguration, _ logLevel: String?) -> Void)? diff --git a/mobile/test/swift/EngineBuilderTests.swift b/mobile/test/swift/EngineBuilderTests.swift index 3e14457503d2..9ac8b45886e8 100644 --- a/mobile/test/swift/EngineBuilderTests.swift +++ b/mobile/test/swift/EngineBuilderTests.swift @@ -3,6 +3,8 @@ import EnvoyEngine import Foundation import XCTest +// swiftlint:disable file_length type_body_length + private let kMockTemplate = """ fixture_template: @@ -19,7 +21,6 @@ fixture_template: private struct TestFilter: Filter {} -// swiftlint:disable:next type_body_length final class EngineBuilderTests: XCTestCase { override func tearDown() { super.tearDown() @@ -27,6 +28,20 @@ final class EngineBuilderTests: XCTestCase { MockEnvoyEngine.onRunWithTemplate = nil } + func testEnableNetworkPathMonitorDefaultsToFalse() { + let builder = EngineBuilder() + XCTAssertFalse(builder.enableNetworkPathMonitor) + } + + @available(iOS 12, *) + func testEnableNetworkPathMonitorSetsToValue() { + let builder = EngineBuilder() + .enableNetworkPathMonitor(true) + XCTAssertTrue(builder.enableNetworkPathMonitor) + builder.enableNetworkPathMonitor(false) + XCTAssertFalse(builder.enableNetworkPathMonitor) + } + func testCustomConfigTemplateUsesSpecifiedYAMLWhenRunningEnvoy() { let expectation = self.expectation(description: "Run called with expected data") MockEnvoyEngine.onRunWithTemplate = { yaml, _, _ in