From 2ba612e8512fe2b23c4df1e8aac1c6692920b6d8 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Fri, 7 Jul 2023 21:15:34 -0400 Subject: [PATCH] Simplify OutlineAppleLib --- .../OutlineConnectivity.swift | 72 ------------------- .../OutlineTunnelSources/OutlineVpn.swift | 42 +++++------ .../OutlineTunnelTest/OutlineTunnelTest.swift | 8 +++ .../plugin/apple/src/OutlinePlugin.swift | 12 ++-- 4 files changed, 33 insertions(+), 101 deletions(-) delete mode 100644 src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineConnectivity.swift diff --git a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineConnectivity.swift b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineConnectivity.swift deleted file mode 100644 index b9595b0a7e..0000000000 --- a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineConnectivity.swift +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2018 The Outline Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import CocoaLumberjackSwift -import Tun2socks - -// Class to perform connectivity tests against remote Outline servers. -class OutlineConnectivity: NSObject { - - // Asynchronously calls |completion| with a boolean representing whether |host| and |port| - // are reachable. - func isServerReachable(host: String, port: UInt16, completion: @escaping((Bool) -> Void)) { - DispatchQueue.global(qos: .background).async { - guard let networkIp = self.getNetworkIpAddress(host) else { - DDLogError("Failed to retrieve the remote host IP address in the network"); - return completion(false) - } - completion(ShadowsocksCheckServerReachable(networkIp, Int(port), nil)) - } - } - - // Calls getaddrinfo to retrieve the IP address literal as a string for |ipv4Address| in the - // active network. This is necessary to support IPv6 DNS64/NAT64 networks. For more details see: - // https://developer.apple.com/library/content/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/UnderstandingandPreparingfortheIPv6Transition/UnderstandingandPreparingfortheIPv6Transition.html - private func getNetworkIpAddress(_ ipv4Address: String) -> String? { - var hints = addrinfo( - ai_flags: AI_DEFAULT, - ai_family: AF_UNSPEC, - ai_socktype: SOCK_STREAM, - ai_protocol: 0, - ai_addrlen: 0, - ai_canonname: nil, - ai_addr: nil, - ai_next: nil) - var info: UnsafeMutablePointer? - let err = getaddrinfo(ipv4Address, nil, &hints, &info) - if err != 0 { - DDLogError("getaddrinfo failed \(String(describing: gai_strerror(err)))") - return nil - } - defer { - freeaddrinfo(info) - } - return getIpAddressString(addr: info?.pointee.ai_addr) - } - - private func getIpAddressString(addr: UnsafePointer?) -> String? { - guard addr != nil else { - DDLogError("Failed to get IP address string: invalid argument") - return nil - } - var host : String? - var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) - let err = getnameinfo(addr, socklen_t(addr!.pointee.sa_len), &buffer, socklen_t(buffer.count), - nil, 0, NI_NUMERICHOST | NI_NUMERICSERV) - if err == 0 { - host = String(cString: buffer) - } - return host - } -} diff --git a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineVpn.swift b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineVpn.swift index 45f191a979..04f3825179 100644 --- a/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineVpn.swift +++ b/src/cordova/apple/OutlineAppleLib/Sources/OutlineTunnelSources/OutlineVpn.swift @@ -14,6 +14,7 @@ import CocoaLumberjackSwift import NetworkExtension +import Tun2socks // Manages the system's VPN tunnel through the VpnExtension process. @objcMembers @@ -27,7 +28,6 @@ public class OutlineVpn: NSObject { public private(set) var activeTunnelId: String? private var tunnelManager: NETunnelProviderManager? private var vpnStatusObserver: VpnStatusObserver? - private let connectivity: OutlineConnectivity private enum Action { static let start = "start" @@ -68,7 +68,6 @@ public class OutlineVpn: NSObject { } override private init() { - connectivity = OutlineConnectivity() super.init() getTunnelManager() { manager in guard manager != nil else { @@ -85,24 +84,21 @@ public class OutlineVpn: NSObject { // MARK: Interface // Starts a VPN tunnel as specified in the OutlineTunnel object. - public func start(_ tunnel: OutlineTunnel, _ completion: @escaping (Callback)) { - guard let tunnelId = tunnel.id else { - DDLogError("Missing tunnel ID") - return completion(ErrorCode.illegalServerConfiguration) - } - if isActive(tunnelId) { + public func start(_ tunnelId: String, configJson: [String: Any], _ completion: @escaping (Callback)) { + guard !isActive(tunnelId) else { return completion(ErrorCode.noError) - } else if isVpnConnected() { - return restartVpn(tunnelId, config: tunnel.config, completion: completion) } - self.startVpn(tunnel, isAutoConnect: false, completion) + if isVpnConnected() { + return restartVpn(tunnelId, configJson: configJson, completion: completion) + } + self.startVpn(tunnelId, configJson: configJson, isAutoConnect: false, completion) } // Starts the last successful VPN tunnel. @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) + self.startVpn(nil, configJson:nil, isAutoConnect: true, completion) } // Tears down the VPN if the tunnel with id |tunnelId| is active. @@ -130,7 +126,8 @@ public class OutlineVpn: NSObject { completion(ErrorCode(rawValue: rawCode) ?? ErrorCode.serverUnreachable) } } else { - connectivity.isServerReachable(host: host, port: port) { isReachable in + DispatchQueue.global(qos: .background).async { + let isReachable = ShadowsocksCheckServerReachable(host, Int(port), nil) completion(isReachable ? ErrorCode.noError : ErrorCode.serverUnreachable) } } @@ -151,9 +148,7 @@ public class OutlineVpn: NSObject { // MARK: Helpers - private func startVpn( - _ tunnel: OutlineTunnel, isAutoConnect: Bool, _ completion: @escaping(Callback)) { - let tunnelId = tunnel.id + private func startVpn(_ tunnelId: String?, configJson: [String: Any]?, isAutoConnect: Bool, _ completion: @escaping(Callback)) { setupVpn() { error in if error != nil { DDLogError("Failed to setup VPN: \(String(describing: error))") @@ -163,17 +158,18 @@ public class OutlineVpn: NSObject { self.sendVpnExtensionMessage(message) { response in self.onStartVpnExtensionMessage(response, completion: completion) } - var config: [String: String]? = nil + var tunnelOptions: [String: Any]? = nil if !isAutoConnect { - config = tunnel.config - config?[MessageKey.tunnelId] = tunnelId + // TODO(fortuna): put this in a subkey + tunnelOptions = configJson + tunnelOptions?[MessageKey.tunnelId] = tunnelId } else { // macOS app was started by launcher. - config = [MessageKey.isOnDemand: "true"]; + tunnelOptions = [MessageKey.isOnDemand: "true"]; } let session = self.tunnelManager?.connection as! NETunnelProviderSession do { - try session.startTunnel(options: config) + try session.startTunnel(options: tunnelOptions) } catch let error as NSError { DDLogError("Failed to start VPN: \(error)") completion(ErrorCode.vpnStartFailure) @@ -189,13 +185,13 @@ public class OutlineVpn: NSObject { } // Sends message to extension to restart the tunnel without tearing down the VPN. - private func restartVpn(_ tunnelId: String, config: [String: String], + private func restartVpn(_ tunnelId: String, configJson: [String: Any], completion: @escaping(Callback)) { if activeTunnelId != nil { vpnStatusObserver?(.disconnected, activeTunnelId!) } let message = [MessageKey.action: Action.restart, MessageKey.tunnelId: tunnelId, - MessageKey.config:config] as [String : Any] + MessageKey.config: configJson] as [String : Any] self.sendVpnExtensionMessage(message) { response in self.onStartVpnExtensionMessage(response, completion: completion) } diff --git a/src/cordova/apple/OutlineAppleLib/Tests/OutlineTunnelTest/OutlineTunnelTest.swift b/src/cordova/apple/OutlineAppleLib/Tests/OutlineTunnelTest/OutlineTunnelTest.swift index 01c6900e34..344b277553 100644 --- a/src/cordova/apple/OutlineAppleLib/Tests/OutlineTunnelTest/OutlineTunnelTest.swift +++ b/src/cordova/apple/OutlineAppleLib/Tests/OutlineTunnelTest/OutlineTunnelTest.swift @@ -1,6 +1,7 @@ import XCTest import NetworkExtension +import Tun2socks @testable import OutlineTunnel @testable import PacketTunnelProvider @@ -37,4 +38,11 @@ final class OutlineTunnelTest: XCTestCase { XCTAssertEqual(["1.1.1.1", "9.9.9.9", "208.67.222.222", "208.67.220.220"], settings.dnsSettings?.servers) } + + func testReachability() { + // TODO(fortuna): run a local server instead. + XCTAssertTrue(ShadowsocksCheckServerReachable("8.8.8.8", 853, nil)) + XCTAssertTrue(ShadowsocksCheckServerReachable("google.com", 443, nil)) + XCTAssertFalse(ShadowsocksCheckServerReachable("nonexistent.getoutline.org", 443, nil)) + } } diff --git a/src/cordova/plugin/apple/src/OutlinePlugin.swift b/src/cordova/plugin/apple/src/OutlinePlugin.swift index 386bfbf5a7..4b8ea2df53 100644 --- a/src/cordova/plugin/apple/src/OutlinePlugin.swift +++ b/src/cordova/plugin/apple/src/OutlinePlugin.swift @@ -76,12 +76,12 @@ class OutlinePlugin: CDVPlugin { errorCode: OutlineVpn.ErrorCode.illegalServerConfiguration) } DDLogInfo("\(Action.start) \(tunnelId)") - guard let config = command.argument(at: 1) as? [String: Any], containsExpectedKeys(config) else { + // TODO(fortuna): Move the config validation to the config parsing code in Go. + guard let configJson = command.argument(at: 1) as? [String: Any], containsExpectedKeys(configJson) else { return sendError("Invalid configuration", callbackId: command.callbackId, errorCode: OutlineVpn.ErrorCode.illegalServerConfiguration) } - let tunnel = OutlineTunnel(id: tunnelId, config: config) - OutlineVpn.shared.start(tunnel) { errorCode in + OutlineVpn.shared.start(tunnelId, configJson:configJson) { errorCode in if errorCode == OutlineVpn.ErrorCode.noError { #if os(macOS) NotificationCenter.default.post( @@ -234,9 +234,9 @@ class OutlinePlugin: CDVPlugin { } // Returns whether |config| contains all the expected keys - private func containsExpectedKeys(_ config: [String: Any]?) -> Bool { - return config?["host"] != nil && config?["port"] != nil && - config?["password"] != nil && config?["method"] != nil + private func containsExpectedKeys(_ configJson: [String: Any]?) -> Bool { + return configJson?["host"] != nil && configJson?["port"] != nil && + configJson?["password"] != nil && configJson?["method"] != nil } // MARK: Callback helpers