Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(cordova/apple): move network settings logic to Swift #1658

Merged
merged 18 commits into from
Jul 7, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -13,54 +13,156 @@
// limitations under the License.

import Foundation
import NetworkExtension

// Serializable class to wrap a tunnel's configuration.
// Properties must be kept in sync with ServerConfig in www/types/outlinePlugin.d.ts
// Note that this class and its non-private properties must be public in order to be visible to the ObjC
// target of the OutlineAppleLib Swift Package.
@objcMembers
public class OutlineTunnel: NSObject, Codable {
public var id: String?
public var host: String?
public var port: String?
public var method: String?
public var password: String?
public var prefix: Data?
public var config: [String: String] {
let scalars = prefix?.map{Unicode.Scalar($0)}
let characters = scalars?.map{Character($0)}
let prefixStr = String(characters ?? [])
return ["host": host ?? "", "port": port ?? "", "password": password ?? "",
"method": method ?? "", "prefix": prefixStr]
}
public var id: String?
public var host: String?
public var port: String?
public var method: String?
public var password: String?
public var prefix: Data?
public var config: [String: String] {
let scalars = prefix?.map{Unicode.Scalar($0)}
let characters = scalars?.map{Character($0)}
let prefixStr = String(characters ?? [])
return ["host": host ?? "", "port": port ?? "", "password": password ?? "",
"method": method ?? "", "prefix": prefixStr]
}

Check warning on line 36 in src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineTunnel.swift

View check run for this annotation

Codecov / codecov/patch

src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineTunnel.swift#L30-L36

Added lines #L30 - L36 were not covered by tests

@objc
public enum TunnelStatus: Int {
case connected = 0
case disconnected = 1
case reconnecting = 2
}

public convenience init(id: String, config: [String: Any]) {
self.init()
self.id = id
self.host = config["host"] as? String
self.password = config["password"] as? String
self.method = config["method"] as? String
if let port = config["port"] {
self.port = String(describing: port) // Handle numeric values
}
if let prefix = config["prefix"] as? String {
self.prefix = Data(prefix.utf16.map{UInt8($0)})
}
}

Check warning on line 57 in src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineTunnel.swift

View check run for this annotation

Codecov / codecov/patch

src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineTunnel.swift#L45-L57

Added lines #L45 - L57 were not covered by tests

public func encode() -> Data? {
return try? JSONEncoder().encode(self)
}

Check warning on line 61 in src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineTunnel.swift

View check run for this annotation

Codecov / codecov/patch

src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineTunnel.swift#L59-L61

Added lines #L59 - L61 were not covered by tests

public static func decode(_ jsonData: Data) -> OutlineTunnel? {
return try? JSONDecoder().decode(OutlineTunnel.self, from: jsonData)
}

Check warning on line 65 in src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineTunnel.swift

View check run for this annotation

Codecov / codecov/patch

src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineTunnel.swift#L63-L65

Added lines #L63 - L65 were not covered by tests

// Helper function that we can call from Objective-C.
@objc public static func getTunnelNetworkSettings(tunnelRemoteAddress: String) -> NEPacketTunnelNetworkSettings {
// The remote address is not used for routing, but for display in Settings > VPN > Outline.
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: tunnelRemoteAddress)

@objc
public enum TunnelStatus: Int {
case connected = 0
case disconnected = 1
case reconnecting = 2
}
// Configure VPN address and routing.
let vpnAddress = selectVpnAddress(interfaceAddresses: getNetworkInterfaceAddresses())
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
let ipv4Settings = NEIPv4Settings(addresses: [vpnAddress], subnetMasks: ["255.255.255.0"])
ipv4Settings.includedRoutes = [NEIPv4Route.default()]
ipv4Settings.excludedRoutes = getExcludedIpv4Routes()
settings.ipv4Settings = ipv4Settings

public convenience init(id: String, config: [String: Any]) {
self.init()
self.id = id
self.host = config["host"] as? String
self.password = config["password"] as? String
self.method = config["method"] as? String
if let port = config["port"] {
self.port = String(describing: port) // Handle numeric values
// Configure with Cloudflare, Quad9, and OpenDNS resolver addresses.
settings.dnsSettings = NEDNSSettings(servers: ["1.1.1.1", "9.9.9.9", "208.67.222.222", "208.67.220.220"])

return settings
}
if let prefix = config["prefix"] as? String {
self.prefix = Data(prefix.utf16.map{UInt8($0)})
}

// Returns all IPv4 addresses of all interfaces.
func getNetworkInterfaceAddresses() -> [String] {
var interfaces: UnsafeMutablePointer<ifaddrs>?
var addresses = [String]()

if getifaddrs(&interfaces) != 0 {
fortuna marked this conversation as resolved.
Show resolved Hide resolved
NSLog("Failed to retrieve network interface addresses")
return addresses

Check warning on line 93 in src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineTunnel.swift

View check run for this annotation

Codecov / codecov/patch

src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineTunnel.swift#L92-L93

Added lines #L92 - L93 were not covered by tests
}
}

var interface = interfaces
while interface != nil {
// Only consider IPv4 interfaces.
if interface?.pointee.ifa_addr.pointee.sa_family == UInt8(AF_INET) {
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
let addr = interface!.pointee.ifa_addr!.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { $0.pointee.sin_addr }
if let ip = String(cString: inet_ntoa(addr), encoding: .ascii) {
fortuna marked this conversation as resolved.
Show resolved Hide resolved
addresses.append(ip)
}
}
interface = interface?.pointee.ifa_next
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
}

freeifaddrs(interfaces)

return addresses
}

public func encode() -> Data? {
return try? JSONEncoder().encode(self)
}
let kVpnSubnetCandidates: [String: String] = [
"10": "10.111.222.0",
"172": "172.16.9.1",
"192": "192.168.20.1",
"169": "169.254.19.0"
]

public static func decode(_ jsonData: Data) -> OutlineTunnel? {
return try? JSONDecoder().decode(OutlineTunnel.self, from: jsonData)
}
// Given the list of known interface addresses, returns a local network IP address to use for the VPN.
func selectVpnAddress(interfaceAddresses: [String]) -> String {
var candidates = kVpnSubnetCandidates

for address in interfaceAddresses {
for subnetPrefix in kVpnSubnetCandidates.keys {
if address.hasPrefix(subnetPrefix) {
// The subnet (not necessarily the address) is in use, remove it from our list.
candidates.removeValue(forKey: subnetPrefix)
}
}
}
if candidates.isEmpty {
// Even though there is an interface bound to the subnet candidates, the collision probability
// with an actual address is low.
return kVpnSubnetCandidates.randomElement()?.value ?? ""

Check warning on line 135 in src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineTunnel.swift

View check run for this annotation

Codecov / codecov/patch

src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineTunnel.swift#L133-L135

Added lines #L133 - L135 were not covered by tests
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
}
// Select a random subnet from the remaining candidates.
return candidates.randomElement()?.value ?? ""
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
}

let kExcludedSubnets = [
"10.0.0.0/8",
"100.64.0.0/10",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.31.196.0/24",
"192.52.193.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"192.175.48.0/24",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"240.0.0.0/4"
]

func getExcludedIpv4Routes() -> [NEIPv4Route] {
var excludedIpv4Routes = [NEIPv4Route]()
for cidrSubnet in kExcludedSubnets {
if let subnet = Subnet.parse(cidrSubnet) {
let route = NEIPv4Route(destinationAddress: subnet.address, subnetMask: subnet.mask)
excludedIpv4Routes.append(route)
}
}
return excludedIpv4Routes
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,6 @@ import Foundation
// target of the OutlineAppleLib Swift Package.
@objcMembers
public class Subnet: NSObject {
public static let kReservedSubnets = [
"10.0.0.0/8",
"100.64.0.0/10",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.31.196.0/24",
"192.52.193.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"192.175.48.0/24",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"240.0.0.0/4"
]

// Parses a CIDR subnet into a Subnet object. Returns nil on failure.
public static func parse(_ cidrSubnet: String) -> Subnet? {
let components = cidrSubnet.components(separatedBy: "/")
Expand All @@ -51,17 +33,6 @@ public class Subnet: NSObject {
return Subnet(address: components[0], prefix: prefix)
}

// Returns a list of reserved Subnets.
public static func getReservedSubnets() -> [Subnet] {
var subnets: [Subnet] = []
for cidrSubnet in kReservedSubnets {
if let subnet = self.parse(cidrSubnet) {
subnets.append(subnet)
}
}
return subnets
}

public var address: String
public var prefix: UInt16
public var mask: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
NSString *const kMessageKeyPort = @"port";
NSString *const kMessageKeyOnDemand = @"is-on-demand";
NSString *const kDefaultPathKey = @"defaultPath";
static NSDictionary *kVpnSubnetCandidates; // Subnets to bind the VPN.

@interface PacketTunnelProvider ()<Tun2socksTunWriter>
@property (nonatomic) NSString *hostNetworkAddress; // IP address of the host in the active network.
Expand Down Expand Up @@ -74,12 +73,6 @@
[DDLog addLogger:_fileLogger];

_tunnelStore = [[OutlineTunnelStore alloc] initWithAppGroup:appGroup];
kVpnSubnetCandidates = @{
@"10" : @"10.111.222.0",
@"172" : @"172.16.9.1",
@"192" : @"192.168.20.1",
@"169" : @"169.254.19.0"
};

_packetQueue = dispatch_queue_create("org.outline.ios.packetqueue", DISPATCH_QUEUE_SERIAL);

Expand Down Expand Up @@ -147,7 +140,7 @@
userInfo:nil]);
}

[self connectTunnel:[self getTunnelNetworkSettings]
[self connectTunnel:[OutlineTunnel getTunnelNetworkSettingsWithTunnelRemoteAddress:self.hostNetworkAddress]

Check warning on line 143 in src/cordova/apple/OutlineAppleLib/Sources/PacketTunnelProviderSources/PacketTunnelProvider.m

View check run for this annotation

Codecov / codecov/patch

src/cordova/apple/OutlineAppleLib/Sources/PacketTunnelProviderSources/PacketTunnelProvider.m#L143

Added line #L143 was not covered by tests
completion:^(NSError *_Nullable error) {
if (error != nil) {
[self execAppCallbackForAction:kActionStart errorCode:vpnPermissionNotGranted];
Expand Down Expand Up @@ -204,7 +197,7 @@
}
DDLogInfo(@"Received app message: %@", action);
void (^callbackWrapper)(NSNumber *) = ^void(NSNumber *errorCode) {
NSString *tunnelId;
NSString *tunnelId = @"";

Check warning on line 200 in src/cordova/apple/OutlineAppleLib/Sources/PacketTunnelProviderSources/PacketTunnelProvider.m

View check run for this annotation

Codecov / codecov/patch

src/cordova/apple/OutlineAppleLib/Sources/PacketTunnelProviderSources/PacketTunnelProvider.m#L200

Added line #L200 was not covered by tests
if (self.tunnelConfig != nil) {
tunnelId = self.tunnelConfig.id;
}
Expand Down Expand Up @@ -300,33 +293,6 @@
}];
}

- (NEPacketTunnelNetworkSettings *) getTunnelNetworkSettings {
NSString *vpnAddress = [self selectVpnAddress];
NEIPv4Settings *ipv4Settings = [[NEIPv4Settings alloc] initWithAddresses:@[ vpnAddress ]
subnetMasks:@[ @"255.255.255.0" ]];
ipv4Settings.includedRoutes = @[[NEIPv4Route defaultRoute]];
ipv4Settings.excludedRoutes = [self getExcludedIpv4Routes];

// The remote address is not used for routing, but for display in Settings > VPN > Outline.
NEPacketTunnelNetworkSettings *settings =
[[NEPacketTunnelNetworkSettings alloc] initWithTunnelRemoteAddress:self.hostNetworkAddress];
settings.IPv4Settings = ipv4Settings;
// Configure with Cloudflare, Quad9, and OpenDNS resolver addresses.
settings.DNSSettings = [[NEDNSSettings alloc]
initWithServers:@[ @"1.1.1.1", @"9.9.9.9", @"208.67.222.222", @"208.67.220.220" ]];
return settings;
}

- (NSArray *)getExcludedIpv4Routes {
NSMutableArray *excludedIpv4Routes = [[NSMutableArray alloc] init];
for (Subnet *subnet in [Subnet getReservedSubnets]) {
NEIPv4Route *route = [[NEIPv4Route alloc] initWithDestinationAddress:subnet.address
subnetMask:subnet.mask];
[excludedIpv4Routes addObject:route];
}
return excludedIpv4Routes;
}

// Registers KVO for the `defaultPath` property to receive network connectivity changes.
- (void)listenForNetworkChanges {
[self stopListeningForNetworkChanges];
Expand Down Expand Up @@ -435,54 +401,6 @@
return [NSString stringWithUTF8String:networkAddress];
}

- (NSArray *)getNetworkInterfaceAddresses {
struct ifaddrs *interfaces = nil;
NSMutableArray *addresses = [NSMutableArray new];
if (getifaddrs(&interfaces) != 0) {
DDLogError(@"Failed to retrieve network interface addresses");
return addresses;
}
struct ifaddrs *interface = interfaces;
while (interface != nil) {
if (interface->ifa_addr->sa_family == AF_INET) {
// Only consider IPv4 interfaces.
NSString *address = [NSString
stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)interface->ifa_addr)->sin_addr)];
[addresses addObject:address];
}
interface = interface->ifa_next;
}
freeifaddrs(interfaces);

