Skip to content

Commit

Permalink
refactor(cordova/apple): move network settings logic to Swift (#1658)
Browse files Browse the repository at this point in the history
  • Loading branch information
fortuna authored Jul 7, 2023
1 parent 97dae88 commit bccebce
Show file tree
Hide file tree
Showing 20 changed files with 264 additions and 367 deletions.
1 change: 1 addition & 0 deletions src/build/spawn_stream.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const spawnStream = (command, ...parameters) =>
const stdout = [];
const stderr = [];

console.debug(`Running [${[command, ...parameters.map(e => `'${e}'`)].join(' ')}]`);
const childProcess = spawn(command, parameters, {env: process.env});

const forEachMessageLine = (buffer, callback) => {
Expand Down
11 changes: 8 additions & 3 deletions src/cordova/apple/OutlineAppleLib/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ let package = Package(
products: [
.library(
name: "OutlineAppleLib",
targets: ["Tun2socks", "OutlineTunnel", "PacketTunnelProvider"]),
targets: ["Tun2socks", "OutlineTunnel"]),
.library(
name: "PacketTunnelProvider",
targets: ["PacketTunnelProvider"]),
],
dependencies: [
.package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack.git", exact: "3.7.4"),
.package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack.git", from: "3.7.4"),
],
targets: [
.target(
Expand All @@ -29,7 +32,9 @@ let package = Package(
),
.target(
name: "OutlineTunnel",
dependencies: [],
dependencies: [
.product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"),
],
path: "Sources/OutlineTunnelSources"
),
.binaryTarget(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import CocoaLumberjack
import CocoaLumberjackSwift
import Tun2socks

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,54 +13,158 @@
// limitations under the License.

import Foundation
import NetworkExtension

import CocoaLumberjackSwift

// 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]
}

@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)})
}
}

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

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

// 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)

// Configure VPN address and routing.
let vpnAddress = selectVpnAddress(interfaceAddresses: getNetworkInterfaceAddresses())
let ipv4Settings = NEIPv4Settings(addresses: [vpnAddress], subnetMasks: ["255.255.255.0"])
ipv4Settings.includedRoutes = [NEIPv4Route.default()]
ipv4Settings.excludedRoutes = getExcludedIpv4Routes()
settings.ipv4Settings = ipv4Settings

@objc
public enum TunnelStatus: Int {
case connected = 0
case disconnected = 1
case reconnecting = 2
}
// 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
}
}

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

guard getifaddrs(&interfaces) == 0 else {
DDLogError("Failed to retrieve network interface addresses")
return addresses
}
if let prefix = config["prefix"] as? String {
self.prefix = Data(prefix.utf16.map{UInt8($0)})

var interface = interfaces
while interface != nil {
// Only consider IPv4 interfaces.
if interface!.pointee.ifa_addr.pointee.sa_family == UInt8(AF_INET) {
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: .utf8) {
addresses.append(ip)
}
}
interface = interface!.pointee.ifa_next
}
}

freeifaddrs(interfaces)

return addresses
}

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 func encode() -> Data? {
return try? JSONEncoder().encode(self)
}
// 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)
}
}
}
guard !candidates.isEmpty else {
// Even though there is an interface bound to the subnet candidates, the collision probability
// with an actual address is low.
return kVpnSubnetCandidates.randomElement()!.value
}
// Select a random subnet from the remaining candidates.
return candidates.randomElement()!.value
}

public static func decode(_ jsonData: Data) -> OutlineTunnel? {
return try? JSONDecoder().decode(OutlineTunnel.self, from: jsonData)
}
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 @@ -12,19 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import CocoaLumberjack
import CocoaLumberjackSwift
import NetworkExtension
import OutlineTunnel

// Manages the system's VPN tunnel through the VpnExtension process.
@objcMembers
class OutlineVpn: NSObject {
static let shared = OutlineVpn()
public class OutlineVpn: NSObject {
public static let shared = OutlineVpn()
private static let kVpnExtensionBundleId = "\(Bundle.main.bundleIdentifier!).VpnExtension"

typealias Callback = (ErrorCode) -> Void
typealias VpnStatusObserver = (NEVPNStatus, String) -> Void
public typealias Callback = (ErrorCode) -> Void
public typealias VpnStatusObserver = (NEVPNStatus, String) -> Void

public private(set) var activeTunnelId: String?
private var tunnelManager: NETunnelProviderManager?
Expand Down Expand Up @@ -87,7 +85,7 @@ class OutlineVpn: NSObject {
// MARK: Interface

// Starts a VPN tunnel as specified in the OutlineTunnel object.
func start(_ tunnel: OutlineTunnel, _ completion: @escaping (Callback)) {
public func start(_ tunnel: OutlineTunnel, _ completion: @escaping (Callback)) {
guard let tunnelId = tunnel.id else {
DDLogError("Missing tunnel ID")
return completion(ErrorCode.illegalServerConfiguration)
Expand All @@ -101,22 +99,22 @@ class OutlineVpn: NSObject {
}

// Starts the last successful VPN tunnel.
func startLastSuccessfulTunnel(_ completion: @escaping (Callback)) {
@objc public func startLastSuccessfulTunnel(_ completion: @escaping (Callback)) {
// Explicitly pass an empty tunnel's configuration, so the VpnExtension process retrieves
// the last configuration from disk.
self.startVpn(OutlineTunnel(), isAutoConnect: true, completion)
}

// Tears down the VPN if the tunnel with id |tunnelId| is active.
func stop(_ tunnelId: String) {
public func stop(_ tunnelId: String) {
if !isActive(tunnelId) {
return DDLogWarn("Cannot stop VPN, tunnel ID \(tunnelId)")
}
stopVpn()
}

// Determines whether a server is reachable via TCP.
func isServerReachable(host: String, port: UInt16, _ completion: @escaping Callback) {
public func isServerReachable(host: String, port: UInt16, _ completion: @escaping Callback) {
if isVpnConnected() {
// All the device's traffic, including the Outline app, go through the VpnExtension process.
// Performing a reachability test, opening a TCP socket to a host/port, will succeed
Expand All @@ -139,12 +137,12 @@ class OutlineVpn: NSObject {
}

// Calls |observer| when the VPN's status changes.
func onVpnStatusChange(_ observer: @escaping(VpnStatusObserver)) {
public func onVpnStatusChange(_ observer: @escaping(VpnStatusObserver)) {
vpnStatusObserver = observer
}

// Returns whether |tunnelId| is actively proxying through the VPN.
func isActive(_ tunnelId: String?) -> Bool {
public func isActive(_ tunnelId: String?) -> Bool {
if self.activeTunnelId == nil {
return false
}
Expand Down Expand Up @@ -310,7 +308,7 @@ class OutlineVpn: NSObject {

// Receives NEVPNStatusDidChange notifications. Calls onTunnelStatusChange for the active
// tunnel.
@objc func vpnStatusChanged() {
func vpnStatusChanged() {
if let vpnStatus = tunnelManager?.connection.status {
if let tunnelId = activeTunnelId {
if (vpnStatus == .disconnected) {
Expand Down
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
Loading

0 comments on commit bccebce

Please sign in to comment.