Skip to content

Commit

Permalink
network monitor: use NWPathMonitor to determine network reachability (
Browse files Browse the repository at this point in the history
#1874)

Description: When running on iOS 12 or later, use the more modern and more featureful alternative to `SCNetworkReachability` from the Network framework.
Risk Level: Medium. It's likely that this new approach to observing network reachability will be more correct and reliable than the `SCNetworkReachability` approach, but it's still possible for differences in behavior to cause issues where the wrong radio type is selected on devices running iOS 12 or later. To mitigate, the new path monitor is opt-in only.
Testing: I've tested this on a physical iPhone, confirming that the logs about switching preferred network matches when either the reachability or path monitor mechanisms are used. Unit tests cover that appending `.enableNetworkPathMonitor()` to the builder commands flips the `useNetworkPathMonitor` flag to true.
Docs Changes: Added.
Release Notes: Added.

Signed-off-by: JP Simard <jp@jpsim.com>
  • Loading branch information
jpsim committed Nov 28, 2022
1 parent 1ce742e commit 2a59aed
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 12 deletions.
2 changes: 1 addition & 1 deletion mobile/EnvoyMobile.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions mobile/dist/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ apple_static_framework_import(
"c++",
],
sdk_frameworks = [
"Network",
"SystemConfiguration",
"UIKit",
],
Expand Down
21 changes: 20 additions & 1 deletion mobile/docs/root/api/starting_envoy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``
Expand Down Expand Up @@ -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
----------------------
Expand Down
9 changes: 9 additions & 0 deletions mobile/docs/root/intro/version_history.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Version history
---------------

Next
====================

Features:

- Adds support for using `NWPathMonitor <https://developer.apple.com/documentation/network/nwpathmonitor>`_
instead of `SCNetworkReachability <https://developer.apple.com/documentation/systemconfiguration/scnetworkreachability>`_
on supported platforms (iOS 12+) to update the preferred Envoy network cluster (e.g. WLAN vs WWAN).

0.4.1 (May 28, 2021)
====================

Expand Down
1 change: 1 addition & 0 deletions mobile/library/objective-c/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ objc_library(
],
module_name = "EnvoyEngine",
sdk_frameworks = [
"Network",
"SystemConfiguration",
"UIKit",
],
Expand Down
13 changes: 11 additions & 2 deletions mobile/library/objective-c/EnvoyEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
17 changes: 15 additions & 2 deletions mobile/library/objective-c/EnvoyEngineImpl.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
71 changes: 70 additions & 1 deletion mobile/library/objective-c/EnvoyNetworkMonitor.m
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#import "library/common/main_interface.h"

#import <Foundation/Foundation.h>
#import <Network/Network.h>
#import <SystemConfiguration/SystemConfiguration.h>

@implementation EnvoyNetworkMonitor
Expand All @@ -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;

Expand Down
18 changes: 15 additions & 3 deletions mobile/library/swift/EngineBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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] = [:]
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion mobile/library/swift/mocks/MockEnvoyEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)?
Expand Down
17 changes: 16 additions & 1 deletion mobile/test/swift/EngineBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import EnvoyEngine
import Foundation
import XCTest

// swiftlint:disable file_length type_body_length

private let kMockTemplate =
"""
fixture_template:
Expand All @@ -19,14 +21,27 @@ fixture_template:

private struct TestFilter: Filter {}

// swiftlint:disable:next type_body_length
final class EngineBuilderTests: XCTestCase {
override func tearDown() {
super.tearDown()
MockEnvoyEngine.onRunWithConfig = nil
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
Expand Down

0 comments on commit 2a59aed

Please sign in to comment.