Compare commits

...

16 Commits

Author SHA1 Message Date
Jason A. Donenfeld fcca2d4fec macOS: Show privacy notice on adding first tunnel
App store reviewers don't understand that this isn't a service.

Revert this as soon as they come to their senses.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-15 01:14:14 +01:00
Jason A. Donenfeld 58181a4d40 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-14 23:07:06 +01:00
Jason A. Donenfeld cf816ede13 wireguard-go: bump for ARM64 ChaCha20
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-14 12:00:59 +01:00
Jason A. Donenfeld fbac282bdc .mobileconfig: fix formatting
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-13 14:31:02 +01:00
Jason A. Donenfeld 61f5e017c8 .mobileconfig: note keychain limitation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-13 14:04:54 +01:00
Roopesh Chander 4547e01283 Preshared key field in the detail view should just say 'enabled'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-12 19:30:59 +05:30
Roopesh Chander 5792db22a6 Log migration of tunnel configuration
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-12 19:17:32 +05:30
Roopesh Chander 9d5aa1d8fa Document installing WireGuard tunnels using Configuration Profiles
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-12 19:14:53 +05:30
Roopesh Chander 6331b81b5d Migrate when we notice a new tunnel in reload()
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-12 17:43:40 +05:30
Roopesh Chander 77f929789c Don't migrate in asTunnelConfiguration()
It causes problems when installing a tunnel through a
Configuration Profile on macOS and activating it first through
Network Preferences.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-12 17:37:27 +05:30
Roopesh Chander b5b72b309f Info.plist: Localize with InfoPlist.strings
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-11 16:08:55 +05:30
Roopesh Chander 966fa7909b macOS: Change keyboard shortcut for importing to Cmd+O
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-10 17:55:50 +05:30
Roopesh Chander 115059f2bb macOS: Adapt to the new applyConfiguration API
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-10 03:35:24 +05:30
Roopesh Chander e53c2d4d17 iOS: Rewrite applying runtime configuration
To make scrolling smoother while the fields are modified

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-10 03:35:24 +05:30
Roopesh Chander 0a3a5ee900 Importing: Ignore case in matching file extensions inside zip files
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-09 19:42:50 +05:30
Roopesh Chander 7720307fc9 TunnelsManager: No need to access tunnelConfiguration on status change
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-09 19:42:50 +05:30
18 changed files with 375 additions and 131 deletions
+145
View File
@@ -0,0 +1,145 @@
# Installing WireGuard tunnels using Configuration Profiles
WireGuard configurations can be installed using Configuration Profiles
through .mobileconfig files.
### Top-level payload entries
A .mobileconfig file is a plist file in XML format. The top-level XML item is a top-level payload dictionary (dict). This payload dictionary should contain the following keys:
- `PayloadDisplayName` (string): The name of the configuration profile, visible when installing the profile
- `PayloadType` (string): Should be `Configuration`
- `PayloadVersion` (integer): Should be `1`
- `PayloadIdentifier` (string): A reverse-DNS style unique identifier for the profile file.
If you install another .mobileconfig file with the same identifier, the new one
overwrites the old one.
- `PayloadUUID` (string): A randomly generated UUID for this payload
- `PayloadContent` (array): Should contain an array of payload dictionaries.
Each of these payload dictionaries can represent a WireGuard tunnel
configuration.
Here's an example .mobileconfig with the above fields filled in:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadDisplayName</key>
<string>WireGuard Demo Configuration Profile</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>com.your-org.wireguard.FCC9BF80-C540-44C1-B243-521FDD1B2905</string>
<key>PayloadUUID</key>
<string>F346AAF4-53A2-4FA1-ACA3-EEE74DBED029</string>
<key>PayloadContent</key>
<array>
<!-- An array of WireGuard configuration payload dictionaries -->
</array>
</dict>
</plist>
```
### WireGuard payload entries
Each WireGuard configuration payload dictionary should contain the following
keys:
- `PayloadDisplayName` (string): Should be `VPN`
- `PayloadType` (string): Should be `com.apple.vpn.managed`
- `PayloadVersion` (integer): Should be `1`
- `PayloadIdentifier` (string): A reverse-DNS style unique identifier for the WireGuard configuration profile.
- `PayloadUUID` (string): A randomly generated UUID for this payload
- `UserDefinedName` (string): The name of the WireGuard tunnel.
This name shall be used to represent the tunnel in the WireGuard app, and in the System UI for VPNs (Settings > VPN on iOS, System Preferences > Network on macOS).
- `VPNType` (string): Should be `VPN`
- `VPNSubType` (string): Should be set as the bundle identifier of the WireGuard app.
- iOS: `com.wireguard.ios`
- macOS: `com.wireguard.macos`
- `VendorConfig` (dict): Should be a dictionary with the following key:
- `WgQuickConfig` (string): Should be a WireGuard configuration in [wg-quick(8)] / [wg(8)] format.
The keys 'FwMark', 'Table', 'PreUp', 'PostUp', 'PreDown', 'PostDown' and 'SaveConfig' are not supported.
- `VPN` (dict): Should be a dictionary with the following keys:
- `RemoteAddress` (string): A non-empty string.
This string is displayed as the server name in the System UI for
VPNs (Settings > VPN on iOS, System Preferences > Network on macOS).
- `AuthenticationMethod` (string): Should be `Password`
Here's an example WireGuard configuration payload dictionary:
```xml
<!-- A WireGuard configuration payload dictionary -->
<dict>
<key>PayloadDisplayName</key>
<string>VPN</string>
<key>PayloadType</key>
<string>com.apple.vpn.managed</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>com.your-org.wireguard.demo-profile-1.demo-tunnel</string>
<key>PayloadUUID</key>
<string>44CDFE9F-4DC7-472A-956F-61C68055117C</string>
<key>UserDefinedName</key>
<string>Demo from MobileConfig file</string>
<key>VPNType</key>
<string>VPN</string>
<key>VPNSubType</key>
<string>com.wireguard.ios</string>
<key>VendorConfig</key>
<dict>
<key>WgQuickConfig</key>
<string>
[Interface]
PrivateKey = mInDaw06K0NgfULRObHJjkWD3ahUC8XC1tVjIf6W+Vo=
Address = 10.10.1.0/24
DNS = 1.1.1.1, 1.0.0.1
[Peer]
PublicKey = JRI8Xc0zKP9kXk8qP84NdUQA04h6DLfFbwJn4g+/PFs=
Endpoint = demo.wireguard.com:12912
AllowedIPs = 0.0.0.0/0
</string>
</dict>
<key>VPN</key>
<dict>
<key>RemoteAddress</key>
<string>demo.wireguard.com:12912</string>
<key>AuthenticationMethod</key>
<string>Password</string>
</dict>
</dict>
```
### Caveats
Configurations added via .mobileconfig will not be migrated into keychain until the WireGuard application is opened once.
[wg-quick(8)]: https://git.zx2c4.com/WireGuard/about/src/tools/man/wg-quick.8
[wg(8)]: https://git.zx2c4.com/WireGuard/about/src/tools/man/wg.8
@@ -34,17 +34,14 @@ extension NETunnelProviderProtocol {
}
func asTunnelConfiguration(called name: String? = nil) -> TunnelConfiguration? {
migrateConfigurationIfNeeded(called: name ?? "unknown")
//TODO: in the case where migrateConfigurationIfNeeded is called by the network extension,
// before the app has started, and when there is, in fact, configuration that needs to be
// put into the keychain, this will generate one new keychain item every time it is started,
// until finally the app is open. Would it be possible to call saveToPreferences here? Or is
// that generally not available to network extensions? In which case, what should our
// behavior be?
guard let passwordReference = passwordReference else { return nil }
guard let config = Keychain.openReference(called: passwordReference) else { return nil }
return try? TunnelConfiguration(fromWgQuickConfig: config, called: name)
if let passwordReference = passwordReference,
let config = Keychain.openReference(called: passwordReference) {
return try? TunnelConfiguration(fromWgQuickConfig: config, called: name)
}
if let oldConfig = providerConfiguration?["WgQuickConfig"] as? String {
return try? TunnelConfiguration(fromWgQuickConfig: oldConfig, called: name)
}
return nil
}
func destroyConfigurationReference() {
@@ -66,6 +63,7 @@ extension NETunnelProviderProtocol {
guard let oldConfig = providerConfiguration?["WgQuickConfig"] as? String else { return false }
providerConfiguration = nil
guard passwordReference == nil else { return true }
wg_log(.debug, message: "Migrating tunnel configuration '\(name)'")
passwordReference = Keychain.makeReference(containing: oldConfig, called: name)
return true
}
@@ -43,6 +43,7 @@
6B653B86220DE2960050E69C /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FF4AC462120B9E0002C96EB /* NetworkExtension.framework */; };
6B707D8421F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B707D8321F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift */; };
6B707D8621F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B707D8321F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift */; };
6BAC16E6221634B300A5FB78 /* AppStorePrivacyNotice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BAC16E42216324B00A5FB78 /* AppStorePrivacyNotice.swift */; };
6BD5C97B220D1AE200784E08 /* key.c in Sources */ = {isa = PBXBuildFile; fileRef = 6BD5C979220D1AE100784E08 /* key.c */; };
6BD5C97C220D1AE200784E08 /* key.c in Sources */ = {isa = PBXBuildFile; fileRef = 6BD5C979220D1AE100784E08 /* key.c */; };
6BD5C97D220D1AE200784E08 /* key.c in Sources */ = {isa = PBXBuildFile; fileRef = 6BD5C979220D1AE100784E08 /* key.c */; };
@@ -63,6 +64,8 @@
6F6899A62180447E0012E523 /* x25519.c in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A52180447E0012E523 /* x25519.c */; };
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A7218044FC0012E523 /* Curve25519.swift */; };
6F693A562179E556008551C1 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; };
6F70E20E221058E1008BDFB4 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6F70E20C221058DF008BDFB4 /* InfoPlist.strings */; };
6F70E20F221058E1008BDFB4 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6F70E20C221058DF008BDFB4 /* InfoPlist.strings */; };
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774DF217181B1006A79B3 /* MainViewController.swift */; };
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E0217181B1006A79B3 /* AppDelegate.swift */; };
6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */; };
@@ -253,6 +256,7 @@
6B5C5E26220A48D30024272E /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
6B62E45E220A6FA900EF34A6 /* PrivateDataConfirmation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateDataConfirmation.swift; sourceTree = "<group>"; };
6B707D8321F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelConfiguration+UapiConfig.swift"; sourceTree = "<group>"; };
6BAC16E42216324B00A5FB78 /* AppStorePrivacyNotice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePrivacyNotice.swift; sourceTree = "<group>"; };
6BD5C979220D1AE100784E08 /* key.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = key.c; sourceTree = "<group>"; };
6BD5C97A220D1AE200784E08 /* key.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = key.h; sourceTree = "<group>"; };
6F4DD16721DA552B00690EAE /* NSTableView+Reuse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTableView+Reuse.swift"; sourceTree = "<group>"; };
@@ -277,6 +281,7 @@
6F6899A52180447E0012E523 /* x25519.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = x25519.c; sourceTree = "<group>"; };
6F6899A7218044FC0012E523 /* Curve25519.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Curve25519.swift; sourceTree = "<group>"; };
6F693A552179E556008551C1 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = "<group>"; };
6F70E20D221058DF008BDFB4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = WireGuard/Base.lproj/InfoPlist.strings; sourceTree = "<group>"; };
6F7774DF217181B1006A79B3 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
6F7774E0217181B1006A79B3 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelsListTableViewController.swift; sourceTree = "<group>"; };
@@ -558,6 +563,7 @@
6F4DD16721DA552B00690EAE /* NSTableView+Reuse.swift */,
5F52D0BE21E3788900283CEA /* NSColor+Hex.swift */,
6FFACD1E21E4D89600E9A2A5 /* ParseError+WireGuardAppError.swift */,
6BAC16E42216324B00A5FB78 /* AppStorePrivacyNotice.swift */,
);
path = macOS;
sourceTree = "<group>";
@@ -619,6 +625,7 @@
6FF4AC0B211EC46F002C96EB = {
isa = PBXGroup;
children = (
6F70E20C221058DF008BDFB4 /* InfoPlist.strings */,
6FE1765421C90BBE002690EA /* Localizable.strings */,
6F5D0C432183B4A4000F85AD /* Shared */,
6FF4AC16211EC46F002C96EB /* WireGuard */,
@@ -874,6 +881,7 @@
buildActionMask = 2147483647;
files = (
6FB1BD6221D2607E00A991BF /* Assets.xcassets in Resources */,
6F70E20F221058E1008BDFB4 /* InfoPlist.strings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -889,6 +897,7 @@
buildActionMask = 2147483647;
files = (
6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */,
6F70E20E221058E1008BDFB4 /* InfoPlist.strings in Resources */,
6FE1765621C90BBE002690EA /* Localizable.strings in Resources */,
6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */,
6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */,
@@ -1150,6 +1159,7 @@
6FB1BDCB21D50F1700A991BF /* Curve25519.swift in Sources */,
6B586C55220CBA6D00427C51 /* Data+KeyEncoding.swift in Sources */,
6F9B582921E8D6D100544D02 /* PopupRow.swift in Sources */,
6BAC16E6221634B300A5FB78 /* AppStorePrivacyNotice.swift in Sources */,
6FB1BDBB21D50F0200A991BF /* Localizable.strings in Sources */,
6FB1BDBC21D50F0200A991BF /* ringlogger.c in Sources */,
6FB1BDBD21D50F0200A991BF /* ringlogger.h in Sources */,
@@ -1291,6 +1301,14 @@
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
6F70E20C221058DF008BDFB4 /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
6F70E20D221058DF008BDFB4 /* Base */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
};
6FE1765421C90BBE002690EA /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
// iOS permission prompts
NSCameraUsageDescription = "Camera is used for scanning QR codes for importing WireGuard configurations";
NSFaceIDUsageDescription = "Face ID is used for authenticating viewing and exporting of private keys";
@@ -111,6 +111,8 @@
"tunnelHandshakeTimestampHours hh:mm:ss (%@)" = "%@ hours";
"tunnelHandshakeTimestampMinutes mm:ss (%@)" = "%@ minutes";
"tunnelPeerPresharedKeyEnabled" = "enabled";
// Error alerts while creating / editing a tunnel configuration
/* Alert title for error in the interface data */
@@ -331,3 +333,5 @@
"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.";
"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.";
+1 -1
View File
@@ -1,2 +1,2 @@
VERSION_NAME = 0.0.20190207
VERSION_ID = 2
VERSION_ID = 3
@@ -83,6 +83,11 @@ class TunnelsManager {
matchingTunnel.refreshStatus()
} else {
// Tunnel was added outside the app
if let proto = loadedTunnelProvider.protocolConfiguration as? NETunnelProviderProtocol {
if proto.migrateConfigurationIfNeeded(called: loadedTunnelProvider.localizedDescription ?? "unknown") {
loadedTunnelProvider.saveToPreferences { _ in }
}
}
let tunnel = TunnelContainer(tunnel: loadedTunnelProvider)
self.tunnels.append(tunnel)
self.tunnels.sort { $0.name < $1.name }
@@ -313,11 +318,7 @@ class TunnelsManager {
guard let self = self,
let session = statusChangeNotification.object as? NETunnelProviderSession,
let tunnelProvider = session.manager as? NETunnelProviderManager,
let tunnelConfiguration = tunnelProvider.tunnelConfiguration,
let tunnel = self.tunnels.first(where: { $0.tunnelConfiguration == tunnelConfiguration }) else { return }
if tunnel.tunnelProvider != tunnelProvider {
return
}
let tunnel = self.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) else { return }
wg_log(.debug, message: "Tunnel '\(tunnel.name)' connection status changed to '\(tunnel.tunnelProvider.connection.status)'")
+25 -34
View File
@@ -67,17 +67,17 @@ class TunnelViewModel {
static let keyLengthInBase64 = 44
struct ChangeHandlers {
enum FieldChange {
struct Changes {
enum FieldChange: Equatable {
case added
case removed
case modified
case modified(newValue: String)
}
var interfaceChanged: ([InterfaceField: FieldChange]) -> Void
var peerChangedAt: (Int, [PeerField: FieldChange]) -> Void
var peersRemovedAt: ([Int]) -> Void
var peersInsertedAt: ([Int]) -> Void
var interfaceChanges: [InterfaceField: FieldChange]
var peerChanges: [(peerIndex: Int, changes: [PeerField: FieldChange])]
var peersRemovedIndices: [Int]
var peersInsertedIndices: [Int]
}
class InterfaceData {
@@ -217,12 +217,12 @@ class TunnelViewModel {
}
}
func applyConfiguration(other: InterfaceConfiguration, otherName: String, changeHandler: ([InterfaceField: ChangeHandlers.FieldChange]) -> Void) {
func applyConfiguration(other: InterfaceConfiguration, otherName: String) -> [InterfaceField: Changes.FieldChange] {
if scratchpad.isEmpty {
populateScratchpad()
}
let otherScratchPad = InterfaceData.createScratchPad(from: other, name: otherName)
var changes = [InterfaceField: ChangeHandlers.FieldChange]()
var changes = [InterfaceField: Changes.FieldChange]()
for field in InterfaceField.allCases {
switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
case ("", ""):
@@ -233,14 +233,12 @@ class TunnelViewModel {
changes[field] = .removed
case (let this, let other):
if this != other {
changes[field] = .modified
changes[field] = .modified(newValue: other)
}
}
}
scratchpad = otherScratchPad
if !changes.isEmpty {
changeHandler(changes)
}
return changes
}
}
@@ -441,12 +439,12 @@ class TunnelViewModel {
excludePrivateIPsValue = isOn
}
func applyConfiguration(other: PeerConfiguration, peerIndex: Int, changeHandler: (Int, [PeerField: ChangeHandlers.FieldChange]) -> Void) {
func applyConfiguration(other: PeerConfiguration) -> [PeerField: Changes.FieldChange] {
if scratchpad.isEmpty {
populateScratchpad()
}
let otherScratchPad = PeerData.createScratchPad(from: other)
var changes = [PeerField: ChangeHandlers.FieldChange]()
var changes = [PeerField: Changes.FieldChange]()
for field in PeerField.allCases {
switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
case ("", ""):
@@ -457,14 +455,12 @@ class TunnelViewModel {
changes[field] = .removed
case (let this, let other):
if this != other {
changes[field] = .modified
changes[field] = .modified(newValue: other)
}
}
}
scratchpad = otherScratchPad
if !changes.isEmpty {
changeHandler(peerIndex, changes)
}
return changes
}
}
@@ -548,21 +544,20 @@ class TunnelViewModel {
}
}
func applyConfiguration(other: TunnelConfiguration, changeHandlers: ChangeHandlers) {
@discardableResult
func applyConfiguration(other: TunnelConfiguration) -> Changes {
// Replaces current data with data from other TunnelConfiguration, ignoring any changes in peer ordering.
// Change handler callbacks are processed in the following order, which is designed to work with both the
// UITableView way (modify - delete - insert) and the NSTableView way (indices are based on past operations):
// - interfaceChanged
// - peerChangedAt
// - peersRemovedAt
// - peersInsertedAt
interfaceData.applyConfiguration(other: other.interface, otherName: other.name ?? "", changeHandler: changeHandlers.interfaceChanged)
let interfaceChanges = interfaceData.applyConfiguration(other: other.interface, otherName: other.name ?? "")
var peerChanges = [(peerIndex: Int, changes: [PeerField: Changes.FieldChange])]()
for otherPeer in other.peers {
if let peersDataIndex = peersData.firstIndex(where: { $0.publicKey == otherPeer.publicKey }) {
let peerData = peersData[peersDataIndex]
peerData.applyConfiguration(other: otherPeer, peerIndex: peersDataIndex, changeHandler: changeHandlers.peerChangedAt)
let changes = peerData.applyConfiguration(other: otherPeer)
if !changes.isEmpty {
peerChanges.append((peerIndex: peersDataIndex, changes: changes))
}
}
}
@@ -573,9 +568,6 @@ class TunnelViewModel {
peersData.remove(at: index)
}
}
if !removedPeerIndices.isEmpty {
changeHandlers.peersRemovedAt(removedPeerIndices)
}
var addedPeerIndices = [Int]()
for otherPeer in other.peers {
@@ -586,15 +578,14 @@ class TunnelViewModel {
peersData.append(peerData)
}
}
if !addedPeerIndices.isEmpty {
changeHandlers.peersInsertedAt(addedPeerIndices)
}
for (index, peer) in peersData.enumerated() {
peer.index = index
peer.numberOfPeers = peersData.count
peer.updateExcludePrivateIPsFieldState()
}
return Changes(interfaceChanges: interfaceChanges, peerChanges: peerChanges, peersRemovedIndices: removedPeerIndices, peersInsertedIndices: addedPeerIndices)
}
}
+2 -2
View File
@@ -77,7 +77,7 @@
<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>
<key>NSCameraUsageDescription</key>
<string>Camera is used for scanning QR codes for importing WireGuard configurations</string>
<string>Localized</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
@@ -123,7 +123,7 @@
</dict>
</array>
<key>NSFaceIDUsageDescription</key>
<string>Face ID is used for authenticating viewing and exporting of private keys</string>
<string>Localized</string>
<key>com.wireguard.ios.app_group_id</key>
<string>group.$(APP_ID_IOS)</string>
</dict>
@@ -158,16 +158,21 @@ class TunnelDetailTableViewController: UITableViewController {
var interfaceFieldIsVisible = self.interfaceFieldIsVisible
var peerFieldIsVisible = self.peerFieldIsVisible
func sectionChanged<T>(fields: [T], fieldIsVisible fieldIsVisibleInput: [Bool], tableView: UITableView, section: Int, changes: [T: TunnelViewModel.ChangeHandlers.FieldChange]) {
func handleSectionFieldsModified<T>(fields: [T], fieldIsVisible: [Bool], section: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
for (index, field) in fields.enumerated() {
guard let change = changes[field] else { continue }
if case .modified(let newValue) = change {
let row = fieldIsVisible[0 ..< index].filter { $0 }.count
let indexPath = IndexPath(row: row, section: section)
if let cell = tableView.cellForRow(at: indexPath) as? KeyValueCell {
cell.value = newValue
}
}
}
}
func handleSectionRowsInsertedOrRemoved<T>(fields: [T], fieldIsVisible fieldIsVisibleInput: [Bool], section: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
var fieldIsVisible = fieldIsVisibleInput
var modifiedIndexPaths = [IndexPath]()
for (index, field) in fields.enumerated() where changes[field] == .modified {
let row = fieldIsVisible[0 ..< index].filter { $0 }.count
modifiedIndexPaths.append(IndexPath(row: row, section: section))
}
if !modifiedIndexPaths.isEmpty {
tableView.reloadRows(at: modifiedIndexPaths, with: .none)
}
var removedIndexPaths = [IndexPath]()
for (index, field) in fields.enumerated().reversed() where changes[field] == .removed {
@@ -190,30 +195,44 @@ class TunnelDetailTableViewController: UITableViewController {
}
}
let changeHandlers = TunnelViewModel.ChangeHandlers(
interfaceChanged: { changes in
sectionChanged(fields: TunnelDetailTableViewController.interfaceFields, fieldIsVisible: interfaceFieldIsVisible,
tableView: tableView, section: interfaceSectionIndex, changes: changes)
},
peerChangedAt: { peerIndex, changes in
sectionChanged(fields: TunnelDetailTableViewController.peerFields, fieldIsVisible: peerFieldIsVisible[peerIndex],
tableView: tableView, section: firstPeerSectionIndex + peerIndex, changes: changes)
},
peersRemovedAt: { peerIndices in
let sectionIndices = peerIndices.map { firstPeerSectionIndex + $0 }
tableView.deleteSections(IndexSet(sectionIndices), with: .automatic)
},
peersInsertedAt: { peerIndices in
let sectionIndices = peerIndices.map { firstPeerSectionIndex + $0 }
tableView.insertSections(IndexSet(sectionIndices), with: .automatic)
}
)
let changes = self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration)
tableView.beginUpdates()
self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration, changeHandlers: changeHandlers)
self.loadSections()
self.loadVisibleFields()
tableView.endUpdates()
if !changes.interfaceChanges.isEmpty {
handleSectionFieldsModified(fields: TunnelDetailTableViewController.interfaceFields, fieldIsVisible: interfaceFieldIsVisible,
section: interfaceSectionIndex, changes: changes.interfaceChanges)
}
for (peerIndex, peerChanges) in changes.peerChanges {
handleSectionFieldsModified(fields: TunnelDetailTableViewController.peerFields, fieldIsVisible: peerFieldIsVisible[peerIndex], section: firstPeerSectionIndex + peerIndex, changes: peerChanges)
}
let isAnyInterfaceFieldAddedOrRemoved = changes.interfaceChanges.contains { $0.value == .added || $0.value == .removed }
let isAnyPeerFieldAddedOrRemoved = changes.peerChanges.contains { $0.changes.contains { $0.value == .added || $0.value == .removed } }
let peersRemovedSectionIndices = changes.peersRemovedIndices.map { firstPeerSectionIndex + $0 }
let peersInsertedSectionIndices = changes.peersInsertedIndices.map { firstPeerSectionIndex + $0 }
if isAnyInterfaceFieldAddedOrRemoved || isAnyPeerFieldAddedOrRemoved || !peersRemovedSectionIndices.isEmpty || !peersInsertedSectionIndices.isEmpty {
tableView.beginUpdates()
if isAnyInterfaceFieldAddedOrRemoved {
handleSectionRowsInsertedOrRemoved(fields: TunnelDetailTableViewController.interfaceFields, fieldIsVisible: interfaceFieldIsVisible, section: interfaceSectionIndex, changes: changes.interfaceChanges)
}
if isAnyPeerFieldAddedOrRemoved {
for (peerIndex, peerChanges) in changes.peerChanges {
handleSectionRowsInsertedOrRemoved(fields: TunnelDetailTableViewController.peerFields, fieldIsVisible: peerFieldIsVisible[peerIndex], section: firstPeerSectionIndex + peerIndex, changes: peerChanges)
}
}
if !peersRemovedSectionIndices.isEmpty {
tableView.deleteSections(IndexSet(peersRemovedSectionIndices), with: .automatic)
}
if !peersInsertedSectionIndices.isEmpty {
tableView.insertSections(IndexSet(peersInsertedSectionIndices), with: .automatic)
}
self.loadSections()
self.loadVisibleFields()
tableView.endUpdates()
} else {
self.loadSections()
self.loadVisibleFields()
}
}
private func reloadRuntimeConfiguration() {
@@ -351,6 +370,8 @@ extension TunnelDetailTableViewController {
cell.key = field.localizedUIString
if field == .persistentKeepAlive {
cell.value = tr(format: "tunnelPeerPersistentKeepaliveValue (%@)", peerData[field])
} else if field == .preSharedKey {
cell.value = tr("tunnelPeerPresharedKeyEnabled")
} else {
cell.value = peerData[field]
}
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Cocoa
class AppStorePrivacyNotice {
// The App Store Review Board does not comprehend the fact that this application
// is not a service and does not have any servers of its own. They therefore require
// us to give a notice regarding collection of user data using our non-existent
// servers. This demand is obviously impossible to fulfill, since it doesn't make sense,
// but we do our best here to show something in that category.
static func show(from sourceVC: NSViewController?, into tunnelsManager: TunnelsManager, _ callback: @escaping () -> Void) {
if tunnelsManager.numberOfTunnels() > 0 {
callback()
return
}
let alert = NSAlert()
alert.messageText = tr("macPrivacyNoticeMessage")
alert.informativeText = tr("macPrivacyNoticeInfo")
alert.alertStyle = NSAlert.Style.warning
if let window = sourceVC?.view.window {
alert.beginSheetModal(for: window) { _ in callback() }
} else {
alert.runModal()
callback()
}
}
}
@@ -13,7 +13,9 @@ class ImportPanelPresenter {
guard let tunnelsManager = tunnelsManager else { return }
guard response == .OK else { return }
guard let url = openPanel.url else { return }
TunnelImporter.importFromFile(url: url, into: tunnelsManager, sourceVC: sourceVC, errorPresenterType: ErrorPresenter.self)
AppStorePrivacyNotice.show(from: sourceVC, into: tunnelsManager) {
TunnelImporter.importFromFile(url: url, into: tunnelsManager, sourceVC: sourceVC, errorPresenterType: ErrorPresenter.self)
}
}
}
}
@@ -102,7 +102,7 @@ extension ManageTunnelsRootViewController {
switch event.charactersIgnoringModifiers {
case "n":
tunnelsListVC?.handleAddEmptyTunnelAction()
case "i":
case "o":
tunnelsListVC?.handleImportTunnelAction()
case "t":
tunnelDetailVC?.handleToggleActiveStatusAction()
@@ -263,7 +263,24 @@ class TunnelDetailTableViewController: NSViewController {
func applyTunnelConfiguration(tunnelConfiguration: TunnelConfiguration) {
// Incorporates changes from tunnelConfiguation. Ignores any changes in peer ordering.
func sectionChanged<T>(fields: [T], modelRowsInSection: inout [(isVisible: Bool, modelRow: TableViewModelRow)], tableView: NSTableView, rowOffset: Int, changes: [T: TunnelViewModel.ChangeHandlers.FieldChange]) {
let tableView = self.tableView
func handleSectionFieldsModified<T>(fields: [T], modelRowsInSection: [(isVisible: Bool, modelRow: TableViewModelRow)], rowOffset: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
var modifiedRowIndices = IndexSet()
for (index, field) in fields.enumerated() {
guard let change = changes[field] else { continue }
if case .modified(_) = change {
let row = modelRowsInSection[0 ..< index].filter { $0.isVisible }.count
modifiedRowIndices.insert(rowOffset + row)
}
}
if !modifiedRowIndices.isEmpty {
tableView.reloadData(forRowIndexes: modifiedRowIndices, columnIndexes: IndexSet(integer: 0))
}
}
func handleSectionFieldsAddedOrRemoved<T>(fields: [T], modelRowsInSection: inout [(isVisible: Bool, modelRow: TableViewModelRow)], rowOffset: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
for (index, field) in fields.enumerated() {
guard let change = changes[field] else { continue }
let row = modelRowsInSection[0 ..< index].filter { $0.isVisible }.count
@@ -275,42 +292,54 @@ class TunnelDetailTableViewController: NSViewController {
tableView.removeRows(at: IndexSet(integer: rowOffset + row), withAnimation: .effectFade)
modelRowsInSection[index].isVisible = false
case .modified:
tableView.removeRows(at: IndexSet(integer: rowOffset + row), withAnimation: [])
tableView.insertRows(at: IndexSet(integer: rowOffset + row), withAnimation: [])
break
}
}
}
var isChanged = false
let changeHandlers = TunnelViewModel.ChangeHandlers(
interfaceChanged: { [weak self] changes in
guard let self = self else { return }
sectionChanged(fields: TunnelDetailTableViewController.interfaceFields, modelRowsInSection: &self.tableViewModelRowsBySection[0],
tableView: self.tableView, rowOffset: 0, changes: changes)
isChanged = true
},
peerChangedAt: { [weak self] peerIndex, changes in
guard let self = self else { return }
let sectionIndex = 1 + peerIndex
let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
sectionChanged(fields: TunnelDetailTableViewController.peerFields, modelRowsInSection: &self.tableViewModelRowsBySection[sectionIndex],
tableView: self.tableView, rowOffset: rowOffset, changes: changes)
isChanged = true
},
peersRemovedAt: { [weak self] peerIndices in
guard let self = self else { return }
for peerIndex in peerIndices {
let changes = self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration)
if !changes.interfaceChanges.isEmpty {
handleSectionFieldsModified(fields: TunnelDetailTableViewController.interfaceFields,
modelRowsInSection: self.tableViewModelRowsBySection[0],
rowOffset: 0, changes: changes.interfaceChanges)
}
for (peerIndex, peerChanges) in changes.peerChanges {
let sectionIndex = 1 + peerIndex
let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
handleSectionFieldsModified(fields: TunnelDetailTableViewController.peerFields,
modelRowsInSection: self.tableViewModelRowsBySection[sectionIndex],
rowOffset: rowOffset, changes: peerChanges)
}
let isAnyInterfaceFieldAddedOrRemoved = changes.interfaceChanges.contains { $0.value == .added || $0.value == .removed }
let isAnyPeerFieldAddedOrRemoved = changes.peerChanges.contains { $0.changes.contains { $0.value == .added || $0.value == .removed } }
if isAnyInterfaceFieldAddedOrRemoved || isAnyPeerFieldAddedOrRemoved || !changes.peersRemovedIndices.isEmpty || !changes.peersInsertedIndices.isEmpty {
tableView.beginUpdates()
if isAnyInterfaceFieldAddedOrRemoved {
handleSectionFieldsAddedOrRemoved(fields: TunnelDetailTableViewController.interfaceFields,
modelRowsInSection: &self.tableViewModelRowsBySection[0],
rowOffset: 0, changes: changes.interfaceChanges)
}
if isAnyPeerFieldAddedOrRemoved {
for (peerIndex, peerChanges) in changes.peerChanges {
let sectionIndex = 1 + peerIndex
let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
handleSectionFieldsAddedOrRemoved(fields: TunnelDetailTableViewController.peerFields, modelRowsInSection: &self.tableViewModelRowsBySection[sectionIndex], rowOffset: rowOffset, changes: peerChanges)
}
}
if !changes.peersRemovedIndices.isEmpty {
for peerIndex in changes.peersRemovedIndices {
let sectionIndex = 1 + peerIndex
let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
let count = self.tableViewModelRowsBySection[sectionIndex].filter { $0.isVisible }.count
self.tableView.removeRows(at: IndexSet(integersIn: rowOffset ..< rowOffset + count), withAnimation: .effectFade)
self.tableViewModelRowsBySection.remove(at: sectionIndex)
}
isChanged = true
},
peersInsertedAt: { [weak self] peerIndices in
guard let self = self else { return }
for peerIndex in peerIndices {
}
if !changes.peersInsertedIndices.isEmpty {
for peerIndex in changes.peersInsertedIndices {
let peerData = self.tunnelViewModel.peersData[peerIndex]
let sectionIndex = 1 + peerIndex
let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
@@ -322,16 +351,10 @@ class TunnelDetailTableViewController: NSViewController {
self.tableView.insertRows(at: IndexSet(integersIn: rowOffset ..< rowOffset + count), withAnimation: .effectFade)
self.tableViewModelRowsBySection.insert(modelRowsInSection, at: sectionIndex)
}
isChanged = true
}
)
tableView.beginUpdates()
self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration, changeHandlers: changeHandlers)
if isChanged {
updateTableViewModelRows()
tableView.endUpdates()
}
tableView.endUpdates()
}
private func reloadRuntimeConfiguration() {
@@ -382,6 +405,8 @@ extension TunnelDetailTableViewController: NSTableViewDelegate {
cell.key = tr(format: "macFieldKey (%@)", localizedKeyString)
if field == .persistentKeepAlive {
cell.value = tr(format: "tunnelPeerPersistentKeepaliveValue (%@)", peerData[field])
} else if field == .preSharedKey {
cell.value = tr("tunnelPeerPresharedKeyEnabled")
} else {
cell.value = peerData[field]
}
@@ -219,14 +219,16 @@ class TunnelEditViewController: NSViewController {
}
} else {
// We're creating a new tunnel
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: onDemandSetting) { [weak self] result in
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self)
return
AppStorePrivacyNotice.show(from: self, into: tunnelsManager) { [weak self] in
self?.tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: onDemandSetting) { [weak self] result in
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self)
return
}
let tunnel: TunnelContainer = result.value!
self?.dismiss(self)
self?.delegate?.tunnelSaved(tunnel: tunnel)
}
let tunnel: TunnelContainer = result.value!
self?.dismiss(self)
self?.delegate?.tunnelSaved(tunnel: tunnel)
}
}
}
@@ -38,7 +38,7 @@ class TunnelsListTableViewController: NSViewController {
let addMenu: NSMenu = {
let addMenu = NSMenu(title: "TunnelsListAdd")
addMenu.addItem(withTitle: tr("macMenuAddEmptyTunnel"), action: #selector(handleAddEmptyTunnelAction), keyEquivalent: "n")
addMenu.addItem(withTitle: tr("macMenuImportTunnels"), action: #selector(handleImportTunnelAction), keyEquivalent: "i")
addMenu.addItem(withTitle: tr("macMenuImportTunnels"), action: #selector(handleImportTunnelAction), keyEquivalent: "o")
return addMenu
}()
@@ -42,6 +42,7 @@ class ZipArchive {
static func unarchive(url: URL, requiredFileExtensions: [String]) throws -> [(fileBaseName: String, contents: Data)] {
var results = [(fileBaseName: String, contents: Data)]()
var requiredFileExtensionsLowercased = requiredFileExtensions.map { $0.lowercased() }
guard let zipFile = unzOpen64(url.path) else {
throw ZipArchiveError.cantOpenInputZipFile
@@ -70,7 +71,7 @@ class ZipArchive {
let isDirectory = (lastChar == "/" || lastChar == "\\")
let fileURL = URL(fileURLWithFileSystemRepresentation: fileNameBuffer, isDirectory: isDirectory, relativeTo: nil)
if !isDirectory && requiredFileExtensions.contains(fileURL.pathExtension) {
if !isDirectory && requiredFileExtensionsLowercased.contains(fileURL.pathExtension.lowercased()) {
var unzippedData = Data()
var bytesRead: Int32 = 0
repeat {