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
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
6 changes: 4 additions & 2 deletions src/cordova/apple/OutlineAppleLib/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ let package = Package(
targets: ["Tun2socks", "OutlineTunnel", "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"),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exact directive prevented the XCode project from upgrading the dependencies.

],
targets: [
.target(
Expand All @@ -29,7 +29,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())
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

@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 {
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -108,15 +106,15 @@ class OutlineVpn: NSObject {
}

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