return addresses;
}

// Selects an IPv4 address for the VPN to bind to from a pool of private subnets by checking against
// the subnets assigned to the existing network interfaces.
- (NSString *)selectVpnAddress {
NSMutableDictionary *candidates =
[[NSMutableDictionary alloc] initWithDictionary:kVpnSubnetCandidates];
for (NSString *address in [self getNetworkInterfaceAddresses]) {
for (NSString *subnetPrefix in kVpnSubnetCandidates) {
if ([address hasPrefix:subnetPrefix]) {
// The subnet (not necessarily the address) is in use, remove it from our list.
[candidates removeObjectForKey:subnetPrefix];
}
}
}
if (candidates.count == 0) {
// Even though there is an interface bound to the subnet candidates, the collision probability
// with an actual address is low.
return [self selectRandomValueFromDictionary:kVpnSubnetCandidates];
}
// Select a random subnet from the remaining candidates.
return [self selectRandomValueFromDictionary:candidates];
}

- (id)selectRandomValueFromDictionary:(NSDictionary *)dict {
return [dict.allValues objectAtIndex:(arc4random_uniform((uint32_t)dict.count))];
}

#pragma mark - tun2socks

// Restarts tun2socks if |configChanged| or the host's IP address has changed in the network.
Expand All @@ -501,7 +419,7 @@
}
if (!configChanged && [activeHostNetworkAddress isEqualToString:self.hostNetworkAddress]) {
// Nothing changed. Connect the tunnel with the current settings.
[self connectTunnel:[self getTunnelNetworkSettings]
[self connectTunnel:[OutlineTunnel getTunnelNetworkSettingsWithTunnelRemoteAddress:self.hostNetworkAddress]

Check warning on line 422 in src/cordova/apple/OutlineAppleLib/Sources/PacketTunnelProviderSources/PacketTunnelProvider.m

View check run for this annotation

Codecov / codecov/patch

src/cordova/apple/OutlineAppleLib/Sources/PacketTunnelProviderSources/PacketTunnelProvider.m#L422

Added line #L422 was not covered by tests
completion:^(NSError *_Nullable error) {
if (error != nil) {
[self cancelTunnelWithError:error];
Expand Down Expand Up @@ -539,7 +457,7 @@
userInfo:nil]];
return;
}
[self connectTunnel:[self getTunnelNetworkSettings]
[self connectTunnel:[OutlineTunnel getTunnelNetworkSettingsWithTunnelRemoteAddress:self.hostNetworkAddress]

Check warning on line 460 in src/cordova/apple/OutlineAppleLib/Sources/PacketTunnelProviderSources/PacketTunnelProvider.m

View check run for this annotation

Codecov / codecov/patch

src/cordova/apple/OutlineAppleLib/Sources/PacketTunnelProviderSources/PacketTunnelProvider.m#L460

Added line #L460 was not covered by tests
completion:^(NSError *_Nullable error) {
if (error != nil) {
[self execAppCallbackForAction:kActionStart errorCode:vpnStartFailure];
Expand Down
Loading
Loading