Compare commits
302 Commits
0.0.201901
...
0.0.201903
Author | SHA1 | Date | |
---|---|---|---|
a9b925c69b | |||
f93f9d62f4 | |||
fc163fc9ff | |||
adc5a7cac2 | |||
0dd22ca45a | |||
bca9fead5e | |||
51822f722a | |||
121d223229 | |||
439fb6bbac | |||
9c8231dcf7 | |||
0440c4a33a | |||
01be43aa7a | |||
e29c6900e5 | |||
a334c25aff | |||
f56b2ad968 | |||
503ac6c8a2 | |||
5f30e021ef | |||
d748382fce | |||
63299a2752 | |||
b7f8f74b56 | |||
8e5a9215de | |||
64925cab89 | |||
062b4d4b16 | |||
d9bdc61fb9 | |||
0ae8d25134 | |||
574d8433b3 | |||
bd339e2876 | |||
fff75adfe1 | |||
01604dd8d1 | |||
bdeb89a9e5 | |||
36dc252512 | |||
5941bf181c | |||
7a450089c0 | |||
5d757982ba | |||
3767a12983 | |||
9795b0609a | |||
0f98312d15 | |||
f81275812c | |||
b2b5e0e379 | |||
a6f80135ef | |||
e23c221aff | |||
50bc994762 | |||
3e05da4486 | |||
c750f28c67 | |||
f6c70500a7 | |||
663923864c | |||
9250780ffc | |||
9bc17034dd | |||
db6f0729c6 | |||
4503c11b0c | |||
fe4f8b666d | |||
90c0f7e92e | |||
3afcee04be | |||
202e7a4890 | |||
d7b16ffb1f | |||
b1dabf5a00 | |||
a389bd93cb | |||
b2a2110d8c | |||
5ed28907ec | |||
ab6d714070 | |||
d3df8734c2 | |||
ea5996abe0 | |||
ce405f856e | |||
98a967acc8 | |||
b01d09dfb5 | |||
7a580e8941 | |||
39fb52a2e3 | |||
69a064d954 | |||
eb684ef711 | |||
b0eff424f9 | |||
c195760b15 | |||
ba3f0db92c | |||
5031a7db4c | |||
a355232e09 | |||
6f7214ff38 | |||
4c88f477a2 | |||
2fb9d6af71 | |||
38ac66071c | |||
910fdfc321 | |||
c38a88988b | |||
cf51344444 | |||
fda36b571d | |||
1be3224d7a | |||
ca088e9ddc | |||
fcca2d4fec | |||
58181a4d40 | |||
cf816ede13 | |||
fbac282bdc | |||
61f5e017c8 | |||
4547e01283 | |||
5792db22a6 | |||
9d5aa1d8fa | |||
6331b81b5d | |||
77f929789c | |||
b5b72b309f | |||
966fa7909b | |||
115059f2bb | |||
e53c2d4d17 | |||
0a3a5ee900 | |||
7720307fc9 | |||
ea827e2ebd | |||
91b1734b7a | |||
bac4851e95 | |||
0e2556544e | |||
2ec51ba8cf | |||
998bbd73e4 | |||
38a6ba7091 | |||
407b367c8d | |||
a231410c52 | |||
f518c00722 | |||
0539929d0c | |||
05547861b6 | |||
9eed5fd898 | |||
1b8b9ed7ee | |||
ef6af03412 | |||
a99a755c34 | |||
ecd66defe5 | |||
1f3ec042e0 | |||
631e9bb70d | |||
446c3e3698 | |||
02e9172940 | |||
8676f3a663 | |||
394a0cbeb0 | |||
868fee0477 | |||
0cddb562fc | |||
bebcaa012b | |||
ed8dc516dc | |||
8c3557a907 | |||
a26d620f11 | |||
30a73a75fd | |||
71d26b4122 | |||
71525c9d4e | |||
02a96d4566 | |||
466db151b8 | |||
1be133f269 | |||
80de2ac6ac | |||
8a6a60482c | |||
657ec34d19 | |||
f7a31ca7bb | |||
3c61db3a21 | |||
618d89941a | |||
cbc602245e | |||
ca61e09536 | |||
4ff6105053 | |||
4134baced1 | |||
0c5739db82 | |||
1f51ff6b17 | |||
08e5d65045 | |||
1189b3d700 | |||
f292a0ec7a | |||
3b29578524 | |||
70ac48ceba | |||
acecc70397 | |||
b0bb2e993a | |||
d1f83d167e | |||
a796c6c485 | |||
6ad3487a9d | |||
eabeb8ff05 | |||
52eec55d36 | |||
3c80490273 | |||
c36a9e4ffd | |||
812e660491 | |||
2fe9f83ba5 | |||
22625e8cc4 | |||
ab3cbee6a2 | |||
fadef52e1b | |||
19f353127e | |||
e5a76be6fd | |||
76527ef0f8 | |||
11e44f9ae5 | |||
77d4a02139 | |||
54f45cb3f8 | |||
704de3b26c | |||
d05b735703 | |||
9f362e8cb0 | |||
2677efc9bf | |||
2aad8cf0e8 | |||
668c4a475c | |||
557b093232 | |||
6f9fa35c5a | |||
d37e230ee0 | |||
e812683d6c | |||
6d8e5cf3ed | |||
244a2df2dd | |||
0848765f50 | |||
8951ea338f | |||
273ee04450 | |||
e2b068af1a | |||
4e2f4e7124 | |||
6509009afc | |||
0b2a4c2811 | |||
e1198b6e08 | |||
d7a88300f6 | |||
b22aeaeb20 | |||
e0798b8a97 | |||
1bc2177883 | |||
15517c4c3d | |||
f6c9ce2d71 | |||
5aa0f1a25f | |||
602639ee25 | |||
d06887d435 | |||
470e4e7f7f | |||
86165d25f7 | |||
69b973efdc | |||
dc8f27c5c3 | |||
796342ddec | |||
b7a5b8eff1 | |||
8a0245190c | |||
98775bf8a0 | |||
7d5eb476e4 | |||
c477d24d67 | |||
bf9cb092a9 | |||
dbd5108475 | |||
47c5f23a0a | |||
a2871e63a7 | |||
811714e21a | |||
f63c9fd598 | |||
e29cf19fdd | |||
26ea353933 | |||
cd5427ef92 | |||
595f7943e2 | |||
55f022688d | |||
96bd50504b | |||
97fec0d992 | |||
470532d146 | |||
321b88864c | |||
ba731e0099 | |||
70848c04de | |||
13e8c6b178 | |||
53e915c578 | |||
bc8ea55023 | |||
e0af06844d | |||
8980b5a524 | |||
df8ab96139 | |||
4a8366421f | |||
c9ee549a2e | |||
f5059ce55b | |||
5a73244ec9 | |||
922b6f76b2 | |||
fc9e2de72c | |||
80977b95de | |||
bbeb732ef3 | |||
94c4922913 | |||
fc03c635c1 | |||
b0612df990 | |||
c2a6241b5c | |||
96fa6d3ba6 | |||
64fe415879 | |||
59bfa7f1df | |||
c2633987c3 | |||
f7b2f73015 | |||
c72f7056b3 | |||
cb778fe7e0 | |||
f3c2904241 | |||
df8b400850 | |||
252d940d34 | |||
efb64b1959 | |||
dfc4b37518 | |||
60cfceec4f | |||
361830a69e | |||
f6ea25573b | |||
de12c27d5b | |||
a221cb566b | |||
f33cd0b6fd | |||
38bb0faf86 | |||
8d9c5e2950 | |||
09f4be17de | |||
60e18dfdd5 | |||
5bc0c5b2b4 | |||
4a4eeb4a21 | |||
ada7db3dca | |||
c946c0ea48 | |||
4a4690b5fa | |||
37fce31d16 | |||
7934d6b0c7 | |||
98e9088aba | |||
2c81c3a379 | |||
04f6ee0f11 | |||
545f8c88f4 | |||
6a27626fc0 | |||
fb1607d4a2 | |||
51a2c272b9 | |||
b5751b6321 | |||
110012dbcc | |||
5c7a149167 | |||
629009d3be | |||
d7d4355f5e | |||
55d6961a2f | |||
c8cd663a05 | |||
a754c4d7ab | |||
95415cd917 | |||
b32b897181 | |||
d5c1acb57e | |||
573f9640de | |||
f6772dc353 | |||
0cbe66df99 | |||
3cd33ebe8f | |||
c7a40d3cb0 | |||
d02b0fd10e | |||
09d7a5229a | |||
1ca5407b85 | |||
10982a57ef |
3
.gitignore
vendored
@ -42,3 +42,6 @@ output
|
||||
|
||||
# Wireguard specific
|
||||
WireGuard/WireGuard/Config/Developer.xcconfig
|
||||
|
||||
# Vim
|
||||
.*.sw*
|
||||
|
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "wireguard-go"]
|
||||
path = wireguard-go
|
||||
url = https://git.zx2c4.com/wireguard-go
|
140
MOBILECONFIG.md
Normal file
@ -0,0 +1,140 @@
|
||||
# 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
|
21
README.md
@ -1,21 +1,14 @@
|
||||
# [WireGuard](https://www.wireguard.com/) for iOS
|
||||
# [WireGuard](https://www.wireguard.com/) for iOS and macOS
|
||||
|
||||
## Building
|
||||
|
||||
- Clone this repo:
|
||||
- Clone this repo recursively:
|
||||
|
||||
```
|
||||
$ git clone https://git.zx2c4.com/wireguard-ios
|
||||
$ git clone --recursive https://git.zx2c4.com/wireguard-ios
|
||||
$ cd wireguard-ios
|
||||
```
|
||||
|
||||
- Init and update submodule:
|
||||
|
||||
```
|
||||
$ git submodule init
|
||||
$ git submodule update
|
||||
```
|
||||
|
||||
- Rename and populate developer team ID file:
|
||||
|
||||
```
|
||||
@ -23,19 +16,19 @@ $ cp WireGuard/WireGuard/Config/Developer.xcconfig.template WireGuard/WireGuard/
|
||||
$ vim WireGuard/WireGuard/Config/Developer.xcconfig
|
||||
```
|
||||
|
||||
- Install swiftlint:
|
||||
- Install swiftlint and go:
|
||||
|
||||
```
|
||||
$ brew install swiftlint
|
||||
$ brew install swiftlint go
|
||||
```
|
||||
|
||||
- Open project in XCode:
|
||||
- Open project in Xcode:
|
||||
|
||||
```
|
||||
$ open ./WireGuard/WireGuard.xcodeproj
|
||||
```
|
||||
|
||||
- Flip switches, press buttons, and make whirling noises until XCode builds it.
|
||||
- Flip switches, press buttons, and make whirling noises until Xcode builds it.
|
||||
|
||||
## MIT License
|
||||
|
||||
|
@ -2,6 +2,11 @@ disabled_rules:
|
||||
- line_length
|
||||
- trailing_whitespace
|
||||
- todo
|
||||
- cyclomatic_complexity
|
||||
- file_length
|
||||
- type_body_length
|
||||
- function_body_length
|
||||
- nesting
|
||||
opt_in_rules:
|
||||
- empty_count
|
||||
- empty_string
|
||||
@ -14,11 +19,7 @@ opt_in_rules:
|
||||
- toggle_bool
|
||||
- unneeded_parentheses_in_closure_argument
|
||||
- unused_import
|
||||
# - trailing_closure
|
||||
file_length:
|
||||
warning: 500
|
||||
cyclomatic_complexity:
|
||||
warning: 10
|
||||
error: 25
|
||||
function_body_length:
|
||||
warning: 45
|
||||
- trailing_closure
|
||||
variable_name:
|
||||
min_length:
|
||||
warning: 0
|
||||
|
@ -5,8 +5,18 @@ import Foundation
|
||||
import os.log
|
||||
|
||||
extension FileManager {
|
||||
static var appGroupId: String? {
|
||||
#if os(iOS)
|
||||
let appGroupIdInfoDictionaryKey = "com.wireguard.ios.app_group_id"
|
||||
#elseif os(macOS)
|
||||
let appGroupIdInfoDictionaryKey = "com.wireguard.macos.app_group_id"
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
return Bundle.main.object(forInfoDictionaryKey: appGroupIdInfoDictionaryKey) as? String
|
||||
}
|
||||
private static var sharedFolderURL: URL? {
|
||||
guard let appGroupId = Bundle.main.object(forInfoDictionaryKey: "com.wireguard.ios.app_group_id") as? String else {
|
||||
guard let appGroupId = FileManager.appGroupId else {
|
||||
os_log("Cannot obtain app group ID from bundle", log: OSLog.default, type: .error)
|
||||
return nil
|
||||
}
|
||||
@ -17,7 +27,7 @@ extension FileManager {
|
||||
return sharedFolderURL
|
||||
}
|
||||
|
||||
static var networkExtensionLogFileURL: URL? {
|
||||
static var logFileURL: URL? {
|
||||
return sharedFolderURL?.appendingPathComponent("tunnel-log.bin")
|
||||
}
|
||||
|
||||
@ -25,14 +35,6 @@ extension FileManager {
|
||||
return sharedFolderURL?.appendingPathComponent("last-error.txt")
|
||||
}
|
||||
|
||||
static var appLogFileURL: URL? {
|
||||
guard let documentDirURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
|
||||
wg_log(.error, message: "Cannot obtain app documents folder URL")
|
||||
return nil
|
||||
}
|
||||
return documentDirURL.appendingPathComponent("app-log.bin")
|
||||
}
|
||||
|
||||
static func deleteFile(at url: URL) -> Bool {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
|
117
WireGuard/Shared/Keychain.swift
Normal file
@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
class Keychain {
|
||||
static func openReference(called ref: Data) -> String? {
|
||||
var result: CFTypeRef?
|
||||
let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
|
||||
kSecValuePersistentRef as String: ref,
|
||||
kSecReturnData as String: true] as CFDictionary,
|
||||
&result)
|
||||
if ret != errSecSuccess || result == nil {
|
||||
wg_log(.error, message: "Unable to open config from keychain: \(ret)")
|
||||
return nil
|
||||
}
|
||||
guard let data = result as? Data else { return nil }
|
||||
return String(data: data, encoding: String.Encoding.utf8)
|
||||
}
|
||||
|
||||
static func makeReference(containing value: String, called name: String, previouslyReferencedBy oldRef: Data? = nil) -> Data? {
|
||||
var ret: OSStatus
|
||||
guard var id = Bundle.main.bundleIdentifier else {
|
||||
wg_log(.error, staticMessage: "Unable to determine bundle identifier")
|
||||
return nil
|
||||
}
|
||||
if id.hasSuffix(".network-extension") {
|
||||
id.removeLast(".network-extension".count)
|
||||
}
|
||||
var items: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrLabel as String: "WireGuard Tunnel: " + name,
|
||||
kSecAttrAccount as String: name + ": " + UUID().uuidString,
|
||||
kSecAttrDescription as String: "wg-quick(8) config",
|
||||
kSecAttrService as String: id,
|
||||
kSecValueData as String: value.data(using: .utf8) as Any,
|
||||
kSecReturnPersistentRef as String: true]
|
||||
|
||||
#if os(iOS)
|
||||
items[kSecAttrAccessGroup as String] = FileManager.appGroupId
|
||||
items[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
||||
#elseif os(macOS)
|
||||
items[kSecAttrSynchronizable as String] = false
|
||||
items[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
||||
|
||||
guard let extensionPath = Bundle.main.builtInPlugInsURL?.appendingPathComponent("WireGuardNetworkExtension.appex").path else {
|
||||
wg_log(.error, staticMessage: "Unable to determine app extension path")
|
||||
return nil
|
||||
}
|
||||
var extensionApp: SecTrustedApplication?
|
||||
var mainApp: SecTrustedApplication?
|
||||
ret = SecTrustedApplicationCreateFromPath(extensionPath, &extensionApp)
|
||||
if ret != kOSReturnSuccess || extensionApp == nil {
|
||||
wg_log(.error, message: "Unable to create keychain extension trusted application object: \(ret)")
|
||||
return nil
|
||||
}
|
||||
ret = SecTrustedApplicationCreateFromPath(nil, &mainApp)
|
||||
if ret != errSecSuccess || mainApp == nil {
|
||||
wg_log(.error, message: "Unable to create keychain local trusted application object: \(ret)")
|
||||
return nil
|
||||
}
|
||||
var access: SecAccess?
|
||||
ret = SecAccessCreate((items[kSecAttrLabel as String] as? String)! as CFString,
|
||||
[extensionApp!, mainApp!] as CFArray,
|
||||
&access)
|
||||
if ret != errSecSuccess || access == nil {
|
||||
wg_log(.error, message: "Unable to create keychain ACL object: \(ret)")
|
||||
return nil
|
||||
}
|
||||
items[kSecAttrAccess as String] = access!
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
|
||||
var ref: CFTypeRef?
|
||||
ret = SecItemAdd(items as CFDictionary, &ref)
|
||||
if ret != errSecSuccess || ref == nil {
|
||||
wg_log(.error, message: "Unable to add config to keychain: \(ret)")
|
||||
return nil
|
||||
}
|
||||
if let oldRef = oldRef {
|
||||
deleteReference(called: oldRef)
|
||||
}
|
||||
return ref as? Data
|
||||
}
|
||||
|
||||
static func deleteReference(called ref: Data) {
|
||||
let ret = SecItemDelete([kSecValuePersistentRef as String: ref] as CFDictionary)
|
||||
if ret != errSecSuccess {
|
||||
wg_log(.error, message: "Unable to delete config from keychain: \(ret)")
|
||||
}
|
||||
}
|
||||
|
||||
static func deleteReferences(except whitelist: Set<Data>) {
|
||||
var result: CFTypeRef?
|
||||
let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: Bundle.main.bundleIdentifier as Any,
|
||||
kSecMatchLimit as String: kSecMatchLimitAll,
|
||||
kSecReturnPersistentRef as String: true] as CFDictionary,
|
||||
&result)
|
||||
if ret != errSecSuccess || result == nil {
|
||||
return
|
||||
}
|
||||
guard let items = result as? [Data] else { return }
|
||||
for item in items {
|
||||
if !whitelist.contains(item) {
|
||||
deleteReference(called: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func verifyReference(called ref: Data) -> Bool {
|
||||
return SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
|
||||
kSecValuePersistentRef as String: ref] as CFDictionary,
|
||||
nil) == errSecSuccess
|
||||
}
|
||||
}
|
@ -12,10 +12,12 @@ public class Logger {
|
||||
static var global: Logger?
|
||||
|
||||
var log: OpaquePointer
|
||||
var tag: String
|
||||
|
||||
init(withFilePath filePath: String) throws {
|
||||
init(tagged tag: String, withFilePath filePath: String) throws {
|
||||
guard let log = open_log(filePath) else { throw LoggerError.openFailure }
|
||||
self.log = log
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -23,17 +25,14 @@ public class Logger {
|
||||
}
|
||||
|
||||
func log(message: String) {
|
||||
write_msg_to_log(log, message.trimmingCharacters(in: .newlines))
|
||||
write_msg_to_log(log, tag, message.trimmingCharacters(in: .newlines))
|
||||
}
|
||||
|
||||
func writeLog(called ourTag: String, mergedWith otherLogFile: String, called otherTag: String, to targetFile: String) -> Bool {
|
||||
guard let other = open_log(otherLogFile) else { return false }
|
||||
let ret = write_logs_to_file(targetFile, log, ourTag, other, otherTag)
|
||||
close_log(other)
|
||||
return ret == 0
|
||||
func writeLog(to targetFile: String) -> Bool {
|
||||
return write_log_to_file(targetFile, self.log) == 0
|
||||
}
|
||||
|
||||
static func configureGlobal(withFilePath filePath: String?) {
|
||||
static func configureGlobal(tagged tag: String, withFilePath filePath: String?) {
|
||||
if Logger.global != nil {
|
||||
return
|
||||
}
|
||||
@ -41,19 +40,17 @@ public class Logger {
|
||||
os_log("Unable to determine log destination path. Log will not be saved to file.", log: OSLog.default, type: .error)
|
||||
return
|
||||
}
|
||||
do {
|
||||
try Logger.global = Logger(withFilePath: filePath)
|
||||
} catch {
|
||||
guard let logger = try? Logger(tagged: tag, withFilePath: filePath) else {
|
||||
os_log("Unable to open log file for writing. Log will not be saved to file.", log: OSLog.default, type: .error)
|
||||
return
|
||||
}
|
||||
Logger.global = logger
|
||||
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
|
||||
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
||||
appVersion += " (\(appBuild))"
|
||||
}
|
||||
let goBackendVersion = WIREGUARD_GO_VERSION
|
||||
Logger.global?.log(message: "App version: \(appVersion); Go backend version: \(goBackendVersion)")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,8 @@
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
@ -19,100 +21,124 @@
|
||||
|
||||
enum {
|
||||
MAX_LOG_LINE_LENGTH = 512,
|
||||
MAX_LINES = 1024,
|
||||
MAGIC = 0xbeefbabeU
|
||||
MAX_LINES = 2048,
|
||||
MAGIC = 0xabadbeefU
|
||||
};
|
||||
|
||||
struct log_line {
|
||||
struct timeval tv;
|
||||
atomic_uint_fast64_t time_ns;
|
||||
char line[MAX_LOG_LINE_LENGTH];
|
||||
};
|
||||
|
||||
struct log {
|
||||
struct { uint32_t first, len; } header;
|
||||
atomic_uint_fast32_t next_index;
|
||||
struct log_line lines[MAX_LINES];
|
||||
uint32_t magic;
|
||||
};
|
||||
|
||||
void write_msg_to_log(struct log *log, const char *msg)
|
||||
void write_msg_to_log(struct log *log, const char *tag, const char *msg)
|
||||
{
|
||||
struct log_line *line = &log->lines[(log->header.first + log->header.len) % MAX_LINES];
|
||||
uint32_t index;
|
||||
struct log_line *line;
|
||||
struct timespec ts;
|
||||
|
||||
if (log->header.len == MAX_LINES)
|
||||
log->header.first = (log->header.first + 1) % MAX_LINES;
|
||||
else
|
||||
++log->header.len;
|
||||
// Race: This isn't synchronized with the fetch_add below, so items might be slightly out of order.
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
|
||||
gettimeofday(&line->tv, NULL);
|
||||
strncpy(line->line, msg, MAX_LOG_LINE_LENGTH - 1);
|
||||
line->line[MAX_LOG_LINE_LENGTH - 1] = '\0';
|
||||
// Race: More than MAX_LINES writers and this will clash.
|
||||
index = atomic_fetch_add(&log->next_index, 1);
|
||||
line = &log->lines[index % MAX_LINES];
|
||||
|
||||
msync(&log->header, sizeof(log->header), MS_ASYNC);
|
||||
// Race: Before this line executes, we'll display old data after new data.
|
||||
atomic_store(&line->time_ns, 0);
|
||||
memset(line->line, 0, MAX_LOG_LINE_LENGTH);
|
||||
|
||||
snprintf(line->line, MAX_LOG_LINE_LENGTH, "[%s] %s", tag, msg);
|
||||
atomic_store(&line->time_ns, ts.tv_sec * 1000000000ULL + ts.tv_nsec);
|
||||
|
||||
msync(&log->next_index, sizeof(log->next_index), MS_ASYNC);
|
||||
msync(line, sizeof(*line), MS_ASYNC);
|
||||
}
|
||||
|
||||
static bool first_before_second(const struct log_line *line1, const struct log_line *line2)
|
||||
int write_log_to_file(const char *file_name, const struct log *input_log)
|
||||
{
|
||||
if (line1->tv.tv_sec <= line2->tv.tv_sec)
|
||||
return true;
|
||||
if (line1->tv.tv_sec == line2->tv.tv_sec)
|
||||
return line1->tv.tv_usec <= line2->tv.tv_usec;
|
||||
return false;
|
||||
}
|
||||
|
||||
int write_logs_to_file(const char *file_name, const struct log *log1, const char *tag1, const struct log *log2, const char *tag2)
|
||||
{
|
||||
uint32_t i1, i2, len1 = log1->header.len, len2 = log2->header.len;
|
||||
struct log *log;
|
||||
uint32_t l, i;
|
||||
FILE *file;
|
||||
int ret;
|
||||
|
||||
if (len1 > MAX_LINES)
|
||||
len1 = MAX_LINES;
|
||||
if (len2 > MAX_LINES)
|
||||
len2 = MAX_LINES;
|
||||
log = malloc(sizeof(*log));
|
||||
if (!log)
|
||||
return -errno;
|
||||
memcpy(log, input_log, sizeof(*log));
|
||||
|
||||
file = fopen(file_name, "w");
|
||||
if (!file)
|
||||
if (!file) {
|
||||
free(log);
|
||||
return -errno;
|
||||
}
|
||||
|
||||
for (i1 = 0, i2 = 0;;) {
|
||||
for (l = 0, i = log->next_index; l < MAX_LINES; ++l, ++i) {
|
||||
const struct log_line *line = &log->lines[i % MAX_LINES];
|
||||
time_t seconds = line->time_ns / 1000000000ULL;
|
||||
uint32_t useconds = (line->time_ns % 1000000000ULL) / 1000ULL;
|
||||
struct tm tm;
|
||||
char buf[MAX_LOG_LINE_LENGTH];
|
||||
const struct log_line *line1 = &log1->lines[(log1->header.first + i1) % MAX_LINES];
|
||||
const struct log_line *line2 = &log2->lines[(log2->header.first + i2) % MAX_LINES];
|
||||
const struct log_line *line;
|
||||
const char *tag;
|
||||
|
||||
if (i1 < len1 && (i2 >= len2 || first_before_second(line1, line2))) {
|
||||
line = line1;
|
||||
tag = tag1;
|
||||
++i1;
|
||||
} else if (i2 < len2 && (i1 >= len1 || first_before_second(line2, line1))) {
|
||||
line = line2;
|
||||
tag = tag2;
|
||||
++i2;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
if (!line->time_ns)
|
||||
continue;
|
||||
|
||||
memcpy(buf, line->line, MAX_LOG_LINE_LENGTH);
|
||||
buf[MAX_LOG_LINE_LENGTH - 1] = '\0';
|
||||
if (!localtime_r(&line->tv.tv_sec, &tm))
|
||||
if (!localtime_r(&seconds, &tm))
|
||||
goto err;
|
||||
if (fprintf(file, "%04d-%02d-%02d %02d:%02d:%02d.%06d: [%s] %s\n",
|
||||
|
||||
if (fprintf(file, "%04d-%02d-%02d %02d:%02d:%02d.%06d: %s\n",
|
||||
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
|
||||
tm.tm_hour, tm.tm_min, tm.tm_sec, line->tv.tv_usec,
|
||||
tag, buf) < 0)
|
||||
tm.tm_hour, tm.tm_min, tm.tm_sec, useconds,
|
||||
line->line) < 0)
|
||||
goto err;
|
||||
|
||||
|
||||
}
|
||||
errno = 0;
|
||||
|
||||
err:
|
||||
ret = -errno;
|
||||
fclose(file);
|
||||
free(log);
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint32_t view_lines_from_cursor(const struct log *input_log, uint32_t cursor, void(*cb)(const char *, uint64_t))
|
||||
{
|
||||
struct log *log;
|
||||
uint32_t l, i = cursor;
|
||||
|
||||
log = malloc(sizeof(*log));
|
||||
if (!log)
|
||||
return cursor;
|
||||
memcpy(log, input_log, sizeof(*log));
|
||||
|
||||
if (i == -1)
|
||||
i = log->next_index;
|
||||
|
||||
for (l = 0; l < MAX_LINES; ++l, ++i) {
|
||||
const struct log_line *line = &log->lines[i % MAX_LINES];
|
||||
|
||||
if (cursor != -1 && i % MAX_LINES == log->next_index % MAX_LINES)
|
||||
break;
|
||||
|
||||
if (!line->time_ns) {
|
||||
if (cursor == -1)
|
||||
continue;
|
||||
else
|
||||
break;
|
||||
}
|
||||
cb(line->line, line->time_ns);
|
||||
cursor = (i + 1) % MAX_LINES;
|
||||
}
|
||||
free(log);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
struct log *open_log(const char *file_name)
|
||||
{
|
||||
int fd;
|
||||
|
@ -6,9 +6,12 @@
|
||||
#ifndef RINGLOGGER_H
|
||||
#define RINGLOGGER_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct log;
|
||||
void write_msg_to_log(struct log *log, const char *msg);
|
||||
int write_logs_to_file(const char *file_name, const struct log *log1, const char *tag1, const struct log *log2, const char *tag2);
|
||||
void write_msg_to_log(struct log *log, const char *tag, const char *msg);
|
||||
int write_log_to_file(const char *file_name, const struct log *input_log);
|
||||
uint32_t view_lines_from_cursor(const struct log *input_log, uint32_t cursor, void(*)(const char *, uint64_t));
|
||||
struct log *open_log(const char *file_name);
|
||||
void close_log(struct log *log);
|
||||
|
||||
|
63
WireGuard/Shared/Logging/test_ringlogger.c
Normal file
@ -0,0 +1,63 @@
|
||||
#include "ringlogger.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <inttypes.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
static void forkwrite(void)
|
||||
{
|
||||
struct log *log = open_log("/tmp/test_log");
|
||||
char c[512];
|
||||
int i, base;
|
||||
bool in_fork = !fork();
|
||||
|
||||
base = 10000 * in_fork;
|
||||
for (i = 0; i < 1024; ++i) {
|
||||
snprintf(c, 512, "bla bla bla %d", base + i);
|
||||
write_msg_to_log(log, "HMM", c);
|
||||
}
|
||||
|
||||
|
||||
if (in_fork)
|
||||
_exit(0);
|
||||
wait(NULL);
|
||||
|
||||
write_log_to_file("/dev/stdout", log);
|
||||
close_log(log);
|
||||
}
|
||||
|
||||
static void writetext(const char *text)
|
||||
{
|
||||
struct log *log = open_log("/tmp/test_log");
|
||||
write_msg_to_log(log, "TXT", text);
|
||||
close_log(log);
|
||||
}
|
||||
|
||||
static void show_line(const char *line, uint64_t time_ns)
|
||||
{
|
||||
printf("%" PRIu64 ": %s\n", time_ns, line);
|
||||
}
|
||||
|
||||
static void follow(void)
|
||||
{
|
||||
uint32_t cursor = -1;
|
||||
struct log *log = open_log("/tmp/test_log");
|
||||
|
||||
for (;;) {
|
||||
cursor = view_lines_from_cursor(log, cursor, show_line);
|
||||
usleep(1000 * 300);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
if (!strcmp(argv[1], "fork"))
|
||||
forkwrite();
|
||||
else if (!strcmp(argv[1], "write"))
|
||||
writetext(argv[2]);
|
||||
else if (!strcmp(argv[1], "follow"))
|
||||
follow();
|
||||
return 0;
|
||||
}
|
54
WireGuard/Shared/Model/Data+KeyEncoding.swift
Normal file
@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
func isKey() -> Bool {
|
||||
return self.count == WG_KEY_LEN
|
||||
}
|
||||
|
||||
func hexKey() -> String? {
|
||||
if self.count != WG_KEY_LEN {
|
||||
return nil
|
||||
}
|
||||
var out = Data(repeating: 0, count: Int(WG_KEY_LEN_HEX))
|
||||
out.withUnsafeMutableBytes { outBytes in
|
||||
self.withUnsafeBytes { inBytes in
|
||||
key_to_hex(outBytes, inBytes)
|
||||
}
|
||||
}
|
||||
out.removeLast()
|
||||
return String(data: out, encoding: .ascii)
|
||||
}
|
||||
|
||||
init?(hexKey hexString: String) {
|
||||
self.init(repeating: 0, count: Int(WG_KEY_LEN))
|
||||
|
||||
if !self.withUnsafeMutableBytes { key_from_hex($0, hexString) } {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func base64Key() -> String? {
|
||||
if self.count != WG_KEY_LEN {
|
||||
return nil
|
||||
}
|
||||
var out = Data(repeating: 0, count: Int(WG_KEY_LEN_BASE64))
|
||||
out.withUnsafeMutableBytes { outBytes in
|
||||
self.withUnsafeBytes { inBytes in
|
||||
key_to_base64(outBytes, inBytes)
|
||||
}
|
||||
}
|
||||
out.removeLast()
|
||||
return String(data: out, encoding: .ascii)
|
||||
}
|
||||
|
||||
init?(base64Key base64String: String) {
|
||||
self.init(repeating: 0, count: Int(WG_KEY_LEN))
|
||||
|
||||
if !self.withUnsafeMutableBytes { key_from_base64($0, base64String) } {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
@ -1,193 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import NetworkExtension
|
||||
|
||||
protocol LegacyModel: Decodable {
|
||||
associatedtype Model
|
||||
|
||||
var migrated: Model { get }
|
||||
}
|
||||
|
||||
struct LegacyDNSServer: LegacyModel {
|
||||
let address: IPAddress
|
||||
|
||||
var migrated: DNSServer {
|
||||
return DNSServer(address: address)
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
var data = try container.decode(Data.self)
|
||||
let ipAddressFromData: IPAddress? = {
|
||||
switch data.count {
|
||||
case 4: return IPv4Address(data)
|
||||
case 16: return IPv6Address(data)
|
||||
default: return nil
|
||||
}
|
||||
}()
|
||||
guard let ipAddress = ipAddressFromData else {
|
||||
throw DecodingError.invalidData
|
||||
}
|
||||
address = ipAddress
|
||||
}
|
||||
|
||||
enum DecodingError: Error {
|
||||
case invalidData
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == LegacyDNSServer {
|
||||
var migrated: [DNSServer] {
|
||||
return map { $0.migrated }
|
||||
}
|
||||
}
|
||||
|
||||
struct LegacyEndpoint: LegacyModel {
|
||||
let host: Network.NWEndpoint.Host
|
||||
let port: Network.NWEndpoint.Port
|
||||
|
||||
var migrated: Endpoint {
|
||||
return Endpoint(host: host, port: port)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let endpointString = try container.decode(String.self)
|
||||
guard !endpointString.isEmpty else { throw DecodingError.invalidData }
|
||||
let startOfPort: String.Index
|
||||
let hostString: String
|
||||
if endpointString.first! == "[" {
|
||||
// Look for IPv6-style endpoint, like [::1]:80
|
||||
let startOfHost = endpointString.index(after: endpointString.startIndex)
|
||||
guard let endOfHost = endpointString.dropFirst().firstIndex(of: "]") else { throw DecodingError.invalidData }
|
||||
let afterEndOfHost = endpointString.index(after: endOfHost)
|
||||
guard endpointString[afterEndOfHost] == ":" else { throw DecodingError.invalidData }
|
||||
startOfPort = endpointString.index(after: afterEndOfHost)
|
||||
hostString = String(endpointString[startOfHost ..< endOfHost])
|
||||
} else {
|
||||
// Look for an IPv4-style endpoint, like 127.0.0.1:80
|
||||
guard let endOfHost = endpointString.firstIndex(of: ":") else { throw DecodingError.invalidData }
|
||||
startOfPort = endpointString.index(after: endOfHost)
|
||||
hostString = String(endpointString[endpointString.startIndex ..< endOfHost])
|
||||
}
|
||||
guard let endpointPort = NWEndpoint.Port(String(endpointString[startOfPort ..< endpointString.endIndex])) else { throw DecodingError.invalidData }
|
||||
let invalidCharacterIndex = hostString.unicodeScalars.firstIndex { char in
|
||||
return !CharacterSet.urlHostAllowed.contains(char)
|
||||
}
|
||||
guard invalidCharacterIndex == nil else { throw DecodingError.invalidData }
|
||||
host = NWEndpoint.Host(hostString)
|
||||
port = endpointPort
|
||||
}
|
||||
|
||||
enum DecodingError: Error {
|
||||
case invalidData
|
||||
}
|
||||
}
|
||||
|
||||
struct LegacyInterfaceConfiguration: LegacyModel {
|
||||
let name: String
|
||||
let privateKey: Data
|
||||
let addresses: [LegacyIPAddressRange]
|
||||
let listenPort: UInt16?
|
||||
let mtu: UInt16?
|
||||
let dns: [LegacyDNSServer]
|
||||
|
||||
var migrated: InterfaceConfiguration {
|
||||
var interface = InterfaceConfiguration(privateKey: privateKey)
|
||||
interface.addresses = addresses.migrated
|
||||
interface.listenPort = listenPort
|
||||
interface.mtu = mtu
|
||||
interface.dns = dns.migrated
|
||||
return interface
|
||||
}
|
||||
}
|
||||
|
||||
struct LegacyIPAddressRange: LegacyModel {
|
||||
let address: IPAddress
|
||||
let networkPrefixLength: UInt8
|
||||
|
||||
var migrated: IPAddressRange {
|
||||
return IPAddressRange(address: address, networkPrefixLength: networkPrefixLength)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
var data = try container.decode(Data.self)
|
||||
networkPrefixLength = data.removeLast()
|
||||
let ipAddressFromData: IPAddress? = {
|
||||
switch data.count {
|
||||
case 4: return IPv4Address(data)
|
||||
case 16: return IPv6Address(data)
|
||||
default: return nil
|
||||
}
|
||||
}()
|
||||
guard let ipAddress = ipAddressFromData else { throw DecodingError.invalidData }
|
||||
address = ipAddress
|
||||
}
|
||||
|
||||
enum DecodingError: Error {
|
||||
case invalidData
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == LegacyIPAddressRange {
|
||||
var migrated: [IPAddressRange] {
|
||||
return map { $0.migrated }
|
||||
}
|
||||
}
|
||||
|
||||
struct LegacyPeerConfiguration: LegacyModel {
|
||||
let publicKey: Data
|
||||
let preSharedKey: Data?
|
||||
let allowedIPs: [LegacyIPAddressRange]
|
||||
let endpoint: LegacyEndpoint?
|
||||
let persistentKeepAlive: UInt16?
|
||||
|
||||
var migrated: PeerConfiguration {
|
||||
var configuration = PeerConfiguration(publicKey: publicKey)
|
||||
configuration.preSharedKey = preSharedKey
|
||||
configuration.allowedIPs = allowedIPs.migrated
|
||||
configuration.endpoint = endpoint?.migrated
|
||||
configuration.persistentKeepAlive = persistentKeepAlive
|
||||
return configuration
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == LegacyPeerConfiguration {
|
||||
var migrated: [PeerConfiguration] {
|
||||
return map { $0.migrated }
|
||||
}
|
||||
}
|
||||
|
||||
final class LegacyTunnelConfiguration: LegacyModel {
|
||||
let interface: LegacyInterfaceConfiguration
|
||||
let peers: [LegacyPeerConfiguration]
|
||||
|
||||
var migrated: TunnelConfiguration {
|
||||
return TunnelConfiguration(name: interface.name, interface: interface.migrated, peers: peers.migrated)
|
||||
}
|
||||
}
|
||||
|
||||
extension NETunnelProviderProtocol {
|
||||
|
||||
@discardableResult
|
||||
func migrateConfigurationIfNeeded() -> Bool {
|
||||
guard let configurationVersion = providerConfiguration?["tunnelConfigurationVersion"] as? Int else { return false }
|
||||
if configurationVersion == 1 {
|
||||
migrateFromConfigurationV1()
|
||||
} else {
|
||||
fatalError("No migration from configuration version \(configurationVersion) exists.")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func migrateFromConfigurationV1() {
|
||||
guard let serializedTunnelConfiguration = providerConfiguration?["tunnelConfiguration"] as? Data else { return }
|
||||
guard let configuration = try? JSONDecoder().decode(LegacyTunnelConfiguration.self, from: serializedTunnelConfiguration) else { return }
|
||||
providerConfiguration = [Keys.wgQuickConfig.rawValue: configuration.migrated.asWgQuickConfig()]
|
||||
}
|
||||
|
||||
}
|
@ -12,17 +12,16 @@ enum PacketTunnelProviderError: String, Error {
|
||||
}
|
||||
|
||||
extension NETunnelProviderProtocol {
|
||||
|
||||
enum Keys: String {
|
||||
case wgQuickConfig = "WgQuickConfig"
|
||||
}
|
||||
|
||||
convenience init?(tunnelConfiguration: TunnelConfiguration) {
|
||||
convenience init?(tunnelConfiguration: TunnelConfiguration, previouslyFrom old: NEVPNProtocol? = nil) {
|
||||
self.init()
|
||||
|
||||
let appId = Bundle.main.bundleIdentifier!
|
||||
guard let name = tunnelConfiguration.name else { return nil }
|
||||
guard let appId = Bundle.main.bundleIdentifier else { return nil }
|
||||
providerBundleIdentifier = "\(appId).network-extension"
|
||||
providerConfiguration = [Keys.wgQuickConfig.rawValue: tunnelConfiguration.asWgQuickConfig()]
|
||||
passwordReference = Keychain.makeReference(containing: tunnelConfiguration.asWgQuickConfig(), called: name, previouslyReferencedBy: old?.passwordReference)
|
||||
if passwordReference == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
let endpoints = tunnelConfiguration.peers.compactMap { $0.endpoint }
|
||||
if endpoints.count == 1 {
|
||||
@ -35,9 +34,37 @@ extension NETunnelProviderProtocol {
|
||||
}
|
||||
|
||||
func asTunnelConfiguration(called name: String? = nil) -> TunnelConfiguration? {
|
||||
migrateConfigurationIfNeeded()
|
||||
guard let serializedConfig = providerConfiguration?[Keys.wgQuickConfig.rawValue] as? String else { return nil }
|
||||
return try? TunnelConfiguration(fromWgQuickConfig: serializedConfig, 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() {
|
||||
guard let ref = passwordReference else { return }
|
||||
Keychain.deleteReference(called: ref)
|
||||
}
|
||||
|
||||
func verifyConfigurationReference() -> Data? {
|
||||
guard let ref = passwordReference else { return nil }
|
||||
return Keychain.verifyReference(called: ref) ? ref : nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func migrateConfigurationIfNeeded(called name: String) -> Bool {
|
||||
/* This is how we did things before we switched to putting items
|
||||
* in the keychain. But it's still useful to keep the migration
|
||||
* around so that .mobileconfig files are easier.
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,9 @@ struct PeerConfiguration {
|
||||
var allowedIPs = [IPAddressRange]()
|
||||
var endpoint: Endpoint?
|
||||
var persistentKeepAlive: UInt16?
|
||||
var rxBytes: UInt64?
|
||||
var txBytes: UInt64?
|
||||
var lastHandshakeTime: Date?
|
||||
|
||||
init(publicKey: Data) {
|
||||
self.publicKey = publicKey
|
||||
|
@ -12,15 +12,29 @@ extension TunnelConfiguration {
|
||||
}
|
||||
|
||||
enum ParseError: Error {
|
||||
case invalidLine(_ line: String.SubSequence)
|
||||
case invalidLine(String.SubSequence)
|
||||
case noInterface
|
||||
case invalidInterface
|
||||
case multipleInterfaces
|
||||
case interfaceHasNoPrivateKey
|
||||
case interfaceHasInvalidPrivateKey(String)
|
||||
case interfaceHasInvalidListenPort(String)
|
||||
case interfaceHasInvalidAddress(String)
|
||||
case interfaceHasInvalidDNS(String)
|
||||
case interfaceHasInvalidMTU(String)
|
||||
case interfaceHasUnrecognizedKey(String)
|
||||
case peerHasNoPublicKey
|
||||
case peerHasInvalidPublicKey(String)
|
||||
case peerHasInvalidPreSharedKey(String)
|
||||
case peerHasInvalidAllowedIP(String)
|
||||
case peerHasInvalidEndpoint(String)
|
||||
case peerHasInvalidPersistentKeepAlive(String)
|
||||
case peerHasInvalidTransferBytes(String)
|
||||
case peerHasInvalidLastHandshakeTime(String)
|
||||
case peerHasUnrecognizedKey(String)
|
||||
case multiplePeersWithSamePublicKey
|
||||
case invalidPeer
|
||||
case multipleEntriesForKey(String)
|
||||
}
|
||||
|
||||
//swiftlint:disable:next function_body_length cyclomatic_complexity
|
||||
convenience init(fromWgQuickConfig wgQuickConfig: String, called name: String? = nil) throws {
|
||||
var interfaceConfiguration: InterfaceConfiguration?
|
||||
var peerConfigurations = [PeerConfiguration]()
|
||||
@ -38,23 +52,39 @@ extension TunnelConfiguration {
|
||||
trimmedLine = String(line)
|
||||
}
|
||||
|
||||
trimmedLine = trimmedLine.trimmingCharacters(in: .whitespaces)
|
||||
trimmedLine = trimmedLine.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let lowercasedLine = trimmedLine.lowercased()
|
||||
|
||||
guard !trimmedLine.isEmpty else { continue }
|
||||
let lowercasedLine = line.lowercased()
|
||||
|
||||
if let equalsIndex = line.firstIndex(of: "=") {
|
||||
// Line contains an attribute
|
||||
let key = line[..<equalsIndex].trimmingCharacters(in: .whitespaces).lowercased()
|
||||
let value = line[line.index(equalsIndex, offsetBy: 1)...].trimmingCharacters(in: .whitespaces)
|
||||
let keysWithMultipleEntriesAllowed: Set<String> = ["address", "allowedips", "dns"]
|
||||
if let presentValue = attributes[key], keysWithMultipleEntriesAllowed.contains(key) {
|
||||
attributes[key] = presentValue + "," + value
|
||||
} else {
|
||||
attributes[key] = value
|
||||
if !trimmedLine.isEmpty {
|
||||
if let equalsIndex = trimmedLine.firstIndex(of: "=") {
|
||||
// Line contains an attribute
|
||||
let keyWithCase = trimmedLine[..<equalsIndex].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let key = keyWithCase.lowercased()
|
||||
let value = trimmedLine[trimmedLine.index(equalsIndex, offsetBy: 1)...].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let keysWithMultipleEntriesAllowed: Set<String> = ["address", "allowedips", "dns"]
|
||||
if let presentValue = attributes[key] {
|
||||
if keysWithMultipleEntriesAllowed.contains(key) {
|
||||
attributes[key] = presentValue + "," + value
|
||||
} else {
|
||||
throw ParseError.multipleEntriesForKey(keyWithCase)
|
||||
}
|
||||
} else {
|
||||
attributes[key] = value
|
||||
}
|
||||
let interfaceSectionKeys: Set<String> = ["privatekey", "listenport", "address", "dns", "mtu"]
|
||||
let peerSectionKeys: Set<String> = ["publickey", "presharedkey", "allowedips", "endpoint", "persistentkeepalive"]
|
||||
if parserState == .inInterfaceSection {
|
||||
guard interfaceSectionKeys.contains(key) else {
|
||||
throw ParseError.interfaceHasUnrecognizedKey(keyWithCase)
|
||||
}
|
||||
} else if parserState == .inPeerSection {
|
||||
guard peerSectionKeys.contains(key) else {
|
||||
throw ParseError.peerHasUnrecognizedKey(keyWithCase)
|
||||
}
|
||||
}
|
||||
} else if lowercasedLine != "[interface]" && lowercasedLine != "[peer]" {
|
||||
throw ParseError.invalidLine(line)
|
||||
}
|
||||
} else if lowercasedLine != "[interface]" && lowercasedLine != "[peer]" {
|
||||
throw ParseError.invalidLine(line)
|
||||
}
|
||||
|
||||
let isLastLine = lineIndex == lines.count - 1
|
||||
@ -62,11 +92,11 @@ extension TunnelConfiguration {
|
||||
if isLastLine || lowercasedLine == "[interface]" || lowercasedLine == "[peer]" {
|
||||
// Previous section has ended; process the attributes collected so far
|
||||
if parserState == .inInterfaceSection {
|
||||
guard let interface = TunnelConfiguration.collate(interfaceAttributes: attributes) else { throw ParseError.invalidInterface }
|
||||
let interface = try TunnelConfiguration.collate(interfaceAttributes: attributes)
|
||||
guard interfaceConfiguration == nil else { throw ParseError.multipleInterfaces }
|
||||
interfaceConfiguration = interface
|
||||
} else if parserState == .inPeerSection {
|
||||
guard let peer = TunnelConfiguration.collate(peerAttributes: attributes) else { throw ParseError.invalidPeer }
|
||||
let peer = try TunnelConfiguration.collate(peerAttributes: attributes)
|
||||
peerConfigurations.append(peer)
|
||||
}
|
||||
}
|
||||
@ -95,7 +125,9 @@ extension TunnelConfiguration {
|
||||
|
||||
func asWgQuickConfig() -> String {
|
||||
var output = "[Interface]\n"
|
||||
output.append("PrivateKey = \(interface.privateKey.base64EncodedString())\n")
|
||||
if let privateKey = interface.privateKey.base64Key() {
|
||||
output.append("PrivateKey = \(privateKey)\n")
|
||||
}
|
||||
if let listenPort = interface.listenPort {
|
||||
output.append("ListenPort = \(listenPort)\n")
|
||||
}
|
||||
@ -113,9 +145,11 @@ extension TunnelConfiguration {
|
||||
|
||||
for peer in peers {
|
||||
output.append("\n[Peer]\n")
|
||||
output.append("PublicKey = \(peer.publicKey.base64EncodedString())\n")
|
||||
if let preSharedKey = peer.preSharedKey {
|
||||
output.append("PresharedKey = \(preSharedKey.base64EncodedString())\n")
|
||||
if let publicKey = peer.publicKey.base64Key() {
|
||||
output.append("PublicKey = \(publicKey)\n")
|
||||
}
|
||||
if let preSharedKey = peer.preSharedKey?.base64Key() {
|
||||
output.append("PresharedKey = \(preSharedKey)\n")
|
||||
}
|
||||
if !peer.allowedIPs.isEmpty {
|
||||
let allowedIPsString = peer.allowedIPs.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||
@ -132,66 +166,83 @@ extension TunnelConfiguration {
|
||||
return output
|
||||
}
|
||||
|
||||
//swiftlint:disable:next cyclomatic_complexity
|
||||
private static func collate(interfaceAttributes attributes: [String: String]) -> InterfaceConfiguration? {
|
||||
// required wg fields
|
||||
guard let privateKeyString = attributes["privatekey"] else { return nil }
|
||||
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else { return nil }
|
||||
private static func collate(interfaceAttributes attributes: [String: String]) throws -> InterfaceConfiguration {
|
||||
guard let privateKeyString = attributes["privatekey"] else {
|
||||
throw ParseError.interfaceHasNoPrivateKey
|
||||
}
|
||||
guard let privateKey = Data(base64Key: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
|
||||
throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
|
||||
}
|
||||
var interface = InterfaceConfiguration(privateKey: privateKey)
|
||||
// other wg fields
|
||||
if let listenPortString = attributes["listenport"] {
|
||||
guard let listenPort = UInt16(listenPortString) else { return nil }
|
||||
guard let listenPort = UInt16(listenPortString) else {
|
||||
throw ParseError.interfaceHasInvalidListenPort(listenPortString)
|
||||
}
|
||||
interface.listenPort = listenPort
|
||||
}
|
||||
// wg-quick fields
|
||||
if let addressesString = attributes["address"] {
|
||||
var addresses = [IPAddressRange]()
|
||||
for addressString in addressesString.splitToArray(trimmingCharacters: .whitespaces) {
|
||||
guard let address = IPAddressRange(from: addressString) else { return nil }
|
||||
for addressString in addressesString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
||||
guard let address = IPAddressRange(from: addressString) else {
|
||||
throw ParseError.interfaceHasInvalidAddress(addressString)
|
||||
}
|
||||
addresses.append(address)
|
||||
}
|
||||
interface.addresses = addresses
|
||||
}
|
||||
if let dnsString = attributes["dns"] {
|
||||
var dnsServers = [DNSServer]()
|
||||
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespaces) {
|
||||
guard let dnsServer = DNSServer(from: dnsServerString) else { return nil }
|
||||
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
||||
guard let dnsServer = DNSServer(from: dnsServerString) else {
|
||||
throw ParseError.interfaceHasInvalidDNS(dnsServerString)
|
||||
}
|
||||
dnsServers.append(dnsServer)
|
||||
}
|
||||
interface.dns = dnsServers
|
||||
}
|
||||
if let mtuString = attributes["mtu"] {
|
||||
guard let mtu = UInt16(mtuString) else { return nil }
|
||||
guard let mtu = UInt16(mtuString) else {
|
||||
throw ParseError.interfaceHasInvalidMTU(mtuString)
|
||||
}
|
||||
interface.mtu = mtu
|
||||
}
|
||||
return interface
|
||||
}
|
||||
|
||||
//swiftlint:disable:next cyclomatic_complexity
|
||||
private static func collate(peerAttributes attributes: [String: String]) -> PeerConfiguration? {
|
||||
// required wg fields
|
||||
guard let publicKeyString = attributes["publickey"] else { return nil }
|
||||
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else { return nil }
|
||||
private static func collate(peerAttributes attributes: [String: String]) throws -> PeerConfiguration {
|
||||
guard let publicKeyString = attributes["publickey"] else {
|
||||
throw ParseError.peerHasNoPublicKey
|
||||
}
|
||||
guard let publicKey = Data(base64Key: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
|
||||
throw ParseError.peerHasInvalidPublicKey(publicKeyString)
|
||||
}
|
||||
var peer = PeerConfiguration(publicKey: publicKey)
|
||||
// wg fields
|
||||
if let preSharedKeyString = attributes["presharedkey"] {
|
||||
guard let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength else { return nil }
|
||||
guard let preSharedKey = Data(base64Key: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength else {
|
||||
throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
|
||||
}
|
||||
peer.preSharedKey = preSharedKey
|
||||
}
|
||||
if let allowedIPsString = attributes["allowedips"] {
|
||||
var allowedIPs = [IPAddressRange]()
|
||||
for allowedIPString in allowedIPsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
||||
guard let allowedIP = IPAddressRange(from: allowedIPString) else { return nil }
|
||||
guard let allowedIP = IPAddressRange(from: allowedIPString) else {
|
||||
throw ParseError.peerHasInvalidAllowedIP(allowedIPString)
|
||||
}
|
||||
allowedIPs.append(allowedIP)
|
||||
}
|
||||
peer.allowedIPs = allowedIPs
|
||||
}
|
||||
if let endpointString = attributes["endpoint"] {
|
||||
guard let endpoint = Endpoint(from: endpointString) else { return nil }
|
||||
guard let endpoint = Endpoint(from: endpointString) else {
|
||||
throw ParseError.peerHasInvalidEndpoint(endpointString)
|
||||
}
|
||||
peer.endpoint = endpoint
|
||||
}
|
||||
if let persistentKeepAliveString = attributes["persistentkeepalive"] {
|
||||
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else { return nil }
|
||||
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else {
|
||||
throw ParseError.peerHasInvalidPersistentKeepAlive(persistentKeepAliveString)
|
||||
}
|
||||
peer.persistentKeepAlive = persistentKeepAlive
|
||||
}
|
||||
return peer
|
||||
|
114
WireGuard/Shared/Model/key.c
Normal file
@ -0,0 +1,114 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*
|
||||
* This is a specialized constant-time base64/hex implementation that resists side-channel attacks.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "key.h"
|
||||
|
||||
static inline void encode_base64(char dest[static 4], const uint8_t src[static 3])
|
||||
{
|
||||
const uint8_t input[] = { (src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63 };
|
||||
|
||||
for (unsigned int i = 0; i < 4; ++i)
|
||||
dest[i] = input[i] + 'A'
|
||||
+ (((25 - input[i]) >> 8) & 6)
|
||||
- (((51 - input[i]) >> 8) & 75)
|
||||
- (((61 - input[i]) >> 8) & 15)
|
||||
+ (((62 - input[i]) >> 8) & 3);
|
||||
|
||||
}
|
||||
|
||||
void key_to_base64(char base64[static WG_KEY_LEN_BASE64], const uint8_t key[static WG_KEY_LEN])
|
||||
{
|
||||
unsigned int i;
|
||||
|
||||
for (i = 0; i < WG_KEY_LEN / 3; ++i)
|
||||
encode_base64(&base64[i * 4], &key[i * 3]);
|
||||
encode_base64(&base64[i * 4], (const uint8_t[]){ key[i * 3 + 0], key[i * 3 + 1], 0 });
|
||||
base64[WG_KEY_LEN_BASE64 - 2] = '=';
|
||||
base64[WG_KEY_LEN_BASE64 - 1] = '\0';
|
||||
}
|
||||
|
||||
static inline int decode_base64(const char src[static 4])
|
||||
{
|
||||
int val = 0;
|
||||
|
||||
for (unsigned int i = 0; i < 4; ++i)
|
||||
val |= (-1
|
||||
+ ((((('A' - 1) - src[i]) & (src[i] - ('Z' + 1))) >> 8) & (src[i] - 64))
|
||||
+ ((((('a' - 1) - src[i]) & (src[i] - ('z' + 1))) >> 8) & (src[i] - 70))
|
||||
+ ((((('0' - 1) - src[i]) & (src[i] - ('9' + 1))) >> 8) & (src[i] + 5))
|
||||
+ ((((('+' - 1) - src[i]) & (src[i] - ('+' + 1))) >> 8) & 63)
|
||||
+ ((((('/' - 1) - src[i]) & (src[i] - ('/' + 1))) >> 8) & 64)
|
||||
) << (18 - 6 * i);
|
||||
return val;
|
||||
}
|
||||
|
||||
bool key_from_base64(uint8_t key[static WG_KEY_LEN], const char *base64)
|
||||
{
|
||||
unsigned int i;
|
||||
volatile uint8_t ret = 0;
|
||||
int val;
|
||||
|
||||
if (strlen(base64) != WG_KEY_LEN_BASE64 - 1 || base64[WG_KEY_LEN_BASE64 - 2] != '=')
|
||||
return false;
|
||||
|
||||
for (i = 0; i < WG_KEY_LEN / 3; ++i) {
|
||||
val = decode_base64(&base64[i * 4]);
|
||||
ret |= (uint32_t)val >> 31;
|
||||
key[i * 3 + 0] = (val >> 16) & 0xff;
|
||||
key[i * 3 + 1] = (val >> 8) & 0xff;
|
||||
key[i * 3 + 2] = val & 0xff;
|
||||
}
|
||||
val = decode_base64((const char[]){ base64[i * 4 + 0], base64[i * 4 + 1], base64[i * 4 + 2], 'A' });
|
||||
ret |= ((uint32_t)val >> 31) | (val & 0xff);
|
||||
key[i * 3 + 0] = (val >> 16) & 0xff;
|
||||
key[i * 3 + 1] = (val >> 8) & 0xff;
|
||||
|
||||
return 1 & ((ret - 1) >> 8);
|
||||
}
|
||||
|
||||
void key_to_hex(char hex[static WG_KEY_LEN_HEX], const uint8_t key[static WG_KEY_LEN])
|
||||
{
|
||||
unsigned int i;
|
||||
|
||||
for (i = 0; i < WG_KEY_LEN; ++i) {
|
||||
hex[i * 2] = 87U + (key[i] >> 4) + ((((key[i] >> 4) - 10U) >> 8) & ~38U);
|
||||
hex[i * 2 + 1] = 87U + (key[i] & 0xf) + ((((key[i] & 0xf) - 10U) >> 8) & ~38U);
|
||||
}
|
||||
hex[i * 2] = '\0';
|
||||
}
|
||||
|
||||
bool key_from_hex(uint8_t key[static WG_KEY_LEN], const char *hex)
|
||||
{
|
||||
uint8_t c, c_acc, c_alpha0, c_alpha, c_num0, c_num, c_val;
|
||||
volatile uint8_t ret = 0;
|
||||
|
||||
if (strlen(hex) != WG_KEY_LEN_HEX - 1)
|
||||
return false;
|
||||
|
||||
for (unsigned int i = 0; i < WG_KEY_LEN_HEX - 1; i += 2) {
|
||||
c = (uint8_t)hex[i];
|
||||
c_num = c ^ 48U;
|
||||
c_num0 = (c_num - 10U) >> 8;
|
||||
c_alpha = (c & ~32U) - 55U;
|
||||
c_alpha0 = ((c_alpha - 10U) ^ (c_alpha - 16U)) >> 8;
|
||||
ret |= ((c_num0 | c_alpha0) - 1) >> 8;
|
||||
c_val = (c_num0 & c_num) | (c_alpha0 & c_alpha);
|
||||
c_acc = c_val * 16U;
|
||||
|
||||
c = (uint8_t)hex[i + 1];
|
||||
c_num = c ^ 48U;
|
||||
c_num0 = (c_num - 10U) >> 8;
|
||||
c_alpha = (c & ~32U) - 55U;
|
||||
c_alpha0 = ((c_alpha - 10U) ^ (c_alpha - 16U)) >> 8;
|
||||
ret |= ((c_num0 | c_alpha0) - 1) >> 8;
|
||||
c_val = (c_num0 & c_num) | (c_alpha0 & c_alpha);
|
||||
key[i / 2] = c_acc | c_val;
|
||||
}
|
||||
|
||||
return 1 & ((ret - 1) >> 8);
|
||||
}
|
22
WireGuard/Shared/Model/key.h
Normal file
@ -0,0 +1,22 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 */
|
||||
/*
|
||||
* Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef KEY_H
|
||||
#define KEY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define WG_KEY_LEN (32)
|
||||
#define WG_KEY_LEN_BASE64 (45)
|
||||
#define WG_KEY_LEN_HEX (65)
|
||||
|
||||
void key_to_base64(char base64[static WG_KEY_LEN_BASE64], const uint8_t key[static WG_KEY_LEN]);
|
||||
bool key_from_base64(uint8_t key[static WG_KEY_LEN], const char *base64);
|
||||
|
||||
void key_to_hex(char hex[static WG_KEY_LEN_HEX], const uint8_t key[static WG_KEY_LEN]);
|
||||
bool key_from_hex(uint8_t key[static WG_KEY_LEN], const char *hex);
|
||||
|
||||
#endif
|
7
WireGuard/WireGuard/Base.lproj/InfoPlist.strings
Normal file
@ -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";
|
@ -13,6 +13,10 @@
|
||||
"tunnelsListSettingsButtonTitle" = "Settings";
|
||||
"tunnelsListCenteredAddTunnelButtonTitle" = "Add a tunnel";
|
||||
"tunnelsListSwipeDeleteButtonTitle" = "Delete";
|
||||
"tunnelsListSelectButtonTitle" = "Select";
|
||||
"tunnelsListSelectAllButtonTitle" = "Select All";
|
||||
"tunnelsListDeleteButtonTitle" = "Delete";
|
||||
"tunnelsListSelectedTitle (%d)" = "%d selected";
|
||||
|
||||
// Tunnels list menu
|
||||
|
||||
@ -23,11 +27,18 @@
|
||||
|
||||
// Tunnels list alerts
|
||||
|
||||
"alertImportedFromMultipleFilesTitle (%d)" = "Created %d tunnels";
|
||||
"alertImportedFromMultipleFilesMessage (%1$d of %2$d)" = "Created %1$d of %2$d tunnels from imported files";
|
||||
|
||||
"alertImportedFromZipTitle (%d)" = "Created %d tunnels";
|
||||
"alertImportedFromZipMessage (%1$d of %2$d)" = "Created %1$d of %2$d tunnels from zip archive";
|
||||
|
||||
"alertUnableToImportTitle" = "Unable to import tunnel";
|
||||
"alertUnableToImportMessage" = "An error occured when importing the tunnel configuration.";
|
||||
"alertBadConfigImportTitle" = "Unable to import tunnel";
|
||||
"alertBadConfigImportMessage (%@)" = "The file ‘%@’ does not contain a valid WireGuard configuration";
|
||||
|
||||
"deleteTunnelsConfirmationAlertButtonTitle" = "Delete";
|
||||
"deleteTunnelConfirmationAlertButtonMessage (%d)" = "Delete %d tunnel";
|
||||
"deleteTunnelsConfirmationAlertButtonMessage (%d)" = "Delete %d tunnels";
|
||||
|
||||
// Tunnel detail and edit UI
|
||||
|
||||
@ -44,6 +55,14 @@
|
||||
"tunnelStatusRestarting" = "Restarting";
|
||||
"tunnelStatusWaiting" = "Waiting";
|
||||
|
||||
"macToggleStatusButtonActivate" = "Activate";
|
||||
"macToggleStatusButtonActivating" = "Activating…";
|
||||
"macToggleStatusButtonDeactivate" = "Deactivate";
|
||||
"macToggleStatusButtonDeactivating" = "Deactivating…";
|
||||
"macToggleStatusButtonReasserting" = "Reactivating…";
|
||||
"macToggleStatusButtonRestarting" = "Restarting…";
|
||||
"macToggleStatusButtonWaiting" = "Waiting…";
|
||||
|
||||
"tunnelSectionTitleInterface" = "Interface";
|
||||
|
||||
"tunnelInterfaceName" = "Name";
|
||||
@ -54,6 +73,7 @@
|
||||
"tunnelInterfaceListenPort" = "Listen port";
|
||||
"tunnelInterfaceMTU" = "MTU";
|
||||
"tunnelInterfaceDNS" = "DNS servers";
|
||||
"tunnelInterfaceStatus" = "Status";
|
||||
|
||||
"tunnelSectionTitlePeer" = "Peer";
|
||||
|
||||
@ -62,15 +82,41 @@
|
||||
"tunnelPeerEndpoint" = "Endpoint";
|
||||
"tunnelPeerPersistentKeepalive" = "Persistent keepalive";
|
||||
"tunnelPeerAllowedIPs" = "Allowed IPs";
|
||||
"tunnelPeerRxBytes" = "Data received";
|
||||
"tunnelPeerTxBytes" = "Data sent";
|
||||
"tunnelPeerLastHandshakeTime" = "Latest handshake";
|
||||
"tunnelPeerExcludePrivateIPs" = "Exclude private IPs";
|
||||
|
||||
"tunnelSectionTitleOnDemand" = "On-Demand Activation";
|
||||
|
||||
"tunnelOnDemandKey" = "Activate on demand";
|
||||
"tunnelOnDemandCellular" = "Cellular";
|
||||
"tunnelOnDemandEthernet" = "Ethernet";
|
||||
"tunnelOnDemandWiFi" = "Wi-Fi";
|
||||
"tunnelOnDemandSSIDsKey" = "SSIDs";
|
||||
|
||||
"tunnelOnDemandAnySSID" = "Any SSID";
|
||||
"tunnelOnDemandOnlyTheseSSIDs" = "Only these SSIDs";
|
||||
"tunnelOnDemandExceptTheseSSIDs" = "Except these SSIDs";
|
||||
"tunnelOnDemandOnlySSID (%d)" = "Only %d SSID";
|
||||
"tunnelOnDemandOnlySSIDs (%d)" = "Only %d SSIDs";
|
||||
"tunnelOnDemandExceptSSID (%d)" = "Except %d SSID";
|
||||
"tunnelOnDemandExceptSSIDs (%d)" = "Except %d SSIDs";
|
||||
"tunnelOnDemandSSIDOptionDescriptionMac (%1$@: %2$@)" = "%1$@: %2$@";
|
||||
|
||||
"tunnelOnDemandSSIDViewTitle" = "SSIDs";
|
||||
"tunnelOnDemandSectionTitleSelectedSSIDs" = "SSIDs";
|
||||
"tunnelOnDemandNoSSIDs" = "No SSIDs";
|
||||
"tunnelOnDemandSectionTitleAddSSIDs" = "Add SSIDs";
|
||||
"tunnelOnDemandAddMessageAddConnectedSSID (%@)" = "Add connected: %@";
|
||||
"tunnelOnDemandAddMessageAddNewSSID" = "Add new";
|
||||
|
||||
"tunnelOnDemandKey" = "On demand";
|
||||
"tunnelOnDemandOptionOff" = "Off";
|
||||
"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular";
|
||||
"tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only";
|
||||
"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular";
|
||||
"tunnelOnDemandOptionCellularOnly" = "Cellular only";
|
||||
"tunnelOnDemandOptionWiFiOrEthernet" = "Wi-Fi or ethernet";
|
||||
"tunnelOnDemandOptionEthernetOnly" = "Ethernet only";
|
||||
|
||||
"addPeerButtonTitle" = "Add peer";
|
||||
|
||||
@ -88,6 +134,26 @@
|
||||
"tunnelEditPlaceholderTextStronglyRecommended" = "Strongly recommended";
|
||||
"tunnelEditPlaceholderTextOff" = "Off";
|
||||
|
||||
"tunnelPeerPersistentKeepaliveValue (%@)" = "every %@ seconds";
|
||||
"tunnelHandshakeTimestampNow" = "Now";
|
||||
"tunnelHandshakeTimestampSystemClockBackward" = "(System clock wound backwards)";
|
||||
"tunnelHandshakeTimestampAgo (%@)" = "%@ ago";
|
||||
"tunnelHandshakeTimestampYear (%d)" = "%d year";
|
||||
"tunnelHandshakeTimestampYears (%d)" = "%d years";
|
||||
"tunnelHandshakeTimestampDay (%d)" = "%d day";
|
||||
"tunnelHandshakeTimestampDays (%d)" = "%d days";
|
||||
"tunnelHandshakeTimestampHour (%d)" = "%d hour";
|
||||
"tunnelHandshakeTimestampHours (%d)" = "%d hours";
|
||||
"tunnelHandshakeTimestampMinute (%d)" = "%d minute";
|
||||
"tunnelHandshakeTimestampMinutes (%d)" = "%d minutes";
|
||||
"tunnelHandshakeTimestampSecond (%d)" = "%d second";
|
||||
"tunnelHandshakeTimestampSeconds (%d)" = "%d seconds";
|
||||
|
||||
"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 */
|
||||
@ -95,23 +161,23 @@
|
||||
|
||||
/* Any one of the following alert messages can go with the above title */
|
||||
"alertInvalidInterfaceMessageNameRequired" = "Interface name is required";
|
||||
"alertInvalidInterfaceMessagePrivateKeyRequired" = "Interface's private key is required";
|
||||
"alertInvalidInterfaceMessagePrivateKeyInvalid" = "Interface's private key must be a 32-byte key in base64 encoding";
|
||||
"alertInvalidInterfaceMessagePrivateKeyRequired" = "Interface’s private key is required";
|
||||
"alertInvalidInterfaceMessagePrivateKeyInvalid" = "Interface’s private key must be a 32-byte key in base64 encoding";
|
||||
"alertInvalidInterfaceMessageAddressInvalid" = "Interface addresses must be a list of comma-separated IP addresses, optionally in CIDR notation";
|
||||
"alertInvalidInterfaceMessageListenPortInvalid" = "Interface's listen port must be between 0 and 65535, or unspecified";
|
||||
"alertInvalidInterfaceMessageMTUInvalid" = "Interface's MTU must be between 576 and 65535, or unspecified";
|
||||
"alertInvalidInterfaceMessageDNSInvalid" = "Interface's DNS servers must be a list of comma-separated IP addresses";
|
||||
"alertInvalidInterfaceMessageListenPortInvalid" = "Interface’s listen port must be between 0 and 65535, or unspecified";
|
||||
"alertInvalidInterfaceMessageMTUInvalid" = "Interface’s MTU must be between 576 and 65535, or unspecified";
|
||||
"alertInvalidInterfaceMessageDNSInvalid" = "Interface’s DNS servers must be a list of comma-separated IP addresses";
|
||||
|
||||
/* Alert title for error in the peer data */
|
||||
"alertInvalidPeerTitle" = "Invalid peer";
|
||||
|
||||
/* Any one of the following alert messages can go with the above title */
|
||||
"alertInvalidPeerMessagePublicKeyRequired" = "Peer's public key is required";
|
||||
"alertInvalidPeerMessagePublicKeyInvalid" = "Peer's public key must be a 32-byte key in base64 encoding";
|
||||
"alertInvalidPeerMessagePreSharedKeyInvalid" = "Peer's preshared key must be a 32-byte key in base64 encoding";
|
||||
"alertInvalidPeerMessageAllowedIPsInvalid" = "Peer's allowed IPs must be a list of comma-separated IP addresses, optionally in CIDR notation";
|
||||
"alertInvalidPeerMessageEndpointInvalid" = "Peer's endpoint must be of the form 'host:port' or '[host]:port'";
|
||||
"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "Peer's persistent keepalive must be between 0 to 65535, or unspecified";
|
||||
"alertInvalidPeerMessagePublicKeyRequired" = "Peer’s public key is required";
|
||||
"alertInvalidPeerMessagePublicKeyInvalid" = "Peer’s public key must be a 32-byte key in base64 encoding";
|
||||
"alertInvalidPeerMessagePreSharedKeyInvalid" = "Peer’s preshared key must be a 32-byte key in base64 encoding";
|
||||
"alertInvalidPeerMessageAllowedIPsInvalid" = "Peer’s allowed IPs must be a list of comma-separated IP addresses, optionally in CIDR notation";
|
||||
"alertInvalidPeerMessageEndpointInvalid" = "Peer’s endpoint must be of the form ‘host:port’ or ‘[host]:port’";
|
||||
"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "Peer’s persistent keepalive must be between 0 to 65535, or unspecified";
|
||||
"alertInvalidPeerMessagePublicKeyDuplicated" = "Two or more peers cannot have the same public key";
|
||||
|
||||
// Scanning QR code UI
|
||||
@ -151,9 +217,6 @@
|
||||
"alertUnableToRemovePreviousLogTitle" = "Log export failed";
|
||||
"alertUnableToRemovePreviousLogMessage" = "The pre-existing log could not be cleared";
|
||||
|
||||
"alertUnableToFindExtensionLogPathTitle" = "Log export failed";
|
||||
"alertUnableToFindExtensionLogPathMessage" = "Unable to determine extension log path";
|
||||
|
||||
"alertUnableToWriteLogTitle" = "Log export failed";
|
||||
"alertUnableToWriteLogMessage" = "Unable to write logs to file";
|
||||
|
||||
@ -174,6 +237,11 @@
|
||||
"alertNoTunnelsInImportedZipArchiveTitle" = "No tunnels in zip archive";
|
||||
"alertNoTunnelsInImportedZipArchiveMessage" = "No .conf tunnel files were found inside the zip archive.";
|
||||
|
||||
// Conf import error alerts
|
||||
|
||||
"alertCantOpenInputConfFileTitle" = "Unable to import from file";
|
||||
"alertCantOpenInputConfFileMessage (%@)" = "The file ‘%@’ could not be read.";
|
||||
|
||||
// Tunnel management error alerts
|
||||
|
||||
"alertTunnelActivationFailureTitle" = "Activation failure";
|
||||
@ -218,3 +286,104 @@
|
||||
"alertSystemErrorMessageTunnelConfigurationStale" = "The configuration is stale.";
|
||||
"alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Reading or writing the configuration failed.";
|
||||
"alertSystemErrorMessageTunnelConfigurationUnknown" = "Unknown system error.";
|
||||
|
||||
// Mac status bar menu / pulldown menu
|
||||
|
||||
"macMenuNetworks (%@)" = "Networks: %@";
|
||||
"macMenuNetworksNone" = "Networks: None";
|
||||
|
||||
"macMenuTitle" = "WireGuard";
|
||||
"macMenuManageTunnels" = "Manage tunnels";
|
||||
"macMenuImportTunnels" = "Import tunnel(s) from file…";
|
||||
"macMenuAddEmptyTunnel" = "Add empty tunnel…";
|
||||
"macMenuExportLog" = "Export log to file…";
|
||||
"macMenuExportTunnels" = "Export tunnels to zip…";
|
||||
"macMenuAbout" = "About WireGuard";
|
||||
"macMenuQuit" = "Quit";
|
||||
|
||||
// Mac manage tunnels window
|
||||
|
||||
"macWindowTitleManageTunnels" = "Manage WireGuard Tunnels";
|
||||
|
||||
"macDeleteTunnelConfirmationAlertMessage (%@)" = "Are you sure you want to delete ‘%@’?";
|
||||
"macDeleteMultipleTunnelsConfirmationAlertMessage (%d)" = "Are you sure you want to delete %d tunnels?";
|
||||
"macDeleteTunnelConfirmationAlertInfo" = "You cannot undo this action.";
|
||||
"macDeleteTunnelConfirmationAlertButtonTitleDelete" = "Delete";
|
||||
"macDeleteTunnelConfirmationAlertButtonTitleCancel" = "Cancel";
|
||||
|
||||
"macButtonImportTunnels" = "Import tunnel(s) from file";
|
||||
"macSheetButtonImport" = "Import";
|
||||
|
||||
"macNameFieldExportLog" = "Export log to";
|
||||
"macSheetButtonExportLog" = "Save";
|
||||
|
||||
"macNameFieldExportZip" = "Export tunnels to";
|
||||
"macSheetButtonExportZip" = "Save";
|
||||
|
||||
"macButtonDeleteTunnels (%d)" = "Delete %d tunnels";
|
||||
|
||||
// Mac detail/edit view fields
|
||||
|
||||
"macFieldKey (%@)" = "%@:";
|
||||
"macFieldOnDemand" = "On-Demand:";
|
||||
"macFieldOnDemandSSIDs" = "SSIDs:";
|
||||
|
||||
// Mac status display
|
||||
|
||||
"macStatus (%@)" = "Status: %@";
|
||||
|
||||
// Mac editing config
|
||||
|
||||
"macEditDiscard" = "Discard";
|
||||
"macEditSave" = "Save";
|
||||
|
||||
"macAlertNameIsEmpty" = "Name is required";
|
||||
"macAlertDuplicateName (%@)" = "Another tunnel already exists with the name ‘%@’.";
|
||||
|
||||
"macAlertInvalidLine (%@)" = "Invalid line: ‘%@’.";
|
||||
|
||||
"macAlertNoInterface" = "Configuration must have an ‘Interface’ section.";
|
||||
"macAlertMultipleInterfaces" = "Configuration must have only one ‘Interface’ section.";
|
||||
"macAlertPrivateKeyInvalid" = "Private key is invalid.";
|
||||
"macAlertListenPortInvalid (%@)" = "Listen port ‘%@’ is invalid.";
|
||||
"macAlertAddressInvalid (%@)" = "Address ‘%@’ is invalid.";
|
||||
"macAlertDNSInvalid (%@)" = "DNS ‘%@’ is invalid.";
|
||||
"macAlertMTUInvalid (%@)" = "MTU ‘%@’ is invalid.";
|
||||
|
||||
"macAlertUnrecognizedInterfaceKey (%@)" = "Interface contains unrecognized key ‘%@’";
|
||||
"macAlertInfoUnrecognizedInterfaceKey" = "Valid keys are: ‘PrivateKey’, ‘ListenPort’, ‘Address’, ‘DNS’ and ‘MTU’.";
|
||||
|
||||
"macAlertPublicKeyInvalid" = "Public key is invalid";
|
||||
"macAlertPreSharedKeyInvalid" = "Preshared key is invalid";
|
||||
"macAlertAllowedIPInvalid (%@)" = "Allowed IP ‘%@’ is invalid";
|
||||
"macAlertEndpointInvalid (%@)" = "Endpoint ‘%@’ is invalid";
|
||||
"macAlertPersistentKeepliveInvalid (%@)" = "Persistent keepalive value ‘%@’ is invalid";
|
||||
|
||||
"macAlertUnrecognizedPeerKey (%@)" = "Peer contains unrecognized key ‘%@’";
|
||||
"macAlertInfoUnrecognizedPeerKey" = "Valid keys are: ‘PublicKey’, ‘PresharedKey’, ‘AllowedIPs’, ‘Endpoint’ and ‘PersistentKeepalive’";
|
||||
|
||||
"macAlertMultipleEntriesForKey (%@)" = "There should be only one entry per section for key ‘%@’";
|
||||
|
||||
// Mac about dialog
|
||||
|
||||
"macAppVersion (%@)" = "App version: %@";
|
||||
"macGoBackendVersion (%@)" = "Go backend version: %@";
|
||||
|
||||
// Privacy
|
||||
|
||||
"macExportPrivateData" = "export tunnel private keys";
|
||||
"macViewPrivateData" = "view tunnel private keys";
|
||||
"iosExportPrivateData" = "Authenticate to export tunnel private keys.";
|
||||
"iosViewPrivateData" = "Authenticate to view tunnel private keys.";
|
||||
|
||||
// Mac alert
|
||||
|
||||
"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.";
|
||||
|
||||
// Mac tooltip
|
||||
|
||||
"macToolTipEditTunnel" = "Edit tunnel (⌘E)";
|
||||
"macToolTipToggleStatus" = "Toggle status (⌘T)";
|
||||
|
@ -3,7 +3,8 @@
|
||||
// You Apple developer account's Team ID
|
||||
DEVELOPMENT_TEAM = <team_id>
|
||||
|
||||
// The bundle identifier of this app.
|
||||
// The bundle identifier of the apps.
|
||||
// Should be an app id created at developer.apple.com
|
||||
// with Network Extensions capabilty.
|
||||
APP_ID = <app_id>
|
||||
APP_ID_IOS = <app_id>
|
||||
APP_ID_MACOS = <app_id>
|
||||
|
@ -1,2 +1,2 @@
|
||||
VERSION_NAME = 0.0.20190107
|
||||
VERSION_NAME = 0.0.20190319
|
||||
VERSION_ID = 1
|
||||
|
@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import Foundation
|
||||
|
||||
struct Curve25519 {
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <CommonCrypto/CommonRandom.h>
|
||||
|
||||
#include "x25519.h"
|
||||
@ -171,7 +172,7 @@ void curve25519_derive_public_key(uint8_t public_key[32], const uint8_t private_
|
||||
|
||||
void curve25519_generate_private_key(uint8_t private_key[32])
|
||||
{
|
||||
CCRandomGenerateBytes(private_key, 32);
|
||||
assert(CCRandomGenerateBytes(private_key, 32) == kCCSuccess);
|
||||
private_key[31] = (private_key[31] & 127) | 64;
|
||||
private_key[0] &= 248;
|
||||
}
|
||||
|
120
WireGuard/WireGuard/Tunnel/ActivateOnDemandOption.swift
Normal file
@ -0,0 +1,120 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
enum ActivateOnDemandOption: Equatable {
|
||||
case off
|
||||
case wiFiInterfaceOnly(ActivateOnDemandSSIDOption)
|
||||
case nonWiFiInterfaceOnly
|
||||
case anyInterface(ActivateOnDemandSSIDOption)
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
private let nonWiFiInterfaceType: NEOnDemandRuleInterfaceType = .cellular
|
||||
#elseif os(macOS)
|
||||
private let nonWiFiInterfaceType: NEOnDemandRuleInterfaceType = .ethernet
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
|
||||
enum ActivateOnDemandSSIDOption: Equatable {
|
||||
case anySSID
|
||||
case onlySpecificSSIDs([String])
|
||||
case exceptSpecificSSIDs([String])
|
||||
}
|
||||
|
||||
extension ActivateOnDemandOption {
|
||||
func apply(on tunnelProviderManager: NETunnelProviderManager) {
|
||||
let rules: [NEOnDemandRule]?
|
||||
switch self {
|
||||
case .off:
|
||||
rules = nil
|
||||
case .wiFiInterfaceOnly(let ssidOption):
|
||||
rules = ssidOnDemandRules(option: ssidOption) + [NEOnDemandRuleDisconnect(interfaceType: nonWiFiInterfaceType)]
|
||||
case .nonWiFiInterfaceOnly:
|
||||
rules = [NEOnDemandRuleConnect(interfaceType: nonWiFiInterfaceType), NEOnDemandRuleDisconnect(interfaceType: .wiFi)]
|
||||
case .anyInterface(let ssidOption):
|
||||
if case .anySSID = ssidOption {
|
||||
rules = [NEOnDemandRuleConnect(interfaceType: .any)]
|
||||
} else {
|
||||
rules = ssidOnDemandRules(option: ssidOption) + [NEOnDemandRuleConnect(interfaceType: nonWiFiInterfaceType)]
|
||||
}
|
||||
}
|
||||
tunnelProviderManager.onDemandRules = rules
|
||||
tunnelProviderManager.isOnDemandEnabled = self != .off
|
||||
}
|
||||
|
||||
init(from tunnelProviderManager: NETunnelProviderManager) {
|
||||
let rules = tunnelProviderManager.onDemandRules ?? []
|
||||
let activateOnDemandOption: ActivateOnDemandOption
|
||||
switch rules.count {
|
||||
case 0:
|
||||
activateOnDemandOption = .off
|
||||
case 1:
|
||||
let rule = rules[0]
|
||||
precondition(rule.action == .connect)
|
||||
activateOnDemandOption = .anyInterface(.anySSID)
|
||||
case 2:
|
||||
let connectRule = rules.first(where: { $0.action == .connect })!
|
||||
let disconnectRule = rules.first(where: { $0.action == .disconnect })!
|
||||
if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == nonWiFiInterfaceType {
|
||||
activateOnDemandOption = .wiFiInterfaceOnly(.anySSID)
|
||||
} else if connectRule.interfaceTypeMatch == nonWiFiInterfaceType && disconnectRule.interfaceTypeMatch == .wiFi {
|
||||
activateOnDemandOption = .nonWiFiInterfaceOnly
|
||||
} else {
|
||||
fatalError("Unexpected onDemandRules set on tunnel provider manager")
|
||||
}
|
||||
case 3:
|
||||
let ssidRule = rules.first(where: { $0.interfaceTypeMatch == .wiFi && $0.ssidMatch != nil })!
|
||||
let nonWiFiRule = rules.first(where: { $0.interfaceTypeMatch == nonWiFiInterfaceType })!
|
||||
let ssids = ssidRule.ssidMatch!
|
||||
switch (ssidRule.action, nonWiFiRule.action) {
|
||||
case (.connect, .connect):
|
||||
activateOnDemandOption = .anyInterface(.onlySpecificSSIDs(ssids))
|
||||
case (.connect, .disconnect):
|
||||
activateOnDemandOption = .wiFiInterfaceOnly(.onlySpecificSSIDs(ssids))
|
||||
case (.disconnect, .connect):
|
||||
activateOnDemandOption = .anyInterface(.exceptSpecificSSIDs(ssids))
|
||||
case (.disconnect, .disconnect):
|
||||
activateOnDemandOption = .wiFiInterfaceOnly(.exceptSpecificSSIDs(ssids))
|
||||
default:
|
||||
fatalError("Unexpected SSID onDemandRules set on tunnel provider manager")
|
||||
}
|
||||
default:
|
||||
fatalError("Unexpected number of onDemandRules set on tunnel provider manager")
|
||||
}
|
||||
|
||||
self = activateOnDemandOption
|
||||
}
|
||||
}
|
||||
|
||||
private extension NEOnDemandRuleConnect {
|
||||
convenience init(interfaceType: NEOnDemandRuleInterfaceType, ssids: [String]? = nil) {
|
||||
self.init()
|
||||
interfaceTypeMatch = interfaceType
|
||||
ssidMatch = ssids
|
||||
}
|
||||
}
|
||||
|
||||
private extension NEOnDemandRuleDisconnect {
|
||||
convenience init(interfaceType: NEOnDemandRuleInterfaceType, ssids: [String]? = nil) {
|
||||
self.init()
|
||||
interfaceTypeMatch = interfaceType
|
||||
ssidMatch = ssids
|
||||
}
|
||||
}
|
||||
|
||||
private func ssidOnDemandRules(option: ActivateOnDemandSSIDOption) -> [NEOnDemandRule] {
|
||||
switch option {
|
||||
case .anySSID:
|
||||
return [NEOnDemandRuleConnect(interfaceType: .wiFi)]
|
||||
case .onlySpecificSSIDs(let ssids):
|
||||
assert(!ssids.isEmpty)
|
||||
return [NEOnDemandRuleConnect(interfaceType: .wiFi, ssids: ssids),
|
||||
NEOnDemandRuleDisconnect(interfaceType: .wiFi)]
|
||||
case .exceptSpecificSSIDs(let ssids):
|
||||
return [NEOnDemandRuleDisconnect(interfaceType: .wiFi, ssids: ssids),
|
||||
NEOnDemandRuleConnect(interfaceType: .wiFi)]
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
struct ActivateOnDemandSetting {
|
||||
var isActivateOnDemandEnabled: Bool
|
||||
var activateOnDemandOption: ActivateOnDemandOption
|
||||
}
|
||||
|
||||
enum ActivateOnDemandOption {
|
||||
case none // Valid only when isActivateOnDemandEnabled is false
|
||||
case useOnDemandOverWiFiOrCellular
|
||||
case useOnDemandOverWiFiOnly
|
||||
case useOnDemandOverCellularOnly
|
||||
}
|
||||
|
||||
extension ActivateOnDemandSetting {
|
||||
func apply(on tunnelProviderManager: NETunnelProviderManager) {
|
||||
tunnelProviderManager.isOnDemandEnabled = isActivateOnDemandEnabled
|
||||
let rules: [NEOnDemandRule]?
|
||||
let connectRule = NEOnDemandRuleConnect()
|
||||
let disconnectRule = NEOnDemandRuleDisconnect()
|
||||
switch activateOnDemandOption {
|
||||
case .none:
|
||||
rules = nil
|
||||
case .useOnDemandOverWiFiOrCellular:
|
||||
rules = [connectRule]
|
||||
case .useOnDemandOverWiFiOnly:
|
||||
connectRule.interfaceTypeMatch = .wiFi
|
||||
disconnectRule.interfaceTypeMatch = .cellular
|
||||
rules = [connectRule, disconnectRule]
|
||||
case .useOnDemandOverCellularOnly:
|
||||
connectRule.interfaceTypeMatch = .cellular
|
||||
disconnectRule.interfaceTypeMatch = .wiFi
|
||||
rules = [connectRule, disconnectRule]
|
||||
}
|
||||
tunnelProviderManager.onDemandRules = rules
|
||||
}
|
||||
|
||||
init(from tunnelProviderManager: NETunnelProviderManager) {
|
||||
let rules = tunnelProviderManager.onDemandRules ?? []
|
||||
let activateOnDemandOption: ActivateOnDemandOption
|
||||
switch rules.count {
|
||||
case 0:
|
||||
activateOnDemandOption = .none
|
||||
case 1:
|
||||
let rule = rules[0]
|
||||
precondition(rule.action == .connect)
|
||||
activateOnDemandOption = .useOnDemandOverWiFiOrCellular
|
||||
case 2:
|
||||
let connectRule = rules.first(where: { $0.action == .connect })!
|
||||
let disconnectRule = rules.first(where: { $0.action == .disconnect })!
|
||||
if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == .cellular {
|
||||
activateOnDemandOption = .useOnDemandOverWiFiOnly
|
||||
} else if connectRule.interfaceTypeMatch == .cellular && disconnectRule.interfaceTypeMatch == .wiFi {
|
||||
activateOnDemandOption = .useOnDemandOverCellularOnly
|
||||
} else {
|
||||
fatalError("Unexpected onDemandRules set on tunnel provider manager")
|
||||
}
|
||||
default:
|
||||
fatalError("Unexpected number of onDemandRules set on tunnel provider manager")
|
||||
}
|
||||
self.activateOnDemandOption = activateOnDemandOption
|
||||
if activateOnDemandOption == .none {
|
||||
isActivateOnDemandEnabled = false
|
||||
} else {
|
||||
isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ActivateOnDemandSetting {
|
||||
static var defaultSetting = ActivateOnDemandSetting(isActivateOnDemandEnabled: false, activateOnDemandOption: .none)
|
||||
}
|
184
WireGuard/WireGuard/Tunnel/TunnelConfiguration+UapiConfig.swift
Normal file
@ -0,0 +1,184 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension TunnelConfiguration {
|
||||
convenience init(fromUapiConfig uapiConfig: String, basedOn base: TunnelConfiguration? = nil) throws {
|
||||
var interfaceConfiguration: InterfaceConfiguration?
|
||||
var peerConfigurations = [PeerConfiguration]()
|
||||
|
||||
var lines = uapiConfig.split(separator: "\n")
|
||||
lines.append("")
|
||||
|
||||
var parserState = ParserState.inInterfaceSection
|
||||
var attributes = [String: String]()
|
||||
|
||||
for line in lines {
|
||||
var key = ""
|
||||
var value = ""
|
||||
|
||||
if !line.isEmpty {
|
||||
guard let equalsIndex = line.firstIndex(of: "=") else { throw ParseError.invalidLine(line) }
|
||||
key = String(line[..<equalsIndex])
|
||||
value = String(line[line.index(equalsIndex, offsetBy: 1)...])
|
||||
}
|
||||
|
||||
if line.isEmpty || key == "public_key" {
|
||||
// Previous section has ended; process the attributes collected so far
|
||||
if parserState == .inInterfaceSection {
|
||||
let interface = try TunnelConfiguration.collate(interfaceAttributes: attributes)
|
||||
guard interfaceConfiguration == nil else { throw ParseError.multipleInterfaces }
|
||||
interfaceConfiguration = interface
|
||||
parserState = .inPeerSection
|
||||
} else if parserState == .inPeerSection {
|
||||
let peer = try TunnelConfiguration.collate(peerAttributes: attributes)
|
||||
peerConfigurations.append(peer)
|
||||
}
|
||||
attributes.removeAll()
|
||||
if line.isEmpty {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let presentValue = attributes[key] {
|
||||
if key == "allowed_ip" {
|
||||
attributes[key] = presentValue + "," + value
|
||||
} else {
|
||||
throw ParseError.multipleEntriesForKey(key)
|
||||
}
|
||||
} else {
|
||||
attributes[key] = value
|
||||
}
|
||||
|
||||
let interfaceSectionKeys: Set<String> = ["private_key", "listen_port", "fwmark"]
|
||||
let peerSectionKeys: Set<String> = ["public_key", "preshared_key", "allowed_ip", "endpoint", "persistent_keepalive_interval", "last_handshake_time_sec", "last_handshake_time_nsec", "rx_bytes", "tx_bytes", "protocol_version"]
|
||||
|
||||
if parserState == .inInterfaceSection {
|
||||
guard interfaceSectionKeys.contains(key) else {
|
||||
throw ParseError.interfaceHasUnrecognizedKey(key)
|
||||
}
|
||||
}
|
||||
if parserState == .inPeerSection {
|
||||
guard peerSectionKeys.contains(key) else {
|
||||
throw ParseError.peerHasUnrecognizedKey(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
|
||||
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
|
||||
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
||||
throw ParseError.multiplePeersWithSamePublicKey
|
||||
}
|
||||
|
||||
interfaceConfiguration?.addresses = base?.interface.addresses ?? []
|
||||
interfaceConfiguration?.dns = base?.interface.dns ?? []
|
||||
interfaceConfiguration?.mtu = base?.interface.mtu
|
||||
|
||||
if let interfaceConfiguration = interfaceConfiguration {
|
||||
self.init(name: base?.name, interface: interfaceConfiguration, peers: peerConfigurations)
|
||||
} else {
|
||||
throw ParseError.noInterface
|
||||
}
|
||||
}
|
||||
|
||||
private static func collate(interfaceAttributes attributes: [String: String]) throws -> InterfaceConfiguration {
|
||||
guard let privateKeyString = attributes["private_key"] else {
|
||||
throw ParseError.interfaceHasNoPrivateKey
|
||||
}
|
||||
guard let privateKey = Data(hexKey: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
|
||||
throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
|
||||
}
|
||||
var interface = InterfaceConfiguration(privateKey: privateKey)
|
||||
if let listenPortString = attributes["listen_port"] {
|
||||
guard let listenPort = UInt16(listenPortString) else {
|
||||
throw ParseError.interfaceHasInvalidListenPort(listenPortString)
|
||||
}
|
||||
if listenPort != 0 {
|
||||
interface.listenPort = listenPort
|
||||
}
|
||||
}
|
||||
return interface
|
||||
}
|
||||
|
||||
private static func collate(peerAttributes attributes: [String: String]) throws -> PeerConfiguration {
|
||||
guard let publicKeyString = attributes["public_key"] else {
|
||||
throw ParseError.peerHasNoPublicKey
|
||||
}
|
||||
guard let publicKey = Data(hexKey: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
|
||||
throw ParseError.peerHasInvalidPublicKey(publicKeyString)
|
||||
}
|
||||
var peer = PeerConfiguration(publicKey: publicKey)
|
||||
if let preSharedKeyString = attributes["preshared_key"] {
|
||||
guard let preSharedKey = Data(hexKey: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength else {
|
||||
throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
|
||||
}
|
||||
// TODO(zx2c4): does the compiler optimize this away?
|
||||
var accumulator: UInt8 = 0
|
||||
for index in 0..<preSharedKey.count {
|
||||
accumulator |= preSharedKey[index]
|
||||
}
|
||||
if accumulator != 0 {
|
||||
peer.preSharedKey = preSharedKey
|
||||
}
|
||||
}
|
||||
if let allowedIPsString = attributes["allowed_ip"] {
|
||||
var allowedIPs = [IPAddressRange]()
|
||||
for allowedIPString in allowedIPsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
||||
guard let allowedIP = IPAddressRange(from: allowedIPString) else {
|
||||
throw ParseError.peerHasInvalidAllowedIP(allowedIPString)
|
||||
}
|
||||
allowedIPs.append(allowedIP)
|
||||
}
|
||||
peer.allowedIPs = allowedIPs
|
||||
}
|
||||
if let endpointString = attributes["endpoint"] {
|
||||
guard let endpoint = Endpoint(from: endpointString) else {
|
||||
throw ParseError.peerHasInvalidEndpoint(endpointString)
|
||||
}
|
||||
peer.endpoint = endpoint
|
||||
}
|
||||
if let persistentKeepAliveString = attributes["persistent_keepalive_interval"] {
|
||||
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else {
|
||||
throw ParseError.peerHasInvalidPersistentKeepAlive(persistentKeepAliveString)
|
||||
}
|
||||
if persistentKeepAlive != 0 {
|
||||
peer.persistentKeepAlive = persistentKeepAlive
|
||||
}
|
||||
}
|
||||
if let rxBytesString = attributes["rx_bytes"] {
|
||||
guard let rxBytes = UInt64(rxBytesString) else {
|
||||
throw ParseError.peerHasInvalidTransferBytes(rxBytesString)
|
||||
}
|
||||
if rxBytes != 0 {
|
||||
peer.rxBytes = rxBytes
|
||||
}
|
||||
}
|
||||
if let txBytesString = attributes["tx_bytes"] {
|
||||
guard let txBytes = UInt64(txBytesString) else {
|
||||
throw ParseError.peerHasInvalidTransferBytes(txBytesString)
|
||||
}
|
||||
if txBytes != 0 {
|
||||
peer.txBytes = txBytes
|
||||
}
|
||||
}
|
||||
if let lastHandshakeTimeSecString = attributes["last_handshake_time_sec"] {
|
||||
var lastHandshakeTimeSince1970: TimeInterval = 0
|
||||
guard let lastHandshakeTimeSec = UInt64(lastHandshakeTimeSecString) else {
|
||||
throw ParseError.peerHasInvalidLastHandshakeTime(lastHandshakeTimeSecString)
|
||||
}
|
||||
if lastHandshakeTimeSec != 0 {
|
||||
lastHandshakeTimeSince1970 += Double(lastHandshakeTimeSec)
|
||||
if let lastHandshakeTimeNsecString = attributes["last_handshake_time_nsec"] {
|
||||
guard let lastHandshakeTimeNsec = UInt64(lastHandshakeTimeNsecString) else {
|
||||
throw ParseError.peerHasInvalidLastHandshakeTime(lastHandshakeTimeNsecString)
|
||||
}
|
||||
lastHandshakeTimeSince1970 += Double(lastHandshakeTimeNsec) / 1000000000.0
|
||||
}
|
||||
peer.lastHandshakeTime = Date(timeIntervalSince1970: lastHandshakeTimeSince1970)
|
||||
}
|
||||
}
|
||||
return peer
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ protocol TunnelsManagerListDelegate: class {
|
||||
func tunnelAdded(at index: Int)
|
||||
func tunnelModified(at index: Int)
|
||||
func tunnelMoved(from oldIndex: Int, to newIndex: Int)
|
||||
func tunnelRemoved(at index: Int)
|
||||
func tunnelRemoved(at index: Int, tunnel: TunnelContainer)
|
||||
}
|
||||
|
||||
protocol TunnelsManagerActivationDelegate: class {
|
||||
@ -25,10 +25,12 @@ class TunnelsManager {
|
||||
weak var activationDelegate: TunnelsManagerActivationDelegate?
|
||||
private var statusObservationToken: AnyObject?
|
||||
private var waiteeObservationToken: AnyObject?
|
||||
private var configurationsObservationToken: AnyObject?
|
||||
|
||||
init(tunnelProviders: [NETunnelProviderManager]) {
|
||||
tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
|
||||
tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
|
||||
startObservingTunnelStatuses()
|
||||
startObservingTunnelConfigurations()
|
||||
}
|
||||
|
||||
static func create(completionHandler: @escaping (WireGuardResult<TunnelsManager>) -> Void) {
|
||||
@ -42,40 +44,60 @@ class TunnelsManager {
|
||||
return
|
||||
}
|
||||
|
||||
let tunnelManagers = managers ?? []
|
||||
tunnelManagers.forEach { tunnelManager in
|
||||
if (tunnelManager.protocolConfiguration as? NETunnelProviderProtocol)?.migrateConfigurationIfNeeded() == true {
|
||||
var tunnelManagers = managers ?? []
|
||||
var refs: Set<Data> = []
|
||||
for (index, tunnelManager) in tunnelManagers.enumerated().reversed() {
|
||||
let proto = tunnelManager.protocolConfiguration as? NETunnelProviderProtocol
|
||||
if proto?.migrateConfigurationIfNeeded(called: tunnelManager.localizedDescription ?? "unknown") ?? false {
|
||||
tunnelManager.saveToPreferences { _ in }
|
||||
}
|
||||
if let ref = proto?.verifyConfigurationReference() {
|
||||
refs.insert(ref)
|
||||
} else {
|
||||
tunnelManager.removeFromPreferences { _ in }
|
||||
tunnelManagers.remove(at: index)
|
||||
}
|
||||
}
|
||||
Keychain.deleteReferences(except: refs)
|
||||
completionHandler(.success(TunnelsManager(tunnelProviders: tunnelManagers)))
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func reload(completionHandler: @escaping (Bool) -> Void) {
|
||||
#if targetEnvironment(simulator)
|
||||
completionHandler(false)
|
||||
#else
|
||||
NETunnelProviderManager.loadAllFromPreferences { managers, _ in
|
||||
guard let managers = managers else {
|
||||
completionHandler(false)
|
||||
return
|
||||
}
|
||||
func reload() {
|
||||
NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, _ in
|
||||
guard let self = self else { return }
|
||||
|
||||
let newTunnels = managers.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
|
||||
let hasChanges = self.tunnels.map { $0.tunnelConfiguration } != newTunnels.map { $0.tunnelConfiguration }
|
||||
if hasChanges {
|
||||
self.tunnels = newTunnels
|
||||
completionHandler(true)
|
||||
} else {
|
||||
completionHandler(false)
|
||||
let loadedTunnelProviders = managers ?? []
|
||||
|
||||
for (index, currentTunnel) in self.tunnels.enumerated().reversed() {
|
||||
if !loadedTunnelProviders.contains(where: { $0.tunnelConfiguration == currentTunnel.tunnelConfiguration }) {
|
||||
// Tunnel was deleted outside the app
|
||||
self.tunnels.remove(at: index)
|
||||
self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: currentTunnel)
|
||||
}
|
||||
}
|
||||
for loadedTunnelProvider in loadedTunnelProviders {
|
||||
if let matchingTunnel = self.tunnels.first(where: { $0.tunnelConfiguration == loadedTunnelProvider.tunnelConfiguration }) {
|
||||
matchingTunnel.tunnelProvider = loadedTunnelProvider
|
||||
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 { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
|
||||
self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func add(tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting, completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) {
|
||||
func add(tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption = .off, completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) {
|
||||
let tunnelName = tunnelConfiguration.name ?? ""
|
||||
if tunnelName.isEmpty {
|
||||
completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
|
||||
@ -88,47 +110,64 @@ class TunnelsManager {
|
||||
}
|
||||
|
||||
let tunnelProviderManager = NETunnelProviderManager()
|
||||
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
|
||||
tunnelProviderManager.localizedDescription = tunnelConfiguration.name
|
||||
tunnelProviderManager.setTunnelConfiguration(tunnelConfiguration)
|
||||
tunnelProviderManager.isEnabled = true
|
||||
|
||||
activateOnDemandSetting.apply(on: tunnelProviderManager)
|
||||
onDemandOption.apply(on: tunnelProviderManager)
|
||||
|
||||
let activeTunnel = tunnels.first { $0.status == .active || $0.status == .activating }
|
||||
|
||||
tunnelProviderManager.saveToPreferences { [weak self] error in
|
||||
guard error == nil else {
|
||||
wg_log(.error, message: "Add: Saving configuration failed: \(error!)")
|
||||
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
|
||||
completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel(systemError: error!)))
|
||||
return
|
||||
}
|
||||
|
||||
guard let self = self else { return }
|
||||
|
||||
#if os(iOS)
|
||||
// HACK: In iOS, adding a tunnel causes deactivation of any currently active tunnel.
|
||||
// This is an ugly hack to reactivate the tunnel that has been deactivated like that.
|
||||
if let activeTunnel = activeTunnel {
|
||||
if activeTunnel.status == .inactive || activeTunnel.status == .deactivating {
|
||||
self.startActivation(of: activeTunnel)
|
||||
}
|
||||
if activeTunnel.status == .active || activeTunnel.status == .activating {
|
||||
activeTunnel.status = .restarting
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
|
||||
self.tunnels.append(tunnel)
|
||||
self.tunnels.sort { $0.name < $1.name }
|
||||
self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
|
||||
self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
|
||||
completionHandler(.success(tunnel))
|
||||
}
|
||||
}
|
||||
|
||||
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt) -> Void) {
|
||||
addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, completionHandler: completionHandler)
|
||||
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
|
||||
addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, lastError: nil, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, completionHandler: @escaping (UInt) -> Void) {
|
||||
private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, lastError: TunnelsManagerError?, completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
|
||||
guard let head = tunnelConfigurations.first else {
|
||||
completionHandler(numberSuccessful)
|
||||
completionHandler(numberSuccessful, lastError)
|
||||
return
|
||||
}
|
||||
let tail = tunnelConfigurations.dropFirst()
|
||||
add(tunnelConfiguration: head) { [weak self, tail] result in
|
||||
DispatchQueue.main.async {
|
||||
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful + (result.isSuccess ? 1 : 0), completionHandler: completionHandler)
|
||||
let numberSuccessful = numberSuccessful + (result.isSuccess ? 1 : 0)
|
||||
let lastError = lastError ?? (result.error as? TunnelsManagerError)
|
||||
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful, lastError: lastError, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
let tunnelName = tunnelConfiguration.name ?? ""
|
||||
if tunnelName.isEmpty {
|
||||
completionHandler(TunnelsManagerError.tunnelNameEmpty)
|
||||
@ -145,33 +184,38 @@ class TunnelsManager {
|
||||
tunnel.name = tunnelName
|
||||
}
|
||||
|
||||
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
|
||||
tunnelProviderManager.localizedDescription = tunnelConfiguration.name
|
||||
var isTunnelConfigurationChanged = false
|
||||
if tunnelProviderManager.tunnelConfiguration != tunnelConfiguration {
|
||||
tunnelProviderManager.setTunnelConfiguration(tunnelConfiguration)
|
||||
isTunnelConfigurationChanged = true
|
||||
}
|
||||
tunnelProviderManager.isEnabled = true
|
||||
|
||||
let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && activateOnDemandSetting.isActivateOnDemandEnabled
|
||||
activateOnDemandSetting.apply(on: tunnelProviderManager)
|
||||
let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && onDemandOption != .off
|
||||
onDemandOption.apply(on: tunnelProviderManager)
|
||||
|
||||
tunnelProviderManager.saveToPreferences { [weak self] error in
|
||||
guard error == nil else {
|
||||
//TODO: the passwordReference for the old one has already been removed at this point and we can't easily roll back!
|
||||
wg_log(.error, message: "Modify: Saving configuration failed: \(error!)")
|
||||
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!))
|
||||
return
|
||||
}
|
||||
guard let self = self else { return }
|
||||
|
||||
if isNameChanged {
|
||||
let oldIndex = self.tunnels.firstIndex(of: tunnel)!
|
||||
self.tunnels.sort { $0.name < $1.name }
|
||||
self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
|
||||
let newIndex = self.tunnels.firstIndex(of: tunnel)!
|
||||
self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex)
|
||||
}
|
||||
self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!)
|
||||
|
||||
if tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting {
|
||||
// Turn off the tunnel, and then turn it back on, so the changes are made effective
|
||||
tunnel.status = .restarting
|
||||
(tunnel.tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
|
||||
if isTunnelConfigurationChanged {
|
||||
if tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting {
|
||||
// Turn off the tunnel, and then turn it back on, so the changes are made effective
|
||||
tunnel.status = .restarting
|
||||
(tunnel.tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
|
||||
}
|
||||
}
|
||||
|
||||
if isActivatingOnDemand {
|
||||
@ -194,6 +238,7 @@ class TunnelsManager {
|
||||
|
||||
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
let tunnelProviderManager = tunnel.tunnelProvider
|
||||
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
|
||||
|
||||
tunnelProviderManager.removeFromPreferences { [weak self] error in
|
||||
guard error == nil else {
|
||||
@ -201,15 +246,35 @@ class TunnelsManager {
|
||||
completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel(systemError: error!))
|
||||
return
|
||||
}
|
||||
if let self = self {
|
||||
let index = self.tunnels.firstIndex(of: tunnel)!
|
||||
if let self = self, let index = self.tunnels.firstIndex(of: tunnel) {
|
||||
self.tunnels.remove(at: index)
|
||||
self.tunnelsListDelegate?.tunnelRemoved(at: index)
|
||||
self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: tunnel)
|
||||
}
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func removeMultiple(tunnels: [TunnelContainer], completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
removeMultiple(tunnels: ArraySlice(tunnels), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
private func removeMultiple(tunnels: ArraySlice<TunnelContainer>, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
guard let head = tunnels.first else {
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
let tail = tunnels.dropFirst()
|
||||
remove(tunnel: head) { [weak self, tail] error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
} else {
|
||||
self?.removeMultiple(tunnels: tail, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func numberOfTunnels() -> Int {
|
||||
return tunnels.count
|
||||
}
|
||||
@ -218,10 +283,25 @@ class TunnelsManager {
|
||||
return tunnels[index]
|
||||
}
|
||||
|
||||
func index(of tunnel: TunnelContainer) -> Int? {
|
||||
return tunnels.firstIndex(of: tunnel)
|
||||
}
|
||||
|
||||
func tunnel(named tunnelName: String) -> TunnelContainer? {
|
||||
return tunnels.first { $0.name == tunnelName }
|
||||
}
|
||||
|
||||
func waitingTunnel() -> TunnelContainer? {
|
||||
return tunnels.first { $0.status == .waiting }
|
||||
}
|
||||
|
||||
func tunnelInOperation() -> TunnelContainer? {
|
||||
if let waitingTunnelObject = waitingTunnel() {
|
||||
return waitingTunnelObject
|
||||
}
|
||||
return tunnels.first { $0.status != .inactive }
|
||||
}
|
||||
|
||||
func startActivation(of tunnel: TunnelContainer) {
|
||||
guard tunnels.contains(tunnel) else { return } // Ensure it's not deleted
|
||||
guard tunnel.status == .inactive else {
|
||||
@ -281,12 +361,7 @@ class TunnelsManager {
|
||||
guard let self = self,
|
||||
let session = statusChangeNotification.object as? NETunnelProviderSession,
|
||||
let tunnelProvider = session.manager as? NETunnelProviderManager,
|
||||
let tunnelConfiguration = TunnelContainer(tunnel: tunnelProvider).tunnelConfiguration,
|
||||
let tunnel = self.tunnels.first(where: { $0.tunnelConfiguration == tunnelConfiguration }) else { return }
|
||||
if tunnel.tunnelProvider != tunnelProvider {
|
||||
tunnel.tunnelProvider = tunnelProvider
|
||||
tunnel.refreshStatus()
|
||||
}
|
||||
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)'")
|
||||
|
||||
@ -304,10 +379,8 @@ class TunnelsManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (tunnel.status == .restarting) && (session.status == .disconnected || session.status == .disconnecting) {
|
||||
if session.status == .disconnected {
|
||||
tunnel.startActivation(activationDelegate: self.activationDelegate)
|
||||
}
|
||||
if tunnel.status == .restarting && session.status == .disconnected {
|
||||
tunnel.startActivation(activationDelegate: self.activationDelegate)
|
||||
return
|
||||
}
|
||||
|
||||
@ -315,6 +388,21 @@ class TunnelsManager {
|
||||
}
|
||||
}
|
||||
|
||||
func startObservingTunnelConfigurations() {
|
||||
configurationsObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNConfigurationChange, object: nil, queue: OperationQueue.main) { [weak self] _ in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
// We schedule reload() in a subsequent runloop to ensure that the completion handler of loadAllFromPreferences
|
||||
// (reload() calls loadAllFromPreferences) is called after the completion handler of the saveToPreferences or
|
||||
// removeFromPreferences call, if any, that caused this notification to fire. This notification can also fire
|
||||
// as a result of a tunnel getting added or removed outside of the app.
|
||||
self?.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func tunnelNameIsLessThan(_ a: String, _ b: String) -> Bool {
|
||||
return a.compare(b, options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive, .numeric]) == .orderedAscending
|
||||
}
|
||||
}
|
||||
|
||||
private func lastErrorTextFromNetworkExtension(for tunnel: TunnelContainer) -> (title: String, message: String)? {
|
||||
@ -353,22 +441,22 @@ class TunnelContainer: NSObject {
|
||||
self.refreshStatus()
|
||||
}
|
||||
self.activationTimer = activationTimer
|
||||
RunLoop.main.add(activationTimer, forMode: .default)
|
||||
RunLoop.main.add(activationTimer, forMode: .common)
|
||||
}
|
||||
}
|
||||
}
|
||||
var activationAttemptId: String?
|
||||
var activationTimer: Timer?
|
||||
var deactivationTimer: Timer?
|
||||
|
||||
fileprivate var tunnelProvider: NETunnelProviderManager
|
||||
private var lastTunnelConnectionStatus: NEVPNStatus?
|
||||
|
||||
var tunnelConfiguration: TunnelConfiguration? {
|
||||
return (tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol)?.asTunnelConfiguration(called: tunnelProvider.localizedDescription)
|
||||
return tunnelProvider.tunnelConfiguration
|
||||
}
|
||||
|
||||
var activateOnDemandSetting: ActivateOnDemandSetting {
|
||||
return ActivateOnDemandSetting(from: tunnelProvider)
|
||||
var onDemandOption: ActivateOnDemandOption {
|
||||
return ActivateOnDemandOption(from: tunnelProvider)
|
||||
}
|
||||
|
||||
init(tunnel: NETunnelProviderManager) {
|
||||
@ -380,13 +468,31 @@ class TunnelContainer: NSObject {
|
||||
super.init()
|
||||
}
|
||||
|
||||
func getRuntimeTunnelConfiguration(completionHandler: @escaping ((TunnelConfiguration?) -> Void)) {
|
||||
guard status != .inactive, let session = tunnelProvider.connection as? NETunnelProviderSession else {
|
||||
completionHandler(tunnelConfiguration)
|
||||
return
|
||||
}
|
||||
guard nil != (try? session.sendProviderMessage(Data(bytes: [ 0 ]), responseHandler: {
|
||||
guard self.status != .inactive, let data = $0, let base = self.tunnelConfiguration, let settings = String(data: data, encoding: .utf8) else {
|
||||
completionHandler(self.tunnelConfiguration)
|
||||
return
|
||||
}
|
||||
completionHandler((try? TunnelConfiguration(fromUapiConfig: settings, basedOn: base)) ?? self.tunnelConfiguration)
|
||||
})) else {
|
||||
completionHandler(tunnelConfiguration)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func refreshStatus() {
|
||||
let status = TunnelStatus(from: tunnelProvider.connection.status)
|
||||
self.status = status
|
||||
if status == .restarting {
|
||||
return
|
||||
}
|
||||
status = TunnelStatus(from: tunnelProvider.connection.status)
|
||||
isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled
|
||||
}
|
||||
|
||||
//swiftlint:disable:next function_body_length
|
||||
fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, activationDelegate: TunnelsManagerActivationDelegate?) {
|
||||
if recursionCount >= 8 {
|
||||
wg_log(.error, message: "startActivation: Failed after 8 attempts. Giving up with \(lastError!)")
|
||||
@ -455,6 +561,27 @@ class TunnelContainer: NSObject {
|
||||
}
|
||||
|
||||
fileprivate func startDeactivation() {
|
||||
wg_log(.debug, message: "startDeactivation: Tunnel: \(name)")
|
||||
(tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
|
||||
}
|
||||
}
|
||||
|
||||
extension NETunnelProviderManager {
|
||||
private static var cachedConfigKey: UInt8 = 0
|
||||
var tunnelConfiguration: TunnelConfiguration? {
|
||||
if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey) as? TunnelConfiguration {
|
||||
return cached
|
||||
}
|
||||
let config = (protocolConfiguration as? NETunnelProviderProtocol)?.asTunnelConfiguration(called: localizedDescription)
|
||||
if config != nil {
|
||||
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, config, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func setTunnelConfiguration(_ tunnelConfiguration: TunnelConfiguration) {
|
||||
protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration, previouslyFrom: protocolConfiguration)
|
||||
localizedDescription = tunnelConfiguration.name
|
||||
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
198
WireGuard/WireGuard/UI/ActivateOnDemandViewModel.swift
Normal file
@ -0,0 +1,198 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
class ActivateOnDemandViewModel {
|
||||
enum OnDemandField {
|
||||
case onDemand
|
||||
case nonWiFiInterface
|
||||
case wiFiInterface
|
||||
case ssid
|
||||
|
||||
var localizedUIString: String {
|
||||
switch self {
|
||||
case .onDemand:
|
||||
return tr("tunnelOnDemandKey")
|
||||
case .nonWiFiInterface:
|
||||
#if os(iOS)
|
||||
return tr("tunnelOnDemandCellular")
|
||||
#elseif os(macOS)
|
||||
return tr("tunnelOnDemandEthernet")
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
case .wiFiInterface: return tr("tunnelOnDemandWiFi")
|
||||
case .ssid: return tr("tunnelOnDemandSSIDsKey")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum OnDemandSSIDOption {
|
||||
case anySSID
|
||||
case onlySpecificSSIDs
|
||||
case exceptSpecificSSIDs
|
||||
|
||||
var localizedUIString: String {
|
||||
switch self {
|
||||
case .anySSID: return tr("tunnelOnDemandAnySSID")
|
||||
case .onlySpecificSSIDs: return tr("tunnelOnDemandOnlyTheseSSIDs")
|
||||
case .exceptSpecificSSIDs: return tr("tunnelOnDemandExceptTheseSSIDs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isNonWiFiInterfaceEnabled = false
|
||||
var isWiFiInterfaceEnabled = false
|
||||
var selectedSSIDs = [String]()
|
||||
var ssidOption: OnDemandSSIDOption = .anySSID
|
||||
}
|
||||
|
||||
extension ActivateOnDemandViewModel {
|
||||
convenience init(tunnel: TunnelContainer) {
|
||||
self.init()
|
||||
if tunnel.isActivateOnDemandEnabled {
|
||||
switch tunnel.onDemandOption {
|
||||
case .off:
|
||||
break
|
||||
case .wiFiInterfaceOnly(let onDemandSSIDOption):
|
||||
isWiFiInterfaceEnabled = true
|
||||
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
|
||||
case .nonWiFiInterfaceOnly:
|
||||
isNonWiFiInterfaceEnabled = true
|
||||
case .anyInterface(let onDemandSSIDOption):
|
||||
isWiFiInterfaceEnabled = true
|
||||
isNonWiFiInterfaceEnabled = true
|
||||
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toOnDemandOption() -> ActivateOnDemandOption {
|
||||
switch (isWiFiInterfaceEnabled, isNonWiFiInterfaceEnabled) {
|
||||
case (false, false):
|
||||
return .off
|
||||
case (false, true):
|
||||
return .nonWiFiInterfaceOnly
|
||||
case (true, false):
|
||||
return .wiFiInterfaceOnly(toSSIDOption())
|
||||
case (true, true):
|
||||
return .anyInterface(toSSIDOption())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ActivateOnDemandViewModel {
|
||||
func isEnabled(field: OnDemandField) -> Bool {
|
||||
switch field {
|
||||
case .nonWiFiInterface:
|
||||
return isNonWiFiInterfaceEnabled
|
||||
case .wiFiInterface:
|
||||
return isWiFiInterfaceEnabled
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func setEnabled(field: OnDemandField, isEnabled: Bool) {
|
||||
switch field {
|
||||
case .nonWiFiInterface:
|
||||
isNonWiFiInterfaceEnabled = isEnabled
|
||||
case .wiFiInterface:
|
||||
isWiFiInterfaceEnabled = isEnabled
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ActivateOnDemandViewModel {
|
||||
var localizedInterfaceDescription: String {
|
||||
switch (isWiFiInterfaceEnabled, isNonWiFiInterfaceEnabled) {
|
||||
case (false, false):
|
||||
return tr("tunnelOnDemandOptionOff")
|
||||
case (true, false):
|
||||
return tr("tunnelOnDemandOptionWiFiOnly")
|
||||
case (false, true):
|
||||
#if os(iOS)
|
||||
return tr("tunnelOnDemandOptionCellularOnly")
|
||||
#elseif os(macOS)
|
||||
return tr("tunnelOnDemandOptionEthernetOnly")
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
case (true, true):
|
||||
#if os(iOS)
|
||||
return tr("tunnelOnDemandOptionWiFiOrCellular")
|
||||
#elseif os(macOS)
|
||||
return tr("tunnelOnDemandOptionWiFiOrEthernet")
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
var localizedSSIDDescription: String {
|
||||
guard isWiFiInterfaceEnabled else { return "" }
|
||||
switch ssidOption {
|
||||
case .anySSID: return tr("tunnelOnDemandAnySSID")
|
||||
case .onlySpecificSSIDs:
|
||||
if selectedSSIDs.count == 1 {
|
||||
return tr(format: "tunnelOnDemandOnlySSID (%d)", selectedSSIDs.count)
|
||||
} else {
|
||||
return tr(format: "tunnelOnDemandOnlySSIDs (%d)", selectedSSIDs.count)
|
||||
}
|
||||
case .exceptSpecificSSIDs:
|
||||
if selectedSSIDs.count == 1 {
|
||||
return tr(format: "tunnelOnDemandExceptSSID (%d)", selectedSSIDs.count)
|
||||
} else {
|
||||
return tr(format: "tunnelOnDemandExceptSSIDs (%d)", selectedSSIDs.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fixSSIDOption() {
|
||||
selectedSSIDs = uniquifiedNonEmptySelectedSSIDs()
|
||||
if selectedSSIDs.isEmpty {
|
||||
ssidOption = .anySSID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ActivateOnDemandViewModel {
|
||||
func ssidViewModel(from ssidOption: ActivateOnDemandSSIDOption) -> (OnDemandSSIDOption, [String]) {
|
||||
switch ssidOption {
|
||||
case .anySSID:
|
||||
return (.anySSID, [])
|
||||
case .onlySpecificSSIDs(let ssids):
|
||||
return (.onlySpecificSSIDs, ssids)
|
||||
case .exceptSpecificSSIDs(let ssids):
|
||||
return (.exceptSpecificSSIDs, ssids)
|
||||
}
|
||||
}
|
||||
|
||||
func toSSIDOption() -> ActivateOnDemandSSIDOption {
|
||||
switch ssidOption {
|
||||
case .anySSID:
|
||||
return .anySSID
|
||||
case .onlySpecificSSIDs:
|
||||
let ssids = uniquifiedNonEmptySelectedSSIDs()
|
||||
return ssids.isEmpty ? .anySSID : .onlySpecificSSIDs(selectedSSIDs)
|
||||
case .exceptSpecificSSIDs:
|
||||
let ssids = uniquifiedNonEmptySelectedSSIDs()
|
||||
return ssids.isEmpty ? .anySSID : .exceptSpecificSSIDs(selectedSSIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func uniquifiedNonEmptySelectedSSIDs() -> [String] {
|
||||
let nonEmptySSIDs = selectedSSIDs.filter { !$0.isEmpty }
|
||||
var seenSSIDs = Set<String>()
|
||||
var uniquified = [String]()
|
||||
for ssid in nonEmptySSIDs {
|
||||
guard !seenSSIDs.contains(ssid) else { continue }
|
||||
uniquified.append(ssid)
|
||||
seenSSIDs.insert(ssid)
|
||||
}
|
||||
return uniquified
|
||||
}
|
||||
}
|
25
WireGuard/WireGuard/UI/ErrorPresenterProtocol.swift
Normal file
@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
protocol ErrorPresenterProtocol {
|
||||
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?, onDismissal: (() -> Void)?)
|
||||
}
|
||||
|
||||
extension ErrorPresenterProtocol {
|
||||
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?) {
|
||||
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: onPresented, onDismissal: nil)
|
||||
}
|
||||
|
||||
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onDismissal: (() -> Void)?) {
|
||||
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: nil, onDismissal: onDismissal)
|
||||
}
|
||||
|
||||
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?) {
|
||||
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: nil, onDismissal: nil)
|
||||
}
|
||||
|
||||
static func showErrorAlert(error: WireGuardAppError, from sourceVC: AnyObject?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
|
||||
let (title, message) = error.alertText
|
||||
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: onPresented, onDismissal: onDismissal)
|
||||
}
|
||||
}
|
37
WireGuard/WireGuard/UI/PrivateDataConfirmation.swift
Normal file
@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import LocalAuthentication
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
class PrivateDataConfirmation {
|
||||
static func confirmAccess(to reason: String, _ after: @escaping () -> Void) {
|
||||
let context = LAContext()
|
||||
|
||||
var error: NSError?
|
||||
if !context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
|
||||
guard let error = error as? LAError else { return }
|
||||
if error.code == .passcodeNotSet {
|
||||
// We give no protection to folks who just don't set a passcode.
|
||||
after()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in
|
||||
DispatchQueue.main.async {
|
||||
#if os(macOS)
|
||||
if !NSApp.isActive {
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
#endif
|
||||
if success {
|
||||
after()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
84
WireGuard/WireGuard/UI/TunnelImporter.swift
Normal file
@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
class TunnelImporter {
|
||||
static func importFromFile(urls: [URL], into tunnelsManager: TunnelsManager, sourceVC: AnyObject?, errorPresenterType: ErrorPresenterProtocol.Type, completionHandler: (() -> Void)? = nil) {
|
||||
guard !urls.isEmpty else {
|
||||
completionHandler?()
|
||||
return
|
||||
}
|
||||
let dispatchGroup = DispatchGroup()
|
||||
var configs = [TunnelConfiguration?]()
|
||||
var lastFileImportErrorText: (title: String, message: String)?
|
||||
for url in urls {
|
||||
if url.pathExtension.lowercased() == "zip" {
|
||||
dispatchGroup.enter()
|
||||
ZipImporter.importConfigFiles(from: url) { result in
|
||||
if let error = result.error {
|
||||
lastFileImportErrorText = error.alertText
|
||||
}
|
||||
if let configsInZip = result.value {
|
||||
configs.append(contentsOf: configsInZip)
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
} else { /* if it is not a zip, we assume it is a conf */
|
||||
let fileName = url.lastPathComponent
|
||||
let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
dispatchGroup.enter()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let fileContents: String
|
||||
do {
|
||||
fileContents = try String(contentsOf: url)
|
||||
} catch let error {
|
||||
DispatchQueue.main.async {
|
||||
if let cocoaError = error as? CocoaError, cocoaError.isFileError {
|
||||
lastFileImportErrorText = (title: tr("alertCantOpenInputConfFileTitle"), message: error.localizedDescription)
|
||||
} else {
|
||||
lastFileImportErrorText = (title: tr("alertCantOpenInputConfFileTitle"), message: tr(format: "alertCantOpenInputConfFileMessage (%@)", fileName))
|
||||
}
|
||||
configs.append(nil)
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
return
|
||||
}
|
||||
let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: fileContents, called: fileBaseName)
|
||||
DispatchQueue.main.async {
|
||||
if tunnelConfiguration == nil {
|
||||
lastFileImportErrorText = (title: tr("alertBadConfigImportTitle"), message: tr(format: "alertBadConfigImportMessage (%@)", fileName))
|
||||
}
|
||||
configs.append(tunnelConfiguration)
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { numberSuccessful, lastAddError in
|
||||
if !configs.isEmpty && numberSuccessful == configs.count {
|
||||
completionHandler?()
|
||||
return
|
||||
}
|
||||
let alertText: (title: String, message: String)?
|
||||
if urls.count == 1 {
|
||||
if urls.first!.pathExtension.lowercased() == "zip" && !configs.isEmpty {
|
||||
alertText = (title: tr(format: "alertImportedFromZipTitle (%d)", numberSuccessful),
|
||||
message: tr(format: "alertImportedFromZipMessage (%1$d of %2$d)", numberSuccessful, configs.count))
|
||||
} else {
|
||||
alertText = lastFileImportErrorText ?? lastAddError?.alertText
|
||||
}
|
||||
} else {
|
||||
alertText = (title: tr(format: "alertImportedFromMultipleFilesTitle (%d)", numberSuccessful),
|
||||
message: tr(format: "alertImportedFromMultipleFilesMessage (%1$d of %2$d)", numberSuccessful, configs.count))
|
||||
}
|
||||
if let alertText = alertText {
|
||||
errorPresenterType.showErrorAlert(title: alertText.title, message: alertText.message, from: sourceVC, onPresented: completionHandler)
|
||||
} else {
|
||||
completionHandler?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import Foundation
|
||||
|
||||
//swiftlint:disable:next type_body_length
|
||||
class TunnelViewModel {
|
||||
|
||||
enum InterfaceField {
|
||||
enum InterfaceField: CaseIterable {
|
||||
case name
|
||||
case privateKey
|
||||
case publicKey
|
||||
@ -15,6 +14,8 @@ class TunnelViewModel {
|
||||
case listenPort
|
||||
case mtu
|
||||
case dns
|
||||
case status
|
||||
case toggleStatus
|
||||
|
||||
var localizedUIString: String {
|
||||
switch self {
|
||||
@ -26,6 +27,8 @@ class TunnelViewModel {
|
||||
case .listenPort: return tr("tunnelInterfaceListenPort")
|
||||
case .mtu: return tr("tunnelInterfaceMTU")
|
||||
case .dns: return tr("tunnelInterfaceDNS")
|
||||
case .status: return tr("tunnelInterfaceStatus")
|
||||
case .toggleStatus: return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -34,12 +37,15 @@ class TunnelViewModel {
|
||||
.generateKeyPair
|
||||
]
|
||||
|
||||
enum PeerField {
|
||||
enum PeerField: CaseIterable {
|
||||
case publicKey
|
||||
case preSharedKey
|
||||
case endpoint
|
||||
case persistentKeepAlive
|
||||
case allowedIPs
|
||||
case rxBytes
|
||||
case txBytes
|
||||
case lastHandshakeTime
|
||||
case excludePrivateIPs
|
||||
case deletePeer
|
||||
|
||||
@ -50,6 +56,9 @@ class TunnelViewModel {
|
||||
case .endpoint: return tr("tunnelPeerEndpoint")
|
||||
case .persistentKeepAlive: return tr("tunnelPeerPersistentKeepalive")
|
||||
case .allowedIPs: return tr("tunnelPeerAllowedIPs")
|
||||
case .rxBytes: return tr("tunnelPeerRxBytes")
|
||||
case .txBytes: return tr("tunnelPeerTxBytes")
|
||||
case .lastHandshakeTime: return tr("tunnelPeerLastHandshakeTime")
|
||||
case .excludePrivateIPs: return tr("tunnelPeerExcludePrivateIPs")
|
||||
case .deletePeer: return tr("deletePeerButtonTitle")
|
||||
}
|
||||
@ -62,6 +71,19 @@ class TunnelViewModel {
|
||||
|
||||
static let keyLengthInBase64 = 44
|
||||
|
||||
struct Changes {
|
||||
enum FieldChange: Equatable {
|
||||
case added
|
||||
case removed
|
||||
case modified(newValue: String)
|
||||
}
|
||||
|
||||
var interfaceChanges: [InterfaceField: FieldChange]
|
||||
var peerChanges: [(peerIndex: Int, changes: [PeerField: FieldChange])]
|
||||
var peersRemovedIndices: [Int]
|
||||
var peersInsertedIndices: [Int]
|
||||
}
|
||||
|
||||
class InterfaceData {
|
||||
var scratchpad = [InterfaceField: String]()
|
||||
var fieldsWithError = Set<InterfaceField>()
|
||||
@ -87,9 +109,9 @@ class TunnelViewModel {
|
||||
scratchpad[field] = stringValue
|
||||
}
|
||||
if field == .privateKey {
|
||||
if stringValue.count == TunnelViewModel.keyLengthInBase64, let privateKey = Data(base64Encoded: stringValue), privateKey.count == TunnelConfiguration.keyLength {
|
||||
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey)
|
||||
scratchpad[.publicKey] = publicKey.base64EncodedString()
|
||||
if stringValue.count == TunnelViewModel.keyLengthInBase64, let privateKey = Data(base64Key: stringValue), privateKey.count == TunnelConfiguration.keyLength {
|
||||
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey).base64Key() ?? ""
|
||||
scratchpad[.publicKey] = publicKey
|
||||
} else {
|
||||
scratchpad.removeValue(forKey: .publicKey)
|
||||
}
|
||||
@ -100,9 +122,14 @@ class TunnelViewModel {
|
||||
func populateScratchpad() {
|
||||
guard let config = validatedConfiguration else { return }
|
||||
guard let name = validatedName else { return }
|
||||
scratchpad = TunnelViewModel.InterfaceData.createScratchPad(from: config, name: name)
|
||||
}
|
||||
|
||||
private static func createScratchPad(from config: InterfaceConfiguration, name: String) -> [InterfaceField: String] {
|
||||
var scratchpad = [InterfaceField: String]()
|
||||
scratchpad[.name] = name
|
||||
scratchpad[.privateKey] = config.privateKey.base64EncodedString()
|
||||
scratchpad[.publicKey] = config.publicKey.base64EncodedString()
|
||||
scratchpad[.privateKey] = config.privateKey.base64Key() ?? ""
|
||||
scratchpad[.publicKey] = config.publicKey.base64Key() ?? ""
|
||||
if !config.addresses.isEmpty {
|
||||
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||
}
|
||||
@ -115,9 +142,9 @@ class TunnelViewModel {
|
||||
if !config.dns.isEmpty {
|
||||
scratchpad[.dns] = config.dns.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||
}
|
||||
return scratchpad
|
||||
}
|
||||
|
||||
//swiftlint:disable:next cyclomatic_complexity function_body_length
|
||||
func save() -> SaveResult<(String, InterfaceConfiguration)> {
|
||||
if let config = validatedConfiguration, let name = validatedName {
|
||||
return .saved((name, config))
|
||||
@ -131,7 +158,7 @@ class TunnelViewModel {
|
||||
fieldsWithError.insert(.privateKey)
|
||||
return .error(tr("alertInvalidInterfaceMessagePrivateKeyRequired"))
|
||||
}
|
||||
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
|
||||
guard let privateKey = Data(base64Key: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
|
||||
fieldsWithError.insert(.privateKey)
|
||||
return .error(tr("alertInvalidInterfaceMessagePrivateKeyInvalid"))
|
||||
}
|
||||
@ -193,6 +220,30 @@ class TunnelViewModel {
|
||||
return !self[field].isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
func applyConfiguration(other: InterfaceConfiguration, otherName: String) -> [InterfaceField: Changes.FieldChange] {
|
||||
if scratchpad.isEmpty {
|
||||
populateScratchpad()
|
||||
}
|
||||
let otherScratchPad = InterfaceData.createScratchPad(from: other, name: otherName)
|
||||
var changes = [InterfaceField: Changes.FieldChange]()
|
||||
for field in InterfaceField.allCases {
|
||||
switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
|
||||
case ("", ""):
|
||||
break
|
||||
case ("", _):
|
||||
changes[field] = .added
|
||||
case (_, ""):
|
||||
changes[field] = .removed
|
||||
case (let this, let other):
|
||||
if this != other {
|
||||
changes[field] = .modified(newValue: other)
|
||||
}
|
||||
}
|
||||
}
|
||||
scratchpad = otherScratchPad
|
||||
return changes
|
||||
}
|
||||
}
|
||||
|
||||
class PeerData {
|
||||
@ -200,6 +251,15 @@ class TunnelViewModel {
|
||||
var scratchpad = [PeerField: String]()
|
||||
var fieldsWithError = Set<PeerField>()
|
||||
var validatedConfiguration: PeerConfiguration?
|
||||
var publicKey: Data? {
|
||||
if let validatedConfiguration = validatedConfiguration {
|
||||
return validatedConfiguration.publicKey
|
||||
}
|
||||
if let scratchPadPublicKey = scratchpad[.publicKey] {
|
||||
return Data(base64Key: scratchPadPublicKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private(set) var shouldAllowExcludePrivateIPsControl = false
|
||||
private(set) var shouldStronglyRecommendDNS = false
|
||||
@ -235,9 +295,17 @@ class TunnelViewModel {
|
||||
|
||||
func populateScratchpad() {
|
||||
guard let config = validatedConfiguration else { return }
|
||||
scratchpad[.publicKey] = config.publicKey.base64EncodedString()
|
||||
if let preSharedKey = config.preSharedKey {
|
||||
scratchpad[.preSharedKey] = preSharedKey.base64EncodedString()
|
||||
scratchpad = TunnelViewModel.PeerData.createScratchPad(from: config)
|
||||
updateExcludePrivateIPsFieldState()
|
||||
}
|
||||
|
||||
private static func createScratchPad(from config: PeerConfiguration) -> [PeerField: String] {
|
||||
var scratchpad = [PeerField: String]()
|
||||
if let publicKey = config.publicKey.base64Key() {
|
||||
scratchpad[.publicKey] = publicKey
|
||||
}
|
||||
if let preSharedKey = config.preSharedKey?.base64Key() {
|
||||
scratchpad[.preSharedKey] = preSharedKey
|
||||
}
|
||||
if !config.allowedIPs.isEmpty {
|
||||
scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||
@ -248,10 +316,18 @@ class TunnelViewModel {
|
||||
if let persistentKeepAlive = config.persistentKeepAlive {
|
||||
scratchpad[.persistentKeepAlive] = String(persistentKeepAlive)
|
||||
}
|
||||
updateExcludePrivateIPsFieldState()
|
||||
if let rxBytes = config.rxBytes {
|
||||
scratchpad[.rxBytes] = prettyBytes(rxBytes)
|
||||
}
|
||||
if let txBytes = config.txBytes {
|
||||
scratchpad[.txBytes] = prettyBytes(txBytes)
|
||||
}
|
||||
if let lastHandshakeTime = config.lastHandshakeTime {
|
||||
scratchpad[.lastHandshakeTime] = prettyTimeAgo(timestamp: lastHandshakeTime)
|
||||
}
|
||||
return scratchpad
|
||||
}
|
||||
|
||||
//swiftlint:disable:next cyclomatic_complexity
|
||||
func save() -> SaveResult<PeerConfiguration> {
|
||||
if let validatedConfiguration = validatedConfiguration {
|
||||
return .saved(validatedConfiguration)
|
||||
@ -261,14 +337,14 @@ class TunnelViewModel {
|
||||
fieldsWithError.insert(.publicKey)
|
||||
return .error(tr("alertInvalidPeerMessagePublicKeyRequired"))
|
||||
}
|
||||
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
|
||||
guard let publicKey = Data(base64Key: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
|
||||
fieldsWithError.insert(.publicKey)
|
||||
return .error(tr("alertInvalidPeerMessagePublicKeyInvalid"))
|
||||
}
|
||||
var config = PeerConfiguration(publicKey: publicKey)
|
||||
var errorMessages = [String]()
|
||||
if let preSharedKeyString = scratchpad[.preSharedKey] {
|
||||
if let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength {
|
||||
if let preSharedKey = Data(base64Key: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength {
|
||||
config.preSharedKey = preSharedKey
|
||||
} else {
|
||||
fieldsWithError.insert(.preSharedKey)
|
||||
@ -329,43 +405,77 @@ class TunnelViewModel {
|
||||
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
|
||||
]
|
||||
|
||||
static func excludePrivateIPsFieldStates(isSinglePeer: Bool, allowedIPs: Set<String>) -> (shouldAllowExcludePrivateIPsControl: Bool, excludePrivateIPsValue: Bool) {
|
||||
guard isSinglePeer else {
|
||||
return (shouldAllowExcludePrivateIPsControl: false, excludePrivateIPsValue: false)
|
||||
}
|
||||
let allowedIPStrings = Set<String>(allowedIPs)
|
||||
if allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) {
|
||||
return (shouldAllowExcludePrivateIPsControl: true, excludePrivateIPsValue: false)
|
||||
} else if allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String) {
|
||||
return (shouldAllowExcludePrivateIPsControl: true, excludePrivateIPsValue: true)
|
||||
} else {
|
||||
return (shouldAllowExcludePrivateIPsControl: false, excludePrivateIPsValue: false)
|
||||
}
|
||||
}
|
||||
|
||||
func updateExcludePrivateIPsFieldState() {
|
||||
if scratchpad.isEmpty {
|
||||
populateScratchpad()
|
||||
}
|
||||
let allowedIPStrings = Set<String>(scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines))
|
||||
(shouldAllowExcludePrivateIPsControl, excludePrivateIPsValue) = TunnelViewModel.PeerData.excludePrivateIPsFieldStates(isSinglePeer: numberOfPeers == 1, allowedIPs: allowedIPStrings)
|
||||
shouldStronglyRecommendDNS = allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) || allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String)
|
||||
guard numberOfPeers == 1 else {
|
||||
shouldAllowExcludePrivateIPsControl = false
|
||||
excludePrivateIPsValue = false
|
||||
return
|
||||
}
|
||||
if allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) {
|
||||
shouldAllowExcludePrivateIPsControl = true
|
||||
excludePrivateIPsValue = false
|
||||
} else if allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String) {
|
||||
shouldAllowExcludePrivateIPsControl = true
|
||||
excludePrivateIPsValue = true
|
||||
}
|
||||
|
||||
static func normalizedIPAddressRangeStrings(_ list: [String]) -> [String] {
|
||||
return list.compactMap { IPAddressRange(from: $0) }.map { $0.stringRepresentation }
|
||||
}
|
||||
|
||||
static func modifiedAllowedIPs(currentAllowedIPs: [String], excludePrivateIPs: Bool, dnsServers: [String], oldDNSServers: [String]?) -> [String] {
|
||||
let normalizedDNSServers = normalizedIPAddressRangeStrings(dnsServers)
|
||||
let normalizedOldDNSServers = oldDNSServers == nil ? normalizedDNSServers : normalizedIPAddressRangeStrings(oldDNSServers!)
|
||||
let ipv6Addresses = normalizedIPAddressRangeStrings(currentAllowedIPs.filter { $0.contains(":") })
|
||||
if excludePrivateIPs {
|
||||
return ipv6Addresses + TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + normalizedDNSServers
|
||||
} else {
|
||||
shouldAllowExcludePrivateIPsControl = false
|
||||
excludePrivateIPsValue = false
|
||||
return ipv6Addresses.filter { !normalizedOldDNSServers.contains($0) } + [TunnelViewModel.PeerData.ipv4DefaultRouteString]
|
||||
}
|
||||
}
|
||||
|
||||
func excludePrivateIPsValueChanged(isOn: Bool, dnsServers: String) {
|
||||
func excludePrivateIPsValueChanged(isOn: Bool, dnsServers: String, oldDNSServers: String? = nil) {
|
||||
let allowedIPStrings = scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)
|
||||
let dnsServerStrings = dnsServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines)
|
||||
let ipv6Addresses = allowedIPStrings.filter { $0.contains(":") }
|
||||
let modifiedAllowedIPStrings: [String]
|
||||
if isOn {
|
||||
modifiedAllowedIPStrings = ipv6Addresses + TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + dnsServerStrings
|
||||
} else {
|
||||
modifiedAllowedIPStrings = ipv6Addresses + [TunnelViewModel.PeerData.ipv4DefaultRouteString]
|
||||
}
|
||||
let oldDNSServerStrings = oldDNSServers?.splitToArray(trimmingCharacters: .whitespacesAndNewlines)
|
||||
let modifiedAllowedIPStrings = TunnelViewModel.PeerData.modifiedAllowedIPs(currentAllowedIPs: allowedIPStrings, excludePrivateIPs: isOn, dnsServers: dnsServerStrings, oldDNSServers: oldDNSServerStrings)
|
||||
scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ")
|
||||
validatedConfiguration = nil
|
||||
excludePrivateIPsValue = isOn
|
||||
}
|
||||
|
||||
func applyConfiguration(other: PeerConfiguration) -> [PeerField: Changes.FieldChange] {
|
||||
if scratchpad.isEmpty {
|
||||
populateScratchpad()
|
||||
}
|
||||
let otherScratchPad = PeerData.createScratchPad(from: other)
|
||||
var changes = [PeerField: Changes.FieldChange]()
|
||||
for field in PeerField.allCases {
|
||||
switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
|
||||
case ("", ""):
|
||||
break
|
||||
case ("", _):
|
||||
changes[field] = .added
|
||||
case (_, ""):
|
||||
changes[field] = .removed
|
||||
case (let this, let other):
|
||||
if this != other {
|
||||
changes[field] = .modified(newValue: other)
|
||||
}
|
||||
}
|
||||
}
|
||||
scratchpad = otherScratchPad
|
||||
return changes
|
||||
}
|
||||
}
|
||||
|
||||
enum SaveResult<Configuration> {
|
||||
@ -373,8 +483,8 @@ class TunnelViewModel {
|
||||
case error(String)
|
||||
}
|
||||
|
||||
var interfaceData: InterfaceData
|
||||
var peersData: [PeerData]
|
||||
private(set) var interfaceData: InterfaceData
|
||||
private(set) var peersData: [PeerData]
|
||||
|
||||
init(tunnelConfiguration: TunnelConfiguration?) {
|
||||
let interfaceData = InterfaceData()
|
||||
@ -419,6 +529,17 @@ class TunnelViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
func updateDNSServersInAllowedIPsIfRequired(oldDNSServers: String, newDNSServers: String) -> Bool {
|
||||
guard peersData.count == 1, let firstPeer = peersData.first else { return false }
|
||||
guard firstPeer.shouldAllowExcludePrivateIPsControl && firstPeer.excludePrivateIPsValue else { return false }
|
||||
let allowedIPStrings = firstPeer[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)
|
||||
let oldDNSServerStrings = TunnelViewModel.PeerData.normalizedIPAddressRangeStrings(oldDNSServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines))
|
||||
let newDNSServerStrings = TunnelViewModel.PeerData.normalizedIPAddressRangeStrings(newDNSServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines))
|
||||
let updatedAllowedIPStrings = allowedIPStrings.filter { !oldDNSServerStrings.contains($0) } + newDNSServerStrings
|
||||
firstPeer[.allowedIPs] = updatedAllowedIPStrings.joined(separator: ", ")
|
||||
return true
|
||||
}
|
||||
|
||||
func save() -> SaveResult<TunnelConfiguration> {
|
||||
let interfaceSaveResult = interfaceData.save()
|
||||
let peerSaveResults = peersData.map { $0.save() } // Save all, to help mark erroring fields in red
|
||||
@ -447,35 +568,130 @@ class TunnelViewModel {
|
||||
return .saved(tunnelConfiguration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelViewModel {
|
||||
static func activateOnDemandOptionText(for activateOnDemandOption: ActivateOnDemandOption) -> String {
|
||||
switch activateOnDemandOption {
|
||||
case .none:
|
||||
return tr("tunnelOnDemandOptionOff")
|
||||
case .useOnDemandOverWiFiOrCellular:
|
||||
return tr("tunnelOnDemandOptionWiFiOrCellular")
|
||||
case .useOnDemandOverWiFiOnly:
|
||||
return tr("tunnelOnDemandOptionWiFiOnly")
|
||||
case .useOnDemandOverCellularOnly:
|
||||
return tr("tunnelOnDemandOptionCellularOnly")
|
||||
func asWgQuickConfig() -> String? {
|
||||
let saveResult = save()
|
||||
if case .saved(let tunnelConfiguration) = saveResult {
|
||||
return tunnelConfiguration.asWgQuickConfig()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func activateOnDemandDetailText(for activateOnDemandSetting: ActivateOnDemandSetting?) -> String {
|
||||
if let activateOnDemandSetting = activateOnDemandSetting {
|
||||
if activateOnDemandSetting.isActivateOnDemandEnabled {
|
||||
return TunnelViewModel.activateOnDemandOptionText(for: activateOnDemandSetting.activateOnDemandOption)
|
||||
} else {
|
||||
return TunnelViewModel.activateOnDemandOptionText(for: .none)
|
||||
@discardableResult
|
||||
func applyConfiguration(other: TunnelConfiguration) -> Changes {
|
||||
// Replaces current data with data from other TunnelConfiguration, ignoring any changes in peer ordering.
|
||||
|
||||
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]
|
||||
let changes = peerData.applyConfiguration(other: otherPeer)
|
||||
if !changes.isEmpty {
|
||||
peerChanges.append((peerIndex: peersDataIndex, changes: changes))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return TunnelViewModel.activateOnDemandOptionText(for: .none)
|
||||
}
|
||||
}
|
||||
|
||||
static func defaultActivateOnDemandOption() -> ActivateOnDemandOption {
|
||||
return .useOnDemandOverWiFiOrCellular
|
||||
var removedPeerIndices = [Int]()
|
||||
for (index, peerData) in peersData.enumerated().reversed() {
|
||||
if let peerPublicKey = peerData.publicKey, !other.peers.contains(where: { $0.publicKey == peerPublicKey}) {
|
||||
removedPeerIndices.append(index)
|
||||
peersData.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
var addedPeerIndices = [Int]()
|
||||
for otherPeer in other.peers {
|
||||
if !peersData.contains(where: { $0.publicKey == otherPeer.publicKey }) {
|
||||
addedPeerIndices.append(peersData.count)
|
||||
let peerData = PeerData(index: peersData.count)
|
||||
peerData.validatedConfiguration = otherPeer
|
||||
peersData.append(peerData)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private func prettyBytes(_ bytes: UInt64) -> String {
|
||||
switch bytes {
|
||||
case 0..<1024:
|
||||
return "\(bytes) B"
|
||||
case 1024 ..< (1024 * 1024):
|
||||
return String(format: "%.2f", Double(bytes) / 1024) + " KiB"
|
||||
case 1024 ..< (1024 * 1024 * 1024):
|
||||
return String(format: "%.2f", Double(bytes) / (1024 * 1024)) + " MiB"
|
||||
case 1024 ..< (1024 * 1024 * 1024 * 1024):
|
||||
return String(format: "%.2f", Double(bytes) / (1024 * 1024 * 1024)) + " GiB"
|
||||
default:
|
||||
return String(format: "%.2f", Double(bytes) / (1024 * 1024 * 1024 * 1024)) + " TiB"
|
||||
}
|
||||
}
|
||||
|
||||
private func prettyTimeAgo(timestamp: Date) -> String {
|
||||
let now = Date()
|
||||
let timeInterval = Int64(now.timeIntervalSince(timestamp))
|
||||
switch timeInterval {
|
||||
case ..<0: return tr("tunnelHandshakeTimestampSystemClockBackward")
|
||||
case 0: return tr("tunnelHandshakeTimestampNow")
|
||||
default:
|
||||
return tr(format: "tunnelHandshakeTimestampAgo (%@)", prettyTime(secondsLeft: timeInterval))
|
||||
}
|
||||
}
|
||||
|
||||
private func prettyTime(secondsLeft: Int64) -> String {
|
||||
var left = secondsLeft
|
||||
var timeStrings = [String]()
|
||||
let years = left / (365 * 24 * 60 * 60)
|
||||
left = left % (365 * 24 * 60 * 60)
|
||||
let days = left / (24 * 60 * 60)
|
||||
left = left % (24 * 60 * 60)
|
||||
let hours = left / (60 * 60)
|
||||
left = left % (60 * 60)
|
||||
let minutes = left / 60
|
||||
let seconds = left % 60
|
||||
|
||||
#if os(iOS)
|
||||
if years > 0 {
|
||||
return years == 1 ? tr(format: "tunnelHandshakeTimestampYear (%d)", years) : tr(format: "tunnelHandshakeTimestampYears (%d)", years)
|
||||
}
|
||||
if days > 0 {
|
||||
return days == 1 ? tr(format: "tunnelHandshakeTimestampDay (%d)", days) : tr(format: "tunnelHandshakeTimestampDays (%d)", days)
|
||||
}
|
||||
if hours > 0 {
|
||||
let hhmmss = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
|
||||
return tr(format: "tunnelHandshakeTimestampHours hh:mm:ss (%@)", hhmmss)
|
||||
}
|
||||
if minutes > 0 {
|
||||
let mmss = String(format: "%02d:%02d", minutes, seconds)
|
||||
return tr(format: "tunnelHandshakeTimestampMinutes mm:ss (%@)", mmss)
|
||||
}
|
||||
return seconds == 1 ? tr(format: "tunnelHandshakeTimestampSecond (%d)", seconds) : tr(format: "tunnelHandshakeTimestampSeconds (%d)", seconds)
|
||||
#elseif os(macOS)
|
||||
if years > 0 {
|
||||
timeStrings.append(years == 1 ? tr(format: "tunnelHandshakeTimestampYear (%d)", years) : tr(format: "tunnelHandshakeTimestampYears (%d)", years))
|
||||
}
|
||||
if days > 0 {
|
||||
timeStrings.append(days == 1 ? tr(format: "tunnelHandshakeTimestampDay (%d)", days) : tr(format: "tunnelHandshakeTimestampDays (%d)", days))
|
||||
}
|
||||
if hours > 0 {
|
||||
timeStrings.append(hours == 1 ? tr(format: "tunnelHandshakeTimestampHour (%d)", hours) : tr(format: "tunnelHandshakeTimestampHours (%d)", hours))
|
||||
}
|
||||
if minutes > 0 {
|
||||
timeStrings.append(minutes == 1 ? tr(format: "tunnelHandshakeTimestampMinute (%d)", minutes) : tr(format: "tunnelHandshakeTimestampMinutes (%d)", minutes))
|
||||
}
|
||||
if seconds > 0 {
|
||||
timeStrings.append(seconds == 1 ? tr(format: "tunnelHandshakeTimestampSecond (%d)", seconds) : tr(format: "tunnelHandshakeTimestampSeconds (%d)", seconds))
|
||||
}
|
||||
return timeStrings.joined(separator: ", ")
|
||||
#endif
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var mainVC: MainViewController?
|
||||
|
||||
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
Logger.configureGlobal(withFilePath: FileManager.appLogFileURL?.path)
|
||||
Logger.configureGlobal(tagged: "APP", withFilePath: FileManager.logFileURL?.path)
|
||||
|
||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.backgroundColor = .white
|
||||
@ -27,7 +27,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
mainVC?.tunnelsListVC?.importFromFile(url: url) {
|
||||
guard let tunnelsManager = mainVC?.tunnelsManager else { return true }
|
||||
TunnelImporter.importFromFile(urls: [url], into: tunnelsManager, sourceVC: mainVC, errorPresenterType: ErrorPresenter.self) {
|
||||
_ = FileManager.deleteFile(at: url)
|
||||
}
|
||||
return true
|
||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 865 B After Width: | Height: | Size: 865 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
25
WireGuard/WireGuard/UI/iOS/ConfirmationAlertPresenter.swift
Normal file
@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ConfirmationAlertPresenter {
|
||||
static func showConfirmationAlert(message: String, buttonTitle: String, from sourceObject: AnyObject, presentingVC: UIViewController, onConfirmed: @escaping (() -> Void)) {
|
||||
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
|
||||
onConfirmed()
|
||||
}
|
||||
let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
|
||||
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
|
||||
alert.addAction(destroyAction)
|
||||
alert.addAction(cancelAction)
|
||||
|
||||
if let sourceView = sourceObject as? UIView {
|
||||
alert.popoverPresentationController?.sourceView = sourceView
|
||||
alert.popoverPresentationController?.sourceRect = sourceView.bounds
|
||||
} else if let sourceBarButtonItem = sourceObject as? UIBarButtonItem {
|
||||
alert.popoverPresentationController?.barButtonItem = sourceBarButtonItem
|
||||
}
|
||||
|
||||
presentingVC.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
@ -4,14 +4,9 @@
|
||||
import UIKit
|
||||
import os.log
|
||||
|
||||
class ErrorPresenter {
|
||||
static func showErrorAlert(error: WireGuardAppError, from sourceVC: UIViewController?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
|
||||
let (title, message) = error.alertText
|
||||
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: onPresented, onDismissal: onDismissal)
|
||||
}
|
||||
|
||||
static func showErrorAlert(title: String, message: String, from sourceVC: UIViewController?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
|
||||
guard let sourceVC = sourceVC else { return }
|
||||
class ErrorPresenter: ErrorPresenterProtocol {
|
||||
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?, onDismissal: (() -> Void)?) {
|
||||
guard let sourceVC = sourceVC as? UIViewController else { return }
|
||||
|
||||
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
|
||||
onDismissal?()
|
||||
|
@ -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>
|
||||
@ -122,7 +122,9 @@
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Localized</string>
|
||||
<key>com.wireguard.ios.app_group_id</key>
|
||||
<string>group.$(APP_ID)</string>
|
||||
<string>group.$(APP_ID_IOS)</string>
|
||||
</dict>
|
||||
</plist>
|
30
WireGuard/WireGuard/UI/iOS/View/ChevronCell.swift
Normal file
@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ChevronCell: UITableViewCell {
|
||||
var message: String {
|
||||
get { return textLabel?.text ?? "" }
|
||||
set(value) { textLabel?.text = value }
|
||||
}
|
||||
|
||||
var detailMessage: String {
|
||||
get { return detailTextLabel?.text ?? "" }
|
||||
set(value) { detailTextLabel?.text = value }
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .value1, reuseIdentifier: reuseIdentifier)
|
||||
accessoryType = .disclosureIndicator
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
message = ""
|
||||
}
|
||||
}
|
64
WireGuard/WireGuard/UI/iOS/View/EditableTextCell.swift
Normal file
@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class EditableTextCell: UITableViewCell {
|
||||
var message: String {
|
||||
get { return valueTextField.text ?? "" }
|
||||
set(value) { valueTextField.text = value }
|
||||
}
|
||||
|
||||
let valueTextField: UITextField = {
|
||||
let valueTextField = UITextField()
|
||||
valueTextField.textAlignment = .left
|
||||
valueTextField.isEnabled = true
|
||||
valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
valueTextField.adjustsFontForContentSizeCategory = true
|
||||
valueTextField.autocapitalizationType = .none
|
||||
valueTextField.autocorrectionType = .no
|
||||
valueTextField.spellCheckingType = .no
|
||||
return valueTextField
|
||||
}()
|
||||
|
||||
var onValueBeingEdited: ((String) -> Void)?
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
valueTextField.delegate = self
|
||||
contentView.addSubview(valueTextField)
|
||||
valueTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueTextField.bottomAnchor, multiplier: 1)
|
||||
bottomAnchorConstraint.priority = .defaultLow
|
||||
NSLayoutConstraint.activate([
|
||||
valueTextField.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leadingAnchor, multiplier: 1),
|
||||
contentView.layoutMarginsGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: valueTextField.trailingAnchor, multiplier: 1),
|
||||
valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
|
||||
bottomAnchorConstraint
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func beginEditing() {
|
||||
valueTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
message = ""
|
||||
}
|
||||
}
|
||||
|
||||
extension EditableTextCell: UITextFieldDelegate {
|
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
if let onValueBeingEdited = onValueBeingEdited {
|
||||
let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
|
||||
onValueBeingEdited(modifiedText)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
@ -68,9 +68,11 @@ class KeyValueCell: UITableViewCell {
|
||||
var isStackedVertically = false
|
||||
var contentSizeBasedConstraints = [NSLayoutConstraint]()
|
||||
|
||||
var onValueChanged: ((String) -> Void)?
|
||||
var onValueChanged: ((String, String) -> Void)?
|
||||
var onValueBeingEdited: ((String) -> Void)?
|
||||
|
||||
var observationToken: AnyObject?
|
||||
|
||||
private var textFieldValueOnBeginEditing: String = ""
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
@ -187,6 +189,7 @@ class KeyValueCell: UITableViewCell {
|
||||
keyboardType = .default
|
||||
onValueChanged = nil
|
||||
onValueBeingEdited = nil
|
||||
observationToken = nil
|
||||
key = ""
|
||||
value = ""
|
||||
configureForContentSize()
|
||||
@ -203,7 +206,7 @@ extension KeyValueCell: UITextFieldDelegate {
|
||||
func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
let isModified = textField.text ?? "" != textFieldValueOnBeginEditing
|
||||
guard isModified else { return }
|
||||
onValueChanged?(textField.text ?? "")
|
||||
onValueChanged?(textFieldValueOnBeginEditing, textField.text ?? "")
|
||||
}
|
||||
|
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
|
@ -22,6 +22,8 @@ class SwitchCell: UITableViewCell {
|
||||
|
||||
var onSwitchToggled: ((Bool) -> Void)?
|
||||
|
||||
var observationToken: AnyObject?
|
||||
|
||||
let switchView = UISwitch()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
@ -45,5 +47,6 @@ class SwitchCell: UITableViewCell {
|
||||
isEnabled = true
|
||||
message = ""
|
||||
isOn = false
|
||||
observationToken = nil
|
||||
}
|
||||
}
|
||||
|
34
WireGuard/WireGuard/UI/iOS/View/TextCell.swift
Normal file
@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class TextCell: UITableViewCell {
|
||||
var message: String {
|
||||
get { return textLabel?.text ?? "" }
|
||||
set(value) { textLabel!.text = value }
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .default, reuseIdentifier: reuseIdentifier)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setTextColor(_ color: UIColor) {
|
||||
textLabel?.textColor = color
|
||||
}
|
||||
|
||||
func setTextAlignment(_ alignment: NSTextAlignment) {
|
||||
textLabel?.textAlignment = alignment
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
message = ""
|
||||
setTextColor(.black)
|
||||
setTextAlignment(.left)
|
||||
}
|
||||
}
|
@ -98,6 +98,11 @@ class TunnelListCell: UITableViewCell {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setEditing(_ editing: Bool, animated: Bool) {
|
||||
super.setEditing(editing, animated: animated)
|
||||
statusSwitch.isEnabled = !editing
|
||||
}
|
||||
|
||||
private func reset() {
|
||||
statusSwitch.isOn = false
|
||||
statusSwitch.isUserInteractionEnabled = false
|
||||
|
@ -8,7 +8,6 @@ class MainViewController: UISplitViewController {
|
||||
var tunnelsManager: TunnelsManager?
|
||||
var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
|
||||
var tunnelsListVC: TunnelsListTableViewController?
|
||||
private var foregroundObservationToken: AnyObject?
|
||||
|
||||
init() {
|
||||
let detailVC = UIViewController()
|
||||
@ -57,29 +56,6 @@ class MainViewController: UISplitViewController {
|
||||
self.onTunnelsManagerReady?(tunnelsManager)
|
||||
self.onTunnelsManagerReady = nil
|
||||
}
|
||||
|
||||
foregroundObservationToken = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.tunnelsManager?.reload { [weak self] hasChanges in
|
||||
guard let self = self, let tunnelsManager = self.tunnelsManager, hasChanges else { return }
|
||||
|
||||
self.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)
|
||||
|
||||
if self.isCollapsed {
|
||||
(self.viewControllers[0] as? UINavigationController)?.popViewController(animated: false)
|
||||
} else {
|
||||
let detailVC = UIViewController()
|
||||
detailVC.view.backgroundColor = .white
|
||||
let detailNC = UINavigationController(rootViewController: detailVC)
|
||||
self.showDetailViewController(detailNC, sender: self)
|
||||
}
|
||||
|
||||
if let presentedNavController = self.presentedViewController as? UINavigationController, presentedNavController.viewControllers.first is TunnelEditTableViewController {
|
||||
self.presentedViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class SSIDOptionDetailTableViewController: UITableViewController {
|
||||
|
||||
let selectedSSIDs: [String]
|
||||
|
||||
init(title: String, ssids: [String]) {
|
||||
selectedSSIDs = ssids
|
||||
super.init(style: .grouped)
|
||||
self.title = title
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.estimatedRowHeight = 44
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.allowsSelection = false
|
||||
|
||||
tableView.register(TextCell.self)
|
||||
}
|
||||
}
|
||||
|
||||
extension SSIDOptionDetailTableViewController {
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return selectedSSIDs.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
return tr("tunnelOnDemandSectionTitleSelectedSSIDs")
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = selectedSSIDs[indexPath.row]
|
||||
return cell
|
||||
}
|
||||
}
|
@ -0,0 +1,295 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import SystemConfiguration.CaptiveNetwork
|
||||
|
||||
protocol SSIDOptionEditTableViewControllerDelegate: class {
|
||||
func ssidOptionSaved(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String])
|
||||
}
|
||||
|
||||
class SSIDOptionEditTableViewController: UITableViewController {
|
||||
private enum Section {
|
||||
case ssidOption
|
||||
case selectedSSIDs
|
||||
case addSSIDs
|
||||
}
|
||||
|
||||
private enum AddSSIDRow {
|
||||
case addConnectedSSID(connectedSSID: String)
|
||||
case addNewSSID
|
||||
}
|
||||
|
||||
weak var delegate: SSIDOptionEditTableViewControllerDelegate?
|
||||
|
||||
private var sections = [Section]()
|
||||
private var addSSIDRows = [AddSSIDRow]()
|
||||
|
||||
let ssidOptionFields: [ActivateOnDemandViewModel.OnDemandSSIDOption] = [
|
||||
.anySSID,
|
||||
.onlySpecificSSIDs,
|
||||
.exceptSpecificSSIDs
|
||||
]
|
||||
|
||||
var selectedOption: ActivateOnDemandViewModel.OnDemandSSIDOption
|
||||
var selectedSSIDs: [String]
|
||||
var connectedSSID: String?
|
||||
|
||||
init(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String]) {
|
||||
selectedOption = option
|
||||
selectedSSIDs = ssids
|
||||
super.init(style: .grouped)
|
||||
connectedSSID = getConnectedSSID()
|
||||
loadSections()
|
||||
loadAddSSIDRows()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = tr("tunnelOnDemandSSIDViewTitle")
|
||||
|
||||
tableView.estimatedRowHeight = 44
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
|
||||
tableView.register(CheckmarkCell.self)
|
||||
tableView.register(EditableTextCell.self)
|
||||
tableView.register(TextCell.self)
|
||||
tableView.isEditing = true
|
||||
tableView.allowsSelectionDuringEditing = true
|
||||
}
|
||||
|
||||
func loadSections() {
|
||||
sections.removeAll()
|
||||
sections.append(.ssidOption)
|
||||
if selectedOption != .anySSID {
|
||||
sections.append(.selectedSSIDs)
|
||||
sections.append(.addSSIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func loadAddSSIDRows() {
|
||||
addSSIDRows.removeAll()
|
||||
if let connectedSSID = connectedSSID {
|
||||
if !selectedSSIDs.contains(connectedSSID) {
|
||||
addSSIDRows.append(.addConnectedSSID(connectedSSID: connectedSSID))
|
||||
}
|
||||
}
|
||||
addSSIDRows.append(.addNewSSID)
|
||||
}
|
||||
|
||||
func updateTableViewAddSSIDRows() {
|
||||
guard let addSSIDSection = sections.firstIndex(of: .addSSIDs) else { return }
|
||||
let numberOfAddSSIDRows = addSSIDRows.count
|
||||
let numberOfAddSSIDRowsInTableView = tableView.numberOfRows(inSection: addSSIDSection)
|
||||
switch (numberOfAddSSIDRowsInTableView, numberOfAddSSIDRows) {
|
||||
case (1, 2):
|
||||
tableView.insertRows(at: [IndexPath(row: 0, section: addSSIDSection)], with: .automatic)
|
||||
case (2, 1):
|
||||
tableView.deleteRows(at: [IndexPath(row: 0, section: addSSIDSection)], with: .automatic)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
delegate?.ssidOptionSaved(option: selectedOption, ssids: selectedSSIDs)
|
||||
}
|
||||
}
|
||||
|
||||
extension SSIDOptionEditTableViewController {
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return sections.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch sections[section] {
|
||||
case .ssidOption:
|
||||
return ssidOptionFields.count
|
||||
case .selectedSSIDs:
|
||||
return selectedSSIDs.isEmpty ? 1 : selectedSSIDs.count
|
||||
case .addSSIDs:
|
||||
return addSSIDRows.count
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
switch sections[indexPath.section] {
|
||||
case .ssidOption:
|
||||
return ssidOptionCell(for: tableView, at: indexPath)
|
||||
case .selectedSSIDs:
|
||||
if !selectedSSIDs.isEmpty {
|
||||
return selectedSSIDCell(for: tableView, at: indexPath)
|
||||
} else {
|
||||
return noSSIDsCell(for: tableView, at: indexPath)
|
||||
}
|
||||
case .addSSIDs:
|
||||
return addSSIDCell(for: tableView, at: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
switch sections[indexPath.section] {
|
||||
case .ssidOption:
|
||||
return false
|
||||
case .selectedSSIDs:
|
||||
return !selectedSSIDs.isEmpty
|
||||
case .addSSIDs:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
|
||||
switch sections[indexPath.section] {
|
||||
case .ssidOption:
|
||||
return .none
|
||||
case .selectedSSIDs:
|
||||
return .delete
|
||||
case .addSSIDs:
|
||||
return .insert
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch sections[section] {
|
||||
case .ssidOption:
|
||||
return nil
|
||||
case .selectedSSIDs:
|
||||
return tr("tunnelOnDemandSectionTitleSelectedSSIDs")
|
||||
case .addSSIDs:
|
||||
return tr("tunnelOnDemandSectionTitleAddSSIDs")
|
||||
}
|
||||
}
|
||||
|
||||
private func ssidOptionCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let field = ssidOptionFields[indexPath.row]
|
||||
let cell: CheckmarkCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = field.localizedUIString
|
||||
cell.isChecked = selectedOption == field
|
||||
cell.isEditing = false
|
||||
return cell
|
||||
}
|
||||
|
||||
private func noSSIDsCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = tr("tunnelOnDemandNoSSIDs")
|
||||
cell.setTextColor(.gray)
|
||||
cell.setTextAlignment(.center)
|
||||
return cell
|
||||
}
|
||||
|
||||
private func selectedSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: EditableTextCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = selectedSSIDs[indexPath.row]
|
||||
cell.isEditing = true
|
||||
cell.onValueBeingEdited = { [weak self, weak cell] text in
|
||||
guard let self = self, let cell = cell else { return }
|
||||
if let row = self.tableView.indexPath(for: cell)?.row {
|
||||
self.selectedSSIDs[row] = text
|
||||
self.loadAddSSIDRows()
|
||||
self.updateTableViewAddSSIDRows()
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
private func addSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
switch addSSIDRows[indexPath.row] {
|
||||
case .addConnectedSSID:
|
||||
cell.message = tr(format: "tunnelOnDemandAddMessageAddConnectedSSID (%@)", connectedSSID!)
|
||||
case .addNewSSID:
|
||||
cell.message = tr("tunnelOnDemandAddMessageAddNewSSID")
|
||||
}
|
||||
cell.isEditing = true
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
|
||||
switch sections[indexPath.section] {
|
||||
case .ssidOption:
|
||||
assertionFailure()
|
||||
case .selectedSSIDs:
|
||||
assert(editingStyle == .delete)
|
||||
selectedSSIDs.remove(at: indexPath.row)
|
||||
if !selectedSSIDs.isEmpty {
|
||||
tableView.deleteRows(at: [indexPath], with: .automatic)
|
||||
} else {
|
||||
tableView.reloadRows(at: [indexPath], with: .automatic)
|
||||
}
|
||||
loadAddSSIDRows()
|
||||
updateTableViewAddSSIDRows()
|
||||
case .addSSIDs:
|
||||
assert(editingStyle == .insert)
|
||||
let newSSID: String
|
||||
switch addSSIDRows[indexPath.row] {
|
||||
case .addConnectedSSID(let connectedSSID):
|
||||
newSSID = connectedSSID
|
||||
case .addNewSSID:
|
||||
newSSID = ""
|
||||
}
|
||||
selectedSSIDs.append(newSSID)
|
||||
loadSections()
|
||||
let selectedSSIDsSection = sections.firstIndex(of: .selectedSSIDs)!
|
||||
let indexPath = IndexPath(row: selectedSSIDs.count - 1, section: selectedSSIDsSection)
|
||||
if selectedSSIDs.count == 1 {
|
||||
tableView.reloadRows(at: [indexPath], with: .automatic)
|
||||
} else {
|
||||
tableView.insertRows(at: [indexPath], with: .automatic)
|
||||
}
|
||||
loadAddSSIDRows()
|
||||
updateTableViewAddSSIDRows()
|
||||
if newSSID.isEmpty {
|
||||
if let selectedSSIDCell = tableView.cellForRow(at: indexPath) as? EditableTextCell {
|
||||
selectedSSIDCell.beginEditing()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SSIDOptionEditTableViewController {
|
||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
switch sections[indexPath.section] {
|
||||
case .ssidOption:
|
||||
return indexPath
|
||||
case .selectedSSIDs, .addSSIDs:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
switch sections[indexPath.section] {
|
||||
case .ssidOption:
|
||||
let previousOption = selectedOption
|
||||
selectedOption = ssidOptionFields[indexPath.row]
|
||||
loadSections()
|
||||
if previousOption == .anySSID {
|
||||
let indexSet = IndexSet(1 ... 2)
|
||||
tableView.insertSections(indexSet, with: .fade)
|
||||
}
|
||||
if selectedOption == .anySSID {
|
||||
let indexSet = IndexSet(1 ... 2)
|
||||
tableView.deleteSections(indexSet, with: .fade)
|
||||
}
|
||||
tableView.reloadSections(IndexSet(integer: indexPath.section), with: .none)
|
||||
case .selectedSSIDs, .addSSIDs:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getConnectedSSID() -> String? {
|
||||
guard let supportedInterfaces = CNCopySupportedInterfaces() as? [CFString] else { return nil }
|
||||
for interface in supportedInterfaces {
|
||||
if let networkInfo = CNCopyCurrentNetworkInfo(interface) {
|
||||
if let ssid = (networkInfo as NSDictionary)[kCNNetworkInfoKeySSID as String] as? String {
|
||||
return !ssid.isEmpty ? ssid : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -86,22 +86,25 @@ class SettingsTableViewController: UITableViewController {
|
||||
}
|
||||
|
||||
func exportConfigurationsAsZipFile(sourceView: UIView) {
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
||||
PrivateDataConfirmation.confirmAccess(to: tr("iosExportPrivateData")) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let tunnelsManager = self.tunnelsManager else { return }
|
||||
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
||||
|
||||
let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip")
|
||||
_ = FileManager.deleteFile(at: destinationURL)
|
||||
let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip")
|
||||
_ = FileManager.deleteFile(at: destinationURL)
|
||||
|
||||
let count = tunnelsManager.numberOfTunnels()
|
||||
let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration }
|
||||
ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] error in
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
return
|
||||
let count = tunnelsManager.numberOfTunnels()
|
||||
let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration }
|
||||
ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] error in
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
return
|
||||
}
|
||||
|
||||
let fileExportVC = UIDocumentPickerViewController(url: destinationURL, in: .exportToService)
|
||||
self?.present(fileExportVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
let fileExportVC = UIDocumentPickerViewController(url: destinationURL, in: .exportToService)
|
||||
self?.present(fileExportVC, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,18 +126,13 @@ class SettingsTableViewController: UITableViewController {
|
||||
}
|
||||
}
|
||||
|
||||
guard let networkExtensionLogFilePath = FileManager.networkExtensionLogFileURL?.path else {
|
||||
ErrorPresenter.showErrorAlert(title: tr("alertUnableToFindExtensionLogPathTitle"), message: tr("alertUnableToFindExtensionLogPathMessage"), from: self)
|
||||
return
|
||||
}
|
||||
|
||||
let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false
|
||||
guard isWritten else {
|
||||
ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self)
|
||||
return
|
||||
}
|
||||
let isWritten = Logger.global?.writeLog(to: destinationURL.path) ?? false
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard isWritten else {
|
||||
ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self)
|
||||
return
|
||||
}
|
||||
let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil)
|
||||
activityVC.popoverPresentationController?.sourceView = sourceView
|
||||
activityVC.popoverPresentationController?.sourceRect = sourceView.bounds
|
||||
|
@ -8,34 +8,61 @@ class TunnelDetailTableViewController: UITableViewController {
|
||||
private enum Section {
|
||||
case status
|
||||
case interface
|
||||
case peer(_ peer: TunnelViewModel.PeerData)
|
||||
case peer(index: Int, peer: TunnelViewModel.PeerData)
|
||||
case onDemand
|
||||
case delete
|
||||
}
|
||||
|
||||
let interfaceFields: [TunnelViewModel.InterfaceField] = [
|
||||
static let interfaceFields: [TunnelViewModel.InterfaceField] = [
|
||||
.name, .publicKey, .addresses,
|
||||
.listenPort, .mtu, .dns
|
||||
]
|
||||
|
||||
let peerFields: [TunnelViewModel.PeerField] = [
|
||||
static let peerFields: [TunnelViewModel.PeerField] = [
|
||||
.publicKey, .preSharedKey, .endpoint,
|
||||
.allowedIPs, .persistentKeepAlive
|
||||
.allowedIPs, .persistentKeepAlive,
|
||||
.rxBytes, .txBytes, .lastHandshakeTime
|
||||
]
|
||||
|
||||
static let onDemandFields: [ActivateOnDemandViewModel.OnDemandField] = [
|
||||
.onDemand, .ssid
|
||||
]
|
||||
|
||||
let tunnelsManager: TunnelsManager
|
||||
let tunnel: TunnelContainer
|
||||
var tunnelViewModel: TunnelViewModel
|
||||
var onDemandViewModel: ActivateOnDemandViewModel
|
||||
|
||||
private var sections = [Section]()
|
||||
private var onDemandStatusObservationToken: AnyObject?
|
||||
private var interfaceFieldIsVisible = [Bool]()
|
||||
private var peerFieldIsVisible = [[Bool]]()
|
||||
|
||||
private var statusObservationToken: AnyObject?
|
||||
private var onDemandObservationToken: AnyObject?
|
||||
private var reloadRuntimeConfigurationTimer: Timer?
|
||||
|
||||
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
|
||||
self.tunnelsManager = tunnelsManager
|
||||
self.tunnel = tunnel
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
|
||||
onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
|
||||
super.init(style: .grouped)
|
||||
loadSections()
|
||||
loadVisibleFields()
|
||||
statusObservationToken = tunnel.observe(\.status) { [weak self] _, _ in
|
||||
guard let self = self else { return }
|
||||
if tunnel.status == .active {
|
||||
self.startUpdatingRuntimeConfiguration()
|
||||
} else if tunnel.status == .inactive {
|
||||
self.reloadRuntimeConfiguration()
|
||||
self.stopUpdatingRuntimeConfiguration()
|
||||
}
|
||||
}
|
||||
onDemandObservationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak self] tunnel, _ in
|
||||
// Handle On-Demand getting turned on/off outside of the app
|
||||
self?.onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
|
||||
self?.updateActivateOnDemandFields()
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
@ -49,10 +76,10 @@ class TunnelDetailTableViewController: UITableViewController {
|
||||
|
||||
tableView.estimatedRowHeight = 44
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.allowsSelection = false
|
||||
tableView.register(SwitchCell.self)
|
||||
tableView.register(KeyValueCell.self)
|
||||
tableView.register(ButtonCell.self)
|
||||
tableView.register(ChevronCell.self)
|
||||
|
||||
restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
|
||||
}
|
||||
@ -61,39 +88,180 @@ class TunnelDetailTableViewController: UITableViewController {
|
||||
sections.removeAll()
|
||||
sections.append(.status)
|
||||
sections.append(.interface)
|
||||
tunnelViewModel.peersData.forEach { sections.append(.peer($0)) }
|
||||
for (index, peer) in tunnelViewModel.peersData.enumerated() {
|
||||
sections.append(.peer(index: index, peer: peer))
|
||||
}
|
||||
sections.append(.onDemand)
|
||||
sections.append(.delete)
|
||||
}
|
||||
|
||||
@objc func editTapped() {
|
||||
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
|
||||
editVC.delegate = self
|
||||
let editNC = UINavigationController(rootViewController: editVC)
|
||||
editNC.modalPresentationStyle = .formSheet
|
||||
present(editNC, animated: true)
|
||||
private func loadVisibleFields() {
|
||||
let visibleInterfaceFields = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: TunnelDetailTableViewController.interfaceFields)
|
||||
interfaceFieldIsVisible = TunnelDetailTableViewController.interfaceFields.map { visibleInterfaceFields.contains($0) }
|
||||
peerFieldIsVisible = tunnelViewModel.peersData.map { peer in
|
||||
let visiblePeerFields = peer.filterFieldsWithValueOrControl(peerFields: TunnelDetailTableViewController.peerFields)
|
||||
return TunnelDetailTableViewController.peerFields.map { visiblePeerFields.contains($0) }
|
||||
}
|
||||
}
|
||||
|
||||
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, onConfirmed: @escaping (() -> Void)) {
|
||||
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
|
||||
onConfirmed()
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
if tunnel.status == .active {
|
||||
self.startUpdatingRuntimeConfiguration()
|
||||
}
|
||||
let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
|
||||
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
|
||||
alert.addAction(destroyAction)
|
||||
alert.addAction(cancelAction)
|
||||
}
|
||||
|
||||
alert.popoverPresentationController?.sourceView = sourceView
|
||||
alert.popoverPresentationController?.sourceRect = sourceView.bounds
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
stopUpdatingRuntimeConfiguration()
|
||||
}
|
||||
|
||||
present(alert, animated: true, completion: nil)
|
||||
@objc func editTapped() {
|
||||
PrivateDataConfirmation.confirmAccess(to: tr("iosViewPrivateData")) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let editVC = TunnelEditTableViewController(tunnelsManager: self.tunnelsManager, tunnel: self.tunnel)
|
||||
editVC.delegate = self
|
||||
let editNC = UINavigationController(rootViewController: editVC)
|
||||
editNC.modalPresentationStyle = .formSheet
|
||||
self.present(editNC, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func startUpdatingRuntimeConfiguration() {
|
||||
reloadRuntimeConfiguration()
|
||||
reloadRuntimeConfigurationTimer?.invalidate()
|
||||
let reloadTimer = Timer(timeInterval: 1 /* second */, repeats: true) { [weak self] _ in
|
||||
self?.reloadRuntimeConfiguration()
|
||||
}
|
||||
reloadRuntimeConfigurationTimer = reloadTimer
|
||||
RunLoop.main.add(reloadTimer, forMode: .common)
|
||||
}
|
||||
|
||||
func stopUpdatingRuntimeConfiguration() {
|
||||
reloadRuntimeConfigurationTimer?.invalidate()
|
||||
reloadRuntimeConfigurationTimer = nil
|
||||
}
|
||||
|
||||
func applyTunnelConfiguration(tunnelConfiguration: TunnelConfiguration) {
|
||||
// Incorporates changes from tunnelConfiguation. Ignores any changes in peer ordering.
|
||||
guard let tableView = self.tableView else { return }
|
||||
let sections = self.sections
|
||||
let interfaceSectionIndex = sections.firstIndex {
|
||||
if case .interface = $0 {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}!
|
||||
let firstPeerSectionIndex = interfaceSectionIndex + 1
|
||||
var interfaceFieldIsVisible = self.interfaceFieldIsVisible
|
||||
var peerFieldIsVisible = self.peerFieldIsVisible
|
||||
|
||||
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 removedIndexPaths = [IndexPath]()
|
||||
for (index, field) in fields.enumerated().reversed() where changes[field] == .removed {
|
||||
let row = fieldIsVisible[0 ..< index].filter { $0 }.count
|
||||
removedIndexPaths.append(IndexPath(row: row, section: section))
|
||||
fieldIsVisible[index] = false
|
||||
}
|
||||
if !removedIndexPaths.isEmpty {
|
||||
tableView.deleteRows(at: removedIndexPaths, with: .automatic)
|
||||
}
|
||||
|
||||
var addedIndexPaths = [IndexPath]()
|
||||
for (index, field) in fields.enumerated() where changes[field] == .added {
|
||||
let row = fieldIsVisible[0 ..< index].filter { $0 }.count
|
||||
addedIndexPaths.append(IndexPath(row: row, section: section))
|
||||
fieldIsVisible[index] = true
|
||||
}
|
||||
if !addedIndexPaths.isEmpty {
|
||||
tableView.insertRows(at: addedIndexPaths, with: .automatic)
|
||||
}
|
||||
}
|
||||
|
||||
let changes = self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration)
|
||||
|
||||
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() {
|
||||
tunnel.getRuntimeTunnelConfiguration { [weak self] tunnelConfiguration in
|
||||
guard let tunnelConfiguration = tunnelConfiguration else { return }
|
||||
guard let self = self else { return }
|
||||
self.applyTunnelConfiguration(tunnelConfiguration: tunnelConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateActivateOnDemandFields() {
|
||||
guard let onDemandSection = sections.firstIndex(where: { if case .onDemand = $0 { return true } else { return false } }) else { return }
|
||||
let numberOfTableViewOnDemandRows = tableView.numberOfRows(inSection: onDemandSection)
|
||||
let ssidRowIndexPath = IndexPath(row: 1, section: onDemandSection)
|
||||
switch (numberOfTableViewOnDemandRows, onDemandViewModel.isWiFiInterfaceEnabled) {
|
||||
case (1, true):
|
||||
tableView.insertRows(at: [ssidRowIndexPath], with: .automatic)
|
||||
case (2, false):
|
||||
tableView.deleteRows(at: [ssidRowIndexPath], with: .automatic)
|
||||
default:
|
||||
break
|
||||
}
|
||||
tableView.reloadSections(IndexSet(integer: onDemandSection), with: .automatic)
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate {
|
||||
func tunnelSaved(tunnel: TunnelContainer) {
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
|
||||
onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
|
||||
loadSections()
|
||||
loadVisibleFields()
|
||||
title = tunnel.name
|
||||
restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
|
||||
tableView.reloadData()
|
||||
@ -113,11 +281,11 @@ extension TunnelDetailTableViewController {
|
||||
case .status:
|
||||
return 1
|
||||
case .interface:
|
||||
return tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count
|
||||
case .peer(let peerData):
|
||||
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
|
||||
return interfaceFieldIsVisible.filter { $0 }.count
|
||||
case .peer(let peerIndex, _):
|
||||
return peerFieldIsVisible[peerIndex].filter { $0 }.count
|
||||
case .onDemand:
|
||||
return 1
|
||||
return onDemandViewModel.isWiFiInterfaceEnabled ? 2 : 1
|
||||
case .delete:
|
||||
return 1
|
||||
}
|
||||
@ -144,8 +312,8 @@ extension TunnelDetailTableViewController {
|
||||
return statusCell(for: tableView, at: indexPath)
|
||||
case .interface:
|
||||
return interfaceCell(for: tableView, at: indexPath)
|
||||
case .peer(let peer):
|
||||
return peerCell(for: tableView, at: indexPath, with: peer)
|
||||
case .peer(let index, let peer):
|
||||
return peerCell(for: tableView, at: indexPath, with: peer, peerIndex: index)
|
||||
case .onDemand:
|
||||
return onDemandCell(for: tableView, at: indexPath)
|
||||
case .delete:
|
||||
@ -183,7 +351,7 @@ extension TunnelDetailTableViewController {
|
||||
}
|
||||
|
||||
statusUpdate(cell, tunnel.status)
|
||||
statusObservationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
|
||||
cell.observationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
|
||||
guard let cell = cell else { return }
|
||||
statusUpdate(cell, tunnel.status)
|
||||
}
|
||||
@ -200,29 +368,50 @@ extension TunnelDetailTableViewController {
|
||||
}
|
||||
|
||||
private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let field = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[indexPath.row]
|
||||
let visibleInterfaceFields = TunnelDetailTableViewController.interfaceFields.enumerated().filter { interfaceFieldIsVisible[$0.offset] }.map { $0.element }
|
||||
let field = visibleInterfaceFields[indexPath.row]
|
||||
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = field.localizedUIString
|
||||
cell.value = tunnelViewModel.interfaceData[field]
|
||||
return cell
|
||||
}
|
||||
|
||||
private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell {
|
||||
let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[indexPath.row]
|
||||
private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData, peerIndex: Int) -> UITableViewCell {
|
||||
let visiblePeerFields = TunnelDetailTableViewController.peerFields.enumerated().filter { peerFieldIsVisible[peerIndex][$0.offset] }.map { $0.element }
|
||||
let field = visiblePeerFields[indexPath.row]
|
||||
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = field.localizedUIString
|
||||
cell.value = peerData[field]
|
||||
if field == .persistentKeepAlive {
|
||||
cell.value = tr(format: "tunnelPeerPersistentKeepaliveValue (%@)", peerData[field])
|
||||
} else if field == .preSharedKey {
|
||||
cell.value = tr("tunnelPeerPresharedKeyEnabled")
|
||||
} else {
|
||||
cell.value = peerData[field]
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = tr("tunnelOnDemandKey")
|
||||
cell.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting)
|
||||
onDemandStatusObservationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak cell] tunnel, _ in
|
||||
cell?.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting)
|
||||
let field = TunnelDetailTableViewController.onDemandFields[indexPath.row]
|
||||
if field == .onDemand {
|
||||
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = field.localizedUIString
|
||||
cell.value = onDemandViewModel.localizedInterfaceDescription
|
||||
return cell
|
||||
} else {
|
||||
assert(field == .ssid)
|
||||
if onDemandViewModel.ssidOption == .anySSID {
|
||||
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = field.localizedUIString
|
||||
cell.value = onDemandViewModel.ssidOption.localizedUIString
|
||||
return cell
|
||||
} else {
|
||||
let cell: ChevronCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = field.localizedUIString
|
||||
cell.detailMessage = onDemandViewModel.localizedSSIDDescription
|
||||
return cell
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
@ -231,7 +420,9 @@ extension TunnelDetailTableViewController {
|
||||
cell.hasDestructiveAction = true
|
||||
cell.onTapped = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.showConfirmationAlert(message: tr("deleteTunnelConfirmationAlertMessage"), buttonTitle: tr("deleteTunnelConfirmationAlertButtonTitle"), from: cell) { [weak self] in
|
||||
ConfirmationAlertPresenter.showConfirmationAlert(message: tr("deleteTunnelConfirmationAlertMessage"),
|
||||
buttonTitle: tr("deleteTunnelConfirmationAlertButtonTitle"),
|
||||
from: cell, presentingVC: self) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.tunnelsManager.remove(tunnel: self.tunnel) { error in
|
||||
if error != nil {
|
||||
@ -239,17 +430,28 @@ extension TunnelDetailTableViewController {
|
||||
return
|
||||
}
|
||||
}
|
||||
if self.splitViewController?.isCollapsed != false {
|
||||
self.navigationController?.navigationController?.popToRootViewController(animated: true)
|
||||
} else {
|
||||
let detailVC = UIViewController()
|
||||
detailVC.view.backgroundColor = .white
|
||||
let detailNC = UINavigationController(rootViewController: detailVC)
|
||||
self.showDetailViewController(detailNC, sender: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TunnelDetailTableViewController {
|
||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
if case .onDemand = sections[indexPath.section],
|
||||
case .ssid = TunnelDetailTableViewController.onDemandFields[indexPath.row] {
|
||||
return indexPath
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if case .onDemand = sections[indexPath.section],
|
||||
case .ssid = TunnelDetailTableViewController.onDemandFields[indexPath.row] {
|
||||
let ssidDetailVC = SSIDOptionDetailTableViewController(title: onDemandViewModel.ssidOption.localizedUIString, ssids: onDemandViewModel.selectedSSIDs)
|
||||
navigationController?.pushViewController(ssidDetailVC, animated: true)
|
||||
}
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
}
|
||||
|
@ -43,16 +43,16 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
.deletePeer
|
||||
]
|
||||
|
||||
let activateOnDemandOptions: [ActivateOnDemandOption] = [
|
||||
.useOnDemandOverWiFiOrCellular,
|
||||
.useOnDemandOverWiFiOnly,
|
||||
.useOnDemandOverCellularOnly
|
||||
let onDemandFields: [ActivateOnDemandViewModel.OnDemandField] = [
|
||||
.nonWiFiInterface,
|
||||
.wiFiInterface,
|
||||
.ssid
|
||||
]
|
||||
|
||||
let tunnelsManager: TunnelsManager
|
||||
let tunnel: TunnelContainer?
|
||||
let tunnelViewModel: TunnelViewModel
|
||||
var activateOnDemandSetting: ActivateOnDemandSetting
|
||||
var onDemandViewModel: ActivateOnDemandViewModel
|
||||
private var sections = [Section]()
|
||||
|
||||
// Use this initializer to edit an existing tunnel.
|
||||
@ -60,7 +60,7 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
self.tunnelsManager = tunnelsManager
|
||||
self.tunnel = tunnel
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
|
||||
activateOnDemandSetting = tunnel.activateOnDemandSetting
|
||||
onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
|
||||
super.init(style: .grouped)
|
||||
loadSections()
|
||||
}
|
||||
@ -70,7 +70,7 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
self.tunnelsManager = tunnelsManager
|
||||
tunnel = nil
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: nil)
|
||||
activateOnDemandSetting = ActivateOnDemandSetting.defaultSetting
|
||||
onDemandViewModel = ActivateOnDemandViewModel()
|
||||
super.init(style: .grouped)
|
||||
loadSections()
|
||||
}
|
||||
@ -92,7 +92,7 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
tableView.register(TunnelEditEditableKeyValueCell.self)
|
||||
tableView.register(ButtonCell.self)
|
||||
tableView.register(SwitchCell.self)
|
||||
tableView.register(CheckmarkCell.self)
|
||||
tableView.register(ChevronCell.self)
|
||||
}
|
||||
|
||||
private func loadSections() {
|
||||
@ -113,9 +113,10 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
ErrorPresenter.showErrorAlert(title: alertTitle, message: errorMessage, from: self)
|
||||
tableView.reloadData() // Highlight erroring fields
|
||||
case .saved(let tunnelConfiguration):
|
||||
let onDemandOption = onDemandViewModel.toOnDemandOption()
|
||||
if let tunnel = tunnel {
|
||||
// We're modifying an existing tunnel
|
||||
tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: activateOnDemandSetting) { [weak self] error in
|
||||
tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, onDemandOption: onDemandOption) { [weak self] error in
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
} else {
|
||||
@ -125,7 +126,7 @@ class TunnelEditTableViewController: UITableViewController {
|
||||
}
|
||||
} else {
|
||||
// We're adding a new tunnel
|
||||
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: activateOnDemandSetting) { [weak self] result in
|
||||
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, onDemandOption: onDemandOption) { [weak self] result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
} else {
|
||||
@ -161,10 +162,10 @@ extension TunnelEditTableViewController {
|
||||
case .addPeer:
|
||||
return 1
|
||||
case .onDemand:
|
||||
if activateOnDemandSetting.isActivateOnDemandEnabled {
|
||||
return 4
|
||||
if onDemandViewModel.isWiFiInterfaceEnabled {
|
||||
return 3
|
||||
} else {
|
||||
return 1
|
||||
return 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -213,7 +214,7 @@ extension TunnelEditTableViewController {
|
||||
cell.onTapped = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.tunnelViewModel.interfaceData[.privateKey] = Curve25519.generatePrivateKey().base64EncodedString()
|
||||
self.tunnelViewModel.interfaceData[.privateKey] = Curve25519.generatePrivateKey().base64Key() ?? ""
|
||||
if let privateKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .privateKey),
|
||||
let publicKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) {
|
||||
let privateKeyIndex = IndexPath(row: privateKeyRow, section: indexPath.section)
|
||||
@ -243,13 +244,15 @@ extension TunnelEditTableViewController {
|
||||
cell.placeholderText = tr("tunnelEditPlaceholderTextStronglyRecommended")
|
||||
cell.keyboardType = .numbersAndPunctuation
|
||||
case .dns:
|
||||
cell.placeholderText = tunnelViewModel.peersData.contains(where: { return $0.shouldStronglyRecommendDNS }) ? tr("tunnelEditPlaceholderTextStronglyRecommended") : tr("tunnelEditPlaceholderTextOptional")
|
||||
cell.placeholderText = tunnelViewModel.peersData.contains(where: { $0.shouldStronglyRecommendDNS }) ? tr("tunnelEditPlaceholderTextStronglyRecommended") : tr("tunnelEditPlaceholderTextOptional")
|
||||
cell.keyboardType = .numbersAndPunctuation
|
||||
case .listenPort, .mtu:
|
||||
cell.placeholderText = tr("tunnelEditPlaceholderTextAutomatic")
|
||||
cell.keyboardType = .numberPad
|
||||
case .publicKey, .generateKeyPair:
|
||||
cell.keyboardType = .default
|
||||
case .status, .toggleStatus:
|
||||
fatalError("Unexpected interface field")
|
||||
}
|
||||
|
||||
cell.isValueValid = (!tunnelViewModel.interfaceData.fieldsWithError.contains(field))
|
||||
@ -259,8 +262,18 @@ extension TunnelEditTableViewController {
|
||||
cell.onValueBeingEdited = { [weak self] value in
|
||||
self?.tunnelViewModel.interfaceData[field] = value
|
||||
}
|
||||
cell.onValueChanged = { [weak self] oldValue, newValue in
|
||||
guard let self = self else { return }
|
||||
let isAllowedIPsChanged = self.tunnelViewModel.updateDNSServersInAllowedIPsIfRequired(oldDNSServers: oldValue, newDNSServers: newValue)
|
||||
if isAllowedIPsChanged {
|
||||
let section = self.sections.firstIndex { if case .peer(_) = $0 { return true } else { return false } }
|
||||
if let section = section, let row = self.peerFields.firstIndex(of: .allowedIPs) {
|
||||
self.tableView.reloadRows(at: [IndexPath(row: row, section: section)], with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cell.onValueChanged = { [weak self] value in
|
||||
cell.onValueChanged = { [weak self] _, value in
|
||||
self?.tunnelViewModel.interfaceData[field] = value
|
||||
}
|
||||
}
|
||||
@ -298,10 +311,14 @@ extension TunnelEditTableViewController {
|
||||
cell.hasDestructiveAction = true
|
||||
cell.onTapped = { [weak self, weak peerData] in
|
||||
guard let self = self, let peerData = peerData else { return }
|
||||
self.showConfirmationAlert(message: tr("deletePeerConfirmationAlertMessage"), buttonTitle: tr("deletePeerConfirmationAlertButtonTitle"), from: cell) { [weak self] in
|
||||
ConfirmationAlertPresenter.showConfirmationAlert(message: tr("deletePeerConfirmationAlertMessage"),
|
||||
buttonTitle: tr("deletePeerConfirmationAlertButtonTitle"),
|
||||
from: cell, presentingVC: self) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let removedSectionIndices = self.deletePeer(peer: peerData)
|
||||
let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
|
||||
|
||||
//swiftlint:disable:next trailing_closure
|
||||
tableView.performBatchUpdates({
|
||||
self.tableView.deleteSections(removedSectionIndices, with: .fade)
|
||||
if shouldShowExcludePrivateIPs {
|
||||
@ -309,7 +326,6 @@ extension TunnelEditTableViewController {
|
||||
let rowIndexPath = IndexPath(row: row, section: self.interfaceFieldsBySection.count /* First peer section */)
|
||||
self.tableView.insertRows(at: [rowIndexPath], with: .fade)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -351,15 +367,17 @@ extension TunnelEditTableViewController {
|
||||
cell.keyboardType = .numberPad
|
||||
case .excludePrivateIPs, .deletePeer:
|
||||
cell.keyboardType = .default
|
||||
case .rxBytes, .txBytes, .lastHandshakeTime:
|
||||
fatalError()
|
||||
}
|
||||
|
||||
cell.isValueValid = !peerData.fieldsWithError.contains(field)
|
||||
cell.value = peerData[field]
|
||||
|
||||
if field == .allowedIPs {
|
||||
let firstInterfaceSection = sections.firstIndex(where: { $0 == .interface })!
|
||||
let interfaceSubSection = interfaceFieldsBySection.firstIndex(where: { $0.contains(.dns) })!
|
||||
let dnsRow = interfaceFieldsBySection[interfaceSubSection].firstIndex(where: { $0 == .dns })!
|
||||
let firstInterfaceSection = sections.firstIndex { $0 == .interface }!
|
||||
let interfaceSubSection = interfaceFieldsBySection.firstIndex { $0.contains(.dns) }!
|
||||
let dnsRow = interfaceFieldsBySection[interfaceSubSection].firstIndex { $0 == .dns }!
|
||||
|
||||
cell.onValueBeingEdited = { [weak self, weak peerData] value in
|
||||
guard let self = self, let peerData = peerData else { return }
|
||||
@ -377,7 +395,7 @@ extension TunnelEditTableViewController {
|
||||
tableView.reloadRows(at: [IndexPath(row: dnsRow, section: firstInterfaceSection + interfaceSubSection)], with: .none)
|
||||
}
|
||||
} else {
|
||||
cell.onValueChanged = { [weak peerData] value in
|
||||
cell.onValueChanged = { [weak peerData] _, value in
|
||||
peerData?[field] = value
|
||||
}
|
||||
}
|
||||
@ -406,36 +424,29 @@ extension TunnelEditTableViewController {
|
||||
}
|
||||
|
||||
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
if indexPath.row == 0 {
|
||||
let field = onDemandFields[indexPath.row]
|
||||
if indexPath.row < 2 {
|
||||
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = tr("tunnelOnDemandKey")
|
||||
cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled
|
||||
cell.message = field.localizedUIString
|
||||
cell.isOn = onDemandViewModel.isEnabled(field: field)
|
||||
cell.onSwitchToggled = { [weak self] isOn in
|
||||
guard let self = self else { return }
|
||||
guard isOn != self.activateOnDemandSetting.isActivateOnDemandEnabled else { return }
|
||||
|
||||
self.activateOnDemandSetting.isActivateOnDemandEnabled = isOn
|
||||
self.loadSections()
|
||||
|
||||
let section = self.sections.firstIndex(where: { $0 == .onDemand })!
|
||||
let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: section) }
|
||||
if isOn {
|
||||
if self.activateOnDemandSetting.activateOnDemandOption == .none {
|
||||
self.activateOnDemandSetting.activateOnDemandOption = TunnelViewModel.defaultActivateOnDemandOption()
|
||||
self.onDemandViewModel.setEnabled(field: field, isEnabled: isOn)
|
||||
let section = self.sections.firstIndex { $0 == .onDemand }!
|
||||
let indexPath = IndexPath(row: 2, section: section)
|
||||
if field == .wiFiInterface {
|
||||
if isOn {
|
||||
tableView.insertRows(at: [indexPath], with: .fade)
|
||||
} else {
|
||||
tableView.deleteRows(at: [indexPath], with: .fade)
|
||||
}
|
||||
self.tableView.insertRows(at: indexPaths, with: .fade)
|
||||
} else {
|
||||
self.tableView.deleteRows(at: indexPaths, with: .fade)
|
||||
}
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
let cell: CheckmarkCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
let rowOption = activateOnDemandOptions[indexPath.row - 1]
|
||||
let selectedOption = activateOnDemandSetting.activateOnDemandOption
|
||||
assert(selectedOption != .none)
|
||||
cell.message = TunnelViewModel.activateOnDemandOptionText(for: rowOption)
|
||||
cell.isChecked = selectedOption == rowOption
|
||||
let cell: ChevronCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = field.localizedUIString
|
||||
cell.detailMessage = onDemandViewModel.localizedSSIDDescription
|
||||
return cell
|
||||
}
|
||||
}
|
||||
@ -452,26 +463,11 @@ extension TunnelEditTableViewController {
|
||||
loadSections()
|
||||
return IndexSet(integer: interfaceFieldsBySection.count + peer.index)
|
||||
}
|
||||
|
||||
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, onConfirmed: @escaping (() -> Void)) {
|
||||
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
|
||||
onConfirmed()
|
||||
}
|
||||
let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
|
||||
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
|
||||
alert.addAction(destroyAction)
|
||||
alert.addAction(cancelAction)
|
||||
|
||||
alert.popoverPresentationController?.sourceView = sourceView
|
||||
alert.popoverPresentationController?.sourceRect = sourceView.bounds
|
||||
|
||||
present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelEditTableViewController {
|
||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
if case .onDemand = sections[indexPath.section], indexPath.row > 0 {
|
||||
if case .onDemand = sections[indexPath.section], indexPath.row == 2 {
|
||||
return indexPath
|
||||
} else {
|
||||
return nil
|
||||
@ -481,16 +477,27 @@ extension TunnelEditTableViewController {
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
switch sections[indexPath.section] {
|
||||
case .onDemand:
|
||||
let option = activateOnDemandOptions[indexPath.row - 1]
|
||||
assert(option != .none)
|
||||
activateOnDemandSetting.activateOnDemandOption = option
|
||||
|
||||
let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: indexPath.section) }
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.reloadRows(at: indexPaths, with: .none)
|
||||
}
|
||||
assert(indexPath.row == 2)
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
let ssidOptionVC = SSIDOptionEditTableViewController(option: onDemandViewModel.ssidOption, ssids: onDemandViewModel.selectedSSIDs)
|
||||
ssidOptionVC.delegate = self
|
||||
navigationController?.pushViewController(ssidOptionVC, animated: true)
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelEditTableViewController: SSIDOptionEditTableViewControllerDelegate {
|
||||
func ssidOptionSaved(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String]) {
|
||||
onDemandViewModel.selectedSSIDs = ssids
|
||||
onDemandViewModel.ssidOption = option
|
||||
onDemandViewModel.fixSSIDOption()
|
||||
if let onDemandSection = sections.firstIndex(where: { $0 == .onDemand }) {
|
||||
if let ssidRowIndex = onDemandFields.firstIndex(of: .ssid) {
|
||||
let indexPath = IndexPath(row: ssidRowIndex, section: onDemandSection)
|
||||
tableView.reloadRows(at: [indexPath], with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,12 @@ class TunnelsListTableViewController: UIViewController {
|
||||
|
||||
var tunnelsManager: TunnelsManager?
|
||||
|
||||
enum TableState: Equatable {
|
||||
case normal
|
||||
case rowSwiped
|
||||
case multiSelect(selectionCount: Int)
|
||||
}
|
||||
|
||||
let tableView: UITableView = {
|
||||
let tableView = UITableView(frame: CGRect.zero, style: .plain)
|
||||
tableView.estimatedRowHeight = 60
|
||||
@ -31,6 +37,13 @@ class TunnelsListTableViewController: UIViewController {
|
||||
return busyIndicator
|
||||
}()
|
||||
|
||||
var detailDisplayedTunnel: TunnelContainer?
|
||||
var tableState: TableState = .normal {
|
||||
didSet {
|
||||
handleTableStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
view = UIView()
|
||||
view.backgroundColor = .white
|
||||
@ -72,13 +85,39 @@ class TunnelsListTableViewController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = tr("tunnelsListTitle")
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSettingsButtonTitle"), style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
|
||||
|
||||
tableState = .normal
|
||||
restorationIdentifier = "TunnelsListVC"
|
||||
}
|
||||
|
||||
func handleTableStateChange() {
|
||||
switch tableState {
|
||||
case .normal:
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSettingsButtonTitle"), style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
|
||||
case .rowSwiped:
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonTapped))
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSelectButtonTitle"), style: .plain, target: self, action: #selector(selectButtonTapped))
|
||||
case .multiSelect(let selectionCount):
|
||||
if selectionCount > 0 {
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonTapped))
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListDeleteButtonTitle"), style: .plain, target: self, action: #selector(deleteButtonTapped(sender:)))
|
||||
} else {
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonTapped))
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSelectAllButtonTitle"), style: .plain, target: self, action: #selector(selectAllButtonTapped))
|
||||
}
|
||||
}
|
||||
if case .multiSelect(let selectionCount) = tableState, selectionCount > 0 {
|
||||
navigationItem.title = tr(format: "tunnelsListSelectedTitle (%d)", selectionCount)
|
||||
} else {
|
||||
navigationItem.title = tr("tunnelsListTitle")
|
||||
}
|
||||
if case .multiSelect = tableState {
|
||||
tableView.allowsMultipleSelectionDuringEditing = true
|
||||
} else {
|
||||
tableView.allowsMultipleSelectionDuringEditing = false
|
||||
}
|
||||
}
|
||||
|
||||
func setTunnelsManager(tunnelsManager: TunnelsManager) {
|
||||
self.tunnelsManager = tunnelsManager
|
||||
tunnelsManager.tunnelsListDelegate = self
|
||||
@ -158,39 +197,57 @@ class TunnelsListTableViewController: UIViewController {
|
||||
present(scanQRCodeNC, animated: true)
|
||||
}
|
||||
|
||||
func importFromFile(url: URL, completionHandler: (() -> Void)?) {
|
||||
@objc func selectButtonTapped() {
|
||||
let shouldCancelSwipe = tableState == .rowSwiped
|
||||
tableState = .multiSelect(selectionCount: 0)
|
||||
if shouldCancelSwipe {
|
||||
tableView.setEditing(false, animated: false)
|
||||
}
|
||||
tableView.setEditing(true, animated: true)
|
||||
}
|
||||
|
||||
@objc func doneButtonTapped() {
|
||||
tableState = .normal
|
||||
tableView.setEditing(false, animated: true)
|
||||
}
|
||||
|
||||
@objc func selectAllButtonTapped() {
|
||||
guard tableView.isEditing else { return }
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
if url.pathExtension == "zip" {
|
||||
ZipImporter.importConfigFiles(from: url) { [weak self] result in
|
||||
if let error = result.error {
|
||||
for index in 0 ..< tunnelsManager.numberOfTunnels() {
|
||||
tableView.selectRow(at: IndexPath(row: index, section: 0), animated: false, scrollPosition: .none)
|
||||
}
|
||||
tableState = .multiSelect(selectionCount: tableView.indexPathsForSelectedRows?.count ?? 0)
|
||||
}
|
||||
|
||||
@objc func cancelButtonTapped() {
|
||||
tableState = .normal
|
||||
tableView.setEditing(false, animated: true)
|
||||
}
|
||||
|
||||
@objc func deleteButtonTapped(sender: AnyObject?) {
|
||||
guard let sender = sender as? UIBarButtonItem else { return }
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
|
||||
let selectedTunnelIndices = tableView.indexPathsForSelectedRows?.map { $0.row } ?? []
|
||||
let selectedTunnels = selectedTunnelIndices.compactMap { tunnelIndex in
|
||||
tunnelIndex >= 0 && tunnelIndex < tunnelsManager.numberOfTunnels() ? tunnelsManager.tunnel(at: tunnelIndex) : nil
|
||||
}
|
||||
guard !selectedTunnels.isEmpty else { return }
|
||||
let message = selectedTunnels.count == 1 ?
|
||||
tr(format: "deleteTunnelConfirmationAlertButtonMessage (%d)", selectedTunnels.count) :
|
||||
tr(format: "deleteTunnelsConfirmationAlertButtonMessage (%d)", selectedTunnels.count)
|
||||
let title = tr("deleteTunnelsConfirmationAlertButtonTitle")
|
||||
ConfirmationAlertPresenter.showConfirmationAlert(message: message, buttonTitle: title,
|
||||
from: sender, presentingVC: self) { [weak self] in
|
||||
self?.tunnelsManager?.removeMultiple(tunnels: selectedTunnels) { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
if let error = error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self)
|
||||
return
|
||||
}
|
||||
let configs = result.value!
|
||||
tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { [weak self] numberSuccessful in
|
||||
if numberSuccessful == configs.count {
|
||||
completionHandler?()
|
||||
return
|
||||
}
|
||||
let title = tr(format: "alertImportedFromZipTitle (%d)", numberSuccessful)
|
||||
let message = tr(format: "alertImportedFromZipMessage (%1$d of %2$d)", numberSuccessful, configs.count)
|
||||
ErrorPresenter.showErrorAlert(title: title, message: message, from: self, onPresented: completionHandler)
|
||||
}
|
||||
}
|
||||
} else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ {
|
||||
let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let fileContents = try? String(contentsOf: url),
|
||||
let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: fileContents, called: fileBaseName) {
|
||||
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] result in
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: self, onPresented: completionHandler)
|
||||
} else {
|
||||
completionHandler?()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ErrorPresenter.showErrorAlert(title: tr("alertUnableToImportTitle"), message: tr("alertUnableToImportMessage"),
|
||||
from: self, onPresented: completionHandler)
|
||||
self.tableState = .normal
|
||||
self.tableView.setEditing(false, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -198,9 +255,8 @@ class TunnelsListTableViewController: UIViewController {
|
||||
|
||||
extension TunnelsListTableViewController: UIDocumentPickerDelegate {
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
urls.forEach {
|
||||
importFromFile(url: $0, completionHandler: nil)
|
||||
}
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
TunnelImporter.importFromFile(urls: urls, into: tunnelsManager, sourceVC: self, errorPresenterType: ErrorPresenter.self)
|
||||
}
|
||||
}
|
||||
|
||||
@ -246,6 +302,10 @@ extension TunnelsListTableViewController: UITableViewDataSource {
|
||||
|
||||
extension TunnelsListTableViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard !tableView.isEditing else {
|
||||
tableState = .multiSelect(selectionCount: tableView.indexPathsForSelectedRows?.count ?? 0)
|
||||
return
|
||||
}
|
||||
guard let tunnelsManager = tunnelsManager else { return }
|
||||
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
|
||||
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager,
|
||||
@ -253,6 +313,14 @@ extension TunnelsListTableViewController: UITableViewDelegate {
|
||||
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
|
||||
tunnelDetailNC.restorationIdentifier = "DetailNC"
|
||||
showDetailViewController(tunnelDetailNC, sender: self) // Shall get propagated up to the split-vc
|
||||
detailDisplayedTunnel = tunnel
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
|
||||
guard !tableView.isEditing else {
|
||||
tableState = .multiSelect(selectionCount: tableView.indexPathsForSelectedRows?.count ?? 0)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView,
|
||||
@ -271,6 +339,18 @@ extension TunnelsListTableViewController: UITableViewDelegate {
|
||||
}
|
||||
return UISwipeActionsConfiguration(actions: [deleteAction])
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
|
||||
if tableState == .normal {
|
||||
tableState = .rowSwiped
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) {
|
||||
if tableState == .rowSwiped {
|
||||
tableState = .normal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelsListTableViewController: TunnelsManagerListDelegate {
|
||||
@ -287,8 +367,22 @@ extension TunnelsListTableViewController: TunnelsManagerListDelegate {
|
||||
tableView.moveRow(at: IndexPath(row: oldIndex, section: 0), to: IndexPath(row: newIndex, section: 0))
|
||||
}
|
||||
|
||||
func tunnelRemoved(at index: Int) {
|
||||
func tunnelRemoved(at index: Int, tunnel: TunnelContainer) {
|
||||
tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
|
||||
centeredAddButton.isHidden = tunnelsManager?.numberOfTunnels() ?? 0 > 0
|
||||
if detailDisplayedTunnel == tunnel, let splitViewController = splitViewController {
|
||||
if splitViewController.isCollapsed != false {
|
||||
(splitViewController.viewControllers[0] as? UINavigationController)?.popToRootViewController(animated: false)
|
||||
} else {
|
||||
let detailVC = UIViewController()
|
||||
detailVC.view.backgroundColor = .white
|
||||
let detailNC = UINavigationController(rootViewController: detailVC)
|
||||
splitViewController.showDetailViewController(detailNC, sender: self)
|
||||
}
|
||||
detailDisplayedTunnel = nil
|
||||
if let presentedNavController = self.presentedViewController as? UINavigationController, presentedNavController.viewControllers.first is TunnelEditTableViewController {
|
||||
self.presentedViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,11 @@
|
||||
<array>
|
||||
<string>packet-tunnel-provider</string>
|
||||
</array>
|
||||
<key>com.apple.developer.networking.wifi-info</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.$(APP_ID)</string>
|
||||
</array>
|
||||
<string>group.$(APP_ID_IOS)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
76
WireGuard/WireGuard/UI/macOS/AppDelegate.swift
Normal file
@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Cocoa
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
var tunnelsManager: TunnelsManager?
|
||||
var tunnelsTracker: TunnelsTracker?
|
||||
var statusItemController: StatusItemController?
|
||||
|
||||
var manageTunnelsRootVC: ManageTunnelsRootViewController?
|
||||
var manageTunnelsWindowObject: NSWindow?
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
Logger.configureGlobal(tagged: "APP", withFilePath: FileManager.logFileURL?.path)
|
||||
|
||||
TunnelsManager.create { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
if let error = result.error {
|
||||
ErrorPresenter.showErrorAlert(error: error, from: nil)
|
||||
return
|
||||
}
|
||||
|
||||
let tunnelsManager: TunnelsManager = result.value!
|
||||
|
||||
let statusMenu = StatusMenu(tunnelsManager: tunnelsManager)
|
||||
statusMenu.windowDelegate = self
|
||||
|
||||
let statusItemController = StatusItemController()
|
||||
statusItemController.statusItem.menu = statusMenu
|
||||
|
||||
let tunnelsTracker = TunnelsTracker(tunnelsManager: tunnelsManager)
|
||||
tunnelsTracker.statusMenu = statusMenu
|
||||
tunnelsTracker.statusItemController = statusItemController
|
||||
|
||||
self.tunnelsManager = tunnelsManager
|
||||
self.tunnelsTracker = tunnelsTracker
|
||||
self.statusItemController = statusItemController
|
||||
}
|
||||
}
|
||||
|
||||
@objc func quit() {
|
||||
guard let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating else {
|
||||
NSApp.terminate(nil)
|
||||
return
|
||||
}
|
||||
let alert = NSAlert()
|
||||
alert.messageText = tr("macAppExitingWithActiveTunnelMessage")
|
||||
alert.informativeText = tr("macAppExitingWithActiveTunnelInfo")
|
||||
if let window = manageTunnelsWindowObject {
|
||||
alert.beginSheetModal(for: window) { _ in
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
} else {
|
||||
alert.runModal()
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: StatusMenuWindowDelegate {
|
||||
func manageTunnelsWindow() -> NSWindow {
|
||||
if manageTunnelsWindowObject == nil {
|
||||
manageTunnelsRootVC = ManageTunnelsRootViewController(tunnelsManager: tunnelsManager!)
|
||||
let window = NSWindow(contentViewController: manageTunnelsRootVC!)
|
||||
window.title = tr("macWindowTitleManageTunnels")
|
||||
window.setContentSize(NSSize(width: 800, height: 480))
|
||||
window.setFrameAutosaveName(NSWindow.FrameAutosaveName("ManageTunnelsWindow")) // Auto-save window position and size
|
||||
manageTunnelsWindowObject = window
|
||||
tunnelsTracker?.manageTunnelsRootVC = manageTunnelsRootVC
|
||||
}
|
||||
return manageTunnelsWindowObject!
|
||||
}
|
||||
}
|
29
WireGuard/WireGuard/UI/macOS/AppStorePrivacyNotice.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
48
WireGuard/WireGuard/UI/macOS/Application.swift
Normal file
@ -0,0 +1,48 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Cocoa
|
||||
|
||||
class Application: NSApplication {
|
||||
|
||||
private let characterKeyCommands = [
|
||||
"x": #selector(NSText.cut(_:)),
|
||||
"c": #selector(NSText.copy(_:)),
|
||||
"v": #selector(NSText.paste(_:)),
|
||||
"z": #selector(UndoActionRespondable.undo(_:)),
|
||||
"a": #selector(NSResponder.selectAll(_:)),
|
||||
"Z": #selector(UndoActionRespondable.redo(_:)),
|
||||
"w": #selector(NSWindow.performClose(_:)),
|
||||
"m": #selector(NSWindow.performMiniaturize(_:)),
|
||||
"q": #selector(AppDelegate.quit)
|
||||
]
|
||||
|
||||
private var appDelegate: AppDelegate? //swiftlint:disable:this weak_delegate
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
appDelegate = AppDelegate() // Keep a strong reference to the app delegate
|
||||
delegate = appDelegate // Set delegate before app.run() gets called in NSApplicationMain()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func sendEvent(_ event: NSEvent) {
|
||||
let modifierFlags = event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue
|
||||
|
||||
if event.type == .keyDown,
|
||||
(modifierFlags == NSEvent.ModifierFlags.command.rawValue || modifierFlags == NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue),
|
||||
let selector = characterKeyCommands[event.charactersIgnoringModifiers ?? ""] {
|
||||
sendAction(selector, to: nil, from: self)
|
||||
} else {
|
||||
super.sendEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc protocol UndoActionRespondable {
|
||||
func undo(_ sender: AnyObject)
|
||||
func redo(_ sender: AnyObject)
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "WireGuardMacAppIcon16.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "WireGuardMacAppIcon32.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "WireGuardMacAppIcon32-1.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "WireGuardMacAppIcon64.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "WireGuardMacAppIcon128.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "WireGuardMacAppIcon256.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "WireGuardMacAppIcon256-1.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "WireGuardMacAppIcon512.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "WireGuardMacAppIcon512-1.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "WireGuardMacAppIcon.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 104 KiB |
After Width: | Height: | Size: 9.6 KiB |
After Width: | Height: | Size: 995 B |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 4.5 KiB |
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
26
WireGuard/WireGuard/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "StatusBarIcon@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "StatusBarIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "StatusBarIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
BIN
WireGuard/WireGuard/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon@1x.png
vendored
Normal file
After Width: | Height: | Size: 978 B |
BIN
WireGuard/WireGuard/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
WireGuard/WireGuard/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.9 KiB |
26
WireGuard/WireGuard/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "StatusBarIconDimmed@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "StatusBarIconDimmed@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "StatusBarIconDimmed@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
BIN
WireGuard/WireGuard/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/StatusBarIconDimmed@1x.png
vendored
Normal file
After Width: | Height: | Size: 881 B |
BIN
WireGuard/WireGuard/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/StatusBarIconDimmed@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
WireGuard/WireGuard/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/StatusBarIconDimmed@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.5 KiB |
26
WireGuard/WireGuard/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "StatusBarIconDot1@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "StatusBarIconDot1@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "StatusBarIconDot1@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
BIN
WireGuard/WireGuard/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/StatusBarIconDot1@1x.png
vendored
Normal file
After Width: | Height: | Size: 953 B |