Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6767d9007 | |||
| bb5760cca4 | |||
| 26b7971ba6 | |||
| b286ede3c6 | |||
| 4ef7afe3ca | |||
| 377f2f0496 | |||
| 7ed5893fc6 | |||
| e70c397e54 | |||
| 6a6be9edde | |||
| 37f8500fe6 | |||
| 207f82dd9d | |||
| de8fedf87a | |||
| 98d306da5b | |||
| c7b7b1247b | |||
| a66f13eb01 | |||
| f50e7ae686 | |||
| 4d6692548c | |||
| 1dccd39818 | |||
| 4cb775c72f | |||
| 4cb783c447 |
@@ -1,12 +1,14 @@
|
|||||||
# [WireGuard](https://www.wireguard.com/) for iOS and macOS
|
# [WireGuard](https://www.wireguard.com/) for iOS and macOS
|
||||||
|
|
||||||
|
This project contains an application for iOS and for macOS, as well as many components shared between the two of them. You may toggle between the two platforms by selecting the target from within Xcode.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
- Clone this repo recursively:
|
- Clone this repo:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git clone --recursive https://git.zx2c4.com/wireguard-ios
|
$ git clone https://git.zx2c4.com/wireguard-apple
|
||||||
$ cd wireguard-ios
|
$ cd wireguard-apple
|
||||||
```
|
```
|
||||||
|
|
||||||
- Rename and populate developer team ID file:
|
- Rename and populate developer team ID file:
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ extension NETunnelProviderProtocol {
|
|||||||
if passwordReference == nil {
|
if passwordReference == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
providerConfiguration = ["UID": getuid()]
|
||||||
|
#endif
|
||||||
|
|
||||||
let endpoints = tunnelConfiguration.peers.compactMap { $0.endpoint }
|
let endpoints = tunnelConfiguration.peers.compactMap { $0.endpoint }
|
||||||
if endpoints.count == 1 {
|
if endpoints.count == 1 {
|
||||||
@@ -60,11 +63,25 @@ extension NETunnelProviderProtocol {
|
|||||||
* in the keychain. But it's still useful to keep the migration
|
* in the keychain. But it's still useful to keep the migration
|
||||||
* around so that .mobileconfig files are easier.
|
* around so that .mobileconfig files are easier.
|
||||||
*/
|
*/
|
||||||
guard let oldConfig = providerConfiguration?["WgQuickConfig"] as? String else { return false }
|
if let oldConfig = providerConfiguration?["WgQuickConfig"] as? String {
|
||||||
providerConfiguration = nil
|
#if os(macOS)
|
||||||
guard passwordReference == nil else { return true }
|
providerConfiguration = ["UID": getuid()]
|
||||||
wg_log(.debug, message: "Migrating tunnel configuration '\(name)'")
|
#elseif os(iOS)
|
||||||
passwordReference = Keychain.makeReference(containing: oldConfig, called: name)
|
providerConfiguration = nil
|
||||||
return true
|
#else
|
||||||
|
#error("Unimplemented")
|
||||||
|
#endif
|
||||||
|
guard passwordReference == nil else { return true }
|
||||||
|
wg_log(.debug, message: "Migrating tunnel configuration '\(name)'")
|
||||||
|
passwordReference = Keychain.makeReference(containing: oldConfig, called: name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
if passwordReference != nil && providerConfiguration?["UID"] == nil && verifyConfigurationReference() {
|
||||||
|
providerConfiguration = ["UID": getuid()]
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ extension TunnelConfiguration {
|
|||||||
var interfaceConfiguration: InterfaceConfiguration?
|
var interfaceConfiguration: InterfaceConfiguration?
|
||||||
var peerConfigurations = [PeerConfiguration]()
|
var peerConfigurations = [PeerConfiguration]()
|
||||||
|
|
||||||
let lines = wgQuickConfig.split(separator: "\n")
|
let lines = wgQuickConfig.split { $0.isNewline }
|
||||||
|
|
||||||
var parserState = ParserState.notInASection
|
var parserState = ParserState.notInASection
|
||||||
var attributes = [String: String]()
|
var attributes = [String: String]()
|
||||||
|
|||||||
@@ -308,7 +308,6 @@
|
|||||||
"macMenuHideApp" = "Hide WireGuard";
|
"macMenuHideApp" = "Hide WireGuard";
|
||||||
"macMenuHideOtherApps" = "Hide Others";
|
"macMenuHideOtherApps" = "Hide Others";
|
||||||
"macMenuShowAllApps" = "Show All";
|
"macMenuShowAllApps" = "Show All";
|
||||||
"macMenuQuitManagingTunnels" = "Quit Tunnel Manager";
|
|
||||||
|
|
||||||
"macMenuFile" = "File";
|
"macMenuFile" = "File";
|
||||||
"macMenuCloseWindow" = "Close Window";
|
"macMenuCloseWindow" = "Close Window";
|
||||||
@@ -406,10 +405,14 @@
|
|||||||
|
|
||||||
// Mac alert
|
// Mac alert
|
||||||
|
|
||||||
|
"macConfirmAndQuitAlertMessage" = "Do you want to close the tunnels manager or quit WireGuard entirely?";
|
||||||
|
"macConfirmAndQuitAlertInfo" = "If you close the tunnels manager, WireGuard will continue to be available from the menu bar icon.";
|
||||||
|
"macConfirmAndQuitInfoWithActiveTunnel (%@)" = "If you close the tunnels manager, WireGuard will continue to be available from the menu bar icon.\n\nNote that if you quit WireGuard entirely the currently active tunnel ('%@') will still remain active until you deactivate it from this application or through the Network panel in System Preferences.";
|
||||||
|
"macConfirmAndQuitAlertQuitWireGuard" = "Quit WireGuard";
|
||||||
|
"macConfirmAndQuitAlertCloseWindow" = "Close Tunnels Manager";
|
||||||
|
|
||||||
"macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel";
|
"macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel";
|
||||||
"macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences.";
|
"macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences.";
|
||||||
"macPrivacyNoticeMessage" = "Privacy notice: be sure you trust this configuration file";
|
|
||||||
"macPrivacyNoticeInfo" = "You will be prompted by the system to allow or disallow adding a VPN configuration. While this application does not send any information to the WireGuard project, information is by design sent to the servers specified inside of the configuration file you have just added, which configures your computer to use those servers as a VPN. Be certain that you trust this configuration before clicking “Allow” in the following dialog.";
|
|
||||||
|
|
||||||
// Mac tooltip
|
// Mac tooltip
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
VERSION_NAME = 0.0.20190531
|
VERSION_NAME = 0.0.20190610
|
||||||
VERSION_ID = 9
|
VERSION_ID = 13
|
||||||
|
|||||||
@@ -46,46 +46,59 @@ extension ActivateOnDemandOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(from tunnelProviderManager: NETunnelProviderManager) {
|
init(from tunnelProviderManager: NETunnelProviderManager) {
|
||||||
let rules = tunnelProviderManager.onDemandRules ?? []
|
if tunnelProviderManager.isOnDemandEnabled, let onDemandRules = tunnelProviderManager.onDemandRules {
|
||||||
let activateOnDemandOption: ActivateOnDemandOption
|
self = ActivateOnDemandOption.create(from: onDemandRules)
|
||||||
|
} else {
|
||||||
|
self = .off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func create(from rules: [NEOnDemandRule]) -> ActivateOnDemandOption {
|
||||||
switch rules.count {
|
switch rules.count {
|
||||||
case 0:
|
case 0:
|
||||||
activateOnDemandOption = .off
|
return .off
|
||||||
case 1:
|
case 1:
|
||||||
let rule = rules[0]
|
let rule = rules[0]
|
||||||
precondition(rule.action == .connect)
|
guard rule.action == .connect else { return .off }
|
||||||
activateOnDemandOption = .anyInterface(.anySSID)
|
return .anyInterface(.anySSID)
|
||||||
case 2:
|
case 2:
|
||||||
let connectRule = rules.first(where: { $0.action == .connect })!
|
guard let connectRule = rules.first(where: { $0.action == .connect }) else {
|
||||||
let disconnectRule = rules.first(where: { $0.action == .disconnect })!
|
wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but no connect rule.")
|
||||||
|
return .off
|
||||||
|
}
|
||||||
|
guard let disconnectRule = rules.first(where: { $0.action == .disconnect }) else {
|
||||||
|
wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but no disconnect rule.")
|
||||||
|
return .off
|
||||||
|
}
|
||||||
if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == nonWiFiInterfaceType {
|
if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == nonWiFiInterfaceType {
|
||||||
activateOnDemandOption = .wiFiInterfaceOnly(.anySSID)
|
return .wiFiInterfaceOnly(.anySSID)
|
||||||
} else if connectRule.interfaceTypeMatch == nonWiFiInterfaceType && disconnectRule.interfaceTypeMatch == .wiFi {
|
} else if connectRule.interfaceTypeMatch == nonWiFiInterfaceType && disconnectRule.interfaceTypeMatch == .wiFi {
|
||||||
activateOnDemandOption = .nonWiFiInterfaceOnly
|
return .nonWiFiInterfaceOnly
|
||||||
} else {
|
} else {
|
||||||
fatalError("Unexpected onDemandRules set on tunnel provider manager")
|
wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but interface types are inconsistent.")
|
||||||
|
return .off
|
||||||
}
|
}
|
||||||
case 3:
|
case 3:
|
||||||
let ssidRule = rules.first(where: { $0.interfaceTypeMatch == .wiFi && $0.ssidMatch != nil })!
|
guard let ssidRule = rules.first(where: { $0.interfaceTypeMatch == .wiFi && $0.ssidMatch != nil }) else { return .off }
|
||||||
let nonWiFiRule = rules.first(where: { $0.interfaceTypeMatch == nonWiFiInterfaceType })!
|
guard let nonWiFiRule = rules.first(where: { $0.interfaceTypeMatch == nonWiFiInterfaceType }) else { return .off }
|
||||||
let ssids = ssidRule.ssidMatch!
|
let ssids = ssidRule.ssidMatch!
|
||||||
switch (ssidRule.action, nonWiFiRule.action) {
|
switch (ssidRule.action, nonWiFiRule.action) {
|
||||||
case (.connect, .connect):
|
case (.connect, .connect):
|
||||||
activateOnDemandOption = .anyInterface(.onlySpecificSSIDs(ssids))
|
return .anyInterface(.onlySpecificSSIDs(ssids))
|
||||||
case (.connect, .disconnect):
|
case (.connect, .disconnect):
|
||||||
activateOnDemandOption = .wiFiInterfaceOnly(.onlySpecificSSIDs(ssids))
|
return .wiFiInterfaceOnly(.onlySpecificSSIDs(ssids))
|
||||||
case (.disconnect, .connect):
|
case (.disconnect, .connect):
|
||||||
activateOnDemandOption = .anyInterface(.exceptSpecificSSIDs(ssids))
|
return .anyInterface(.exceptSpecificSSIDs(ssids))
|
||||||
case (.disconnect, .disconnect):
|
case (.disconnect, .disconnect):
|
||||||
activateOnDemandOption = .wiFiInterfaceOnly(.exceptSpecificSSIDs(ssids))
|
return .wiFiInterfaceOnly(.exceptSpecificSSIDs(ssids))
|
||||||
default:
|
default:
|
||||||
fatalError("Unexpected SSID onDemandRules set on tunnel provider manager")
|
wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found")
|
||||||
|
return .off
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
fatalError("Unexpected number of onDemandRules set on tunnel provider manager")
|
wg_log(.error, message: "Unexpected number of onDemandRules set on tunnel provider manager: \(rules.count) rules found")
|
||||||
|
return .off
|
||||||
}
|
}
|
||||||
|
|
||||||
self = activateOnDemandOption
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,12 @@ class TunnelsManager {
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
let passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
|
let passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
|
||||||
#elseif os(macOS)
|
#elseif os(macOS)
|
||||||
let passwordRef = proto.passwordReference // To handle multiple users in macOS, we skip verifying
|
let passwordRef: Data?
|
||||||
|
if proto.providerConfiguration?["UID"] as? uid_t == getuid() {
|
||||||
|
passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
|
||||||
|
} else {
|
||||||
|
passwordRef = proto.passwordReference // To handle multiple users in macOS, we skip verifying
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
#error("Unimplemented")
|
#error("Unimplemented")
|
||||||
#endif
|
#endif
|
||||||
@@ -262,10 +267,15 @@ class TunnelsManager {
|
|||||||
|
|
||||||
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||||
let tunnelProviderManager = tunnel.tunnelProvider
|
let tunnelProviderManager = tunnel.tunnelProvider
|
||||||
if tunnel.isTunnelConfigurationAvailableInKeychain {
|
#if os(macOS)
|
||||||
|
if tunnel.isTunnelAvailableToUser {
|
||||||
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
|
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
|
||||||
}
|
}
|
||||||
|
#elseif os(iOS)
|
||||||
|
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
|
||||||
|
#else
|
||||||
|
#error("Unimplemented")
|
||||||
|
#endif
|
||||||
tunnelProviderManager.removeFromPreferences { [weak self] error in
|
tunnelProviderManager.removeFromPreferences { [weak self] error in
|
||||||
guard error == nil else {
|
guard error == nil else {
|
||||||
wg_log(.error, message: "Remove: Saving configuration failed: \(error!)")
|
wg_log(.error, message: "Remove: Saving configuration failed: \(error!)")
|
||||||
@@ -493,14 +503,16 @@ class TunnelContainer: NSObject {
|
|||||||
return tunnelProvider.tunnelConfiguration
|
return tunnelProvider.tunnelConfiguration
|
||||||
}
|
}
|
||||||
|
|
||||||
var isTunnelConfigurationAvailableInKeychain: Bool {
|
|
||||||
return tunnelProvider.isTunnelConfigurationAvailableInKeychain
|
|
||||||
}
|
|
||||||
|
|
||||||
var onDemandOption: ActivateOnDemandOption {
|
var onDemandOption: ActivateOnDemandOption {
|
||||||
return ActivateOnDemandOption(from: tunnelProvider)
|
return ActivateOnDemandOption(from: tunnelProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
var isTunnelAvailableToUser: Bool {
|
||||||
|
return (tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration?["UID"] as? uid_t == getuid()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
init(tunnel: NETunnelProviderManager) {
|
init(tunnel: NETunnelProviderManager) {
|
||||||
name = tunnel.localizedDescription ?? "Unnamed"
|
name = tunnel.localizedDescription ?? "Unnamed"
|
||||||
let status = TunnelStatus(from: tunnel.connection.status)
|
let status = TunnelStatus(from: tunnel.connection.status)
|
||||||
@@ -609,18 +621,8 @@ class TunnelContainer: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension NETunnelProviderManager {
|
extension NETunnelProviderManager {
|
||||||
private static var cachedIsConfigAvailableInKeychainKey: UInt8 = 0
|
|
||||||
private static var cachedConfigKey: UInt8 = 0
|
private static var cachedConfigKey: UInt8 = 0
|
||||||
|
|
||||||
var isTunnelConfigurationAvailableInKeychain: Bool {
|
|
||||||
if let cachedNumber = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedIsConfigAvailableInKeychainKey) as? NSNumber {
|
|
||||||
return cachedNumber.boolValue
|
|
||||||
}
|
|
||||||
let isAvailable = (protocolConfiguration as? NETunnelProviderProtocol)?.verifyConfigurationReference() ?? false
|
|
||||||
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedIsConfigAvailableInKeychainKey, NSNumber(value: isAvailable), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
||||||
return isAvailable
|
|
||||||
}
|
|
||||||
|
|
||||||
var tunnelConfiguration: TunnelConfiguration? {
|
var tunnelConfiguration: TunnelConfiguration? {
|
||||||
if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey) as? TunnelConfiguration {
|
if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey) as? TunnelConfiguration {
|
||||||
return cached
|
return cached
|
||||||
@@ -636,17 +638,9 @@ extension NETunnelProviderManager {
|
|||||||
protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration, previouslyFrom: protocolConfiguration)
|
protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration, previouslyFrom: protocolConfiguration)
|
||||||
localizedDescription = tunnelConfiguration.name
|
localizedDescription = tunnelConfiguration.name
|
||||||
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedIsConfigAvailableInKeychainKey, NSNumber(value: true), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isEquivalentTo(_ tunnel: TunnelContainer) -> Bool {
|
func isEquivalentTo(_ tunnel: TunnelContainer) -> Bool {
|
||||||
switch (isTunnelConfigurationAvailableInKeychain, tunnel.isTunnelConfigurationAvailableInKeychain) {
|
return localizedDescription == tunnel.name && tunnelConfiguration == tunnel.tunnelConfiguration
|
||||||
case (true, true):
|
|
||||||
return tunnelConfiguration == tunnel.tunnelConfiguration
|
|
||||||
case (false, false):
|
|
||||||
return localizedDescription == tunnel.name
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,10 +44,20 @@ class TunnelImporter {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: fileContents, called: fileBaseName)
|
var parseError: Error?
|
||||||
|
var tunnelConfiguration: TunnelConfiguration?
|
||||||
|
do {
|
||||||
|
tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: fileContents, called: fileBaseName)
|
||||||
|
} catch let error {
|
||||||
|
parseError = error
|
||||||
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if tunnelConfiguration == nil {
|
if parseError != nil {
|
||||||
lastFileImportErrorText = (title: tr("alertBadConfigImportTitle"), message: tr(format: "alertBadConfigImportMessage (%@)", fileName))
|
if let parseError = parseError as? WireGuardAppError {
|
||||||
|
lastFileImportErrorText = parseError.alertText
|
||||||
|
} else {
|
||||||
|
lastFileImportErrorText = (title: tr("alertBadConfigImportTitle"), message: tr(format: "alertBadConfigImportMessage (%@)", fileName))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
configs.append(tunnelConfiguration)
|
configs.append(tunnelConfiguration)
|
||||||
dispatchGroup.leave()
|
dispatchGroup.leave()
|
||||||
|
|||||||
@@ -34,10 +34,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||||
guard let tunnelsManager = mainVC?.tunnelsManager else { return true }
|
mainVC?.importFromDisposableFile(url: url)
|
||||||
TunnelImporter.importFromFile(urls: [url], into: tunnelsManager, sourceVC: mainVC, errorPresenterType: ErrorPresenter.self) {
|
|
||||||
_ = FileManager.deleteFile(at: url)
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,19 @@ extension MainViewController {
|
|||||||
onTunnelsManagerReady = showTunnelDetailBlock
|
onTunnelsManagerReady = showTunnelDetailBlock
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func importFromDisposableFile(url: URL) {
|
||||||
|
let importFromFileBlock: (TunnelsManager) -> Void = { [weak self] tunnelsManager in
|
||||||
|
TunnelImporter.importFromFile(urls: [url], into: tunnelsManager, sourceVC: self, errorPresenterType: ErrorPresenter.self) {
|
||||||
|
_ = FileManager.deleteFile(at: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let tunnelsManager = tunnelsManager {
|
||||||
|
importFromFileBlock(tunnelsManager)
|
||||||
|
} else {
|
||||||
|
onTunnelsManagerReady = importFromFileBlock
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainViewController: UISplitViewControllerDelegate {
|
extension MainViewController: UISplitViewControllerDelegate {
|
||||||
|
|||||||
@@ -266,6 +266,10 @@ extension SSIDOptionEditTableViewController {
|
|||||||
case .ssidOption:
|
case .ssidOption:
|
||||||
let previousOption = selectedOption
|
let previousOption = selectedOption
|
||||||
selectedOption = ssidOptionFields[indexPath.row]
|
selectedOption = ssidOptionFields[indexPath.row]
|
||||||
|
guard previousOption != selectedOption else {
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
loadSections()
|
loadSections()
|
||||||
if previousOption == .anySSID {
|
if previousOption == .anySSID {
|
||||||
let indexSet = IndexSet(1 ... 2)
|
let indexSet = IndexSet(1 ... 2)
|
||||||
|
|||||||
@@ -78,6 +78,33 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func confirmAndQuit() {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = tr("macConfirmAndQuitAlertMessage")
|
||||||
|
if let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating {
|
||||||
|
alert.informativeText = tr(format: "macConfirmAndQuitInfoWithActiveTunnel (%@)", currentTunnel.name)
|
||||||
|
} else {
|
||||||
|
alert.informativeText = tr("macConfirmAndQuitAlertInfo")
|
||||||
|
}
|
||||||
|
alert.addButton(withTitle: tr("macConfirmAndQuitAlertCloseWindow"))
|
||||||
|
alert.addButton(withTitle: tr("macConfirmAndQuitAlertQuitWireGuard"))
|
||||||
|
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
if let manageWindow = manageTunnelsWindowObject {
|
||||||
|
manageWindow.orderFront(self)
|
||||||
|
alert.beginSheetModal(for: manageWindow) { response in
|
||||||
|
switch response {
|
||||||
|
case .alertFirstButtonReturn:
|
||||||
|
manageWindow.close()
|
||||||
|
case .alertSecondButtonReturn:
|
||||||
|
NSApp.terminate(nil)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc func quit() {
|
@objc func quit() {
|
||||||
if let manageWindow = manageTunnelsWindowObject, manageWindow.attachedSheet != nil {
|
if let manageWindow = manageTunnelsWindowObject, manageWindow.attachedSheet != nil {
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
@@ -105,10 +132,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
||||||
if UserDefaults.standard.bool(forKey: "shouldSuppressAppStoreUpdateDetection") {
|
|
||||||
wg_log(.debug, staticMessage: "App Store update detection is suppressed")
|
|
||||||
return .terminateNow
|
|
||||||
}
|
|
||||||
guard let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating else {
|
guard let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating else {
|
||||||
return .terminateNow
|
return .terminateNow
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ class MainMenu: NSMenu {
|
|||||||
|
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
menu.addItem(withTitle: tr("macMenuQuitManagingTunnels"),
|
menu.addItem(withTitle: tr("macMenuQuit"),
|
||||||
action: #selector(NSWindow.performClose(_:)), keyEquivalent: "q")
|
action: #selector(AppDelegate.confirmAndQuit), keyEquivalent: "q")
|
||||||
|
|
||||||
return menu
|
return menu
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ extension StatusMenu {
|
|||||||
func insertTunnelMenuItem(for tunnel: TunnelContainer, at tunnelIndex: Int) {
|
func insertTunnelMenuItem(for tunnel: TunnelContainer, at tunnelIndex: Int) {
|
||||||
let menuItem = TunnelMenuItem(tunnel: tunnel, action: #selector(tunnelClicked(sender:)))
|
let menuItem = TunnelMenuItem(tunnel: tunnel, action: #selector(tunnelClicked(sender:)))
|
||||||
menuItem.target = self
|
menuItem.target = self
|
||||||
menuItem.isHidden = !tunnel.isTunnelConfigurationAvailableInKeychain
|
menuItem.isHidden = !tunnel.isTunnelAvailableToUser
|
||||||
insertItem(menuItem, at: firstTunnelMenuItemIndex + tunnelIndex)
|
insertItem(menuItem, at: firstTunnelMenuItemIndex + tunnelIndex)
|
||||||
if numberOfTunnelMenuItems == 0 {
|
if numberOfTunnelMenuItems == 0 {
|
||||||
insertItem(NSMenuItem.separator(), at: firstTunnelMenuItemIndex + tunnelIndex + 1)
|
insertItem(NSMenuItem.separator(), at: firstTunnelMenuItemIndex + tunnelIndex + 1)
|
||||||
|
|||||||
@@ -3,13 +3,25 @@
|
|||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class LogViewCell: NSTextField {
|
class LogViewCell: NSTableCellView {
|
||||||
|
var text: String = "" {
|
||||||
|
didSet { textField?.stringValue = text }
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
isSelectable = false
|
|
||||||
isEditable = false
|
let textField = NSTextField(wrappingLabelWithString: "")
|
||||||
isBordered = false
|
addSubview(textField)
|
||||||
backgroundColor = .clear
|
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
textField.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||||
|
textField.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||||
|
textField.topAnchor.constraint(equalTo: self.topAnchor),
|
||||||
|
textField.bottomAnchor.constraint(equalTo: self.bottomAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
self.textField = textField
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@@ -17,19 +29,19 @@ class LogViewCell: NSTextField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
stringValue = ""
|
textField?.stringValue = ""
|
||||||
preferredMaxLayoutWidth = 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LogViewTimestampCell: LogViewCell {
|
class LogViewTimestampCell: LogViewCell {
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
maximumNumberOfLines = 1
|
if let textField = textField {
|
||||||
lineBreakMode = .byClipping
|
textField.maximumNumberOfLines = 1
|
||||||
preferredMaxLayoutWidth = 0
|
textField.lineBreakMode = .byClipping
|
||||||
setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
|
textField.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
|
||||||
setContentHuggingPriority(.defaultLow, for: .vertical)
|
textField.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@@ -40,10 +52,12 @@ class LogViewTimestampCell: LogViewCell {
|
|||||||
class LogViewMessageCell: LogViewCell {
|
class LogViewMessageCell: LogViewCell {
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
maximumNumberOfLines = 0
|
if let textField = textField {
|
||||||
lineBreakMode = .byWordWrapping
|
textField.maximumNumberOfLines = 0
|
||||||
setContentCompressionResistancePriority(.required, for: .vertical)
|
textField.lineBreakMode = .byWordWrapping
|
||||||
setContentHuggingPriority(.required, for: .vertical)
|
textField.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||||
|
textField.setContentHuggingPriority(.required, for: .vertical)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
|||||||
@@ -146,7 +146,8 @@ class LogViewController: NSViewController {
|
|||||||
])
|
])
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
containerView.widthAnchor.constraint(equalToConstant: 640),
|
containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 640),
|
||||||
|
containerView.widthAnchor.constraint(lessThanOrEqualToConstant: 1200),
|
||||||
containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 240)
|
containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 240)
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -192,6 +193,10 @@ class LogViewController: NSViewController {
|
|||||||
updateLogEntriesTimer = nil
|
updateLogEntriesTimer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear() {
|
||||||
|
view.window?.setFrameAutosaveName(NSWindow.FrameAutosaveName("LogWindow"))
|
||||||
|
}
|
||||||
|
|
||||||
override func viewWillDisappear() {
|
override func viewWillDisappear() {
|
||||||
super.viewWillDisappear()
|
super.viewWillDisappear()
|
||||||
stopUpdatingLogEntries()
|
stopUpdatingLogEntries()
|
||||||
@@ -250,12 +255,11 @@ extension LogViewController: NSTableViewDelegate {
|
|||||||
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
||||||
if LogColumn.time.isRepresenting(tableColumn: tableColumn) {
|
if LogColumn.time.isRepresenting(tableColumn: tableColumn) {
|
||||||
let cell: LogViewTimestampCell = tableView.dequeueReusableCell()
|
let cell: LogViewTimestampCell = tableView.dequeueReusableCell()
|
||||||
cell.stringValue = logEntries[row].timestamp
|
cell.text = logEntries[row].timestamp
|
||||||
return cell
|
return cell
|
||||||
} else if LogColumn.logMessage.isRepresenting(tableColumn: tableColumn) {
|
} else if LogColumn.logMessage.isRepresenting(tableColumn: tableColumn) {
|
||||||
let cell: LogViewMessageCell = tableView.dequeueReusableCell()
|
let cell: LogViewMessageCell = tableView.dequeueReusableCell()
|
||||||
cell.stringValue = logEntries[row].message
|
cell.text = logEntries[row].message
|
||||||
cell.preferredMaxLayoutWidth = tableColumn?.width ?? 0
|
|
||||||
return cell
|
return cell
|
||||||
} else {
|
} else {
|
||||||
fatalError()
|
fatalError()
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ extension ManageTunnelsRootViewController: TunnelsListTableViewControllerDelegat
|
|||||||
assert(!tunnelIndices.isEmpty)
|
assert(!tunnelIndices.isEmpty)
|
||||||
if tunnelIndices.count == 1 {
|
if tunnelIndices.count == 1 {
|
||||||
let tunnel = tunnelsManager.tunnel(at: tunnelIndices.first!)
|
let tunnel = tunnelsManager.tunnel(at: tunnelIndices.first!)
|
||||||
if tunnel.isTunnelConfigurationAvailableInKeychain {
|
if tunnel.isTunnelAvailableToUser {
|
||||||
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
|
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
|
||||||
setTunnelDetailContentVC(tunnelDetailVC)
|
setTunnelDetailContentVC(tunnelDetailVC)
|
||||||
self.tunnelDetailVC = tunnelDetailVC
|
self.tunnelDetailVC = tunnelDetailVC
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class TunnelsListTableViewController: NSViewController {
|
|||||||
|
|
||||||
let tunnelsManager: TunnelsManager
|
let tunnelsManager: TunnelsManager
|
||||||
weak var delegate: TunnelsListTableViewControllerDelegate?
|
weak var delegate: TunnelsListTableViewControllerDelegate?
|
||||||
var isRemovingTunnels = false
|
var isRemovingTunnelsFromWithinTheApp = false
|
||||||
|
|
||||||
let tableView: NSTableView = {
|
let tableView: NSTableView = {
|
||||||
let tableView = NSTableView()
|
let tableView = NSTableView()
|
||||||
@@ -183,8 +183,10 @@ class TunnelsListTableViewController: NSViewController {
|
|||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.selectTunnel(at: nextSelection)
|
self.selectTunnel(at: nextSelection)
|
||||||
let selectedTunnels = selectedTunnelIndices.map { self.tunnelsManager.tunnel(at: $0) }
|
let selectedTunnels = selectedTunnelIndices.map { self.tunnelsManager.tunnel(at: $0) }
|
||||||
|
self.isRemovingTunnelsFromWithinTheApp = true
|
||||||
self.tunnelsManager.removeMultiple(tunnels: selectedTunnels) { [weak self] error in
|
self.tunnelsManager.removeMultiple(tunnels: selectedTunnels) { [weak self] error in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
self.isRemovingTunnelsFromWithinTheApp = false
|
||||||
defer { completion() }
|
defer { completion() }
|
||||||
if let error = error {
|
if let error = error {
|
||||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||||
@@ -281,9 +283,14 @@ extension TunnelsListTableViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func tunnelRemoved(at index: Int) {
|
func tunnelRemoved(at index: Int) {
|
||||||
|
let selectedIndices = tableView.selectedRowIndexes
|
||||||
|
let isSingleSelectedTunnelBeingRemoved = selectedIndices.contains(index) && selectedIndices.count == 1
|
||||||
tableView.removeRows(at: IndexSet(integer: index), withAnimation: .slideLeft)
|
tableView.removeRows(at: IndexSet(integer: index), withAnimation: .slideLeft)
|
||||||
if tunnelsManager.numberOfTunnels() == 0 {
|
if tunnelsManager.numberOfTunnels() == 0 {
|
||||||
delegate?.tunnelsListEmpty()
|
delegate?.tunnelsListEmpty()
|
||||||
|
} else if !isRemovingTunnelsFromWithinTheApp && isSingleSelectedTunnelBeingRemoved {
|
||||||
|
let newSelection = min(index, tunnelsManager.numberOfTunnels() - 1)
|
||||||
|
tableView.selectRowIndexes(IndexSet(integer: newSelection), byExtendingSelection: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
private var handle: Int32?
|
private var handle: Int32?
|
||||||
private var networkMonitor: NWPathMonitor?
|
private var networkMonitor: NWPathMonitor?
|
||||||
private var ifname: String?
|
private var ifname: String?
|
||||||
private var lastSeenInterfaces: [String] = []
|
|
||||||
private var packetTunnelSettingsGenerator: PacketTunnelSettingsGenerator?
|
private var packetTunnelSettingsGenerator: PacketTunnelSettingsGenerator?
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@@ -143,20 +142,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
|
|
||||||
private func pathUpdate(path: Network.NWPath) {
|
private func pathUpdate(path: Network.NWPath) {
|
||||||
guard let handle = handle else { return }
|
guard let handle = handle else { return }
|
||||||
guard let ifname = ifname else { return }
|
|
||||||
wg_log(.debug, message: "Network change detected with \(path.status) route and interface order \(path.availableInterfaces)")
|
wg_log(.debug, message: "Network change detected with \(path.status) route and interface order \(path.availableInterfaces)")
|
||||||
guard path.status == .satisfied else { return }
|
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if let packetTunnelSettingsGenerator = packetTunnelSettingsGenerator {
|
if let packetTunnelSettingsGenerator = packetTunnelSettingsGenerator {
|
||||||
_ = packetTunnelSettingsGenerator.endpointUapiConfiguration().withGoString { return wgSetConfig(handle, $0) }
|
_ = packetTunnelSettingsGenerator.endpointUapiConfiguration().withGoString { return wgSetConfig(handle, $0) }
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
let interfaces = path.availableInterfaces.filter { $0.name != ifname }.compactMap { $0.name }
|
wgBumpSockets(handle)
|
||||||
if !interfaces.elementsEqual(lastSeenInterfaces) {
|
|
||||||
lastSeenInterfaces = interfaces
|
|
||||||
wgBumpSockets(handle)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export CGO_ENABLED := 1
|
|||||||
build: $(DESTDIR)/libwg-go.a
|
build: $(DESTDIR)/libwg-go.a
|
||||||
version-header: $(DESTDIR)/wireguard-go-version.h
|
version-header: $(DESTDIR)/wireguard-go-version.h
|
||||||
|
|
||||||
GOBUILDVERSION_NEEDED := go version go1.12.1 darwin/amd64
|
GOBUILDVERSION_NEEDED := go version go1.12.5 darwin/amd64
|
||||||
GOBUILDVERSION_CURRENT := $(shell go version 2>/dev/null)
|
GOBUILDVERSION_CURRENT := $(shell go version 2>/dev/null)
|
||||||
export REAL_GOROOT := $(shell go env GOROOT 2>/dev/null)
|
export REAL_GOROOT := $(shell go env GOROOT 2>/dev/null)
|
||||||
export GOROOT := $(BUILDDIR)/goroot
|
export GOROOT := $(BUILDDIR)/goroot
|
||||||
|
|||||||
Reference in New Issue
Block a user