Skip to content

Commit

Permalink
Merge branch 'daniellacosse/macos-warning' of https://github.com/Jigs…
Browse files Browse the repository at this point in the history
…aw-Code/outline-client into daniellacosse/macos-warning
  • Loading branch information
daniellacosse committed Jul 11, 2023
2 parents 8f41d63 + c385d04 commit 2470b9d
Show file tree
Hide file tree
Showing 21 changed files with 296 additions and 467 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 @@ -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,24 +12,22 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import CocoaLumberjack
import CocoaLumberjackSwift
import NetworkExtension
import OutlineTunnel
import Tun2socks

// 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?
private var vpnStatusObserver: VpnStatusObserver?
private let connectivity: OutlineConnectivity

private enum Action {
static let start = "start"
Expand Down Expand Up @@ -70,7 +68,6 @@ class OutlineVpn: NSObject {
}

override private init() {
connectivity = OutlineConnectivity()
super.init()
getTunnelManager() { manager in
guard manager != nil else {
Expand All @@ -87,36 +84,33 @@ class OutlineVpn: NSObject {
// MARK: Interface

// Starts a VPN tunnel as specified in the OutlineTunnel object.
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.
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)
self.startVpn(nil, configJson:nil, 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 @@ -132,19 +126,20 @@ 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)
}
}
}

// 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 All @@ -153,9 +148,7 @@ 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))")
Expand All @@ -165,17 +158,18 @@ 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)
Expand All @@ -191,13 +185,13 @@ 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)
}
Expand Down Expand Up @@ -310,7 +304,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
Loading

0 comments on commit 2470b9d

Please sign in to comment.